mega-stream-proxy 0.1.9 → 0.2.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/README.md +14 -49
- package/dist/megaFile.js +22 -9
- package/dist/{Player/playerAndroid/MegaVideoPlayerAndroid.d.ts → player/MegaVideoPlayer.d.ts} +4 -13
- package/dist/player/MegaVideoPlayer.js +307 -0
- package/dist/{Player/playerweb → player}/PlayerControls.d.ts +2 -14
- package/dist/player/PlayerControls.js +289 -0
- package/dist/{Player/playerweb → player}/SubtitleDisplay.d.ts +3 -3
- package/dist/{Player/playerweb → player}/SubtitleDisplay.js +1 -1
- package/dist/player/index.d.ts +3 -0
- package/dist/player/index.js +3 -0
- package/dist/{Player/playerweb/MegaVideoPlayer.d.ts → playerweb/MegaVideoPlayerWeb.d.ts} +6 -18
- package/dist/{Player/playerweb/MegaVideoPlayer.js → playerweb/MegaVideoPlayerWeb.js} +18 -18
- package/dist/{Player/playerAndroid/PlayerControlsAndroid.d.ts → playerweb/PlayerControlsWeb.d.ts} +2 -16
- package/dist/playerweb/PlayerControlsWeb.js +289 -0
- package/dist/playerweb/SubtitleDisplayWeb.d.ts +15 -0
- package/dist/playerweb/SubtitleDisplayWeb.js +32 -0
- package/dist/playerweb/index.d.ts +3 -0
- package/dist/playerweb/index.js +3 -0
- package/dist/proxy/native.js +11 -3
- package/expo-module.config.json +3 -0
- package/package.json +15 -18
- package/src/megaFile.ts +28 -15
- package/src/player/MegaVideoPlayer.tsx +474 -0
- package/src/{Player/playerweb → player}/PlayerControls.tsx +200 -369
- package/src/{Player/playerweb → player}/SubtitleDisplay.tsx +3 -3
- package/src/player/index.ts +7 -0
- package/src/{Player/playerweb/MegaVideoPlayer.tsx → playerweb/MegaVideoPlayerWeb.tsx} +41 -58
- package/src/playerweb/PlayerControlsWeb.tsx +629 -0
- package/src/playerweb/SubtitleDisplayWeb.tsx +48 -0
- package/src/playerweb/index.ts +3 -0
- package/src/proxy/native.ts +10 -3
- package/dist/Player/native.d.ts +0 -3
- package/dist/Player/native.js +0 -3
- package/dist/Player/playerAndroid/MegaVideoPlayerAndroid.js +0 -624
- package/dist/Player/playerAndroid/PlayerControlsAndroid.js +0 -370
- package/dist/Player/playerAndroid/SubtitleDisplayAndroid.d.ts +0 -21
- package/dist/Player/playerAndroid/SubtitleDisplayAndroid.js +0 -55
- package/dist/Player/playerAndroid/index.d.ts +0 -3
- package/dist/Player/playerAndroid/index.js +0 -3
- package/dist/Player/playerweb/PlayerControls.js +0 -346
- package/dist/Player/playerweb/index.d.ts +0 -3
- package/dist/Player/playerweb/index.js +0 -3
- package/dist/Player/web.d.ts +0 -3
- package/dist/Player/web.js +0 -3
- package/src/Player/playerAndroid/MegaVideoPlayerAndroid.tsx +0 -825
- package/src/Player/playerAndroid/PlayerControlsAndroid.tsx +0 -824
- package/src/Player/playerAndroid/SubtitleDisplayAndroid.tsx +0 -80
- package/src/Player/playerAndroid/index.ts +0 -7
- package/src/Player/playerweb/index.ts +0 -3
|
@@ -0,0 +1,474 @@
|
|
|
1
|
+
// The exact MyMovies episode player (components/MegaVideoPlayer.web.tsx),
|
|
2
|
+
// packaged as a cross-platform component (Android / iOS / web). Driven by the
|
|
3
|
+
// react-native-video `<Video>` component, with ordered part URLs played
|
|
4
|
+
// back-to-back and auto-advancing to the next part when the current one ends.
|
|
5
|
+
// Sources come in as proxy URLs — from `mega-stream-proxy/native`
|
|
6
|
+
// (http://127.0.0.1:<port>/…) on Android/iOS or from the Service Worker
|
|
7
|
+
// (`/__mmstream__/…`) on the web.
|
|
8
|
+
|
|
9
|
+
import React, {
|
|
10
|
+
forwardRef,
|
|
11
|
+
useEffect,
|
|
12
|
+
useImperativeHandle,
|
|
13
|
+
useState,
|
|
14
|
+
} from "react";
|
|
15
|
+
import {
|
|
16
|
+
ActivityIndicator,
|
|
17
|
+
StyleSheet,
|
|
18
|
+
Text,
|
|
19
|
+
View,
|
|
20
|
+
} from "react-native";
|
|
21
|
+
import { Video } from "react-native-video";
|
|
22
|
+
import { Ionicons } from "@expo/vector-icons";
|
|
23
|
+
|
|
24
|
+
import { PlayerControls } from "./PlayerControls.js";
|
|
25
|
+
import { SubtitleDisplay } from "./SubtitleDisplay.js";
|
|
26
|
+
import {
|
|
27
|
+
loadSubtitles,
|
|
28
|
+
type SubCue,
|
|
29
|
+
type SubtitleFormat,
|
|
30
|
+
type SubtitleTrack,
|
|
31
|
+
} from "../subtitles/index.js";
|
|
32
|
+
|
|
33
|
+
export type MegaVideoPlayerProps = {
|
|
34
|
+
/** A playable URL — a direct stream or the first part URL. */
|
|
35
|
+
uri?: string | null;
|
|
36
|
+
/** Ordered additional part URLs for multi-part episodes, played in sequence. */
|
|
37
|
+
sources?: string[];
|
|
38
|
+
title?: string;
|
|
39
|
+
/** Pass your subtitle file here — MEGA links, plain http(s) CORS hosts, or a known extension (.srt/.ass/.vtt). */
|
|
40
|
+
subtitleUrl?: string;
|
|
41
|
+
/** optional format hint (defaults to URL extension, then content sniffing) */
|
|
42
|
+
subtitleFormat?: SubtitleFormat;
|
|
43
|
+
/** language name shown in the player controls for the single `subtitleUrl` track
|
|
44
|
+
* (e.g. "English", "French"). Defaults to "SRT". */
|
|
45
|
+
subtitleLang?: string;
|
|
46
|
+
subtitleTracks?: SubtitleTrack[];
|
|
47
|
+
/** preselect a subtitle track by language on load */
|
|
48
|
+
initialSubtitleLang?: string | null;
|
|
49
|
+
/** shift the cue timestamps by an offset in seconds (can be negative) */
|
|
50
|
+
subtitleOffset?: number;
|
|
51
|
+
autoPlay?: boolean;
|
|
52
|
+
onEnd?: () => void;
|
|
53
|
+
onError?: (message: string) => void;
|
|
54
|
+
onProgress?: (info: { currentTime: number; duration: number }) => void;
|
|
55
|
+
onFullscreenChange?: (fullscreen: boolean) => void;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export type MegaVideoPlayerHandle = {
|
|
59
|
+
/** Loads a new playable URL into the player. */
|
|
60
|
+
add: (source: string, opts?: { title?: string }) => Promise<void>;
|
|
61
|
+
/** Stops playback and clears the current source. */
|
|
62
|
+
stop: () => void;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export const MegaVideoPlayer = forwardRef<
|
|
66
|
+
MegaVideoPlayerHandle,
|
|
67
|
+
MegaVideoPlayerProps
|
|
68
|
+
>(function MegaVideoPlayer(
|
|
69
|
+
{
|
|
70
|
+
uri,
|
|
71
|
+
sources,
|
|
72
|
+
title,
|
|
73
|
+
subtitleUrl,
|
|
74
|
+
subtitleFormat,
|
|
75
|
+
subtitleLang,
|
|
76
|
+
subtitleTracks: subtitleTracksProp,
|
|
77
|
+
initialSubtitleLang,
|
|
78
|
+
subtitleOffset,
|
|
79
|
+
autoPlay = true,
|
|
80
|
+
onEnd,
|
|
81
|
+
onError,
|
|
82
|
+
onProgress,
|
|
83
|
+
onFullscreenChange,
|
|
84
|
+
},
|
|
85
|
+
ref,
|
|
86
|
+
) {
|
|
87
|
+
const [playing, setPlaying] = useState(false);
|
|
88
|
+
const [buffering, setBuffering] = useState(true);
|
|
89
|
+
const [loading, setLoading] = useState(false);
|
|
90
|
+
const [currentTime, setCurrentTime] = useState(0);
|
|
91
|
+
const [duration, setDuration] = useState(0);
|
|
92
|
+
const [muted, setMuted] = useState(false);
|
|
93
|
+
const [volume, setVolume] = useState(1);
|
|
94
|
+
const [visible, setVisible] = useState(true);
|
|
95
|
+
const [subtitlesOn, setSubtitlesOn] = useState(true);
|
|
96
|
+
const [subFontSize, setSubFontSize] = useState(14);
|
|
97
|
+
const [cues, setCues] = useState<SubCue[]>([]);
|
|
98
|
+
const [subtitleError, setSubtitleError] = useState<string | null>(null);
|
|
99
|
+
const [error, setError] = useState<string | null>(null);
|
|
100
|
+
const [fullscreen, setFullscreen] = useState(false);
|
|
101
|
+
const [sourceUri, setSourceUri] = useState<string | null>(null);
|
|
102
|
+
const [tracks, setTracks] = useState<SubtitleTrack[]>([]);
|
|
103
|
+
|
|
104
|
+
const videoRef = React.useRef<any>(null);
|
|
105
|
+
const mountedRef = React.useRef(true);
|
|
106
|
+
const autoplayRef = React.useRef(autoPlay !== false);
|
|
107
|
+
const hideTimerRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
108
|
+
const subTrackIdxRef = React.useRef(0);
|
|
109
|
+
const sourceIndexRef = React.useRef(
|
|
110
|
+
Math.max(0, (sources ?? []).indexOf(uri ?? "")),
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
// ---- source management ----
|
|
114
|
+
|
|
115
|
+
async function add(source: string, opts?: { title?: string }): Promise<void> {
|
|
116
|
+
if (!source) return;
|
|
117
|
+
setError(null);
|
|
118
|
+
setLoading(true);
|
|
119
|
+
setBuffering(true);
|
|
120
|
+
setSourceUri(source);
|
|
121
|
+
setCurrentTime(0);
|
|
122
|
+
setDuration(0);
|
|
123
|
+
|
|
124
|
+
if (!mountedRef.current) return;
|
|
125
|
+
console.log(`[MegaVideoPlayer] add: ${source} (${opts?.title ?? title})`);
|
|
126
|
+
if (autoplayRef.current) {
|
|
127
|
+
setPlaying(true);
|
|
128
|
+
scheduleHide();
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function stop(): void {
|
|
133
|
+
setSourceUri(null);
|
|
134
|
+
setPlaying(false);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
useImperativeHandle(ref, () => ({ add, stop }), [add, stop]);
|
|
138
|
+
|
|
139
|
+
// ---- player controllers ----
|
|
140
|
+
|
|
141
|
+
const playPause = (): void => {
|
|
142
|
+
setPlaying((p) => !p);
|
|
143
|
+
setVisible(true);
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const skip = (delta: number): void => {
|
|
147
|
+
const next = Math.min(Math.max(currentTime + delta, 0), duration);
|
|
148
|
+
videoRef.current?.seek(next);
|
|
149
|
+
setCurrentTime(next);
|
|
150
|
+
setVisible(true);
|
|
151
|
+
scheduleHide();
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const seek = (seconds: number): void => {
|
|
155
|
+
const clamped = Math.min(Math.max(seconds, 0), duration || seconds);
|
|
156
|
+
videoRef.current?.seek(clamped);
|
|
157
|
+
setCurrentTime(clamped);
|
|
158
|
+
setVisible(true);
|
|
159
|
+
scheduleHide();
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
const toggleMute = (): void => {
|
|
163
|
+
setMuted((m) => !m);
|
|
164
|
+
setVisible(true);
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
const changeVolume = (v: number): void => {
|
|
168
|
+
setVolume(v);
|
|
169
|
+
if (v > 0) setMuted(false);
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
const toggleSubtitles = (): void => {
|
|
173
|
+
setSubtitlesOn((s) => !s);
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
const toggleControls = (): void => {
|
|
177
|
+
setVisible((v) => !v);
|
|
178
|
+
if (!visible && playing) scheduleHide();
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
const enterFullscreen = (next?: boolean | unknown): void => {
|
|
182
|
+
// PlayerControls calls through onPress, which passes the press event as
|
|
183
|
+
// the first argument — ignore it (it is not the desired fullscreen value).
|
|
184
|
+
const target = typeof next === "boolean" ? next : !fullscreen;
|
|
185
|
+
setFullscreen(target);
|
|
186
|
+
setVisible(true);
|
|
187
|
+
onFullscreenChange?.(target);
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
const changeSubtitleFontSize = (px: number): void => {
|
|
191
|
+
setSubFontSize(px);
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
const selectSubtitleTrack = (lang: string): void => {
|
|
195
|
+
const idx = tracks.findIndex((t) => t.lang === lang);
|
|
196
|
+
if (idx >= 0) {
|
|
197
|
+
subTrackIdxRef.current = idx;
|
|
198
|
+
setSubtitlesOn(true);
|
|
199
|
+
loadCues(tracks);
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
// ---- react-native-video handlers ----
|
|
204
|
+
|
|
205
|
+
const onVideoLoad = (data: { duration?: number }): void => {
|
|
206
|
+
const d = data.duration ?? duration;
|
|
207
|
+
setDuration(d);
|
|
208
|
+
setBuffering(false);
|
|
209
|
+
setLoading(false);
|
|
210
|
+
console.log(`[MegaVideoPlayer] onLoad duration=${d}`);
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
const onVideoProgress = (data: { currentTime?: number }): void => {
|
|
214
|
+
const t = data.currentTime ?? 0;
|
|
215
|
+
setCurrentTime(t);
|
|
216
|
+
onProgress?.({ currentTime: t, duration });
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
const onVideoError = (e: any): void => {
|
|
220
|
+
if (!mountedRef.current) return;
|
|
221
|
+
const message =
|
|
222
|
+
e?.error?.message || e?.error?.errorString || String(e) || "Video error";
|
|
223
|
+
console.warn("[MegaVideoPlayer] onError:", message);
|
|
224
|
+
setError(message);
|
|
225
|
+
setBuffering(false);
|
|
226
|
+
setLoading(false);
|
|
227
|
+
onError?.(message);
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
const onVideoEnd = (): void => {
|
|
231
|
+
const list = sources ?? [];
|
|
232
|
+
const next = list[sourceIndexRef.current + 1];
|
|
233
|
+
if (next) {
|
|
234
|
+
sourceIndexRef.current += 1;
|
|
235
|
+
console.log(
|
|
236
|
+
`[MegaVideoPlayer] advancing to part ${sourceIndexRef.current + 1}/${list.length}`,
|
|
237
|
+
);
|
|
238
|
+
void add(next, { title });
|
|
239
|
+
} else {
|
|
240
|
+
setPlaying(false);
|
|
241
|
+
setVisible(true);
|
|
242
|
+
onEnd?.();
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
// ---- helpers ----
|
|
247
|
+
|
|
248
|
+
function scheduleHide() {
|
|
249
|
+
if (hideTimerRef.current) clearTimeout(hideTimerRef.current);
|
|
250
|
+
hideTimerRef.current = setTimeout(() => {
|
|
251
|
+
setPlaying((p) => {
|
|
252
|
+
if (p) setVisible(false);
|
|
253
|
+
return p;
|
|
254
|
+
});
|
|
255
|
+
}, 3000);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function loadCues(trackList: SubtitleTrack[] = tracks) {
|
|
259
|
+
const track =
|
|
260
|
+
trackList[Math.min(subTrackIdxRef.current, trackList.length - 1)] ?? null;
|
|
261
|
+
setCues([]);
|
|
262
|
+
setSubtitleError(null);
|
|
263
|
+
if (!track) return;
|
|
264
|
+
loadSubtitles(track.url, { format: track.format ?? undefined })
|
|
265
|
+
.then((c) => {
|
|
266
|
+
if (!mountedRef.current) return;
|
|
267
|
+
setCues(c);
|
|
268
|
+
setSubtitleError(
|
|
269
|
+
c.length === 0 ? "No cues parsed from this subtitle file" : null,
|
|
270
|
+
);
|
|
271
|
+
})
|
|
272
|
+
.catch((e: unknown) => {
|
|
273
|
+
console.warn("[subtitles] load failed:", track.url, e);
|
|
274
|
+
if (mountedRef.current) setSubtitleError("Subtitles unavailable");
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function syncSubtitleTracks() {
|
|
279
|
+
const nextTracks = subtitleTracksProp?.length
|
|
280
|
+
? subtitleTracksProp
|
|
281
|
+
: subtitleUrl
|
|
282
|
+
? [{ lang: subtitleLang ?? "SRT", url: subtitleUrl, format: subtitleFormat }]
|
|
283
|
+
: [];
|
|
284
|
+
setTracks(nextTracks);
|
|
285
|
+
// Preselect the requested track (from the watch screen's subtitle picker)
|
|
286
|
+
if (initialSubtitleLang) {
|
|
287
|
+
const idx = nextTracks.findIndex((t) => t.lang === initialSubtitleLang);
|
|
288
|
+
if (idx >= 0) subTrackIdxRef.current = idx;
|
|
289
|
+
}
|
|
290
|
+
loadCues(nextTracks);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// ---- effects ----
|
|
294
|
+
|
|
295
|
+
useEffect(() => {
|
|
296
|
+
mountedRef.current = true;
|
|
297
|
+
autoplayRef.current = autoPlay !== false;
|
|
298
|
+
return () => {
|
|
299
|
+
mountedRef.current = false;
|
|
300
|
+
if (hideTimerRef.current) clearTimeout(hideTimerRef.current);
|
|
301
|
+
};
|
|
302
|
+
}, [autoPlay]);
|
|
303
|
+
|
|
304
|
+
useEffect(() => {
|
|
305
|
+
syncSubtitleTracks();
|
|
306
|
+
if (uri) void add(uri, { title });
|
|
307
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
308
|
+
}, []);
|
|
309
|
+
|
|
310
|
+
useEffect(() => {
|
|
311
|
+
if (!uri) return;
|
|
312
|
+
sourceIndexRef.current = Math.max(0, (sources ?? []).indexOf(uri));
|
|
313
|
+
void add(uri, { title });
|
|
314
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
315
|
+
}, [uri]);
|
|
316
|
+
|
|
317
|
+
useEffect(() => {
|
|
318
|
+
syncSubtitleTracks();
|
|
319
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
320
|
+
}, [subtitleTracksProp, subtitleUrl]);
|
|
321
|
+
|
|
322
|
+
useEffect(() => {
|
|
323
|
+
if (!visible || !playing) return;
|
|
324
|
+
const t = setTimeout(() => setVisible(false), 3000);
|
|
325
|
+
return () => clearTimeout(t);
|
|
326
|
+
}, [visible, playing]);
|
|
327
|
+
|
|
328
|
+
// ---- render ----
|
|
329
|
+
|
|
330
|
+
const activeCue =
|
|
331
|
+
cues.find((c) => currentTime - (subtitleOffset ?? 0) >= c.start &&
|
|
332
|
+
currentTime - (subtitleOffset ?? 0) < c.end) ?? null;
|
|
333
|
+
const activeTrack =
|
|
334
|
+
tracks[Math.min(subTrackIdxRef.current, tracks.length - 1)] ?? null;
|
|
335
|
+
|
|
336
|
+
if (error) {
|
|
337
|
+
return (
|
|
338
|
+
<View style={[styles.wrap, styles.center]}>
|
|
339
|
+
<Ionicons name="alert-circle-outline" size={40} color="#ff6b6b" />
|
|
340
|
+
<Text style={styles.errorText}>{error}</Text>
|
|
341
|
+
<Text style={styles.errorHint}>Use "Download & Play" instead.</Text>
|
|
342
|
+
</View>
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
return (
|
|
347
|
+
<View style={[styles.wrap, fullscreen && styles.fullscreen]}>
|
|
348
|
+
{sourceUri ? (
|
|
349
|
+
<Video
|
|
350
|
+
ref={videoRef}
|
|
351
|
+
source={{ uri: sourceUri }}
|
|
352
|
+
style={StyleSheet.absoluteFill}
|
|
353
|
+
resizeMode="contain"
|
|
354
|
+
paused={!playing}
|
|
355
|
+
muted={muted}
|
|
356
|
+
volume={volume}
|
|
357
|
+
progressUpdateInterval={1000}
|
|
358
|
+
onLoadStart={() => {
|
|
359
|
+
if (mountedRef.current) setBuffering(true);
|
|
360
|
+
}}
|
|
361
|
+
onLoad={onVideoLoad}
|
|
362
|
+
onBuffer={({ isBuffering }: { isBuffering?: boolean }) => {
|
|
363
|
+
if (mountedRef.current) setBuffering(!!isBuffering);
|
|
364
|
+
}}
|
|
365
|
+
onProgress={onVideoProgress}
|
|
366
|
+
onError={onVideoError}
|
|
367
|
+
onEnd={onVideoEnd}
|
|
368
|
+
/>
|
|
369
|
+
) : null}
|
|
370
|
+
|
|
371
|
+
{loading && (
|
|
372
|
+
<View style={styles.centerBadge} pointerEvents="none">
|
|
373
|
+
<ActivityIndicator color="#e50914" size="large" />
|
|
374
|
+
<Text style={styles.loadingText}>Starting stream…</Text>
|
|
375
|
+
</View>
|
|
376
|
+
)}
|
|
377
|
+
|
|
378
|
+
<SubtitleDisplay cue={activeCue} enabled={subtitlesOn} fontSize={subFontSize} />
|
|
379
|
+
{subtitlesOn && subtitleError && !cues.length && (
|
|
380
|
+
<View pointerEvents="none" style={styles.subErrorWrap}>
|
|
381
|
+
<Text style={styles.subErrorText}>
|
|
382
|
+
{subtitleError} · {activeTrack?.lang ?? "no track"}
|
|
383
|
+
</Text>
|
|
384
|
+
</View>
|
|
385
|
+
)}
|
|
386
|
+
|
|
387
|
+
<PlayerControls
|
|
388
|
+
playing={playing}
|
|
389
|
+
buffering={buffering}
|
|
390
|
+
currentTime={currentTime}
|
|
391
|
+
duration={duration}
|
|
392
|
+
muted={muted}
|
|
393
|
+
title={title}
|
|
394
|
+
visible={visible}
|
|
395
|
+
subtitlesOn={subtitlesOn}
|
|
396
|
+
volume={volume}
|
|
397
|
+
subtitleTracks={tracks.map((t) => t.lang)}
|
|
398
|
+
subtitleTrack={activeTrack?.lang}
|
|
399
|
+
onSelectSubtitleTrack={selectSubtitleTrack}
|
|
400
|
+
subtitleFontSize={subFontSize}
|
|
401
|
+
onChangeSubtitleFontSize={changeSubtitleFontSize}
|
|
402
|
+
onToggleVisible={toggleControls}
|
|
403
|
+
onPlayPause={playPause}
|
|
404
|
+
onSkip={skip}
|
|
405
|
+
onSeek={seek}
|
|
406
|
+
onToggleMute={toggleMute}
|
|
407
|
+
onToggleSubtitles={toggleSubtitles}
|
|
408
|
+
onVolume={changeVolume}
|
|
409
|
+
onFullscreen={enterFullscreen}
|
|
410
|
+
/>
|
|
411
|
+
</View>
|
|
412
|
+
);
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
const styles = StyleSheet.create({
|
|
416
|
+
wrap: {
|
|
417
|
+
flex: 1,
|
|
418
|
+
width: "100%",
|
|
419
|
+
height: "100%",
|
|
420
|
+
backgroundColor: "#000",
|
|
421
|
+
},
|
|
422
|
+
fullscreen: {
|
|
423
|
+
position: "absolute",
|
|
424
|
+
left: 0,
|
|
425
|
+
right: 0,
|
|
426
|
+
top: 0,
|
|
427
|
+
bottom: 0,
|
|
428
|
+
zIndex: 10,
|
|
429
|
+
},
|
|
430
|
+
center: {
|
|
431
|
+
flex: 1,
|
|
432
|
+
justifyContent: "center",
|
|
433
|
+
alignItems: "center",
|
|
434
|
+
gap: 12,
|
|
435
|
+
padding: 20,
|
|
436
|
+
backgroundColor: "#000",
|
|
437
|
+
},
|
|
438
|
+
errorText: {
|
|
439
|
+
color: "#ff6b6b",
|
|
440
|
+
fontSize: 14,
|
|
441
|
+
textAlign: "center",
|
|
442
|
+
},
|
|
443
|
+
errorHint: {
|
|
444
|
+
color: "#9aa0aa",
|
|
445
|
+
fontSize: 12,
|
|
446
|
+
textAlign: "center",
|
|
447
|
+
},
|
|
448
|
+
centerBadge: {
|
|
449
|
+
...StyleSheet.absoluteFillObject,
|
|
450
|
+
justifyContent: "center",
|
|
451
|
+
alignItems: "center",
|
|
452
|
+
gap: 12,
|
|
453
|
+
},
|
|
454
|
+
loadingText: { color: "#fff", fontSize: 14 },
|
|
455
|
+
subErrorWrap: {
|
|
456
|
+
position: "absolute",
|
|
457
|
+
left: 0,
|
|
458
|
+
right: 0,
|
|
459
|
+
bottom: 56,
|
|
460
|
+
alignItems: "center",
|
|
461
|
+
paddingHorizontal: 24,
|
|
462
|
+
zIndex: 5,
|
|
463
|
+
},
|
|
464
|
+
subErrorText: {
|
|
465
|
+
backgroundColor: "rgba(229,9,20,0.8)",
|
|
466
|
+
color: "#fff",
|
|
467
|
+
fontSize: 12,
|
|
468
|
+
textAlign: "center",
|
|
469
|
+
paddingHorizontal: 10,
|
|
470
|
+
paddingVertical: 4,
|
|
471
|
+
borderRadius: 4,
|
|
472
|
+
overflow: "hidden",
|
|
473
|
+
},
|
|
474
|
+
});
|