<h1 align="center">🎬 zezo-ott-react-native-video-player</h1>

<p align="center">
  <a href="https://www.npmjs.com/package/@zezosoft/zezo-ott-react-native-video-player">
    <img src="https://img.shields.io/npm/v/@zezosoft/zezo-ott-react-native-video-player.svg?style=flat-square&color=FF3B30" alt="npm version" />
  </a>
  <a href="https://www.npmjs.com/package/@zezosoft/zezo-ott-react-native-video-player">
    <img src="https://img.shields.io/npm/dm/@zezosoft/zezo-ott-react-native-video-player.svg?style=flat-square&color=blue" alt="downloads" />
  </a>
  <img src="https://img.shields.io/npm/l/@zezosoft/zezo-ott-react-native-video-player.svg?style=flat-square&color=green" alt="license" />
  <img src="https://img.shields.io/badge/platform-Android%20%7C%20iOS-lightgrey?style=flat-square" alt="platform" />
  <img src="https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square" alt="PRs Welcome" />
</p>

<p align="center">
  A production-ready, feature-rich <strong>React Native OTT Video Player</strong> for Android &amp; iOS.<br/>
  Built for premium streaming platforms — playlist/season management, VAST ads,<br/>
  audio/subtitle switching, gesture controls, and Zustand-based global state.
</p>

---

## Table of Contents

- [Features](#-features)
- [Architecture](#-architecture--data-flow)
- [Installation](#-installation--setup)
- [Quick Start](#-quick-start)
- [API Reference](#-api-reference)
- [Zustand Stores](#-zustand-global-stores)
- [Theming](#-theming)
- [Troubleshooting](#-troubleshooting)
- [License](#-license)

---

## ✨ 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 to avoid duplicate native code linking issues.

### Step 1 — Install the library

```sh
yarn add @zezosoft/zezo-ott-react-native-video-player
```

### Step 2 — Install peer dependencies

The player relies on native modules for video playback, gestures, animations, linear gradients, and screen orientation. These **must** be installed in your root React Native project:

#### 📋 Dependency Categorization

| Category | Packages | Reason |
| --- | --- | --- |
| **`peerDependencies`** (Native Modules & Frameworks) | `react`, `react-native`, `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` | Contains native C++/ObjC/Java code or React singleton instances. Must be installed in host app so autolinking works and duplicate native symbols/contexts are avoided. |
| **`dependencies`** (Pure JS Utilities) | `@zezosoft/zezo-ott-api-client`, `lucide-react-native`, `react-native-responsive-fontsize`, `react-native-size-matters`, `zustand` | Pure JavaScript packages. Automatically installed when installing this library. No extra setup required. |

#### 💻 Peer Dependencies Install Command

```sh
yarn add 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
```

### Step 3 — Platform setup

**iOS:**

```sh
cd ios && pod install
```

**Android** — wrap root with `GestureHandlerRootView`:

```tsx
import { GestureHandlerRootView } from 'react-native-gesture-handler';

export default function App() {
  return (
    <GestureHandlerRootView style={{ flex: 1 }}>
      <AppNavigation />
    </GestureHandlerRootView>
  );
}
```

### Step 4 — Rebuild

```sh
# iOS
cd ios && pod install && cd ..
# Android — clean rebuild recommended
```

---

## ⚡ Quick Start

### 1. Single Video Player

```tsx
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:**

```typescript
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:**

```tsx
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()}
    />
  );
}
```

<details>
<summary>Full example with all options</summary>

```tsx
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' },
});
```

</details>

---

## 📋 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](#-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`

```typescript
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.

```typescript
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 |

```tsx
<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

<p>
  <a href="https://github.com/Naresh-Dhamu" target="_blank">
    <img src="https://avatars.githubusercontent.com/u/89912059?v=4" width="80" height="80" alt="Naresh Dhamu" style="border-radius: 50%; margin-left:10px">
    <br />
    <strong>Naresh Dhamu</strong>
  </a>
</p>

---

## 📄 License

MIT © [Zezo Soft](https://github.com/Zezo-Soft)

---

<p align="center">⚡ Powered by <a href="https://zezosoft.com" target="_blank"><strong>ZezoSoft</strong></a></p>
