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,396 @@
1
+ /**
2
+ * Android / iOS streaming proxy. Binds a local HTTP-ish TCP server on
3
+ * 127.0.0.1 so the OS video player can request DECRYPTED byte ranges (HTTP
4
+ * Range) without writing files to device storage. Each connection is answered
5
+ * by piping the truncated, decrypted megajs download stream back to the socket.
6
+ *
7
+ * Single-file: `createStream(url)` registers one route -> one playable URL.
8
+ * - stitched: `createStreams(urls)` -> `/part-N.mp4` URLs, played
9
+ * in sequence by the app's player.
10
+ *
11
+ * All requests are routed by a keyed session id (`/<sessionId>/<name>`). The
12
+ * TCP server is a singleton bound once per app lifetime via `ensureProxy()`;
13
+ * sessions come and go independently, and `releaseStream(sessionId)` tears a
14
+ * single route down without touching the others.
15
+ *
16
+ * Multi-part files are NOT byte-concatenated: each part has its own moov with
17
+ * part-relative sample offsets, so playback advances part by part instead.
18
+ *
19
+ * Platform: React Native only. Requires the `react-native-tcp-socket` peer.
20
+ */
21
+ import TcpSocket from "react-native-tcp-socket";
22
+ import { Buffer } from "buffer";
23
+ import { loadMegaFile } from "../megaFile.js";
24
+ import { parseRange, rangeHeaders, type RangeResult } from "../ranges.js";
25
+ import type { MegaFileLike } from "./types.js";
26
+
27
+ /** Structural types decoupled from react-native-tcp-socket's odd type surface. */
28
+ type TcpServerLike = {
29
+ on: (event: string, cb: (...args: any[]) => void) => TcpServerLike;
30
+ removeListener: (event: string, cb: (...args: any[]) => void) => TcpServerLike;
31
+ listen: (opts: { port: number; host?: string }, cb?: () => void) => void;
32
+ address: () => { port: number; address?: string; family?: string } | string | null;
33
+ close: (cb?: () => void) => void;
34
+ };
35
+
36
+ type TcpSocketLike = {
37
+ on: (event: string, cb: (...args: any[]) => void) => TcpSocketLike;
38
+ write: (data: string | Uint8Array) => boolean;
39
+ end: () => void;
40
+ destroy: () => void;
41
+ };
42
+
43
+ type TcpModule = {
44
+ createServer: (
45
+ options: { noDelay?: boolean },
46
+ cb?: (socket: unknown) => void,
47
+ ) => TcpServerLike;
48
+ };
49
+
50
+ const tcp = TcpSocket as unknown as TcpModule;
51
+ const createServer = tcp.createServer;
52
+
53
+ type ServePart = { name: string; file: MegaFileLike; size: number };
54
+
55
+ type ParsedRequest = {
56
+ method: string;
57
+ target: string;
58
+ headers: Record<string, string>;
59
+ };
60
+
61
+ /** One registered route: an ordered set of playable parts (single file = one part). */
62
+ type Session = {
63
+ parts: ServePart[];
64
+ };
65
+
66
+ let server: TcpServerLike | null = null;
67
+ let proxyBase = "";
68
+ let proxyReady: Promise<void> | null = null;
69
+ const sessions = new Map<string, Session>();
70
+ let sessionCounter = 0;
71
+
72
+ /** Collision-resistant session key: time base + monotonic counter + random tail. */
73
+ function newSessionId(): string {
74
+ sessionCounter += 1;
75
+ const t = Date.now().toString(36);
76
+ const c = sessionCounter.toString(36);
77
+ const r = Math.random().toString(36).slice(2, 6);
78
+ return `${t}${c}${r}`;
79
+ }
80
+
81
+ /** Coerce whatever the socket hands us (Buffer, Uint8Array, ArrayBuffer, string, array). */
82
+ function toBytes(data: unknown): Uint8Array {
83
+ if (typeof data === "string") {
84
+ const out = new Uint8Array(data.length);
85
+ for (let i = 0; i < data.length; i++) out[i] = data.charCodeAt(i) & 0xff;
86
+ return out;
87
+ }
88
+ if (data instanceof ArrayBuffer) return new Uint8Array(data);
89
+ if (ArrayBuffer.isView(data)) {
90
+ return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
91
+ }
92
+ if (Array.isArray(data)) return Uint8Array.from(data);
93
+ return new Uint8Array(0);
94
+ }
95
+
96
+ /** Latin-1 decode: polyfill-proof byte -> char conversion for header parsing. */
97
+ function bytesToText(bytes: Uint8Array): string {
98
+ let out = "";
99
+ for (let i = 0; i < bytes.length; i++) out += String.fromCharCode(bytes[i]);
100
+ return out;
101
+ }
102
+
103
+ function findDoubleCrlf(bytes: Uint8Array): number {
104
+ for (let i = 0; i < bytes.length - 3; i++) {
105
+ if (
106
+ bytes[i] === 13 &&
107
+ bytes[i + 1] === 10 &&
108
+ bytes[i + 2] === 13 &&
109
+ bytes[i + 3] === 10
110
+ ) {
111
+ return i;
112
+ }
113
+ }
114
+ return -1;
115
+ }
116
+
117
+ function parseHttpHead(head: string): ParsedRequest {
118
+ const lines = head.split("\r\n");
119
+ const parts = (lines[0] ?? "").split(" ");
120
+ const headers: Record<string, string> = {};
121
+ for (let i = 1; i < lines.length; i++) {
122
+ const line = lines[i];
123
+ const idx = line.indexOf(":");
124
+ if (idx > 0) {
125
+ headers[line.slice(0, idx).trim().toLowerCase()] = line.slice(idx + 1).trim();
126
+ }
127
+ }
128
+ return { method: parts[0] ?? "", target: parts[1] ?? "", headers };
129
+ }
130
+
131
+ function serveError(socket: TcpSocketLike, status: number, reason: string): void {
132
+ const body = `${status} ${reason}`;
133
+ const head = [
134
+ `HTTP/1.1 ${status} ${reason}`,
135
+ "Content-Type: text/plain",
136
+ `Content-Length: ${body.length}`,
137
+ "Cache-Control: no-store",
138
+ "Connection: close",
139
+ "",
140
+ "",
141
+ ].join("\r\n");
142
+ socket.write(head + body);
143
+ socket.end();
144
+ }
145
+
146
+ function handleConnection(socket: TcpSocketLike): void {
147
+ const buf: number[] = [];
148
+ let handled = false;
149
+
150
+ socket.on("data", (data: unknown) => {
151
+ if (handled) return;
152
+ const bytes = toBytes(data);
153
+ for (let i = 0; i < bytes.length; i++) buf.push(bytes[i]);
154
+
155
+ const headEnd = findDoubleCrlf(Uint8Array.from(buf));
156
+ if (headEnd === -1) {
157
+ if (buf.length > 64 * 1024) {
158
+ handled = true;
159
+ serveError(socket, 400, "Bad Request");
160
+ }
161
+ return;
162
+ }
163
+
164
+ handled = true;
165
+ const head = bytesToText(Uint8Array.from(buf.slice(0, headEnd)));
166
+ handleRequest(socket, parseHttpHead(head));
167
+ });
168
+
169
+ socket.on("error", () => {});
170
+ socket.on("close", () => {});
171
+ }
172
+
173
+ /** Serves one file (a MEGA link or one stitched part) over a socket, honoring Range. */
174
+ function serveSingleFile(socket: TcpSocketLike, req: ParsedRequest, file: MegaFileLike, size: number): void {
175
+ if (size <= 0) return serveError(socket, 503, "Stream has no size");
176
+
177
+ const range: RangeResult = parseRange(req.headers.range, size);
178
+ if (!range.satisfiable) return serveError(socket, 416, "Range Not Satisfiable");
179
+
180
+ const headers = rangeHeaders(range, size, "video/mp4");
181
+ const statusLine = range.partial ? "HTTP/1.1 206 Partial Content" : "HTTP/1.1 200 OK";
182
+ const head = [
183
+ statusLine,
184
+ ...Object.entries(headers).map(([k, v]) => `${k}: ${v}`),
185
+ "Connection: close",
186
+ "",
187
+ "",
188
+ ].join("\r\n");
189
+
190
+ if (req.method === "HEAD") {
191
+ socket.write(head);
192
+ socket.end();
193
+ return;
194
+ }
195
+
196
+ console.log(
197
+ `[megaStream] ${req.method} ${req.target} ${req.headers.range ?? "full"} -> ${range.start}-${range.end} of ${size}`,
198
+ );
199
+
200
+ const stream = file.download({
201
+ start: range.start,
202
+ end: range.end,
203
+ // Do NOT use maxConnections: 1 — that path pipes a streaming body which
204
+ // React Native's fetch layer cannot provide. The default multi-connection
205
+ // path reads chunks via arrayBuffer(), the proven working path.
206
+ maxConnections: 4,
207
+ });
208
+
209
+ let headSent = false;
210
+ let socketClosed = false;
211
+
212
+ const closeSocket = () => {
213
+ if (socketClosed) return;
214
+ socketClosed = true;
215
+ try {
216
+ socket.end();
217
+ } catch {}
218
+ };
219
+
220
+ const teardown = () => {
221
+ if (socketClosed) return;
222
+ socketClosed = true;
223
+ try {
224
+ stream.destroy?.();
225
+ } catch {}
226
+ try {
227
+ socket.destroy();
228
+ } catch {}
229
+ };
230
+
231
+ stream.on("data", (chunk: Uint8Array) => {
232
+ if (socketClosed) return;
233
+ if (!headSent) {
234
+ headSent = true;
235
+ try {
236
+ socket.write(head);
237
+ } catch {
238
+ teardown();
239
+ return;
240
+ }
241
+ }
242
+ try {
243
+ const bytes =
244
+ chunk instanceof Uint8Array
245
+ ? chunk
246
+ : new Uint8Array((chunk as ArrayBufferLike)?.byteLength ?? 0);
247
+ if (!socket.write(Buffer.isBuffer(chunk) ? chunk : bytes)) {
248
+ stream.pause?.();
249
+ }
250
+ } catch {
251
+ teardown();
252
+ }
253
+ });
254
+
255
+ socket.on("drain", () => {
256
+ if (!socketClosed) stream.resume?.();
257
+ });
258
+ stream.on("end", () => closeSocket());
259
+ stream.on("error", (err: Error) => {
260
+ console.warn("[megaStream] stream error:", err?.message ?? err);
261
+ teardown();
262
+ });
263
+ socket.on("close", () => teardown());
264
+ socket.on("error", () => teardown());
265
+ }
266
+
267
+ function handleRequest(socket: TcpSocketLike, req: ParsedRequest): void {
268
+ console.log(
269
+ `[megaStream] <- ${req.method || "(none)"} ${req.target || "(none)"} range=${req.headers.range ?? "none"}`,
270
+ );
271
+ if (req.method && req.method !== "GET" && req.method !== "HEAD") {
272
+ console.warn(`[megaStream] unexpected method ${req.method} — serving as GET`);
273
+ }
274
+
275
+ const [sessionId, name] = req.target.replace(/^\//, "").split(/\?|#/)[0].split("/", 2);
276
+ if (!sessionId) return serveError(socket, 404, "Not Found");
277
+
278
+ const session = sessions.get(sessionId);
279
+ if (!session) return serveError(socket, 404, `No stream session ${sessionId}`);
280
+
281
+ if (name) {
282
+ const part = session.parts.find((p) => p.name === name);
283
+ if (!part) return serveError(socket, 404, `Not Found: ${name}`);
284
+ return serveSingleFile(socket, req, part.file, part.size);
285
+ }
286
+ return serveError(socket, 404, "Not Found");
287
+ }
288
+
289
+ async function bindProxyServer(): Promise<TcpServerLike> {
290
+ const srv = createServer({ noDelay: true }, (socket: unknown) =>
291
+ handleConnection(socket as TcpSocketLike),
292
+ );
293
+ await new Promise<void>((resolve, reject) => {
294
+ const onErr = (e: Error) => {
295
+ srv.removeListener("error", onErr);
296
+ reject(e);
297
+ };
298
+ srv.on("error", onErr);
299
+ srv.listen({ port: 0, host: "127.0.0.1" }, () => {
300
+ srv.removeListener("error", onErr);
301
+ resolve();
302
+ });
303
+ });
304
+ return srv;
305
+ }
306
+
307
+ /**
308
+ * Idempotent: binds the singleton local proxy server once per app lifetime.
309
+ * Safe to call concurrently and repeatedly — subsequent calls are no-ops until
310
+ * `stopProxy()` is called. Does not load any MEGA file.
311
+ */
312
+ export async function ensureProxy(): Promise<void> {
313
+ if (server) return;
314
+ if (proxyReady) return proxyReady;
315
+ proxyReady = (async () => {
316
+ const srv = await bindProxyServer();
317
+ const addr = srv.address();
318
+ const port = typeof addr === "object" && addr ? addr.port : 0;
319
+ if (!port) throw new Error("Failed to bind local stream server");
320
+ server = srv;
321
+ proxyBase = `http://127.0.0.1:${port}`;
322
+ console.log(`[megaStream] proxy listening on ${proxyBase}`);
323
+ })().catch((e) => {
324
+ proxyReady = null; // allow a retry after a failed bind
325
+ throw e;
326
+ });
327
+ return proxyReady;
328
+ }
329
+
330
+ /** Registers one MEGA link as its own keyed session; returns its local URL. */
331
+ export async function createStream(megaUrl: string): Promise<{ url: string; sessionId: string }> {
332
+ if (!megaUrl?.trim()) throw new Error("No MEGA file to stream.");
333
+ await ensureProxy();
334
+
335
+ const file = await loadMegaFile(megaUrl);
336
+ if (!file.size) throw new Error("MEGA file has no size");
337
+
338
+ const sessionId = newSessionId();
339
+ const name = file.name || "stream.mp4";
340
+ sessions.set(sessionId, { parts: [{ name, file, size: file.size }] });
341
+
342
+ const url = `${proxyBase}/${sessionId}/${name}`;
343
+ console.log(`[megaStream] session ${sessionId}: serving '${name}' via ${url}`);
344
+ return { url, sessionId };
345
+ }
346
+
347
+ /**
348
+ * Loads all MEGA parts and serves each at its own `/part-N.mp4` URL inside one
349
+ * session. Returns the ordered local URLs the player should play in sequence,
350
+ * plus the session id to release later. Nothing is written to disk; every byte
351
+ * range is streamed from MEGA as the player reads it.
352
+ */
353
+ export async function createStreams(
354
+ megaUrls: string[],
355
+ ): Promise<{ urls: string[]; sessionId: string }> {
356
+ const clean = (megaUrls ?? []).map((u) => u?.trim()).filter(Boolean);
357
+ if (!clean.length) throw new Error("No video parts to stream.");
358
+ await ensureProxy();
359
+
360
+ const parts: ServePart[] = [];
361
+ for (let i = 0; i < clean.length; i++) {
362
+ const file = await loadMegaFile(clean[i]);
363
+ if (!file.size) throw new Error("MEGA part has no size.");
364
+ parts.push({ name: `part-${i}.mp4`, file, size: file.size });
365
+ }
366
+ if (!parts.length) throw new Error("No streamable parts found.");
367
+
368
+ const sessionId = newSessionId();
369
+ sessions.set(sessionId, { parts });
370
+
371
+ const urls = parts.map((p) => `${proxyBase}/${sessionId}/${p.name}`);
372
+ console.log(
373
+ `[megaStream] session ${sessionId}: streaming ${parts.length} parts (${parts.map((p) => p.size).join(",")}) via ${urls[0]}`,
374
+ );
375
+ return { urls, sessionId };
376
+ }
377
+
378
+ /** Removes one session; other sessions and the singleton server keep running. */
379
+ export function releaseStream(sessionId: string): void {
380
+ if (!sessionId || !sessions.has(sessionId)) return;
381
+ sessions.delete(sessionId);
382
+ console.log(`[megaStream] released session ${sessionId}`);
383
+ }
384
+
385
+ /** Tear-down for tests / HMR: unbinds the server and clears every session. */
386
+ export function stopProxy(): void {
387
+ sessions.clear();
388
+ if (server) {
389
+ try {
390
+ server.close();
391
+ } catch {}
392
+ server = null;
393
+ }
394
+ proxyBase = "";
395
+ proxyReady = null;
396
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Access to the bundled Service Worker assets (sw.js + megajs.browser.js).
3
+ * Consumers (web / Expo / static hosts) copy these into their `public/` (or
4
+ * equivalent) so the client from `mega-stream-proxy/web` can register them.
5
+ */
6
+ import { copyFileSync, mkdirSync } from "node:fs";
7
+ import { dirname, join } from "node:path";
8
+ import { fileURLToPath } from "node:url";
9
+ import { SW_MARKER } from "../../swDefine.js";
10
+
11
+ export { SW_MARKER };
12
+
13
+ /** Paths of the bundled files inside this package (dist/proxy/sw). */
14
+ export function swAssetPaths(): { sw: string; megajs: string; marker: string } {
15
+ const dir = dirname(fileURLToPath(import.meta.url));
16
+ return { sw: join(dir, "sw.js"), megajs: join(dir, "megajs.browser.js"), marker: SW_MARKER };
17
+ }
18
+
19
+ export type CopiedSwAssets = { sw: string; megajs: string; marker: string };
20
+
21
+ /**
22
+ * Copies `sw.js` and `megajs.browser.js` into `outDir` (e.g. your `public/`
23
+ * or `web/` folder). Both files must be served from the same directory as the
24
+ * worker script so the SW can `importScripts("./megajs.browser.js")`.
25
+ *
26
+ * Returns the destination paths + the URL marker the client will use.
27
+ */
28
+ export function copySwAssets(outDir: string): CopiedSwAssets {
29
+ const src = swAssetPaths();
30
+ mkdirSync(outDir, { recursive: true });
31
+ const sw = join(outDir, "sw.js");
32
+ const megajs = join(outDir, "megajs.browser.js");
33
+ copyFileSync(src.sw, sw);
34
+ copyFileSync(src.megajs, megajs);
35
+ console.log(`[megaStream] copied sw.js + megajs.browser.js -> ${outDir}`);
36
+ return { sw, megajs, marker: SW_MARKER };
37
+ }