npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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

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-player

Step 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-bar

npm 7+ installs missing peer deps automatically. Already have these? Skip this step.

Step 3 — Platform setup

iOS:

cd ios && pod install

Android — 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 install after 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.plist supported orientations

👨‍💻 Developer


📄 License

MIT © Zezo Soft