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,9 @@
1
+ export { parseSubtitles, detectSubtitleFormat, type SubCue, type SubtitleFormat, } from "./parse.js";
2
+ export { loadSubtitles, type LoadSubtitlesOptions } from "./load.js";
3
+ import type { SubtitleFormat } from "./parse.js";
4
+ /** A playable subtitle track for the player components. */
5
+ export type SubtitleTrack = {
6
+ lang: string;
7
+ url: string;
8
+ format?: SubtitleFormat;
9
+ };
@@ -0,0 +1,2 @@
1
+ export { parseSubtitles, detectSubtitleFormat, } from "./parse.js";
2
+ export { loadSubtitles } from "./load.js";
@@ -0,0 +1,18 @@
1
+ import { type SubCue, type SubtitleFormat } from "./parse.js";
2
+ export type LoadSubtitlesOptions = {
3
+ format?: SubtitleFormat;
4
+ /**
5
+ * Reads a local file URI (e.g. an ExoPlayer/RNFS cached subtitle). Pass your
6
+ * app's file reader here (e.g. `expo-file-system` `readAsStringAsync`) —
7
+ * the package stays decoupled from any file-system implementation.
8
+ */
9
+ readLocalFile?: (uri: string) => Promise<string>;
10
+ };
11
+ /**
12
+ * Fetches a subtitle file and parses it (srt/ass/vtt).
13
+ * MEGA links are downloaded + decrypted through the same pipeline the videos
14
+ * use (a plain fetch would hit MEGA's HTML page / CORS block and parse nothing);
15
+ * any other host must allow CORS on web.
16
+ * Throws on failure so callers can surface the reason (network, MEGA quota…).
17
+ */
18
+ export declare function loadSubtitles(url: string, options?: LoadSubtitlesOptions): Promise<SubCue[]>;
@@ -0,0 +1,58 @@
1
+ import { Buffer } from "buffer";
2
+ import { collectMegaFileStream } from "../stream.js";
3
+ import { detectSubtitleFormat, parseSubtitles, stripBom, } from "./parse.js";
4
+ const IS_MEGA_LINK = /mega(\.co)?\.nz|mega\.io/i;
5
+ /** reading a MEGA link is capped at this many bytes (subtitle files are small) */
6
+ const MAX_MEGA_BYTES = 8 * 1024 * 1024;
7
+ const MEGA_TIMEOUT_MS = 60_000;
8
+ /**
9
+ * Downloads + decrypts the whole (small) MEGA file into memory. Subtitle files
10
+ * are tiny by nature, so buffering the stream is fine here.
11
+ */
12
+ async function readMegaToBuffer(url) {
13
+ const chunks = [];
14
+ let total = 0;
15
+ const timeout = new Promise((_, reject) => {
16
+ setTimeout(() => reject(new Error("Subtitle download timed out.")), MEGA_TIMEOUT_MS);
17
+ });
18
+ const load = collectMegaFileStream(url, async (chunk) => {
19
+ if (total + chunk.length > MAX_MEGA_BYTES) {
20
+ throw new Error("Subtitle file is unexpectedly large.");
21
+ }
22
+ chunks.push(chunk);
23
+ total += chunk.length;
24
+ }).then(() => Buffer.concat(chunks.map((c) => Buffer.from(c))));
25
+ return Promise.race([load, timeout]);
26
+ }
27
+ /**
28
+ * Fetches a subtitle file and parses it (srt/ass/vtt).
29
+ * MEGA links are downloaded + decrypted through the same pipeline the videos
30
+ * use (a plain fetch would hit MEGA's HTML page / CORS block and parse nothing);
31
+ * any other host must allow CORS on web.
32
+ * Throws on failure so callers can surface the reason (network, MEGA quota…).
33
+ */
34
+ export async function loadSubtitles(url, options) {
35
+ const format = options?.format;
36
+ let text;
37
+ if (IS_MEGA_LINK.test(url)) {
38
+ const buf = await readMegaToBuffer(url);
39
+ text = buf.toString("utf8");
40
+ }
41
+ else if (/^(file|content):|^\//.test(url)) {
42
+ // a local file already on disk (offline subtitle)
43
+ if (!options?.readLocalFile) {
44
+ throw new Error("Local subtitle file requested but no readLocalFile reader was provided.");
45
+ }
46
+ text = await options.readLocalFile(url);
47
+ }
48
+ else {
49
+ const res = await fetch(url);
50
+ if (!res.ok)
51
+ throw new Error(`Subtitles HTTP ${res.status}`);
52
+ text = await res.text();
53
+ }
54
+ const fmt = format ?? detectSubtitleFormat(url);
55
+ // MEGA links never carry an extension, so a URL guess is unreliable: let the
56
+ // parser sniff ASS content when no explicit format was supplied.
57
+ return parseSubtitles(stripBom(text), format ? fmt : undefined);
58
+ }
@@ -0,0 +1,19 @@
1
+ export type SubCue = {
2
+ /** seconds */
3
+ start: number;
4
+ /** seconds */
5
+ end: number;
6
+ text: string;
7
+ };
8
+ export type SubtitleFormat = "srt" | "ass" | "vtt";
9
+ /** Strip a leading UTF-8 BOM (\uFEFF) if present */
10
+ export declare const stripBom: (s: string) => string;
11
+ /**
12
+ * Derive the format from a URL/file name when not explicitly given.
13
+ * NOTE: MEGA links are `https://mega.nz/file/<id>#<key>` (or `/#!id!key`, or
14
+ * `/folder/…`) so they carry NO file extension — this returns a neutral
15
+ * default and the content is sniffed later in loadSubtitles/parseSubtitles.
16
+ */
17
+ export declare function detectSubtitleFormat(url: string): SubtitleFormat;
18
+ /** Minimal WebVTT/SRT parser -> flat cue list */
19
+ export declare function parseSubtitles(raw: string, format?: SubtitleFormat): SubCue[];
@@ -0,0 +1,95 @@
1
+ /** supports 00:00:01.000 (VTT) and 00:00:01,000 (SRT/ASS) */
2
+ const toSeconds = (ts) => {
3
+ const m = ts.trim().match(/^(?:(\d+):)?(\d{1,2}):(\d{1,2})[.,](\d{1,3})$/);
4
+ if (!m)
5
+ return NaN;
6
+ const h = Number(m[1] ?? 0);
7
+ const min = Number(m[2]);
8
+ const s = Number(m[3]);
9
+ const ms = Number(m[4].padEnd(3, "0"));
10
+ return h * 3600 + min * 60 + s + ms / 1000;
11
+ };
12
+ /** Strip a leading UTF-8 BOM (\uFEFF) if present */
13
+ export const stripBom = (s) => s.charCodeAt(0) === 0xfeff ? s.slice(1) : s;
14
+ /**
15
+ * Derive the format from a URL/file name when not explicitly given.
16
+ * NOTE: MEGA links are `https://mega.nz/file/<id>#<key>` (or `/#!id!key`, or
17
+ * `/folder/…`) so they carry NO file extension — this returns a neutral
18
+ * default and the content is sniffed later in loadSubtitles/parseSubtitles.
19
+ */
20
+ export function detectSubtitleFormat(url) {
21
+ const clean = url.split("?")[0].split("#")[0] ?? "";
22
+ const ext = (clean.split(".").pop() ?? "").toLowerCase();
23
+ if (ext === "ass" || ext === "ssa")
24
+ return "ass";
25
+ if (ext === "srt")
26
+ return "srt";
27
+ if (ext === "vtt")
28
+ return "vtt";
29
+ // extensionless (MEGA links, direct embed endpoints) — not enough info
30
+ return "srt";
31
+ }
32
+ const stripAssTags = (t) => t
33
+ .replace(/\{[^}]*\}/g, "")
34
+ .replace(/\\N/gi, "\n")
35
+ .replace(/\\n/gi, "\n")
36
+ .replace(/\\h/gi, " ")
37
+ .trim();
38
+ /** True when the raw text looks like an ASS/SSA script (content sniff) */
39
+ function isAssLike(raw) {
40
+ return /^\s*\[(?:Script Info|V4\+? Styles|Events)\s*\]/m.test(raw);
41
+ }
42
+ /** Minimal ASS/SSA parser: reads the [Events] section Dialogue lines */
43
+ function parseAss(raw) {
44
+ const cues = [];
45
+ const body = raw.replace(/\r/g, "").split(/\n+/);
46
+ for (const line of body) {
47
+ if (!/^Dialogue\s*:/.test(line))
48
+ continue;
49
+ // Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV,
50
+ // Effect, Text — the first 9 commas separate fields.
51
+ const commaParts = line.split(",");
52
+ if (commaParts.length < 10)
53
+ continue;
54
+ const start = toSeconds(commaParts[1].trim());
55
+ const end = toSeconds(commaParts[2].trim());
56
+ const text = stripAssTags(commaParts.slice(9).join(","));
57
+ if (!Number.isFinite(start) || !Number.isFinite(end) || !text)
58
+ continue;
59
+ cues.push({ start, end, text });
60
+ }
61
+ return cues;
62
+ }
63
+ /** Minimal WebVTT/SRT parser -> flat cue list */
64
+ export function parseSubtitles(raw, format) {
65
+ const text = stripBom(raw);
66
+ // ASS must be detected by content: MEGA-stored urls have no extension, so a
67
+ // URL-derived "srt"/"vtt" guess would silently drop every ASS cue.
68
+ if (format === "ass" || (!format && isAssLike(text)))
69
+ return parseAss(text);
70
+ const cues = [];
71
+ const blocks = text
72
+ .replace(/\r/g, "")
73
+ .replace(/^WEBVTT.*$/m, "")
74
+ .split(/\n\n+/);
75
+ for (const block of blocks) {
76
+ const lines = block.split("\n").filter((l) => l.trim() !== "");
77
+ const tIdx = lines.findIndex((l) => l.includes("-->"));
78
+ if (tIdx === -1)
79
+ continue;
80
+ const [a, b] = lines[tIdx].split("-->");
81
+ const start = toSeconds(a);
82
+ const endRaw = b.split(/\s+/).filter(Boolean)[0] ?? "";
83
+ const end = toSeconds(endRaw);
84
+ if (!Number.isFinite(start) || !Number.isFinite(end))
85
+ continue;
86
+ const text = lines
87
+ .slice(tIdx + 1)
88
+ .join("\n")
89
+ .replace(/<[^>]+>/g, "")
90
+ .trim();
91
+ if (text)
92
+ cues.push({ start, end, text });
93
+ }
94
+ return cues;
95
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * URL prefix for streaming URLs answered by the bundled Service Worker.
3
+ * Keep in sync with `src/proxy/sw/sw.js` (asserted by tests).
4
+ */
5
+ export declare const SW_MARKER = "/__mmstream__/";
@@ -0,0 +1,5 @@
1
+ /**
2
+ * URL prefix for streaming URLs answered by the bundled Service Worker.
3
+ * Keep in sync with `src/proxy/sw/sw.js` (asserted by tests).
4
+ */
5
+ export const SW_MARKER = "/__mmstream__/";
package/package.json ADDED
@@ -0,0 +1,112 @@
1
+ {
2
+ "name": "mega-stream-proxy",
3
+ "version": "0.1.9",
4
+ "description": "Stream decrypted MEGA files over HTTP byte ranges on Android (react-native-tcp-socket) and the web (Service Worker), play them with an Expo Video player + subtitles, and manage MEGA accounts with pluggable storage. Zero-config: runtime native deps are regular dependencies (auto-linked).",
5
+ "type": "module",
6
+ "sideEffects": false,
7
+ "main": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "default": "./dist/index.js"
13
+ },
14
+ "./native": {
15
+ "types": "./dist/proxy/native.d.ts",
16
+ "default": "./dist/proxy/native.js"
17
+ },
18
+ "./web": {
19
+ "types": "./dist/proxy/web.d.ts",
20
+ "default": "./dist/proxy/web.js"
21
+ },
22
+ "./accounts": {
23
+ "types": "./dist/accounts/index.d.ts",
24
+ "default": "./dist/accounts/index.js"
25
+ },
26
+ "./sw": {
27
+ "types": "./dist/proxy/sw/bundle.d.ts",
28
+ "default": "./dist/proxy/sw/bundle.js"
29
+ },
30
+ "./player": {
31
+ "types": "./dist/Player/playerweb/index.d.ts",
32
+ "default": "./dist/Player/playerweb/index.js"
33
+ },
34
+ "./playerAndroid": {
35
+ "types": "./dist/Player/playerAndroid/index.d.ts",
36
+ "default": "./dist/Player/playerAndroid/index.js"
37
+ },
38
+ "./subtitles": {
39
+ "types": "./dist/subtitles/index.d.ts",
40
+ "default": "./dist/subtitles/index.js"
41
+ }
42
+ },
43
+ "files": [
44
+ "dist",
45
+ "android",
46
+ "src"
47
+ ],
48
+ "scripts": {
49
+ "build": "tsc -p tsconfig.json && node scripts/copy-sw-assets.js",
50
+ "typecheck": "tsc --noEmit",
51
+ "test": "npm run build && node --test \"tests/*.test.mjs\"",
52
+ "prepack": "npm run build"
53
+ },
54
+ "engines": {
55
+ "node": ">=18"
56
+ },
57
+ "dependencies": {
58
+ "@react-native-async-storage/async-storage": "^2.0.0",
59
+ "buffer": "^6.0.3",
60
+ "megajs": "^1.3.0",
61
+ "react-native-tcp-socket": "^6.1.0"
62
+ },
63
+ "peerDependencies": {
64
+ "@expo/vector-icons": ">=13",
65
+ "expo-font": ">=13",
66
+ "expo-video": ">=1.0",
67
+ "react": ">=18",
68
+ "react-native": ">=0.71"
69
+ },
70
+ "peerDependenciesMeta": {
71
+ "@expo/vector-icons": {
72
+ "optional": true
73
+ },
74
+ "expo-font": {
75
+ "optional": true
76
+ },
77
+ "expo-video": {
78
+ "optional": true
79
+ },
80
+ "react-native-video": {
81
+ "optional": true
82
+ }
83
+ },
84
+ "devDependencies": {
85
+ "@expo/vector-icons": "~14.0.4",
86
+ "@types/node": "^22.0.0",
87
+ "@types/react": "~18.3.12",
88
+ "buffer": "^6.0.3",
89
+ "expo-font": "~13.0.4",
90
+ "expo-video": "~2.0.6",
91
+ "megajs": "^1.3.0",
92
+ "react": "18.3.1",
93
+ "react-native": "0.76.9",
94
+ "react-native-tcp-socket": "^6.1.0",
95
+ "react-native-video": ">=6",
96
+ "typescript": "^5.5.0"
97
+ },
98
+ "keywords": [
99
+ "mega",
100
+ "megajs",
101
+ "streaming",
102
+ "http-range",
103
+ "react-native",
104
+ "expo",
105
+ "video",
106
+ "subtitles",
107
+ "service-worker",
108
+ "proxy",
109
+ "accounts"
110
+ ],
111
+ "license": "MIT"
112
+ }