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,212 @@
1
+ // public/sw.js
2
+ // Web streaming proxy — the browser-side equivalent of the native
3
+ // services/megaProxy.ts TCP server. The page registers this service worker
4
+ // and tells it which MEGA parts to serve. The <video> element then loads
5
+ // same-origin URLs like /__mmstream__/<session>/part-0.mp4 and makes HTTP
6
+ // Range requests; this worker answers them one at a time by piping the
7
+ // truncated, decrypted megajs download stream straight into the response —
8
+ // exactly like the mobile proxy streams byte ranges from MEGA on demand.
9
+ //
10
+ // megajs is loaded via importScripts("megajs.browser.js") (the UMD build,
11
+ // copied from node_modules — self-contained, works without window/document).
12
+
13
+ importScripts("megajs.browser.js");
14
+
15
+ const STREAM_MARKER = "/__mmstream__/";
16
+
17
+ // Map<`${session}/${name}`, { url: string, promise?: Promise<{file,size}> }>
18
+ const parts = new Map();
19
+
20
+ /* ------------------------------------------------------------------ */
21
+ /* Setup */
22
+ /* ------------------------------------------------------------------ */
23
+
24
+ self.addEventListener("install", () => {
25
+ self.skipWaiting();
26
+ });
27
+
28
+ self.addEventListener("activate", (event) => {
29
+ event.waitUntil(self.clients.claim());
30
+ });
31
+
32
+ self.addEventListener("message", (event) => {
33
+ const data = (event && event.data) || {};
34
+ if (data.type === "parts" && data.session && Array.isArray(data.parts)) {
35
+ for (const p of data.parts) {
36
+ if (p && p.name && p.url) {
37
+ parts.set(`${data.session}/${p.name}`, { url: p.url });
38
+ }
39
+ }
40
+ } else if (data.type === "clear" && data.session) {
41
+ for (const key of parts.keys()) {
42
+ if (key.startsWith(`${data.session}/`)) parts.delete(key);
43
+ }
44
+ }
45
+ });
46
+
47
+ /* ------------------------------------------------------------------ */
48
+ /* Helpers */
49
+ /* ------------------------------------------------------------------ */
50
+
51
+ function toView(chunk) {
52
+ // Copy the bytes: megajs may reuse its Buffers after the "data" event.
53
+ const src = chunk instanceof Uint8Array ? chunk : new Uint8Array(chunk);
54
+ const out = new Uint8Array(src.byteLength);
55
+ out.set(src);
56
+ return out;
57
+ }
58
+
59
+ function textResponse(status, text, extraHeaders) {
60
+ const headers = Object.assign({ "Content-Type": "text/plain" }, extraHeaders);
61
+ return new Response(text, { status, headers });
62
+ }
63
+
64
+ async function loadPart(part) {
65
+ if (!part.promise) {
66
+ part.promise = (async () => {
67
+ const file = self.mega.File.fromURL(part.url);
68
+ await file.loadAttributes();
69
+ return { file, size: Number(file.size) || 0 };
70
+ })();
71
+ }
72
+ return part.promise;
73
+ }
74
+
75
+ /* ------------------------------------------------------------------ */
76
+ /* Streaming — mirrors native serveSingleFile (HTTP Range over MEGA) */
77
+ /* ------------------------------------------------------------------ */
78
+
79
+ async function handleStream(request, session, name) {
80
+ const key = `${session}/${name}`;
81
+ const part = parts.get(key);
82
+ if (!part) return textResponse(404, `Not Found: ${name}`);
83
+
84
+ let parsed;
85
+ try {
86
+ parsed = await loadPart(part);
87
+ } catch (e) {
88
+ console.warn("[mmstream] load failed:", name, e);
89
+ return textResponse(503, "MEGA load failed");
90
+ }
91
+ const { file, size } = parsed;
92
+ if (size <= 0) return textResponse(503, "Stream has no size");
93
+
94
+ // HTTP Range parsing — same rules as the native proxy.
95
+ let start = 0;
96
+ let end = size - 1;
97
+ let isPartial = false;
98
+
99
+ const rangeHeader = request.headers.get("range");
100
+ if (rangeHeader) {
101
+ const m = /^bytes=(\d*)-(\d*)$/i.exec(rangeHeader.trim());
102
+ if (!m || (m[1] === "" && m[2] === "")) {
103
+ return new Response(null, { status: 416 });
104
+ }
105
+ const rawStart = m[1] === "" ? -1 : Number(m[1]);
106
+ const rawEnd = m[2] === "" ? -1 : Number(m[2]);
107
+ if (Number.isNaN(rawStart) || Number.isNaN(rawEnd)) {
108
+ return new Response(null, { status: 416 });
109
+ }
110
+ if (rawStart === -1) {
111
+ // suffix range: last N bytes
112
+ start = Math.max(size - rawEnd, 0);
113
+ end = size - 1;
114
+ } else {
115
+ start = rawStart;
116
+ end = rawEnd === -1 || rawEnd >= size ? size - 1 : rawEnd;
117
+ if (start > end || start >= size) {
118
+ return new Response(null, { status: 416 });
119
+ }
120
+ }
121
+ isPartial = true;
122
+ }
123
+
124
+ const length = end - start + 1;
125
+ const headers = {
126
+ "Content-Type": "video/mp4",
127
+ "Accept-Ranges": "bytes",
128
+ "Content-Length": String(length),
129
+ "Cache-Control": "no-store",
130
+ };
131
+ if (isPartial) {
132
+ headers["Content-Range"] = `bytes ${start}-${end}/${size}`;
133
+ }
134
+
135
+ if (request.method === "HEAD") {
136
+ return new Response(null, { status: isPartial ? 206 : 200, headers });
137
+ }
138
+
139
+ console.log(`[mmstream] ${request.method} ${name} ${rangeHeader || "full"} -> ${start}-${end} of ${size}`);
140
+
141
+ // Stream the requested byte range from MEGA as it decrypts.
142
+ const stream = file.download({
143
+ start,
144
+ end,
145
+ maxConnections: 4, // same multi-connection path as the native proxy
146
+ });
147
+
148
+ let done = false;
149
+
150
+ const body = new ReadableStream({
151
+ start(controller) {
152
+ stream.on("data", (chunk) => {
153
+ if (done) return;
154
+ if (controller.desiredSize <= 0) {
155
+ try {
156
+ stream.pause();
157
+ } catch {}
158
+ }
159
+ try {
160
+ controller.enqueue(toView(chunk));
161
+ } catch (e) {
162
+ try {
163
+ stream.destroy();
164
+ } catch {}
165
+ }
166
+ });
167
+ stream.on("end", () => {
168
+ if (done) return;
169
+ done = true;
170
+ try {
171
+ controller.close();
172
+ } catch {}
173
+ });
174
+ stream.on("error", (e) => {
175
+ if (done) return;
176
+ done = true;
177
+ try {
178
+ controller.error(e);
179
+ } catch {}
180
+ });
181
+ },
182
+ pull() {
183
+ try {
184
+ stream.resume();
185
+ } catch {}
186
+ },
187
+ cancel() {
188
+ done = true;
189
+ try {
190
+ if (stream.destroy) stream.destroy();
191
+ } catch {}
192
+ },
193
+ });
194
+
195
+ return new Response(body, { status: isPartial ? 206 : 200, headers });
196
+ }
197
+
198
+ self.addEventListener("fetch", (event) => {
199
+ const url = new URL(event.request.url);
200
+ if (url.origin !== self.location.origin) return;
201
+ if (!url.pathname.startsWith(STREAM_MARKER)) return;
202
+
203
+ const rest = url.pathname.slice(STREAM_MARKER.length); // <session>/<name>
204
+ const slash = rest.indexOf("/");
205
+ if (slash < 0) {
206
+ event.respondWith(textResponse(404, "Not Found"));
207
+ return;
208
+ }
209
+ const session = rest.slice(0, slash);
210
+ const name = rest.slice(slash + 1);
211
+ event.respondWith(handleStream(event.request, session, name));
212
+ });
@@ -0,0 +1,15 @@
1
+ /** Minimal structural types so this package does not need megajs typings. */
2
+ /** A MEGA file as returned by megajs `File.fromURL(url)` (after loadAttributes). */
3
+ export interface MegaFileLike {
4
+ size?: number;
5
+ name?: string;
6
+ loadAttributes: () => Promise<unknown>;
7
+ download: (opts?: Record<string, unknown>) => StreamLike;
8
+ }
9
+ /** Event-emitter download stream returned by `MegaFileLike.download()`. */
10
+ export interface StreamLike {
11
+ on: (event: string, listener: (...args: any[]) => void) => StreamLike;
12
+ pause?: () => void;
13
+ resume?: () => void;
14
+ destroy?: () => void;
15
+ }
@@ -0,0 +1,2 @@
1
+ /** Minimal structural types so this package does not need megajs typings. */
2
+ export {};
@@ -0,0 +1,24 @@
1
+ import type { MegaFileLike } from "./types.js";
2
+ type PlayableUrl = string;
3
+ /** Idempotent: registers the Service Worker once per app lifetime (no-op after). */
4
+ export declare function ensureProxy(): Promise<void>;
5
+ /** Registers one MEGA link as its own keyed session; returns its playable URL. */
6
+ export declare function createStream(megaUrl: string): Promise<{
7
+ url: PlayableUrl;
8
+ sessionId: string;
9
+ }>;
10
+ /**
11
+ * Returns the ordered URLs the player should play in sequence, plus the session
12
+ * id to release later. On the Service Worker path every URL is streamed from
13
+ * MEGA on demand (start + HTTP Range) exactly like the mobile proxy; nothing is
14
+ * downloaded up front.
15
+ */
16
+ export declare function createStreams(megaUrls: string[]): Promise<{
17
+ urls: PlayableUrl[];
18
+ sessionId: string;
19
+ }>;
20
+ /** Tears down one session: clears its worker part map and revokes its Blob URLs. */
21
+ export declare function releaseStream(sessionId: string): void;
22
+ /** Tear-down for tests / HMR: clears every session and the worker cache. */
23
+ export declare function stopProxy(): void;
24
+ export type { MegaFileLike };
@@ -0,0 +1,231 @@
1
+ /**
2
+ * Web streaming proxy. Registers the bundled Service Worker (see ./sw) with
3
+ * the MEGA part list, then returns same-origin URLs like
4
+ * /__mmstream__/<session>/part-0.mp4. The <video> element performs HTTP Range
5
+ * requests against those URLs and the worker streams the truncated, decrypted
6
+ * megajs bytes on demand — playback starts in well under a second without
7
+ * downloading the file, exactly like the Android TCP proxy.
8
+ *
9
+ * The worker + SW session map is a singleton established once per app lifetime
10
+ * via `ensureProxy()`. Each `createStream(url)` / `createStreams(urls)` call
11
+ * registers its own keyed session and returns its session id; the sessions are
12
+ * independent, and `releaseStream(sessionId)` tears one down while the others
13
+ * keep streaming.
14
+ *
15
+ * Fallback: when a Service Worker cannot be used (no secure context), parts
16
+ * are fully downloaded + decrypted into Blob object URLs.
17
+ */
18
+ import { collectMegaFileStream } from "../stream.js";
19
+ import { SW_MARKER } from "../swDefine.js";
20
+ function errMsg(e) {
21
+ return e instanceof Error ? e.message : String(e);
22
+ }
23
+ function revokeUrls(urls) {
24
+ for (const url of urls) {
25
+ try {
26
+ URL.revokeObjectURL(url);
27
+ }
28
+ catch { }
29
+ }
30
+ }
31
+ const sessions = new Map();
32
+ let swCache = null;
33
+ let sessionCounter = 0;
34
+ /** Collision-resistant session key: time base + monotonic counter + random tail. */
35
+ function newSessionId() {
36
+ sessionCounter += 1;
37
+ const t = Date.now().toString(36);
38
+ const c = sessionCounter.toString(36);
39
+ const r = Math.random().toString(36).slice(2, 6);
40
+ return `${t}${c}${r}`;
41
+ }
42
+ /* ------------------------------------------------------------------ */
43
+ /* Service Worker */
44
+ /* ------------------------------------------------------------------ */
45
+ function swAvailable() {
46
+ if (typeof window === "undefined" || typeof navigator === "undefined") {
47
+ return false;
48
+ }
49
+ if (!("serviceWorker" in navigator))
50
+ return false;
51
+ const proto = window.location.protocol;
52
+ const host = window.location.hostname;
53
+ return proto === "https:" || host === "localhost" || host === "127.0.0.1";
54
+ }
55
+ function origin() {
56
+ return typeof window === "undefined" ? "" : window.location.origin;
57
+ }
58
+ /** Registers + activates the streaming worker once; returns whether it controls the page. */
59
+ function ensureSw() {
60
+ if (swCache)
61
+ return swCache;
62
+ swCache = (async () => {
63
+ if (!swAvailable() || !origin())
64
+ return false;
65
+ try {
66
+ await navigator.serviceWorker.register(`${origin()}/sw.js`, {
67
+ scope: `${origin()}/`,
68
+ });
69
+ await navigator.serviceWorker.ready;
70
+ if (navigator.serviceWorker.controller)
71
+ return true;
72
+ // The worker claims clients on activation; wait for control so the
73
+ // <video> fetches below are actually intercepted. If the page never
74
+ // becomes controlled, fall back to Blob URLs instead of a 404 stream.
75
+ return await new Promise((resolve) => {
76
+ const onControl = () => {
77
+ navigator.serviceWorker.removeEventListener("controllerchange", onControl);
78
+ resolve(true);
79
+ };
80
+ navigator.serviceWorker.addEventListener("controllerchange", onControl);
81
+ setTimeout(() => {
82
+ navigator.serviceWorker.removeEventListener("controllerchange", onControl);
83
+ resolve(false);
84
+ }, 3000);
85
+ });
86
+ }
87
+ catch (e) {
88
+ console.warn(`[megaStream] service worker unavailable: ${errMsg(e)}`);
89
+ return false;
90
+ }
91
+ })();
92
+ return swCache;
93
+ }
94
+ async function sendToSw(message) {
95
+ try {
96
+ if (typeof navigator === "undefined")
97
+ return;
98
+ const registration = await navigator.serviceWorker?.getRegistration();
99
+ if (registration?.active)
100
+ registration.active.postMessage(message);
101
+ }
102
+ catch (e) {
103
+ console.warn(`[megaStream] worker message failed: ${errMsg(e)}`);
104
+ }
105
+ }
106
+ async function startStreaming(sessionId, parts) {
107
+ await sendToSw({ type: "parts", session: sessionId, parts });
108
+ const out = parts.map((p) => `${origin()}${SW_MARKER}${sessionId}/${p.name}`);
109
+ console.log(`[megaStream] session ${sessionId}: streaming ${parts.length} parts (web, service worker) via ${out[0]}`);
110
+ return out;
111
+ }
112
+ /* ------------------------------------------------------------------ */
113
+ /* Blob fallback (no Service Worker allowed) */
114
+ /* ------------------------------------------------------------------ */
115
+ /** Download + decrypt a MEGA part into a video Blob object URL (tracked by session). */
116
+ async function megaToBlobUrl(url, blobUrls) {
117
+ const chunks = [];
118
+ await collectMegaFileStream(url.trim(), async (chunk) => {
119
+ chunks.push(new Uint8Array(chunk));
120
+ });
121
+ let total = 0;
122
+ for (const c of chunks)
123
+ total += c.length;
124
+ const merged = new Uint8Array(total);
125
+ let offset = 0;
126
+ for (const c of chunks) {
127
+ merged.set(c, offset);
128
+ offset += c.length;
129
+ }
130
+ const blob = new Blob([merged.buffer], { type: "video/mp4" });
131
+ const objectUrl = URL.createObjectURL(blob);
132
+ blobUrls.push(objectUrl);
133
+ return objectUrl;
134
+ }
135
+ /** Fully downloads every part into object URLs; resolves once part 1 is ready. */
136
+ async function startBlobFallback(megaUrls, blobUrls) {
137
+ const urls = (megaUrls ?? []).map((u) => u?.trim()).filter(Boolean);
138
+ const out = new Array(urls.length).fill(null);
139
+ urls.forEach((u, i) => {
140
+ megaToBlobUrl(u, blobUrls)
141
+ .then((blobUrl) => {
142
+ out[i] = blobUrl;
143
+ console.log(`[megaStream] part ${i + 1}/${urls.length} ready`);
144
+ })
145
+ .catch((e) => console.warn(`[megaStream] part ${i + 1}/${urls.length} failed: ${errMsg(e)}`));
146
+ });
147
+ const deadline = Date.now() + 30_000;
148
+ while (out[0] == null && Date.now() < deadline) {
149
+ await new Promise((r) => setTimeout(r, 100));
150
+ }
151
+ if (out[0] == null) {
152
+ throw new Error("Failed to stream the first part from MEGA.");
153
+ }
154
+ return out;
155
+ }
156
+ /* ------------------------------------------------------------------ */
157
+ /* Public contract (same signatures as the native proxy) */
158
+ /* ------------------------------------------------------------------ */
159
+ /** Idempotent: registers the Service Worker once per app lifetime (no-op after). */
160
+ export async function ensureProxy() {
161
+ await ensureSw();
162
+ }
163
+ /** Registers one MEGA link as its own keyed session; returns its playable URL. */
164
+ export async function createStream(megaUrl) {
165
+ if (!megaUrl?.trim())
166
+ throw new Error("No MEGA file to stream.");
167
+ const sessionId = newSessionId();
168
+ const blobUrls = [];
169
+ let url;
170
+ try {
171
+ if (await ensureSw()) {
172
+ [url] = await startStreaming(sessionId, [{ name: "stream.mp4", url: megaUrl.trim() }]);
173
+ }
174
+ else {
175
+ console.warn("[megaStream] no service worker — falling back to full Blob download");
176
+ url = await megaToBlobUrl(megaUrl, blobUrls);
177
+ console.log(`[megaStream] session ${sessionId}: serving 1 file via ${url.slice(0, 40)}…`);
178
+ }
179
+ }
180
+ catch (e) {
181
+ revokeUrls(blobUrls);
182
+ throw e;
183
+ }
184
+ sessions.set(sessionId, { blobUrls });
185
+ return { url, sessionId };
186
+ }
187
+ /**
188
+ * Returns the ordered URLs the player should play in sequence, plus the session
189
+ * id to release later. On the Service Worker path every URL is streamed from
190
+ * MEGA on demand (start + HTTP Range) exactly like the mobile proxy; nothing is
191
+ * downloaded up front.
192
+ */
193
+ export async function createStreams(megaUrls) {
194
+ const clean = (megaUrls ?? []).map((u) => u?.trim()).filter(Boolean);
195
+ if (!clean.length)
196
+ throw new Error("No video parts to stream.");
197
+ const sessionId = newSessionId();
198
+ const blobUrls = [];
199
+ let urls;
200
+ try {
201
+ if (await ensureSw()) {
202
+ urls = await startStreaming(sessionId, clean.map((u, i) => ({ name: `part-${i}.mp4`, url: u })));
203
+ }
204
+ else {
205
+ console.warn("[megaStream] no service worker — falling back to full Blob download");
206
+ urls = await startBlobFallback(clean, blobUrls);
207
+ }
208
+ }
209
+ catch (e) {
210
+ revokeUrls(blobUrls);
211
+ throw e;
212
+ }
213
+ sessions.set(sessionId, { blobUrls });
214
+ return { urls, sessionId };
215
+ }
216
+ /** Tears down one session: clears its worker part map and revokes its Blob URLs. */
217
+ export function releaseStream(sessionId) {
218
+ const session = sessions.get(sessionId);
219
+ if (!session)
220
+ return;
221
+ sessions.delete(sessionId);
222
+ void sendToSw({ type: "clear", session: sessionId });
223
+ revokeUrls(session.blobUrls);
224
+ console.log(`[megaStream] released session ${sessionId}`);
225
+ }
226
+ /** Tear-down for tests / HMR: clears every session and the worker cache. */
227
+ export function stopProxy() {
228
+ for (const id of Array.from(sessions.keys()))
229
+ releaseStream(id);
230
+ swCache = null;
231
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Shared HTTP Range parsing used by BOTH the native TCP proxy and the web
3
+ * Service Worker, so every platform answers `Range` headers identically.
4
+ */
5
+ export type RangeResult = {
6
+ satisfiable: true;
7
+ start: number;
8
+ end: number;
9
+ length: number;
10
+ partial: boolean;
11
+ } | {
12
+ satisfiable: false;
13
+ };
14
+ /**
15
+ * Parses an HTTP `Range` request header (`bytes=...`) against a known size.
16
+ *
17
+ * - `bytes=start-end` explicit window
18
+ * - `bytes=start-` open-ended
19
+ * - `bytes=-N` suffix (last N bytes)
20
+ * - no header / `null` full body (200)
21
+ * - invalid / out of range → `{ satisfiable: false }` (answer 416)
22
+ */
23
+ export declare function parseRange(rangeHeader: string | null | undefined, size: number): RangeResult;
24
+ /** Breaks a range into the MEGA byte offsets consumeable by file.download({start,end}). */
25
+ export declare function toMegaRange(r: RangeResult): {
26
+ start: number;
27
+ end: number;
28
+ } | null;
29
+ /** Response headers for a range-capable stream of one file. */
30
+ export declare function rangeHeaders(r: RangeResult, size: number, contentType?: string): Record<string, string>;
package/dist/ranges.js ADDED
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Shared HTTP Range parsing used by BOTH the native TCP proxy and the web
3
+ * Service Worker, so every platform answers `Range` headers identically.
4
+ */
5
+ /**
6
+ * Parses an HTTP `Range` request header (`bytes=...`) against a known size.
7
+ *
8
+ * - `bytes=start-end` explicit window
9
+ * - `bytes=start-` open-ended
10
+ * - `bytes=-N` suffix (last N bytes)
11
+ * - no header / `null` full body (200)
12
+ * - invalid / out of range → `{ satisfiable: false }` (answer 416)
13
+ */
14
+ export function parseRange(rangeHeader, size) {
15
+ if (!rangeHeader) {
16
+ return { satisfiable: true, start: 0, end: size - 1, length: size, partial: false };
17
+ }
18
+ if (size < 0)
19
+ return { satisfiable: false };
20
+ const m = /^bytes=(\d*)-(\d*)$/i.exec(rangeHeader.trim());
21
+ if (!m || (m[1] === "" && m[2] === ""))
22
+ return { satisfiable: false };
23
+ const rawStart = m[1] === "" ? -1 : Number(m[1]);
24
+ const rawEnd = m[2] === "" ? -1 : Number(m[2]);
25
+ if (Number.isNaN(rawStart) || Number.isNaN(rawEnd) || size === 0) {
26
+ return { satisfiable: false };
27
+ }
28
+ let start;
29
+ let end;
30
+ if (rawStart === -1) {
31
+ // suffix range: last N bytes
32
+ start = Math.max(size - rawEnd, 0);
33
+ end = size - 1;
34
+ }
35
+ else {
36
+ start = rawStart;
37
+ end = rawEnd === -1 || rawEnd >= size ? size - 1 : rawEnd;
38
+ if (start > end || start >= size)
39
+ return { satisfiable: false };
40
+ }
41
+ // Any explicit Range header (even bytes=0-) is answered with a 206,
42
+ // matching what range-capable servers (nginx, lighttpd) do.
43
+ return { satisfiable: true, start, end, length: end - start + 1, partial: true };
44
+ }
45
+ /** Breaks a range into the MEGA byte offsets consumeable by file.download({start,end}). */
46
+ export function toMegaRange(r) {
47
+ if (!r.satisfiable)
48
+ return null;
49
+ return { start: r.start, end: r.end };
50
+ }
51
+ /** Response headers for a range-capable stream of one file. */
52
+ export function rangeHeaders(r, size, contentType = "video/mp4") {
53
+ const headers = {
54
+ "Content-Type": contentType,
55
+ "Accept-Ranges": "bytes",
56
+ "Content-Length": String(r.satisfiable ? r.length : 0),
57
+ "Cache-Control": "no-store",
58
+ };
59
+ if (r.satisfiable && r.partial) {
60
+ headers["Content-Range"] = `bytes ${r.start}-${r.end}/${size}`;
61
+ }
62
+ return headers;
63
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Downloads + decrypts a MEGA link, invoking onChunk for each ~1MB chunk as it
3
+ * arrives (backpressure-aware). Used by the web proxy's Blob fallback and by
4
+ * consumers that want the raw bytes. No files are written by this function.
5
+ */
6
+ export declare function collectMegaFileStream(url: string, onChunk: (chunk: Uint8Array) => Promise<void>): Promise<{
7
+ name: string;
8
+ size: number;
9
+ }>;
package/dist/stream.js ADDED
@@ -0,0 +1,100 @@
1
+ import { withTimeout } from "./megaFile.js";
2
+ const ATTRS_TIMEOUT_MS = 30_000;
3
+ const STALL_TIMEOUT_MS = 60_000;
4
+ const CHUNK_SIZE = 1024 * 1024; // 1MB flush target
5
+ /**
6
+ * Downloads + decrypts a MEGA link, invoking onChunk for each ~1MB chunk as it
7
+ * arrives (backpressure-aware). Used by the web proxy's Blob fallback and by
8
+ * consumers that want the raw bytes. No files are written by this function.
9
+ */
10
+ export async function collectMegaFileStream(url, onChunk) {
11
+ const file = await loadFileFromUrl(url);
12
+ await withTimeout(file.loadAttributes(), ATTRS_TIMEOUT_MS, "MEGA did not respond. Check your connection and try again.");
13
+ const name = file.name || "mega-download";
14
+ const size = Number(file.size ?? 0);
15
+ return new Promise((resolve, reject) => {
16
+ let totalBytes = 0;
17
+ let lastDataAt = Date.now();
18
+ let paused = false;
19
+ let buffer = [];
20
+ let bufferSize = 0;
21
+ let ended = false;
22
+ let stream;
23
+ const fail = (err) => {
24
+ clearInterval(watchdog);
25
+ try {
26
+ stream?.destroy?.();
27
+ }
28
+ catch { }
29
+ reject(err);
30
+ };
31
+ const flush = async () => {
32
+ if (buffer.length === 0)
33
+ return;
34
+ paused = true;
35
+ let total = 0;
36
+ for (const c of buffer)
37
+ total += c.length;
38
+ const combined = new Uint8Array(total);
39
+ let offset = 0;
40
+ for (const c of buffer) {
41
+ combined.set(c, offset);
42
+ offset += c.length;
43
+ }
44
+ buffer = [];
45
+ bufferSize = 0;
46
+ try {
47
+ await onChunk(combined);
48
+ paused = false;
49
+ if (!ended)
50
+ stream?.resume?.();
51
+ }
52
+ catch (error) {
53
+ fail(error instanceof Error ? error : new Error(String(error)));
54
+ }
55
+ };
56
+ const watchdog = setInterval(() => {
57
+ const elapsed = Date.now() - lastDataAt;
58
+ if (elapsed > STALL_TIMEOUT_MS && !paused) {
59
+ fail(new Error("Download stalled (no data received)."));
60
+ }
61
+ }, 5000);
62
+ try {
63
+ stream = file.download({});
64
+ }
65
+ catch (e) {
66
+ clearInterval(watchdog);
67
+ reject(e instanceof Error ? e : new Error(String(e)));
68
+ return;
69
+ }
70
+ stream.on("data", (chunk) => {
71
+ lastDataAt = Date.now();
72
+ totalBytes += chunk.length;
73
+ const copy = new Uint8Array(chunk.byteLength);
74
+ copy.set(chunk instanceof Uint8Array ? chunk : new Uint8Array(chunk));
75
+ buffer.push(copy);
76
+ bufferSize += copy.length;
77
+ if (bufferSize >= CHUNK_SIZE)
78
+ void flush();
79
+ });
80
+ stream.on("error", (e) => {
81
+ fail(e instanceof Error ? e : new Error(String(e)));
82
+ });
83
+ stream.on("end", async () => {
84
+ ended = true;
85
+ clearInterval(watchdog);
86
+ await flush();
87
+ resolve({ name, size: totalBytes || size });
88
+ });
89
+ stream.resume?.();
90
+ });
91
+ }
92
+ async function loadFileFromUrl(url) {
93
+ try {
94
+ const { File } = (await import("megajs"));
95
+ return File.fromURL(url.trim());
96
+ }
97
+ catch {
98
+ throw new Error("Invalid MEGA link.");
99
+ }
100
+ }