mega-stream-proxy 0.1.9

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 (88) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +518 -0
  3. package/android/build.gradle +31 -0
  4. package/android/src/main/AndroidManifest.xml +3 -0
  5. package/dist/Player/native.d.ts +3 -0
  6. package/dist/Player/native.js +3 -0
  7. package/dist/Player/playerAndroid/MegaVideoPlayerAndroid.d.ts +47 -0
  8. package/dist/Player/playerAndroid/MegaVideoPlayerAndroid.js +624 -0
  9. package/dist/Player/playerAndroid/PlayerControlsAndroid.d.ts +54 -0
  10. package/dist/Player/playerAndroid/PlayerControlsAndroid.js +370 -0
  11. package/dist/Player/playerAndroid/SubtitleDisplayAndroid.d.ts +21 -0
  12. package/dist/Player/playerAndroid/SubtitleDisplayAndroid.js +55 -0
  13. package/dist/Player/playerAndroid/index.d.ts +3 -0
  14. package/dist/Player/playerAndroid/index.js +3 -0
  15. package/dist/Player/playerweb/MegaVideoPlayer.d.ts +92 -0
  16. package/dist/Player/playerweb/MegaVideoPlayer.js +368 -0
  17. package/dist/Player/playerweb/PlayerControls.d.ts +52 -0
  18. package/dist/Player/playerweb/PlayerControls.js +346 -0
  19. package/dist/Player/playerweb/SubtitleDisplay.d.ts +15 -0
  20. package/dist/Player/playerweb/SubtitleDisplay.js +32 -0
  21. package/dist/Player/playerweb/index.d.ts +3 -0
  22. package/dist/Player/playerweb/index.js +3 -0
  23. package/dist/Player/web.d.ts +3 -0
  24. package/dist/Player/web.js +3 -0
  25. package/dist/accounts/async-storage.d.ts +10 -0
  26. package/dist/accounts/async-storage.js +46 -0
  27. package/dist/accounts/index.d.ts +4 -0
  28. package/dist/accounts/index.js +3 -0
  29. package/dist/accounts/local-storage.d.ts +3 -0
  30. package/dist/accounts/local-storage.js +63 -0
  31. package/dist/accounts/manager.d.ts +39 -0
  32. package/dist/accounts/manager.js +149 -0
  33. package/dist/accounts/types.d.ts +35 -0
  34. package/dist/accounts/types.js +1 -0
  35. package/dist/index.d.ts +5 -0
  36. package/dist/index.js +4 -0
  37. package/dist/megaFile.d.ts +7 -0
  38. package/dist/megaFile.js +31 -0
  39. package/dist/proxy/native.d.ts +25 -0
  40. package/dist/proxy/native.js +336 -0
  41. package/dist/proxy/sw/bundle.d.ts +21 -0
  42. package/dist/proxy/sw/bundle.js +32 -0
  43. package/dist/proxy/sw/megajs.browser.js +27 -0
  44. package/dist/proxy/sw/sw.js +212 -0
  45. package/dist/proxy/types.d.ts +15 -0
  46. package/dist/proxy/types.js +2 -0
  47. package/dist/proxy/web.d.ts +24 -0
  48. package/dist/proxy/web.js +231 -0
  49. package/dist/ranges.d.ts +30 -0
  50. package/dist/ranges.js +63 -0
  51. package/dist/stream.d.ts +9 -0
  52. package/dist/stream.js +100 -0
  53. package/dist/subtitles/index.d.ts +9 -0
  54. package/dist/subtitles/index.js +2 -0
  55. package/dist/subtitles/load.d.ts +18 -0
  56. package/dist/subtitles/load.js +58 -0
  57. package/dist/subtitles/parse.d.ts +19 -0
  58. package/dist/subtitles/parse.js +95 -0
  59. package/dist/swDefine.d.ts +5 -0
  60. package/dist/swDefine.js +5 -0
  61. package/package.json +112 -0
  62. package/src/Player/playerAndroid/MegaVideoPlayerAndroid.tsx +825 -0
  63. package/src/Player/playerAndroid/PlayerControlsAndroid.tsx +824 -0
  64. package/src/Player/playerAndroid/SubtitleDisplayAndroid.tsx +80 -0
  65. package/src/Player/playerAndroid/index.ts +7 -0
  66. package/src/Player/playerweb/MegaVideoPlayer.tsx +551 -0
  67. package/src/Player/playerweb/PlayerControls.tsx +798 -0
  68. package/src/Player/playerweb/SubtitleDisplay.tsx +48 -0
  69. package/src/Player/playerweb/index.ts +3 -0
  70. package/src/accounts/async-storage.ts +55 -0
  71. package/src/accounts/index.ts +9 -0
  72. package/src/accounts/local-storage.ts +66 -0
  73. package/src/accounts/manager.ts +174 -0
  74. package/src/accounts/types.ts +38 -0
  75. package/src/index.ts +10 -0
  76. package/src/megaFile.ts +48 -0
  77. package/src/proxy/native.ts +396 -0
  78. package/src/proxy/sw/bundle.ts +37 -0
  79. package/src/proxy/sw/megajs.browser.js +27 -0
  80. package/src/proxy/sw/sw.js +212 -0
  81. package/src/proxy/types.ts +17 -0
  82. package/src/proxy/web.ts +266 -0
  83. package/src/ranges.ts +72 -0
  84. package/src/stream.ts +117 -0
  85. package/src/subtitles/index.ts +15 -0
  86. package/src/subtitles/load.ts +83 -0
  87. package/src/subtitles/parse.ts +105 -0
  88. package/src/swDefine.ts +5 -0
@@ -0,0 +1,624 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // The exact MyMovies episode player (components/MegaVideoPlayer.web.tsx),
3
+ // packaged as a cross-platform component (Android / iOS / web). Driven by the
4
+ // react-native-video `<Video>` component, with ordered part URLs played
5
+ // back-to-back and auto-advancing to the next part when the current one ends.
6
+ // Sources come in as proxy URLs — from `mega-stream-proxy/native`
7
+ // (http://127.0.0.1:<port>/…) on Android/iOS or from the Service Worker
8
+ // (`/__mmstream__/…`) on the web.
9
+ import React, { forwardRef, useEffect, useImperativeHandle, useMemo, useState, } from "react";
10
+ import { ActivityIndicator, Platform, StyleSheet, Text, View, } from "react-native";
11
+ import { Video } from "react-native-video";
12
+ import { Ionicons } from "@expo/vector-icons";
13
+ import { PlayerControlsAndroid } from "./PlayerControlsAndroid.js";
14
+ import { SubtitleDisplayAndroid } from "./SubtitleDisplayAndroid.js";
15
+ import { loadSubtitles, } from "../../subtitles/index.js";
16
+ export const MegaVideoPlayerAndroid = forwardRef(function MegaVideoPlayer({ uri, sources, title, subtitleUrl, subtitleFormat, subtitleLang, subtitleTracks: subtitleTracksProp, initialSubtitleLang, subtitleOffset, resolutions, resolution, onSelectResolution, autoPlay = true, onEnd, onError, onProgress, onFullscreenChange, width, height, }, ref) {
17
+ const [playing, setPlaying] = useState(false);
18
+ const [buffering, setBuffering] = useState(true);
19
+ const [loading, setLoading] = useState(false);
20
+ const [currentTime, setCurrentTime] = useState(0);
21
+ const [duration, setDuration] = useState(0);
22
+ const [muted, setMuted] = useState(false);
23
+ const [volume, setVolume] = useState(1);
24
+ const [visible, setVisible] = useState(true);
25
+ const [subtitlesOn, setSubtitlesOn] = useState(true);
26
+ const [subFontSize, setSubFontSize] = useState(14);
27
+ const [cues, setCues] = useState([]);
28
+ const [subtitleError, setSubtitleError] = useState(null);
29
+ const [error, setError] = useState(null);
30
+ const [fullscreen, setFullscreen] = useState(false);
31
+ const [controlsBump, setControlsBump] = useState(0);
32
+ const [sourceUri, setSourceUri] = useState(null);
33
+ const [tracks, setTracks] = useState([]);
34
+ // Keep the `<Video>` source object identity stable — a fresh `{ uri }` on
35
+ // every render makes the native player re-evaluate (and on some platforms
36
+ // reload) the source, which resets the playback clock.
37
+ const videoSource = useMemo(() => (sourceUri ? { uri: sourceUri } : undefined), [sourceUri]);
38
+ const videoRef = React.useRef(null);
39
+ const mountedRef = React.useRef(true);
40
+ const autoplayRef = React.useRef(autoPlay !== false);
41
+ const hideTimerRef = React.useRef(null);
42
+ const subTrackIdxRef = React.useRef(0);
43
+ const sourceIndexRef = React.useRef(Math.max(0, (sources ?? []).indexOf(uri ?? "")));
44
+ /** sources that auto-advance on end. Quality-switch files are full merged
45
+ * sources, so they stop part auto-advance. */
46
+ const activeSourcesRef = React.useRef(sources ?? []);
47
+ /** position to resume a quality switch at, applied once playback is moving */
48
+ const pendingResumeRef = React.useRef(null);
49
+ const prevResolutionRef = React.useRef(resolution);
50
+ /** web-only: the UI's intended play state + element volumes, so the DOM
51
+ * bridge can operate the raw <video> element without React re-renders. */
52
+ const playingRef = React.useRef(false);
53
+ const volumeRef = React.useRef(volume);
54
+ const mutedRef = React.useRef(muted);
55
+ const onErrorRef = React.useRef(onError);
56
+ const onProgressRef = React.useRef(onProgress);
57
+ // ---- source management ----
58
+ async function add(source, opts) {
59
+ if (!source)
60
+ return;
61
+ setError(null);
62
+ setLoading(true);
63
+ setBuffering(true);
64
+ setSourceUri(source);
65
+ setCurrentTime(0);
66
+ setDuration(0);
67
+ if (!mountedRef.current)
68
+ return;
69
+ console.log(`[MegaVideoPlayer] add: ${source} (${opts?.title ?? title})`);
70
+ if (autoplayRef.current) {
71
+ setPlaying(true);
72
+ scheduleHide();
73
+ }
74
+ }
75
+ function stop() {
76
+ setSourceUri(null);
77
+ setPlaying(false);
78
+ }
79
+ useImperativeHandle(ref, () => ({ add, stop }), [add, stop]);
80
+ // ---- player controllers ----
81
+ const playPause = () => {
82
+ setPlaying((p) => !p);
83
+ setVisible(true);
84
+ if (Platform.OS === "web") {
85
+ const el = videoRef.current?.nativeHtmlVideoRef?.current ?? null;
86
+ if (el) {
87
+ try {
88
+ if (playing) {
89
+ el.pause();
90
+ }
91
+ else {
92
+ setLoading(false);
93
+ const p = el.play();
94
+ if (p && typeof p.catch === "function")
95
+ p.catch(() => { });
96
+ }
97
+ }
98
+ catch { }
99
+ }
100
+ }
101
+ };
102
+ const skip = (delta) => {
103
+ const next = Math.min(Math.max(currentTime + delta, 0), duration);
104
+ if (Platform.OS === "web") {
105
+ const el = videoRef.current?.nativeHtmlVideoRef?.current ?? null;
106
+ if (el) {
107
+ try {
108
+ el.currentTime = next;
109
+ }
110
+ catch { }
111
+ }
112
+ }
113
+ else {
114
+ videoRef.current?.seek(next);
115
+ }
116
+ setCurrentTime(next);
117
+ setVisible(true);
118
+ scheduleHide();
119
+ };
120
+ const seek = (seconds) => {
121
+ const clamped = Math.min(Math.max(seconds, 0), duration || seconds);
122
+ if (Platform.OS === "web") {
123
+ const el = videoRef.current?.nativeHtmlVideoRef?.current ?? null;
124
+ if (el) {
125
+ try {
126
+ el.currentTime = clamped;
127
+ }
128
+ catch { }
129
+ }
130
+ }
131
+ else {
132
+ videoRef.current?.seek(clamped);
133
+ }
134
+ setCurrentTime(clamped);
135
+ setVisible(true);
136
+ scheduleHide();
137
+ };
138
+ const toggleMute = () => {
139
+ setMuted((m) => {
140
+ const next = !m;
141
+ mutedRef.current = next;
142
+ if (Platform.OS === "web") {
143
+ const el = videoRef.current?.nativeHtmlVideoRef?.current ?? null;
144
+ if (el) {
145
+ try {
146
+ el.muted = next;
147
+ }
148
+ catch { }
149
+ }
150
+ }
151
+ return next;
152
+ });
153
+ setVisible(true);
154
+ };
155
+ const changeVolume = (v) => {
156
+ const clamped = Math.min(Math.max(v, 0), 1);
157
+ volumeRef.current = clamped;
158
+ setVolume(clamped);
159
+ if (clamped > 0)
160
+ setMuted(false);
161
+ if (Platform.OS === "web") {
162
+ const el = videoRef.current?.nativeHtmlVideoRef?.current ?? null;
163
+ if (el) {
164
+ try {
165
+ el.volume = clamped;
166
+ }
167
+ catch { }
168
+ }
169
+ }
170
+ };
171
+ const toggleSubtitles = () => {
172
+ setSubtitlesOn((s) => !s);
173
+ };
174
+ const toggleControls = () => {
175
+ setVisible((v) => !v);
176
+ if (!visible && playing)
177
+ scheduleHide();
178
+ };
179
+ /** any interaction with the control bar — reset the auto-hide timer */
180
+ const onInteract = () => {
181
+ setControlsBump((n) => n + 1);
182
+ };
183
+ const enterFullscreen = (next) => {
184
+ // PlayerControls calls through onPress, which passes the press event as
185
+ // the first argument — ignore it (it is not the desired fullscreen value).
186
+ const target = typeof next === "boolean" ? next : !fullscreen;
187
+ setFullscreen(target);
188
+ setVisible(true);
189
+ onFullscreenChange?.(target);
190
+ if (Platform.OS === "web") {
191
+ const doc = globalThis?.document ?? null;
192
+ const el = videoRef.current?.nativeHtmlVideoRef?.current ?? null;
193
+ try {
194
+ if (target) {
195
+ el?.requestFullscreen?.();
196
+ }
197
+ else if (doc?.fullscreenElement) {
198
+ doc.exitFullscreen();
199
+ }
200
+ }
201
+ catch { }
202
+ }
203
+ };
204
+ const changeSubtitleFontSize = (px) => {
205
+ setSubFontSize(px);
206
+ };
207
+ const selectSubtitleTrack = (lang) => {
208
+ const idx = tracks.findIndex((t) => t.lang === lang);
209
+ if (idx >= 0) {
210
+ subTrackIdxRef.current = idx;
211
+ setSubtitlesOn(true);
212
+ loadCues(tracks);
213
+ }
214
+ };
215
+ // ---- react-native-video handlers ----
216
+ const onVideoLoad = (data) => {
217
+ const d = data.duration ?? duration;
218
+ setDuration(d);
219
+ setBuffering(false);
220
+ setLoading(false);
221
+ console.log(`[MegaVideoPlayer] onLoad duration=${d}`);
222
+ };
223
+ const onVideoProgress = (data) => {
224
+ const t = data.currentTime ?? 0;
225
+ // Always move the clock first — never let a pending quality-switch resume
226
+ // swallow progress events.
227
+ setCurrentTime(t);
228
+ onProgress?.({ currentTime: t, duration });
229
+ const pending = pendingResumeRef.current;
230
+ // quality switch: resume the previous position once the new stream is
231
+ // actually flowing (t > 0) — seeking too early can stall the load.
232
+ if (pending != null && t > 0 && duration > 0) {
233
+ pendingResumeRef.current = null;
234
+ const target = duration > pending + 20 ? pending : Math.max(duration - 20, 0);
235
+ videoRef.current?.seek(target);
236
+ setCurrentTime(target);
237
+ }
238
+ };
239
+ const onVideoPlaybackStateChanged = (data) => {
240
+ if (!mountedRef.current)
241
+ return;
242
+ // Web only: browsers may deny autoplay, so mirror the real element state
243
+ // back into the UI. On native, `isPlaying` drops to false while buffering,
244
+ // which must NOT be interpreted as a user pause.
245
+ if (Platform.OS !== "web")
246
+ return;
247
+ if (typeof data?.isPlaying === "boolean") {
248
+ setPlaying((prev) => (prev === data.isPlaying ? prev : data.isPlaying));
249
+ if (data.isPlaying) {
250
+ setBuffering(false);
251
+ setLoading(false);
252
+ }
253
+ }
254
+ };
255
+ const onVideoError = (e) => {
256
+ if (!mountedRef.current)
257
+ return;
258
+ const message = e?.error?.message || e?.error?.errorString || String(e) || "Video error";
259
+ console.warn("[MegaVideoPlayer] onError:", message);
260
+ setError(message);
261
+ setBuffering(false);
262
+ setLoading(false);
263
+ onError?.(message);
264
+ };
265
+ const onVideoEnd = () => {
266
+ const list = activeSourcesRef.current;
267
+ const next = list[sourceIndexRef.current + 1];
268
+ if (next) {
269
+ sourceIndexRef.current += 1;
270
+ console.log(`[MegaVideoPlayer] advancing to part ${sourceIndexRef.current + 1}/${list.length}`);
271
+ void add(next, { title });
272
+ }
273
+ else {
274
+ setPlaying(false);
275
+ setVisible(true);
276
+ onEnd?.();
277
+ }
278
+ };
279
+ // ---- helpers ----
280
+ function scheduleHide() {
281
+ if (hideTimerRef.current)
282
+ clearTimeout(hideTimerRef.current);
283
+ hideTimerRef.current = setTimeout(() => {
284
+ setPlaying((p) => {
285
+ if (p)
286
+ setVisible(false);
287
+ return p;
288
+ });
289
+ }, 3000);
290
+ }
291
+ function loadCues(trackList = tracks) {
292
+ const track = trackList[Math.min(subTrackIdxRef.current, trackList.length - 1)] ?? null;
293
+ setCues([]);
294
+ setSubtitleError(null);
295
+ if (!track)
296
+ return;
297
+ loadSubtitles(track.url, { format: track.format ?? undefined })
298
+ .then((c) => {
299
+ if (!mountedRef.current)
300
+ return;
301
+ setCues(c);
302
+ setSubtitleError(c.length === 0 ? "No cues parsed from this subtitle file" : null);
303
+ })
304
+ .catch((e) => {
305
+ console.warn("[subtitles] load failed:", track.url, e);
306
+ if (mountedRef.current)
307
+ setSubtitleError("Subtitles unavailable");
308
+ });
309
+ }
310
+ function syncSubtitleTracks() {
311
+ const nextTracks = subtitleTracksProp?.length
312
+ ? subtitleTracksProp
313
+ : subtitleUrl
314
+ ? [
315
+ {
316
+ lang: subtitleLang ?? "SRT",
317
+ url: subtitleUrl,
318
+ format: subtitleFormat,
319
+ },
320
+ ]
321
+ : [];
322
+ setTracks(nextTracks);
323
+ // Preselect the requested track (from the watch screen's subtitle picker)
324
+ if (initialSubtitleLang) {
325
+ const idx = nextTracks.findIndex((t) => t.lang === initialSubtitleLang);
326
+ if (idx >= 0)
327
+ subTrackIdxRef.current = idx;
328
+ }
329
+ loadCues(nextTracks);
330
+ }
331
+ // ---- effects ----
332
+ useEffect(() => {
333
+ mountedRef.current = true;
334
+ autoplayRef.current = autoPlay !== false;
335
+ return () => {
336
+ mountedRef.current = false;
337
+ if (hideTimerRef.current)
338
+ clearTimeout(hideTimerRef.current);
339
+ };
340
+ }, [autoPlay]);
341
+ useEffect(() => {
342
+ if (!uri)
343
+ return;
344
+ sourceIndexRef.current = Math.max(0, (sources ?? []).indexOf(uri));
345
+ const prevRes = prevResolutionRef.current;
346
+ const qualitySwitch = resolution != null && resolution !== prevRes;
347
+ prevResolutionRef.current = resolution;
348
+ if (qualitySwitch) {
349
+ pendingResumeRef.current = currentTime;
350
+ const isDefaultChain = (sources ?? []).includes(uri ?? "");
351
+ // a resolution file is one merged source — don't auto-advance through it
352
+ activeSourcesRef.current = isDefaultChain ? (sources ?? []) : [];
353
+ }
354
+ else {
355
+ pendingResumeRef.current = null;
356
+ activeSourcesRef.current = sources ?? [];
357
+ }
358
+ void add(uri, { title });
359
+ // eslint-disable-next-line react-hooks/exhaustive-deps
360
+ }, [uri]);
361
+ useEffect(() => {
362
+ activeSourcesRef.current = sources ?? [];
363
+ }, [sources]);
364
+ useEffect(() => {
365
+ syncSubtitleTracks();
366
+ // eslint-disable-next-line react-hooks/exhaustive-deps
367
+ }, [subtitleTracksProp, subtitleUrl]);
368
+ useEffect(() => {
369
+ if (!visible || !playing)
370
+ return;
371
+ const t = setTimeout(() => setVisible(false), 3000);
372
+ return () => clearTimeout(t);
373
+ }, [visible, playing, controlsBump]);
374
+ // Safety net for the playback clock: some platforms/builds deliver
375
+ // `onProgress` sparsely. While playing, poll the player position directly so
376
+ // the timer, scrubber and subtitles keep advancing.
377
+ useEffect(() => {
378
+ if (!playing)
379
+ return;
380
+ let cancelled = false;
381
+ const id = setInterval(async () => {
382
+ try {
383
+ const pos = await videoRef.current?.getCurrentPosition?.();
384
+ if (cancelled || typeof pos !== "number" || !Number.isFinite(pos))
385
+ return;
386
+ setCurrentTime((prev) => (Math.abs(prev - pos) > 0.4 ? pos : prev));
387
+ }
388
+ catch {
389
+ // player not ready yet — ignore
390
+ }
391
+ }, 500);
392
+ return () => {
393
+ cancelled = true;
394
+ clearInterval(id);
395
+ };
396
+ }, [playing]);
397
+ useEffect(() => {
398
+ playingRef.current = playing;
399
+ }, [playing]);
400
+ useEffect(() => {
401
+ onErrorRef.current = onError;
402
+ onProgressRef.current = onProgress;
403
+ }, [onError, onProgress]);
404
+ // Web-only: react-native-video's web driver only maps a few DOM events
405
+ // (onLoad <-> durationchange, sparse onProgress) and calls `play()` on load,
406
+ // which browsers deny without a user gesture. Instead of fighting that, take
407
+ // over the raw <video> element directly: attach our own listeners, mirror the
408
+ // clock/duration/buffering/loading state from the element, keep the icon in
409
+ // sync with the real paused state, force-play whenever the UI wants playback
410
+ // but the element drifted to paused (autoplay-denial recovery), and apply
411
+ // volume/muted/fullscreen changes. Native stays on react-native-video.
412
+ useEffect(() => {
413
+ if (Platform.OS !== "web")
414
+ return;
415
+ let cancelled = false;
416
+ let el = null;
417
+ let listeners = [];
418
+ const detach = () => {
419
+ if (!el)
420
+ return;
421
+ for (const [type, fn] of listeners) {
422
+ try {
423
+ el.removeEventListener(type, fn);
424
+ }
425
+ catch { }
426
+ }
427
+ listeners = [];
428
+ el = null;
429
+ };
430
+ const add = (type, fn) => {
431
+ try {
432
+ el.addEventListener(type, fn);
433
+ listeners.push([type, fn]);
434
+ }
435
+ catch { }
436
+ };
437
+ const attach = () => {
438
+ const next = videoRef.current?.nativeHtmlVideoRef?.current ?? null;
439
+ if (!next)
440
+ return null;
441
+ if (next === el)
442
+ return el;
443
+ detach();
444
+ el = next;
445
+ el.muted = mutedRef.current;
446
+ el.volume = Math.min(Math.max(volumeRef.current, 0), 1);
447
+ add("timeupdate", () => {
448
+ const t = typeof el.currentTime === "number" ? el.currentTime : 0;
449
+ setCurrentTime((prev) => (Math.abs(prev - t) > 0.25 ? t : prev));
450
+ const d = typeof el.duration === "number" ? el.duration : 0;
451
+ onProgressRef.current?.({
452
+ currentTime: t,
453
+ duration: Number.isFinite(d) ? d : 0,
454
+ });
455
+ });
456
+ add("durationchange", () => {
457
+ const d = typeof el.duration === "number" ? el.duration : 0;
458
+ if (typeof d === "number" && Number.isFinite(d) && d > 0) {
459
+ setDuration(d);
460
+ }
461
+ setLoading(false);
462
+ setBuffering(false);
463
+ });
464
+ add("play", () => {
465
+ setLoading(false);
466
+ setBuffering(false);
467
+ setPlaying(true);
468
+ });
469
+ add("pause", () => {
470
+ setBuffering(false);
471
+ setPlaying(false);
472
+ });
473
+ add("waiting", () => setBuffering(true));
474
+ add("canplay", () => {
475
+ setBuffering(false);
476
+ setLoading(false);
477
+ });
478
+ add("loadstart", () => setLoading(true));
479
+ add("error", () => {
480
+ const msg = (el?.error && (el.error.message || `Video error ${el.error.code}`)) ||
481
+ "Video decode error";
482
+ console.warn("[MegaVideoPlayer] web element error:", msg);
483
+ setLoading(false);
484
+ setBuffering(false);
485
+ setError(msg);
486
+ onError?.(msg);
487
+ });
488
+ const doc = globalThis?.document ?? null;
489
+ if (doc) {
490
+ add("fullscreenchange", () => {
491
+ try {
492
+ setFullscreen(!!doc.fullscreenElement);
493
+ }
494
+ catch { }
495
+ });
496
+ }
497
+ return el;
498
+ };
499
+ const id = setInterval(() => {
500
+ if (cancelled)
501
+ return;
502
+ const elem = attach();
503
+ if (!elem)
504
+ return;
505
+ try {
506
+ // Autoplay-deadlock breaker: the UI wants playback, but the browser
507
+ // held the element at pause (initial autoplay denied, or a temporary
508
+ // browser pause). Re-assert play every tick so a single user gesture
509
+ // anywhere in the app unblocks it; seek once we finally have a size.
510
+ if (playingRef.current && elem.paused && !elem.ended) {
511
+ setLoading(false);
512
+ el = elem;
513
+ const p = elem.play();
514
+ if (p && typeof p.catch === "function")
515
+ p.catch(() => { });
516
+ }
517
+ // Mirror the real state back so the icon can never lie about pause.
518
+ const real = elem.paused;
519
+ if (real && playingRef.current && elem.ended) {
520
+ setPlaying(false);
521
+ }
522
+ }
523
+ catch { }
524
+ }, 500);
525
+ return () => {
526
+ cancelled = true;
527
+ clearInterval(id);
528
+ detach();
529
+ };
530
+ // eslint-disable-next-line react-hooks/exhaustive-deps
531
+ }, []);
532
+ // ---- render ----
533
+ const activeCue = cues.find((c) => currentTime - (subtitleOffset ?? 0) >= c.start &&
534
+ currentTime - (subtitleOffset ?? 0) < c.end) ?? null;
535
+ const activeTrack = tracks[Math.min(subTrackIdxRef.current, tracks.length - 1)] ?? null;
536
+ if (error) {
537
+ return (_jsxs(View, { style: [
538
+ styles.wrap,
539
+ styles.center,
540
+ {
541
+ width: width ? width : "100%",
542
+ height: height ? height : "100%",
543
+ flexDirection: "column",
544
+ },
545
+ ], children: [_jsx(Ionicons, { name: "alert-circle-outline", size: 40, color: "#ff6b6b" }), _jsx(Text, { style: styles.errorText, children: error }), _jsx(Text, { style: styles.errorHint, children: "Use \"Download & Play\" instead." })] }));
546
+ }
547
+ return (_jsxs(View, { style: [
548
+ styles.wrap,
549
+ { width: width ? width : "100%", height: height ? height : "100%" },
550
+ fullscreen && styles.fullscreen,
551
+ ], children: [sourceUri ? (_jsx(Video, { ref: videoRef, source: videoSource, style: [
552
+ StyleSheet.absoluteFill,
553
+ { width: width ? width : "100%", height: height ? height : "100%" },
554
+ ], resizeMode: "contain", paused: !playing, muted: muted, volume: Platform.OS === "web" ? undefined : volume, progressUpdateInterval: 1000, onLoadStart: () => {
555
+ if (mountedRef.current)
556
+ setBuffering(true);
557
+ }, onLoad: onVideoLoad, onBuffer: ({ isBuffering }) => {
558
+ if (mountedRef.current)
559
+ setBuffering(!!isBuffering);
560
+ }, onProgress: onVideoProgress, onPlaybackStateChanged: onVideoPlaybackStateChanged, onError: onVideoError, onEnd: onVideoEnd })) : null, loading && !buffering && (_jsxs(View, { style: [
561
+ styles.centerBadge,
562
+ StyleSheet.absoluteFill,
563
+ { width: width ? width : "100%", height: height ? height : "100%" },
564
+ ], pointerEvents: "none", children: [_jsx(ActivityIndicator, { color: "#e50914", size: "large" }), _jsx(Text, { style: styles.loadingText, children: "Starting stream\u2026" })] })), _jsx(SubtitleDisplayAndroid, { cue: activeCue, enabled: subtitlesOn, fontSize: subFontSize, width: width, height: height }), subtitlesOn && subtitleError && !cues.length && (_jsx(View, { pointerEvents: "none", style: styles.subErrorWrap, children: _jsxs(Text, { style: styles.subErrorText, children: [subtitleError, " \u00B7 ", activeTrack?.lang ?? "no track"] }) })), _jsx(PlayerControlsAndroid, { playing: playing, buffering: buffering, currentTime: currentTime, duration: duration, muted: muted, title: title, visible: visible, subtitlesOn: subtitlesOn, volume: volume, subtitleTracks: tracks.map((t) => t.lang), subtitleTrack: activeTrack?.lang, onSelectSubtitleTrack: selectSubtitleTrack, subtitleFontSize: subFontSize, onChangeSubtitleFontSize: changeSubtitleFontSize, onToggleVisible: toggleControls, onPlayPause: playPause, onSkip: skip, onSeek: seek, onToggleMute: toggleMute, onToggleSubtitles: toggleSubtitles, onVolume: changeVolume, resolutions: resolutions, resolution: resolution, onSelectResolution: onSelectResolution, onFullscreen: enterFullscreen, fullscreen: fullscreen, onInteract: onInteract, width: width, height: height })] }));
565
+ });
566
+ const styles = StyleSheet.create({
567
+ wrap: {
568
+ flex: 1,
569
+ width: "100%",
570
+ height: "100%",
571
+ backgroundColor: "#000",
572
+ },
573
+ fullscreen: {
574
+ position: "absolute",
575
+ left: 0,
576
+ right: 0,
577
+ top: 0,
578
+ bottom: 0,
579
+ zIndex: 10,
580
+ },
581
+ center: {
582
+ flex: 1,
583
+ justifyContent: "center",
584
+ alignItems: "center",
585
+ gap: 12,
586
+ padding: 20,
587
+ backgroundColor: "#000",
588
+ },
589
+ errorText: {
590
+ color: "#ff6b6b",
591
+ fontSize: 14,
592
+ textAlign: "center",
593
+ },
594
+ errorHint: {
595
+ color: "#9aa0aa",
596
+ fontSize: 12,
597
+ textAlign: "center",
598
+ },
599
+ centerBadge: {
600
+ justifyContent: "center",
601
+ alignItems: "center",
602
+ gap: 12,
603
+ },
604
+ loadingText: { color: "#fff", fontSize: 14 },
605
+ subErrorWrap: {
606
+ position: "absolute",
607
+ left: 0,
608
+ right: 0,
609
+ bottom: 56,
610
+ alignItems: "center",
611
+ paddingHorizontal: 24,
612
+ zIndex: 5,
613
+ },
614
+ subErrorText: {
615
+ backgroundColor: "rgba(229,9,20,0.8)",
616
+ color: "#fff",
617
+ fontSize: 12,
618
+ textAlign: "center",
619
+ paddingHorizontal: 10,
620
+ paddingVertical: 4,
621
+ borderRadius: 4,
622
+ overflow: "hidden",
623
+ },
624
+ });
@@ -0,0 +1,54 @@
1
+ export type PlayerControlsAndroidProps = {
2
+ playing: boolean;
3
+ buffering: boolean;
4
+ currentTime: number;
5
+ duration: number;
6
+ muted: boolean;
7
+ title?: string;
8
+ visible: boolean;
9
+ subtitlesOn: boolean;
10
+ volume: number;
11
+ /** subtitle language labels (when >1 the CC switcher is shown) */
12
+ subtitleTracks?: string[];
13
+ /** currently active subtitle language */
14
+ subtitleTrack?: string;
15
+ /** select a subtitle language from the list directly */
16
+ onSelectSubtitleTrack?: (lang: string) => void;
17
+ /** current subtitle font size (px) */
18
+ subtitleFontSize?: number;
19
+ /** adjust the subtitle font size (px) */
20
+ onChangeSubtitleFontSize?: (px: number) => void;
21
+ /** 0..1 — how much of the whole merged episode has been downloaded. Renders
22
+ * a second, thinner line under the playhead track. */
23
+ loadProgress?: number;
24
+ /** resolution labels in display order — the FIRST is the default source
25
+ * (usually "HD: <url>"), e.g. ["Default", "1080p", "720p"]. Shown in the
26
+ * settings popup. */
27
+ resolutions?: string[];
28
+ /** currently active resolution label */
29
+ resolution?: string;
30
+ /** user picked a resolution label — resolve it to a URL and update `uri`. */
31
+ onSelectResolution?: (label: string) => void;
32
+ /** [start, end] seconds (merged timeline) of the intro — highlighted on the
33
+ * track + shows a "Skip Intro" button while the playhead is inside it. */
34
+ introRegion?: [number, number];
35
+ /** [start, end] seconds (merged timeline) of the outro */
36
+ outroRegion?: [number, number];
37
+ onSkipIntro?: () => void;
38
+ onSkipOutro?: () => void;
39
+ onToggleVisible: () => void;
40
+ onPlayPause: () => void;
41
+ onSkip: (deltaSeconds: number) => void;
42
+ onSeek: (seconds: number) => void;
43
+ onToggleMute: () => void;
44
+ onToggleSubtitles: () => void;
45
+ onVolume: (v: number) => void;
46
+ onFullscreen: () => void;
47
+ /** current fullscreen state — switches the expand/contract icon */
48
+ fullscreen?: boolean;
49
+ /** any user interaction with the bar/menus — reset the auto-hide timer */
50
+ onInteract?: () => void;
51
+ width?: number;
52
+ height?: number;
53
+ };
54
+ export declare function PlayerControlsAndroid(props: PlayerControlsAndroidProps): import("react").JSX.Element;