npm package: @ibad9/react-native-video-player
Description: YouTube-grade native video player for React Native with fully native controls. ExoPlayer (Android) + AVPlayer (iOS). HLS, DASH, DRM, IMA ads, Google Cast, AirPlay, PiP.
Latest version: 1.0.0

# @ibad9/react-native-video-player

YouTube-grade native video player for React Native with **fully native controls**. Zero JS-side UI rendering — all controls (play/pause, seek, quality, speed, captions, fullscreen, PiP, Cast, AirPlay) are rendered natively for maximum performance.

## Features

| Feature | Android | iOS |
|---------|---------|-----|
| HLS streaming | ✅ | ✅ |
| DASH streaming | ✅ | ❌ (iOS limitation) |
| DRM | Widevine / ClearKey | FairPlay |
| IMA pre-roll ads | ✅ | ✅ |
| Google Cast | ✅ | ✅ |
| AirPlay | ❌ | ✅ |
| Picture-in-Picture | ✅ | ✅ |
| Double-tap seek | ✅ | ✅ |
| Long-press 2x speed | ✅ | ✅ |
| Quality selector | ✅ | ✅ |
| Playback speed | ✅ | ✅ |
| Captions / subtitles | ✅ | ✅ |
| Watermark overlay | ✅ | ✅ |
| Fullscreen (button + rotation) | ✅ | ✅ |
| Error recovery (silent retry) | ✅ | ✅ |
| Live streams (LIVE badge) | ✅ | ✅ |

## Installation

```bash
npm install @ibad9/react-native-video-player
# or
yarn add @ibad9/react-native-video-player
```

### iOS

```bash
cd ios && pod install
```

### Android

No additional steps — autolinking handles everything.

## Required App-Level Configuration

### Android — `AndroidManifest.xml`

Add these to your `<application>` tag:

```xml
<!-- Google Cast options provider -->
<meta-data
  android:name="com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME"
  android:value="com.nativevideoplayer.feature.CastOptionsProvider" />
```

Add to your `<activity>` tag (for PiP support):

```xml
<activity
  ...
  android:supportsPictureInPicture="true"
  android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize|uiMode">
```

### iOS — `AppDelegate.swift`

Initialize Google Cast before React Native starts:

```swift
import GoogleCast

func application(_ application: UIApplication, didFinishLaunchingWithOptions ...) -> Bool {
    // Initialize Google Cast
    let castOptions = GCKCastOptions(discoveryCriteria: GCKDiscoveryCriteria(applicationID: kGCKDefaultMediaReceiverApplicationID))
    castOptions.physicalVolumeButtonsWillControlDeviceVolume = true
    GCKCastContext.setSharedInstanceWith(castOptions)

    // ... rest of setup
}
```

### iOS — `Info.plist`

For PiP and background audio, add:

```xml
<key>UIBackgroundModes</key>
<array>
    <string>audio</string>
</array>
```

For HTTP streams (non-HTTPS), add the domain to App Transport Security:

```xml
<key>NSAppTransportSecurity</key>
<dict>
    <key>NSAllowsArbitraryLoads</key>
    <true/>
</dict>
```

## Usage

```tsx
import NativeVideoPlayer from '@ibad9/react-native-video-player';
import type { NativeVideoPlayerRef } from '@ibad9/react-native-video-player';

const playerRef = useRef<NativeVideoPlayerRef>(null);

<NativeVideoPlayer
  ref={playerRef}
  source={{
    uri: 'https://example.com/stream.m3u8',
    metadata: { title: 'My Video', artist: 'Channel Name' },
  }}
  isLive={false}
  pipEnabled={true}
  castEnabled={true}
  adTagUrl={`https://pubads.g.doubleclick.net/gampad/ads?...&correlator=${Date.now()}`}
  watermarkUrl="https://example.com/logo.png"
  onLoad={(data) => console.log('Duration:', data.duration)}
  onError={(data) => console.error(data.error.message)}
  onEnd={() => console.log('Playback ended')}
  style={{ width: '100%', aspectRatio: 16 / 9, backgroundColor: '#000' }}
/>

// Imperative methods
playerRef.current?.seek(30000); // seek to 30s
playerRef.current?.destroy();   // release player
```

## Props

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `source` | `{ uri, headers?, startPosition?, metadata? }` | **required** | Media source |
| `drm` | `{ type, licenseServer, headers? }` | — | DRM config (`widevine`/`clearkey`/`fairplay`) |
| `textTracks` | `{ uri, language, title, mimeType }[]` | — | Sidecar subtitle tracks |
| `adTagUrl` | `string` | — | VAST/VMAP ad tag URL (enables IMA) |
| `isLive` | `boolean` | `false` | Low-latency buffer + LIVE badge |
| `pipEnabled` | `boolean` | `false` | Enable Picture-in-Picture |
| `castEnabled` | `boolean` | `false` | Show Google Cast button |
| `castReceiverAppId` | `string` | — | Custom Cast receiver app ID |
| `bufferConfig` | `{ minBufferMs?, maxBufferMs?, ... }` | — | Custom buffer config |
| `watermarkUrl` | `string` | — | URL to watermark image (top-right) |

## Events

| Event | Payload | Description |
|-------|---------|-------------|
| `onLoad` | `{ duration, naturalSize, audioTracks, textTracks, videoTracks }` | Media loaded |
| `onProgress` | `{ currentTime, playableDuration }` | Every 250ms |
| `onEnd` | — | Playback ended |
| `onError` | `{ error: { code, message, isFatal } }` | Error occurred |
| `onPlaybackStateChanged` | `{ isPlaying }` | Play/pause |
| `onPipChanged` | `{ isActive }` | PiP entered/exited |
| `onCastStateChanged` | `{ state, deviceName? }` | Cast connection |
| `onAdEvent` | `{ event, adData? }` | IMA ad lifecycle |
| `onAudioBecomingNoisy` | — | Headphones unplugged |
| `onAudioFocusChanged` | `{ hasAudioFocus }` | Audio focus change |

## Ref Methods

| Method | Description |
|--------|-------------|
| `seek(positionMs)` | Seek to position |
| `destroy()` | Release player resources |

## License

MIT