Shipnative
ShipnativeShipnative
Sign in
GuideAugust 2026 · 9 min read

React Native Video Player: expo-video vs react-native-video

Video is the feature that looks like one afternoon and turns into three days. The tutorial you found uses expo-av, which is deprecated. The YouTube link you pasted does not play and never will. It works perfectly in the simulator and is silent on a real iPhone. This guide covers which package to install in 2026, working code for both real options, a scrolling feed that does not eat memory, and the four bugs that account for most video support tickets.

What to install in 2026

PackageStatusExpo GoBest forShape
expo-videoCurrent, maintained✅ Basic playbackAlmost every Expo appPlayer object + VideoView
react-native-videoCurrent, community❌ Dev buildBare RN, exotic DRM/adsSingle <Video> component
expo-av (Video)⚠️ Deprecated✅ YesNothing new — migrate offReplaced by expo-video
react-native-youtube-iframeCurrent✅ YesYouTube content onlyA WebView, not a native player

The important row is the third one. A large share of the React Native video tutorials still ranking today import { Video } from expo-av. That code still runs, and it is a migration you will be forced into later — so if you are starting now, start on expo-video.

expo-video: player and view are separate

npx expo install expo-video

The mental model differs from expo-av: a player owns the source and playback state, and a view renders it. One player can move between views, which is what makes fullscreen transitions and picture-in-picture work without reloading the stream.

import { useVideoPlayer, VideoView } from 'expo-video';
import { useEvent } from 'expo';
import { Pressable, StyleSheet, Text, View } from 'react-native';

const SOURCE = 'https://cdn.example.com/clips/intro.m3u8';

export default function ClipPlayer() {
  const player = useVideoPlayer(SOURCE, (p) => {
    p.loop = true;
    p.muted = true;      // required for autoplay that users tolerate
    p.play();
  });

  const { isPlaying } = useEvent(player, 'playingChange', {
    isPlaying: player.playing,
  });

  return (
    <View>
      <VideoView
        player={player}
        style={styles.video}
        contentFit="cover"
        allowsFullscreen
        allowsPictureInPicture
        nativeControls={false}   // draw your own overlay
      />
      <Pressable
        style={styles.overlay}
        onPress={() => (isPlaying ? player.pause() : player.play())}
      >
        <Text style={styles.icon}>{isPlaying ? '❚❚' : '▶'}</Text>
      </Pressable>
    </View>
  );
}

const styles = StyleSheet.create({
  video: { width: '100%', aspectRatio: 16 / 9, backgroundColor: '#000' },
  overlay: { ...StyleSheet.absoluteFillObject, alignItems: 'center', justifyContent: 'center' },
  icon: { color: '#fff', fontSize: 34 },
});

Note aspectRatio rather than a fixed height. Video without a reserved aspect ratio causes the layout to jump when the first frame arrives, and on a feed that jump compounds into scroll position drift.

Background playback and picture-in-picture change the native project, so they are enabled through the config plugin and then need a rebuild — they cannot be switched on from JavaScript alone:

// app.json
{
  "expo": {
    "plugins": [
      ["expo-video", {
        "supportsBackgroundPlayback": true,
        "supportsPictureInPicture": true
      }]
    ]
  }
}

react-native-video: one component

The community package keeps everything on a single component, which some teams prefer and which maps more directly onto older tutorials:

import Video from 'react-native-video';

<Video
  source={{ uri: SOURCE }}
  style={{ width: '100%', aspectRatio: 16 / 9 }}
  resizeMode="cover"
  controls
  paused={!isActive}
  repeat
  onError={(e) => console.warn('playback failed', e)}
  onBuffer={({ isBuffering }) => setBuffering(isBuffering)}
/>

It requires a development build in Expo — npx expo run:ios — and the Expo config plugin if you want its notification controls or background modes. Choose it deliberately, for a capability you have confirmed you need; picking it by default means giving up the version alignment that expo install gives you for free.

The feed pattern: one player, many rows

Every short-video app starts with a FlatListwhere each row mounts its own player. It looks fine with five items and falls over at fifty — memory climbs, frames drop, and on Android you eventually hit the device’s decoder limit and rows render black. The fix is to let the list decide who is visible and play only that one:

const [activeId, setActiveId] = useState(null);

// must be stable — a new object here throws
// "Changing onViewableItemsChanged on the fly is not supported"
const viewabilityConfig = useRef({ itemVisiblePercentThreshold: 70 }).current;
const onViewableItemsChanged = useRef(({ viewableItems }) => {
  setActiveId(viewableItems[0]?.item?.id ?? null);
}).current;

<FlatList
  data={clips}
  keyExtractor={(c) => c.id}
  pagingEnabled
  windowSize={3}                 // keep the mounted range small
  removeClippedSubviews
  viewabilityConfig={viewabilityConfig}
  onViewableItemsChanged={onViewableItemsChanged}
  renderItem={({ item }) => (
    <ClipRow clip={item} isActive={item.id === activeId} />
  )}
/>

Inside ClipRow, react to isActive by calling player.play() or player.pause(), and show the poster image until playback actually starts. Two details make it feel professional: preload the next clip only, and always render a thumbnail underneath, because the first frame of a stream is never instant on cellular.

The list mechanics here are the same ones covered in the FlatList guide — video simply punishes you faster for getting them wrong.

Four bugs that will reach your inbox

  1. Silent on iPhone, fine everywhere else. iOS uses an ambient audio session by default, which obeys the physical silent switch. Set a playback audio session once at app start so sound plays regardless. Nobody catches this in the simulator, and roughly half of your iOS users keep their phone on silent.
  2. Shipping MP4s where you need HLS. A single large MP4 must download before it can seek, so a two-minute clip on a poor connection looks broken. HLS (.m3u8) streams in segments and adapts bitrate. Both players handle it natively — this is a question for whatever service encodes your uploads.
  3. No poster, no loading state. Between mount and first frame you show a black rectangle, which users read as a crash. Render the thumbnail underneath the player and fade it out on the first playing event.
  4. Players that outlive their screen. Navigating away without releasing playback leaves audio running under the next screen. Pause on blur — with Expo Router, useFocusEffect — and let the player be cleaned up with the component.

Decide the shape before you write the player

Almost all of the difficulty above comes from one decision made too late: whether video is a detail in your app — a how-it-works clip on an onboarding screen — or the product, as in a feed. The first is thirty lines and one package. The second is a hosting bill, an encoding pipeline, a moderation policy, and the list work above.

If you are not sure which one you are building, it is cheaper to look at it than to argue about it. Describe the screen — “a vertical clip feed with autoplay on the visible item and a tap-to-mute control” — and ShipNative generates it as real React Native you can scroll on your own phone, then export and take further.

Frequently Asked Questions

Is expo-av deprecated? What replaced it?

Yes. The Video component in expo-av has been superseded by the standalone expo-video package, and expo-av is on a deprecation path. New projects should install expo-video directly. The API is different rather than a drop-in swap: expo-video separates a player object, created with useVideoPlayer, from the VideoView that displays it.

expo-video or react-native-video — which should I use?

Use expo-video in any Expo project; it is maintained by the Expo team, installs with expo install, and covers the common cases including HLS streaming, picture-in-picture, and background playback. Reach for react-native-video when you need something it exposes that expo-video does not for your version — certain DRM setups, ad integrations, or fine-grained track selection — and accept a development build either way.

Why will not my YouTube link play in React Native?

A youtube.com/watch URL is a web page, not a video stream, so no native player can open it — and extracting the underlying stream violates YouTube’s terms of service. Embed the official player instead with react-native-youtube-iframe, or host your own file and point the player at that.

How do I autoplay video in a scrolling feed without killing performance?

Render one player, not one per row. Track which row is visible using FlatList onViewableItemsChanged with a viewabilityConfig around 60 to 80 percent, play only that item, and pause everything else. Mounting a player per cell is what makes a feed drop frames and spike memory after twenty or so items.

Why is my video silent when the iPhone ring switch is on silent?

iOS routes app audio through the ambient audio session by default, which respects the silent switch. You have to opt into a playback session explicitly at startup so sound plays regardless of the hardware switch. This is the single most common bug report on video features and it never reproduces on Android or the simulator.

Does video playback work in Expo Go?

expo-video is included in the Expo Go runtime, so basic playback works there. Anything that needs a config plugin — background playback, picture-in-picture entitlements — requires a development build, because those change the native project. react-native-video always requires a development build.

→

React Native Image Upload

Recording video usually means uploading it — same pipeline, bigger files.

Read guide →
→

React Native Performance: 10 Fixes

A video feed is the harshest test of list performance you can ship.

Read guide →

Ship a real React Native app today

Describe, preview, and export Expo code — free to start.

Build with ShipNative →
ShipNative logoShipnative

Build mobile apps with AI. Describe, preview, and ship to iOS & Android in minutes.

Features

Text to App AIApp Generator from ScreenshotPRD to Mobile App

Tools

All free toolsApp Cost CalculatorApp Name GeneratorApp Store Keyword ToolReact Native Components

Blog

All blog postsHow to Build an App Without CodingBest AI Tools for Real Mobile AppsExpo EAS App Store ChecklistLovable, Cursor & v0 for MobileBest AI App Builders in 2026React Native AI App Builder Guide

Legal

FAQTerms of ServicePrivacy Policy

© 2026 ShipNative. All rights reserved.