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
package/README.md CHANGED
@@ -34,11 +34,15 @@ mega-link ──> megajs File.fromURL ──┬─> Android/iOS: 127.0.0.1:PORT/
34
34
 
35
35
  - **Web**: a modern browser at `https:` (or `localhost`) for Service Worker
36
36
  streaming; Node 18+ for the `copySwAssets` deploy step.
37
- - **Android / iOS**: React Native ≥ 0.71 (Expo SDK 52+ recommended) — the native
37
+ - **Android / iOS**: React Native ≥ 0.71 (Expo SDK 52 recommended) — the native
38
38
  streaming module is auto-linked, no manual Gradle/Xcode edits.
39
39
  - All platforms need `react`, `react-native`, `expo-video`, and `@expo/vector-icons`
40
40
  (Expo apps already ship the last two).
41
41
 
42
+ ## for sdk 54 + support versions
43
+ visit url https://www.npmjs.com/package/mega-proxy?activeTab=readme
44
+
45
+
42
46
  ## Install
43
47
 
44
48
  ```sh
@@ -69,7 +73,7 @@ your Expo app already provides them.
69
73
  import { useEffect, useMemo } from "react";
70
74
  import { View } from "react-native";
71
75
  import { ensureProxy, createStream, releaseStream } from "mega-stream-proxy/native";
72
- import { MegaVideoPlayerWeb } from "mega-stream-proxy/player";
76
+ import { MegaVideoPlayer } from "mega-stream-proxy/player";
73
77
 
74
78
  export function WatchScreen({ megaUrl, subtitleUrl }) {
75
79
  const ref = useMemo(() => ({ url: null as string | null, sessionId: null as string | null }), []);
@@ -93,51 +97,12 @@ export function WatchScreen({ megaUrl, subtitleUrl }) {
93
97
 
94
98
  return (
95
99
  <View style={{ flex: 1 }}>
96
- <MegaVideoPlayerWeb uri={ref.url} subtitleUrl={subtitleUrl} autoPlay />
100
+ <MegaVideoPlayer uri={ref.url} subtitleUrl={subtitleUrl} autoPlay />
97
101
  </View>
98
102
  );
99
103
  }
100
104
  ```
101
105
 
102
- ## all Platfroms (streaming)
103
-
104
- ```tsx
105
- import { useEffect, useMemo } from "react";
106
- import { View, Platform } from "react-native";
107
- import { ensureProxy, createStream, releaseStream } from "mega-stream-proxy/native";
108
- import { MegaVideoPlayerAndroid } from "mega-stream-proxy/playerAndroid";
109
- import { MegaVideoPlayerWeb } from "mega-stream-proxy/player";
110
- export function WatchScreen({ megaUrl, subtitleUrl }) {
111
- const ref = useMemo(() => ({ url: null as string | null, sessionId: null as string | null }), []);
112
-
113
- useEffect(() => {
114
- let alive = true;
115
- void ensureProxy(); // idempotent — starts the singleton server once per app lifetime
116
- createStream(megaUrl).then(({ url, sessionId }) => {
117
- if (!alive) {
118
- releaseStream(sessionId);
119
- return;
120
- }
121
- ref.url = url;
122
- ref.sessionId = sessionId;
123
- });
124
- return () => {
125
- alive = false;
126
- if (ref.sessionId) releaseStream(ref.sessionId);
127
- };
128
- }, [megaUrl]);
129
-
130
- return (
131
- <View>
132
- {Platform.OS === "web" ? (
133
- <MegaVideoPlayerWeb uri={ref.url} subtitleUrl={subtitleUrl} />
134
- ) : (
135
- <MegaVideoPlayerAndroid uri={ref.url} subtitleUrl={subtitleUrl} />
136
- )}
137
- </View>
138
- )
139
- }
140
-
141
106
  - `ensureProxy(): Promise<void>` — idempotent; binds the singleton local server once
142
107
  per app lifetime. Safe to call repeatedly; no-op until `stopProxy()`.
143
108
  - `createStream(megaUrl): Promise<{ url, sessionId }>` — one keyed session per MEGA
@@ -175,7 +140,7 @@ Server from a host that allows a Service Worker (`https:` or `localhost`).
175
140
  import { useEffect, useMemo } from "react";
176
141
  import { View } from "react-native";
177
142
  import { ensureProxy, createStream, releaseStream } from "mega-stream-proxy/web";
178
- import { MegaVideoPlayerWeb } from "mega-stream-proxy/player";
143
+ import { MegaVideoPlayer } from "mega-stream-proxy/player";
179
144
 
180
145
  export function WatchScreen({ megaUrl, subtitleUrl }) {
181
146
  const ref = useMemo(() => ({ url: null as string | null, sessionId: null as string | null }), []);
@@ -199,7 +164,7 @@ export function WatchScreen({ megaUrl, subtitleUrl }) {
199
164
 
200
165
  return (
201
166
  <View style={{ flex: 1 }}>
202
- <MegaVideoPlayerWeb uri={ref.url} subtitleUrl={subtitleUrl} autoPlay />
167
+ <MegaVideoPlayer uri={ref.url} subtitleUrl={subtitleUrl} autoPlay />
203
168
  </View>
204
169
  );
205
170
  }
@@ -309,9 +274,9 @@ The same pattern applies to multi-part episodes: `createStreams(megaUrls)` retur
309
274
 
310
275
  ## Player component
311
276
 
312
- `import { MegaVideoPlayerWeb, PlayerControlsWeb, SubtitleDisplayWeb } from "mega-stream-proxy/player";`
277
+ `import { MegaVideoPlayer, PlayerControls, SubtitleDisplay } from "mega-stream-proxy/player";`
313
278
 
314
- This is the **same player MyMovies uses** (`components/MegaVideoPlayerWeb.tsx`),
279
+ This is the **same player MyMovies uses** (`components/MegaVideoPlayer.web.tsx`),
315
280
  byte-for-byte — same expo-video engine, same custom controls (scrub bar, volume,
316
281
  skip ±10s, subtitle picker + text-size slider, intro/outro regions), same subtitle
317
282
  overlay style, same auto-hiding chrome, and the same multi-part advance logic. It
@@ -320,7 +285,7 @@ runs identically on Android, iOS and web.
320
285
  ### Simple usage — pass the subtitle URL as a prop
321
286
 
322
287
  ```tsx
323
- <MegaVideoPlayerWeb
288
+ <MegaVideoPlayer
324
289
  uri="https://…/part-0.mp4" // or the proxy URL from /native or /web
325
290
  title="Episode 1"
326
291
  subtitleUrl="https://cdn.example.com/subs.srt" // <-- the subtitle file
@@ -343,7 +308,7 @@ The player shows a subtitle selector in the controls (same UI as MyMovies):
343
308
  tabs: **Language** (the subtitle list) and **Text Size**.
344
309
 
345
310
  ```tsx
346
- <MegaVideoPlayerWeb
311
+ <MegaVideoPlayer
347
312
  uri={url}
348
313
  subtitleTracks={[
349
314
  { lang: "English", url: "https://cdn.example.com/subs.en.srt" },
@@ -376,7 +341,7 @@ Format auto-detection order: `subtitleFormat` prop → URL extension → content
376
341
  ### Multi-language tracks
377
342
 
378
343
  ```tsx
379
- <MegaVideoPlayerWeb
344
+ <MegaVideoPlayer
380
345
  uri={url}
381
346
  subtitleTracks={[
382
347
  { lang: "English", url: "https://cdn.example.com/subs.srt" },
package/dist/megaFile.js CHANGED
@@ -1,18 +1,31 @@
1
1
  const ATTRS_TIMEOUT_MS = 30_000;
2
+ const ATTRS_RETRIES = 2;
2
3
  /** Lazily resolves megajs `File.fromURL` + attributes (incl. folder-link child). */
3
4
  export async function loadMegaFile(url) {
4
5
  const { File } = (await import("megajs"));
5
6
  const file = File.fromURL(url.trim());
6
- await withTimeout(file.loadAttributes(), ATTRS_TIMEOUT_MS, "MEGA did not respond. Check your connection and try again.");
7
- // Folder links: loadAttributes resolves to the folder itself — pick the
8
- // first non-directory child instead (mirrors the megajs docs sample).
9
- const group = file;
10
- if (group.children?.length) {
11
- const child = group.find?.((n) => !n.children, true);
12
- if (child)
13
- return child;
7
+ // MEGA API calls can fail transiently (dns/rate-limit/network blip). Retry a
8
+ // couple of times before surfacing "MEGA did not respond."
9
+ let lastError;
10
+ for (let attempt = 1; attempt <= ATTRS_RETRIES; attempt++) {
11
+ try {
12
+ await withTimeout(file.loadAttributes(), ATTRS_TIMEOUT_MS, "MEGA did not respond. Check your connection and try again.");
13
+ // Folder links: loadAttributes resolves to the folder itself — pick the
14
+ // first non-directory child instead (mirrors the megajs docs sample).
15
+ const group = file;
16
+ if (group.children?.length) {
17
+ const child = group.find?.((n) => !n.children, true);
18
+ if (child)
19
+ return child;
20
+ }
21
+ return file;
22
+ }
23
+ catch (e) {
24
+ lastError = e;
25
+ console.warn(`[megaStream] loadAttributes attempt ${attempt}/${ATTRS_RETRIES} failed`);
26
+ }
14
27
  }
15
- return file;
28
+ throw lastError instanceof Error ? lastError : new Error(String(lastError));
16
29
  }
17
30
  export function fileSizeOf(file) {
18
31
  return Number(file.size ?? 0);
@@ -1,6 +1,6 @@
1
1
  import React from "react";
2
- import { type SubtitleFormat, type SubtitleTrack } from "../../subtitles/index.js";
3
- export type MegaVideoPlayerAndroidProps = {
2
+ import { type SubtitleFormat, type SubtitleTrack } from "../subtitles/index.js";
3
+ export type MegaVideoPlayerProps = {
4
4
  /** A playable URL — a direct stream or the first part URL. */
5
5
  uri?: string | null;
6
6
  /** Ordered additional part URLs for multi-part episodes, played in sequence. */
@@ -18,13 +18,6 @@ export type MegaVideoPlayerAndroidProps = {
18
18
  initialSubtitleLang?: string | null;
19
19
  /** shift the cue timestamps by an offset in seconds (can be negative) */
20
20
  subtitleOffset?: number;
21
- /** resolution labels in display order — the FIRST is the default source
22
- * (usually "HD: <url>"), e.g. ["Default", "1080p", "720p"]. */
23
- resolutions?: string[];
24
- /** currently active resolution label */
25
- resolution?: string;
26
- /** user picked a resolution label — resolve it to a URL and update `uri`. */
27
- onSelectResolution?: (label: string) => void;
28
21
  autoPlay?: boolean;
29
22
  onEnd?: () => void;
30
23
  onError?: (message: string) => void;
@@ -33,10 +26,8 @@ export type MegaVideoPlayerAndroidProps = {
33
26
  duration: number;
34
27
  }) => void;
35
28
  onFullscreenChange?: (fullscreen: boolean) => void;
36
- width?: number;
37
- height?: number;
38
29
  };
39
- export type MegaVideoPlayerAndroidHandle = {
30
+ export type MegaVideoPlayerHandle = {
40
31
  /** Loads a new playable URL into the player. */
41
32
  add: (source: string, opts?: {
42
33
  title?: string;
@@ -44,4 +35,4 @@ export type MegaVideoPlayerAndroidHandle = {
44
35
  /** Stops playback and clears the current source. */
45
36
  stop: () => void;
46
37
  };
47
- export declare const MegaVideoPlayerAndroid: React.ForwardRefExoticComponent<MegaVideoPlayerAndroidProps & React.RefAttributes<MegaVideoPlayerAndroidHandle>>;
38
+ export declare const MegaVideoPlayer: React.ForwardRefExoticComponent<MegaVideoPlayerProps & React.RefAttributes<MegaVideoPlayerHandle>>;
@@ -0,0 +1,307 @@
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, useState, } from "react";
10
+ import { ActivityIndicator, StyleSheet, Text, View, } from "react-native";
11
+ import { Video } from "react-native-video";
12
+ import { Ionicons } from "@expo/vector-icons";
13
+ import { PlayerControls } from "./PlayerControls.js";
14
+ import { SubtitleDisplay } from "./SubtitleDisplay.js";
15
+ import { loadSubtitles, } from "../subtitles/index.js";
16
+ export const MegaVideoPlayer = forwardRef(function MegaVideoPlayer({ uri, sources, title, subtitleUrl, subtitleFormat, subtitleLang, subtitleTracks: subtitleTracksProp, initialSubtitleLang, subtitleOffset, autoPlay = true, onEnd, onError, onProgress, onFullscreenChange, }, 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 [sourceUri, setSourceUri] = useState(null);
32
+ const [tracks, setTracks] = useState([]);
33
+ const videoRef = React.useRef(null);
34
+ const mountedRef = React.useRef(true);
35
+ const autoplayRef = React.useRef(autoPlay !== false);
36
+ const hideTimerRef = React.useRef(null);
37
+ const subTrackIdxRef = React.useRef(0);
38
+ const sourceIndexRef = React.useRef(Math.max(0, (sources ?? []).indexOf(uri ?? "")));
39
+ // ---- source management ----
40
+ async function add(source, opts) {
41
+ if (!source)
42
+ return;
43
+ setError(null);
44
+ setLoading(true);
45
+ setBuffering(true);
46
+ setSourceUri(source);
47
+ setCurrentTime(0);
48
+ setDuration(0);
49
+ if (!mountedRef.current)
50
+ return;
51
+ console.log(`[MegaVideoPlayer] add: ${source} (${opts?.title ?? title})`);
52
+ if (autoplayRef.current) {
53
+ setPlaying(true);
54
+ scheduleHide();
55
+ }
56
+ }
57
+ function stop() {
58
+ setSourceUri(null);
59
+ setPlaying(false);
60
+ }
61
+ useImperativeHandle(ref, () => ({ add, stop }), [add, stop]);
62
+ // ---- player controllers ----
63
+ const playPause = () => {
64
+ setPlaying((p) => !p);
65
+ setVisible(true);
66
+ };
67
+ const skip = (delta) => {
68
+ const next = Math.min(Math.max(currentTime + delta, 0), duration);
69
+ videoRef.current?.seek(next);
70
+ setCurrentTime(next);
71
+ setVisible(true);
72
+ scheduleHide();
73
+ };
74
+ const seek = (seconds) => {
75
+ const clamped = Math.min(Math.max(seconds, 0), duration || seconds);
76
+ videoRef.current?.seek(clamped);
77
+ setCurrentTime(clamped);
78
+ setVisible(true);
79
+ scheduleHide();
80
+ };
81
+ const toggleMute = () => {
82
+ setMuted((m) => !m);
83
+ setVisible(true);
84
+ };
85
+ const changeVolume = (v) => {
86
+ setVolume(v);
87
+ if (v > 0)
88
+ setMuted(false);
89
+ };
90
+ const toggleSubtitles = () => {
91
+ setSubtitlesOn((s) => !s);
92
+ };
93
+ const toggleControls = () => {
94
+ setVisible((v) => !v);
95
+ if (!visible && playing)
96
+ scheduleHide();
97
+ };
98
+ const enterFullscreen = (next) => {
99
+ // PlayerControls calls through onPress, which passes the press event as
100
+ // the first argument — ignore it (it is not the desired fullscreen value).
101
+ const target = typeof next === "boolean" ? next : !fullscreen;
102
+ setFullscreen(target);
103
+ setVisible(true);
104
+ onFullscreenChange?.(target);
105
+ };
106
+ const changeSubtitleFontSize = (px) => {
107
+ setSubFontSize(px);
108
+ };
109
+ const selectSubtitleTrack = (lang) => {
110
+ const idx = tracks.findIndex((t) => t.lang === lang);
111
+ if (idx >= 0) {
112
+ subTrackIdxRef.current = idx;
113
+ setSubtitlesOn(true);
114
+ loadCues(tracks);
115
+ }
116
+ };
117
+ // ---- react-native-video handlers ----
118
+ const onVideoLoad = (data) => {
119
+ const d = data.duration ?? duration;
120
+ setDuration(d);
121
+ setBuffering(false);
122
+ setLoading(false);
123
+ console.log(`[MegaVideoPlayer] onLoad duration=${d}`);
124
+ };
125
+ const onVideoProgress = (data) => {
126
+ const t = data.currentTime ?? 0;
127
+ setCurrentTime(t);
128
+ onProgress?.({ currentTime: t, duration });
129
+ };
130
+ const onVideoError = (e) => {
131
+ if (!mountedRef.current)
132
+ return;
133
+ const message = e?.error?.message || e?.error?.errorString || String(e) || "Video error";
134
+ console.warn("[MegaVideoPlayer] onError:", message);
135
+ setError(message);
136
+ setBuffering(false);
137
+ setLoading(false);
138
+ onError?.(message);
139
+ };
140
+ const onVideoEnd = () => {
141
+ const list = sources ?? [];
142
+ const next = list[sourceIndexRef.current + 1];
143
+ if (next) {
144
+ sourceIndexRef.current += 1;
145
+ console.log(`[MegaVideoPlayer] advancing to part ${sourceIndexRef.current + 1}/${list.length}`);
146
+ void add(next, { title });
147
+ }
148
+ else {
149
+ setPlaying(false);
150
+ setVisible(true);
151
+ onEnd?.();
152
+ }
153
+ };
154
+ // ---- helpers ----
155
+ function scheduleHide() {
156
+ if (hideTimerRef.current)
157
+ clearTimeout(hideTimerRef.current);
158
+ hideTimerRef.current = setTimeout(() => {
159
+ setPlaying((p) => {
160
+ if (p)
161
+ setVisible(false);
162
+ return p;
163
+ });
164
+ }, 3000);
165
+ }
166
+ function loadCues(trackList = tracks) {
167
+ const track = trackList[Math.min(subTrackIdxRef.current, trackList.length - 1)] ?? null;
168
+ setCues([]);
169
+ setSubtitleError(null);
170
+ if (!track)
171
+ return;
172
+ loadSubtitles(track.url, { format: track.format ?? undefined })
173
+ .then((c) => {
174
+ if (!mountedRef.current)
175
+ return;
176
+ setCues(c);
177
+ setSubtitleError(c.length === 0 ? "No cues parsed from this subtitle file" : null);
178
+ })
179
+ .catch((e) => {
180
+ console.warn("[subtitles] load failed:", track.url, e);
181
+ if (mountedRef.current)
182
+ setSubtitleError("Subtitles unavailable");
183
+ });
184
+ }
185
+ function syncSubtitleTracks() {
186
+ const nextTracks = subtitleTracksProp?.length
187
+ ? subtitleTracksProp
188
+ : subtitleUrl
189
+ ? [{ lang: subtitleLang ?? "SRT", url: subtitleUrl, format: subtitleFormat }]
190
+ : [];
191
+ setTracks(nextTracks);
192
+ // Preselect the requested track (from the watch screen's subtitle picker)
193
+ if (initialSubtitleLang) {
194
+ const idx = nextTracks.findIndex((t) => t.lang === initialSubtitleLang);
195
+ if (idx >= 0)
196
+ subTrackIdxRef.current = idx;
197
+ }
198
+ loadCues(nextTracks);
199
+ }
200
+ // ---- effects ----
201
+ useEffect(() => {
202
+ mountedRef.current = true;
203
+ autoplayRef.current = autoPlay !== false;
204
+ return () => {
205
+ mountedRef.current = false;
206
+ if (hideTimerRef.current)
207
+ clearTimeout(hideTimerRef.current);
208
+ };
209
+ }, [autoPlay]);
210
+ useEffect(() => {
211
+ syncSubtitleTracks();
212
+ if (uri)
213
+ void add(uri, { title });
214
+ // eslint-disable-next-line react-hooks/exhaustive-deps
215
+ }, []);
216
+ useEffect(() => {
217
+ if (!uri)
218
+ return;
219
+ sourceIndexRef.current = Math.max(0, (sources ?? []).indexOf(uri));
220
+ void add(uri, { title });
221
+ // eslint-disable-next-line react-hooks/exhaustive-deps
222
+ }, [uri]);
223
+ useEffect(() => {
224
+ syncSubtitleTracks();
225
+ // eslint-disable-next-line react-hooks/exhaustive-deps
226
+ }, [subtitleTracksProp, subtitleUrl]);
227
+ useEffect(() => {
228
+ if (!visible || !playing)
229
+ return;
230
+ const t = setTimeout(() => setVisible(false), 3000);
231
+ return () => clearTimeout(t);
232
+ }, [visible, playing]);
233
+ // ---- render ----
234
+ const activeCue = cues.find((c) => currentTime - (subtitleOffset ?? 0) >= c.start &&
235
+ currentTime - (subtitleOffset ?? 0) < c.end) ?? null;
236
+ const activeTrack = tracks[Math.min(subTrackIdxRef.current, tracks.length - 1)] ?? null;
237
+ if (error) {
238
+ return (_jsxs(View, { style: [styles.wrap, styles.center], 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." })] }));
239
+ }
240
+ return (_jsxs(View, { style: [styles.wrap, fullscreen && styles.fullscreen], children: [sourceUri ? (_jsx(Video, { ref: videoRef, source: { uri: sourceUri }, style: StyleSheet.absoluteFill, resizeMode: "contain", paused: !playing, muted: muted, volume: volume, progressUpdateInterval: 1000, onLoadStart: () => {
241
+ if (mountedRef.current)
242
+ setBuffering(true);
243
+ }, onLoad: onVideoLoad, onBuffer: ({ isBuffering }) => {
244
+ if (mountedRef.current)
245
+ setBuffering(!!isBuffering);
246
+ }, onProgress: onVideoProgress, onError: onVideoError, onEnd: onVideoEnd })) : null, loading && (_jsxs(View, { style: styles.centerBadge, pointerEvents: "none", children: [_jsx(ActivityIndicator, { color: "#e50914", size: "large" }), _jsx(Text, { style: styles.loadingText, children: "Starting stream\u2026" })] })), _jsx(SubtitleDisplay, { cue: activeCue, enabled: subtitlesOn, fontSize: subFontSize }), 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(PlayerControls, { 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, onFullscreen: enterFullscreen })] }));
247
+ });
248
+ const styles = StyleSheet.create({
249
+ wrap: {
250
+ flex: 1,
251
+ width: "100%",
252
+ height: "100%",
253
+ backgroundColor: "#000",
254
+ },
255
+ fullscreen: {
256
+ position: "absolute",
257
+ left: 0,
258
+ right: 0,
259
+ top: 0,
260
+ bottom: 0,
261
+ zIndex: 10,
262
+ },
263
+ center: {
264
+ flex: 1,
265
+ justifyContent: "center",
266
+ alignItems: "center",
267
+ gap: 12,
268
+ padding: 20,
269
+ backgroundColor: "#000",
270
+ },
271
+ errorText: {
272
+ color: "#ff6b6b",
273
+ fontSize: 14,
274
+ textAlign: "center",
275
+ },
276
+ errorHint: {
277
+ color: "#9aa0aa",
278
+ fontSize: 12,
279
+ textAlign: "center",
280
+ },
281
+ centerBadge: {
282
+ ...StyleSheet.absoluteFillObject,
283
+ justifyContent: "center",
284
+ alignItems: "center",
285
+ gap: 12,
286
+ },
287
+ loadingText: { color: "#fff", fontSize: 14 },
288
+ subErrorWrap: {
289
+ position: "absolute",
290
+ left: 0,
291
+ right: 0,
292
+ bottom: 56,
293
+ alignItems: "center",
294
+ paddingHorizontal: 24,
295
+ zIndex: 5,
296
+ },
297
+ subErrorText: {
298
+ backgroundColor: "rgba(229,9,20,0.8)",
299
+ color: "#fff",
300
+ fontSize: 12,
301
+ textAlign: "center",
302
+ paddingHorizontal: 10,
303
+ paddingVertical: 4,
304
+ borderRadius: 4,
305
+ overflow: "hidden",
306
+ },
307
+ });
@@ -1,4 +1,4 @@
1
- export type PlayerControlsWebProps = {
1
+ export type PlayerControlsProps = {
2
2
  playing: boolean;
3
3
  buffering: boolean;
4
4
  currentTime: number;
@@ -28,18 +28,6 @@ export type PlayerControlsWebProps = {
28
28
  outroRegion?: [number, number];
29
29
  onSkipIntro?: () => void;
30
30
  onSkipOutro?: () => void;
31
- /** resolution labels in display order — the FIRST is the default source
32
- * (the episode URL), e.g. ["Default", "1080p", "720p", "480p"]. */
33
- resolutions?: string[];
34
- /** currently active resolution label */
35
- resolution?: string;
36
- width?: number;
37
- height?: number;
38
- cue?: any;
39
- enabled?: any;
40
- fontSize?: any;
41
- /** select a resolution — the parent resolves the label to its URL */
42
- onSelectResolution?: (label: string) => void;
43
31
  onToggleVisible: () => void;
44
32
  onPlayPause: () => void;
45
33
  onSkip: (deltaSeconds: number) => void;
@@ -49,4 +37,4 @@ export type PlayerControlsWebProps = {
49
37
  onVolume: (v: number) => void;
50
38
  onFullscreen: () => void;
51
39
  };
52
- export declare function PlayerControlsWeb(props: PlayerControlsWebProps): import("react").JSX.Element;
40
+ export declare function PlayerControls(props: PlayerControlsProps): import("react").JSX.Element;