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
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ExtendedVlcPlayerView.d.ts","sourceRoot":"","sources":["../src/ExtendedVlcPlayerView.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAG/B,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,SAAS,CAAC;AAI1D;;;GAGG;AACH,eAAO,MAAM,qBAAqB,4FA6CjC,CAAC"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { requireNativeViewManager } from 'expo-modules-core';
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
import { Platform } from 'react-native';
|
|
4
|
+
const NativeView = requireNativeViewManager('ExtendedVlcPlayerView');
|
|
5
|
+
/**
|
|
6
|
+
* Drop-in replacement for `expo-video`'s `VideoView`. Renders the VLC-backed
|
|
7
|
+
* native player and forwards events back to the JS callbacks.
|
|
8
|
+
*/
|
|
9
|
+
export const ExtendedVlcPlayerView = React.forwardRef(function ExtendedVlcPlayerView(props, ref) {
|
|
10
|
+
const { player, style, contentFit = 'contain', onLoad, onProgress, onPlaying, onPaused, onEnded, onError, onBuffering, onPictureInPictureStart, onPictureInPictureStop, } = props;
|
|
11
|
+
// The `player` object carries the nativeId internally (the hook bumps
|
|
12
|
+
// it on first render). The native view reads it via a getter so we
|
|
13
|
+
// don't have to thread the number through props.
|
|
14
|
+
const nativeId = player._nativeId ?? 0;
|
|
15
|
+
return (<NativeView ref={ref} style={style} player={nativeId} contentFit={contentFit}
|
|
16
|
+
// Standard Fabric "bubbling event" props. They are passed as
|
|
17
|
+
// RCTBubblingEventBlock on the native side; the Swift
|
|
18
|
+
// PlayerSession fires the corresponding closures which the
|
|
19
|
+
// PlayerRegistryBridge forwards back to the view component,
|
|
20
|
+
// which then invokes the right block.
|
|
21
|
+
onLoad={onLoad} onProgress={onProgress} onPlaying={onPlaying} onPaused={onPaused} onEnded={onEnded} onError={onError} onBuffering={onBuffering} onPictureInPictureStart={onPictureInPictureStart} onPictureInPictureStop={onPictureInPictureStop}/>);
|
|
22
|
+
});
|
|
23
|
+
// Surface a helpful message on platforms where the view manager is not
|
|
24
|
+
// registered (e.g. web). The real native view manager is registered by
|
|
25
|
+
// the iOS / Android sides.
|
|
26
|
+
if (Platform.OS !== 'ios' && Platform.OS !== 'android') {
|
|
27
|
+
// eslint-disable-next-line no-console
|
|
28
|
+
console.warn('[extended-vlc-player] ExtendedVlcPlayerView is only supported on iOS and Android.');
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=ExtendedVlcPlayerView.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ExtendedVlcPlayerView.js","sourceRoot":"","sources":["../src/ExtendedVlcPlayerView.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,wBAAwB,EAAE,MAAM,mBAAmB,CAAC;AAC7D,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAC/B,OAAO,EAAE,QAAQ,EAAkB,MAAM,cAAc,CAAC;AAIxD,MAAM,UAAU,GAAG,wBAAwB,CAAC,uBAAuB,CAAC,CAAC;AAErE;;;GAGG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG,KAAK,CAAC,UAAU,CACnD,SAAS,qBAAqB,CAAC,KAAK,EAAE,GAAG;IACvC,MAAM,EACJ,MAAM,EACN,KAAK,EACL,UAAU,GAAG,SAAS,EACtB,MAAM,EACN,UAAU,EACV,SAAS,EACT,QAAQ,EACR,OAAO,EACP,OAAO,EACP,WAAW,EACX,uBAAuB,EACvB,sBAAsB,GACvB,GAAG,KAAK,CAAC;IAEV,sEAAsE;IACtE,mEAAmE;IACnE,iDAAiD;IACjD,MAAM,QAAQ,GAAI,MAA4C,CAAC,SAAS,IAAI,CAAC,CAAC;IAE9E,OAAO,CACL,CAAC,UAAU,CACT,GAAG,CAAC,CAAC,GAAG,CAAC,CACT,KAAK,CAAC,CAAC,KAAkB,CAAC,CAC1B,MAAM,CAAC,CAAC,QAAQ,CAAC,CACjB,UAAU,CAAC,CAAC,UAAU,CAAC;IACvB,6DAA6D;IAC7D,sDAAsD;IACtD,2DAA2D;IAC3D,4DAA4D;IAC5D,sCAAsC;IACtC,MAAM,CAAC,CAAC,MAAM,CAAC,CACf,UAAU,CAAC,CAAC,UAAU,CAAC,CACvB,SAAS,CAAC,CAAC,SAAS,CAAC,CACrB,QAAQ,CAAC,CAAC,QAAQ,CAAC,CACnB,OAAO,CAAC,CAAC,OAAO,CAAC,CACjB,OAAO,CAAC,CAAC,OAAO,CAAC,CACjB,WAAW,CAAC,CAAC,WAAW,CAAC,CACzB,uBAAuB,CAAC,CAAC,uBAAuB,CAAC,CACjD,sBAAsB,CAAC,CAAC,sBAAsB,CAAC,EAC/C,CACH,CAAC;AACJ,CAAC,CACF,CAAC;AAEF,uEAAuE;AACvE,uEAAuE;AACvE,2BAA2B;AAC3B,IAAI,QAAQ,CAAC,EAAE,KAAK,KAAK,IAAI,QAAQ,CAAC,EAAE,KAAK,SAAS,EAAE,CAAC;IACvD,sCAAsC;IACtC,OAAO,CAAC,IAAI,CACV,mFAAmF,CACpF,CAAC;AACJ,CAAC","sourcesContent":["import { requireNativeViewManager } from 'expo-modules-core';\nimport * as React from 'react';\nimport { Platform, type ViewStyle } from 'react-native';\n\nimport type { ExtendedVlcPlayerViewProps } from './types';\n\nconst NativeView = requireNativeViewManager('ExtendedVlcPlayerView');\n\n/**\n * Drop-in replacement for `expo-video`'s `VideoView`. Renders the VLC-backed\n * native player and forwards events back to the JS callbacks.\n */\nexport const ExtendedVlcPlayerView = React.forwardRef<unknown, ExtendedVlcPlayerViewProps>(\n function ExtendedVlcPlayerView(props, ref) {\n const {\n player,\n style,\n contentFit = 'contain',\n onLoad,\n onProgress,\n onPlaying,\n onPaused,\n onEnded,\n onError,\n onBuffering,\n onPictureInPictureStart,\n onPictureInPictureStop,\n } = props;\n\n // The `player` object carries the nativeId internally (the hook bumps\n // it on first render). The native view reads it via a getter so we\n // don't have to thread the number through props.\n const nativeId = (player as unknown as { _nativeId?: number })._nativeId ?? 0;\n\n return (\n <NativeView\n ref={ref}\n style={style as ViewStyle}\n player={nativeId}\n contentFit={contentFit}\n // Standard Fabric \"bubbling event\" props. They are passed as\n // RCTBubblingEventBlock on the native side; the Swift\n // PlayerSession fires the corresponding closures which the\n // PlayerRegistryBridge forwards back to the view component,\n // which then invokes the right block.\n onLoad={onLoad}\n onProgress={onProgress}\n onPlaying={onPlaying}\n onPaused={onPaused}\n onEnded={onEnded}\n onError={onError}\n onBuffering={onBuffering}\n onPictureInPictureStart={onPictureInPictureStart}\n onPictureInPictureStop={onPictureInPictureStop}\n />\n );\n }\n);\n\n// Surface a helpful message on platforms where the view manager is not\n// registered (e.g. web). The real native view manager is registered by\n// the iOS / Android sides.\nif (Platform.OS !== 'ios' && Platform.OS !== 'android') {\n // eslint-disable-next-line no-console\n console.warn(\n '[extended-vlc-player] ExtendedVlcPlayerView is only supported on iOS and Android.'\n );\n}\n"]}
|
package/build/index.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
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 { ContentFit, ExtendedVlcErrorEvent, ExtendedVlcLoadEvent, ExtendedVlcPlayer, ExtendedVlcPlayerViewProps, ExtendedVlcProgressEvent, ExtendedVlcSource, ExtendedVlcTrack, } from './types';
|
|
12
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EAAE,qBAAqB,EAAE,MAAM,yBAAyB,CAAC;AAChE,OAAO,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AAC9D,YAAY,EACV,UAAU,EACV,qBAAqB,EACrB,oBAAoB,EACpB,iBAAiB,EACjB,0BAA0B,EAC1B,wBAAwB,EACxB,iBAAiB,EACjB,gBAAgB,GACjB,MAAM,SAAS,CAAC"}
|
package/build/index.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
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
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EAAE,qBAAqB,EAAE,MAAM,yBAAyB,CAAC;AAChE,OAAO,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC","sourcesContent":["/**\n * `extended-vlc-player` public API.\n *\n * The default export is the native view component; named exports are the\n * imperative player hook and the type contracts. Consumers normally use:\n *\n * import { useExtendedVlcPlayer, ExtendedVlcPlayerView } from 'extended-vlc-player';\n */\nexport { ExtendedVlcPlayerView } from './ExtendedVlcPlayerView';\nexport { useExtendedVlcPlayer } from './useExtendedVlcPlayer';\nexport type {\n ContentFit,\n ExtendedVlcErrorEvent,\n ExtendedVlcLoadEvent,\n ExtendedVlcPlayer,\n ExtendedVlcPlayerViewProps,\n ExtendedVlcProgressEvent,\n ExtendedVlcSource,\n ExtendedVlcTrack,\n} from './types';\n"]}
|
package/build/types.d.ts
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type { ViewStyle } from 'react-native';
|
|
2
|
+
/**
|
|
3
|
+
* Source descriptor for the VLC player. Mirrors `expo-video`'s `VideoSource`
|
|
4
|
+
* surface so the existing call sites (a string URL or an object) keep
|
|
5
|
+
* working without change.
|
|
6
|
+
*/
|
|
7
|
+
export type ExtendedVlcSource = string | {
|
|
8
|
+
uri: string;
|
|
9
|
+
headers?: Record<string, string>;
|
|
10
|
+
/**
|
|
11
|
+
* Optional drm descriptor. NOTE: the new module does not decrypt
|
|
12
|
+
* Widevine/FairPlay streams (libVLC has no DRM path). The field is
|
|
13
|
+
* accepted for API parity with `expo-video`; if a DRM source reaches
|
|
14
|
+
* the player it will emit `onError` and the caller should fall back
|
|
15
|
+
* to `expo-video`.
|
|
16
|
+
*/
|
|
17
|
+
drm?: unknown;
|
|
18
|
+
};
|
|
19
|
+
/** Audio / subtitle track entry exposed by VLC after a source loads. */
|
|
20
|
+
export interface ExtendedVlcTrack {
|
|
21
|
+
index: number;
|
|
22
|
+
/** Localised label, e.g. "English", "Türkçe 5.1". */
|
|
23
|
+
label?: string;
|
|
24
|
+
/** RFC 5646 language code when VLC exposes one. */
|
|
25
|
+
language?: string;
|
|
26
|
+
/** Codec identifier reported by libVLC (e.g. "mp4a", "ac3", "subrip"). */
|
|
27
|
+
codec?: string;
|
|
28
|
+
}
|
|
29
|
+
export interface ExtendedVlcLoadEvent {
|
|
30
|
+
duration: number;
|
|
31
|
+
audioTracks: ExtendedVlcTrack[];
|
|
32
|
+
textTracks: ExtendedVlcTrack[];
|
|
33
|
+
}
|
|
34
|
+
export interface ExtendedVlcProgressEvent {
|
|
35
|
+
currentTime: number;
|
|
36
|
+
duration: number;
|
|
37
|
+
/** 0..1 position; mirrors the legacy VLC position field. */
|
|
38
|
+
position: number;
|
|
39
|
+
}
|
|
40
|
+
export interface ExtendedVlcErrorEvent {
|
|
41
|
+
code?: string;
|
|
42
|
+
message: string;
|
|
43
|
+
domain?: string;
|
|
44
|
+
}
|
|
45
|
+
export type ContentFit = 'contain' | 'cover' | 'fill';
|
|
46
|
+
export interface ExtendedVlcPlayerViewProps {
|
|
47
|
+
/** The player object returned by `useExtendedVlcPlayer`. */
|
|
48
|
+
player: ExtendedVlcPlayer;
|
|
49
|
+
style?: ViewStyle;
|
|
50
|
+
contentFit?: ContentFit;
|
|
51
|
+
onLoad?: (e: ExtendedVlcLoadEvent) => void;
|
|
52
|
+
onProgress?: (e: ExtendedVlcProgressEvent) => void;
|
|
53
|
+
onPlaying?: (e: {
|
|
54
|
+
duration: number;
|
|
55
|
+
}) => void;
|
|
56
|
+
onPaused?: (e: {
|
|
57
|
+
target: number;
|
|
58
|
+
}) => void;
|
|
59
|
+
onEnded?: () => void;
|
|
60
|
+
onError?: (e: ExtendedVlcErrorEvent) => void;
|
|
61
|
+
onBuffering?: (e: {
|
|
62
|
+
isBuffering: boolean;
|
|
63
|
+
}) => void;
|
|
64
|
+
onPictureInPictureStart?: () => void;
|
|
65
|
+
onPictureInPictureStop?: () => void;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Imperative player object. Mirrors the surface of `BackgroundVideoPlayer`'s
|
|
69
|
+
* `videoRef.current` so consumers can swap the import without touching
|
|
70
|
+
* the rest of their UI.
|
|
71
|
+
*/
|
|
72
|
+
export interface ExtendedVlcPlayer {
|
|
73
|
+
play(): void;
|
|
74
|
+
pause(): void;
|
|
75
|
+
stop(): void;
|
|
76
|
+
/** `seconds` is a wall-clock position in seconds (matches expo-video). */
|
|
77
|
+
seek(seconds: number): void;
|
|
78
|
+
/** `rate` is a playback rate multiplier (0.5, 1, 1.5, 2). */
|
|
79
|
+
setRate(rate: number): void;
|
|
80
|
+
/** `volume` is 0..1 (matches the existing VLC contract). */
|
|
81
|
+
setVolume(volume: number): void;
|
|
82
|
+
/** `index` of the audio track in the most recent `onLoad` payload, or -1 to disable. */
|
|
83
|
+
setAudioTrack(index: number): void;
|
|
84
|
+
/** `index` of the subtitle track, or -1 to disable. */
|
|
85
|
+
setSubtitleTrack(index: number): void;
|
|
86
|
+
/** Replace the current source without remounting the view. */
|
|
87
|
+
replace(source: ExtendedVlcSource): void;
|
|
88
|
+
/** Start the system Picture-in-Picture overlay. Resolves true when entered. */
|
|
89
|
+
startPictureInPicture(): Promise<boolean>;
|
|
90
|
+
/** Exit PiP if it is active. Resolves true when the controller stopped. */
|
|
91
|
+
stopPictureInPicture(): Promise<boolean>;
|
|
92
|
+
/** True while PiP is active. Mirrors the native state. */
|
|
93
|
+
isPictureInPictureActive(): Promise<boolean>;
|
|
94
|
+
/** True when the device + iOS version support PiP. */
|
|
95
|
+
isPictureInPictureSupported(): Promise<boolean>;
|
|
96
|
+
}
|
|
97
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAE9C;;;;GAIG;AACH,MAAM,MAAM,iBAAiB,GACzB,MAAM,GACN;IACE,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC;;;;;;OAMG;IACH,GAAG,CAAC,EAAE,OAAO,CAAC;CACf,CAAC;AAEN,wEAAwE;AACxE,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,qDAAqD;IACrD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,mDAAmD;IACnD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,0EAA0E;IAC1E,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,oBAAoB;IACnC,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,gBAAgB,EAAE,CAAC;IAChC,UAAU,EAAE,gBAAgB,EAAE,CAAC;CAChC;AAED,MAAM,WAAW,wBAAwB;IACvC,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,4DAA4D;IAC5D,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,qBAAqB;IACpC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,MAAM,UAAU,GAAG,SAAS,GAAG,OAAO,GAAG,MAAM,CAAC;AAEtD,MAAM,WAAW,0BAA0B;IACzC,4DAA4D;IAC5D,MAAM,EAAE,iBAAiB,CAAC;IAC1B,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB,MAAM,CAAC,EAAE,CAAC,CAAC,EAAE,oBAAoB,KAAK,IAAI,CAAC;IAC3C,UAAU,CAAC,EAAE,CAAC,CAAC,EAAE,wBAAwB,KAAK,IAAI,CAAC;IACnD,SAAS,CAAC,EAAE,CAAC,CAAC,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;IAC9C,QAAQ,CAAC,EAAE,CAAC,CAAC,EAAE;QAAE,MAAM,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;IAC3C,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,OAAO,CAAC,EAAE,CAAC,CAAC,EAAE,qBAAqB,KAAK,IAAI,CAAC;IAC7C,WAAW,CAAC,EAAE,CAAC,CAAC,EAAE;QAAE,WAAW,EAAE,OAAO,CAAA;KAAE,KAAK,IAAI,CAAC;IACpD,uBAAuB,CAAC,EAAE,MAAM,IAAI,CAAC;IACrC,sBAAsB,CAAC,EAAE,MAAM,IAAI,CAAC;CACrC;AAED;;;;GAIG;AACH,MAAM,WAAW,iBAAiB;IAChC,IAAI,IAAI,IAAI,CAAC;IACb,KAAK,IAAI,IAAI,CAAC;IACd,IAAI,IAAI,IAAI,CAAC;IACb,0EAA0E;IAC1E,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,6DAA6D;IAC7D,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,4DAA4D;IAC5D,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,wFAAwF;IACxF,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACnC,uDAAuD;IACvD,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACtC,8DAA8D;IAC9D,OAAO,CAAC,MAAM,EAAE,iBAAiB,GAAG,IAAI,CAAC;IACzC,+EAA+E;IAC/E,qBAAqB,IAAI,OAAO,CAAC,OAAO,CAAC,CAAC;IAC1C,2EAA2E;IAC3E,oBAAoB,IAAI,OAAO,CAAC,OAAO,CAAC,CAAC;IACzC,0DAA0D;IAC1D,wBAAwB,IAAI,OAAO,CAAC,OAAO,CAAC,CAAC;IAC7C,sDAAsD;IACtD,2BAA2B,IAAI,OAAO,CAAC,OAAO,CAAC,CAAC;CACjD"}
|
package/build/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"","sourcesContent":["import type { ViewStyle } from 'react-native';\n\n/**\n * Source descriptor for the VLC player. Mirrors `expo-video`'s `VideoSource`\n * surface so the existing call sites (a string URL or an object) keep\n * working without change.\n */\nexport type ExtendedVlcSource =\n | string\n | {\n uri: string;\n headers?: Record<string, string>;\n /**\n * Optional drm descriptor. NOTE: the new module does not decrypt\n * Widevine/FairPlay streams (libVLC has no DRM path). The field is\n * accepted for API parity with `expo-video`; if a DRM source reaches\n * the player it will emit `onError` and the caller should fall back\n * to `expo-video`.\n */\n drm?: unknown;\n };\n\n/** Audio / subtitle track entry exposed by VLC after a source loads. */\nexport interface ExtendedVlcTrack {\n index: number;\n /** Localised label, e.g. \"English\", \"Türkçe 5.1\". */\n label?: string;\n /** RFC 5646 language code when VLC exposes one. */\n language?: string;\n /** Codec identifier reported by libVLC (e.g. \"mp4a\", \"ac3\", \"subrip\"). */\n codec?: string;\n}\n\nexport interface ExtendedVlcLoadEvent {\n duration: number; // seconds\n audioTracks: ExtendedVlcTrack[];\n textTracks: ExtendedVlcTrack[];\n}\n\nexport interface ExtendedVlcProgressEvent {\n currentTime: number; // seconds\n duration: number; // seconds\n /** 0..1 position; mirrors the legacy VLC position field. */\n position: number;\n}\n\nexport interface ExtendedVlcErrorEvent {\n code?: string;\n message: string;\n domain?: string;\n}\n\nexport type ContentFit = 'contain' | 'cover' | 'fill';\n\nexport interface ExtendedVlcPlayerViewProps {\n /** The player object returned by `useExtendedVlcPlayer`. */\n player: ExtendedVlcPlayer;\n style?: ViewStyle;\n contentFit?: ContentFit;\n onLoad?: (e: ExtendedVlcLoadEvent) => void;\n onProgress?: (e: ExtendedVlcProgressEvent) => void;\n onPlaying?: (e: { duration: number }) => void;\n onPaused?: (e: { target: number }) => void;\n onEnded?: () => void;\n onError?: (e: ExtendedVlcErrorEvent) => void;\n onBuffering?: (e: { isBuffering: boolean }) => void;\n onPictureInPictureStart?: () => void;\n onPictureInPictureStop?: () => void;\n}\n\n/**\n * Imperative player object. Mirrors the surface of `BackgroundVideoPlayer`'s\n * `videoRef.current` so consumers can swap the import without touching\n * the rest of their UI.\n */\nexport interface ExtendedVlcPlayer {\n play(): void;\n pause(): void;\n stop(): void;\n /** `seconds` is a wall-clock position in seconds (matches expo-video). */\n seek(seconds: number): void;\n /** `rate` is a playback rate multiplier (0.5, 1, 1.5, 2). */\n setRate(rate: number): void;\n /** `volume` is 0..1 (matches the existing VLC contract). */\n setVolume(volume: number): void;\n /** `index` of the audio track in the most recent `onLoad` payload, or -1 to disable. */\n setAudioTrack(index: number): void;\n /** `index` of the subtitle track, or -1 to disable. */\n setSubtitleTrack(index: number): void;\n /** Replace the current source without remounting the view. */\n replace(source: ExtendedVlcSource): void;\n /** Start the system Picture-in-Picture overlay. Resolves true when entered. */\n startPictureInPicture(): Promise<boolean>;\n /** Exit PiP if it is active. Resolves true when the controller stopped. */\n stopPictureInPicture(): Promise<boolean>;\n /** True while PiP is active. Mirrors the native state. */\n isPictureInPictureActive(): Promise<boolean>;\n /** True when the device + iOS version support PiP. */\n isPictureInPictureSupported(): Promise<boolean>;\n}\n"]}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ExtendedVlcPlayer, ExtendedVlcSource } from './types';
|
|
2
|
+
export interface UseExtendedVlcPlayerOptions {
|
|
3
|
+
/** Called once after the player is constructed. Useful for attaching PiP handlers. */
|
|
4
|
+
onReady?: (player: ExtendedVlcPlayer) => void;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Returns a stable player object for the given source. The native player
|
|
8
|
+
* is created once (lazy on first method call) and replaced in place when
|
|
9
|
+
* `source` changes via the same `replace` API used by `expo-video`.
|
|
10
|
+
*/
|
|
11
|
+
export declare function useExtendedVlcPlayer(source: ExtendedVlcSource, options?: UseExtendedVlcPlayerOptions): ExtendedVlcPlayer;
|
|
12
|
+
//# sourceMappingURL=useExtendedVlcPlayer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useExtendedVlcPlayer.d.ts","sourceRoot":"","sources":["../src/useExtendedVlcPlayer.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAEV,iBAAiB,EAEjB,iBAAiB,EAClB,MAAM,SAAS,CAAC;AAqCjB,MAAM,WAAW,2BAA2B;IAC1C,sFAAsF;IACtF,OAAO,CAAC,EAAE,CAAC,MAAM,EAAE,iBAAiB,KAAK,IAAI,CAAC;CAC/C;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,iBAAiB,EACzB,OAAO,GAAE,2BAAgC,GACxC,iBAAiB,CAqGnB"}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
2
|
+
import { requireNativeModule } from 'expo-modules-core';
|
|
3
|
+
// Native module is registered by the platform-specific module under the
|
|
4
|
+
// JS module name "ExtendedVlcPlayer". The requireNativeModule helper resolves
|
|
5
|
+
// it at runtime; if the native side is missing, accessing the module throws
|
|
6
|
+
// — we surface a clear error so the consumer knows what failed instead of
|
|
7
|
+
// silently producing a no-op player.
|
|
8
|
+
let nativeModule = null;
|
|
9
|
+
try {
|
|
10
|
+
nativeModule = requireNativeModule('ExtendedVlcPlayer');
|
|
11
|
+
}
|
|
12
|
+
catch (error) {
|
|
13
|
+
// Allow JS-side import of the module to succeed even when the native side
|
|
14
|
+
// is not yet built (e.g. in a Jest test environment). We throw a helpful
|
|
15
|
+
// error only when the consumer actually calls a method.
|
|
16
|
+
nativeModule = null;
|
|
17
|
+
}
|
|
18
|
+
function getModule() {
|
|
19
|
+
if (!nativeModule) {
|
|
20
|
+
throw new Error('[extended-vlc-player] Native module is not available. ' +
|
|
21
|
+
'Run `npx expo prebuild --clean` and rebuild the app, ' +
|
|
22
|
+
'or check that the plugin ran successfully.');
|
|
23
|
+
}
|
|
24
|
+
return nativeModule;
|
|
25
|
+
}
|
|
26
|
+
function normalizeSource(source) {
|
|
27
|
+
if (typeof source === 'string')
|
|
28
|
+
return { uri: source };
|
|
29
|
+
const { uri } = source;
|
|
30
|
+
if (!uri) {
|
|
31
|
+
throw new Error('[extended-vlc-player] source.uri is required');
|
|
32
|
+
}
|
|
33
|
+
return { uri, headers: source.headers ?? null, drm: source.drm ?? null };
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Returns a stable player object for the given source. The native player
|
|
37
|
+
* is created once (lazy on first method call) and replaced in place when
|
|
38
|
+
* `source` changes via the same `replace` API used by `expo-video`.
|
|
39
|
+
*/
|
|
40
|
+
export function useExtendedVlcPlayer(source, options = {}) {
|
|
41
|
+
const normalized = useMemo(() => normalizeSource(source), [source]);
|
|
42
|
+
// We hold a numeric id of the "current native source" so we can detect
|
|
43
|
+
// when the JS source prop has changed and tell the native module to swap.
|
|
44
|
+
const [nativeId, setNativeId] = useState(0);
|
|
45
|
+
const isMountedRef = useRef(true);
|
|
46
|
+
// Event subscription is wired through the Fabric view component, not the
|
|
47
|
+
// module — events flow back into the JS player object via the
|
|
48
|
+
// ExtendedVlcPlayerView's callbacks. We keep the module call surface
|
|
49
|
+
// (play/pause/seek/PiP) here.
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
isMountedRef.current = true;
|
|
52
|
+
return () => {
|
|
53
|
+
isMountedRef.current = false;
|
|
54
|
+
};
|
|
55
|
+
}, []);
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
// When the source prop changes, ask the native module to swap in place
|
|
58
|
+
// instead of remounting the view (which would reset audio session, PiP
|
|
59
|
+
// delegate wiring, etc.).
|
|
60
|
+
try {
|
|
61
|
+
getModule().replace(normalized);
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
// Surface as console error; the player view will also emit onError
|
|
65
|
+
// once the Fabric event dispatcher picks it up.
|
|
66
|
+
// eslint-disable-next-line no-console
|
|
67
|
+
console.warn('[extended-vlc-player] replace failed:', error);
|
|
68
|
+
}
|
|
69
|
+
}, [normalized]);
|
|
70
|
+
// The player object is intentionally stable across renders. Methods are
|
|
71
|
+
// closures over `nativeId` so the latest normalized source is always
|
|
72
|
+
// referenced, but the object identity does not change — consumers can
|
|
73
|
+
// safely put it in dependency arrays.
|
|
74
|
+
const player = useMemo(() => {
|
|
75
|
+
const p = {
|
|
76
|
+
_nativeId: nativeId,
|
|
77
|
+
play: () => getModule().play(nativeId),
|
|
78
|
+
pause: () => getModule().pause(nativeId),
|
|
79
|
+
stop: () => getModule().stop(nativeId),
|
|
80
|
+
seek: (seconds) => getModule().seek(nativeId, seconds),
|
|
81
|
+
setRate: (rate) => getModule().setRate(nativeId, rate),
|
|
82
|
+
setVolume: (volume) => getModule().setVolume(nativeId, volume),
|
|
83
|
+
setAudioTrack: (index) => getModule().setAudioTrack(nativeId, index),
|
|
84
|
+
setSubtitleTrack: (index) => getModule().setSubtitleTrack(nativeId, index),
|
|
85
|
+
replace: (next) => {
|
|
86
|
+
const nextNormalized = normalizeSource(next);
|
|
87
|
+
try {
|
|
88
|
+
getModule().replace({
|
|
89
|
+
...nextNormalized,
|
|
90
|
+
// Tag the replace with the current nativeId so the existing
|
|
91
|
+
// instance is updated rather than a new one allocated.
|
|
92
|
+
instanceId: nativeId,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
// eslint-disable-next-line no-console
|
|
97
|
+
console.warn('[extended-vlc-player] replace failed:', error);
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
startPictureInPicture: () => getModule().startPictureInPicture(nativeId),
|
|
101
|
+
stopPictureInPicture: () => getModule().stopPictureInPicture(nativeId),
|
|
102
|
+
isPictureInPictureActive: () => getModule().isPictureInPictureActive(nativeId),
|
|
103
|
+
isPictureInPictureSupported: () => getModule().isPictureInPictureSupported(),
|
|
104
|
+
};
|
|
105
|
+
return p;
|
|
106
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
107
|
+
}, [nativeId]);
|
|
108
|
+
// Bridge the native callback API. The hook surfaces the player as soon
|
|
109
|
+
// as it exists, so consumers can attach PiP handlers from `onReady`.
|
|
110
|
+
useEffect(() => {
|
|
111
|
+
if (options.onReady) {
|
|
112
|
+
try {
|
|
113
|
+
options.onReady(player);
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
// eslint-disable-next-line no-console
|
|
117
|
+
console.warn('[extended-vlc-player] onReady handler threw:', error);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}, [player, options]);
|
|
121
|
+
// Bump the native id once on mount so the first source prop is committed
|
|
122
|
+
// to the native side. We use a ref-based "first" instead of an effect to
|
|
123
|
+
// avoid an extra render.
|
|
124
|
+
const firstRef = useRef(true);
|
|
125
|
+
useEffect(() => {
|
|
126
|
+
if (firstRef.current) {
|
|
127
|
+
firstRef.current = false;
|
|
128
|
+
setNativeId(1);
|
|
129
|
+
}
|
|
130
|
+
}, []);
|
|
131
|
+
// Mark unused variables to keep TypeScript happy with strict checks.
|
|
132
|
+
void useCallback;
|
|
133
|
+
void isMountedRef;
|
|
134
|
+
void undefined;
|
|
135
|
+
return player;
|
|
136
|
+
}
|
|
137
|
+
//# sourceMappingURL=useExtendedVlcPlayer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useExtendedVlcPlayer.js","sourceRoot":"","sources":["../src/useExtendedVlcPlayer.ts"],"names":[],"mappings":"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","sourcesContent":["import { useCallback, useEffect, useMemo, useRef, useState } from 'react';\nimport { requireNativeModule } from 'expo-modules-core';\n\nimport type {\n ExtendedVlcLoadEvent,\n ExtendedVlcPlayer,\n ExtendedVlcProgressEvent,\n ExtendedVlcSource,\n} from './types';\n\n// Native module is registered by the platform-specific module under the\n// JS module name \"ExtendedVlcPlayer\". The requireNativeModule helper resolves\n// it at runtime; if the native side is missing, accessing the module throws\n// — we surface a clear error so the consumer knows what failed instead of\n// silently producing a no-op player.\nlet nativeModule: any = null;\ntry {\n nativeModule = requireNativeModule('ExtendedVlcPlayer');\n} catch (error) {\n // Allow JS-side import of the module to succeed even when the native side\n // is not yet built (e.g. in a Jest test environment). We throw a helpful\n // error only when the consumer actually calls a method.\n nativeModule = null;\n}\n\nfunction getModule() {\n if (!nativeModule) {\n throw new Error(\n '[extended-vlc-player] Native module is not available. ' +\n 'Run `npx expo prebuild --clean` and rebuild the app, ' +\n 'or check that the plugin ran successfully.'\n );\n }\n return nativeModule;\n}\n\nfunction normalizeSource(source: ExtendedVlcSource) {\n if (typeof source === 'string') return { uri: source };\n const { uri } = source;\n if (!uri) {\n throw new Error('[extended-vlc-player] source.uri is required');\n }\n return { uri, headers: source.headers ?? null, drm: source.drm ?? null };\n}\n\nexport interface UseExtendedVlcPlayerOptions {\n /** Called once after the player is constructed. Useful for attaching PiP handlers. */\n onReady?: (player: ExtendedVlcPlayer) => void;\n}\n\n/**\n * Returns a stable player object for the given source. The native player\n * is created once (lazy on first method call) and replaced in place when\n * `source` changes via the same `replace` API used by `expo-video`.\n */\nexport function useExtendedVlcPlayer(\n source: ExtendedVlcSource,\n options: UseExtendedVlcPlayerOptions = {}\n): ExtendedVlcPlayer {\n const normalized = useMemo(() => normalizeSource(source), [source]);\n // We hold a numeric id of the \"current native source\" so we can detect\n // when the JS source prop has changed and tell the native module to swap.\n const [nativeId, setNativeId] = useState(0);\n const isMountedRef = useRef(true);\n\n // Event subscription is wired through the Fabric view component, not the\n // module — events flow back into the JS player object via the\n // ExtendedVlcPlayerView's callbacks. We keep the module call surface\n // (play/pause/seek/PiP) here.\n\n useEffect(() => {\n isMountedRef.current = true;\n return () => {\n isMountedRef.current = false;\n };\n }, []);\n\n useEffect(() => {\n // When the source prop changes, ask the native module to swap in place\n // instead of remounting the view (which would reset audio session, PiP\n // delegate wiring, etc.).\n try {\n getModule().replace(normalized);\n } catch (error) {\n // Surface as console error; the player view will also emit onError\n // once the Fabric event dispatcher picks it up.\n // eslint-disable-next-line no-console\n console.warn('[extended-vlc-player] replace failed:', error);\n }\n }, [normalized]);\n\n // The player object is intentionally stable across renders. Methods are\n // closures over `nativeId` so the latest normalized source is always\n // referenced, but the object identity does not change — consumers can\n // safely put it in dependency arrays.\n const player = useMemo<ExtendedVlcPlayer>(() => {\n const p: ExtendedVlcPlayer & { _nativeId: number } = {\n _nativeId: nativeId,\n play: () => getModule().play(nativeId),\n pause: () => getModule().pause(nativeId),\n stop: () => getModule().stop(nativeId),\n seek: (seconds: number) => getModule().seek(nativeId, seconds),\n setRate: (rate: number) => getModule().setRate(nativeId, rate),\n setVolume: (volume: number) => getModule().setVolume(nativeId, volume),\n setAudioTrack: (index: number) => getModule().setAudioTrack(nativeId, index),\n setSubtitleTrack: (index: number) => getModule().setSubtitleTrack(nativeId, index),\n replace: (next: ExtendedVlcSource) => {\n const nextNormalized = normalizeSource(next);\n try {\n getModule().replace({\n ...nextNormalized,\n // Tag the replace with the current nativeId so the existing\n // instance is updated rather than a new one allocated.\n instanceId: nativeId,\n });\n } catch (error) {\n // eslint-disable-next-line no-console\n console.warn('[extended-vlc-player] replace failed:', error);\n }\n },\n startPictureInPicture: () => getModule().startPictureInPicture(nativeId),\n stopPictureInPicture: () => getModule().stopPictureInPicture(nativeId),\n isPictureInPictureActive: () => getModule().isPictureInPictureActive(nativeId),\n isPictureInPictureSupported: () => getModule().isPictureInPictureSupported(),\n };\n return p;\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [nativeId]);\n\n // Bridge the native callback API. The hook surfaces the player as soon\n // as it exists, so consumers can attach PiP handlers from `onReady`.\n useEffect(() => {\n if (options.onReady) {\n try {\n options.onReady(player);\n } catch (error) {\n // eslint-disable-next-line no-console\n console.warn('[extended-vlc-player] onReady handler threw:', error);\n }\n }\n }, [player, options]);\n\n // Bump the native id once on mount so the first source prop is committed\n // to the native side. We use a ref-based \"first\" instead of an effect to\n // avoid an extra render.\n const firstRef = useRef(true);\n useEffect(() => {\n if (firstRef.current) {\n firstRef.current = false;\n setNativeId(1);\n }\n }, []);\n\n // Mark unused variables to keep TypeScript happy with strict checks.\n void useCallback;\n void isMountedRef;\n void (undefined as ExtendedVlcLoadEvent | ExtendedVlcProgressEvent | undefined);\n\n return player;\n}\n"]}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import AVFoundation
|
|
2
|
+
import Foundation
|
|
3
|
+
|
|
4
|
+
/// Centralizes the AVAudioSession configuration the player needs in
|
|
5
|
+
/// order to be eligible for background playback and Picture-in-Picture.
|
|
6
|
+
enum AudioSessionConfigurator {
|
|
7
|
+
/// Idempotent: setting the same category twice is a no-op.
|
|
8
|
+
static func configureForPlayback() {
|
|
9
|
+
do {
|
|
10
|
+
let session = AVAudioSession.sharedInstance()
|
|
11
|
+
try session.setCategory(
|
|
12
|
+
.playback,
|
|
13
|
+
mode: .moviePlayback,
|
|
14
|
+
options: [.allowAirPlay, .allowBluetoothA2DP]
|
|
15
|
+
)
|
|
16
|
+
try session.setActive(true, options: [])
|
|
17
|
+
} catch {
|
|
18
|
+
// Audio session configuration failures are non-fatal: the player
|
|
19
|
+
// still works in the foreground, but PiP may be denied by the
|
|
20
|
+
// system. Surface as console error so the issue is visible in
|
|
21
|
+
// development logs.
|
|
22
|
+
NSLog("[extended-vlc-player] AVAudioSession configuration failed: \(error)")
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
require 'json'
|
|
2
|
+
|
|
3
|
+
package = JSON.parse(File.read(File.join(__dir__, '..', 'package.json')))
|
|
4
|
+
|
|
5
|
+
Pod::Spec.new do |s|
|
|
6
|
+
s.name = 'ExtendedVlcPlayer'
|
|
7
|
+
s.version = package['version']
|
|
8
|
+
s.summary = package['description']
|
|
9
|
+
s.description = package['description']
|
|
10
|
+
s.license = 'MIT'
|
|
11
|
+
s.author = 'bbStudio'
|
|
12
|
+
s.homepage = 'https://github.com/berattufekli/iptv_expo'
|
|
13
|
+
s.platforms = {
|
|
14
|
+
:ios => '16.4',
|
|
15
|
+
:tvos => '16.4'
|
|
16
|
+
}
|
|
17
|
+
s.swift_version = '5.9'
|
|
18
|
+
s.source = { git: 'https://github.com/berattufekli/iptv_expo' }
|
|
19
|
+
s.static_framework = true
|
|
20
|
+
|
|
21
|
+
s.dependency 'ExpoModulesCore'
|
|
22
|
+
s.dependency 'MobileVLCKit', '~> 3.7.0'
|
|
23
|
+
|
|
24
|
+
s.source_files = '**/*.{h,m,mm,swift}'
|
|
25
|
+
s.pod_target_xcconfig = {
|
|
26
|
+
'DEFINES_MODULE' => 'YES',
|
|
27
|
+
'SWIFT_OBJC_INTERFACE_HEADER_NAME' => 'ExtendedVlcPlayer-Swift.h',
|
|
28
|
+
'OTHER_SWIFT_FLAGS' => '$(inherited) -D COCOAPODS -Xfrontend -module-name -Xfrontend ExtendedVlcPlayer',
|
|
29
|
+
# The host app's Podfile already enables use_frameworks! :linkage => :static
|
|
30
|
+
# so we keep this pod consistent with that mode.
|
|
31
|
+
}
|
|
32
|
+
end
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import AVFoundation
|
|
2
|
+
import ExpoModulesCore
|
|
3
|
+
import Foundation
|
|
4
|
+
import MobileVLCKit
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* TurboModule surface for `extended-vlc-player`.
|
|
8
|
+
*
|
|
9
|
+
* Owns the long-lived PlayerRegistry that maps JS player ids to
|
|
10
|
+
* PlayerSession instances. The Fabric view component creates sessions
|
|
11
|
+
* via the bridge (`PlayerRegistryBridge`) and the JS code interacts
|
|
12
|
+
* with a session through the imperative methods exposed here.
|
|
13
|
+
*/
|
|
14
|
+
public final class ExtendedVlcPlayerModule: Module {
|
|
15
|
+
public func definition() -> ModuleDefinition {
|
|
16
|
+
Name("ExtendedVlcPlayer")
|
|
17
|
+
|
|
18
|
+
OnCreate {
|
|
19
|
+
AudioSessionConfigurator.configureForPlayback()
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
AsyncFunction("isPictureInPictureSupported") { () -> Bool in
|
|
23
|
+
AVPictureInPictureController.isPictureInPictureSupported()
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
AsyncFunction("isPictureInPictureActive") { (_ instanceId: Int) -> Bool in
|
|
27
|
+
PlayerRegistry.shared.session(for: instanceId)?.isPictureInPictureActive() ?? false
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
AsyncFunction("play") { (instanceId: Int) in
|
|
31
|
+
PlayerRegistry.shared.session(for: instanceId)?.play()
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
AsyncFunction("pause") { (instanceId: Int) in
|
|
35
|
+
PlayerRegistry.shared.session(for: instanceId)?.pause()
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
AsyncFunction("stop") { (instanceId: Int) in
|
|
39
|
+
PlayerRegistry.shared.session(for: instanceId)?.stop()
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
AsyncFunction("seek") { (instanceId: Int, seconds: Double) in
|
|
43
|
+
PlayerRegistry.shared.session(for: instanceId)?.seek(to: seconds)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
AsyncFunction("setRate") { (instanceId: Int, rate: Double) in
|
|
47
|
+
PlayerRegistry.shared.session(for: instanceId)?.setRate(rate)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
AsyncFunction("setVolume") { (instanceId: Int, volume: Double) in
|
|
51
|
+
PlayerRegistry.shared.session(for: instanceId)?.setVolume(Float(max(0, min(1, volume))))
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
AsyncFunction("setAudioTrack") { (instanceId: Int, index: Int) in
|
|
55
|
+
PlayerRegistry.shared.session(for: instanceId)?.setAudioTrack(index: index)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
AsyncFunction("setSubtitleTrack") { (instanceId: Int, index: Int) in
|
|
59
|
+
PlayerRegistry.shared.session(for: instanceId)?.setSubtitleTrack(index: index)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
AsyncFunction("replace") { (instanceId: Int, source: ReplacePayload) in
|
|
63
|
+
let uri = source.uri
|
|
64
|
+
if uri.isEmpty {
|
|
65
|
+
throw InvalidSourceException()
|
|
66
|
+
}
|
|
67
|
+
let url: URL
|
|
68
|
+
if let parsed = URL(string: uri), parsed.scheme != nil {
|
|
69
|
+
url = parsed
|
|
70
|
+
} else {
|
|
71
|
+
// Some Xtream providers hand us a raw IP+path; default to https.
|
|
72
|
+
url = URL(string: "https://\(uri)") ?? URL(fileURLWithPath: uri)
|
|
73
|
+
}
|
|
74
|
+
let media = VLCMedia(url: url)
|
|
75
|
+
PlayerRegistry.shared.session(for: instanceId)?.replace(media: media)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
AsyncFunction("startPictureInPicture") { (instanceId: Int) -> Bool in
|
|
79
|
+
PlayerRegistry.shared.session(for: instanceId)?.startPictureInPicture() ?? false
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
AsyncFunction("stopPictureInPicture") { (instanceId: Int) -> Bool in
|
|
83
|
+
PlayerRegistry.shared.session(for: instanceId)?.stopPictureInPicture() ?? false
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
struct ReplacePayload: Record {
|
|
89
|
+
@Field var uri: String = ""
|
|
90
|
+
@Field var headers: [String: String]?
|
|
91
|
+
@Field var drm: Any?
|
|
92
|
+
@Field var instanceId: Int = 0
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
struct InvalidSourceException: Exception {}
|
|
96
|
+
|
|
97
|
+
/// Registry is shared between this module and the Obj-C++ Fabric view
|
|
98
|
+
/// (via `PlayerRegistryBridge`). The lock is held briefly to keep the
|
|
99
|
+
/// hot path fast.
|
|
100
|
+
final class PlayerRegistry {
|
|
101
|
+
static let shared = PlayerRegistry()
|
|
102
|
+
private let lock = NSLock()
|
|
103
|
+
private var sessions: [Int: PlayerSession] = [:]
|
|
104
|
+
private var nextId: Int = 0
|
|
105
|
+
|
|
106
|
+
func next() -> (Int, PlayerSession) {
|
|
107
|
+
lock.lock()
|
|
108
|
+
defer { lock.unlock() }
|
|
109
|
+
nextId += 1
|
|
110
|
+
let id = nextId
|
|
111
|
+
let session = PlayerSession(id: id)
|
|
112
|
+
sessions[id] = session
|
|
113
|
+
return (id, session)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
func session(for id: Int) -> PlayerSession? {
|
|
117
|
+
lock.lock()
|
|
118
|
+
defer { lock.unlock() }
|
|
119
|
+
return sessions[id]
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
func remove(id: Int) {
|
|
123
|
+
lock.lock()
|
|
124
|
+
defer { lock.unlock() }
|
|
125
|
+
if let session = sessions[id] {
|
|
126
|
+
session.stop()
|
|
127
|
+
}
|
|
128
|
+
sessions[id] = nil
|
|
129
|
+
}
|
|
130
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
#import <React/RCTViewComponentView.h>
|
|
2
|
+
#import <UIKit/UIKit.h>
|
|
3
|
+
|
|
4
|
+
NS_ASSUME_NONNULL_BEGIN
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Fabric view component for `ExtendedVlcPlayerView`. This is the
|
|
8
|
+
* Obj-C++ entry point that React Native's New Architecture uses to mount
|
|
9
|
+
* a native UIView.
|
|
10
|
+
*
|
|
11
|
+
* The actual UIView is a vanilla UIView that we set as the
|
|
12
|
+
* `VLCMediaPlayer.drawable` so libVLC draws into it directly. We don't
|
|
13
|
+
* subclass any specific VLC class because libVLC's drawable protocol is
|
|
14
|
+
* `UIView` — any UIView works.
|
|
15
|
+
*/
|
|
16
|
+
@interface ExtendedVlcPlayerViewComponentView : RCTViewComponentView
|
|
17
|
+
@end
|
|
18
|
+
|
|
19
|
+
NS_ASSUME_NONNULL_END
|