Skip to content

Latest commit

 

History

74 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

react-native-video-provider

npm version license platform new architecture

React Native Video Provider

One native player. Many surfaces. Zero interruptions.

Documentation

A singleton-engine video library for React Native (Android + iOS, New Architecture). The app owns exactly one native playback engine (ExoPlayer / AVPlayer) for its whole lifetime; React components are just rendering surfaces the engine attaches to. Moving from a feed cell to a detail screen to fullscreen to a floating window never reloads, rebuffers, or resets position — exactly how YouTube, Netflix and Twitter/X players behave.

Feed ──▶ Detail ──▶ Fullscreen (rotation unlocked) ──▶ Floating window
              same engine · same buffer · same position
  • 🎯 Singleton engine — created silently once by the provider; survives navigation, tab switches, unmounts, Fast Refresh
  • 🔁 Same-video handoffsetSource with the same id is a no-op for the engine; the player just re-parents to the new surface
  • 📱 Single-engine feedVideoFeed, a TikTok/Reels-style list where only the scrolled-into-focus video plays, on one player, flat memory
  • ▶️ YouTube built intype: 'youtube'; a second native, re-parentable WebView engine with the same controls, fullscreen and handoff (no extra dep)
  • 📺 Fullscreen — built-in host that locks landscape (no accidental sensor rotation) and restores the previous orientation on exit
  • 🧭 Orientation control — force portrait/landscape (+ inverted) per player, scoped to fullscreen, or as a standing lock
  • ⏸️ Focus-aware — auto-pause on app background and on screen navigation (React Navigation's useIsFocused())
  • 🎈 Floating player — built-in draggable in-app window
  • 🖼 Picture in Picture — Android + iOS
  • New Architecture native — TurboModule + Fabric, typed end to end
  • 🧠 Zustand-powered state — selector subscriptions, no re-render storms

See docs/ARCHITECTURE.md for the full design and docs/API.md for the complete API reference.

Contents

Installation

npm install react-native-video-provider react-native-svg
cd ios && pod install

react-native-svg is a peer dependency (used by the built-in control icons). YouTube (type: 'youtube') needs no extra dependency — it runs on a native WebView (Android WebView / iOS WKWebView) built into the library.

Requires the New Architecture. Works on React Native 0.79+ — the TurboModule spec uses direct codegen-type imports so it parses on 0.79's codegen as well as 0.80+.

Quick start

1. Wrap the app once. The provider silently creates the engine and mounts the fullscreen/floating hosts above your app:

import { VideoProvider } from 'react-native-video-provider';

export default function App() {
  return (
    <VideoProvider>
      <Navigation />
    </VideoProvider>
  );
}

2. Play a video anywhere:

import { VideoPlayer } from 'react-native-video-provider';

<VideoPlayer
  source={{ id: '123', uri: 'https://example.com/video.m3u8', title: 'Big Buck Bunny' }}
  style={{ aspectRatio: 16 / 9 }}
/>

Play a YouTube video — same component, same state/events/commands. Just set type: 'youtube' and put the YouTube video id in uri:

<VideoPlayer source={{ id: 'y1', uri: 'dQw4w9WgXcQ', type: 'youtube' }} style={{ aspectRatio: 16 / 9 }} />

YouTube runs on a native, re-parentable WebView that is a second engine inside the singleton core — moved between surfaces exactly like the native player view. So YouTube gets the same built-in VideoControls, the same fullscreen host, and seamless inline→fullscreen handoff with no reload, just like native video. It loads the embed with a youtube.com referrer (which is what plays referrer-restricted videos) and hides YouTube's own UI; state and commands bridge through the IFrame API to the standard usePlayback / useVideoEvents / play()/pause()/seek(). type: 'url' (default) and type: 'youtube' are fully interchangeable.

3. Open a detail screen with the same video — because the id matches, the engine is untouched and playback continues from the exact frame:

// DetailScreen.tsx — same source id ⇒ handoff, not reload
<VideoPlayer source={{ id: '123', uri }} style={{ aspectRatio: 16 / 9 }} />

4. Fullscreen / floating / PiP from anywhere:

const player = useVideo();

player.enterFullscreen(); // locks landscape (no sensor rotation)
player.showFloating();    // draggable in-app window
player.enterPiP();        // system picture-in-picture

5. Force an orientation — values: 'auto' | 'portrait' | 'inverted-portrait' | 'landscape' | 'inverted-landscape'.

Scoped to fullscreen (applied when fullscreen opens, restored when it closes — the rest of the app is unaffected):

// This player's fullscreen (incl. the built-in controls' button) locks to
// portrait — e.g. a vertical video:
<VideoPlayer source={video} fullscreenOrientation="portrait" />

// Or per call:
const { enter, toggle } = useFullscreen();
enter('landscape');

Fullscreen locks orientation by default — it doesn't follow the device sensor. Tapping the fullscreen button rotates to landscape and it stays put however you hold the phone; tapping exit returns to portrait.

To let fullscreen rotate freely with the device instead — landscape-left, landscape-right and portrait, with no exit/re-enter — set rotation:

<VideoPlayer source={video} rotation />

That's shorthand for fullscreenOrientation="auto"; pass that directly if you need a specific lock, and it wins over rotation. On iOS the app must allow landscape in its Info.plist and forward orientation from the AppDelegate (see Platform setup), or the OS won't permit rotation at all.

Full orientation reference — sensor-driven fullscreen entry, standing locks, and how the two rotation props compose

To also stop the inline video from sensor-rotating with the rest of the app, set lockPortrait on the provider — the app stays portrait and only fullscreen rotates:

<VideoProvider config={{ lockPortrait: true }}>

The two rotation props compose — one gets you into fullscreen, the other governs what happens once there:

Prop Effect
componentRotation Turning the device to landscape enters fullscreen; turning back exits. No button press.
rotation Once fullscreen, keep following the sensor instead of locking.
// Fully sensor-driven, YouTube style:
<VideoPlayer source={video} componentRotation rotation />

Both are off by default and both need the app to allow landscape at the OS level. autoFullscreenOnRotate is the older name for componentRotation; either works.

Or as a standing lock, independent of fullscreen:

// While this player is mounted (released on unmount):
<VideoPlayer source={video} orientation="landscape" />

// Imperative:
player.setOrientation('inverted-landscape');
player.setOrientation('auto'); // release

On iOS this needs the AppDelegate forwarding shown in Platform setup. Inverted portrait is ignored by iPhones without a home button (the OS doesn't allow it).

Surfaces (the core idea)

<VideoSurface> never creates a player — it registers a mount point. The engine renders into at most one surface at a time; attach(id) re-parents the native player view with no playback interruption. If you attach to a surface that hasn't mounted yet (navigation in flight), the engine attaches the moment it appears.

<VideoSurface surfaceId="feed" style={{ aspectRatio: 16 / 9 }} />

const player = useVideo();
player.setSource(video);   // load (or hand off)
player.attach('feed');     // render here

<VideoPlayer> is the convenience wrapper that does setSource + attach + optional controls in one component. Handy props:

<VideoPlayer
  source={video}
  autoplay muted repeat          // playback flags
  resizeMode="contain"           // contain | cover | stretch
  controls                       // built-in chrome (SVG icons)
  onLoadComplete={(m) => {}}     // duration/dimensions ready
  onBuffering={(b) => {}}
  onError={(e) => {}}
  ref={playerRef}                // → the VideoManager (playerRef.current.seek(…))
/>

Pass live for a live stream: the controls hide the seek bar/times and the center play/pause (only a loader shows), and persistent leftTopIcon / rightTopIcon badges sit in the top corners. A live feed that errors or drops auto-retries with backoff, pausing while offline and resuming on reconnect (install @react-native-community/netinfo for connectivity; opt out with <VideoProvider config={{ liveAutoRetry: false }}>). thumbnail shows a poster over the video during the initial load:

<VideoPlayer
  source={liveSource}
  live
  leftTopIcon={() => <LottieView source={liveAnim} autoPlay loop style={{ width: 44, height: 20 }} />}
  rightTopIcon={() => <ViewerCountBadge />}
  thumbnail={() => <Image source={{ uri: poster }} style={StyleSheet.absoluteFill} resizeMode="cover" />}
/>

Video feed (single engine, only the focused one plays)

<VideoFeed> is a TikTok/Reels-style vertical feed. It renders many videos in a FlatList but plays only the one scrolled into focus — on the same single engine, so memory and CPU stay flat no matter how long the feed. Each item is just a surface; scrolling hands the one player off to the focused item.

import { VideoFeed } from 'react-native-video-provider';

<VideoFeed
  data={videos} // [{ id, uri, title? }, …] — each needs a stable id
  renderOverlay={({ item, focused }) => (
    <Caption title={item.title} paused={!focused} />
  )}
/>

Any extra FlatList prop passes through (onEndReached for infinite scroll, ListHeaderComponent, …).

Pausing on focus loss

Both <VideoPlayer> and <VideoFeed> pause automatically when the app is backgrounded (opt out with pauseOnFocusLost={false} for background audio).

For screen navigation (navigating to another screen while the app stays foregrounded), pass your navigation library's focus flag — React Navigation keeps screens mounted, so there is no other reliable signal:

import { useIsFocused } from '@react-navigation/native';

function Screen() {
  const isFocused = useIsFocused();
  return <VideoPlayer source={video} isFocused={isFocused} />;
  // VideoFeed takes the same prop.
}

false pauses; returning to true resumes (reclaiming the engine if another video took it while you were away).

State & events

// Selector subscriptions — re-render only for what you display
const position = usePlayback((s) => s.position);
const { isFullscreen, toggle } = useFullscreen();

useVideoEvents({
  onEnd: () => playNext(),
  onError: (e) => console.warn(e.code, e.message),
});

Platform setup

iOS — fullscreen rotation

iOS asks the AppDelegate which orientations are allowed. Forward that to the library so fullscreen can rotate to landscape and setOrientation / the orientation prop can force a rotation (skip this only if you don't use those features and your app already supports all orientations):

// AppDelegate.swift
import Video

func application(_ application: UIApplication,
                 supportedInterfaceOrientationsFor window: UIWindow?)
    -> UIInterfaceOrientationMask {
  // Pass your app's own restriction as the default — the library only
  // overrides it while it holds a lock (fullscreen / setOrientation).
  return VideoOrientation.mask(withDefault: .portrait)
}
If your app swizzles RN modal orientation

Some apps override RCTModalHostViewController.supportedInterfaceOrientations to force an app-wide orientation. That swizzle also applies to this library's fullscreen host (an RN Modal) and will pin it — landscape fullscreen then silently fails on iOS while working on Android. Route the swizzle through the library too:

extension UIViewController {
  @objc func rct_supportedInterfaceOrientations() -> UIInterfaceOrientationMask {
    return VideoOrientation.mask(withDefault: .portrait)
  }
}

Orientation is app-wide, not per-player. VideoOrientation is the single authority, so use it for other players/screens too rather than patching them or hand-rolling a second mechanism:

const player = useVideo();
player.setOrientation('landscape'); // entering some other fullscreen
player.setOrientation('auto');      // release it

iOS — Picture in Picture / background audio

Enable Audio, AirPlay and Picture in Picture in Signing & Capabilities → Background Modes (adds audio to UIBackgroundModes in Info.plist).

Android — Picture in Picture

Declare PiP support on your main activity in AndroidManifest.xml:

<activity
  android:name=".MainActivity"
  android:supportsPictureInPicture="true"
  android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize|uiMode|density" />

(The orientation|screenSize entries are part of the default RN template and also keep fullscreen rotation from recreating the activity.)

Example app

yarn
yarn example android   # or: yarn example ios

The example shows a feed → detail handoff with a live status panel, plus fullscreen, floating and PiP buttons.

Roadmap

Not yet built — expand for the full list
  • Queue (next/previous/playlist/autoplay)
  • Background playback (Android MediaSessionService + notification, iOS remote commands / lock-screen controls)
  • Quality, subtitle and audio-track selection
  • Brightness/volume swipe gestures, pinch zoom
  • True ahead-of-time preloading (Media3 PreloadManager)

Contributing

See CONTRIBUTING.md and the architecture doc. PRs that add a second player instance will be rejected on principle. 🙂

License

MIT