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.
Files changed (49) hide show
  1. package/README.md +14 -49
  2. package/dist/megaFile.js +22 -9
  3. package/dist/{Player/playerAndroid/MegaVideoPlayerAndroid.d.ts → player/MegaVideoPlayer.d.ts} +4 -13
  4. package/dist/player/MegaVideoPlayer.js +307 -0
  5. package/dist/{Player/playerweb → player}/PlayerControls.d.ts +2 -14
  6. package/dist/player/PlayerControls.js +289 -0
  7. package/dist/{Player/playerweb → player}/SubtitleDisplay.d.ts +3 -3
  8. package/dist/{Player/playerweb → player}/SubtitleDisplay.js +1 -1
  9. package/dist/player/index.d.ts +3 -0
  10. package/dist/player/index.js +3 -0
  11. package/dist/{Player/playerweb/MegaVideoPlayer.d.ts → playerweb/MegaVideoPlayerWeb.d.ts} +6 -18
  12. package/dist/{Player/playerweb/MegaVideoPlayer.js → playerweb/MegaVideoPlayerWeb.js} +18 -18
  13. package/dist/{Player/playerAndroid/PlayerControlsAndroid.d.ts → playerweb/PlayerControlsWeb.d.ts} +2 -16
  14. package/dist/playerweb/PlayerControlsWeb.js +289 -0
  15. package/dist/playerweb/SubtitleDisplayWeb.d.ts +15 -0
  16. package/dist/playerweb/SubtitleDisplayWeb.js +32 -0
  17. package/dist/playerweb/index.d.ts +3 -0
  18. package/dist/playerweb/index.js +3 -0
  19. package/dist/proxy/native.js +11 -3
  20. package/expo-module.config.json +3 -0
  21. package/package.json +15 -18
  22. package/src/megaFile.ts +28 -15
  23. package/src/player/MegaVideoPlayer.tsx +474 -0
  24. package/src/{Player/playerweb → player}/PlayerControls.tsx +200 -369
  25. package/src/{Player/playerweb → player}/SubtitleDisplay.tsx +3 -3
  26. package/src/player/index.ts +7 -0
  27. package/src/{Player/playerweb/MegaVideoPlayer.tsx → playerweb/MegaVideoPlayerWeb.tsx} +41 -58
  28. package/src/playerweb/PlayerControlsWeb.tsx +629 -0
  29. package/src/playerweb/SubtitleDisplayWeb.tsx +48 -0
  30. package/src/playerweb/index.ts +3 -0
  31. package/src/proxy/native.ts +10 -3
  32. package/dist/Player/native.d.ts +0 -3
  33. package/dist/Player/native.js +0 -3
  34. package/dist/Player/playerAndroid/MegaVideoPlayerAndroid.js +0 -624
  35. package/dist/Player/playerAndroid/PlayerControlsAndroid.js +0 -370
  36. package/dist/Player/playerAndroid/SubtitleDisplayAndroid.d.ts +0 -21
  37. package/dist/Player/playerAndroid/SubtitleDisplayAndroid.js +0 -55
  38. package/dist/Player/playerAndroid/index.d.ts +0 -3
  39. package/dist/Player/playerAndroid/index.js +0 -3
  40. package/dist/Player/playerweb/PlayerControls.js +0 -346
  41. package/dist/Player/playerweb/index.d.ts +0 -3
  42. package/dist/Player/playerweb/index.js +0 -3
  43. package/dist/Player/web.d.ts +0 -3
  44. package/dist/Player/web.js +0 -3
  45. package/src/Player/playerAndroid/MegaVideoPlayerAndroid.tsx +0 -825
  46. package/src/Player/playerAndroid/PlayerControlsAndroid.tsx +0 -824
  47. package/src/Player/playerAndroid/SubtitleDisplayAndroid.tsx +0 -80
  48. package/src/Player/playerAndroid/index.ts +0 -7
  49. package/src/Player/playerweb/index.ts +0 -3
@@ -1,825 +0,0 @@
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
- useMemo,
14
- useState,
15
- } from "react";
16
- import {
17
- ActivityIndicator,
18
- Platform,
19
- StyleSheet,
20
- Text,
21
- View,
22
- } from "react-native";
23
- import { Video } from "react-native-video";
24
- import { Ionicons } from "@expo/vector-icons";
25
-
26
- import { PlayerControlsAndroid } from "./PlayerControlsAndroid.js";
27
- import { SubtitleDisplayAndroid } from "./SubtitleDisplayAndroid.js";
28
- import {
29
- loadSubtitles,
30
- type SubCue,
31
- type SubtitleFormat,
32
- type SubtitleTrack,
33
- } from "../../subtitles/index.js";
34
-
35
- export type MegaVideoPlayerAndroidProps = {
36
- /** A playable URL — a direct stream or the first part URL. */
37
- uri?: string | null;
38
- /** Ordered additional part URLs for multi-part episodes, played in sequence. */
39
- sources?: string[];
40
- title?: string;
41
- /** Pass your subtitle file here — MEGA links, plain http(s) CORS hosts, or a known extension (.srt/.ass/.vtt). */
42
- subtitleUrl?: string;
43
- /** optional format hint (defaults to URL extension, then content sniffing) */
44
- subtitleFormat?: SubtitleFormat;
45
- /** language name shown in the player controls for the single `subtitleUrl` track
46
- * (e.g. "English", "French"). Defaults to "SRT". */
47
- subtitleLang?: string;
48
- subtitleTracks?: SubtitleTrack[];
49
- /** preselect a subtitle track by language on load */
50
- initialSubtitleLang?: string | null;
51
- /** shift the cue timestamps by an offset in seconds (can be negative) */
52
- subtitleOffset?: number;
53
- /** resolution labels in display order — the FIRST is the default source
54
- * (usually "HD: <url>"), e.g. ["Default", "1080p", "720p"]. */
55
- resolutions?: string[];
56
- /** currently active resolution label */
57
- resolution?: string;
58
- /** user picked a resolution label — resolve it to a URL and update `uri`. */
59
- onSelectResolution?: (label: string) => void;
60
- autoPlay?: boolean;
61
- onEnd?: () => void;
62
- onError?: (message: string) => void;
63
- onProgress?: (info: { currentTime: number; duration: number }) => void;
64
- onFullscreenChange?: (fullscreen: boolean) => void;
65
- width?: number;
66
- height?: number;
67
- };
68
-
69
- export type MegaVideoPlayerAndroidHandle = {
70
- /** Loads a new playable URL into the player. */
71
- add: (source: string, opts?: { title?: string }) => Promise<void>;
72
- /** Stops playback and clears the current source. */
73
- stop: () => void;
74
- };
75
-
76
- export const MegaVideoPlayerAndroid = forwardRef<
77
- MegaVideoPlayerAndroidHandle,
78
- MegaVideoPlayerAndroidProps
79
- >(function MegaVideoPlayer(
80
- {
81
- uri,
82
- sources,
83
- title,
84
- subtitleUrl,
85
- subtitleFormat,
86
- subtitleLang,
87
- subtitleTracks: subtitleTracksProp,
88
- initialSubtitleLang,
89
- subtitleOffset,
90
- resolutions,
91
- resolution,
92
- onSelectResolution,
93
- autoPlay = true,
94
- onEnd,
95
- onError,
96
- onProgress,
97
- onFullscreenChange,
98
- width,
99
- height,
100
- },
101
- ref,
102
- ) {
103
- const [playing, setPlaying] = useState(false);
104
- const [buffering, setBuffering] = useState(true);
105
- const [loading, setLoading] = useState(false);
106
- const [currentTime, setCurrentTime] = useState(0);
107
- const [duration, setDuration] = useState(0);
108
- const [muted, setMuted] = useState(false);
109
- const [volume, setVolume] = useState(1);
110
- const [visible, setVisible] = useState(true);
111
- const [subtitlesOn, setSubtitlesOn] = useState(true);
112
- const [subFontSize, setSubFontSize] = useState(14);
113
- const [cues, setCues] = useState<SubCue[]>([]);
114
- const [subtitleError, setSubtitleError] = useState<string | null>(null);
115
- const [error, setError] = useState<string | null>(null);
116
- const [fullscreen, setFullscreen] = useState(false);
117
- const [controlsBump, setControlsBump] = useState(0);
118
- const [sourceUri, setSourceUri] = useState<string | null>(null);
119
- const [tracks, setTracks] = useState<SubtitleTrack[]>([]);
120
-
121
- // Keep the `<Video>` source object identity stable — a fresh `{ uri }` on
122
- // every render makes the native player re-evaluate (and on some platforms
123
- // reload) the source, which resets the playback clock.
124
- const videoSource = useMemo(
125
- () => (sourceUri ? { uri: sourceUri } : undefined),
126
- [sourceUri],
127
- );
128
-
129
- const videoRef = React.useRef<any>(null);
130
- const mountedRef = React.useRef(true);
131
- const autoplayRef = React.useRef(autoPlay !== false);
132
- const hideTimerRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);
133
- const subTrackIdxRef = React.useRef(0);
134
- const sourceIndexRef = React.useRef(
135
- Math.max(0, (sources ?? []).indexOf(uri ?? "")),
136
- );
137
- /** sources that auto-advance on end. Quality-switch files are full merged
138
- * sources, so they stop part auto-advance. */
139
- const activeSourcesRef = React.useRef<string[]>(sources ?? []);
140
- /** position to resume a quality switch at, applied once playback is moving */
141
- const pendingResumeRef = React.useRef<number | null>(null);
142
- const prevResolutionRef = React.useRef<string | undefined>(resolution);
143
- /** web-only: the UI's intended play state + element volumes, so the DOM
144
- * bridge can operate the raw <video> element without React re-renders. */
145
- const playingRef = React.useRef(false);
146
- const volumeRef = React.useRef(volume);
147
- const mutedRef = React.useRef(muted);
148
- const onErrorRef = React.useRef(onError);
149
- const onProgressRef = React.useRef(onProgress);
150
-
151
- // ---- source management ----
152
-
153
- async function add(source: string, opts?: { title?: string }): Promise<void> {
154
- if (!source) return;
155
- setError(null);
156
- setLoading(true);
157
- setBuffering(true);
158
- setSourceUri(source);
159
- setCurrentTime(0);
160
- setDuration(0);
161
-
162
- if (!mountedRef.current) return;
163
- console.log(`[MegaVideoPlayer] add: ${source} (${opts?.title ?? title})`);
164
- if (autoplayRef.current) {
165
- setPlaying(true);
166
- scheduleHide();
167
- }
168
- }
169
-
170
- function stop(): void {
171
- setSourceUri(null);
172
- setPlaying(false);
173
- }
174
-
175
- useImperativeHandle(ref, () => ({ add, stop }), [add, stop]);
176
-
177
- // ---- player controllers ----
178
-
179
- const playPause = (): void => {
180
- setPlaying((p) => !p);
181
- setVisible(true);
182
- if (Platform.OS === "web") {
183
- const el = (videoRef.current as any)?.nativeHtmlVideoRef?.current ?? null;
184
- if (el) {
185
- try {
186
- if (playing) {
187
- el.pause();
188
- } else {
189
- setLoading(false);
190
- const p = el.play();
191
- if (p && typeof p.catch === "function") p.catch(() => {});
192
- }
193
- } catch {}
194
- }
195
- }
196
- };
197
-
198
- const skip = (delta: number): void => {
199
- const next = Math.min(Math.max(currentTime + delta, 0), duration);
200
- if (Platform.OS === "web") {
201
- const el = (videoRef.current as any)?.nativeHtmlVideoRef?.current ?? null;
202
- if (el) {
203
- try {
204
- el.currentTime = next;
205
- } catch {}
206
- }
207
- } else {
208
- videoRef.current?.seek(next);
209
- }
210
- setCurrentTime(next);
211
- setVisible(true);
212
- scheduleHide();
213
- };
214
-
215
- const seek = (seconds: number): void => {
216
- const clamped = Math.min(Math.max(seconds, 0), duration || seconds);
217
- if (Platform.OS === "web") {
218
- const el = (videoRef.current as any)?.nativeHtmlVideoRef?.current ?? null;
219
- if (el) {
220
- try {
221
- el.currentTime = clamped;
222
- } catch {}
223
- }
224
- } else {
225
- videoRef.current?.seek(clamped);
226
- }
227
- setCurrentTime(clamped);
228
- setVisible(true);
229
- scheduleHide();
230
- };
231
-
232
- const toggleMute = (): void => {
233
- setMuted((m) => {
234
- const next = !m;
235
- mutedRef.current = next;
236
- if (Platform.OS === "web") {
237
- const el = (videoRef.current as any)?.nativeHtmlVideoRef?.current ?? null;
238
- if (el) {
239
- try {
240
- el.muted = next;
241
- } catch {}
242
- }
243
- }
244
- return next;
245
- });
246
- setVisible(true);
247
- };
248
-
249
- const changeVolume = (v: number): void => {
250
- const clamped = Math.min(Math.max(v, 0), 1);
251
- volumeRef.current = clamped;
252
- setVolume(clamped);
253
- if (clamped > 0) setMuted(false);
254
- if (Platform.OS === "web") {
255
- const el = (videoRef.current as any)?.nativeHtmlVideoRef?.current ?? null;
256
- if (el) {
257
- try {
258
- el.volume = clamped;
259
- } catch {}
260
- }
261
- }
262
- };
263
-
264
- const toggleSubtitles = (): void => {
265
- setSubtitlesOn((s) => !s);
266
- };
267
-
268
- const toggleControls = (): void => {
269
- setVisible((v) => !v);
270
- if (!visible && playing) scheduleHide();
271
- };
272
-
273
- /** any interaction with the control bar — reset the auto-hide timer */
274
- const onInteract = (): void => {
275
- setControlsBump((n) => n + 1);
276
- };
277
-
278
- const enterFullscreen = (next?: boolean | unknown): void => {
279
- // PlayerControls calls through onPress, which passes the press event as
280
- // the first argument — ignore it (it is not the desired fullscreen value).
281
- const target = typeof next === "boolean" ? next : !fullscreen;
282
- setFullscreen(target);
283
- setVisible(true);
284
- onFullscreenChange?.(target);
285
- if (Platform.OS === "web") {
286
- const doc = (globalThis as any)?.document ?? null;
287
- const el = (videoRef.current as any)?.nativeHtmlVideoRef?.current ?? null;
288
- try {
289
- if (target) {
290
- el?.requestFullscreen?.();
291
- } else if (doc?.fullscreenElement) {
292
- doc.exitFullscreen();
293
- }
294
- } catch {}
295
- }
296
- };
297
-
298
- const changeSubtitleFontSize = (px: number): void => {
299
- setSubFontSize(px);
300
- };
301
-
302
- const selectSubtitleTrack = (lang: string): void => {
303
- const idx = tracks.findIndex((t) => t.lang === lang);
304
- if (idx >= 0) {
305
- subTrackIdxRef.current = idx;
306
- setSubtitlesOn(true);
307
- loadCues(tracks);
308
- }
309
- };
310
-
311
- // ---- react-native-video handlers ----
312
-
313
- const onVideoLoad = (data: { duration?: number }): void => {
314
- const d = data.duration ?? duration;
315
- setDuration(d);
316
- setBuffering(false);
317
- setLoading(false);
318
- console.log(`[MegaVideoPlayer] onLoad duration=${d}`);
319
- };
320
-
321
- const onVideoProgress = (data: { currentTime?: number }): void => {
322
- const t = data.currentTime ?? 0;
323
- // Always move the clock first — never let a pending quality-switch resume
324
- // swallow progress events.
325
- setCurrentTime(t);
326
- onProgress?.({ currentTime: t, duration });
327
-
328
- const pending = pendingResumeRef.current;
329
- // quality switch: resume the previous position once the new stream is
330
- // actually flowing (t > 0) — seeking too early can stall the load.
331
- if (pending != null && t > 0 && duration > 0) {
332
- pendingResumeRef.current = null;
333
- const target =
334
- duration > pending + 20 ? pending : Math.max(duration - 20, 0);
335
- videoRef.current?.seek(target);
336
- setCurrentTime(target);
337
- }
338
- };
339
-
340
- const onVideoPlaybackStateChanged = (data: { isPlaying?: boolean }): void => {
341
- if (!mountedRef.current) return;
342
- // Web only: browsers may deny autoplay, so mirror the real element state
343
- // back into the UI. On native, `isPlaying` drops to false while buffering,
344
- // which must NOT be interpreted as a user pause.
345
- if (Platform.OS !== "web") return;
346
- if (typeof data?.isPlaying === "boolean") {
347
- setPlaying((prev) => (prev === data.isPlaying ? prev : data.isPlaying!));
348
- if (data.isPlaying) {
349
- setBuffering(false);
350
- setLoading(false);
351
- }
352
- }
353
- };
354
-
355
- const onVideoError = (e: any): void => {
356
- if (!mountedRef.current) return;
357
- const message =
358
- e?.error?.message || e?.error?.errorString || String(e) || "Video error";
359
- console.warn("[MegaVideoPlayer] onError:", message);
360
- setError(message);
361
- setBuffering(false);
362
- setLoading(false);
363
- onError?.(message);
364
- };
365
-
366
- const onVideoEnd = (): void => {
367
- const list = activeSourcesRef.current;
368
- const next = list[sourceIndexRef.current + 1];
369
- if (next) {
370
- sourceIndexRef.current += 1;
371
- console.log(
372
- `[MegaVideoPlayer] advancing to part ${sourceIndexRef.current + 1}/${list.length}`,
373
- );
374
- void add(next, { title });
375
- } else {
376
- setPlaying(false);
377
- setVisible(true);
378
- onEnd?.();
379
- }
380
- };
381
-
382
- // ---- helpers ----
383
-
384
- function scheduleHide() {
385
- if (hideTimerRef.current) clearTimeout(hideTimerRef.current);
386
- hideTimerRef.current = setTimeout(() => {
387
- setPlaying((p) => {
388
- if (p) setVisible(false);
389
- return p;
390
- });
391
- }, 3000);
392
- }
393
-
394
- function loadCues(trackList: SubtitleTrack[] = tracks) {
395
- const track =
396
- trackList[Math.min(subTrackIdxRef.current, trackList.length - 1)] ?? null;
397
- setCues([]);
398
- setSubtitleError(null);
399
- if (!track) return;
400
- loadSubtitles(track.url, { format: track.format ?? undefined })
401
- .then((c) => {
402
- if (!mountedRef.current) return;
403
- setCues(c);
404
- setSubtitleError(
405
- c.length === 0 ? "No cues parsed from this subtitle file" : null,
406
- );
407
- })
408
- .catch((e: unknown) => {
409
- console.warn("[subtitles] load failed:", track.url, e);
410
- if (mountedRef.current) setSubtitleError("Subtitles unavailable");
411
- });
412
- }
413
-
414
- function syncSubtitleTracks() {
415
- const nextTracks = subtitleTracksProp?.length
416
- ? subtitleTracksProp
417
- : subtitleUrl
418
- ? [
419
- {
420
- lang: subtitleLang ?? "SRT",
421
- url: subtitleUrl,
422
- format: subtitleFormat,
423
- },
424
- ]
425
- : [];
426
- setTracks(nextTracks);
427
- // Preselect the requested track (from the watch screen's subtitle picker)
428
- if (initialSubtitleLang) {
429
- const idx = nextTracks.findIndex((t) => t.lang === initialSubtitleLang);
430
- if (idx >= 0) subTrackIdxRef.current = idx;
431
- }
432
- loadCues(nextTracks);
433
- }
434
-
435
- // ---- effects ----
436
-
437
- useEffect(() => {
438
- mountedRef.current = true;
439
- autoplayRef.current = autoPlay !== false;
440
- return () => {
441
- mountedRef.current = false;
442
- if (hideTimerRef.current) clearTimeout(hideTimerRef.current);
443
- };
444
- }, [autoPlay]);
445
-
446
- useEffect(() => {
447
- if (!uri) return;
448
- sourceIndexRef.current = Math.max(0, (sources ?? []).indexOf(uri));
449
- const prevRes = prevResolutionRef.current;
450
- const qualitySwitch = resolution != null && resolution !== prevRes;
451
- prevResolutionRef.current = resolution;
452
- if (qualitySwitch) {
453
- pendingResumeRef.current = currentTime;
454
- const isDefaultChain = (sources ?? []).includes(uri ?? "");
455
- // a resolution file is one merged source — don't auto-advance through it
456
- activeSourcesRef.current = isDefaultChain ? (sources ?? []) : [];
457
- } else {
458
- pendingResumeRef.current = null;
459
- activeSourcesRef.current = sources ?? [];
460
- }
461
- void add(uri, { title });
462
- // eslint-disable-next-line react-hooks/exhaustive-deps
463
- }, [uri]);
464
-
465
- useEffect(() => {
466
- activeSourcesRef.current = sources ?? [];
467
- }, [sources]);
468
-
469
- useEffect(() => {
470
- syncSubtitleTracks();
471
- // eslint-disable-next-line react-hooks/exhaustive-deps
472
- }, [subtitleTracksProp, subtitleUrl]);
473
-
474
- useEffect(() => {
475
- if (!visible || !playing) return;
476
- const t = setTimeout(() => setVisible(false), 3000);
477
- return () => clearTimeout(t);
478
- }, [visible, playing, controlsBump]);
479
-
480
- // Safety net for the playback clock: some platforms/builds deliver
481
- // `onProgress` sparsely. While playing, poll the player position directly so
482
- // the timer, scrubber and subtitles keep advancing.
483
- useEffect(() => {
484
- if (!playing) return;
485
- let cancelled = false;
486
- const id = setInterval(async () => {
487
- try {
488
- const pos = await videoRef.current?.getCurrentPosition?.();
489
- if (cancelled || typeof pos !== "number" || !Number.isFinite(pos))
490
- return;
491
- setCurrentTime((prev) => (Math.abs(prev - pos) > 0.4 ? pos : prev));
492
- } catch {
493
- // player not ready yet — ignore
494
- }
495
- }, 500);
496
- return () => {
497
- cancelled = true;
498
- clearInterval(id);
499
- };
500
- }, [playing]);
501
-
502
- useEffect(() => {
503
- playingRef.current = playing;
504
- }, [playing]);
505
-
506
- useEffect(() => {
507
- onErrorRef.current = onError;
508
- onProgressRef.current = onProgress;
509
- }, [onError, onProgress]);
510
-
511
- // Web-only: react-native-video's web driver only maps a few DOM events
512
- // (onLoad <-> durationchange, sparse onProgress) and calls `play()` on load,
513
- // which browsers deny without a user gesture. Instead of fighting that, take
514
- // over the raw <video> element directly: attach our own listeners, mirror the
515
- // clock/duration/buffering/loading state from the element, keep the icon in
516
- // sync with the real paused state, force-play whenever the UI wants playback
517
- // but the element drifted to paused (autoplay-denial recovery), and apply
518
- // volume/muted/fullscreen changes. Native stays on react-native-video.
519
- useEffect(() => {
520
- if (Platform.OS !== "web") return;
521
- let cancelled = false;
522
- let el: any = null;
523
- let listeners: Array<[string, (ev?: unknown) => void]> = [];
524
-
525
- const detach = (): void => {
526
- if (!el) return;
527
- for (const [type, fn] of listeners) {
528
- try {
529
- el.removeEventListener(type, fn);
530
- } catch {}
531
- }
532
- listeners = [];
533
- el = null;
534
- };
535
-
536
- const add = (type: string, fn: (ev?: unknown) => void): void => {
537
- try {
538
- el.addEventListener(type, fn);
539
- listeners.push([type, fn]);
540
- } catch {}
541
- };
542
-
543
- const attach = (): any => {
544
- const next = (videoRef.current as any)?.nativeHtmlVideoRef?.current ?? null;
545
- if (!next) return null;
546
- if (next === el) return el;
547
- detach();
548
- el = next;
549
- el.muted = mutedRef.current;
550
- el.volume = Math.min(Math.max(volumeRef.current, 0), 1);
551
-
552
- add("timeupdate", () => {
553
- const t = typeof el.currentTime === "number" ? el.currentTime : 0;
554
- setCurrentTime((prev) => (Math.abs(prev - t) > 0.25 ? t : prev));
555
- const d = typeof el.duration === "number" ? el.duration : 0;
556
- onProgressRef.current?.({
557
- currentTime: t,
558
- duration: Number.isFinite(d) ? d : 0,
559
- });
560
- });
561
- add("durationchange", () => {
562
- const d = typeof el.duration === "number" ? el.duration : 0;
563
- if (typeof d === "number" && Number.isFinite(d) && d > 0) {
564
- setDuration(d);
565
- }
566
- setLoading(false);
567
- setBuffering(false);
568
- });
569
- add("play", () => {
570
- setLoading(false);
571
- setBuffering(false);
572
- setPlaying(true);
573
- });
574
- add("pause", () => {
575
- setBuffering(false);
576
- setPlaying(false);
577
- });
578
- add("waiting", () => setBuffering(true));
579
- add("canplay", () => {
580
- setBuffering(false);
581
- setLoading(false);
582
- });
583
- add("loadstart", () => setLoading(true));
584
- add("error", () => {
585
- const msg =
586
- (el?.error && (el.error.message || `Video error ${el.error.code}`)) ||
587
- "Video decode error";
588
- console.warn("[MegaVideoPlayer] web element error:", msg);
589
- setLoading(false);
590
- setBuffering(false);
591
- setError(msg);
592
- onError?.(msg);
593
- });
594
- const doc = (globalThis as any)?.document ?? null;
595
- if (doc) {
596
- add("fullscreenchange", () => {
597
- try {
598
- setFullscreen(!!doc.fullscreenElement);
599
- } catch {}
600
- });
601
- }
602
- return el;
603
- };
604
-
605
- const id = setInterval(() => {
606
- if (cancelled) return;
607
- const elem = attach();
608
- if (!elem) return;
609
- try {
610
- // Autoplay-deadlock breaker: the UI wants playback, but the browser
611
- // held the element at pause (initial autoplay denied, or a temporary
612
- // browser pause). Re-assert play every tick so a single user gesture
613
- // anywhere in the app unblocks it; seek once we finally have a size.
614
- if (playingRef.current && elem.paused && !elem.ended) {
615
- setLoading(false);
616
- el = elem;
617
- const p = elem.play();
618
- if (p && typeof p.catch === "function") p.catch(() => {});
619
- }
620
- // Mirror the real state back so the icon can never lie about pause.
621
- const real = elem.paused;
622
- if (real && playingRef.current && elem.ended) {
623
- setPlaying(false);
624
- }
625
- } catch {}
626
- }, 500);
627
-
628
- return () => {
629
- cancelled = true;
630
- clearInterval(id);
631
- detach();
632
- };
633
- // eslint-disable-next-line react-hooks/exhaustive-deps
634
- }, []);
635
-
636
- // ---- render ----
637
-
638
- const activeCue =
639
- cues.find(
640
- (c) =>
641
- currentTime - (subtitleOffset ?? 0) >= c.start &&
642
- currentTime - (subtitleOffset ?? 0) < c.end,
643
- ) ?? null;
644
- const activeTrack =
645
- tracks[Math.min(subTrackIdxRef.current, tracks.length - 1)] ?? null;
646
-
647
- if (error) {
648
- return (
649
- <View
650
- style={[
651
- styles.wrap,
652
- styles.center,
653
- {
654
- width: width ? width : "100%",
655
- height: height ? height : "100%",
656
- flexDirection: "column",
657
- },
658
- ]}
659
- >
660
- <Ionicons name="alert-circle-outline" size={40} color="#ff6b6b" />
661
- <Text style={styles.errorText}>{error}</Text>
662
- <Text style={styles.errorHint}>Use "Download & Play" instead.</Text>
663
- </View>
664
- );
665
- }
666
-
667
- return (
668
- <View
669
- style={[
670
- styles.wrap,
671
- { width: width ? width : "100%", height: height ? height : "100%" },
672
- fullscreen && styles.fullscreen,
673
- ]}
674
- >
675
- {sourceUri ? (
676
- <Video
677
- ref={videoRef}
678
- source={videoSource}
679
- style={[
680
- StyleSheet.absoluteFill,
681
- { width: width ? width : "100%", height: height ? height : "100%" },
682
- ]}
683
- resizeMode="contain"
684
- paused={!playing}
685
- muted={muted}
686
- volume={Platform.OS === "web" ? undefined : volume}
687
- progressUpdateInterval={1000}
688
- onLoadStart={() => {
689
- if (mountedRef.current) setBuffering(true);
690
- }}
691
- onLoad={onVideoLoad}
692
- onBuffer={({ isBuffering }: { isBuffering?: boolean }) => {
693
- if (mountedRef.current) setBuffering(!!isBuffering);
694
- }}
695
- onProgress={onVideoProgress}
696
- onPlaybackStateChanged={onVideoPlaybackStateChanged}
697
- onError={onVideoError}
698
- onEnd={onVideoEnd}
699
- />
700
- ) : null}
701
-
702
- {loading && !buffering && (
703
- <View
704
- style={[
705
- styles.centerBadge,
706
- StyleSheet.absoluteFill,
707
- { width: width ? width : "100%", height: height ? height : "100%" },
708
- ]}
709
- pointerEvents="none"
710
- >
711
- <ActivityIndicator color="#e50914" size="large" />
712
- <Text style={styles.loadingText}>Starting stream…</Text>
713
- </View>
714
- )}
715
-
716
- <SubtitleDisplayAndroid
717
- cue={activeCue}
718
- enabled={subtitlesOn}
719
- fontSize={subFontSize}
720
- width={width}
721
- height={height}
722
- />
723
- {subtitlesOn && subtitleError && !cues.length && (
724
- <View pointerEvents="none" style={styles.subErrorWrap}>
725
- <Text style={styles.subErrorText}>
726
- {subtitleError} · {activeTrack?.lang ?? "no track"}
727
- </Text>
728
- </View>
729
- )}
730
-
731
- <PlayerControlsAndroid
732
- playing={playing}
733
- buffering={buffering}
734
- currentTime={currentTime}
735
- duration={duration}
736
- muted={muted}
737
- title={title}
738
- visible={visible}
739
- subtitlesOn={subtitlesOn}
740
- volume={volume}
741
- subtitleTracks={tracks.map((t) => t.lang)}
742
- subtitleTrack={activeTrack?.lang}
743
- onSelectSubtitleTrack={selectSubtitleTrack}
744
- subtitleFontSize={subFontSize}
745
- onChangeSubtitleFontSize={changeSubtitleFontSize}
746
- onToggleVisible={toggleControls}
747
- onPlayPause={playPause}
748
- onSkip={skip}
749
- onSeek={seek}
750
- onToggleMute={toggleMute}
751
- onToggleSubtitles={toggleSubtitles}
752
- onVolume={changeVolume}
753
- resolutions={resolutions}
754
- resolution={resolution}
755
- onSelectResolution={onSelectResolution}
756
- onFullscreen={enterFullscreen}
757
- fullscreen={fullscreen}
758
- onInteract={onInteract}
759
- width={width}
760
- height={height}
761
- />
762
- </View>
763
- );
764
- });
765
-
766
- const styles = StyleSheet.create({
767
- wrap: {
768
- flex: 1,
769
- width: "100%",
770
- height: "100%",
771
- backgroundColor: "#000",
772
- },
773
- fullscreen: {
774
- position: "absolute",
775
- left: 0,
776
- right: 0,
777
- top: 0,
778
- bottom: 0,
779
- zIndex: 10,
780
- },
781
- center: {
782
- flex: 1,
783
- justifyContent: "center",
784
- alignItems: "center",
785
- gap: 12,
786
- padding: 20,
787
- backgroundColor: "#000",
788
- },
789
- errorText: {
790
- color: "#ff6b6b",
791
- fontSize: 14,
792
- textAlign: "center",
793
- },
794
- errorHint: {
795
- color: "#9aa0aa",
796
- fontSize: 12,
797
- textAlign: "center",
798
- },
799
- centerBadge: {
800
-
801
- justifyContent: "center",
802
- alignItems: "center",
803
- gap: 12,
804
- },
805
- loadingText: { color: "#fff", fontSize: 14 },
806
- subErrorWrap: {
807
- position: "absolute",
808
- left: 0,
809
- right: 0,
810
- bottom: 56,
811
- alignItems: "center",
812
- paddingHorizontal: 24,
813
- zIndex: 5,
814
- },
815
- subErrorText: {
816
- backgroundColor: "rgba(229,9,20,0.8)",
817
- color: "#fff",
818
- fontSize: 12,
819
- textAlign: "center",
820
- paddingHorizontal: 10,
821
- paddingVertical: 4,
822
- borderRadius: 4,
823
- overflow: "hidden",
824
- },
825
- });