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.
- package/LICENSE +21 -0
- package/README.md +518 -0
- package/android/build.gradle +31 -0
- package/android/src/main/AndroidManifest.xml +3 -0
- package/dist/Player/native.d.ts +3 -0
- package/dist/Player/native.js +3 -0
- package/dist/Player/playerAndroid/MegaVideoPlayerAndroid.d.ts +47 -0
- package/dist/Player/playerAndroid/MegaVideoPlayerAndroid.js +624 -0
- package/dist/Player/playerAndroid/PlayerControlsAndroid.d.ts +54 -0
- package/dist/Player/playerAndroid/PlayerControlsAndroid.js +370 -0
- package/dist/Player/playerAndroid/SubtitleDisplayAndroid.d.ts +21 -0
- package/dist/Player/playerAndroid/SubtitleDisplayAndroid.js +55 -0
- package/dist/Player/playerAndroid/index.d.ts +3 -0
- package/dist/Player/playerAndroid/index.js +3 -0
- package/dist/Player/playerweb/MegaVideoPlayer.d.ts +92 -0
- package/dist/Player/playerweb/MegaVideoPlayer.js +368 -0
- package/dist/Player/playerweb/PlayerControls.d.ts +52 -0
- package/dist/Player/playerweb/PlayerControls.js +346 -0
- package/dist/Player/playerweb/SubtitleDisplay.d.ts +15 -0
- package/dist/Player/playerweb/SubtitleDisplay.js +32 -0
- package/dist/Player/playerweb/index.d.ts +3 -0
- package/dist/Player/playerweb/index.js +3 -0
- package/dist/Player/web.d.ts +3 -0
- package/dist/Player/web.js +3 -0
- package/dist/accounts/async-storage.d.ts +10 -0
- package/dist/accounts/async-storage.js +46 -0
- package/dist/accounts/index.d.ts +4 -0
- package/dist/accounts/index.js +3 -0
- package/dist/accounts/local-storage.d.ts +3 -0
- package/dist/accounts/local-storage.js +63 -0
- package/dist/accounts/manager.d.ts +39 -0
- package/dist/accounts/manager.js +149 -0
- package/dist/accounts/types.d.ts +35 -0
- package/dist/accounts/types.js +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +4 -0
- package/dist/megaFile.d.ts +7 -0
- package/dist/megaFile.js +31 -0
- package/dist/proxy/native.d.ts +25 -0
- package/dist/proxy/native.js +336 -0
- package/dist/proxy/sw/bundle.d.ts +21 -0
- package/dist/proxy/sw/bundle.js +32 -0
- package/dist/proxy/sw/megajs.browser.js +27 -0
- package/dist/proxy/sw/sw.js +212 -0
- package/dist/proxy/types.d.ts +15 -0
- package/dist/proxy/types.js +2 -0
- package/dist/proxy/web.d.ts +24 -0
- package/dist/proxy/web.js +231 -0
- package/dist/ranges.d.ts +30 -0
- package/dist/ranges.js +63 -0
- package/dist/stream.d.ts +9 -0
- package/dist/stream.js +100 -0
- package/dist/subtitles/index.d.ts +9 -0
- package/dist/subtitles/index.js +2 -0
- package/dist/subtitles/load.d.ts +18 -0
- package/dist/subtitles/load.js +58 -0
- package/dist/subtitles/parse.d.ts +19 -0
- package/dist/subtitles/parse.js +95 -0
- package/dist/swDefine.d.ts +5 -0
- package/dist/swDefine.js +5 -0
- package/package.json +112 -0
- package/src/Player/playerAndroid/MegaVideoPlayerAndroid.tsx +825 -0
- package/src/Player/playerAndroid/PlayerControlsAndroid.tsx +824 -0
- package/src/Player/playerAndroid/SubtitleDisplayAndroid.tsx +80 -0
- package/src/Player/playerAndroid/index.ts +7 -0
- package/src/Player/playerweb/MegaVideoPlayer.tsx +551 -0
- package/src/Player/playerweb/PlayerControls.tsx +798 -0
- package/src/Player/playerweb/SubtitleDisplay.tsx +48 -0
- package/src/Player/playerweb/index.ts +3 -0
- package/src/accounts/async-storage.ts +55 -0
- package/src/accounts/index.ts +9 -0
- package/src/accounts/local-storage.ts +66 -0
- package/src/accounts/manager.ts +174 -0
- package/src/accounts/types.ts +38 -0
- package/src/index.ts +10 -0
- package/src/megaFile.ts +48 -0
- package/src/proxy/native.ts +396 -0
- package/src/proxy/sw/bundle.ts +37 -0
- package/src/proxy/sw/megajs.browser.js +27 -0
- package/src/proxy/sw/sw.js +212 -0
- package/src/proxy/types.ts +17 -0
- package/src/proxy/web.ts +266 -0
- package/src/ranges.ts +72 -0
- package/src/stream.ts +117 -0
- package/src/subtitles/index.ts +15 -0
- package/src/subtitles/load.ts +83 -0
- package/src/subtitles/parse.ts +105 -0
- package/src/swDefine.ts +5 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 mega-stream-proxy contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,518 @@
|
|
|
1
|
+
# mega-stream-proxy
|
|
2
|
+
|
|
3
|
+
Stream decrypted MEGA files over HTTP **byte ranges** — exactly like a VOD server —
|
|
4
|
+
on **Android/iOS** (a local TCP proxy) and the **web** (a Service Worker). Ships a
|
|
5
|
+
ready-to-use Expo Video player + subtitle engine, and MEGA **account management**
|
|
6
|
+
with pluggable storage.
|
|
7
|
+
|
|
8
|
+
No server required. No full-file downloads at playback time. The video element asks
|
|
9
|
+
for the exact range it needs and `mega-stream-proxy` decrypts and serves only those bytes.
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
mega-link ──> megajs File.fromURL ──┬─> Android/iOS: 127.0.0.1:PORT/… ──> VideoPlayer
|
|
13
|
+
└─> Web: Service Worker /__mmstream__/… ──> <video>
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Features
|
|
17
|
+
|
|
18
|
+
- **Native proxy** (`/native`): runs a local HTTP server via `react-native-tcp-socket`,
|
|
19
|
+
answers `Range` requests by piping decrypted megajs chunks (`maxConnections: 4`).
|
|
20
|
+
- **Web proxy** (`/web` + `/sw`): registers the bundled Service Worker, returns
|
|
21
|
+
same-origin `/__mmstream__/…` URLs that the worker range-streams on demand.
|
|
22
|
+
Graceful fallback: without a secure context it downloads+decrypts into Blob URLs.
|
|
23
|
+
- **Player** (`/player`): cross-platform `MegaVideoPlayer` (expo-video) with custom
|
|
24
|
+
controls, multi-part auto-advance, subtitle overlay (srt/ass/vtt), subtitle track
|
|
25
|
+
picker, text-size slider, intro/outro skip regions.
|
|
26
|
+
- **Subtitles** (`/subtitles`): SRT/ASS/VTT parser, format auto-detect including
|
|
27
|
+
content sniffing for extension-less MEGA links.
|
|
28
|
+
- **Accounts** (`/accounts`): `MegaAccountManager` with CRUD, live megajs sessions,
|
|
29
|
+
Cloud-Drive tree, file export-link, and a pluggable `MegaAccountStore`
|
|
30
|
+
(`createLocalStorageStore` for web, `createAsyncStorageStore` for React Native,
|
|
31
|
+
or your own backend).
|
|
32
|
+
|
|
33
|
+
## Requirements
|
|
34
|
+
|
|
35
|
+
- **Web**: a modern browser at `https:` (or `localhost`) for Service Worker
|
|
36
|
+
streaming; Node 18+ for the `copySwAssets` deploy step.
|
|
37
|
+
- **Android / iOS**: React Native ≥ 0.71 (Expo SDK 52+ recommended) — the native
|
|
38
|
+
streaming module is auto-linked, no manual Gradle/Xcode edits.
|
|
39
|
+
- All platforms need `react`, `react-native`, `expo-video`, and `@expo/vector-icons`
|
|
40
|
+
(Expo apps already ship the last two).
|
|
41
|
+
|
|
42
|
+
## Install
|
|
43
|
+
|
|
44
|
+
```sh
|
|
45
|
+
# from npm (once published)
|
|
46
|
+
npm install mega-stream-proxy
|
|
47
|
+
|
|
48
|
+
# or from the packed tarball / a local folder
|
|
49
|
+
npm install ./mega-stream-proxy-0.1.0.tgz
|
|
50
|
+
npm install "file:../mega-stream-proxy"
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Runtime dependencies are **regular `dependencies`**, so installing the package pulls
|
|
54
|
+
in everything (megajs, buffer, react-native-tcp-socket, async-storage) with **zero
|
|
55
|
+
extra setup**:
|
|
56
|
+
|
|
57
|
+
- React Native apps: `react-native-tcp-socket` is **auto-linked** by RN autolinking —
|
|
58
|
+
you do **not** edit `android/build.gradle`. The package ships an `android/` gradle
|
|
59
|
+
module purely for clean autolinking; it contains no native code.
|
|
60
|
+
- Web apps: `copySwAssets()` copies the worker + vendored megajs browser build into
|
|
61
|
+
your `public/` directory. No bundler changes.
|
|
62
|
+
|
|
63
|
+
`react`, `react-native`, `expo-video` and `@expo/vector-icons` are peer/optional deps —
|
|
64
|
+
your Expo app already provides them.
|
|
65
|
+
|
|
66
|
+
## Android / iOS (native streaming)
|
|
67
|
+
|
|
68
|
+
```tsx
|
|
69
|
+
import { useEffect, useMemo } from "react";
|
|
70
|
+
import { View } from "react-native";
|
|
71
|
+
import { ensureProxy, createStream, releaseStream } from "mega-stream-proxy/native";
|
|
72
|
+
import { MegaVideoPlayerWeb } from "mega-stream-proxy/player";
|
|
73
|
+
|
|
74
|
+
export function WatchScreen({ megaUrl, subtitleUrl }) {
|
|
75
|
+
const ref = useMemo(() => ({ url: null as string | null, sessionId: null as string | null }), []);
|
|
76
|
+
|
|
77
|
+
useEffect(() => {
|
|
78
|
+
let alive = true;
|
|
79
|
+
void ensureProxy(); // idempotent — starts the singleton server once per app lifetime
|
|
80
|
+
createStream(megaUrl).then(({ url, sessionId }) => {
|
|
81
|
+
if (!alive) {
|
|
82
|
+
releaseStream(sessionId);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
ref.url = url;
|
|
86
|
+
ref.sessionId = sessionId;
|
|
87
|
+
});
|
|
88
|
+
return () => {
|
|
89
|
+
alive = false;
|
|
90
|
+
if (ref.sessionId) releaseStream(ref.sessionId);
|
|
91
|
+
};
|
|
92
|
+
}, [megaUrl]);
|
|
93
|
+
|
|
94
|
+
return (
|
|
95
|
+
<View style={{ flex: 1 }}>
|
|
96
|
+
<MegaVideoPlayerWeb uri={ref.url} subtitleUrl={subtitleUrl} autoPlay />
|
|
97
|
+
</View>
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
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
|
+
- `ensureProxy(): Promise<void>` — idempotent; binds the singleton local server once
|
|
142
|
+
per app lifetime. Safe to call repeatedly; no-op until `stopProxy()`.
|
|
143
|
+
- `createStream(megaUrl): Promise<{ url, sessionId }>` — one keyed session per MEGA
|
|
144
|
+
link; concurrent streams get independent routes on the same server.
|
|
145
|
+
- `createStreams(megaUrls): Promise<{ urls, sessionId }>` — ordered parts, played
|
|
146
|
+
back-to-back by the player, under one session.
|
|
147
|
+
- `releaseStream(sessionId)` — tears down that session only; other sessions and the
|
|
148
|
+
server keep running.
|
|
149
|
+
- `stopProxy()` — unbinds the server and clears all sessions (tests / HMR).
|
|
150
|
+
|
|
151
|
+
Android networking: loopback `http://127.0.0.1` is allowed in Expo dev builds by
|
|
152
|
+
default — no permissions or cleartext config is needed. For a release APK keep
|
|
153
|
+
`usesCleartextTraffic` enabled (your RN app templates usually set it) or the
|
|
154
|
+
stream URL stays on loopback, which most players accept.
|
|
155
|
+
|
|
156
|
+
## Web
|
|
157
|
+
|
|
158
|
+
### 1. Copy the Service Worker assets (build/deploy step, node)
|
|
159
|
+
|
|
160
|
+
```js
|
|
161
|
+
import { copySwAssets } from "mega-stream-proxy/sw";
|
|
162
|
+
|
|
163
|
+
// e.g. Expo web: copy into ./public (before `expo export`)
|
|
164
|
+
const files = copySwAssets("public");
|
|
165
|
+
console.log("copied", files.sw, files.megajs);
|
|
166
|
+
// sw.js -> public/sw.js (the range-streaming worker)
|
|
167
|
+
// megajs.browser.js -> public/megajs.browser.js (vendored megajs UMD)
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Server from a host that allows a Service Worker (`https:` or `localhost`).
|
|
171
|
+
|
|
172
|
+
### 2. Stream + play
|
|
173
|
+
|
|
174
|
+
```tsx
|
|
175
|
+
import { useEffect, useMemo } from "react";
|
|
176
|
+
import { View } from "react-native";
|
|
177
|
+
import { ensureProxy, createStream, releaseStream } from "mega-stream-proxy/web";
|
|
178
|
+
import { MegaVideoPlayerWeb } from "mega-stream-proxy/player";
|
|
179
|
+
|
|
180
|
+
export function WatchScreen({ megaUrl, subtitleUrl }) {
|
|
181
|
+
const ref = useMemo(() => ({ url: null as string | null, sessionId: null as string | null }), []);
|
|
182
|
+
|
|
183
|
+
useEffect(() => {
|
|
184
|
+
let alive = true;
|
|
185
|
+
void ensureProxy(); // idempotent — ensures the Service Worker is registered once
|
|
186
|
+
createStream(megaUrl).then(({ url, sessionId }) => {
|
|
187
|
+
if (!alive) {
|
|
188
|
+
releaseStream(sessionId);
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
ref.url = url; // e.g. /__mmstream__/abc/part-0.mp4
|
|
192
|
+
ref.sessionId = sessionId;
|
|
193
|
+
});
|
|
194
|
+
return () => {
|
|
195
|
+
alive = false;
|
|
196
|
+
if (ref.sessionId) releaseStream(ref.sessionId);
|
|
197
|
+
};
|
|
198
|
+
}, [megaUrl]);
|
|
199
|
+
|
|
200
|
+
return (
|
|
201
|
+
<View style={{ flex: 1 }}>
|
|
202
|
+
<MegaVideoPlayerWeb uri={ref.url} subtitleUrl={subtitleUrl} autoPlay />
|
|
203
|
+
</View>
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
The web API matches the native one exactly: `ensureProxy()`, `createStream(megaUrl) →
|
|
209
|
+
{ url, sessionId }`, `createStreams(megaUrls) → { urls, sessionId }`,
|
|
210
|
+
`releaseStream(sessionId)`, and `stopProxy()`. Each `createStream`/`createStreams` call
|
|
211
|
+
registers its own keyed session — several streams can coexist, and releasing one
|
|
212
|
+
session leaves the others untouched.
|
|
213
|
+
|
|
214
|
+
If no Service Worker is available the proxy silently falls back to full download into
|
|
215
|
+
Blob object URLs (still plays).
|
|
216
|
+
|
|
217
|
+
## Using the proxy URL with your own player (expo-video / expo-av)
|
|
218
|
+
|
|
219
|
+
You don't need `MegaVideoPlayer` — the proxy returns a plain, range-capable HTTP URL,
|
|
220
|
+
so it works with any player that speaks HTTP ranges. The only lifecycle to remember is:
|
|
221
|
+
call `ensureProxy()` once, hold the `sessionId` from `createStream`, and call
|
|
222
|
+
`releaseStream(sessionId)` in your unmount cleanup. Import from
|
|
223
|
+
`mega-stream-proxy/native` on Android/iOS or `mega-stream-proxy/web` on the web — the
|
|
224
|
+
API is identical.
|
|
225
|
+
|
|
226
|
+
### expo-video (VideoView)
|
|
227
|
+
|
|
228
|
+
```tsx
|
|
229
|
+
import { useEffect, useRef } from "react";
|
|
230
|
+
import { View } from "react-native";
|
|
231
|
+
import { createVideoPlayer, VideoView } from "expo-video";
|
|
232
|
+
import { ensureProxy, createStream, releaseStream } from "mega-stream-proxy/web"; // or /native
|
|
233
|
+
|
|
234
|
+
export function WatchScreen({ megaUrl }) {
|
|
235
|
+
const player = useRef(createVideoPlayer(null)).current;
|
|
236
|
+
const sessionId = useRef<string | null>(null);
|
|
237
|
+
|
|
238
|
+
useEffect(() => {
|
|
239
|
+
let alive = true;
|
|
240
|
+
void ensureProxy(); // idempotent
|
|
241
|
+
createStream(megaUrl).then(({ url, sessionId: id }) => {
|
|
242
|
+
if (!alive) {
|
|
243
|
+
releaseStream(id);
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
sessionId.current = id;
|
|
247
|
+
player.replace({ uri: url });
|
|
248
|
+
});
|
|
249
|
+
return () => {
|
|
250
|
+
alive = false;
|
|
251
|
+
if (sessionId.current) releaseStream(sessionId.current);
|
|
252
|
+
player.release();
|
|
253
|
+
};
|
|
254
|
+
}, [megaUrl]);
|
|
255
|
+
|
|
256
|
+
return (
|
|
257
|
+
<View style={{ flex: 1 }}>
|
|
258
|
+
<VideoView player={player} style={{ flex: 1 }} contentFit="contain" />
|
|
259
|
+
</View>
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
### expo-av (Video)
|
|
265
|
+
|
|
266
|
+
```tsx
|
|
267
|
+
import { useEffect, useRef } from "react";
|
|
268
|
+
import { View } from "react-native";
|
|
269
|
+
import { Video, ResizeMode } from "expo-av";
|
|
270
|
+
import { ensureProxy, createStream, releaseStream } from "mega-stream-proxy/web"; // or /native
|
|
271
|
+
|
|
272
|
+
export function WatchScreen({ megaUrl }) {
|
|
273
|
+
const uri = useRef<string | null>(null);
|
|
274
|
+
const sessionId = useRef<string | null>(null);
|
|
275
|
+
|
|
276
|
+
useEffect(() => {
|
|
277
|
+
let alive = true;
|
|
278
|
+
void ensureProxy(); // idempotent
|
|
279
|
+
createStream(megaUrl).then(({ url, sessionId: id }) => {
|
|
280
|
+
if (!alive) {
|
|
281
|
+
releaseStream(id);
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
sessionId.current = id;
|
|
285
|
+
uri.current = url;
|
|
286
|
+
});
|
|
287
|
+
return () => {
|
|
288
|
+
alive = false;
|
|
289
|
+
if (sessionId.current) releaseStream(sessionId.current);
|
|
290
|
+
};
|
|
291
|
+
}, [megaUrl]);
|
|
292
|
+
|
|
293
|
+
return (
|
|
294
|
+
<View style={{ flex: 1 }}>
|
|
295
|
+
<Video
|
|
296
|
+
style={{ flex: 1 }}
|
|
297
|
+
source={{ uri: uri.current ?? undefined }}
|
|
298
|
+
resizeMode={ResizeMode.CONTAIN}
|
|
299
|
+
shouldPlay
|
|
300
|
+
/>
|
|
301
|
+
</View>
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
The same pattern applies to multi-part episodes: `createStreams(megaUrls)` returns
|
|
307
|
+
`{ urls, sessionId }` — feed the ordered URLs to your player's queue or switch the
|
|
308
|
+
`source` when one part ends, then `releaseStream(sessionId)` once when you leave.
|
|
309
|
+
|
|
310
|
+
## Player component
|
|
311
|
+
|
|
312
|
+
`import { MegaVideoPlayerWeb, PlayerControlsWeb, SubtitleDisplayWeb } from "mega-stream-proxy/player";`
|
|
313
|
+
|
|
314
|
+
This is the **same player MyMovies uses** (`components/MegaVideoPlayerWeb.tsx`),
|
|
315
|
+
byte-for-byte — same expo-video engine, same custom controls (scrub bar, volume,
|
|
316
|
+
skip ±10s, subtitle picker + text-size slider, intro/outro regions), same subtitle
|
|
317
|
+
overlay style, same auto-hiding chrome, and the same multi-part advance logic. It
|
|
318
|
+
runs identically on Android, iOS and web.
|
|
319
|
+
|
|
320
|
+
### Simple usage — pass the subtitle URL as a prop
|
|
321
|
+
|
|
322
|
+
```tsx
|
|
323
|
+
<MegaVideoPlayerWeb
|
|
324
|
+
uri="https://…/part-0.mp4" // or the proxy URL from /native or /web
|
|
325
|
+
title="Episode 1"
|
|
326
|
+
subtitleUrl="https://cdn.example.com/subs.srt" // <-- the subtitle file
|
|
327
|
+
subtitleFormat="srt" // optional (auto-detected from URL/extension/content)
|
|
328
|
+
subtitleLang="English" // language name shown in the player controls
|
|
329
|
+
autoPlay
|
|
330
|
+
/>
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
Pass `subtitleLang` to control the language name the player displays in the
|
|
334
|
+
controls (the `CC`/language chip next to the progress bar). Without it the player
|
|
335
|
+
shows `SRT`.
|
|
336
|
+
|
|
337
|
+
### Selecting subtitles from the list
|
|
338
|
+
|
|
339
|
+
The player shows a subtitle selector in the controls (same UI as MyMovies):
|
|
340
|
+
|
|
341
|
+
- The **`CC` button** toggles subtitles on/off.
|
|
342
|
+
- The **language chip** (e.g. `ENG`) opens the subtitle menu — a dropdown with two
|
|
343
|
+
tabs: **Language** (the subtitle list) and **Text Size**.
|
|
344
|
+
|
|
345
|
+
```tsx
|
|
346
|
+
<MegaVideoPlayerWeb
|
|
347
|
+
uri={url}
|
|
348
|
+
subtitleTracks={[
|
|
349
|
+
{ lang: "English", url: "https://cdn.example.com/subs.en.srt" },
|
|
350
|
+
{ lang: "Español", url: "https://cdn.example.com/subs.es.srt" },
|
|
351
|
+
{ lang: "Français", url: "https://mega.nz/file/xyz#key", format: "srt" },
|
|
352
|
+
]}
|
|
353
|
+
initialSubtitleLang="English"
|
|
354
|
+
autoPlay
|
|
355
|
+
/>
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
- The **Language tab lists every subtitle**; tapping one selects it, marks it with a
|
|
359
|
+
checkmark, loads it, and starts showing it (with a single `subtitleUrl` the list
|
|
360
|
+
shows that one track, selectable too).
|
|
361
|
+
- The **Text Size tab** drags a 10–34px slider for the overlay font.
|
|
362
|
+
- Requires `initialSubtitleLang`/`subtitleTracks`/`subtitleUrl` — see prop list below.
|
|
363
|
+
- All these controls are the exact `PlayerControls` used in the MyMovies player.
|
|
364
|
+
|
|
365
|
+
`subtitleUrl` can be:
|
|
366
|
+
|
|
367
|
+
- a **MEGA link** (`https://mega.nz/file/…`) — decrypted through the same pipeline
|
|
368
|
+
as the video, so no CORS/extension issues;
|
|
369
|
+
- a plain **http(s) URL** — the host must allow CORS;
|
|
370
|
+
- a **local file URI** (`file:`, `content:`, or `/`) — provide a file reader with
|
|
371
|
+
the `readLocalFile` option on `loadSubtitles` (see Subtitles section).
|
|
372
|
+
|
|
373
|
+
Format auto-detection order: `subtitleFormat` prop → URL extension → content sniffing
|
|
374
|
+
(`.srt`/`.ass`/`.ssa`/`.vtt`, plus ASS detected even from extension-less URLs).
|
|
375
|
+
|
|
376
|
+
### Multi-language tracks
|
|
377
|
+
|
|
378
|
+
```tsx
|
|
379
|
+
<MegaVideoPlayerWeb
|
|
380
|
+
uri={url}
|
|
381
|
+
subtitleTracks={[
|
|
382
|
+
{ lang: "English", url: "https://cdn.example.com/subs.srt" },
|
|
383
|
+
{ lang: "Français", url: "https://mega.nz/file/xyz#key", format: "srt" },
|
|
384
|
+
]}
|
|
385
|
+
initialSubtitleLang="English"
|
|
386
|
+
subtitleOffset={0} // seconds to shift cues (can be negative)
|
|
387
|
+
autoPlay
|
|
388
|
+
onEnd={() => goToNextEpisode()}
|
|
389
|
+
onProgress={({ currentTime, duration }) => {}}
|
|
390
|
+
onError={(message) => {}}
|
|
391
|
+
/>
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
### Full prop list
|
|
395
|
+
|
|
396
|
+
`MegaVideoPlayer` props:
|
|
397
|
+
|
|
398
|
+
- `uri` — playable URL (first part). Optional if only `sources` is given.
|
|
399
|
+
- `sources` — ordered part URLs; the player auto-advances when a part ends
|
|
400
|
+
(sequence advance, not concatenation).
|
|
401
|
+
- `title` — shown in the top control bar.
|
|
402
|
+
- `subtitleUrl` + `subtitleFormat` — single subtitle track (the common case).
|
|
403
|
+
- `subtitleLang` — language name shown in the controls for the single `subtitleUrl`
|
|
404
|
+
track (e.g. `"English"`). Defaults to `"SRT"`.
|
|
405
|
+
- `subtitleTracks` — `{ lang, url, format? }[]` for multi-language; the player
|
|
406
|
+
shows the subtitle list in the controls and selecting one switches tracks
|
|
407
|
+
(same behavior as MyMovies).
|
|
408
|
+
- `initialSubtitleLang` — preselected track language on load.
|
|
409
|
+
- `subtitleOffset` — seconds to shift cue timestamps (negative = earlier).
|
|
410
|
+
- `autoPlay` — auto-start (default `true`).
|
|
411
|
+
- `onEnd` — fired when the last part finishes.
|
|
412
|
+
- `onProgress({ currentTime, duration })`, `onError(message)`,
|
|
413
|
+
`onFullscreenChange(boolean)` — callbacks.
|
|
414
|
+
|
|
415
|
+
All subtitles render through `SubtitleDisplay` (dark pill, white text, centered,
|
|
416
|
+
resizable 10–34px via the controls' Text Size slider).
|
|
417
|
+
|
|
418
|
+
## Subtitles
|
|
419
|
+
|
|
420
|
+
```ts
|
|
421
|
+
import { loadSubtitles, parseSubtitles, detectSubtitleFormat } from "mega-stream-proxy/subtitles";
|
|
422
|
+
|
|
423
|
+
const cues = await loadSubtitles("https://mega.nz/file/xyz#key");
|
|
424
|
+
// MEGA links are decrypted through the same pipeline as video (no CORS issues).
|
|
425
|
+
// Plain http(s) hosts must allow CORS.
|
|
426
|
+
|
|
427
|
+
const cues = await loadSubtitles("/data/sub/1.srt", {
|
|
428
|
+
readLocalFile: (uri) => FileSystem.readAsStringAsync(uri, { encoding: FileSystem.EncodingType.UTF8 }),
|
|
429
|
+
});
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
- Supported formats: SRT, ASS/SSA, WebVTT. Extension-less URLs (MEGA links) are
|
|
433
|
+
sniffed by content.
|
|
434
|
+
- `parseSubtitles(text, format?)` is import-pure and tree-shakable — batch-process
|
|
435
|
+
files anywhere.
|
|
436
|
+
|
|
437
|
+
## Accounts
|
|
438
|
+
|
|
439
|
+
```ts
|
|
440
|
+
import { MegaAccountManager } from "mega-stream-proxy/accounts";
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
**Web** — persist in `localStorage` (with cross-tab live updates):
|
|
444
|
+
|
|
445
|
+
```ts
|
|
446
|
+
import { createLocalStorageStore } from "mega-stream-proxy/accounts";
|
|
447
|
+
const accounts = new MegaAccountManager(createLocalStorageStore());
|
|
448
|
+
|
|
449
|
+
await accounts.addAccount({ email: "a@b.c", password: "…", label: "Work" });
|
|
450
|
+
const list = await accounts.listAccounts(); // MegaAccount[]
|
|
451
|
+
const tree = await accounts.loadTree(list[0]); // MegaTreeNode[] (folders, videos, subs)
|
|
452
|
+
const link = await accounts.exportLink(list[0], tree.find(n => n.isVideo).handle);
|
|
453
|
+
await accounts.deleteAccount(list[0].id);
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
**React Native** — persist in AsyncStorage:
|
|
457
|
+
|
|
458
|
+
```ts
|
|
459
|
+
import { createAsyncStorageStore } from "mega-stream-proxy/accounts";
|
|
460
|
+
import AsyncStorage from "@react-native-async-storage/async-storage";
|
|
461
|
+
const accounts = new MegaAccountManager(createAsyncStorageStore(AsyncStorage));
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
**Bring your own backend** (Firestore, your server…):
|
|
465
|
+
|
|
466
|
+
```ts
|
|
467
|
+
import type { MegaAccountStore, MegaAccount } from "mega-stream-proxy/accounts";
|
|
468
|
+
const myStore: MegaAccountStore = {
|
|
469
|
+
list: () => db.collection("accounts").get().then(q => q.docs.map(d => d.data())),
|
|
470
|
+
save: (a: MegaAccount) => db.doc(a.id).set(a),
|
|
471
|
+
remove: (id) => db.doc(id).delete(),
|
|
472
|
+
subscribe: (cb) => unsubFirestore(cb),
|
|
473
|
+
};
|
|
474
|
+
const accounts = new MegaAccountManager(myStore);
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
Other helpers: `testAccount()` returns `{ name, folders, files }` (login check),
|
|
478
|
+
`subscribe(cb)` streams account changes, `closeSession(id)` / `closeAll()` free sessions,
|
|
479
|
+
`MegaAccount` — you can enrich stores (encrypt passwords, etc.) before saving.
|
|
480
|
+
|
|
481
|
+
## Export map
|
|
482
|
+
|
|
483
|
+
| Subpath | Contents |
|
|
484
|
+
| ------------------ | --------------------------------------------------------------- |
|
|
485
|
+
| `mega-stream-proxy` | core: `loadMegaFile`, `collectMegaFileStream`, `parseRange`, `rangeHeaders`, `SW_MARKER` |
|
|
486
|
+
| `mega-stream-proxy/native` | `ensureProxy`, `createStream`, `createStreams`, `releaseStream`, `stopProxy` (TCP, keyed sessions on a singleton server) |
|
|
487
|
+
| `mega-stream-proxy/web` | same API, Service-Worker backed (Blob fallback) |
|
|
488
|
+
| `mega-stream-proxy/sw` | `copySwAssets(outDir)`, `swAssetPaths()` (deploy-time node helper) |
|
|
489
|
+
| `mega-stream-proxy/player` | `MegaVideoPlayer`, `PlayerControls`, `SubtitleDisplay` |
|
|
490
|
+
| `mega-stream-proxy/subtitles`| `loadSubtitles`, `parseSubtitles`, `detectSubtitleFormat`, types |
|
|
491
|
+
| `mega-stream-proxy/accounts`| `MegaAccountManager`, `createLocalStorageStore`, `createAsyncStorageStore`, types |
|
|
492
|
+
|
|
493
|
+
## Android build
|
|
494
|
+
|
|
495
|
+
The package includes `android/build.gradle` (com.android.library, namespace
|
|
496
|
+
`com.megastreamproxy`) so RN autolinking resolves it without touching your Gradle
|
|
497
|
+
config. All native streaming is performed by `react-native-tcp-socket`, which is a
|
|
498
|
+
declared dependency and is auto-linked too. Build as usual:
|
|
499
|
+
|
|
500
|
+
```sh
|
|
501
|
+
npx expo run:android # Expo apps
|
|
502
|
+
./gradlew assembleDebug # bare RN apps
|
|
503
|
+
```
|
|
504
|
+
|
|
505
|
+
## Development
|
|
506
|
+
|
|
507
|
+
```sh
|
|
508
|
+
npm install # installs everything (incl. dev deps for typechecking the player)
|
|
509
|
+
npm run build # tsc -> dist/ + copies sw assets
|
|
510
|
+
npm run test # build + node:test suite
|
|
511
|
+
npm pack # -> mega-stream-proxy-0.1.0.tgz
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
Node 18+. TypeScript strict. ESM (`"type": "module"`).
|
|
515
|
+
|
|
516
|
+
## License
|
|
517
|
+
|
|
518
|
+
MIT — see [LICENSE](./LICENSE).
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// mega-stream-proxy is a JS-only library — this module exists so React Native
|
|
2
|
+
// autolinking picks the package up cleanly and no consumer edits their own
|
|
3
|
+
// build.gradle. All native work is delegated to react-native-tcp-socket, which
|
|
4
|
+
// is a regular dependency and therefore auto-linked by the app itself.
|
|
5
|
+
|
|
6
|
+
apply plugin: "com.android.library"
|
|
7
|
+
|
|
8
|
+
def safeExtGet(prop, fallback) {
|
|
9
|
+
rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
android {
|
|
13
|
+
namespace = "com.megastreamproxy"
|
|
14
|
+
|
|
15
|
+
compileSdkVersion safeExtGet("compileSdkVersion", 35)
|
|
16
|
+
buildToolsVersion = safeExtGet("buildToolsVersion", "35.0.0")
|
|
17
|
+
|
|
18
|
+
defaultConfig {
|
|
19
|
+
minSdkVersion safeExtGet("minSdkVersion", 24)
|
|
20
|
+
targetSdkVersion safeExtGet("targetSdkVersion", 35)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
lintOptions {
|
|
24
|
+
abortOnError false
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
compileOptions {
|
|
28
|
+
sourceCompatibility JavaVersion.VERSION_1_8
|
|
29
|
+
targetCompatibility JavaVersion.VERSION_1_8
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { MegaVideoPlayerAndroid, type MegaVideoPlayerAndroidProps, type MegaVideoPlayerAndroidHandle, } from "./playerAndroid/MegaVideoPlayerAndroid.js";
|
|
2
|
+
export { PlayerControlsAndroid, type PlayerControlsAndroidProps } from "./playerAndroid/PlayerControlsAndroid.js";
|
|
3
|
+
export { SubtitleDisplayAndroid, type SubCueShapeAndroid } from "./playerAndroid/SubtitleDisplayAndroid.js";
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { type SubtitleFormat, type SubtitleTrack } from "../../subtitles/index.js";
|
|
3
|
+
export type MegaVideoPlayerAndroidProps = {
|
|
4
|
+
/** A playable URL — a direct stream or the first part URL. */
|
|
5
|
+
uri?: string | null;
|
|
6
|
+
/** Ordered additional part URLs for multi-part episodes, played in sequence. */
|
|
7
|
+
sources?: string[];
|
|
8
|
+
title?: string;
|
|
9
|
+
/** Pass your subtitle file here — MEGA links, plain http(s) CORS hosts, or a known extension (.srt/.ass/.vtt). */
|
|
10
|
+
subtitleUrl?: string;
|
|
11
|
+
/** optional format hint (defaults to URL extension, then content sniffing) */
|
|
12
|
+
subtitleFormat?: SubtitleFormat;
|
|
13
|
+
/** language name shown in the player controls for the single `subtitleUrl` track
|
|
14
|
+
* (e.g. "English", "French"). Defaults to "SRT". */
|
|
15
|
+
subtitleLang?: string;
|
|
16
|
+
subtitleTracks?: SubtitleTrack[];
|
|
17
|
+
/** preselect a subtitle track by language on load */
|
|
18
|
+
initialSubtitleLang?: string | null;
|
|
19
|
+
/** shift the cue timestamps by an offset in seconds (can be negative) */
|
|
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
|
+
autoPlay?: boolean;
|
|
29
|
+
onEnd?: () => void;
|
|
30
|
+
onError?: (message: string) => void;
|
|
31
|
+
onProgress?: (info: {
|
|
32
|
+
currentTime: number;
|
|
33
|
+
duration: number;
|
|
34
|
+
}) => void;
|
|
35
|
+
onFullscreenChange?: (fullscreen: boolean) => void;
|
|
36
|
+
width?: number;
|
|
37
|
+
height?: number;
|
|
38
|
+
};
|
|
39
|
+
export type MegaVideoPlayerAndroidHandle = {
|
|
40
|
+
/** Loads a new playable URL into the player. */
|
|
41
|
+
add: (source: string, opts?: {
|
|
42
|
+
title?: string;
|
|
43
|
+
}) => Promise<void>;
|
|
44
|
+
/** Stops playback and clears the current source. */
|
|
45
|
+
stop: () => void;
|
|
46
|
+
};
|
|
47
|
+
export declare const MegaVideoPlayerAndroid: React.ForwardRefExoticComponent<MegaVideoPlayerAndroidProps & React.RefAttributes<MegaVideoPlayerAndroidHandle>>;
|