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,149 @@
1
+ const VIDEO_EXT = /\.(mp4|mkv|avi|mov|webm|m4v|ts|flv|wmv|mpg|mpeg|3gp)$/i;
2
+ const SUBTITLE_EXT = /\.(srt|ass|ssa|vtt)$/i;
3
+ function nextId() {
4
+ try {
5
+ return globalThis.crypto?.randomUUID?.() ?? fallbackId();
6
+ }
7
+ catch {
8
+ return fallbackId();
9
+ }
10
+ }
11
+ function fallbackId() {
12
+ return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
13
+ }
14
+ /**
15
+ * Owns the MEGA accounts list (via a pluggable store) plus the live megajs
16
+ * `Storage` sessions used while browsing a saved account's cloud drive.
17
+ */
18
+ export class MegaAccountManager {
19
+ store;
20
+ sessions = new Map();
21
+ constructor(store) {
22
+ this.store = store;
23
+ }
24
+ /* ------------------------- account CRUD ------------------------- */
25
+ async listAccounts() {
26
+ return this.store.list();
27
+ }
28
+ subscribe(cb) {
29
+ return this.store.subscribe?.(cb) ?? (() => { });
30
+ }
31
+ async addAccount(input) {
32
+ const account = {
33
+ id: nextId(),
34
+ email: input.email.trim(),
35
+ password: input.password,
36
+ label: (input.label ?? "").trim(),
37
+ createdAt: Date.now(),
38
+ };
39
+ await this.store.save(account);
40
+ return account;
41
+ }
42
+ async deleteAccount(id) {
43
+ this.closeSession(id);
44
+ await this.store.remove(id);
45
+ }
46
+ /* ------------------------- sessions ------------------------- */
47
+ async openStorage(account) {
48
+ const existing = this.sessions.get(account.id);
49
+ if (existing)
50
+ return existing;
51
+ const p = (async () => {
52
+ const { Storage } = (await import("megajs"));
53
+ const storage = new Storage({
54
+ email: account.email.trim(),
55
+ password: account.password,
56
+ userAgent: "mega-stream-proxy",
57
+ });
58
+ await storage.ready;
59
+ return storage;
60
+ })();
61
+ this.sessions.set(account.id, p);
62
+ p.catch(() => this.sessions.delete(account.id));
63
+ return p;
64
+ }
65
+ closeSession(accountId) {
66
+ const p = this.sessions.get(accountId);
67
+ this.sessions.delete(accountId);
68
+ p?.then((s) => {
69
+ try {
70
+ s.close();
71
+ }
72
+ catch { }
73
+ });
74
+ }
75
+ closeAll() {
76
+ for (const id of [...this.sessions.keys()])
77
+ this.closeSession(id);
78
+ }
79
+ /** login only — verifies credentials and counts the cloud drive. */
80
+ async testAccount(account) {
81
+ const { Storage } = (await import("megajs"));
82
+ const storage = new Storage({
83
+ email: account.email.trim(),
84
+ password: account.password,
85
+ userAgent: "mega-stream-proxy",
86
+ });
87
+ await storage.ready;
88
+ let folders = 0;
89
+ let files = 0;
90
+ for (const node of Object.values(storage.files ?? {})) {
91
+ if (node.directory)
92
+ folders += 1;
93
+ else
94
+ files += 1;
95
+ }
96
+ const result = { name: storage.name ?? account.email, folders, files };
97
+ try {
98
+ storage.close();
99
+ }
100
+ catch { }
101
+ return result;
102
+ }
103
+ /** Full Cloud-Drive tree of a saved account, root first. */
104
+ async loadTree(account) {
105
+ const storage = await this.openStorage(account);
106
+ const out = [];
107
+ const walk = (node, parentPath) => {
108
+ const isFolder = Boolean(node.directory);
109
+ const name = String(node.name ?? "");
110
+ const path = parentPath ? `${parentPath}/${name}` : name;
111
+ out.push({
112
+ handle: String(node.nodeId ?? name),
113
+ name,
114
+ isFolder,
115
+ isVideo: !isFolder && VIDEO_EXT.test(name),
116
+ isSubtitle: !isFolder && SUBTITLE_EXT.test(name),
117
+ size: Number(node.size ?? 0),
118
+ path,
119
+ });
120
+ if (!isFolder || !Array.isArray(node.children))
121
+ return;
122
+ for (const child of node.children)
123
+ walk(child, path);
124
+ };
125
+ const root = storage.root;
126
+ if (root && Array.isArray(root.children)) {
127
+ for (const child of root.children)
128
+ walk(child, "");
129
+ }
130
+ return out.filter((n) => n.isFolder || n.isVideo || n.isSubtitle);
131
+ }
132
+ /** Export a shareable link (with key) for a file inside the logged-in account. */
133
+ async exportLink(account, handle) {
134
+ const storage = await this.openStorage(account);
135
+ const node = storage.files?.[handle];
136
+ if (!node)
137
+ throw new Error("File not found in this MEGA account.");
138
+ if (typeof node.link !== "function")
139
+ throw new Error("Node has no link method.");
140
+ return new Promise((resolve, reject) => {
141
+ node.link?.((err, link) => {
142
+ if (err || !link)
143
+ reject(err ?? new Error("Failed to generate link"));
144
+ else
145
+ resolve(link);
146
+ });
147
+ });
148
+ }
149
+ }
@@ -0,0 +1,35 @@
1
+ /** A saved MEGA account (credentials + label). Passwords are handled by the store. */
2
+ export type MegaAccount = {
3
+ id: string;
4
+ email: string;
5
+ password: string;
6
+ label: string;
7
+ createdAt: number;
8
+ };
9
+ /**
10
+ * Pluggable persistence for MEGA accounts. Bring your own backend (Firestore,
11
+ * AsyncStorage, localStorage, …) by implementing this interface.
12
+ */
13
+ export type MegaAccountStore = {
14
+ list: () => Promise<MegaAccount[]>;
15
+ /** Upsert by id. */
16
+ save: (account: MegaAccount) => Promise<void>;
17
+ remove: (id: string) => Promise<void>;
18
+ /** Optional live updates; return an unsubscribe function. */
19
+ subscribe?: (cb: (accounts: MegaAccount[]) => void, key?: string) => () => void;
20
+ };
21
+ export type MegaTreeNode = {
22
+ /** MEGA node handle */
23
+ handle: string;
24
+ name: string;
25
+ isFolder: boolean;
26
+ isVideo: boolean;
27
+ isSubtitle: boolean;
28
+ size: number;
29
+ path: string;
30
+ };
31
+ export type AccountTestResult = {
32
+ name: string;
33
+ folders: number;
34
+ files: number;
35
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,5 @@
1
+ export { loadMegaFile, fileSizeOf, withTimeout } from "./megaFile.js";
2
+ export { collectMegaFileStream } from "./stream.js";
3
+ export { parseRange, toMegaRange, rangeHeaders, type RangeResult, } from "./ranges.js";
4
+ export { SW_MARKER } from "./swDefine.js";
5
+ export type { MegaFileLike, StreamLike } from "./proxy/types.js";
package/dist/index.js ADDED
@@ -0,0 +1,4 @@
1
+ export { loadMegaFile, fileSizeOf, withTimeout } from "./megaFile.js";
2
+ export { collectMegaFileStream } from "./stream.js";
3
+ export { parseRange, toMegaRange, rangeHeaders, } from "./ranges.js";
4
+ export { SW_MARKER } from "./swDefine.js";
@@ -0,0 +1,7 @@
1
+ import type { MegaFileLike } from "./proxy/types.js";
2
+ /** Lazily resolves megajs `File.fromURL` + attributes (incl. folder-link child). */
3
+ export declare function loadMegaFile(url: string): Promise<MegaFileLike>;
4
+ export declare function fileSizeOf(file: {
5
+ size?: number;
6
+ }): number;
7
+ export declare function withTimeout<T>(p: Promise<T>, ms: number, msg: string): Promise<T>;
@@ -0,0 +1,31 @@
1
+ const ATTRS_TIMEOUT_MS = 30_000;
2
+ /** Lazily resolves megajs `File.fromURL` + attributes (incl. folder-link child). */
3
+ export async function loadMegaFile(url) {
4
+ const { File } = (await import("megajs"));
5
+ const file = File.fromURL(url.trim());
6
+ await withTimeout(file.loadAttributes(), ATTRS_TIMEOUT_MS, "MEGA did not respond. Check your connection and try again.");
7
+ // Folder links: loadAttributes resolves to the folder itself — pick the
8
+ // first non-directory child instead (mirrors the megajs docs sample).
9
+ const group = file;
10
+ if (group.children?.length) {
11
+ const child = group.find?.((n) => !n.children, true);
12
+ if (child)
13
+ return child;
14
+ }
15
+ return file;
16
+ }
17
+ export function fileSizeOf(file) {
18
+ return Number(file.size ?? 0);
19
+ }
20
+ export async function withTimeout(p, ms, msg) {
21
+ return new Promise((resolve, reject) => {
22
+ const t = setTimeout(() => reject(new Error(msg)), ms);
23
+ p.then((v) => {
24
+ clearTimeout(t);
25
+ resolve(v);
26
+ }, (e) => {
27
+ clearTimeout(t);
28
+ reject(e);
29
+ });
30
+ });
31
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Idempotent: binds the singleton local proxy server once per app lifetime.
3
+ * Safe to call concurrently and repeatedly — subsequent calls are no-ops until
4
+ * `stopProxy()` is called. Does not load any MEGA file.
5
+ */
6
+ export declare function ensureProxy(): Promise<void>;
7
+ /** Registers one MEGA link as its own keyed session; returns its local URL. */
8
+ export declare function createStream(megaUrl: string): Promise<{
9
+ url: string;
10
+ sessionId: string;
11
+ }>;
12
+ /**
13
+ * Loads all MEGA parts and serves each at its own `/part-N.mp4` URL inside one
14
+ * session. Returns the ordered local URLs the player should play in sequence,
15
+ * plus the session id to release later. Nothing is written to disk; every byte
16
+ * range is streamed from MEGA as the player reads it.
17
+ */
18
+ export declare function createStreams(megaUrls: string[]): Promise<{
19
+ urls: string[];
20
+ sessionId: string;
21
+ }>;
22
+ /** Removes one session; other sessions and the singleton server keep running. */
23
+ export declare function releaseStream(sessionId: string): void;
24
+ /** Tear-down for tests / HMR: unbinds the server and clears every session. */
25
+ export declare function stopProxy(): void;
@@ -0,0 +1,336 @@
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 } from "../ranges.js";
25
+ const tcp = TcpSocket;
26
+ const createServer = tcp.createServer;
27
+ let server = null;
28
+ let proxyBase = "";
29
+ let proxyReady = null;
30
+ const sessions = new Map();
31
+ let sessionCounter = 0;
32
+ /** Collision-resistant session key: time base + monotonic counter + random tail. */
33
+ function newSessionId() {
34
+ sessionCounter += 1;
35
+ const t = Date.now().toString(36);
36
+ const c = sessionCounter.toString(36);
37
+ const r = Math.random().toString(36).slice(2, 6);
38
+ return `${t}${c}${r}`;
39
+ }
40
+ /** Coerce whatever the socket hands us (Buffer, Uint8Array, ArrayBuffer, string, array). */
41
+ function toBytes(data) {
42
+ if (typeof data === "string") {
43
+ const out = new Uint8Array(data.length);
44
+ for (let i = 0; i < data.length; i++)
45
+ out[i] = data.charCodeAt(i) & 0xff;
46
+ return out;
47
+ }
48
+ if (data instanceof ArrayBuffer)
49
+ return new Uint8Array(data);
50
+ if (ArrayBuffer.isView(data)) {
51
+ return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
52
+ }
53
+ if (Array.isArray(data))
54
+ return Uint8Array.from(data);
55
+ return new Uint8Array(0);
56
+ }
57
+ /** Latin-1 decode: polyfill-proof byte -> char conversion for header parsing. */
58
+ function bytesToText(bytes) {
59
+ let out = "";
60
+ for (let i = 0; i < bytes.length; i++)
61
+ out += String.fromCharCode(bytes[i]);
62
+ return out;
63
+ }
64
+ function findDoubleCrlf(bytes) {
65
+ for (let i = 0; i < bytes.length - 3; i++) {
66
+ if (bytes[i] === 13 &&
67
+ bytes[i + 1] === 10 &&
68
+ bytes[i + 2] === 13 &&
69
+ bytes[i + 3] === 10) {
70
+ return i;
71
+ }
72
+ }
73
+ return -1;
74
+ }
75
+ function parseHttpHead(head) {
76
+ const lines = head.split("\r\n");
77
+ const parts = (lines[0] ?? "").split(" ");
78
+ const headers = {};
79
+ for (let i = 1; i < lines.length; i++) {
80
+ const line = lines[i];
81
+ const idx = line.indexOf(":");
82
+ if (idx > 0) {
83
+ headers[line.slice(0, idx).trim().toLowerCase()] = line.slice(idx + 1).trim();
84
+ }
85
+ }
86
+ return { method: parts[0] ?? "", target: parts[1] ?? "", headers };
87
+ }
88
+ function serveError(socket, status, reason) {
89
+ const body = `${status} ${reason}`;
90
+ const head = [
91
+ `HTTP/1.1 ${status} ${reason}`,
92
+ "Content-Type: text/plain",
93
+ `Content-Length: ${body.length}`,
94
+ "Cache-Control: no-store",
95
+ "Connection: close",
96
+ "",
97
+ "",
98
+ ].join("\r\n");
99
+ socket.write(head + body);
100
+ socket.end();
101
+ }
102
+ function handleConnection(socket) {
103
+ const buf = [];
104
+ let handled = false;
105
+ socket.on("data", (data) => {
106
+ if (handled)
107
+ return;
108
+ const bytes = toBytes(data);
109
+ for (let i = 0; i < bytes.length; i++)
110
+ buf.push(bytes[i]);
111
+ const headEnd = findDoubleCrlf(Uint8Array.from(buf));
112
+ if (headEnd === -1) {
113
+ if (buf.length > 64 * 1024) {
114
+ handled = true;
115
+ serveError(socket, 400, "Bad Request");
116
+ }
117
+ return;
118
+ }
119
+ handled = true;
120
+ const head = bytesToText(Uint8Array.from(buf.slice(0, headEnd)));
121
+ handleRequest(socket, parseHttpHead(head));
122
+ });
123
+ socket.on("error", () => { });
124
+ socket.on("close", () => { });
125
+ }
126
+ /** Serves one file (a MEGA link or one stitched part) over a socket, honoring Range. */
127
+ function serveSingleFile(socket, req, file, size) {
128
+ if (size <= 0)
129
+ return serveError(socket, 503, "Stream has no size");
130
+ const range = parseRange(req.headers.range, size);
131
+ if (!range.satisfiable)
132
+ return serveError(socket, 416, "Range Not Satisfiable");
133
+ const headers = rangeHeaders(range, size, "video/mp4");
134
+ const statusLine = range.partial ? "HTTP/1.1 206 Partial Content" : "HTTP/1.1 200 OK";
135
+ const head = [
136
+ statusLine,
137
+ ...Object.entries(headers).map(([k, v]) => `${k}: ${v}`),
138
+ "Connection: close",
139
+ "",
140
+ "",
141
+ ].join("\r\n");
142
+ if (req.method === "HEAD") {
143
+ socket.write(head);
144
+ socket.end();
145
+ return;
146
+ }
147
+ console.log(`[megaStream] ${req.method} ${req.target} ${req.headers.range ?? "full"} -> ${range.start}-${range.end} of ${size}`);
148
+ const stream = file.download({
149
+ start: range.start,
150
+ end: range.end,
151
+ // Do NOT use maxConnections: 1 — that path pipes a streaming body which
152
+ // React Native's fetch layer cannot provide. The default multi-connection
153
+ // path reads chunks via arrayBuffer(), the proven working path.
154
+ maxConnections: 4,
155
+ });
156
+ let headSent = false;
157
+ let socketClosed = false;
158
+ const closeSocket = () => {
159
+ if (socketClosed)
160
+ return;
161
+ socketClosed = true;
162
+ try {
163
+ socket.end();
164
+ }
165
+ catch { }
166
+ };
167
+ const teardown = () => {
168
+ if (socketClosed)
169
+ return;
170
+ socketClosed = true;
171
+ try {
172
+ stream.destroy?.();
173
+ }
174
+ catch { }
175
+ try {
176
+ socket.destroy();
177
+ }
178
+ catch { }
179
+ };
180
+ stream.on("data", (chunk) => {
181
+ if (socketClosed)
182
+ return;
183
+ if (!headSent) {
184
+ headSent = true;
185
+ try {
186
+ socket.write(head);
187
+ }
188
+ catch {
189
+ teardown();
190
+ return;
191
+ }
192
+ }
193
+ try {
194
+ const bytes = chunk instanceof Uint8Array
195
+ ? chunk
196
+ : new Uint8Array(chunk?.byteLength ?? 0);
197
+ if (!socket.write(Buffer.isBuffer(chunk) ? chunk : bytes)) {
198
+ stream.pause?.();
199
+ }
200
+ }
201
+ catch {
202
+ teardown();
203
+ }
204
+ });
205
+ socket.on("drain", () => {
206
+ if (!socketClosed)
207
+ stream.resume?.();
208
+ });
209
+ stream.on("end", () => closeSocket());
210
+ stream.on("error", (err) => {
211
+ console.warn("[megaStream] stream error:", err?.message ?? err);
212
+ teardown();
213
+ });
214
+ socket.on("close", () => teardown());
215
+ socket.on("error", () => teardown());
216
+ }
217
+ function handleRequest(socket, req) {
218
+ console.log(`[megaStream] <- ${req.method || "(none)"} ${req.target || "(none)"} range=${req.headers.range ?? "none"}`);
219
+ if (req.method && req.method !== "GET" && req.method !== "HEAD") {
220
+ console.warn(`[megaStream] unexpected method ${req.method} — serving as GET`);
221
+ }
222
+ const [sessionId, name] = req.target.replace(/^\//, "").split(/\?|#/)[0].split("/", 2);
223
+ if (!sessionId)
224
+ return serveError(socket, 404, "Not Found");
225
+ const session = sessions.get(sessionId);
226
+ if (!session)
227
+ return serveError(socket, 404, `No stream session ${sessionId}`);
228
+ if (name) {
229
+ const part = session.parts.find((p) => p.name === name);
230
+ if (!part)
231
+ return serveError(socket, 404, `Not Found: ${name}`);
232
+ return serveSingleFile(socket, req, part.file, part.size);
233
+ }
234
+ return serveError(socket, 404, "Not Found");
235
+ }
236
+ async function bindProxyServer() {
237
+ const srv = createServer({ noDelay: true }, (socket) => handleConnection(socket));
238
+ await new Promise((resolve, reject) => {
239
+ const onErr = (e) => {
240
+ srv.removeListener("error", onErr);
241
+ reject(e);
242
+ };
243
+ srv.on("error", onErr);
244
+ srv.listen({ port: 0, host: "127.0.0.1" }, () => {
245
+ srv.removeListener("error", onErr);
246
+ resolve();
247
+ });
248
+ });
249
+ return srv;
250
+ }
251
+ /**
252
+ * Idempotent: binds the singleton local proxy server once per app lifetime.
253
+ * Safe to call concurrently and repeatedly — subsequent calls are no-ops until
254
+ * `stopProxy()` is called. Does not load any MEGA file.
255
+ */
256
+ export async function ensureProxy() {
257
+ if (server)
258
+ return;
259
+ if (proxyReady)
260
+ return proxyReady;
261
+ proxyReady = (async () => {
262
+ const srv = await bindProxyServer();
263
+ const addr = srv.address();
264
+ const port = typeof addr === "object" && addr ? addr.port : 0;
265
+ if (!port)
266
+ throw new Error("Failed to bind local stream server");
267
+ server = srv;
268
+ proxyBase = `http://127.0.0.1:${port}`;
269
+ console.log(`[megaStream] proxy listening on ${proxyBase}`);
270
+ })().catch((e) => {
271
+ proxyReady = null; // allow a retry after a failed bind
272
+ throw e;
273
+ });
274
+ return proxyReady;
275
+ }
276
+ /** Registers one MEGA link as its own keyed session; returns its local URL. */
277
+ export async function createStream(megaUrl) {
278
+ if (!megaUrl?.trim())
279
+ throw new Error("No MEGA file to stream.");
280
+ await ensureProxy();
281
+ const file = await loadMegaFile(megaUrl);
282
+ if (!file.size)
283
+ throw new Error("MEGA file has no size");
284
+ const sessionId = newSessionId();
285
+ const name = file.name || "stream.mp4";
286
+ sessions.set(sessionId, { parts: [{ name, file, size: file.size }] });
287
+ const url = `${proxyBase}/${sessionId}/${name}`;
288
+ console.log(`[megaStream] session ${sessionId}: serving '${name}' via ${url}`);
289
+ return { url, sessionId };
290
+ }
291
+ /**
292
+ * Loads all MEGA parts and serves each at its own `/part-N.mp4` URL inside one
293
+ * session. Returns the ordered local URLs the player should play in sequence,
294
+ * plus the session id to release later. Nothing is written to disk; every byte
295
+ * range is streamed from MEGA as the player reads it.
296
+ */
297
+ export async function createStreams(megaUrls) {
298
+ const clean = (megaUrls ?? []).map((u) => u?.trim()).filter(Boolean);
299
+ if (!clean.length)
300
+ throw new Error("No video parts to stream.");
301
+ await ensureProxy();
302
+ const parts = [];
303
+ for (let i = 0; i < clean.length; i++) {
304
+ const file = await loadMegaFile(clean[i]);
305
+ if (!file.size)
306
+ throw new Error("MEGA part has no size.");
307
+ parts.push({ name: `part-${i}.mp4`, file, size: file.size });
308
+ }
309
+ if (!parts.length)
310
+ throw new Error("No streamable parts found.");
311
+ const sessionId = newSessionId();
312
+ sessions.set(sessionId, { parts });
313
+ const urls = parts.map((p) => `${proxyBase}/${sessionId}/${p.name}`);
314
+ console.log(`[megaStream] session ${sessionId}: streaming ${parts.length} parts (${parts.map((p) => p.size).join(",")}) via ${urls[0]}`);
315
+ return { urls, sessionId };
316
+ }
317
+ /** Removes one session; other sessions and the singleton server keep running. */
318
+ export function releaseStream(sessionId) {
319
+ if (!sessionId || !sessions.has(sessionId))
320
+ return;
321
+ sessions.delete(sessionId);
322
+ console.log(`[megaStream] released session ${sessionId}`);
323
+ }
324
+ /** Tear-down for tests / HMR: unbinds the server and clears every session. */
325
+ export function stopProxy() {
326
+ sessions.clear();
327
+ if (server) {
328
+ try {
329
+ server.close();
330
+ }
331
+ catch { }
332
+ server = null;
333
+ }
334
+ proxyBase = "";
335
+ proxyReady = null;
336
+ }
@@ -0,0 +1,21 @@
1
+ import { SW_MARKER } from "../../swDefine.js";
2
+ export { SW_MARKER };
3
+ /** Paths of the bundled files inside this package (dist/proxy/sw). */
4
+ export declare function swAssetPaths(): {
5
+ sw: string;
6
+ megajs: string;
7
+ marker: string;
8
+ };
9
+ export type CopiedSwAssets = {
10
+ sw: string;
11
+ megajs: string;
12
+ marker: string;
13
+ };
14
+ /**
15
+ * Copies `sw.js` and `megajs.browser.js` into `outDir` (e.g. your `public/`
16
+ * or `web/` folder). Both files must be served from the same directory as the
17
+ * worker script so the SW can `importScripts("./megajs.browser.js")`.
18
+ *
19
+ * Returns the destination paths + the URL marker the client will use.
20
+ */
21
+ export declare function copySwAssets(outDir: string): CopiedSwAssets;
@@ -0,0 +1,32 @@
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
+ export { SW_MARKER };
11
+ /** Paths of the bundled files inside this package (dist/proxy/sw). */
12
+ export function swAssetPaths() {
13
+ const dir = dirname(fileURLToPath(import.meta.url));
14
+ return { sw: join(dir, "sw.js"), megajs: join(dir, "megajs.browser.js"), marker: SW_MARKER };
15
+ }
16
+ /**
17
+ * Copies `sw.js` and `megajs.browser.js` into `outDir` (e.g. your `public/`
18
+ * or `web/` folder). Both files must be served from the same directory as the
19
+ * worker script so the SW can `importScripts("./megajs.browser.js")`.
20
+ *
21
+ * Returns the destination paths + the URL marker the client will use.
22
+ */
23
+ export function copySwAssets(outDir) {
24
+ const src = swAssetPaths();
25
+ mkdirSync(outDir, { recursive: true });
26
+ const sw = join(outDir, "sw.js");
27
+ const megajs = join(outDir, "megajs.browser.js");
28
+ copyFileSync(src.sw, sw);
29
+ copyFileSync(src.megajs, megajs);
30
+ console.log(`[megaStream] copied sw.js + megajs.browser.js -> ${outDir}`);
31
+ return { sw, megajs, marker: SW_MARKER };
32
+ }