@zezosoft/zezo-ott-react-native-video-player
v1.3.7
Published
Production-ready React Native OTT video player library for Android & iOS. Features: playlists, seasons, auto-next playback, subtitles (SRT/VTT), custom theming, analytics tracking, fullscreen mode, gesture controls, ads player (pre-roll/mid-roll/post-roll
Maintainers
Keywords
Readme
Table of Contents
✨ Features
| | Feature | Description |
| --- | ------------------- | -------------------------------------------------- |
| 🎮 | Modern Controls | Play/Pause, seek, fast-forward/rewind, fullscreen |
| 📋 | Playlist & Episodes | Multi-season drawer, auto-play next |
| 🔊 | Audio & Subtitles | Runtime switching — embedded & WebVTT/SRT sidecars |
| 🔒 | Control Lock | Prevent accidental seeks during playback |
| ⚡ | Playback Speed | 0.5× → 2.0× speed selector |
| 💳 | Paywall Guard | Block premium content via onEpisodePress |
| 📺 | VAST Ad Engine | Pre/mid/post-roll XML ads with skip support |
| ⏭️ | Skip Intro / Next | Dynamic overlays at configurable timestamps |
| 👆 | Gesture System | Double-tap left/right to seek |
| 🔍 | Pinch-to-Zoom | Pinch gesture to toggle between Original and Zoomed to Fill resize modes |
📦 Architecture & Data Flow
OTT Backend / API ──────────┐
Play Payload / History ─────┤
▼
buildPlaylist
/ \
▼ ▼
useVideoPlayerStore usePlayerStore
(Tracks & Seasons) (Play State)
\ /
▼ ▼
VideoPlaylistPlayer
│
▼
VideoPlayer
/ | \
▼ ▼ ▼
RNVideo VAST Ad Gestures
(native) Engine (double tap)🚀 Installation & Setup
Ships compiled JS (
lib/) to npm. Native modules are peer dependencies — your app uses a single copy.
Step 1 — Install the library
npm install @zezosoft/zezo-ott-react-native-video-player
# or
yarn add @zezosoft/zezo-ott-react-native-video-playerStep 2 — Install peer dependencies
npm install \
react-native-video \
react-native-gesture-handler \
react-native-reanimated \
react-native-worklets \
react-native-linear-gradient \
react-native-orientation-director \
react-native-safe-area-context \
react-native-svg \
react-native-subtitles \
react-native-system-navigation-barnpm 7+ installs missing peer deps automatically. Already have these? Skip this step.
Step 3 — Platform setup
iOS:
cd ios && pod installAndroid — wrap root with GestureHandlerRootView:
import { GestureHandlerRootView } from 'react-native-gesture-handler';
export default function App() {
return (
<GestureHandlerRootView style={{ flex: 1 }}>
<AppNavigation />
</GestureHandlerRootView>
);
}Step 4 — Rebuild
# iOS
cd ios && pod install && cd ..
# Android — clean rebuild recommended⚡ Quick Start
1. Single Video Player
import { VideoPlayer } from '@zezosoft/zezo-ott-react-native-video-player';
export default function MoviePlayerScreen() {
return (
<VideoPlayer
source={{
uri: 'https://example.com/video.m3u8',
tracks: {
subtitles: [
{
type: 'text/vtt',
label: 'English',
uri: 'https://example.com/en.vtt',
language: 'en',
},
],
},
}}
title="Tears of Steel"
subtitle="Sci-Fi Short Film"
autoPlay
lockLandscape
onBack={() => navigation.goBack()}
/>
);
}2. Series & Playlist Player
Step A — Build the playlist store:
import { buildPlaylist } from '@zezosoft/zezo-ott-react-native-video-player';
const { playlist, activeTrack } = buildPlaylist(contentData, {
current_episode: 'episode_1_id',
current_season: 'season_1_id',
is_trailer: false,
});Step B — Render the player:
import { VideoPlaylistPlayer } from '@zezosoft/zezo-ott-react-native-video-player';
export default function SeriesPlayerScreen() {
return (
<VideoPlaylistPlayer
theme={{ primary: '#E50914', surface: '#181818' }}
adConfig={{ preRoll: true, midRolls: 2, postRoll: true }}
autoNext
onEpisodePress={(episode) => {
if (episode.content_offering_type === 'PREMIUM') {
Alert.alert('Subscribe to watch');
return false; // blocks playback
}
return true;
}}
onBack={() => navigation.goBack()}
/>
);
}import React, { useCallback } from 'react';
import { View, StyleSheet, Alert } from 'react-native';
import {
VideoPlaylistPlayer,
EpisodeItem,
} from '@zezosoft/zezo-ott-react-native-video-player';
export default function SeriesPlayerScreen() {
const handleEpisodePress = useCallback((episode: EpisodeItem) => {
if (episode.content_offering_type === 'PREMIUM') {
Alert.alert(
'Subscription Required',
'Subscribe to watch premium episodes.'
);
return false;
}
return true;
}, []);
return (
<View style={styles.container}>
<VideoPlaylistPlayer
theme={{ primary: '#E50914', surface: '#181818' }}
adTagUrl="https://pubads.g.doubleclick.net/gampad/ads?..."
adConfig={{ preRoll: true, midRolls: 2, postRoll: true }}
autoNext={true}
onEpisodePress={handleEpisodePress}
onBack={() => Alert.alert('Exit Video Player')}
onProgress={(prog) =>
console.log(
`Track: ${prog.activeTrack?.episodeName}, Time: ${prog.currentTime}`
)
}
/>
</View>
);
}
const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: '#000' },
});📋 API Reference
VideoPlayer
Stand-alone player for a single video. All props are optional unless marked required.
🎬 Source & Media
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| source | ReactVideoSource & { adTagUrl?: string; tracks?: MediaTracks; fallbackUrl?: string; } | required | Stream URI, sidecar subtitles, per-track VAST ad tag, and optional fallback URL if the primary fails |
| title | string | — | Primary title shown in the top bar |
| subtitle | string | — | Secondary line (e.g. episode info) shown below the title |
| thumbnailUri | string | — | Image shown while the stream is loading or paused |
| seekTime | number | — | Resume position in seconds; ignored when isTrailer is true |
| isTrailer | boolean | false | When true, skips all VAST ads and appends "- Trailer" to subtitle |
| isLive | boolean | false | Switches the seekbar to a live-stream indicator UI |
▶️ Playback
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| autoPlay | boolean | true | Start playback immediately on load |
| muted | boolean | false | Begin with audio muted |
| autoNext | boolean | true | Automatically advance to next episode when the current one ends |
| showControls | boolean | true | Toggle the entire control overlay (gestures still active) |
| lockLandscape | boolean | true | Lock device orientation to landscape while the player is focused |
| isFocused | boolean | — | Pass your screen focus state to pause/resume automatically on tab/nav changes |
⏭️ Skip Intro & Next Episode
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| skipIntro | { start: number; end: number } \| null | null | Shows Skip Intro pill between start and end seconds |
| nextAt | number \| null | null | Second at which the Next Episode popup appears |
| onSkipIntro | () => void | — | Called when the user taps Skip Intro |
| onNext | () => void | — | Called when the user taps Next Episode or autoNext fires |
📺 VAST Ads
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| adConfig | { preRoll?: boolean; midRolls?: number; postRoll?: boolean } | — | Enable pre/mid/post-roll ad slots |
| defaultAdSkipOffset | number | 5 | Seconds of ad play before the Skip button appears |
| doubleTapSkipOffset | number | 10 | Seconds seeked per double-tap gesture |
| onAdStart | (url: string) => void | — | Fired when an ad begins playing |
| onAdEnd | (url: string) => void | — | Fired when an ad finishes |
| onAdSkipped | (url: string) => void | — | Fired when the user skips an ad |
| onAdClick | (clickThroughUrl?: string) => void | — | Fired when the user taps the ad |
📡 Callbacks
| Prop | Type | Description |
|------|------|-------------|
| onBack | () => void | Tapping the back arrow |
| onProgress | (p: { currentTime: number; playableDuration: number; seekableDuration: number }) => void | Fires every ~250 ms during playback |
| onEpisodePress | (ep: EpisodeItem) => boolean \| void | Episode drawer tap — return false to block the switch |
🎨 Customisation
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| theme | Partial<VideoPlayerTheme> | built-in | Override player colors — see Theming |
| seasons | Season[] | — | Season/episode list to populate the drawer |
| activeEpisodeId | string | — | ID of the currently playing episode (highlighted in drawer) |
| controlsStyles | ControlsStyles | { hideFullscreen: false } | Granular style overrides from react-native-video |
VideoPlaylistPlayer
Drops in on top of useVideoPlayerStore. The following props are managed automatically by the store and must not be passed directly:
source·title·subtitle·skipIntro·nextAt·onNext·thumbnailUri·seasons
All remaining VideoPlayer props are supported, plus:
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| adTagUrl | string | — | Fallback VAST tag URL used for every track in the playlist |
| autoNext | boolean | true | Advance to next track automatically when one ends |
| stream | (track: MediaTrack) => Promise<PlaybackSource> \| PlaybackSource | — | Dynamic stream resolver function called before loading a track (e.g. to fetch authenticated URL & custom headers) |
| onProgress | (p: { currentTime: number; playableDuration: number; seekableDuration: number; activeTrack: MediaTrack \| null }) => void | — | Same as VideoPlayer.onProgress but includes the current MediaTrack |
EpisodeItem
| Property | Type | Required | Description |
|----------|------|:--------:|-------------|
| id | string | ✅ | Unique identifier |
| title | string | ✅ | Display title |
| episodeNumber | number | ✅ | Episode index within its season |
| videoUrl | string | ✅ | Stream URL (.m3u8 or .mp4) |
| content_offering_type | 'FREE' \| 'PREMIUM' \| 'BUY_OR_RENT' \| 'TVOD' | ✅ | Entitlement tier — use in onEpisodePress to gate premium content |
| duration | string | — | Human-readable length, e.g. "45m" |
| thumbnail | string | — | Episode thumbnail URI |
| description | string | — | Short synopsis shown in the drawer |
| releaseDate | Date | — | Publication date |
| skipIntro | { start: number \| null; end: number \| null } \| null | — | Intro range passed to skipIntro prop |
| nextAt | number \| null | — | Second at which the Next Episode popup fires |
Season
| Property | Type | Required | Description |
|----------|------|:--------:|-------------|
| id | string | ✅ | Unique season identifier |
| title | string | ✅ | Label shown in the drawer header, e.g. "Season 1" |
| episodes | EpisodeItem[] | ✅ | Ordered episode list |
PlaybackSource
| Property | Type | Required | Description |
|----------|------|:--------:|-------------|
| uri | string | ✅ | Stream URL to be loaded by the player |
| headers | Record<string, string> | — | HTTP headers (e.g. for custom Authorization tokens, Cookies) |
MediaTrack
Represents a track in the playlist store useVideoPlayerStore.
| Property | Type | Required | Description |
|----------|------|:--------:|-------------|
| id | string | ✅ | Unique identifier |
| contentId | string | ✅ | Parent content/show identifier |
| type | string | ✅ | Content type, e.g. 'movie', 'series', or 'live_stream' |
| source | MediaSource | ✅ | Underlying source settings (link, type, keyType, isTrailer) |
| title | string | ✅ | Track title |
| offeringType | 'FREE' \| 'PREMIUM' \| 'BUY_OR_RENT' \| 'TVOD' | ✅ | Tier protection settings |
| description | string | — | Short description/synopsis |
| thumbnail | string | — | Thumbnail image URI |
| duration | number | — | Duration in seconds |
| episode | EpisodeInfo | — | Season and episode details for series tracks |
| subtitles | Subtitle[] | — | List of sidecar/runtime subtitles |
| skipIntro | SkipIntro | — | Skip intro timestamps |
| nextAt | number | — | Timestamp for triggering the Next Episode prompt |
🎮 Zustand Global Stores
useVideoPlayerStore
import { useVideoPlayerStore } from '@zezosoft/zezo-ott-react-native-video-player';
const activeTrack = useVideoPlayerStore((s) => s.activeTrack);
const playlist = useVideoPlayerStore((s) => s.playlist);
useVideoPlayerStore.getState().setActiveTrack(newTrack);
useVideoPlayerStore.getState().nextTrack();
useVideoPlayerStore.getState().reset();usePlayerStore
Manages runtime player playback state, gesture states, and active bottom sheets.
import { usePlayerStore } from '@zezosoft/zezo-ott-react-native-video-player';
// Read states
const paused = usePlayerStore((s) => s.paused);
const resizeMode = usePlayerStore((s) => s.resizeMode); // 'contain' | 'cover'
const isLocked = usePlayerStore((s) => s.isLocked);
const buffering = usePlayerStore((s) => s.buffering);
// Set states
usePlayerStore.getState().setPaused(true);
usePlayerStore.getState().setSpeed(1.5);
usePlayerStore.getState().setMuted(true);
usePlayerStore.getState().setResizeMode('cover'); // switch zoom mode
usePlayerStore.getState().setIsLocked(true); // lock controls UI
usePlayerStore.getState().setActiveSheet('audio_subtitle'); // open sheet: 'audio_subtitle' | 'quality' | 'episodes' | 'speed' | null🎨 Theming
Pass a Partial<VideoPlayerTheme> to the theme prop of either player component. Only override the keys you need.
| Key | Default | Where it applies |
|-----|---------|------------------|
| primary | #FF3B30 | Seek-bar fill, active state highlights |
| onPrimary | #FFFFFF | Icons/text rendered on primary surfaces |
| background | rgba(10,10,13,1) | Player root background |
| surface | rgba(20,20,24,0.82) | Bottom sheets, episode drawer, modals |
| text | #FFFFFF | All primary labels and titles |
| textSecondary | #B0B5C3 | Episode metadata, subtitle captions |
| track | rgba(255,255,255,0.20) | Unfilled portion of the seek bar |
| buffer | rgba(255,255,255,0.38) | Buffered segment on the seek bar |
| backdrop | rgba(0,0,0,0.72) | Dimmed overlay behind modals |
| divider | rgba(255,255,255,0.10) | List separators |
| pillBackground | rgba(26,26,30,0.88) | Skip Intro / Next Episode pill |
| white | #FFFFFF | Pure white utility color |
| black | rgba(0,0,0,1) | Pure black utility color |
<VideoPlaylistPlayer
theme={{
primary: '#00D2C4',
surface: '#0F121F',
background: '#07080F',
pillBackground: 'rgba(0, 210, 196, 0.2)',
}}
/>🛠️ Troubleshooting
"Tried to register two views" — duplicate native module
- Remove
node_modules+ lockfile and reinstall - Keep all native packages only in your app root
package.json - Run
cd ios && pod installafter reinstalling
Gestures not triggering seek
- Ensure root is wrapped with
<GestureHandlerRootView style={{ flex: 1 }}>
Audio/subtitle tracks missing
- Confirm HLS manifest includes multiple audio tracks
- Subtitle URIs must be absolute and CORS-accessible
Screen not locking to landscape
- Add Landscape Left, Landscape Right, and Portrait to
Info.plistsupported orientations
👨💻 Developer
📄 License
MIT © Zezo Soft
