mega-stream-proxy 0.1.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +518 -0
- package/android/build.gradle +31 -0
- package/android/src/main/AndroidManifest.xml +3 -0
- package/dist/Player/native.d.ts +3 -0
- package/dist/Player/native.js +3 -0
- package/dist/Player/playerAndroid/MegaVideoPlayerAndroid.d.ts +47 -0
- package/dist/Player/playerAndroid/MegaVideoPlayerAndroid.js +624 -0
- package/dist/Player/playerAndroid/PlayerControlsAndroid.d.ts +54 -0
- package/dist/Player/playerAndroid/PlayerControlsAndroid.js +370 -0
- package/dist/Player/playerAndroid/SubtitleDisplayAndroid.d.ts +21 -0
- package/dist/Player/playerAndroid/SubtitleDisplayAndroid.js +55 -0
- package/dist/Player/playerAndroid/index.d.ts +3 -0
- package/dist/Player/playerAndroid/index.js +3 -0
- package/dist/Player/playerweb/MegaVideoPlayer.d.ts +92 -0
- package/dist/Player/playerweb/MegaVideoPlayer.js +368 -0
- package/dist/Player/playerweb/PlayerControls.d.ts +52 -0
- package/dist/Player/playerweb/PlayerControls.js +346 -0
- package/dist/Player/playerweb/SubtitleDisplay.d.ts +15 -0
- package/dist/Player/playerweb/SubtitleDisplay.js +32 -0
- package/dist/Player/playerweb/index.d.ts +3 -0
- package/dist/Player/playerweb/index.js +3 -0
- package/dist/Player/web.d.ts +3 -0
- package/dist/Player/web.js +3 -0
- package/dist/accounts/async-storage.d.ts +10 -0
- package/dist/accounts/async-storage.js +46 -0
- package/dist/accounts/index.d.ts +4 -0
- package/dist/accounts/index.js +3 -0
- package/dist/accounts/local-storage.d.ts +3 -0
- package/dist/accounts/local-storage.js +63 -0
- package/dist/accounts/manager.d.ts +39 -0
- package/dist/accounts/manager.js +149 -0
- package/dist/accounts/types.d.ts +35 -0
- package/dist/accounts/types.js +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +4 -0
- package/dist/megaFile.d.ts +7 -0
- package/dist/megaFile.js +31 -0
- package/dist/proxy/native.d.ts +25 -0
- package/dist/proxy/native.js +336 -0
- package/dist/proxy/sw/bundle.d.ts +21 -0
- package/dist/proxy/sw/bundle.js +32 -0
- package/dist/proxy/sw/megajs.browser.js +27 -0
- package/dist/proxy/sw/sw.js +212 -0
- package/dist/proxy/types.d.ts +15 -0
- package/dist/proxy/types.js +2 -0
- package/dist/proxy/web.d.ts +24 -0
- package/dist/proxy/web.js +231 -0
- package/dist/ranges.d.ts +30 -0
- package/dist/ranges.js +63 -0
- package/dist/stream.d.ts +9 -0
- package/dist/stream.js +100 -0
- package/dist/subtitles/index.d.ts +9 -0
- package/dist/subtitles/index.js +2 -0
- package/dist/subtitles/load.d.ts +18 -0
- package/dist/subtitles/load.js +58 -0
- package/dist/subtitles/parse.d.ts +19 -0
- package/dist/subtitles/parse.js +95 -0
- package/dist/swDefine.d.ts +5 -0
- package/dist/swDefine.js +5 -0
- package/package.json +112 -0
- package/src/Player/playerAndroid/MegaVideoPlayerAndroid.tsx +825 -0
- package/src/Player/playerAndroid/PlayerControlsAndroid.tsx +824 -0
- package/src/Player/playerAndroid/SubtitleDisplayAndroid.tsx +80 -0
- package/src/Player/playerAndroid/index.ts +7 -0
- package/src/Player/playerweb/MegaVideoPlayer.tsx +551 -0
- package/src/Player/playerweb/PlayerControls.tsx +798 -0
- package/src/Player/playerweb/SubtitleDisplay.tsx +48 -0
- package/src/Player/playerweb/index.ts +3 -0
- package/src/accounts/async-storage.ts +55 -0
- package/src/accounts/index.ts +9 -0
- package/src/accounts/local-storage.ts +66 -0
- package/src/accounts/manager.ts +174 -0
- package/src/accounts/types.ts +38 -0
- package/src/index.ts +10 -0
- package/src/megaFile.ts +48 -0
- package/src/proxy/native.ts +396 -0
- package/src/proxy/sw/bundle.ts +37 -0
- package/src/proxy/sw/megajs.browser.js +27 -0
- package/src/proxy/sw/sw.js +212 -0
- package/src/proxy/types.ts +17 -0
- package/src/proxy/web.ts +266 -0
- package/src/ranges.ts +72 -0
- package/src/stream.ts +117 -0
- package/src/subtitles/index.ts +15 -0
- package/src/subtitles/load.ts +83 -0
- package/src/subtitles/parse.ts +105 -0
- package/src/swDefine.ts +5 -0
|
@@ -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,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
|
+
}
|
package/dist/swDefine.js
ADDED
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
|
+
}
|