extended-vlc-player 0.1.2 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/README.md +411 -17
  2. package/package.json +1 -1
package/README.md CHANGED
@@ -1,23 +1,177 @@
1
1
  # extended-vlc-player
2
2
 
3
- A React Native video player for Expo SDK 57 / RN 0.86 / New Architecture that:
3
+ A React Native video player for **Expo SDK 57 / RN 0.86 / New Architecture (Fabric)** that decodes every container that VLC's mobile engines support, with a drop-in `expo-video`-compatible JSX surface and **true system Picture-in-Picture on iOS** — including for codecs the system `AVPlayer` cannot play (MKV, AVI, FLV, WMV, WebM, etc.).
4
4
 
5
- - Decodes every container **MobileVLCKit** (iOS) and **libVLC** (Android) support: MKV, AVI, FLV, WMV, WebM, MP4, MOV, M4V, TS, M3U8, 3GP, OGG, RM, VOB.
6
- - Delivers true system **Picture-in-Picture** on iOS, even for codecs the system AVPlayer cannot decode, by bridging MobileVLCKit's snapshot output to `AVSampleBufferDisplayLayer` + `AVPictureInPictureController.ContentSource` (the same path WebRTC video-call apps use).
7
- - Native Android PiP via `enterPictureInPictureMode`.
8
- - Mirrors the `expo-video` JSX surface so the existing `BackgroundVideoPlayer.jsx` can be migrated with a one-line import swap.
5
+ Under the hood:
9
6
 
10
- ## DRM
7
+ | Platform | Engine | Default version | Min OS |
8
+ | -------- | ----------------------------------- | --------------- | --------- |
9
+ | iOS | [MobileVLCKit](https://code.videolan.org/videolan/VLCKit) | `~3.7.3` | iOS 16.4+ |
10
+ | Android | [libVLC](https://code.videolan.org/videolan/vlc-android) | `3.6.0` | API 26+ |
11
11
 
12
- `extended-vlc-player` does **not** support Widevine / FairPlay DRM (VLC has no DRM path). For DRM content keep using `expo-video` the two can coexist in the same app.
12
+ The JS surface intentionally mirrors `expo-video` so an existing `<VideoView>` consumer (e.g. `BackgroundVideoPlayer.jsx`) can be migrated with a one-line import swap.
13
13
 
14
- ## Apple TV / Android TV
14
+ ---
15
15
 
16
- Out of scope for this iteration. The module is phone/tablet only. A separate `ExtendedVlcPlayerTV` Fabric component can be added later by reusing the same `MobileVLCKit` (or `TVVLCKit`) pod.
16
+ ## Table of contents
17
+
18
+ - [Why?](#why)
19
+ - [Supported containers](#supported-containers)
20
+ - [Supported features](#supported-features)
21
+ - [Picture-in-Picture (PiP)](#picture-in-picture-pip)
22
+ - [Explicitly out of scope](#explicitly-out-of-scope)
23
+ - [Install](#install)
24
+ - [Usage](#usage)
25
+ - [API reference](#api-reference)
26
+ - [Plugin options](#plugin-options)
27
+ - [Background audio & iOS session](#background-audio--ios-session)
28
+ - [Bundle size impact](#bundle-size-impact)
29
+ - [Platform support matrix](#platform-support-matrix)
30
+ - [Known limitations](#known-limitations)
31
+ - [Troubleshooting](#troubleshooting)
32
+ - [Roadmap](#roadmap)
33
+ - [License](#license)
34
+
35
+ ---
36
+
37
+ ## Why?
38
+
39
+ `expo-video` (backed by `AVPlayer` on iOS and `ExoPlayer` on Android) is the default RN video stack — and the right choice in most apps. But it cannot decode several containers IPTV, anime-fansub, and many other long-tail sources ship with:
40
+
41
+ - **MKV / Matroska** with H.264, H.265/HEVC, VP9, AV1
42
+ - **AVI** (DivX, Xvid, older codecs)
43
+ - **FLV** (Flash video, still common in live IPTV)
44
+ - **WMV** (Windows Media)
45
+ - **WebM** with VP8/VP9/Opus
46
+ - **RM / RMVB** (RealMedia)
47
+ - **VOB** (DVD)
48
+ - **3GP** (legacy mobile)
49
+ - **OGG / OGV** (Theora/Vorbis)
50
+ - **M3U8 / TS** HLS streams that `AVPlayer` rejects (some non-standard muxings, HEVC over HLS, etc.)
51
+
52
+ `extended-vlc-player` exists for the cases where the default `expo-video` stack throws `AVFoundationErrorDomain Code=-11828 "Cannot Open"` and you need the broadest possible "it just plays" surface. The two players can **coexist in the same app** — keep `expo-video` for DRM content and use this module for everything else (see [Explicitly out of scope](#explicitly-out-of-scope)).
53
+
54
+ ---
55
+
56
+ ## Supported containers
57
+
58
+ Everything MobileVLCKit / libVLC can demux. In practice that means:
59
+
60
+ | Container | Extensions | Notes |
61
+ | ----------- | ---------------------- | ----- |
62
+ | MP4 | `.mp4`, `.m4v` | Also handles HEVC, AV1 |
63
+ | MOV | `.mov` | QuickTime, ProRes |
64
+ | Matroska | `.mkv`, `.mk3d`, `.mka` | Multi-audio, multi-subtitle, chapters |
65
+ | WebM | `.webm` | VP8 / VP9 / AV1 / Opus |
66
+ | AVI | `.avi` | DivX, Xvid, legacy |
67
+ | FLV | `.flv` | Common in IPTV |
68
+ | Windows Media | `.wmv`, `.asf` | |
69
+ | OGG | `.ogv`, `.ogg` | Theora / Vorbis |
70
+ | 3GP | `.3gp`, `.3g2` | Legacy mobile |
71
+ | MPEG-TS | `.ts`, `.m2ts`, `.mts` | Raw transport streams |
72
+ | HLS | `.m3u8` | VLC handles muxings AVPlayer rejects |
73
+ | VOB | `.vob` | DVD |
74
+ | RealMedia | `.rm`, `.rmvb` | Limited support, depends on build |
75
+
76
+ > Codec coverage comes from the underlying VLC build, not from this module. MobileVLCKit 3.7.x and libVLC 3.6.x both ship the ffmpeg-based demuxer stack, so the same codec matrix applies on iOS and Android.
77
+
78
+ ---
79
+
80
+ ## Supported features
81
+
82
+ | Feature | iOS | Android |
83
+ | -------------------------------------- | --- | ------- |
84
+ | Play / pause / stop | ✅ | ✅ |
85
+ | Seek by seconds | ✅ | ✅ |
86
+ | Playback rate (0.1x – 4x) | ✅ | ✅ |
87
+ | Volume control (0 – 1) | ✅ | ✅ |
88
+ | Audio track selection (by index) | ✅ | ✅ |
89
+ | Subtitle track selection (by index) | ✅ | ✅ |
90
+ | `replace()` source swap without remount | ✅ | ✅ |
91
+ | `contentFit`: contain / cover / fill | ✅ | ✅ |
92
+ | Custom HTTP headers per source | ✅ | ✅ |
93
+ | Network / live / file caching (1.5s) | ✅ | ✅ |
94
+ | Progress / time / duration events | ✅ | ✅ |
95
+ | Buffering events | ✅ | ✅ |
96
+ | Play / pause / ended / error events | ✅ | ✅ |
97
+ | System Picture-in-Picture (PiP) | ✅ (see below) | ✅ (API 26+) |
98
+ | Background audio session | ✅ | ⚠️ (see below) |
99
+ | Foreground service / media notification | ❌ | ⚠️ (planned) |
100
+
101
+ ---
102
+
103
+ ## Picture-in-Picture (PiP)
104
+
105
+ PiP is the headline differentiator of this module. iOS PiP is hard to get right for non-`AVPlayer` renderers, so this section is intentionally detailed.
106
+
107
+ ### iOS
108
+
109
+ `AVPictureInPictureController` only accepts content from a `PlayerLayer`, `AVSampleBufferDisplayLayer`, or a manually constructed `ContentSource`. `MobileVLCKit` exposes neither a `CALayer` nor a `CVPixelBuffer` of the decoded frame — its only public "frame out" is the snapshot API.
110
+
111
+ The module bridges that gap with a three-step pipeline:
112
+
113
+ 1. `PlayerSession.snapshotTick` (a `CADisplayLink` at the display refresh rate) calls `VLCMediaPlayer.saveVideoSnapshot(at:withWidth:andHeight:)` to capture the current frame as a JPEG.
114
+ 2. `PipBridge.feed(image:)` decodes the `UIImage`, draws it into a pooled `CVPixelBuffer` via Core Graphics, wraps it in a `CMSampleBuffer`, and enqueues it on an `AVSampleBufferDisplayLayer` that is parented inside the player view.
115
+ 3. `AVPictureInPictureController.ContentSource(sampleBufferDisplayLayer:playbackDelegate:)` uses that layer as the PiP source — the same path WebRTC video-call apps use. The `SampleBufferPlaybackDelegate` forwards the iOS "play/pause/seek from PiP overlay" gestures back to the VLC media player, so the system controls in the PiP window actually work.
116
+
117
+ Why not go straight from VLC → `CVPixelBuffer`? MobileVLCKit does not expose the underlying `CVPixelBufferRef` of a decoded frame. A snapshot-bridge is the only path that doesn't require forking the VLCKit pod.
118
+
119
+ Trade-offs:
120
+
121
+ - **Latency** — the snapshot path is one frame behind the live drawable. In practice the user-perceivable delay is sub-100 ms.
122
+ - **CPU** — the bridge re-encodes each snapshot to BGRA pixel buffers at 30+ fps. On older devices this shows up as ~3-5% sustained CPU while PiP is active. The iOS PiP overlay caps its own frame rate at 30 fps, which keeps this manageable.
123
+ - **Drift** — `AVPictureInPictureController` re-evaluates the sample buffer cadence itself; we only need to keep feeding the layer.
124
+
125
+ ### Android
126
+
127
+ Native PiP via `Activity.enterPictureInPictureMode(PictureInPictureParams)`. The `app.plugin.js` patches `MainActivity` to add `android:supportsPictureInPicture="true"` and the `configChanges` set required for PiP to actually work, so the JS side only has to call `player.startPictureInPicture()`.
128
+
129
+ ### PiP API
130
+
131
+ ```ts
132
+ await player.startPictureInPicture(); // returns true if entered
133
+ await player.stopPictureInPicture(); // returns true if it was active
134
+ const active = await player.isPictureInPictureActive();
135
+ const supported = await player.isPictureInPictureSupported(); // device + OS check
136
+
137
+ <ExtendedVlcPlayerView
138
+ player={player}
139
+ onPictureInPictureStart={() => console.log('PiP started')}
140
+ onPictureInPictureStop={() => console.log('PiP stopped')}
141
+ />
142
+ ```
143
+
144
+ ---
145
+
146
+ ## Explicitly out of scope
147
+
148
+ These are **not** in this module. If you need them, keep using `expo-video` alongside it.
149
+
150
+ - **DRM** — Widevine (Android) and FairPlay (iOS). libVLC has no DRM path. The `drm` field on `ExtendedVlcSource` is accepted for API parity but is ignored; if a DRM source reaches the player it will emit `onError` and you should fall back to `expo-video`.
151
+ - **Apple TV / Android TV** — the module is phone/tablet only. A separate `ExtendedVlcPlayerTV` Fabric component can be added later by reusing the same `MobileVLCKit` / `libVLC` pod and adding a tvOS-targeted build configuration.
152
+ - **Web** — there is no WebAssembly VLC build wired into this module.
153
+ - **AirPlay / Chromecast** — VLC has its own renderer / output subsystems but they are not exposed.
154
+ - **Recording** — the module does not write a transcoded output.
155
+ - **DASH / MSS adaptive streaming** — only HLS via the container layer is exposed; DASH can be added by routing the manifest URL through `VLCMedia` explicitly.
156
+
157
+ The two players can sit side-by-side in the same app:
158
+
159
+ ```tsx
160
+ const useVlc = prefersWideFormat || knownUnplayableOnAVPlayer;
161
+
162
+ if (useVlc) {
163
+ const player = useExtendedVlcPlayer(source);
164
+ return <ExtendedVlcPlayerView player={player} style={{ flex: 1 }} />;
165
+ }
166
+
167
+ return <VideoView player={expoVideoPlayer} style={{ flex: 1 }} />;
168
+ ```
169
+
170
+ ---
17
171
 
18
172
  ## Install
19
173
 
20
- In `mobile/package.json`:
174
+ In the host app's `package.json`:
21
175
 
22
176
  ```json
23
177
  "dependencies": {
@@ -25,7 +179,7 @@ In `mobile/package.json`:
25
179
  }
26
180
  ```
27
181
 
28
- Then add the plugin to `app.json`:
182
+ Then in `app.json`:
29
183
 
30
184
  ```json
31
185
  "plugins": [
@@ -36,10 +190,27 @@ Then add the plugin to `app.json`:
36
190
  ]
37
191
  ```
38
192
 
39
- Run `npx expo prebuild --clean` so the new pod + gradle deps are wired.
193
+ Then wire the native side:
194
+
195
+ ```bash
196
+ npx expo prebuild --clean
197
+ ```
198
+
199
+ The plugin (`app.plugin.js`) does the following, idempotently:
200
+
201
+ - **iOS Podfile** — adds `pod 'MobileVLCKit', '~> 3.7.3'` and a `post_install` hook that pins `IPHONEOS_DEPLOYMENT_TARGET = 16.4` on the MobileVLCKit target.
202
+ - **iOS Info.plist** — adds `"audio"` to `UIBackgroundModes` so the audio session is eligible for background playback and PiP.
203
+ - **Android `android/app/build.gradle`** — adds `implementation "org.videolan.android:libvlc:3.6.0"`. ABI filters are inherited from the host app.
204
+ - **Android `AndroidManifest.xml`** — adds `android:supportsPictureInPicture="true"` and the `configChanges` set to `MainActivity`.
205
+
206
+ To publish to npm, run `npm publish` from the module root. To consume from a local checkout, `file:` works during development and you should switch to the registry version before shipping (see [Troubleshooting](#troubleshooting)).
207
+
208
+ ---
40
209
 
41
210
  ## Usage
42
211
 
212
+ Minimal:
213
+
43
214
  ```tsx
44
215
  import { useExtendedVlcPlayer, ExtendedVlcPlayerView } from 'extended-vlc-player';
45
216
 
@@ -49,11 +220,234 @@ function MyPlayer({ uri }: { uri: string }) {
49
220
  }
50
221
  ```
51
222
 
223
+ With the full event surface:
224
+
225
+ ```tsx
226
+ import {
227
+ useExtendedVlcPlayer,
228
+ ExtendedVlcPlayerView,
229
+ type ExtendedVlcSource,
230
+ } from 'extended-vlc-player';
231
+
232
+ const source: ExtendedVlcSource = {
233
+ uri: 'https://example.com/video.mkv',
234
+ headers: { 'X-Auth-Token': 'abc' },
235
+ };
236
+
237
+ function ChannelPlayer({ uri }: { uri: string }) {
238
+ const player = useExtendedVlcPlayer(source, {
239
+ onReady: (p) => p.setVolume(0.8),
240
+ });
241
+
242
+ return (
243
+ <ExtendedVlcPlayerView
244
+ player={player}
245
+ style={{ flex: 1 }}
246
+ contentFit="contain"
247
+ onLoad={(e) => {
248
+ console.log(`duration=${e.duration}s, ${e.audioTracks.length} audio tracks`);
249
+ }}
250
+ onProgress={({ currentTime, duration }) => {
251
+ // throttle as needed
252
+ }}
253
+ onBuffering={({ isBuffering }) => setLoading(isBuffering)}
254
+ onError={(err) => console.warn('VLC error:', err)}
255
+ />
256
+ );
257
+ }
258
+ ```
259
+
260
+ Migrating from `expo-video`:
261
+
262
+ ```diff
263
+ -import { useVideoPlayer, VideoView } from 'expo-video';
264
+ +import { useExtendedVlcPlayer, ExtendedVlcPlayerView } from 'extended-vlc-player';
265
+
266
+ -const player = useVideoPlayer(uri, p => { p.play(); });
267
+ - <VideoView player={player} style={{ flex: 1 }} />
268
+ +const player = useExtendedVlcPlayer(uri);
269
+ + <ExtendedVlcPlayerView player={player} style={{ flex: 1 }} />
270
+ ```
271
+
272
+ Player method names (`play` / `pause` / `seek` / `setRate` / `setVolume`) are identical to `expo-video`, so the surrounding controls keep working unchanged.
273
+
274
+ ---
275
+
276
+ ## API reference
277
+
278
+ ### `useExtendedVlcPlayer(source, options?)`
279
+
280
+ Returns a stable player object. Methods are closures over the current native instance; the object identity does not change across renders, so it is safe in `useEffect` dependency arrays.
281
+
282
+ | Method | Returns | Description |
283
+ | --------------------------------------- | ---------------------- | ----------- |
284
+ | `play()` | `void` | Start playback. |
285
+ | `pause()` | `void` | Pause. |
286
+ | `stop()` | `void` | Stop and detach the current media. |
287
+ | `seek(seconds: number)` | `void` | Jump to a wall-clock position in seconds. |
288
+ | `setRate(rate: number)` | `void` | Playback rate multiplier (0.1 – 4.0, clamped). |
289
+ | `setVolume(volume: number)` | `void` | `0` – `1`, mapped to VLC's internal `0` – `100` / `200` scale. |
290
+ | `setAudioTrack(index: number)` | `void` | Index from the most recent `onLoad` payload's `audioTracks`, or `-1` to disable. |
291
+ | `setSubtitleTrack(index: number)` | `void` | Same for `textTracks`, or `-1` to disable. |
292
+ | `replace(source)` | `void` | Swap media without remounting the view. |
293
+ | `startPictureInPicture()` | `Promise<boolean>` | Enters the system PiP overlay. |
294
+ | `stopPictureInPicture()` | `Promise<boolean>` | Exits PiP. |
295
+ | `isPictureInPictureActive()` | `Promise<boolean>` | Whether the PiP controller is currently active. |
296
+ | `isPictureInPictureSupported()` | `Promise<boolean>` | Whether the device + OS support PiP. |
297
+
298
+ ### `<ExtendedVlcPlayerView>`
299
+
300
+ | Prop | Type | Default | Description |
301
+ | ------------------------------- | --------------------- | ------------ | ----------- |
302
+ | `player` | `ExtendedVlcPlayer` | required | From `useExtendedVlcPlayer`. |
303
+ | `style` | `ViewStyle` | — | Layout style. |
304
+ | `contentFit` | `'contain' \| 'cover' \| 'fill'` | `'contain'` | How the video is scaled within the view. |
305
+ | `onLoad` | `(e) => void` | — | Fired once VLC has parsed the media. `e.duration` in seconds, plus `audioTracks` and `textTracks`. |
306
+ | `onProgress` | `(e) => void` | — | Time updates. `e.currentTime`, `e.duration`, `e.position` (0..1). |
307
+ | `onPlaying` | `(e) => void` | — | First `playing` state. |
308
+ | `onPaused` | `(e) => void` | — | `e.target` is the position the user paused at. |
309
+ | `onEnded` | `() => void` | — | `stopped` or `ended` state reached. |
310
+ | `onError` | `(e) => void` | — | `e.message`, `e.code`, `e.domain`. |
311
+ | `onBuffering` | `(e) => void` | — | `e.isBuffering`. |
312
+ | `onPictureInPictureStart` | `() => void` | — | |
313
+ | `onPictureInPictureStop` | `() => void` | — | |
314
+
315
+ ### `ExtendedVlcSource`
316
+
317
+ ```ts
318
+ type ExtendedVlcSource = string | {
319
+ uri: string;
320
+ headers?: Record<string, string>;
321
+ /** Accepted for API parity. Ignored — VLC has no DRM path. */
322
+ drm?: unknown;
323
+ };
324
+ ```
325
+
326
+ ---
327
+
328
+ ## Plugin options
329
+
330
+ All options are optional. Defaults match what the plugin is pinned to in CI.
331
+
332
+ ```ts
333
+ [
334
+ 'extended-vlc-player',
335
+ {
336
+ ios: {
337
+ // MobileVLCKit pod version. Locked to ~3.7.x.
338
+ mobileVlcKitVersion: '3.7.3',
339
+ // Whether to embed bitcode. App Store no longer accepts bitcode;
340
+ // this is here for completeness and is effectively a no-op.
341
+ enableBitcode: false,
342
+ },
343
+ android: {
344
+ // org.videolan.android:libvlc version.
345
+ libVlcVersion: '3.6.0',
346
+ },
347
+ pip: {
348
+ // Reserved for the snapshot bridge. The current bridge runs at the
349
+ // display refresh rate; future revisions may sample down.
350
+ snapshotFps: 30,
351
+ // Reserved for the snapshot bridge. 'low' | 'medium' | 'high'.
352
+ snapshotQuality: 'medium',
353
+ },
354
+ },
355
+ ]
356
+ ```
357
+
358
+ ---
359
+
360
+ ## Background audio & iOS session
361
+
362
+ iOS rejects background audio unless `UIBackgroundModes` includes `"audio"`. The plugin adds it idempotently on `npx expo prebuild`, so the VLC media player keeps playing and the PiP window keeps being controllable while the user backgrounds the app.
363
+
364
+ On Android, libVLC continues decoding as long as the host activity is alive, but the module does **not** ship a foreground media notification service yet. If the user switches apps, playback is at the mercy of the system's process priority. See the [Roadmap](#roadmap).
365
+
366
+ ---
367
+
52
368
  ## Bundle size impact
53
369
 
54
- | Platform | Pre-existing | After install |
55
- |---|---|---|
56
- | iOS IPA | ~50-60 MB | +50-65 MB (MobileVLCKit) |
57
- | Android AAB per-ABI | ~30-40 MB | +30-40 MB (libVLC .so) |
370
+ The VLC engines are large — they ship the ffmpeg-based demuxer stack with most codecs. Plan the budget accordingly.
371
+
372
+ | Platform | Pre-existing | After install (raw) | After install (user-visible) |
373
+ | ------------------------ | ------------ | ------------------- | --------------------------- |
374
+ | iOS IPA | ~50 – 60 MB | **+50 – 65 MB** (MobileVLCKit) | **~25 – 35 MB** (App Thinning) |
375
+ | Android AAB per-ABI | ~30 – 40 MB | **+30 – 40 MB** (libVLC `.so`) | **~10 – 15 MB** (after ABI splits) |
376
+
377
+ The **download** size still grows by the pre-split number; App Thinning and ABI splits only reduce the *installed* footprint. If you can detect a stream is MP4/H.264 with AAC, prefer `expo-video` and keep the VLC engine for fallback only.
378
+
379
+ ---
380
+
381
+ ## Platform support matrix
382
+
383
+ | | iOS | Android |
384
+ | ------------------------ | -------------------------------- | -------------------------------- |
385
+ | OS minimum | 16.4 | API 26 (Android 8.0) |
386
+ | New Architecture (Fabric)| ✅ required | ✅ required |
387
+ | Expo | SDK 57 | SDK 57 |
388
+ | React Native | 0.86 | 0.86 |
389
+ | Architectures (Android) | — | `arm64-v8a`, `armeabi-v7a`, `x86`, `x86_64` (filtered by host app) |
390
+ | PiP | ✅ via sample-buffer bridge | ✅ via `enterPictureInPictureMode` |
391
+
392
+ ---
393
+
394
+ ## Known limitations
395
+
396
+ - **iOS PiP is one frame behind** the live drawable due to the snapshot bridge (sub-100 ms in practice).
397
+ - **No DRM.** `Widevine` / `FairPlay` sources will fail with `onError`; use `expo-video` for them.
398
+ - **Apple TV / Android TV** are not built. The host platform decides whether to instantiate this module; the `iOS` config in `expo-module.config.json` currently lists only `apple` (phone) targets, not `appletvos`.
399
+ - **Android background audio** is not yet bound to a foreground service. Long-running audio in the background may be paused by the system.
400
+ - **Snapshot-bridge CPU** on iOS is ~3-5% sustained while PiP is active. Older devices (iPhone 8 / X) may see a small thermal impact.
401
+ - **The `audioTracks` / `textTracks` payloads** in the `onLoad` event currently expose only `{ index }` — the human-readable label / language / codec are typed in the JS surface (`ExtendedVlcTrack`) but the iOS bridge does not yet read them from `MobileVLCKit` (Android is the same). A future patch adds the `audioTrackNames` / `videoSubTitlesNames` arrays.
402
+ - **`file:` npm installs** in a monorepo can pull duplicate transitive deps. See [Troubleshooting](#troubleshooting).
403
+
404
+ ---
405
+
406
+ ## Troubleshooting
407
+
408
+ - **"Native module is not available"** — run `npx expo prebuild --clean` and rebuild. If the host app was generated before the plugin was added, the prebuild was skipped; rerun it so the Podfile / build.gradle / AndroidManifest changes land.
409
+ - **PiP is supported on the device but `startPictureInPicture()` returns false** — on iOS, the underlying drawable may not be in the view hierarchy. Ensure the `ExtendedVlcPlayerView` is mounted and visible at least once before calling `startPictureInPicture`. The view's `onLoad` is a safe trigger.
410
+ - **MKV streams still fail with "Cannot Open" on iOS** — the iOS path goes through `AVSampleBufferDisplayLayer` for PiP but the actual decode is still done by MobileVLCKit. If the stream fails, the `onError` event carries the VLC error message; check it before assuming a codec issue.
411
+ - **`expo-doctor` flags `expo-modules-core` as a direct dep** — this module imports from `expo-modules-core` (`requireNativeModule`) and lists it as a `peerDependency`, which the npm resolver forces onto the consumer. The flag is a known false positive. It is fixed in the published module by removing `expo-modules-core` from `peerDependencies` so it is resolved transitively via `expo`.
412
+ - **`npm install file:../extended-vlc-player` brings duplicate deps** — when you eventually pin to the published version, use `npm install extended-vlc-player@<published-version> --save-exact` to force npm to re-resolve from the registry and dedupe.
413
+
414
+ ---
415
+
416
+ ## Roadmap
417
+
418
+ Ordered roughly by near-term value. Nothing here is a promise — items depend on user demand and the upstream VLC release cadence.
419
+
420
+ ### Near term
421
+
422
+ - **Apple TV / Android TV** — `ExtendedVlcPlayerTV` Fabric component reusing the same `MobileVLCKit` / `libVLC` pod with a `tvOSTargetOSVersion` build config.
423
+ - **Track label enrichment** — populate `label` / `language` / `codec` on `ExtendedVlcTrack` from `audioTrackNames` / `videoSubTitlesNames` and the `MediaPlayer.TrackDescription` API.
424
+ - **Android background audio** — foreground service + `MediaSession` so playback survives the app being swiped away.
425
+ - **DASH manifest support** — route `*.mpd` URLs through `VLCMedia` explicitly so the demuxer sees a DASH source rather than a generic file.
426
+
427
+ ### Medium term
428
+
429
+ - **Hardware-accelerated iOS PiP bridge** — drop the snapshot path by tapping into MobileVLCKit's internal `CVPixelBufferRef` once a stable private API is documented; expected to cut PiP CPU from ~3-5% to < 1%.
430
+ - **LL-HLS tuning** — lower the live cache window for low-latency HLS sources, expose `--http-reconnect` and `--network-caching` knobs per-source.
431
+ - **AirPlay routing** — secondary display via VLC's `VDPAU` / `mmal` output path. Currently a no-op.
432
+ - **Chromecast support** — the receiver side needs a custom cast app; the module will expose a `castUrl` shortcut.
433
+
434
+ ### Long term
435
+
436
+ - **Web build** — emscripten / WebAssembly VLC for Expo Web, behind the same `useExtendedVlcPlayer` hook.
437
+ - **Recording / transcoding** — a `recordToFile()` API that wires `VLCMediaPlayer`'s `media`-output path.
438
+ - **Latency-targeted mode** for live IPTV (HLS LL / WebRTC ingest), including `jitter-buffer` / `live-caching` tuning per source.
439
+ - **Adaptive ABR** — surface libVLC's adaptive logic as JS events so the app can render its own quality switcher.
440
+
441
+ ---
442
+
443
+ ## License
444
+
445
+ MIT — see [`LICENSE`](./LICENSE). MobileVLCKit and libVLC are LGPL-2.1-or-later; their licenses are inherited by the engines that this module links against, not by this module's source.
446
+
447
+ ---
448
+
449
+ ## Related
58
450
 
59
- App Thinning on iOS and ABI splits on Android bring the **user-visible install** increase down to ~25-35 MB iOS / ~10-15 MB Android; the *download* size still grows by the pre-split number.
451
+ - `expo-video` the default RN video player. Use alongside this one for DRM content.
452
+ - [MobileVLCKit](https://code.videolan.org/videolan/VLCKit) — iOS engine.
453
+ - [libVLC for Android](https://code.videolan.org/videolan/vlc-android/-/tree/master/libvlc) — Android engine.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "extended-vlc-player",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "React Native video player built on MobileVLCKit (iOS) and libVLC (Android) with true iOS Picture-in-Picture via AVSampleBufferDisplayLayer bridge.",
5
5
  "license": "MIT",
6
6
  "main": "build/index.js",