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,289 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useMemo, useState } from "react";
3
+ import { ActivityIndicator, PanResponder, StyleSheet, Text, TouchableOpacity, View, } from "react-native";
4
+ import { Ionicons } from "@expo/vector-icons";
5
+ const fmt = (s) => {
6
+ if (!Number.isFinite(s) || s < 0)
7
+ s = 0;
8
+ const h = Math.floor(s / 3600);
9
+ const m = Math.floor((s % 3600) / 60);
10
+ const sec = Math.floor(s % 60);
11
+ return `${h.toString().padStart(2, "0")}:${m
12
+ .toString()
13
+ .padStart(2, "0")}:${sec.toString().padStart(2, "0")}`;
14
+ };
15
+ const clamp01 = (v) => Math.min(1, Math.max(0, v));
16
+ export function PlayerControls(props) {
17
+ const { playing, buffering, currentTime, duration, muted, title, visible, subtitlesOn, volume, subtitleTracks, subtitleTrack, onSelectSubtitleTrack, subtitleFontSize, onChangeSubtitleFontSize, loadProgress, introRegion, outroRegion, onSkipIntro, onSkipOutro, onToggleVisible, onPlayPause, onSkip, onSeek, onToggleMute, onToggleSubtitles, onVolume, onFullscreen, } = props;
18
+ const hasSubs = (subtitleTracks?.length ?? 0) > 0;
19
+ const subLang = (subtitleTrack ?? subtitleTracks?.[0] ?? "CC")
20
+ .split("-")[0]
21
+ .slice(0, 5)
22
+ .toUpperCase();
23
+ const [trackW, setTrackW] = useState(0);
24
+ const [scrubPct, setScrubPct] = useState(null);
25
+ const [volW, setVolW] = useState(0);
26
+ const [subOpen, setSubOpen] = useState(false);
27
+ const [subTab, setSubTab] = useState("lang");
28
+ const pct = duration > 0 ? clamp01(currentTime / duration) : 0;
29
+ const volPct = clamp01(volume);
30
+ const inIntro = !!introRegion &&
31
+ introRegion[1] > introRegion[0] &&
32
+ currentTime >= introRegion[0] &&
33
+ currentTime <= introRegion[1] &&
34
+ !!onSkipIntro;
35
+ const inOutro = !!outroRegion &&
36
+ outroRegion[1] > outroRegion[0] &&
37
+ currentTime >= outroRegion[0] &&
38
+ currentTime <= outroRegion[1] &&
39
+ !!onSkipOutro;
40
+ const pan = useMemo(() => PanResponder.create({
41
+ onStartShouldSetPanResponder: () => true,
42
+ onMoveShouldSetPanResponder: () => true,
43
+ onPanResponderGrant: (e) => setScrubPct(trackW > 0 ? clamp01(e.nativeEvent.locationX / trackW) : 0),
44
+ onPanResponderMove: (e) => setScrubPct(trackW > 0 ? clamp01(e.nativeEvent.locationX / trackW) : 0),
45
+ onPanResponderRelease: (e) => {
46
+ const p = trackW > 0 ? clamp01(e.nativeEvent.locationX / trackW) : 0;
47
+ setScrubPct(null);
48
+ onSeek(p * duration);
49
+ },
50
+ }), [trackW, duration, onSeek]);
51
+ const volPan = useMemo(() => PanResponder.create({
52
+ onStartShouldSetPanResponder: () => true,
53
+ onMoveShouldSetPanResponder: () => true,
54
+ onPanResponderGrant: (e) => onVolume(volW > 0 ? clamp01(e.nativeEvent.locationX / volW) : 1),
55
+ onPanResponderMove: (e) => onVolume(volW > 0 ? clamp01(e.nativeEvent.locationX / volW) : 1),
56
+ }), [volW, onVolume]);
57
+ const SIZE_MIN = 10;
58
+ const SIZE_MAX = 34;
59
+ const [sizeW, setSizeW] = useState(0);
60
+ const currentSize = subtitleFontSize ?? 14;
61
+ const sizePct = clamp01((currentSize - SIZE_MIN) / (SIZE_MAX - SIZE_MIN));
62
+ const sizePan = useMemo(() => PanResponder.create({
63
+ onStartShouldSetPanResponder: () => true,
64
+ onMoveShouldSetPanResponder: () => true,
65
+ onPanResponderGrant: (e) => {
66
+ if (!onChangeSubtitleFontSize)
67
+ return;
68
+ const p = sizeW > 0 ? clamp01(e.nativeEvent.locationX / sizeW) : 0;
69
+ onChangeSubtitleFontSize?.(Math.round(SIZE_MIN + p * (SIZE_MAX - SIZE_MIN)));
70
+ },
71
+ onPanResponderMove: (e) => {
72
+ if (!onChangeSubtitleFontSize)
73
+ return;
74
+ const p = sizeW > 0 ? clamp01(e.nativeEvent.locationX / sizeW) : 0;
75
+ onChangeSubtitleFontSize?.(Math.round(SIZE_MIN + p * (SIZE_MAX - SIZE_MIN)));
76
+ },
77
+ }), [sizeW, onChangeSubtitleFontSize]);
78
+ const shownPct = scrubPct ?? pct;
79
+ const volIcon = muted || volume === 0 ? "volume-mute" : volume < 0.5 ? "volume-low" : "volume-high";
80
+ return (_jsxs(View, { style: styles.root, children: [_jsx(TouchableOpacity, { style: StyleSheet.absoluteFill, activeOpacity: 1, onPress: onToggleVisible }), buffering && (_jsx(View, { style: styles.centerBadge, pointerEvents: "none", children: _jsx(ActivityIndicator, { color: "#e50914", size: "large" }) })), visible && (_jsxs(_Fragment, { children: [_jsxs(View, { style: [styles.bar, styles.top], pointerEvents: "box-none", children: [!!title && (_jsx(Text, { numberOfLines: 1, style: styles.title, children: title })), _jsx(TouchableOpacity, { onPress: onToggleMute, hitSlop: 8, children: _jsx(Ionicons, { name: muted ? "volume-mute" : "volume-high", size: 20, color: "#fff" }) })] }), _jsxs(View, { style: styles.centerRow, pointerEvents: "box-none", children: [_jsx(TouchableOpacity, { style: styles.skipBtn, onPress: () => onSkip(-10), children: _jsx(Ionicons, { name: "play-back", size: 30, color: "#fff" }) }), _jsx(TouchableOpacity, { style: styles.ppBtn, onPress: onPlayPause, children: _jsx(Ionicons, { name: buffering ? "ellipsis-horizontal" : playing ? "pause" : "play", size: 38, color: "#fff" }) }), _jsx(TouchableOpacity, { style: styles.skipBtn, onPress: () => onSkip(10), children: _jsx(Ionicons, { name: "play-forward", size: 30, color: "#fff" }) })] }), _jsxs(View, { style: [styles.bar, styles.bottom], pointerEvents: "box-none", children: [_jsx(Text, { style: styles.time, children: fmt(scrubPct !== null ? scrubPct * duration : currentTime) }), _jsxs(View, { style: styles.trackWrap, onLayout: (e) => setTrackW(e.nativeEvent.layout.width), ...pan.panHandlers, children: [_jsxs(View, { style: styles.track, children: [loadProgress != null && loadProgress > 0 && (_jsx(View, { style: [
81
+ styles.loadLine,
82
+ { width: `${clamp01(loadProgress) * 100}%` },
83
+ ] })), introRegion && duration > 0 && introRegion[1] > introRegion[0] && (_jsx(View, { style: [
84
+ styles.introHighlight,
85
+ {
86
+ left: `${clamp01(introRegion[0] / duration) * 100}%`,
87
+ width: `${clamp01((introRegion[1] - introRegion[0]) / duration) * 100}%`,
88
+ },
89
+ ] })), outroRegion && duration > 0 && outroRegion[1] > outroRegion[0] && (_jsx(View, { style: [
90
+ styles.outroHighlight,
91
+ {
92
+ left: `${clamp01(outroRegion[0] / duration) * 100}%`,
93
+ width: `${clamp01((outroRegion[1] - outroRegion[0]) / duration) * 100}%`,
94
+ },
95
+ ] })), _jsx(View, { style: [styles.fill, { width: `${shownPct * 100}%` }] })] }), _jsx(View, { style: [
96
+ styles.thumb,
97
+ { left: `${shownPct * 100}%` },
98
+ scrubPct === null && styles.thumbIdle,
99
+ ] })] }), _jsx(Text, { style: styles.time, children: fmt(duration) }), _jsx(TouchableOpacity, { style: [styles.ccBtn, subtitlesOn && styles.ccBtnOn], onPress: onToggleSubtitles, hitSlop: 6, children: _jsx(Text, { style: [styles.ccText, subtitlesOn && styles.ccTextOn], children: "CC" }) }), hasSubs ? (_jsxs(View, { style: styles.subWrap, children: [_jsxs(TouchableOpacity, { style: [styles.langBtn, subtitlesOn && styles.langBtnOn], onPress: () => setSubOpen((v) => !v), hitSlop: 6, children: [_jsx(Text, { style: [styles.ccText, subtitlesOn && styles.ccTextOn], children: subLang }), _jsx(Ionicons, { name: subOpen ? "chevron-up" : "chevron-down", size: 12, color: subtitlesOn ? "#fff" : "#9aa0aa" })] }), subOpen ? (_jsxs(View, { style: styles.subMenu, children: [_jsxs(View, { style: styles.subTabs, children: [_jsx(TouchableOpacity, { style: [styles.subTab, subTab === "lang" && styles.subTabOn], onPress: () => setSubTab("lang"), children: _jsx(Text, { style: [styles.subTabText, subTab === "lang" && styles.subTabTextOn], children: "Language" }) }), _jsx(TouchableOpacity, { style: [styles.subTab, subTab === "size" && styles.subTabOn], onPress: () => setSubTab("size"), children: _jsx(Text, { style: [styles.subTabText, subTab === "size" && styles.subTabTextOn], children: "Text Size" }) })] }), subTab === "lang" ? (_jsx(View, { style: styles.subBody, children: subtitleTracks?.map((lang) => {
100
+ const active = lang === subtitleTrack;
101
+ return (_jsxs(TouchableOpacity, { style: [styles.subItem, active && styles.subItemOn], onPress: () => {
102
+ setSubOpen(false);
103
+ onSelectSubtitleTrack?.(lang);
104
+ }, children: [_jsx(Text, { style: [styles.subItemText, active && styles.subItemTextOn], children: lang }), active ? (_jsx(Ionicons, { name: "checkmark", size: 14, color: "#fff" })) : null] }, lang));
105
+ }) })) : (_jsxs(View, { style: styles.subBody, children: [_jsxs(View, { style: styles.sizeRow, children: [_jsx(TouchableOpacity, { style: styles.sizeBtn, onPress: () => onChangeSubtitleFontSize?.(Math.max(SIZE_MIN, Math.round((subtitleFontSize ?? 14) - 2))), children: _jsx(Ionicons, { name: "remove", size: 16, color: "#fff" }) }), _jsx(View, { style: [
106
+ styles.sizeTrack,
107
+ { opacity: onChangeSubtitleFontSize ? 1 : 0.5 },
108
+ ], onLayout: (e) => setSizeW(e.nativeEvent.layout.width), ...sizePan.panHandlers, children: _jsx(View, { style: [styles.sizeFill, { width: `${sizePct * 100}%` }] }) }), _jsx(TouchableOpacity, { style: styles.sizeBtn, onPress: () => onChangeSubtitleFontSize?.(Math.min(SIZE_MAX, Math.round((subtitleFontSize ?? 14) + 2))), children: _jsx(Ionicons, { name: "add", size: 16, color: "#fff" }) })] }), _jsxs(Text, { style: styles.sizeLabel, children: [currentSize, "px"] })] }))] })) : null] })) : null, _jsx(TouchableOpacity, { onPress: onToggleMute, hitSlop: 6, children: _jsx(Ionicons, { name: volIcon, size: 17, color: "#fff" }) }), _jsx(View, { style: styles.volTrackWrap, onLayout: (e) => setVolW(e.nativeEvent.layout.width), ...volPan.panHandlers, children: _jsx(View, { style: styles.volTrack, children: _jsx(View, { style: [styles.volFill, { width: `${volPct * 100}%` }] }) }) }), _jsx(TouchableOpacity, { onPress: onFullscreen, hitSlop: 8, children: _jsx(Ionicons, { name: "expand", size: 18, color: "#fff" }) })] })] })), visible && inIntro && (_jsx(TouchableOpacity, { style: styles.skipRegionBtn, onPress: onSkipIntro, children: _jsx(Text, { style: styles.skipRegionText, children: "Skip Intro \u203A" }) })), visible && inOutro && (_jsx(TouchableOpacity, { style: styles.skipRegionBtn, onPress: onSkipOutro, children: _jsx(Text, { style: styles.skipRegionText, children: "Skip Outro \u203A" }) }))] }));
109
+ }
110
+ const styles = StyleSheet.create({
111
+ root: { ...StyleSheet.absoluteFillObject, zIndex: 2 },
112
+ centerBadge: {
113
+ ...StyleSheet.absoluteFillObject,
114
+ justifyContent: "center",
115
+ alignItems: "center",
116
+ width: "100%",
117
+ height: "100%",
118
+ },
119
+ bar: {
120
+ position: "absolute",
121
+ left: 0,
122
+ right: 0,
123
+ flexDirection: "row",
124
+ alignItems: "center",
125
+ paddingHorizontal: 12,
126
+ backgroundColor: "rgba(0,0,0,0.45)",
127
+ gap: 8,
128
+ },
129
+ top: { top: 0, height: 42 },
130
+ bottom: { bottom: 0, height: 44 },
131
+ title: { color: "#fff", fontWeight: "700", fontSize: 13, flex: 1 },
132
+ centerRow: {
133
+ ...StyleSheet.absoluteFillObject,
134
+ justifyContent: "center",
135
+ alignItems: "center",
136
+ flexDirection: "row",
137
+ gap: 34,
138
+ },
139
+ ppBtn: {
140
+ width: 64,
141
+ height: 64,
142
+ borderRadius: 32,
143
+ backgroundColor: "rgba(229,9,20,0.92)",
144
+ justifyContent: "center",
145
+ alignItems: "center",
146
+ },
147
+ skipBtn: { opacity: 0.9 },
148
+ time: {
149
+ color: "#ddd",
150
+ fontSize: 11,
151
+ fontVariant: ["tabular-nums"],
152
+ minWidth: 58,
153
+ },
154
+ trackWrap: { flex: 1, paddingVertical: 12, justifyContent: "center" },
155
+ track: {
156
+ height: 4,
157
+ borderRadius: 2,
158
+ backgroundColor: "#3a3f47",
159
+ overflow: "hidden",
160
+ },
161
+ fill: { height: 4, borderRadius: 2, backgroundColor: "#e50914" },
162
+ loadLine: {
163
+ position: "absolute",
164
+ left: 0,
165
+ top: 0,
166
+ height: 4,
167
+ borderRadius: 2,
168
+ backgroundColor: "rgba(255,255,255,0.35)",
169
+ },
170
+ introHighlight: {
171
+ position: "absolute",
172
+ top: 0,
173
+ height: 4,
174
+ backgroundColor: "rgba(0,229,255,0.55)",
175
+ },
176
+ outroHighlight: {
177
+ position: "absolute",
178
+ top: 0,
179
+ height: 4,
180
+ backgroundColor: "rgba(255,0,200,0.55)",
181
+ },
182
+ skipRegionBtn: {
183
+ position: "absolute",
184
+ right: 16,
185
+ bottom: 60,
186
+ flexDirection: "row",
187
+ alignItems: "center",
188
+ backgroundColor: "rgba(0,0,0,0.6)",
189
+ borderWidth: 1,
190
+ borderColor: "rgba(255,255,255,0.25)",
191
+ borderRadius: 20,
192
+ paddingHorizontal: 14,
193
+ paddingVertical: 8,
194
+ },
195
+ skipRegionText: { color: "#fff", fontWeight: "700", fontSize: 13 },
196
+ thumb: {
197
+ position: "absolute",
198
+ top: 12 - 7 + 2,
199
+ marginLeft: -6,
200
+ width: 12,
201
+ height: 12,
202
+ borderRadius: 6,
203
+ backgroundColor: "#e50914",
204
+ },
205
+ thumbIdle: { opacity: 0.85 },
206
+ ccBtn: {
207
+ borderWidth: 1.5,
208
+ borderColor: "#666",
209
+ borderRadius: 4,
210
+ paddingHorizontal: 5,
211
+ paddingVertical: 1,
212
+ },
213
+ ccBtnOn: { borderColor: "#e50914", backgroundColor: "rgba(229,9,20,0.25)" },
214
+ ccText: { color: "#888", fontSize: 10, fontWeight: "900" },
215
+ ccTextOn: { color: "#fff" },
216
+ langBtn: {
217
+ borderWidth: 1.5,
218
+ borderColor: "#555",
219
+ borderRadius: 4,
220
+ paddingHorizontal: 6,
221
+ paddingVertical: 1,
222
+ },
223
+ langBtnOn: {
224
+ borderColor: "#7dd3fc",
225
+ backgroundColor: "rgba(125,211,252,0.18)",
226
+ },
227
+ subWrap: { position: "relative", zIndex: 20 },
228
+ subMenu: {
229
+ position: "absolute",
230
+ bottom: 26,
231
+ right: 0,
232
+ zIndex: 30,
233
+ width: 190,
234
+ backgroundColor: "#14161c",
235
+ borderWidth: 1,
236
+ borderColor: "#2a2f3a",
237
+ borderRadius: 8,
238
+ overflow: "hidden",
239
+ elevation: 12,
240
+ shadowColor: "#000",
241
+ shadowOpacity: 0.5,
242
+ shadowRadius: 10,
243
+ shadowOffset: { width: 0, height: 3 },
244
+ },
245
+ subTabs: { flexDirection: "row", borderBottomWidth: 1, borderBottomColor: "#262a34" },
246
+ subTab: { flex: 1, alignItems: "center", paddingVertical: 7, backgroundColor: "#1a1d24" },
247
+ subTabOn: { backgroundColor: "#232833" },
248
+ subTabText: { color: "#9aa0aa", fontSize: 12, fontWeight: "700" },
249
+ subTabTextOn: { color: "#7dd3fc", fontWeight: "800" },
250
+ subBody: { paddingVertical: 4 },
251
+ subItem: {
252
+ flexDirection: "row",
253
+ alignItems: "center",
254
+ justifyContent: "space-between",
255
+ gap: 10,
256
+ paddingHorizontal: 12,
257
+ paddingVertical: 7,
258
+ },
259
+ subItemOn: { backgroundColor: "rgba(125,211,252,0.15)" },
260
+ subItemText: { color: "#cbd5e1", fontSize: 13, fontWeight: "600" },
261
+ subItemTextOn: { color: "#7dd3fc", fontWeight: "800" },
262
+ sizeRow: {
263
+ flexDirection: "row",
264
+ alignItems: "center",
265
+ gap: 10,
266
+ paddingHorizontal: 10,
267
+ paddingVertical: 10,
268
+ },
269
+ sizeBtn: {
270
+ width: 24,
271
+ height: 24,
272
+ borderRadius: 4,
273
+ alignItems: "center",
274
+ justifyContent: "center",
275
+ backgroundColor: "#2a2f3a",
276
+ },
277
+ sizeTrack: { flex: 1, height: 22, justifyContent: "center" },
278
+ sizeFill: { height: 4, borderRadius: 2, backgroundColor: "#7dd3fc" },
279
+ sizeLabel: {
280
+ color: "#9aa0aa",
281
+ fontSize: 11,
282
+ fontWeight: "700",
283
+ textAlign: "center",
284
+ paddingBottom: 6,
285
+ },
286
+ volTrackWrap: { width: 56, paddingVertical: 12, justifyContent: "center" },
287
+ volTrack: { height: 4, borderRadius: 2, backgroundColor: "#3a3f47", overflow: "hidden" },
288
+ volFill: { height: 4, backgroundColor: "#fff" },
289
+ });
@@ -0,0 +1,15 @@
1
+ export type SubCueShape = {
2
+ start: number;
3
+ end: number;
4
+ text: string;
5
+ };
6
+ /**
7
+ * Renders the active subtitle cue over the video surface. Positioned
8
+ * absolutely with zIndex 1 (below the controls at zIndex 2, above the
9
+ * video background at zIndex 0).
10
+ */
11
+ export declare function SubtitleDisplay({ cue, enabled, fontSize, }: {
12
+ cue: SubCueShape | null;
13
+ enabled: boolean;
14
+ fontSize: number;
15
+ }): import("react").JSX.Element | null;
@@ -0,0 +1,32 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { StyleSheet, Text, View } from "react-native";
3
+ /**
4
+ * Renders the active subtitle cue over the video surface. Positioned
5
+ * absolutely with zIndex 1 (below the controls at zIndex 2, above the
6
+ * video background at zIndex 0).
7
+ */
8
+ export function SubtitleDisplay({ cue, enabled, fontSize, }) {
9
+ if (!enabled || !cue?.text)
10
+ return null;
11
+ return (_jsx(View, { pointerEvents: "none", style: styles.wrap, children: _jsx(Text, { style: [styles.text, { fontSize, lineHeight: fontSize * 1.35 }], children: cue.text }) }));
12
+ }
13
+ const styles = StyleSheet.create({
14
+ wrap: {
15
+ position: "absolute",
16
+ left: 0,
17
+ right: 0,
18
+ bottom: 56,
19
+ alignItems: "center",
20
+ paddingHorizontal: 24,
21
+ zIndex: 1,
22
+ },
23
+ text: {
24
+ backgroundColor: "rgba(0,0,0,0.65)",
25
+ color: "#fff",
26
+ textAlign: "center",
27
+ paddingHorizontal: 8,
28
+ paddingVertical: 3,
29
+ borderRadius: 4,
30
+ overflow: "hidden",
31
+ },
32
+ });
@@ -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";
@@ -0,0 +1,3 @@
1
+ export { MegaVideoPlayer } from "./MegaVideoPlayerWeb.js";
2
+ export { PlayerControls } from "./PlayerControlsWeb.js";
3
+ export { SubtitleDisplay } from "./SubtitleDisplayWeb.js";
@@ -219,13 +219,21 @@ function handleRequest(socket, req) {
219
219
  if (req.method && req.method !== "GET" && req.method !== "HEAD") {
220
220
  console.warn(`[megaStream] unexpected method ${req.method} — serving as GET`);
221
221
  }
222
- const [sessionId, name] = req.target.replace(/^\//, "").split(/\?|#/)[0].split("/", 2);
222
+ const [sessionId, encodedName] = req.target.replace(/^\//, "").split(/\?|#/)[0].split("/", 2);
223
223
  if (!sessionId)
224
224
  return serveError(socket, 404, "Not Found");
225
225
  const session = sessions.get(sessionId);
226
226
  if (!session)
227
227
  return serveError(socket, 404, `No stream session ${sessionId}`);
228
- if (name) {
228
+ if (encodedName) {
229
+ // Clients send the URL-encoded name on the wire (e.g. "My%20Video.mp4").
230
+ let name;
231
+ try {
232
+ name = decodeURIComponent(encodedName);
233
+ }
234
+ catch {
235
+ name = encodedName; // not valid encoding — match literally
236
+ }
229
237
  const part = session.parts.find((p) => p.name === name);
230
238
  if (!part)
231
239
  return serveError(socket, 404, `Not Found: ${name}`);
@@ -284,7 +292,7 @@ export async function createStream(megaUrl) {
284
292
  const sessionId = newSessionId();
285
293
  const name = file.name || "stream.mp4";
286
294
  sessions.set(sessionId, { parts: [{ name, file, size: file.size }] });
287
- const url = `${proxyBase}/${sessionId}/${name}`;
295
+ const url = `${proxyBase}/${sessionId}/${encodeURIComponent(name)}`;
288
296
  console.log(`[megaStream] session ${sessionId}: serving '${name}' via ${url}`);
289
297
  return { url, sessionId };
290
298
  }
@@ -0,0 +1,3 @@
1
+ {
2
+ "platforms": []
3
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mega-stream-proxy",
3
- "version": "0.1.9",
3
+ "version": "0.2.0",
4
4
  "description": "Stream decrypted MEGA files over HTTP byte ranges on Android (react-native-tcp-socket) and the web (Service Worker), play them with an Expo Video player + subtitles, and manage MEGA accounts with pluggable storage. Zero-config: runtime native deps are regular dependencies (auto-linked).",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -28,12 +28,12 @@
28
28
  "default": "./dist/proxy/sw/bundle.js"
29
29
  },
30
30
  "./player": {
31
- "types": "./dist/Player/playerweb/index.d.ts",
32
- "default": "./dist/Player/playerweb/index.js"
31
+ "types": "./dist/player/index.d.ts",
32
+ "default": "./dist/player/index.js"
33
33
  },
34
- "./playerAndroid": {
35
- "types": "./dist/Player/playerAndroid/index.d.ts",
36
- "default": "./dist/Player/playerAndroid/index.js"
34
+ "./playerweb": {
35
+ "types": "./dist/playerweb/index.d.ts",
36
+ "default": "./dist/playerweb/index.js"
37
37
  },
38
38
  "./subtitles": {
39
39
  "types": "./dist/subtitles/index.d.ts",
@@ -43,7 +43,8 @@
43
43
  "files": [
44
44
  "dist",
45
45
  "android",
46
- "src"
46
+ "src",
47
+ "expo-module.config.json"
47
48
  ],
48
49
  "scripts": {
49
50
  "build": "tsc -p tsconfig.json && node scripts/copy-sw-assets.js",
@@ -57,15 +58,18 @@
57
58
  "dependencies": {
58
59
  "@react-native-async-storage/async-storage": "^2.0.0",
59
60
  "buffer": "^6.0.3",
61
+ "expo-modules-core": "^2.1.2 || ^57.0.0",
62
+ "expo-video": "^3.0.15",
60
63
  "megajs": "^1.3.0",
61
64
  "react-native-tcp-socket": "^6.1.0"
62
65
  },
63
66
  "peerDependencies": {
64
67
  "@expo/vector-icons": ">=13",
68
+ "expo": ">=52",
65
69
  "expo-font": ">=13",
66
- "expo-video": ">=1.0",
67
70
  "react": ">=18",
68
- "react-native": ">=0.71"
71
+ "react-native": ">=0.76",
72
+ "react-native-video": ">=6"
69
73
  },
70
74
  "peerDependenciesMeta": {
71
75
  "@expo/vector-icons": {
@@ -73,12 +77,6 @@
73
77
  },
74
78
  "expo-font": {
75
79
  "optional": true
76
- },
77
- "expo-video": {
78
- "optional": true
79
- },
80
- "react-native-video": {
81
- "optional": true
82
80
  }
83
81
  },
84
82
  "devDependencies": {
@@ -87,12 +85,11 @@
87
85
  "@types/react": "~18.3.12",
88
86
  "buffer": "^6.0.3",
89
87
  "expo-font": "~13.0.4",
90
- "expo-video": "~2.0.6",
91
- "megajs": "^1.3.0",
88
+ "expo-modules-core": "~2.1.4",
92
89
  "react": "18.3.1",
93
90
  "react-native": "0.76.9",
94
91
  "react-native-tcp-socket": "^6.1.0",
95
- "react-native-video": ">=6",
92
+ "react-native-video": "^6.19.2",
96
93
  "typescript": "^5.5.0"
97
94
  },
98
95
  "keywords": [
package/src/megaFile.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import type { MegaFileLike } from "./proxy/types.js";
2
2
 
3
3
  const ATTRS_TIMEOUT_MS = 30_000;
4
+ const ATTRS_RETRIES = 2;
4
5
 
5
6
  /** Lazily resolves megajs `File.fromURL` + attributes (incl. folder-link child). */
6
7
  export async function loadMegaFile(url: string): Promise<MegaFileLike> {
@@ -8,23 +9,35 @@ export async function loadMegaFile(url: string): Promise<MegaFileLike> {
8
9
  File: { fromURL: (u: string) => MegaFileLike };
9
10
  };
10
11
  const file = File.fromURL(url.trim());
11
- await withTimeout<unknown>(
12
- file.loadAttributes(),
13
- ATTRS_TIMEOUT_MS,
14
- "MEGA did not respond. Check your connection and try again.",
15
- );
16
12
 
17
- // Folder links: loadAttributes resolves to the folder itself — pick the
18
- // first non-directory child instead (mirrors the megajs docs sample).
19
- const group = file as unknown as {
20
- children?: unknown[];
21
- find?: (fn: (n: any) => boolean, recursive: boolean) => MegaFileLike | undefined;
22
- };
23
- if (group.children?.length) {
24
- const child = group.find?.((n: any) => !n.children, true);
25
- if (child) return child;
13
+ // MEGA API calls can fail transiently (dns/rate-limit/network blip). Retry a
14
+ // couple of times before surfacing "MEGA did not respond."
15
+ let lastError: unknown;
16
+ for (let attempt = 1; attempt <= ATTRS_RETRIES; attempt++) {
17
+ try {
18
+ await withTimeout<unknown>(
19
+ file.loadAttributes(),
20
+ ATTRS_TIMEOUT_MS,
21
+ "MEGA did not respond. Check your connection and try again.",
22
+ );
23
+
24
+ // Folder links: loadAttributes resolves to the folder itself — pick the
25
+ // first non-directory child instead (mirrors the megajs docs sample).
26
+ const group = file as unknown as {
27
+ children?: unknown[];
28
+ find?: (fn: (n: any) => boolean, recursive: boolean) => MegaFileLike | undefined;
29
+ };
30
+ if (group.children?.length) {
31
+ const child = group.find?.((n: any) => !n.children, true);
32
+ if (child) return child;
33
+ }
34
+ return file;
35
+ } catch (e) {
36
+ lastError = e;
37
+ console.warn(`[megaStream] loadAttributes attempt ${attempt}/${ATTRS_RETRIES} failed`);
38
+ }
26
39
  }
27
- return file;
40
+ throw lastError instanceof Error ? lastError : new Error(String(lastError));
28
41
  }
29
42
 
30
43
  export function fileSizeOf(file: { size?: number }): number {