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.
- package/LICENSE +21 -0
- package/README.md +59 -0
- package/android/build.gradle +36 -0
- package/android/src/main/AndroidManifest.xml +16 -0
- package/android/src/main/java/expo/modules/extendedvlcplayer/ExtendedVlcPlayerModule.kt +99 -0
- package/android/src/main/java/expo/modules/extendedvlcplayer/ExtendedVlcPlayerViewComponentView.kt +82 -0
- package/android/src/main/java/expo/modules/extendedvlcplayer/PlayerRegistry.kt +47 -0
- package/android/src/main/java/expo/modules/extendedvlcplayer/PlayerSession.kt +168 -0
- package/app.plugin.js +144 -0
- package/build/ExtendedVlcPlayerView.d.ts +8 -0
- package/build/ExtendedVlcPlayerView.d.ts.map +1 -0
- package/build/ExtendedVlcPlayerView.js +30 -0
- package/build/ExtendedVlcPlayerView.js.map +1 -0
- package/build/index.d.ts +12 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +11 -0
- package/build/index.js.map +1 -0
- package/build/types.d.ts +97 -0
- package/build/types.d.ts.map +1 -0
- package/build/types.js +2 -0
- package/build/types.js.map +1 -0
- package/build/useExtendedVlcPlayer.d.ts +12 -0
- package/build/useExtendedVlcPlayer.d.ts.map +1 -0
- package/build/useExtendedVlcPlayer.js +137 -0
- package/build/useExtendedVlcPlayer.js.map +1 -0
- package/expo-module.config.json +9 -0
- package/ios/AudioSessionConfigurator.swift +25 -0
- package/ios/ExtendedVlcPlayer.podspec +32 -0
- package/ios/ExtendedVlcPlayerModule.swift +130 -0
- package/ios/ExtendedVlcPlayerViewComponentView.h +19 -0
- package/ios/ExtendedVlcPlayerViewComponentView.mm +101 -0
- package/ios/PipBridge.swift +144 -0
- package/ios/PlayerRegistryBridge.swift +67 -0
- package/ios/PlayerSession.swift +335 -0
- package/package.json +65 -0
- package/src/ExtendedVlcPlayerView.tsx +68 -0
- package/src/index.ts +20 -0
- package/src/types.ts +100 -0
- 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
|
+
}
|