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
@@ -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
+ });