extended-vlc-player 0.1.0

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 (39) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +59 -0
  3. package/android/build.gradle +36 -0
  4. package/android/src/main/AndroidManifest.xml +16 -0
  5. package/android/src/main/java/expo/modules/extendedvlcplayer/ExtendedVlcPlayerModule.kt +99 -0
  6. package/android/src/main/java/expo/modules/extendedvlcplayer/ExtendedVlcPlayerViewComponentView.kt +82 -0
  7. package/android/src/main/java/expo/modules/extendedvlcplayer/PlayerRegistry.kt +47 -0
  8. package/android/src/main/java/expo/modules/extendedvlcplayer/PlayerSession.kt +168 -0
  9. package/app.plugin.js +144 -0
  10. package/build/ExtendedVlcPlayerView.d.ts +8 -0
  11. package/build/ExtendedVlcPlayerView.d.ts.map +1 -0
  12. package/build/ExtendedVlcPlayerView.js +30 -0
  13. package/build/ExtendedVlcPlayerView.js.map +1 -0
  14. package/build/index.d.ts +12 -0
  15. package/build/index.d.ts.map +1 -0
  16. package/build/index.js +11 -0
  17. package/build/index.js.map +1 -0
  18. package/build/types.d.ts +97 -0
  19. package/build/types.d.ts.map +1 -0
  20. package/build/types.js +2 -0
  21. package/build/types.js.map +1 -0
  22. package/build/useExtendedVlcPlayer.d.ts +12 -0
  23. package/build/useExtendedVlcPlayer.d.ts.map +1 -0
  24. package/build/useExtendedVlcPlayer.js +137 -0
  25. package/build/useExtendedVlcPlayer.js.map +1 -0
  26. package/expo-module.config.json +9 -0
  27. package/ios/AudioSessionConfigurator.swift +25 -0
  28. package/ios/ExtendedVlcPlayer.podspec +32 -0
  29. package/ios/ExtendedVlcPlayerModule.swift +130 -0
  30. package/ios/ExtendedVlcPlayerViewComponentView.h +19 -0
  31. package/ios/ExtendedVlcPlayerViewComponentView.mm +101 -0
  32. package/ios/PipBridge.swift +144 -0
  33. package/ios/PlayerRegistryBridge.swift +67 -0
  34. package/ios/PlayerSession.swift +335 -0
  35. package/package.json +65 -0
  36. package/src/ExtendedVlcPlayerView.tsx +68 -0
  37. package/src/index.ts +20 -0
  38. package/src/types.ts +100 -0
  39. package/src/useExtendedVlcPlayer.ts +160 -0
package/package.json ADDED
@@ -0,0 +1,65 @@
1
+ {
2
+ "name": "extended-vlc-player",
3
+ "version": "0.1.0",
4
+ "description": "React Native video player built on MobileVLCKit (iOS) and libVLC (Android) with true iOS Picture-in-Picture via AVSampleBufferDisplayLayer bridge.",
5
+ "license": "MIT",
6
+ "main": "build/index.js",
7
+ "types": "build/index.d.ts",
8
+ "author": {
9
+ "name": "Berat Tüfekli",
10
+ "email": "berattufekli@gmail.com"
11
+ },
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "https://github.com/berattufekli/extended-vlc-player.git"
15
+ },
16
+ "bugs": {
17
+ "url": "https://github.com/berattufekli/extended-vlc-player/issues"
18
+ },
19
+ "homepage": "https://github.com/berattufekli/extended-vlc-player#readme",
20
+ "keywords": [
21
+ "react-native",
22
+ "expo",
23
+ "video",
24
+ "player",
25
+ "vlc",
26
+ "mobilevlckit",
27
+ "mkv",
28
+ "matroska",
29
+ "picture-in-picture",
30
+ "pip",
31
+ "iptv",
32
+ "expo-module"
33
+ ],
34
+ "scripts": {
35
+ "build": "expo-module build",
36
+ "clean": "expo-module clean",
37
+ "lint": "expo-module lint",
38
+ "test": "jest",
39
+ "typecheck": "tsc --noEmit",
40
+ "prepublishOnly": "tsc --noEmit"
41
+ },
42
+ "files": [
43
+ "build",
44
+ "ios",
45
+ "android",
46
+ "src",
47
+ "app.plugin.js",
48
+ "expo-module.config.json",
49
+ "README.md",
50
+ "LICENSE"
51
+ ],
52
+ "peerDependencies": {
53
+ "expo": "*",
54
+ "expo-modules-core": "*",
55
+ "react": "*",
56
+ "react-native": "*"
57
+ },
58
+ "devDependencies": {
59
+ "@types/react": "~19.2.14",
60
+ "expo": "^57.0.0",
61
+ "expo-module-scripts": "^56.0.3",
62
+ "expo-modules-core": "~57.0.2",
63
+ "typescript": "~6.0.3"
64
+ }
65
+ }
@@ -0,0 +1,68 @@
1
+ import { requireNativeViewManager } from 'expo-modules-core';
2
+ import * as React from 'react';
3
+ import { Platform, type ViewStyle } from 'react-native';
4
+
5
+ import type { ExtendedVlcPlayerViewProps } from './types';
6
+
7
+ const NativeView = requireNativeViewManager('ExtendedVlcPlayerView');
8
+
9
+ /**
10
+ * Drop-in replacement for `expo-video`'s `VideoView`. Renders the VLC-backed
11
+ * native player and forwards events back to the JS callbacks.
12
+ */
13
+ export const ExtendedVlcPlayerView = React.forwardRef<unknown, ExtendedVlcPlayerViewProps>(
14
+ function ExtendedVlcPlayerView(props, ref) {
15
+ const {
16
+ player,
17
+ style,
18
+ contentFit = 'contain',
19
+ onLoad,
20
+ onProgress,
21
+ onPlaying,
22
+ onPaused,
23
+ onEnded,
24
+ onError,
25
+ onBuffering,
26
+ onPictureInPictureStart,
27
+ onPictureInPictureStop,
28
+ } = props;
29
+
30
+ // The `player` object carries the nativeId internally (the hook bumps
31
+ // it on first render). The native view reads it via a getter so we
32
+ // don't have to thread the number through props.
33
+ const nativeId = (player as unknown as { _nativeId?: number })._nativeId ?? 0;
34
+
35
+ return (
36
+ <NativeView
37
+ ref={ref}
38
+ style={style as ViewStyle}
39
+ player={nativeId}
40
+ contentFit={contentFit}
41
+ // Standard Fabric "bubbling event" props. They are passed as
42
+ // RCTBubblingEventBlock on the native side; the Swift
43
+ // PlayerSession fires the corresponding closures which the
44
+ // PlayerRegistryBridge forwards back to the view component,
45
+ // which then invokes the right block.
46
+ onLoad={onLoad}
47
+ onProgress={onProgress}
48
+ onPlaying={onPlaying}
49
+ onPaused={onPaused}
50
+ onEnded={onEnded}
51
+ onError={onError}
52
+ onBuffering={onBuffering}
53
+ onPictureInPictureStart={onPictureInPictureStart}
54
+ onPictureInPictureStop={onPictureInPictureStop}
55
+ />
56
+ );
57
+ }
58
+ );
59
+
60
+ // Surface a helpful message on platforms where the view manager is not
61
+ // registered (e.g. web). The real native view manager is registered by
62
+ // the iOS / Android sides.
63
+ if (Platform.OS !== 'ios' && Platform.OS !== 'android') {
64
+ // eslint-disable-next-line no-console
65
+ console.warn(
66
+ '[extended-vlc-player] ExtendedVlcPlayerView is only supported on iOS and Android.'
67
+ );
68
+ }
package/src/index.ts ADDED
@@ -0,0 +1,20 @@
1
+ /**
2
+ * `extended-vlc-player` public API.
3
+ *
4
+ * The default export is the native view component; named exports are the
5
+ * imperative player hook and the type contracts. Consumers normally use:
6
+ *
7
+ * import { useExtendedVlcPlayer, ExtendedVlcPlayerView } from 'extended-vlc-player';
8
+ */
9
+ export { ExtendedVlcPlayerView } from './ExtendedVlcPlayerView';
10
+ export { useExtendedVlcPlayer } from './useExtendedVlcPlayer';
11
+ export type {
12
+ ContentFit,
13
+ ExtendedVlcErrorEvent,
14
+ ExtendedVlcLoadEvent,
15
+ ExtendedVlcPlayer,
16
+ ExtendedVlcPlayerViewProps,
17
+ ExtendedVlcProgressEvent,
18
+ ExtendedVlcSource,
19
+ ExtendedVlcTrack,
20
+ } from './types';
package/src/types.ts ADDED
@@ -0,0 +1,100 @@
1
+ import type { ViewStyle } from 'react-native';
2
+
3
+ /**
4
+ * Source descriptor for the VLC player. Mirrors `expo-video`'s `VideoSource`
5
+ * surface so the existing call sites (a string URL or an object) keep
6
+ * working without change.
7
+ */
8
+ export type ExtendedVlcSource =
9
+ | string
10
+ | {
11
+ uri: string;
12
+ headers?: Record<string, string>;
13
+ /**
14
+ * Optional drm descriptor. NOTE: the new module does not decrypt
15
+ * Widevine/FairPlay streams (libVLC has no DRM path). The field is
16
+ * accepted for API parity with `expo-video`; if a DRM source reaches
17
+ * the player it will emit `onError` and the caller should fall back
18
+ * to `expo-video`.
19
+ */
20
+ drm?: unknown;
21
+ };
22
+
23
+ /** Audio / subtitle track entry exposed by VLC after a source loads. */
24
+ export interface ExtendedVlcTrack {
25
+ index: number;
26
+ /** Localised label, e.g. "English", "Türkçe 5.1". */
27
+ label?: string;
28
+ /** RFC 5646 language code when VLC exposes one. */
29
+ language?: string;
30
+ /** Codec identifier reported by libVLC (e.g. "mp4a", "ac3", "subrip"). */
31
+ codec?: string;
32
+ }
33
+
34
+ export interface ExtendedVlcLoadEvent {
35
+ duration: number; // seconds
36
+ audioTracks: ExtendedVlcTrack[];
37
+ textTracks: ExtendedVlcTrack[];
38
+ }
39
+
40
+ export interface ExtendedVlcProgressEvent {
41
+ currentTime: number; // seconds
42
+ duration: number; // seconds
43
+ /** 0..1 position; mirrors the legacy VLC position field. */
44
+ position: number;
45
+ }
46
+
47
+ export interface ExtendedVlcErrorEvent {
48
+ code?: string;
49
+ message: string;
50
+ domain?: string;
51
+ }
52
+
53
+ export type ContentFit = 'contain' | 'cover' | 'fill';
54
+
55
+ export interface ExtendedVlcPlayerViewProps {
56
+ /** The player object returned by `useExtendedVlcPlayer`. */
57
+ player: ExtendedVlcPlayer;
58
+ style?: ViewStyle;
59
+ contentFit?: ContentFit;
60
+ onLoad?: (e: ExtendedVlcLoadEvent) => void;
61
+ onProgress?: (e: ExtendedVlcProgressEvent) => void;
62
+ onPlaying?: (e: { duration: number }) => void;
63
+ onPaused?: (e: { target: number }) => void;
64
+ onEnded?: () => void;
65
+ onError?: (e: ExtendedVlcErrorEvent) => void;
66
+ onBuffering?: (e: { isBuffering: boolean }) => void;
67
+ onPictureInPictureStart?: () => void;
68
+ onPictureInPictureStop?: () => void;
69
+ }
70
+
71
+ /**
72
+ * Imperative player object. Mirrors the surface of `BackgroundVideoPlayer`'s
73
+ * `videoRef.current` so consumers can swap the import without touching
74
+ * the rest of their UI.
75
+ */
76
+ export interface ExtendedVlcPlayer {
77
+ play(): void;
78
+ pause(): void;
79
+ stop(): void;
80
+ /** `seconds` is a wall-clock position in seconds (matches expo-video). */
81
+ seek(seconds: number): void;
82
+ /** `rate` is a playback rate multiplier (0.5, 1, 1.5, 2). */
83
+ setRate(rate: number): void;
84
+ /** `volume` is 0..1 (matches the existing VLC contract). */
85
+ setVolume(volume: number): void;
86
+ /** `index` of the audio track in the most recent `onLoad` payload, or -1 to disable. */
87
+ setAudioTrack(index: number): void;
88
+ /** `index` of the subtitle track, or -1 to disable. */
89
+ setSubtitleTrack(index: number): void;
90
+ /** Replace the current source without remounting the view. */
91
+ replace(source: ExtendedVlcSource): void;
92
+ /** Start the system Picture-in-Picture overlay. Resolves true when entered. */
93
+ startPictureInPicture(): Promise<boolean>;
94
+ /** Exit PiP if it is active. Resolves true when the controller stopped. */
95
+ stopPictureInPicture(): Promise<boolean>;
96
+ /** True while PiP is active. Mirrors the native state. */
97
+ isPictureInPictureActive(): Promise<boolean>;
98
+ /** True when the device + iOS version support PiP. */
99
+ isPictureInPictureSupported(): Promise<boolean>;
100
+ }
@@ -0,0 +1,160 @@
1
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
2
+ import { requireNativeModule } from 'expo-modules-core';
3
+
4
+ import type {
5
+ ExtendedVlcLoadEvent,
6
+ ExtendedVlcPlayer,
7
+ ExtendedVlcProgressEvent,
8
+ ExtendedVlcSource,
9
+ } from './types';
10
+
11
+ // Native module is registered by the platform-specific module under the
12
+ // JS module name "ExtendedVlcPlayer". The requireNativeModule helper resolves
13
+ // it at runtime; if the native side is missing, accessing the module throws
14
+ // — we surface a clear error so the consumer knows what failed instead of
15
+ // silently producing a no-op player.
16
+ let nativeModule: any = null;
17
+ try {
18
+ nativeModule = requireNativeModule('ExtendedVlcPlayer');
19
+ } catch (error) {
20
+ // Allow JS-side import of the module to succeed even when the native side
21
+ // is not yet built (e.g. in a Jest test environment). We throw a helpful
22
+ // error only when the consumer actually calls a method.
23
+ nativeModule = null;
24
+ }
25
+
26
+ function getModule() {
27
+ if (!nativeModule) {
28
+ throw new Error(
29
+ '[extended-vlc-player] Native module is not available. ' +
30
+ 'Run `npx expo prebuild --clean` and rebuild the app, ' +
31
+ 'or check that the plugin ran successfully.'
32
+ );
33
+ }
34
+ return nativeModule;
35
+ }
36
+
37
+ function normalizeSource(source: ExtendedVlcSource) {
38
+ if (typeof source === 'string') return { uri: source };
39
+ const { uri } = source;
40
+ if (!uri) {
41
+ throw new Error('[extended-vlc-player] source.uri is required');
42
+ }
43
+ return { uri, headers: source.headers ?? null, drm: source.drm ?? null };
44
+ }
45
+
46
+ export interface UseExtendedVlcPlayerOptions {
47
+ /** Called once after the player is constructed. Useful for attaching PiP handlers. */
48
+ onReady?: (player: ExtendedVlcPlayer) => void;
49
+ }
50
+
51
+ /**
52
+ * Returns a stable player object for the given source. The native player
53
+ * is created once (lazy on first method call) and replaced in place when
54
+ * `source` changes via the same `replace` API used by `expo-video`.
55
+ */
56
+ export function useExtendedVlcPlayer(
57
+ source: ExtendedVlcSource,
58
+ options: UseExtendedVlcPlayerOptions = {}
59
+ ): ExtendedVlcPlayer {
60
+ const normalized = useMemo(() => normalizeSource(source), [source]);
61
+ // We hold a numeric id of the "current native source" so we can detect
62
+ // when the JS source prop has changed and tell the native module to swap.
63
+ const [nativeId, setNativeId] = useState(0);
64
+ const isMountedRef = useRef(true);
65
+
66
+ // Event subscription is wired through the Fabric view component, not the
67
+ // module — events flow back into the JS player object via the
68
+ // ExtendedVlcPlayerView's callbacks. We keep the module call surface
69
+ // (play/pause/seek/PiP) here.
70
+
71
+ useEffect(() => {
72
+ isMountedRef.current = true;
73
+ return () => {
74
+ isMountedRef.current = false;
75
+ };
76
+ }, []);
77
+
78
+ useEffect(() => {
79
+ // When the source prop changes, ask the native module to swap in place
80
+ // instead of remounting the view (which would reset audio session, PiP
81
+ // delegate wiring, etc.).
82
+ try {
83
+ getModule().replace(normalized);
84
+ } catch (error) {
85
+ // Surface as console error; the player view will also emit onError
86
+ // once the Fabric event dispatcher picks it up.
87
+ // eslint-disable-next-line no-console
88
+ console.warn('[extended-vlc-player] replace failed:', error);
89
+ }
90
+ }, [normalized]);
91
+
92
+ // The player object is intentionally stable across renders. Methods are
93
+ // closures over `nativeId` so the latest normalized source is always
94
+ // referenced, but the object identity does not change — consumers can
95
+ // safely put it in dependency arrays.
96
+ const player = useMemo<ExtendedVlcPlayer>(() => {
97
+ const p: ExtendedVlcPlayer & { _nativeId: number } = {
98
+ _nativeId: nativeId,
99
+ play: () => getModule().play(nativeId),
100
+ pause: () => getModule().pause(nativeId),
101
+ stop: () => getModule().stop(nativeId),
102
+ seek: (seconds: number) => getModule().seek(nativeId, seconds),
103
+ setRate: (rate: number) => getModule().setRate(nativeId, rate),
104
+ setVolume: (volume: number) => getModule().setVolume(nativeId, volume),
105
+ setAudioTrack: (index: number) => getModule().setAudioTrack(nativeId, index),
106
+ setSubtitleTrack: (index: number) => getModule().setSubtitleTrack(nativeId, index),
107
+ replace: (next: ExtendedVlcSource) => {
108
+ const nextNormalized = normalizeSource(next);
109
+ try {
110
+ getModule().replace({
111
+ ...nextNormalized,
112
+ // Tag the replace with the current nativeId so the existing
113
+ // instance is updated rather than a new one allocated.
114
+ instanceId: nativeId,
115
+ });
116
+ } catch (error) {
117
+ // eslint-disable-next-line no-console
118
+ console.warn('[extended-vlc-player] replace failed:', error);
119
+ }
120
+ },
121
+ startPictureInPicture: () => getModule().startPictureInPicture(nativeId),
122
+ stopPictureInPicture: () => getModule().stopPictureInPicture(nativeId),
123
+ isPictureInPictureActive: () => getModule().isPictureInPictureActive(nativeId),
124
+ isPictureInPictureSupported: () => getModule().isPictureInPictureSupported(),
125
+ };
126
+ return p;
127
+ // eslint-disable-next-line react-hooks/exhaustive-deps
128
+ }, [nativeId]);
129
+
130
+ // Bridge the native callback API. The hook surfaces the player as soon
131
+ // as it exists, so consumers can attach PiP handlers from `onReady`.
132
+ useEffect(() => {
133
+ if (options.onReady) {
134
+ try {
135
+ options.onReady(player);
136
+ } catch (error) {
137
+ // eslint-disable-next-line no-console
138
+ console.warn('[extended-vlc-player] onReady handler threw:', error);
139
+ }
140
+ }
141
+ }, [player, options]);
142
+
143
+ // Bump the native id once on mount so the first source prop is committed
144
+ // to the native side. We use a ref-based "first" instead of an effect to
145
+ // avoid an extra render.
146
+ const firstRef = useRef(true);
147
+ useEffect(() => {
148
+ if (firstRef.current) {
149
+ firstRef.current = false;
150
+ setNativeId(1);
151
+ }
152
+ }, []);
153
+
154
+ // Mark unused variables to keep TypeScript happy with strict checks.
155
+ void useCallback;
156
+ void isMountedRef;
157
+ void (undefined as ExtendedVlcLoadEvent | ExtendedVlcProgressEvent | undefined);
158
+
159
+ return player;
160
+ }