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,629 @@
1
+ import { useMemo, useState } from "react";
2
+ import {
3
+ ActivityIndicator,
4
+ PanResponder,
5
+ StyleSheet,
6
+ Text,
7
+ TouchableOpacity,
8
+ View,
9
+ } from "react-native";
10
+ import { Ionicons } from "@expo/vector-icons";
11
+
12
+ export type PlayerControlsProps = {
13
+ playing: boolean;
14
+ buffering: boolean;
15
+ currentTime: number;
16
+ duration: number;
17
+ muted: boolean;
18
+ title?: string;
19
+ visible: boolean;
20
+ subtitlesOn: boolean;
21
+ volume: number;
22
+ /** subtitle language labels (when >1 the CC switcher is shown) */
23
+ subtitleTracks?: string[];
24
+ /** currently active subtitle language */
25
+ subtitleTrack?: string;
26
+ /** select a subtitle language from the list directly */
27
+ onSelectSubtitleTrack?: (lang: string) => void;
28
+ /** current subtitle font size (px) */
29
+ subtitleFontSize?: number;
30
+ /** adjust the subtitle font size (px) */
31
+ onChangeSubtitleFontSize?: (px: number) => void;
32
+ /** 0..1 — how much of the whole merged episode has been downloaded. Renders
33
+ * a second, thinner line under the playhead track. */
34
+ loadProgress?: number;
35
+ /** [start, end] seconds (merged timeline) of the intro — highlighted on the
36
+ * track + shows a "Skip Intro" button while the playhead is inside it. */
37
+ introRegion?: [number, number];
38
+ /** [start, end] seconds (merged timeline) of the outro */
39
+ outroRegion?: [number, number];
40
+ onSkipIntro?: () => void;
41
+ onSkipOutro?: () => void;
42
+ onToggleVisible: () => void;
43
+ onPlayPause: () => void;
44
+ onSkip: (deltaSeconds: number) => void;
45
+ onSeek: (seconds: number) => void;
46
+ onToggleMute: () => void;
47
+ onToggleSubtitles: () => void;
48
+ onVolume: (v: number) => void;
49
+ onFullscreen: () => void;
50
+ };
51
+
52
+ const fmt = (s: number) => {
53
+ if (!Number.isFinite(s) || s < 0) s = 0;
54
+ const h = Math.floor(s / 3600);
55
+ const m = Math.floor((s % 3600) / 60);
56
+ const sec = Math.floor(s % 60);
57
+ return `${h.toString().padStart(2, "0")}:${m
58
+ .toString()
59
+ .padStart(2, "0")}:${sec.toString().padStart(2, "0")}`;
60
+ };
61
+
62
+ const clamp01 = (v: number) => Math.min(1, Math.max(0, v));
63
+
64
+ export function PlayerControls(props: PlayerControlsProps) {
65
+ const {
66
+ playing,
67
+ buffering,
68
+ currentTime,
69
+ duration,
70
+ muted,
71
+ title,
72
+ visible,
73
+ subtitlesOn,
74
+ volume,
75
+ subtitleTracks,
76
+ subtitleTrack,
77
+ onSelectSubtitleTrack,
78
+ subtitleFontSize,
79
+ onChangeSubtitleFontSize,
80
+ loadProgress,
81
+ introRegion,
82
+ outroRegion,
83
+ onSkipIntro,
84
+ onSkipOutro,
85
+ onToggleVisible,
86
+ onPlayPause,
87
+ onSkip,
88
+ onSeek,
89
+ onToggleMute,
90
+ onToggleSubtitles,
91
+ onVolume,
92
+ onFullscreen,
93
+ } = props;
94
+
95
+ const hasSubs = (subtitleTracks?.length ?? 0) > 0;
96
+ const subLang = (subtitleTrack ?? subtitleTracks?.[0] ?? "CC")
97
+ .split("-")[0]
98
+ .slice(0, 5)
99
+ .toUpperCase();
100
+
101
+ const [trackW, setTrackW] = useState(0);
102
+ const [scrubPct, setScrubPct] = useState<number | null>(null);
103
+ const [volW, setVolW] = useState(0);
104
+ const [subOpen, setSubOpen] = useState(false);
105
+ const [subTab, setSubTab] = useState<"lang" | "size">("lang");
106
+
107
+ const pct = duration > 0 ? clamp01(currentTime / duration) : 0;
108
+ const volPct = clamp01(volume);
109
+ const inIntro =
110
+ !!introRegion &&
111
+ introRegion[1] > introRegion[0] &&
112
+ currentTime >= introRegion[0] &&
113
+ currentTime <= introRegion[1] &&
114
+ !!onSkipIntro;
115
+ const inOutro =
116
+ !!outroRegion &&
117
+ outroRegion[1] > outroRegion[0] &&
118
+ currentTime >= outroRegion[0] &&
119
+ currentTime <= outroRegion[1] &&
120
+ !!onSkipOutro;
121
+
122
+ const pan = useMemo(
123
+ () =>
124
+ PanResponder.create({
125
+ onStartShouldSetPanResponder: () => true,
126
+ onMoveShouldSetPanResponder: () => true,
127
+ onPanResponderGrant: (e) =>
128
+ setScrubPct(trackW > 0 ? clamp01(e.nativeEvent.locationX / trackW) : 0),
129
+ onPanResponderMove: (e) =>
130
+ setScrubPct(trackW > 0 ? clamp01(e.nativeEvent.locationX / trackW) : 0),
131
+ onPanResponderRelease: (e) => {
132
+ const p = trackW > 0 ? clamp01(e.nativeEvent.locationX / trackW) : 0;
133
+ setScrubPct(null);
134
+ onSeek(p * duration);
135
+ },
136
+ }),
137
+ [trackW, duration, onSeek],
138
+ );
139
+
140
+ const volPan = useMemo(
141
+ () =>
142
+ PanResponder.create({
143
+ onStartShouldSetPanResponder: () => true,
144
+ onMoveShouldSetPanResponder: () => true,
145
+ onPanResponderGrant: (e) =>
146
+ onVolume(volW > 0 ? clamp01(e.nativeEvent.locationX / volW) : 1),
147
+ onPanResponderMove: (e) =>
148
+ onVolume(volW > 0 ? clamp01(e.nativeEvent.locationX / volW) : 1),
149
+ }),
150
+ [volW, onVolume],
151
+ );
152
+
153
+ const SIZE_MIN = 10;
154
+ const SIZE_MAX = 34;
155
+ const [sizeW, setSizeW] = useState(0);
156
+ const currentSize = subtitleFontSize ?? 14;
157
+ const sizePct = clamp01((currentSize - SIZE_MIN) / (SIZE_MAX - SIZE_MIN));
158
+ const sizePan = useMemo(
159
+ () =>
160
+ PanResponder.create({
161
+ onStartShouldSetPanResponder: () => true,
162
+ onMoveShouldSetPanResponder: () => true,
163
+ onPanResponderGrant: (e) => {
164
+ if (!onChangeSubtitleFontSize) return;
165
+ const p = sizeW > 0 ? clamp01(e.nativeEvent.locationX / sizeW) : 0;
166
+ onChangeSubtitleFontSize?.(Math.round(SIZE_MIN + p * (SIZE_MAX - SIZE_MIN)));
167
+ },
168
+ onPanResponderMove: (e) => {
169
+ if (!onChangeSubtitleFontSize) return;
170
+ const p = sizeW > 0 ? clamp01(e.nativeEvent.locationX / sizeW) : 0;
171
+ onChangeSubtitleFontSize?.(Math.round(SIZE_MIN + p * (SIZE_MAX - SIZE_MIN)));
172
+ },
173
+ }),
174
+ [sizeW, onChangeSubtitleFontSize],
175
+ );
176
+
177
+ const shownPct = scrubPct ?? pct;
178
+ const volIcon = muted || volume === 0 ? "volume-mute" : volume < 0.5 ? "volume-low" : "volume-high";
179
+
180
+ return (
181
+ <View style={styles.root}>
182
+ {/* tap surface toggles the chrome */}
183
+ <TouchableOpacity
184
+ style={StyleSheet.absoluteFill}
185
+ activeOpacity={1}
186
+ onPress={onToggleVisible}
187
+ />
188
+
189
+ {buffering && (
190
+ <View style={styles.centerBadge} pointerEvents="none">
191
+ <ActivityIndicator color="#e50914" size="large" />
192
+ </View>
193
+ )}
194
+
195
+ {visible && (
196
+ <>
197
+ {/* top gradient-ish bar */}
198
+ <View style={[styles.bar, styles.top]} pointerEvents="box-none">
199
+ {!!title && (
200
+ <Text numberOfLines={1} style={styles.title}>
201
+ {title}
202
+ </Text>
203
+ )}
204
+ <TouchableOpacity onPress={onToggleMute} hitSlop={8}>
205
+ <Ionicons name={muted ? "volume-mute" : "volume-high"} size={20} color="#fff" />
206
+ </TouchableOpacity>
207
+ </View>
208
+
209
+ {/* center buttons */}
210
+ <View style={styles.centerRow} pointerEvents="box-none">
211
+ <TouchableOpacity style={styles.skipBtn} onPress={() => onSkip(-10)}>
212
+ <Ionicons name="play-back" size={30} color="#fff" />
213
+ </TouchableOpacity>
214
+ <TouchableOpacity style={styles.ppBtn} onPress={onPlayPause}>
215
+ <Ionicons
216
+ name={buffering ? "ellipsis-horizontal" : playing ? "pause" : "play"}
217
+ size={38}
218
+ color="#fff"
219
+ />
220
+ </TouchableOpacity>
221
+ <TouchableOpacity style={styles.skipBtn} onPress={() => onSkip(10)}>
222
+ <Ionicons name="play-forward" size={30} color="#fff" />
223
+ </TouchableOpacity>
224
+ </View>
225
+
226
+ {/* bottom bar */}
227
+ <View style={[styles.bar, styles.bottom]} pointerEvents="box-none">
228
+ <Text style={styles.time}>
229
+ {fmt(scrubPct !== null ? scrubPct * duration : currentTime)}
230
+ </Text>
231
+
232
+ <View
233
+ style={styles.trackWrap}
234
+ onLayout={(e) => setTrackW(e.nativeEvent.layout.width)}
235
+ {...pan.panHandlers}
236
+ >
237
+ <View style={styles.track}>
238
+ {loadProgress != null && loadProgress > 0 && (
239
+ <View
240
+ style={[
241
+ styles.loadLine,
242
+ { width: `${clamp01(loadProgress) * 100}%` },
243
+ ]}
244
+ />
245
+ )}
246
+ {introRegion && duration > 0 && introRegion[1] > introRegion[0] && (
247
+ <View
248
+ style={[
249
+ styles.introHighlight,
250
+ {
251
+ left: `${clamp01(introRegion[0] / duration) * 100}%`,
252
+ width: `${clamp01((introRegion[1] - introRegion[0]) / duration) * 100}%`,
253
+ },
254
+ ]}
255
+ />
256
+ )}
257
+ {outroRegion && duration > 0 && outroRegion[1] > outroRegion[0] && (
258
+ <View
259
+ style={[
260
+ styles.outroHighlight,
261
+ {
262
+ left: `${clamp01(outroRegion[0] / duration) * 100}%`,
263
+ width: `${clamp01((outroRegion[1] - outroRegion[0]) / duration) * 100}%`,
264
+ },
265
+ ]}
266
+ />
267
+ )}
268
+ <View style={[styles.fill, { width: `${shownPct * 100}%` }]} />
269
+ </View>
270
+ <View
271
+ style={[
272
+ styles.thumb,
273
+ { left: `${shownPct * 100}%` },
274
+ scrubPct === null && styles.thumbIdle,
275
+ ]}
276
+ />
277
+ </View>
278
+
279
+ <Text style={styles.time}>{fmt(duration)}</Text>
280
+
281
+ {/* CC toggle */}
282
+ <TouchableOpacity
283
+ style={[styles.ccBtn, subtitlesOn && styles.ccBtnOn]}
284
+ onPress={onToggleSubtitles}
285
+ hitSlop={6}
286
+ >
287
+ <Text style={[styles.ccText, subtitlesOn && styles.ccTextOn]}>CC</Text>
288
+ </TouchableOpacity>
289
+
290
+ {/* subtitle selector (shown with at least ONE track so the Text Size
291
+ slider is reachable). Tap to open a dropdown with two tabs:
292
+ language list | text size slider. */}
293
+ {hasSubs ? (
294
+ <View style={styles.subWrap}>
295
+ <TouchableOpacity
296
+ style={[styles.langBtn, subtitlesOn && styles.langBtnOn]}
297
+ onPress={() => setSubOpen((v) => !v)}
298
+ hitSlop={6}
299
+ >
300
+ <Text style={[styles.ccText, subtitlesOn && styles.ccTextOn]}>
301
+ {subLang}
302
+ </Text>
303
+ <Ionicons
304
+ name={subOpen ? "chevron-up" : "chevron-down"}
305
+ size={12}
306
+ color={subtitlesOn ? "#fff" : "#9aa0aa"}
307
+ />
308
+ </TouchableOpacity>
309
+ {subOpen ? (
310
+ <View style={styles.subMenu}>
311
+ <View style={styles.subTabs}>
312
+ <TouchableOpacity
313
+ style={[styles.subTab, subTab === "lang" && styles.subTabOn]}
314
+ onPress={() => setSubTab("lang")}
315
+ >
316
+ <Text
317
+ style={[styles.subTabText, subTab === "lang" && styles.subTabTextOn]}
318
+ >
319
+ Language
320
+ </Text>
321
+ </TouchableOpacity>
322
+ <TouchableOpacity
323
+ style={[styles.subTab, subTab === "size" && styles.subTabOn]}
324
+ onPress={() => setSubTab("size")}
325
+ >
326
+ <Text
327
+ style={[styles.subTabText, subTab === "size" && styles.subTabTextOn]}
328
+ >
329
+ Text Size
330
+ </Text>
331
+ </TouchableOpacity>
332
+ </View>
333
+
334
+ {subTab === "lang" ? (
335
+ <View style={styles.subBody}>
336
+ {subtitleTracks?.map((lang) => {
337
+ const active = lang === subtitleTrack;
338
+ return (
339
+ <TouchableOpacity
340
+ key={lang}
341
+ style={[styles.subItem, active && styles.subItemOn]}
342
+ onPress={() => {
343
+ setSubOpen(false);
344
+ onSelectSubtitleTrack?.(lang);
345
+ }}
346
+ >
347
+ <Text style={[styles.subItemText, active && styles.subItemTextOn]}>
348
+ {lang}
349
+ </Text>
350
+ {active ? (
351
+ <Ionicons name="checkmark" size={14} color="#fff" />
352
+ ) : null}
353
+ </TouchableOpacity>
354
+ );
355
+ })}
356
+ </View>
357
+ ) : (
358
+ <View style={styles.subBody}>
359
+ <View style={styles.sizeRow}>
360
+ <TouchableOpacity
361
+ style={styles.sizeBtn}
362
+ onPress={() =>
363
+ onChangeSubtitleFontSize?.(
364
+ Math.max(
365
+ SIZE_MIN,
366
+ Math.round((subtitleFontSize ?? 14) - 2),
367
+ ),
368
+ )
369
+ }
370
+ >
371
+ <Ionicons name="remove" size={16} color="#fff" />
372
+ </TouchableOpacity>
373
+
374
+ <View
375
+ style={[
376
+ styles.sizeTrack,
377
+ { opacity: onChangeSubtitleFontSize ? 1 : 0.5 },
378
+ ]}
379
+ onLayout={(e) => setSizeW(e.nativeEvent.layout.width)}
380
+ {...sizePan.panHandlers}
381
+ >
382
+ <View style={[styles.sizeFill, { width: `${sizePct * 100}%` }]} />
383
+ </View>
384
+
385
+ <TouchableOpacity
386
+ style={styles.sizeBtn}
387
+ onPress={() =>
388
+ onChangeSubtitleFontSize?.(
389
+ Math.min(
390
+ SIZE_MAX,
391
+ Math.round((subtitleFontSize ?? 14) + 2),
392
+ ),
393
+ )
394
+ }
395
+ >
396
+ <Ionicons name="add" size={16} color="#fff" />
397
+ </TouchableOpacity>
398
+ </View>
399
+ <Text style={styles.sizeLabel}>{currentSize}px</Text>
400
+ </View>
401
+ )}
402
+ </View>
403
+ ) : null}
404
+ </View>
405
+ ) : null}
406
+
407
+ {/* volume: icon tap = mute, slider drag = level */}
408
+ <TouchableOpacity onPress={onToggleMute} hitSlop={6}>
409
+ <Ionicons name={volIcon as any} size={17} color="#fff" />
410
+ </TouchableOpacity>
411
+ <View
412
+ style={styles.volTrackWrap}
413
+ onLayout={(e) => setVolW(e.nativeEvent.layout.width)}
414
+ {...volPan.panHandlers}
415
+ >
416
+ <View style={styles.volTrack}>
417
+ <View style={[styles.volFill, { width: `${volPct * 100}%` }]} />
418
+ </View>
419
+ </View>
420
+
421
+ <TouchableOpacity onPress={onFullscreen} hitSlop={8}>
422
+ <Ionicons name="expand" size={18} color="#fff" />
423
+ </TouchableOpacity>
424
+ </View>
425
+ </>
426
+ )}
427
+
428
+ {visible && inIntro && (
429
+ <TouchableOpacity style={styles.skipRegionBtn} onPress={onSkipIntro}>
430
+ <Text style={styles.skipRegionText}>Skip Intro ›</Text>
431
+ </TouchableOpacity>
432
+ )}
433
+ {visible && inOutro && (
434
+ <TouchableOpacity style={styles.skipRegionBtn} onPress={onSkipOutro}>
435
+ <Text style={styles.skipRegionText}>Skip Outro ›</Text>
436
+ </TouchableOpacity>
437
+ )}
438
+ </View>
439
+ );
440
+ }
441
+
442
+ const styles = StyleSheet.create({
443
+ root: { ...StyleSheet.absoluteFillObject, zIndex: 2 },
444
+
445
+ centerBadge: {
446
+ ...StyleSheet.absoluteFillObject,
447
+ justifyContent: "center",
448
+ alignItems: "center",
449
+ width: "100%",
450
+ height: "100%",
451
+ },
452
+
453
+ bar: {
454
+ position: "absolute",
455
+ left: 0,
456
+ right: 0,
457
+ flexDirection: "row",
458
+ alignItems: "center",
459
+ paddingHorizontal: 12,
460
+ backgroundColor: "rgba(0,0,0,0.45)",
461
+ gap: 8,
462
+ },
463
+ top: { top: 0, height: 42 },
464
+ bottom: { bottom: 0, height: 44 },
465
+
466
+ title: { color: "#fff", fontWeight: "700", fontSize: 13, flex: 1 },
467
+
468
+ centerRow: {
469
+ ...StyleSheet.absoluteFillObject,
470
+ justifyContent: "center",
471
+ alignItems: "center",
472
+ flexDirection: "row",
473
+ gap: 34,
474
+ },
475
+ ppBtn: {
476
+ width: 64,
477
+ height: 64,
478
+ borderRadius: 32,
479
+ backgroundColor: "rgba(229,9,20,0.92)",
480
+ justifyContent: "center",
481
+ alignItems: "center",
482
+ },
483
+ skipBtn: { opacity: 0.9 },
484
+
485
+ time: {
486
+ color: "#ddd",
487
+ fontSize: 11,
488
+ fontVariant: ["tabular-nums"],
489
+ minWidth: 58,
490
+ },
491
+ trackWrap: { flex: 1, paddingVertical: 12, justifyContent: "center" },
492
+ track: {
493
+ height: 4,
494
+ borderRadius: 2,
495
+ backgroundColor: "#3a3f47",
496
+ overflow: "hidden",
497
+ },
498
+ fill: { height: 4, borderRadius: 2, backgroundColor: "#e50914" },
499
+ loadLine: {
500
+ position: "absolute",
501
+ left: 0,
502
+ top: 0,
503
+ height: 4,
504
+ borderRadius: 2,
505
+ backgroundColor: "rgba(255,255,255,0.35)",
506
+ },
507
+ introHighlight: {
508
+ position: "absolute",
509
+ top: 0,
510
+ height: 4,
511
+ backgroundColor: "rgba(0,229,255,0.55)",
512
+ },
513
+ outroHighlight: {
514
+ position: "absolute",
515
+ top: 0,
516
+ height: 4,
517
+ backgroundColor: "rgba(255,0,200,0.55)",
518
+ },
519
+ skipRegionBtn: {
520
+ position: "absolute",
521
+ right: 16,
522
+ bottom: 60,
523
+ flexDirection: "row",
524
+ alignItems: "center",
525
+ backgroundColor: "rgba(0,0,0,0.6)",
526
+ borderWidth: 1,
527
+ borderColor: "rgba(255,255,255,0.25)",
528
+ borderRadius: 20,
529
+ paddingHorizontal: 14,
530
+ paddingVertical: 8,
531
+ },
532
+ skipRegionText: { color: "#fff", fontWeight: "700", fontSize: 13 },
533
+ thumb: {
534
+ position: "absolute",
535
+ top: 12 - 7 + 2,
536
+ marginLeft: -6,
537
+ width: 12,
538
+ height: 12,
539
+ borderRadius: 6,
540
+ backgroundColor: "#e50914",
541
+ },
542
+ thumbIdle: { opacity: 0.85 },
543
+
544
+ ccBtn: {
545
+ borderWidth: 1.5,
546
+ borderColor: "#666",
547
+ borderRadius: 4,
548
+ paddingHorizontal: 5,
549
+ paddingVertical: 1,
550
+ },
551
+ ccBtnOn: { borderColor: "#e50914", backgroundColor: "rgba(229,9,20,0.25)" },
552
+ ccText: { color: "#888", fontSize: 10, fontWeight: "900" },
553
+ ccTextOn: { color: "#fff" },
554
+ langBtn: {
555
+ borderWidth: 1.5,
556
+ borderColor: "#555",
557
+ borderRadius: 4,
558
+ paddingHorizontal: 6,
559
+ paddingVertical: 1,
560
+ },
561
+ langBtnOn: {
562
+ borderColor: "#7dd3fc",
563
+ backgroundColor: "rgba(125,211,252,0.18)",
564
+ },
565
+
566
+ subWrap: { position: "relative", zIndex: 20 },
567
+ subMenu: {
568
+ position: "absolute",
569
+ bottom: 26,
570
+ right: 0,
571
+ zIndex: 30,
572
+ width: 190,
573
+ backgroundColor: "#14161c",
574
+ borderWidth: 1,
575
+ borderColor: "#2a2f3a",
576
+ borderRadius: 8,
577
+ overflow: "hidden",
578
+ elevation: 12,
579
+ shadowColor: "#000",
580
+ shadowOpacity: 0.5,
581
+ shadowRadius: 10,
582
+ shadowOffset: { width: 0, height: 3 },
583
+ },
584
+ subTabs: { flexDirection: "row", borderBottomWidth: 1, borderBottomColor: "#262a34" },
585
+ subTab: { flex: 1, alignItems: "center", paddingVertical: 7, backgroundColor: "#1a1d24" },
586
+ subTabOn: { backgroundColor: "#232833" },
587
+ subTabText: { color: "#9aa0aa", fontSize: 12, fontWeight: "700" },
588
+ subTabTextOn: { color: "#7dd3fc", fontWeight: "800" },
589
+ subBody: { paddingVertical: 4 },
590
+ subItem: {
591
+ flexDirection: "row",
592
+ alignItems: "center",
593
+ justifyContent: "space-between",
594
+ gap: 10,
595
+ paddingHorizontal: 12,
596
+ paddingVertical: 7,
597
+ },
598
+ subItemOn: { backgroundColor: "rgba(125,211,252,0.15)" },
599
+ subItemText: { color: "#cbd5e1", fontSize: 13, fontWeight: "600" },
600
+ subItemTextOn: { color: "#7dd3fc", fontWeight: "800" },
601
+ sizeRow: {
602
+ flexDirection: "row",
603
+ alignItems: "center",
604
+ gap: 10,
605
+ paddingHorizontal: 10,
606
+ paddingVertical: 10,
607
+ },
608
+ sizeBtn: {
609
+ width: 24,
610
+ height: 24,
611
+ borderRadius: 4,
612
+ alignItems: "center",
613
+ justifyContent: "center",
614
+ backgroundColor: "#2a2f3a",
615
+ },
616
+ sizeTrack: { flex: 1, height: 22, justifyContent: "center" },
617
+ sizeFill: { height: 4, borderRadius: 2, backgroundColor: "#7dd3fc" },
618
+ sizeLabel: {
619
+ color: "#9aa0aa",
620
+ fontSize: 11,
621
+ fontWeight: "700",
622
+ textAlign: "center",
623
+ paddingBottom: 6,
624
+ },
625
+
626
+ volTrackWrap: { width: 56, paddingVertical: 12, justifyContent: "center" },
627
+ volTrack: { height: 4, borderRadius: 2, backgroundColor: "#3a3f47", overflow: "hidden" },
628
+ volFill: { height: 4, backgroundColor: "#fff" },
629
+ });
@@ -0,0 +1,48 @@
1
+ import { StyleSheet, Text, View } from "react-native";
2
+
3
+ export type SubCueShape = { 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
+ export function SubtitleDisplay({
11
+ cue,
12
+ enabled,
13
+ fontSize,
14
+ }: {
15
+ cue: SubCueShape | null;
16
+ enabled: boolean;
17
+ fontSize: number;
18
+ }) {
19
+ if (!enabled || !cue?.text) return null;
20
+ return (
21
+ <View pointerEvents="none" style={styles.wrap}>
22
+ <Text style={[styles.text, { fontSize, lineHeight: fontSize * 1.35 }]}>
23
+ {cue.text}
24
+ </Text>
25
+ </View>
26
+ );
27
+ }
28
+
29
+ const styles = StyleSheet.create({
30
+ wrap: {
31
+ position: "absolute",
32
+ left: 0,
33
+ right: 0,
34
+ bottom: 56,
35
+ alignItems: "center",
36
+ paddingHorizontal: 24,
37
+ zIndex: 1,
38
+ },
39
+ text: {
40
+ backgroundColor: "rgba(0,0,0,0.65)",
41
+ color: "#fff",
42
+ textAlign: "center",
43
+ paddingHorizontal: 8,
44
+ paddingVertical: 3,
45
+ borderRadius: 4,
46
+ overflow: "hidden",
47
+ },
48
+ });
@@ -0,0 +1,3 @@
1
+ export { MegaVideoPlayer, type MegaVideoPlayerProps } from "./MegaVideoPlayerWeb.js";
2
+ export { PlayerControls, type PlayerControlsProps } from "./PlayerControlsWeb.js";
3
+ export { SubtitleDisplay, type SubCueShape } from "./SubtitleDisplayWeb.js";