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,80 @@
1
+ import { StyleSheet, Text, View } from "react-native";
2
+
3
+ export type SubCueShapeAndroid = { start: number; end: number; text: string };
4
+
5
+ /**
6
+ * Renders the active subtitle cue over the video surface. Positioned
7
+ * absolutely with zIndex 1 (below the controls at zIndex 2, above the
8
+ * video background at zIndex 0).
9
+ *
10
+ * When `width`/`height` are supplied they are used to size the overlay to the
11
+ * exact area the video occupies, so cues stay aligned with the picture (e.g.
12
+ * when the player is letterboxed inside a wider container).
13
+ */
14
+ export function SubtitleDisplayAndroid({
15
+ cue,
16
+ enabled,
17
+ fontSize,
18
+ width,
19
+ height,
20
+ }: {
21
+ cue: SubCueShapeAndroid | null;
22
+ enabled: boolean;
23
+ fontSize: number;
24
+ width?: number;
25
+ height?: number;
26
+ }) {
27
+ if (!enabled || !cue?.text) return null;
28
+ const sized = width != null || height != null;
29
+ return (
30
+ <View
31
+ pointerEvents="none"
32
+ style={
33
+ sized
34
+ ? [
35
+ styles.videoArea,
36
+ {
37
+ width: width ?? "100%",
38
+ height: height ?? "100%",
39
+ },
40
+ ]
41
+ : styles.wrap
42
+ }
43
+ >
44
+ <Text style={[styles.text, { fontSize, lineHeight: fontSize * 1.35 }]}>
45
+ {cue.text}
46
+ </Text>
47
+ </View>
48
+ );
49
+ }
50
+
51
+ const styles = StyleSheet.create({
52
+ wrap: {
53
+ position: "absolute",
54
+ left: 0,
55
+ right: 0,
56
+ bottom: 56,
57
+ alignItems: "center",
58
+ paddingHorizontal: 24,
59
+ zIndex: 1,
60
+ },
61
+ videoArea: {
62
+ position: "absolute",
63
+ left: 0,
64
+ top: 0,
65
+ justifyContent: "flex-end",
66
+ alignItems: "center",
67
+ paddingHorizontal: 24,
68
+ paddingBottom: 56,
69
+ zIndex: 1,
70
+ },
71
+ text: {
72
+ backgroundColor: "rgba(0,0,0,0.65)",
73
+ color: "#fff",
74
+ textAlign: "center",
75
+ paddingHorizontal: 8,
76
+ paddingVertical: 3,
77
+ borderRadius: 4,
78
+ overflow: "hidden",
79
+ },
80
+ });
@@ -0,0 +1,7 @@
1
+ export {
2
+ MegaVideoPlayerAndroid,
3
+ type MegaVideoPlayerAndroidProps,
4
+ type MegaVideoPlayerAndroidHandle,
5
+ } from "./MegaVideoPlayerAndroid.js";
6
+ export { PlayerControlsAndroid, type PlayerControlsAndroidProps } from "./PlayerControlsAndroid.js";
7
+ export { SubtitleDisplayAndroid, type SubCueShapeAndroid } from "./SubtitleDisplayAndroid.js";
@@ -0,0 +1,551 @@
1
+ // The exact MyMovies episode player (components/MegaVideoPlayer.web.tsx),
2
+ // packaged as a cross-platform component (Android / iOS / web). A direct
3
+ // VideoPlayer instance (createVideoPlayer) driven from a class component, with
4
+ // ordered part URLs played back-to-back and auto-advancing to the next part when
5
+ // the current one ends. Sources come in as proxy URLs — from
6
+ // `mega-stream-proxy/native` (http://127.0.0.1:<port>/…) on Android/iOS or from
7
+ // the Service Worker (`/__mmstream__/…`) on the web.
8
+
9
+ import React from "react";
10
+ import { ActivityIndicator, StyleSheet, Text, View } from "react-native";
11
+ import { createVideoPlayer, VideoView, type VideoPlayer } from "expo-video";
12
+ import { Ionicons } from "@expo/vector-icons";
13
+
14
+ import { PlayerControlsWeb } from "./PlayerControls.js";
15
+ import {
16
+ loadSubtitles,
17
+ type SubCue,
18
+ type SubtitleFormat,
19
+ type SubtitleTrack,
20
+ } from "../../subtitles/index.js";
21
+
22
+ /** expo-video listener subscriptions just need `remove()`. */
23
+ type EventSubscriptionLike = { remove(): void };
24
+
25
+ export type MegaVideoPlayerWebProps = {
26
+ /** A playable URL — a direct stream or the first part URL. */
27
+ uri?: string | null;
28
+ /** Ordered additional part URLs for multi-part episodes, played in sequence. */
29
+ sources?: string[];
30
+ title?: string;
31
+ /** Pass your subtitle file here — MEGA links, plain http(s) CORS hosts, or a known extension (.srt/.ass/.vtt). */
32
+ subtitleUrl?: string;
33
+ /** optional format hint (defaults to URL extension, then content sniffing) */
34
+ subtitleFormat?: SubtitleFormat;
35
+ /** language name shown in the player controls for the single `subtitleUrl` track
36
+ * (e.g. "English", "French"). Defaults to "SRT". */
37
+ subtitleLang?: string;
38
+ subtitleTracks?: SubtitleTrack[];
39
+ /** preselect a subtitle track by language on load */
40
+ initialSubtitleLang?: string | null;
41
+ /** shift the cue timestamps by an offset in seconds (can be negative) */
42
+ subtitleOffset?: number;
43
+ /** resolution labels in display order — the FIRST is the default source
44
+ * (the episode URL), e.g. ["Default", "1080p", "720p", "480p"]. */
45
+ resolutions?: string[];
46
+ /** currently active resolution label */
47
+ resolution?: string;
48
+ /** select a resolution — the parent resolves the label to its URL and
49
+ * updates `uri` so the new source is loaded. */
50
+ onSelectResolution?: (label: string) => void;
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
+ width?: number;
57
+ height?: number;
58
+ };
59
+
60
+ type MegaVideoPlayerWebState = {
61
+ playing: boolean;
62
+ buffering: boolean;
63
+ loading: boolean;
64
+ currentTime: number;
65
+ duration: number;
66
+ muted: boolean;
67
+ volume: number;
68
+ visible: boolean;
69
+ subtitlesOn: boolean;
70
+ subFontSize: number;
71
+ cues: SubCue[];
72
+ subtitleError: string | null;
73
+ error: string | null;
74
+ fullscreen: boolean;
75
+ width?: number;
76
+ height?: number;
77
+ };
78
+
79
+ export class MegaVideoPlayerWeb extends React.Component<
80
+ MegaVideoPlayerWebProps,
81
+ MegaVideoPlayerWebState
82
+ > {
83
+ private player: VideoPlayer;
84
+ private subs: EventSubscriptionLike[] = [];
85
+ private hideTimer: ReturnType<typeof setTimeout> | null = null;
86
+ private mountedRef = true;
87
+ private autoplayRef = true;
88
+ private subTrackIdx = 0;
89
+ private tracks: SubtitleTrack[] = [];
90
+ private sourceIndexRef = 0;
91
+
92
+ constructor(props: MegaVideoPlayerWebProps) {
93
+ super(props);
94
+ this.state = {
95
+ playing: false,
96
+ buffering: true,
97
+ loading: false,
98
+ currentTime: 0,
99
+ duration: 0,
100
+ muted: false,
101
+ volume: 1,
102
+ visible: true,
103
+ subtitlesOn: true,
104
+ subFontSize: 14,
105
+ cues: [],
106
+ subtitleError: null,
107
+ error: null,
108
+ fullscreen: false,
109
+ width: this.props.width,
110
+ height: this.props.height,
111
+ };
112
+
113
+ this.player = createVideoPlayer(null);
114
+ this.player.loop = false;
115
+ this.player.timeUpdateEventInterval = 1;
116
+ this.player.bufferOptions = { preferredForwardBufferDuration: 60 };
117
+ this.sourceIndexRef = Math.max(
118
+ 0,
119
+ (this.props.sources ?? []).indexOf(this.props.uri ?? ""),
120
+ );
121
+ }
122
+
123
+ // ---- lifecycle ----
124
+
125
+ componentDidMount() {
126
+ this.mountedRef = true;
127
+ this.autoplayRef = this.props.autoPlay !== false;
128
+
129
+ this.subs.push(
130
+ this.player.addListener("statusChange", ({ status, error }) => {
131
+ if (!this.mountedRef) return;
132
+ console.log(
133
+ `[MegaVideoPlayer] status: ${status}` +
134
+ (error ? ` error: ${error.message}` : ""),
135
+ );
136
+ this.setState({
137
+ buffering: status === "loading",
138
+ error:
139
+ status === "error"
140
+ ? (error?.message ?? "Video error")
141
+ : this.state.error,
142
+ });
143
+ if (error) {
144
+ this.props.onError?.(error.message ?? "Video error");
145
+ }
146
+ if (status === "readyToPlay") {
147
+ this.setState({
148
+ buffering: false,
149
+ duration: this.player.duration || this.state.duration,
150
+ });
151
+ if (this.autoplayRef) this.player.play();
152
+ }
153
+ }),
154
+ );
155
+ this.subs.push(
156
+ this.player.addListener("playingChange", ({ isPlaying }) => {
157
+ if (!this.mountedRef) return;
158
+ console.log(`[MegaVideoPlayer] playing: ${isPlaying}`);
159
+ this.setState({ playing: isPlaying });
160
+ if (isPlaying) this.scheduleHide();
161
+ else this.setState({ visible: true });
162
+ }),
163
+ );
164
+ this.subs.push(
165
+ this.player.addListener("timeUpdate", ({ currentTime }) => {
166
+ if (!this.mountedRef) return;
167
+ this.setState({ currentTime });
168
+ this.props.onProgress?.({
169
+ currentTime,
170
+ duration: this.player.duration || this.state.duration,
171
+ });
172
+ }),
173
+ );
174
+ this.subs.push(
175
+ this.player.addListener("playToEnd", () => {
176
+ const sources = this.props.sources ?? [];
177
+ const next = sources[this.sourceIndexRef + 1];
178
+ if (next) {
179
+ this.sourceIndexRef += 1;
180
+ console.log(
181
+ `[MegaVideoPlayer] advancing to part ${this.sourceIndexRef + 1}/${sources.length}`,
182
+ );
183
+ void this.add(next, { title: this.props.title });
184
+ } else {
185
+ this.props.onEnd?.();
186
+ }
187
+ }),
188
+ );
189
+
190
+ this.syncSubtitleTracks();
191
+ if (this.props.uri) {
192
+ void this.add(this.props.uri, { title: this.props.title });
193
+ }
194
+ }
195
+
196
+ componentDidUpdate(prevProps: MegaVideoPlayerWebProps) {
197
+ if (this.props.uri !== prevProps.uri && this.props.uri) {
198
+ this.sourceIndexRef = Math.max(
199
+ 0,
200
+ (this.props.sources ?? []).indexOf(this.props.uri),
201
+ );
202
+ void this.add(this.props.uri, { title: this.props.title });
203
+ }
204
+ if (
205
+ this.props.subtitleTracks !== prevProps.subtitleTracks ||
206
+ this.props.subtitleUrl !== prevProps.subtitleUrl
207
+ ) {
208
+ this.syncSubtitleTracks();
209
+ }
210
+ }
211
+
212
+ componentWillUnmount() {
213
+ this.mountedRef = false;
214
+ if (this.hideTimer) clearTimeout(this.hideTimer);
215
+ this.subs.forEach((s) => s.remove());
216
+ this.subs = [];
217
+ if (this.player) {
218
+ try {
219
+ this.player.pause();
220
+ this.player.release();
221
+ } catch {}
222
+ }
223
+ }
224
+
225
+ // ---- source management ----
226
+
227
+ /** Loads a new playable URL into the player. */
228
+ async add(source: string, opts?: { title?: string }): Promise<void> {
229
+ if (!source) return;
230
+ this.setState({ error: null, loading: true, buffering: true });
231
+
232
+ try {
233
+ if (!this.mountedRef) return;
234
+ console.log(`[MegaVideoPlayer] add: ${source}`);
235
+ this.player.replace({
236
+ uri: source,
237
+ metadata: { title: opts?.title ?? this.props.title },
238
+ });
239
+ this.setState({
240
+ loading: false,
241
+ currentTime: 0,
242
+ duration: 0,
243
+ });
244
+ } catch (e) {
245
+ if (!this.mountedRef) return;
246
+ const message = e instanceof Error ? e.message : String(e);
247
+ console.warn("[MegaVideoPlayer] add failed:", message);
248
+ this.setState({ error: message, loading: false, buffering: false });
249
+ this.props.onError?.(message);
250
+ }
251
+ }
252
+
253
+ /** Stops playback and clears the current source. */
254
+ stop(): void {
255
+ try {
256
+ this.player.pause();
257
+ this.player.replace(null);
258
+ } catch {}
259
+ }
260
+
261
+ // ---- player controllers (all on the class) ----
262
+
263
+ playPause = (): void => {
264
+ if (this.player.playing) this.player.pause();
265
+ else this.player.play();
266
+ this.setState({ playing: this.player.playing, visible: true });
267
+ this.scheduleHide();
268
+ };
269
+
270
+ skip = (delta: number): void => {
271
+ const next = Math.min(
272
+ Math.max(this.state.currentTime + delta, 0),
273
+ this.state.duration,
274
+ );
275
+ this.player.currentTime = next;
276
+ this.setState({ currentTime: next, visible: true });
277
+ this.scheduleHide();
278
+ };
279
+
280
+ seek = (seconds: number): void => {
281
+ const clamped = Math.min(
282
+ Math.max(seconds, 0),
283
+ this.state.duration || seconds,
284
+ );
285
+ this.player.currentTime = clamped;
286
+ this.setState({ currentTime: clamped, visible: true });
287
+ this.scheduleHide();
288
+ };
289
+
290
+ toggleMute = (): void => {
291
+ const next = !this.state.muted;
292
+ this.player.muted = next;
293
+ this.setState({ muted: next, visible: true });
294
+ };
295
+
296
+ changeVolume = (v: number): void => {
297
+ this.player.volume = v;
298
+ this.setState({ volume: v });
299
+ if (v > 0 && this.state.muted) {
300
+ this.player.muted = false;
301
+ this.setState({ muted: false });
302
+ }
303
+ };
304
+
305
+ toggleSubtitles = (): void => {
306
+ this.setState((s) => ({ subtitlesOn: !s.subtitlesOn }));
307
+ };
308
+
309
+ toggleControls = (): void => {
310
+ this.setState((s) => ({ visible: !s.visible }));
311
+ if (!this.state.visible && this.state.playing) this.scheduleHide();
312
+ };
313
+
314
+ enterFullscreen = (next?: boolean | unknown): void => {
315
+ // PlayerControls calls through onPress, which passes the press event as
316
+ // the first argument — ignore it (it is not the desired fullscreen value).
317
+ const target = typeof next === "boolean" ? next : !this.state.fullscreen;
318
+ this.setState({ fullscreen: target, visible: true });
319
+ this.props.onFullscreenChange?.(target);
320
+ };
321
+
322
+ changeSubtitleFontSize = (px: number): void => {
323
+ this.setState({ subFontSize: px });
324
+ };
325
+
326
+ selectSubtitleTrack = (lang: string): void => {
327
+ const idx = this.tracks.findIndex((t) => t.lang === lang);
328
+ if (idx >= 0) {
329
+ this.subTrackIdx = idx;
330
+ this.setState({ subtitlesOn: true });
331
+ this.loadSubtitles();
332
+ }
333
+ };
334
+
335
+ // ---- helpers ----
336
+
337
+ private scheduleHide() {
338
+ if (this.hideTimer) clearTimeout(this.hideTimer);
339
+ this.hideTimer = setTimeout(() => {
340
+ if (this.state.playing) this.setState({ visible: false });
341
+ }, 3000);
342
+ }
343
+
344
+ private syncSubtitleTracks() {
345
+ const {
346
+ subtitleTracks,
347
+ subtitleUrl,
348
+ subtitleFormat,
349
+ subtitleLang,
350
+ initialSubtitleLang,
351
+ } = this.props;
352
+ this.tracks = subtitleTracks?.length
353
+ ? subtitleTracks
354
+ : subtitleUrl
355
+ ? [
356
+ {
357
+ lang: subtitleLang ?? "SRT",
358
+ url: subtitleUrl,
359
+ format: subtitleFormat,
360
+ },
361
+ ]
362
+ : [];
363
+ // Preselect the requested track (from the watch screen's subtitle picker)
364
+ if (initialSubtitleLang) {
365
+ const idx = this.tracks.findIndex((t) => t.lang === initialSubtitleLang);
366
+ if (idx >= 0) this.subTrackIdx = idx;
367
+ }
368
+ this.loadSubtitles();
369
+ }
370
+
371
+ private loadSubtitles() {
372
+ const track =
373
+ this.tracks[Math.min(this.subTrackIdx, this.tracks.length - 1)] ?? null;
374
+ this.setState({ cues: [], subtitleError: null });
375
+ if (!track) return;
376
+ loadSubtitles(track.url, { format: track.format ?? undefined })
377
+ .then((c) => {
378
+ if (!this.mountedRef) return;
379
+ this.setState({
380
+ cues: c,
381
+ subtitleError:
382
+ c.length === 0 ? "No cues parsed from this subtitle file" : null,
383
+ });
384
+ })
385
+ .catch((e: unknown) => {
386
+ console.warn("[subtitles] load failed:", track.url, e);
387
+ if (this.mountedRef)
388
+ this.setState({ subtitleError: "Subtitles unavailable" });
389
+ });
390
+ }
391
+
392
+ // ---- render ----
393
+
394
+ render() {
395
+ const s = this.state;
396
+ const offset = this.props.subtitleOffset ?? 0;
397
+ const cueTime = s.currentTime - offset;
398
+ const activeCue =
399
+ s.cues.find((c) => cueTime >= c.start && cueTime < c.end) ?? null;
400
+ const activeTrack =
401
+ this.tracks[Math.min(this.subTrackIdx, this.tracks.length - 1)] ?? null;
402
+
403
+ if (s.error) {
404
+ return (
405
+ <View style={[styles.wrap, styles.center]}>
406
+ <Ionicons name="alert-circle-outline" size={40} color="#ff6b6b" />
407
+ <Text style={styles.errorText}>{s.error}</Text>
408
+ <Text style={styles.errorHint}>Use "Download & Play" instead.</Text>
409
+ </View>
410
+ );
411
+ }
412
+
413
+ return (
414
+ <View
415
+ style={[
416
+ styles.wrap,
417
+ { width: this.state.width, height: this.state.height },
418
+ s.fullscreen && styles.fullscreen,
419
+ ]}
420
+ >
421
+ <VideoView
422
+ player={this.player}
423
+ style={[
424
+ StyleSheet.absoluteFill,
425
+ {
426
+ alignSelf: "center",
427
+ width: this.state.width,
428
+ height: this.state.height,
429
+ zIndex: 0
430
+ },
431
+ ]}
432
+ nativeControls={false}
433
+ contentFit="contain"
434
+ allowsFullscreen={false}
435
+ />
436
+
437
+ {s.loading && (
438
+ <View
439
+ style={[styles.centerBadge, { width: this.state.width }]}
440
+ pointerEvents="none"
441
+ >
442
+ <ActivityIndicator color="#e50914" size="large" />
443
+ <Text style={styles.loadingText}>Starting stream…</Text>
444
+ </View>
445
+ )}
446
+
447
+ {s.subtitlesOn && s.subtitleError && !s.cues.length && (
448
+ <View pointerEvents="none" style={styles.subErrorWrap}>
449
+ <Text style={styles.subErrorText}>
450
+ {s.subtitleError} · {activeTrack?.lang ?? "no track"}
451
+ </Text>
452
+ </View>
453
+ )}
454
+
455
+ <PlayerControlsWeb
456
+ playing={s.playing}
457
+ buffering={s.buffering}
458
+ currentTime={s.currentTime}
459
+ duration={s.duration}
460
+ muted={s.muted}
461
+ title={this.props.title}
462
+ visible={s.visible}
463
+ subtitlesOn={s.subtitlesOn}
464
+ volume={s.volume}
465
+ subtitleTracks={this.tracks.map((t) => t.lang)}
466
+ subtitleTrack={activeTrack?.lang}
467
+ onSelectSubtitleTrack={this.selectSubtitleTrack}
468
+ subtitleFontSize={s.subFontSize}
469
+ onChangeSubtitleFontSize={this.changeSubtitleFontSize}
470
+ resolutions={this.props.resolutions}
471
+ resolution={this.props.resolution}
472
+ onSelectResolution={this.props.onSelectResolution}
473
+ onToggleVisible={this.toggleControls}
474
+ onPlayPause={this.playPause}
475
+ onSkip={this.skip}
476
+ onSeek={this.seek}
477
+ onToggleMute={this.toggleMute}
478
+ onToggleSubtitles={this.toggleSubtitles}
479
+ onVolume={this.changeVolume}
480
+ onFullscreen={this.enterFullscreen}
481
+ width={this.state.width}
482
+ height={this.state.height}
483
+ cue={activeCue}
484
+ enabled={s.subtitlesOn}
485
+ fontSize={s.subFontSize}
486
+ />
487
+ </View>
488
+ );
489
+ }
490
+ }
491
+
492
+ const styles = StyleSheet.create({
493
+ wrap: {
494
+ flex: 1,
495
+ width: "100%",
496
+ height: "100%",
497
+ backgroundColor: "#000",
498
+ },
499
+ fullscreen: {
500
+ position: "absolute",
501
+ left: 0,
502
+ right: 0,
503
+ top: 0,
504
+ bottom: 0,
505
+ zIndex: 10,
506
+ },
507
+ center: {
508
+ flex: 1,
509
+ justifyContent: "center",
510
+ alignItems: "center",
511
+ gap: 12,
512
+ padding: 20,
513
+ backgroundColor: "#000",
514
+ },
515
+ errorText: {
516
+ color: "#ff6b6b",
517
+ fontSize: 14,
518
+ textAlign: "center",
519
+ },
520
+ errorHint: {
521
+ color: "#9aa0aa",
522
+ fontSize: 12,
523
+ textAlign: "center",
524
+ },
525
+ centerBadge: {
526
+ ...StyleSheet.absoluteFillObject,
527
+ justifyContent: "center",
528
+ alignItems: "center",
529
+ gap: 12,
530
+ },
531
+ loadingText: { color: "#fff", fontSize: 14 },
532
+ subErrorWrap: {
533
+ position: "absolute",
534
+ left: 0,
535
+ right: 0,
536
+ bottom: 56,
537
+ alignItems: "center",
538
+ paddingHorizontal: 24,
539
+ zIndex: 5,
540
+ },
541
+ subErrorText: {
542
+ backgroundColor: "rgba(229,9,20,0.8)",
543
+ color: "#fff",
544
+ fontSize: 12,
545
+ textAlign: "center",
546
+ paddingHorizontal: 10,
547
+ paddingVertical: 4,
548
+ borderRadius: 4,
549
+ overflow: "hidden",
550
+ },
551
+ });