@cinecrew/cinecrew-player 0.1.1

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 (66) hide show
  1. package/CineCrewPlayer.podspec +22 -0
  2. package/LICENSE +21 -0
  3. package/NOTICE +6 -0
  4. package/README.md +574 -0
  5. package/android/build.gradle +48 -0
  6. package/android/proguard-rules.pro +29 -0
  7. package/android/src/main/AndroidManifest.xml +2 -0
  8. package/android/src/main/java/com/yuanzhou/vlc/ReactVlcPlayerPackage.java +28 -0
  9. package/android/src/main/java/com/yuanzhou/vlc/vlcplayer/ReactVlcPlayerView.java +988 -0
  10. package/android/src/main/java/com/yuanzhou/vlc/vlcplayer/ReactVlcPlayerViewManager.java +208 -0
  11. package/android/src/main/java/com/yuanzhou/vlc/vlcplayer/VideoEventEmitter.java +136 -0
  12. package/android/src/main/res/values/strings.xml +3 -0
  13. package/app.plugin.cjs +1 -0
  14. package/assets/cinecrew-player-logo.svg +44 -0
  15. package/package.json +132 -0
  16. package/packages/react-native-vlc-media-player/LICENSE +21 -0
  17. package/packages/react-native-vlc-media-player/VLCPlayer.js +207 -0
  18. package/packages/react-native-vlc-media-player/expo/android/withGradleTasks.cjs +49 -0
  19. package/packages/react-native-vlc-media-player/expo/ios/withMobileVlcKit.cjs +28 -0
  20. package/packages/react-native-vlc-media-player/expo/withVlcMediaPlayer.cjs +10 -0
  21. package/packages/react-native-vlc-media-player/index.d.ts +354 -0
  22. package/packages/react-native-vlc-media-player/ios/RCTVLCPlayer/RCTVLCPlayer.h +36 -0
  23. package/packages/react-native-vlc-media-player/ios/RCTVLCPlayer/RCTVLCPlayer.m +635 -0
  24. package/packages/react-native-vlc-media-player/ios/RCTVLCPlayer/RCTVLCPlayerManager.h +5 -0
  25. package/packages/react-native-vlc-media-player/ios/RCTVLCPlayer/RCTVLCPlayerManager.m +100 -0
  26. package/src/native/InlineLivePlayer.js +321 -0
  27. package/src/native/MediaPlayerView.js +3093 -0
  28. package/src/native/customization.js +44 -0
  29. package/src/native/index.js +107 -0
  30. package/src/native/media/ElectronVideoPlayer.js +288 -0
  31. package/src/native/media/EmojiPickerModal.js +30 -0
  32. package/src/native/media/LiveChatDrawer.js +1177 -0
  33. package/src/native/media/LiveRecordingOverlay.js +319 -0
  34. package/src/native/media/WebVideoPlayer.js +320 -0
  35. package/src/native/media/YouTubeVideoPlayer.native.js +120 -0
  36. package/src/native/media/YouTubeVideoPlayer.web.js +169 -0
  37. package/src/native/media/player/AudioOnlyView.js +337 -0
  38. package/src/native/media/player/CenterControls.js +94 -0
  39. package/src/native/media/player/ExoVideoFallback.js +329 -0
  40. package/src/native/media/player/PipOverlay.js +1 -0
  41. package/src/native/media/player/PlayerBottomBar.js +295 -0
  42. package/src/native/media/player/PlayerTopBar.js +255 -0
  43. package/src/native/media/player/VLCBoundary.js +41 -0
  44. package/src/native/media/player/VerticalIndicator.js +184 -0
  45. package/src/native/media/player/index.js +9 -0
  46. package/src/native/media/player/playerConstants.js +159 -0
  47. package/src/native/media/web/useWebAc3AudioPlayback.native.js +6 -0
  48. package/src/native/media/web/useWebAc3AudioPlayback.web.js +219 -0
  49. package/src/native/media/web/useWebHlsPlayback.native.js +3 -0
  50. package/src/native/media/web/useWebHlsPlayback.web.js +66 -0
  51. package/src/native/media/web/useWebMediaSession.js +90 -0
  52. package/src/native/media/web/useWebMpegTsPlayback.native.js +3 -0
  53. package/src/native/media/web/useWebMpegTsPlayback.web.js +356 -0
  54. package/src/native/media/web/useWebVideoAspectRatio.js +52 -0
  55. package/src/native/media/web/webPlaybackErrors.js +4 -0
  56. package/src/utils/layoutUtils.js +12 -0
  57. package/src/utils/mediaUtils.js +7 -0
  58. package/src/utils/recording.js +8 -0
  59. package/src/utils/runtimePlatform.js +19 -0
  60. package/src/utils/sourceUtils.js +99 -0
  61. package/src/utils/youtubeHtml.js +34 -0
  62. package/src/web/index.js +1129 -0
  63. package/src/web/styles.css +378 -0
  64. package/types/index.d.ts +268 -0
  65. package/vendor/mpegts.js/LICENSE +202 -0
  66. package/vendor/mpegts.js/mpegts.js +3 -0
@@ -0,0 +1,319 @@
1
+ import React, { useEffect, useRef } from 'react';
2
+ import { Animated, Easing, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
3
+ import { isElectron, isWeb } from '../../utils/runtimePlatform';
4
+ import { PlayerIcon } from '../customization';
5
+
6
+ const REC_RED = '#FF5252';
7
+ const DEFAULT_DARK_PALETTE = {
8
+ mode: 'dark', primary: '#00E5FF', outline: 'rgba(255,255,255,0.14)',
9
+ surface: '#12161C', surfaceHighlight: '#262B33',
10
+ onSurfacePrimary: '#FFFFFF', onSurfaceSecondary: 'rgba(255,255,255,0.68)',
11
+ };
12
+
13
+ function formatRecElapsed(elapsedMs = 0) {
14
+ const totalSeconds = Math.max(0, Math.floor(Number(elapsedMs) / 1000));
15
+ const hours = Math.floor(totalSeconds / 3600);
16
+ const minutes = Math.floor((totalSeconds % 3600) / 60);
17
+ const seconds = totalSeconds % 60;
18
+ return hours > 0
19
+ ? `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`
20
+ : `${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;
21
+ }
22
+
23
+ function resolveTheme(colors) {
24
+ const palette = colors?.onSurfacePrimary ? colors : DEFAULT_DARK_PALETTE;
25
+ const isLight = palette.mode === 'light';
26
+ return {
27
+ isLight,
28
+ barBg: isLight ? 'rgba(255,255,255,0.94)' : 'rgba(18,22,28,0.92)',
29
+ border: isLight ? palette.outline : 'rgba(255,82,82,0.45)',
30
+ text: palette.onSurfacePrimary,
31
+ muted: palette.onSurfaceSecondary,
32
+ btnBg: isLight ? palette.surfaceHighlight : 'rgba(255,255,255,0.08)',
33
+ stopBg: isLight ? 'rgba(255,82,82,0.12)' : 'rgba(255,82,82,0.22)',
34
+ noticeBg: isLight ? palette.surface : 'rgba(18,22,28,0.94)',
35
+ noticeBorder: palette.outline,
36
+ primary: palette.primary,
37
+ };
38
+ }
39
+
40
+ function RecDot({ paused }) {
41
+ const pulse = useRef(new Animated.Value(1)).current;
42
+
43
+ useEffect(() => {
44
+ if (paused) {
45
+ pulse.stopAnimation();
46
+ pulse.setValue(1);
47
+ return undefined;
48
+ }
49
+ const loop = Animated.loop(
50
+ Animated.sequence([
51
+ Animated.timing(pulse, {
52
+ toValue: 0.25,
53
+ duration: 650,
54
+ easing: Easing.inOut(Easing.quad),
55
+ useNativeDriver: true,
56
+ }),
57
+ Animated.timing(pulse, {
58
+ toValue: 1,
59
+ duration: 650,
60
+ easing: Easing.inOut(Easing.quad),
61
+ useNativeDriver: true,
62
+ }),
63
+ ]),
64
+ );
65
+ loop.start();
66
+ return () => loop.stop();
67
+ }, [paused, pulse]);
68
+
69
+ return (
70
+ <Animated.View
71
+ style={[
72
+ styles.dot,
73
+ { opacity: pulse, transform: [{ scale: pulse.interpolate({ inputRange: [0.25, 1], outputRange: [0.75, 1] }) }] },
74
+ ]}
75
+ />
76
+ );
77
+ }
78
+
79
+ export function LiveRecordingOverlay({
80
+ status,
81
+ elapsedMs,
82
+ colors,
83
+ topInset = 12,
84
+ showTransport = true,
85
+ onPause,
86
+ onResume,
87
+ onStop,
88
+ }) {
89
+ if (status !== 'recording' && status !== 'paused') return null;
90
+
91
+ const theme = resolveTheme(colors);
92
+ const paused = status === 'paused';
93
+
94
+ return (
95
+ <View
96
+ pointerEvents="box-none"
97
+ style={[styles.wrap, { top: Math.max(topInset, 10) }]}
98
+ >
99
+ <View
100
+ style={[
101
+ styles.bar,
102
+ {
103
+ backgroundColor: theme.barBg,
104
+ borderColor: paused ? theme.border : REC_RED,
105
+ },
106
+ ]}
107
+ >
108
+ <View style={styles.recLabel}>
109
+ <RecDot paused={paused} />
110
+ <Text style={[styles.recText, { color: REC_RED }]}>
111
+ {paused ? 'PAUSED' : 'REC'}
112
+ </Text>
113
+ <Text style={[styles.timeText, { color: theme.text }]}>
114
+ {formatRecElapsed(elapsedMs)}
115
+ </Text>
116
+ </View>
117
+
118
+ {showTransport ? (
119
+ <View style={styles.transport}>
120
+ <TouchableOpacity
121
+ style={[styles.roundBtn, { backgroundColor: theme.btnBg }]}
122
+ onPress={(e) => {
123
+ e?.stopPropagation?.();
124
+ if (paused) onResume?.();
125
+ else onPause?.();
126
+ }}
127
+ hitSlop={10}
128
+ accessibilityLabel={paused ? 'Resume recording' : 'Pause recording'}
129
+ >
130
+ <PlayerIcon
131
+ name={paused ? 'play' : 'pause'}
132
+ size={16}
133
+ color={theme.text}
134
+ />
135
+ </TouchableOpacity>
136
+ <TouchableOpacity
137
+ style={[styles.roundBtn, { backgroundColor: theme.stopBg }]}
138
+ onPress={(e) => {
139
+ e?.stopPropagation?.();
140
+ onStop?.();
141
+ }}
142
+ hitSlop={10}
143
+ accessibilityLabel="Stop recording"
144
+ >
145
+ <PlayerIcon name="stop" size={16} color={REC_RED} />
146
+ </TouchableOpacity>
147
+ </View>
148
+ ) : null}
149
+ </View>
150
+ </View>
151
+ );
152
+ }
153
+
154
+ export function LiveRecordingNotice({ notice, colors, onDismiss }) {
155
+ if (!notice) return null;
156
+ const theme = resolveTheme(colors);
157
+ const isError = notice.type === 'error';
158
+ let iconName = 'check-circle-outline';
159
+ if (isError) iconName = 'alert-circle-outline';
160
+ else if (notice.type === 'info') iconName = 'information-outline';
161
+ const accent = isError ? REC_RED : theme.primary;
162
+
163
+ return (
164
+ <TouchableOpacity
165
+ activeOpacity={0.9}
166
+ onPress={onDismiss}
167
+ style={[
168
+ styles.notice,
169
+ {
170
+ backgroundColor: theme.noticeBg,
171
+ borderColor: isError ? REC_RED : theme.primary,
172
+ },
173
+ ]}
174
+ >
175
+ <PlayerIcon
176
+ name={iconName}
177
+ size={18}
178
+ color={accent}
179
+ />
180
+ <Text style={[styles.noticeText, { color: theme.text }]} numberOfLines={2}>
181
+ {notice.message}
182
+ </Text>
183
+ <PlayerIcon name="close" size={16} color={theme.muted} />
184
+ </TouchableOpacity>
185
+ );
186
+ }
187
+
188
+ export function PipRecordingBadge({ status, elapsedMs }) {
189
+ if (status !== 'recording' && status !== 'paused') return null;
190
+ return (
191
+ <View style={[styles.pipRec, status === 'paused' && styles.pipRecPaused]}>
192
+ <View style={styles.pipDot} />
193
+ <Text style={styles.pipRecText}>
194
+ {status === 'paused' ? 'PAUSED' : `REC ${formatRecElapsed(elapsedMs)}`}
195
+ </Text>
196
+ </View>
197
+ );
198
+ }
199
+
200
+ const styles = StyleSheet.create({
201
+ wrap: {
202
+ position: 'absolute',
203
+ left: 0,
204
+ right: 0,
205
+ zIndex: 80,
206
+ alignItems: 'center',
207
+ pointerEvents: 'box-none',
208
+ },
209
+ bar: {
210
+ flexDirection: 'row',
211
+ alignItems: 'center',
212
+ gap: 10,
213
+ paddingLeft: 12,
214
+ paddingRight: 8,
215
+ paddingVertical: 6,
216
+ borderRadius: 22,
217
+ borderWidth: 1,
218
+ ...(isWeb() || isElectron()
219
+ ? {
220
+ boxShadow: '0 6px 18px rgba(0,0,0,0.28)',
221
+ }
222
+ : {
223
+ shadowColor: '#000',
224
+ shadowOffset: { width: 0, height: 4 },
225
+ shadowOpacity: 0.28,
226
+ shadowRadius: 10,
227
+ elevation: 8,
228
+ }),
229
+ },
230
+ recLabel: {
231
+ flexDirection: 'row',
232
+ alignItems: 'center',
233
+ gap: 8,
234
+ },
235
+ dot: {
236
+ width: 9,
237
+ height: 9,
238
+ borderRadius: 5,
239
+ backgroundColor: REC_RED,
240
+ },
241
+ recText: {
242
+ fontSize: 11,
243
+ fontWeight: '800',
244
+ letterSpacing: 1.2,
245
+ },
246
+ timeText: {
247
+ fontSize: 13,
248
+ fontWeight: '700',
249
+ fontVariant: ['tabular-nums'],
250
+ minWidth: 42,
251
+ },
252
+ transport: {
253
+ flexDirection: 'row',
254
+ alignItems: 'center',
255
+ gap: 6,
256
+ },
257
+ roundBtn: {
258
+ width: 28,
259
+ height: 28,
260
+ borderRadius: 14,
261
+ alignItems: 'center',
262
+ justifyContent: 'center',
263
+ },
264
+ notice: {
265
+ position: 'absolute',
266
+ left: 16,
267
+ right: 16,
268
+ bottom: 28,
269
+ zIndex: 90,
270
+ flexDirection: 'row',
271
+ alignItems: 'center',
272
+ gap: 10,
273
+ paddingHorizontal: 14,
274
+ paddingVertical: 12,
275
+ borderRadius: 14,
276
+ borderWidth: 1,
277
+ ...(isWeb() || isElectron()
278
+ ? {
279
+ boxShadow: '0 8px 22px rgba(0,0,0,0.28)',
280
+ }
281
+ : {
282
+ shadowColor: '#000',
283
+ shadowOffset: { width: 0, height: 4 },
284
+ shadowOpacity: 0.25,
285
+ shadowRadius: 10,
286
+ elevation: 8,
287
+ }),
288
+ },
289
+ noticeText: {
290
+ flex: 1,
291
+ fontSize: 13,
292
+ fontWeight: '600',
293
+ lineHeight: 18,
294
+ },
295
+ pipRec: {
296
+ flexDirection: 'row',
297
+ alignItems: 'center',
298
+ backgroundColor: REC_RED,
299
+ paddingHorizontal: 5,
300
+ paddingVertical: 1.5,
301
+ borderRadius: 4,
302
+ gap: 3,
303
+ },
304
+ pipRecPaused: {
305
+ backgroundColor: '#C77700',
306
+ },
307
+ pipDot: {
308
+ width: 4,
309
+ height: 4,
310
+ borderRadius: 2,
311
+ backgroundColor: '#FFF',
312
+ },
313
+ pipRecText: {
314
+ color: '#FFF',
315
+ fontSize: 8,
316
+ fontWeight: '800',
317
+ letterSpacing: 0.3,
318
+ },
319
+ });
@@ -0,0 +1,320 @@
1
+ import { forwardRef, useImperativeHandle, useRef, useEffect, useCallback, useState } from 'react';
2
+ import { View, StyleSheet } from 'react-native';
3
+ import { useWebMediaSession } from './web/useWebMediaSession';
4
+ import { useWebVideoAspectRatio } from './web/useWebVideoAspectRatio';
5
+ import { useWebMpegTsPlayback } from './web/useWebMpegTsPlayback';
6
+ import { useWebHlsPlayback } from './web/useWebHlsPlayback';
7
+ import { useWebAc3AudioPlayback } from './web/useWebAc3AudioPlayback';
8
+
9
+ export const WebVideoPlayer = forwardRef(({
10
+ streamUrl,
11
+ paused,
12
+ muted,
13
+ volume = 100,
14
+ playbackRate = 1,
15
+ videoAspectRatio,
16
+ title,
17
+ posterUrl,
18
+ isLive,
19
+ audioOnly = false,
20
+ videoOnly = false,
21
+ captions = [],
22
+ onProgress,
23
+ onPlaying,
24
+ onBuffering,
25
+ onError,
26
+ onEnded,
27
+ onTogglePlayPause,
28
+ onSeekBy,
29
+ onPlaybackRoute,
30
+ }, ref) => {
31
+ const videoRef = useRef(null);
32
+ const pausedRef = useRef(paused);
33
+ const playbackRateRef = useRef(playbackRate);
34
+ const lastKnownPosRef = useRef(0);
35
+ const onPlaybackRouteRef = useRef(onPlaybackRoute);
36
+ const onPlayingRef = useRef(onPlaying);
37
+ const onBufferingRef = useRef(onBuffering);
38
+ const onErrorRef = useRef(onError);
39
+ const playbackErrorRef = useRef(onError);
40
+ const onEndedRef = useRef(onEnded);
41
+ const [activeUrl, setActiveUrl] = useState('');
42
+ pausedRef.current = paused;
43
+ playbackRateRef.current = playbackRate;
44
+ onPlaybackRouteRef.current = onPlaybackRoute;
45
+ onPlayingRef.current = onPlaying;
46
+ onBufferingRef.current = onBuffering;
47
+ onErrorRef.current = onError;
48
+ onEndedRef.current = onEnded;
49
+
50
+ const handlePlaybackError = useCallback((error) => {
51
+ onErrorRef.current?.(error);
52
+ }, []);
53
+ playbackErrorRef.current = handlePlaybackError;
54
+
55
+ const mpegTsPlayback = useWebMpegTsPlayback({
56
+ streamUrl,
57
+ activeUrl,
58
+ isLive,
59
+ videoOnly,
60
+ videoRef,
61
+ pausedRef,
62
+ onErrorRef: playbackErrorRef,
63
+ onBufferingRef,
64
+ });
65
+ const useMpegTsSource = mpegTsPlayback.useMpegTs;
66
+ const useAc3Fallback = mpegTsPlayback.useAc3Fallback;
67
+ const useHlsSource = useWebHlsPlayback({
68
+ activeUrl,
69
+ isLive,
70
+ videoRef,
71
+ pausedRef,
72
+ onErrorRef: playbackErrorRef,
73
+ });
74
+ const ac3Audio = useWebAc3AudioPlayback({
75
+ active: useAc3Fallback,
76
+ streamUrl: activeUrl,
77
+ enabled: !muted,
78
+ paused,
79
+ volume,
80
+ videoRef,
81
+ });
82
+
83
+ // 1. Aspect Ratio Styling
84
+ const { videoStyle, applyAspectRatio } = useWebVideoAspectRatio(videoAspectRatio, audioOnly);
85
+ const videoSource = useMpegTsSource || useHlsSource ? undefined : activeUrl;
86
+ let streamMode = useHlsSource ? 'hls' : 'native';
87
+ if (useMpegTsSource) streamMode = 'mpegts';
88
+ const resolvedScheme = new URL(activeUrl || 'http://localhost', 'http://localhost').protocol.replace(':', '');
89
+
90
+ // Report active streaming route
91
+ const reportPlaybackRoute = useCallback((video, attachedUrl) => {
92
+ const emit = onPlaybackRouteRef.current;
93
+ if (typeof emit !== 'function') return;
94
+ const current = video?.currentSrc || video?.src || '';
95
+ const httpCurrent = /^https?:/i.test(current) && !current.startsWith('blob:') ? current : '';
96
+ const raw = httpCurrent || attachedUrl || '';
97
+ if (raw) emit(raw);
98
+ }, []);
99
+
100
+ // Progress emitter
101
+ const emitProgress = useCallback(() => {
102
+ const video = videoRef.current;
103
+ if (!video) return;
104
+
105
+ const curTime = video.currentTime || 0;
106
+ const duration = video.duration || 0;
107
+
108
+ if (Number.isFinite(curTime)) {
109
+ lastKnownPosRef.current = curTime;
110
+ }
111
+
112
+ if (typeof onProgress === 'function') {
113
+ onProgress({
114
+ currentTime: curTime * 1000,
115
+ duration: (Number.isFinite(duration) && duration > 0 ? duration : 0) * 1000,
116
+ target: curTime,
117
+ });
118
+ }
119
+ }, [onProgress]);
120
+
121
+ // The host supplies the media URL. Scheme and source resolution are left to
122
+ // the consumer and the playback engine on each platform.
123
+ useEffect(() => {
124
+ if (!streamUrl) {
125
+ setActiveUrl('');
126
+ return undefined;
127
+ }
128
+ setActiveUrl(streamUrl);
129
+ return undefined;
130
+ }, [streamUrl]);
131
+
132
+ // MediaSession API
133
+ useWebMediaSession({
134
+ title,
135
+ posterUrl,
136
+ isLive,
137
+ paused,
138
+ videoRef,
139
+ pausedRef,
140
+ lastKnownPosRef,
141
+ onTogglePlayPause,
142
+ onSeekBy,
143
+ });
144
+
145
+ // Sync play/pause prop changes
146
+ useEffect(() => {
147
+ const video = videoRef.current;
148
+ if (!video) return;
149
+
150
+ if (paused) {
151
+ video.pause();
152
+ } else if (
153
+ video.dataset.mpegtsCodecGate !== 'pending'
154
+ && video.paused
155
+ && (video.src || video.currentSrc)
156
+ ) {
157
+ video.play().catch(() => {});
158
+ }
159
+ }, [paused, activeUrl]);
160
+
161
+ // Gain and volume
162
+ useEffect(() => {
163
+ const video = videoRef.current;
164
+ if (!video) return;
165
+
166
+ try {
167
+ video.muted = muted;
168
+ video.volume = Math.max(0, Math.min(1, (volume ?? 100) / 100));
169
+ } catch {}
170
+
171
+ }, [muted, volume, activeUrl]);
172
+
173
+ // Playback rate
174
+ useEffect(() => {
175
+ const video = videoRef.current;
176
+ if (!video) return;
177
+ if (playbackRate) {
178
+ try {
179
+ video.playbackRate = playbackRate;
180
+ } catch {}
181
+ }
182
+ }, [playbackRate, activeUrl]);
183
+
184
+ // Imperative handle for parent component control
185
+ useImperativeHandle(ref, () => ({
186
+ reload() {
187
+ const video = videoRef.current;
188
+ if (video && activeUrl) {
189
+ video.currentTime = 0;
190
+ video.play().catch(() => {});
191
+ }
192
+ },
193
+ seek(ratio) {
194
+ const video = videoRef.current;
195
+ if (!video) return;
196
+ const duration = video.duration || 0;
197
+ if (Number.isFinite(duration) && duration > 0) {
198
+ video.currentTime = Math.max(0, Math.min(1, ratio)) * duration;
199
+ }
200
+ },
201
+ seekTo(seconds) {
202
+ const video = videoRef.current;
203
+ if (!video || !Number.isFinite(seconds)) return;
204
+ video.currentTime = seconds;
205
+ },
206
+ changeVideoAspectRatio(aspect) {
207
+ applyAspectRatio(aspect);
208
+ },
209
+ async requestPictureInPicture() {
210
+ const video = videoRef.current;
211
+ if (video && typeof video.requestPictureInPicture === 'function' && typeof document !== 'undefined' && document.pictureInPictureEnabled) {
212
+ return await video.requestPictureInPicture();
213
+ }
214
+ return null;
215
+ },
216
+ async exitPictureInPicture() {
217
+ if (typeof document !== 'undefined' && document.pictureInPictureElement && typeof document.exitPictureInPicture === 'function') {
218
+ return await document.exitPictureInPicture();
219
+ }
220
+ return null;
221
+ },
222
+ getVideoElement() {
223
+ return videoRef.current;
224
+ },
225
+ activateAudio(nextVolume) {
226
+ return ac3Audio.activateAudio(nextVolume);
227
+ },
228
+ deactivateAudio() {
229
+ ac3Audio.deactivateAudio();
230
+ },
231
+ }));
232
+
233
+ return (
234
+ <View style={styles.container} collapsable={false}>
235
+ {activeUrl ? (
236
+ <video
237
+ key={activeUrl}
238
+ ref={videoRef}
239
+ data-resolved-scheme={resolvedScheme}
240
+ data-stream-mode={streamMode}
241
+ src={videoSource}
242
+ autoPlay={!paused}
243
+ muted={muted}
244
+ playsInline
245
+ controls={false}
246
+ style={videoStyle}
247
+ onPlaying={() => {
248
+ const video = videoRef.current;
249
+ reportPlaybackRoute(video, activeUrl);
250
+ onPlayingRef.current?.();
251
+ onBufferingRef.current?.(false);
252
+ emitProgress();
253
+ }}
254
+ onWaiting={() => {
255
+ onBufferingRef.current?.(true);
256
+ }}
257
+ onPause={() => {
258
+ onBufferingRef.current?.(false);
259
+ }}
260
+ onTimeUpdate={emitProgress}
261
+ onLoadedMetadata={() => {
262
+ emitProgress();
263
+ reportPlaybackRoute(videoRef.current, activeUrl);
264
+ }}
265
+ onEnded={() => {
266
+ onEndedRef.current?.();
267
+ }}
268
+ onCanPlay={() => {
269
+ const video = videoRef.current;
270
+ if (video && video.dataset.mpegtsCodecGate !== 'pending' && !pausedRef.current && video.paused) {
271
+ video.play().catch(() => {});
272
+ }
273
+ }}
274
+ onError={(e) => {
275
+ const video = videoRef.current;
276
+ const mediaError = video?.error;
277
+ if (!video?.src && !video?.currentSrc) {
278
+ return;
279
+ }
280
+ if (video && video.readyState >= 2) {
281
+ return;
282
+ }
283
+ playbackErrorRef.current?.({
284
+ message: mediaError?.message || 'Failed to load stream.',
285
+ err: mediaError || e,
286
+ });
287
+ }}
288
+ >
289
+ <track
290
+ kind="captions"
291
+ src={captions[0]?.src || 'data:text/vtt,WEBVTT%0A%0A'}
292
+ srcLang={captions[0]?.language || 'und'}
293
+ label={captions[0]?.label || 'Captions'}
294
+ default={!captions.length || Boolean(captions[0]?.default)}
295
+ />
296
+ {captions.slice(1).map((caption, index) => (
297
+ <track
298
+ key={caption.id || caption.src || index}
299
+ kind="captions"
300
+ src={caption.src}
301
+ srcLang={caption.language || 'und'}
302
+ label={caption.label || caption.language || `Caption ${index + 1}`}
303
+ default={Boolean(caption.default)}
304
+ />
305
+ ))}
306
+ </video>
307
+ ) : null}
308
+ </View>
309
+ );
310
+ });
311
+
312
+ const styles = StyleSheet.create({
313
+ container: {
314
+ ...StyleSheet.absoluteFillObject,
315
+ backgroundColor: '#000',
316
+ justifyContent: 'center',
317
+ alignItems: 'center',
318
+ overflow: 'hidden',
319
+ },
320
+ });