One native player. Many surfaces. Zero interruptions.
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 handoff —
setSourcewith the sameidis a no-op for the engine; the player just re-parents to the new surface - 📱 Single-engine feed —
VideoFeed, a TikTok/Reels-style list where only the scrolled-into-focus video plays, on one player, flat memory ▶️ YouTube built in —type: '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.
- Installation
- Quick start
- Surfaces (the core idea)
- Video feed
- Pausing on focus loss
- State & events
- Platform setup
- Example app
- Roadmap
- Contributing
npm install react-native-video-provider react-native-svg
cd ios && pod installreact-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+.
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-picture5. 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'); // releaseOn 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).
<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" />}
/><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, …).
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).
// 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),
});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 itEnable Audio, AirPlay and Picture in Picture in Signing & Capabilities →
Background Modes (adds audio to UIBackgroundModes in Info.plist).
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.)
yarn
yarn example android # or: yarn example iosThe example shows a feed → detail handoff with a live status panel, plus fullscreen, floating and PiP buttons.
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)
See CONTRIBUTING.md and the architecture doc. PRs that add a second player instance will be rejected on principle. 🙂
MIT
