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
@@ -272,13 +272,20 @@ function handleRequest(socket: TcpSocketLike, req: ParsedRequest): void {
272
272
  console.warn(`[megaStream] unexpected method ${req.method} — serving as GET`);
273
273
  }
274
274
 
275
- const [sessionId, name] = req.target.replace(/^\//, "").split(/\?|#/)[0].split("/", 2);
275
+ const [sessionId, encodedName] = req.target.replace(/^\//, "").split(/\?|#/)[0].split("/", 2);
276
276
  if (!sessionId) return serveError(socket, 404, "Not Found");
277
277
 
278
278
  const session = sessions.get(sessionId);
279
279
  if (!session) return serveError(socket, 404, `No stream session ${sessionId}`);
280
280
 
281
- if (name) {
281
+ if (encodedName) {
282
+ // Clients send the URL-encoded name on the wire (e.g. "My%20Video.mp4").
283
+ let name: string;
284
+ try {
285
+ name = decodeURIComponent(encodedName);
286
+ } catch {
287
+ name = encodedName; // not valid encoding — match literally
288
+ }
282
289
  const part = session.parts.find((p) => p.name === name);
283
290
  if (!part) return serveError(socket, 404, `Not Found: ${name}`);
284
291
  return serveSingleFile(socket, req, part.file, part.size);
@@ -339,7 +346,7 @@ export async function createStream(megaUrl: string): Promise<{ url: string; sess
339
346
  const name = file.name || "stream.mp4";
340
347
  sessions.set(sessionId, { parts: [{ name, file, size: file.size }] });
341
348
 
342
- const url = `${proxyBase}/${sessionId}/${name}`;
349
+ const url = `${proxyBase}/${sessionId}/${encodeURIComponent(name)}`;
343
350
  console.log(`[megaStream] session ${sessionId}: serving '${name}' via ${url}`);
344
351
  return { url, sessionId };
345
352
  }
@@ -1,3 +0,0 @@
1
- export { MegaVideoPlayerAndroid, type MegaVideoPlayerAndroidProps, type MegaVideoPlayerAndroidHandle, } from "./playerAndroid/MegaVideoPlayerAndroid.js";
2
- export { PlayerControlsAndroid, type PlayerControlsAndroidProps } from "./playerAndroid/PlayerControlsAndroid.js";
3
- export { SubtitleDisplayAndroid, type SubCueShapeAndroid } from "./playerAndroid/SubtitleDisplayAndroid.js";
@@ -1,3 +0,0 @@
1
- export { MegaVideoPlayerAndroid, } from "./playerAndroid/MegaVideoPlayerAndroid.js";
2
- export { PlayerControlsAndroid } from "./playerAndroid/PlayerControlsAndroid.js";
3
- export { SubtitleDisplayAndroid } from "./playerAndroid/SubtitleDisplayAndroid.js";
@@ -1,624 +0,0 @@
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
- });