mega-proxy
v0.1.4
Published
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 reg
Maintainers
Readme
mega-proxy
Stream decrypted MEGA files over HTTP byte ranges — on Android/iOS via a
local TCP proxy (react-native-tcp-socket) and on the web via a Service
Worker — then play them with an Expo Video player (with subtitles) or manage
MEGA accounts with pluggable storage.
No files are written to disk: every byte range is downloaded and decrypted from MEGA on demand, so playback starts quickly without downloading the whole file.
Supported Expo SDK versions
Any Expo SDK 52 or newer (expo >= 52.0.0) is supported — the peer ranges
are open-ended, so no reinstalls are needed when you upgrade Expo, and older
SDKs (52+) are matched by the >=1/>=16 peers below.
| Expo SDK | expo-video | expo-file-system | Notes | | --- | --- | --- | --- | | 52 | ~1.x | ~16.x | legacy file-system API | | 53 | ~2.x | ~17.x | legacy file-system API | | 54+ / newest | ~2.x | ~18.x | legacy file-system API still exported |
The package only uses stable, cross-version APIs:
expo-video >= 1—useVideoPlayer/VideoViewplayer + controlsexpo-file-system >= 16—documentDirectory,makeDirectoryAsync,createDownloadResumable,getInfoAsync(the legacy API, present in every supported SDK; used bydownloadFileto save decrypted files to disk)react-native-tcp-socket >= 6— themega-proxy/nativeproxyexpo-font >= 12,@expo/vector-icons >= 14— optional player UI
If your app targets a very new SDK and a peer warning ever appears, install with
--legacy-peer-deps — the runtime surface does not change between SDKs 52+.
Installation
npm install mega-proxyPeers (install as needed for your platform):
| Package | Required for |
| --- | --- |
| react-native-video (>=6) | mega-proxy/player |
| react-native-tcp-socket (>=6) | mega-proxy/native (Android/iOS) |
| @react-native-async-storage/async-storage | mega-proxy/accounts (native store) |
| @expo/vector-icons, expo-font | optional (player UI icons/fonts) |
Package exports
| Import | Purpose |
| --- | --- |
| mega-proxy | Core helpers (MEGA file loading, streaming, range parsing) |
| mega-proxy/native | Android/iOS streaming proxy |
| mega-proxy/web | Web streaming proxy (Service Worker) |
| mega-proxy/player | Video player + controls + subtitle overlay |
| mega-proxy/subtitles | Subtitle loading / parsing (SRT, ASS, VTT) |
| mega-proxy/accounts | MEGA account manager with pluggable stores |
| mega-proxy/sw | Service Worker assets (sw.js, megajs.browser.js) |
All code is type-safe; pick any import surface TypeScript will flag unresolved peers you haven't installed.
Core ( mega-proxy)
import {
loadMegaFile, // File.fromURL + loadAttributes (retries, folder-link child support)
fileSizeOf, // number : Number(file.size ?? 0)
withTimeout, // Promise<T> with a rejection timeout + message
collectMegaFileStream, // download+decrypt a MEGA link in ~1MB chunks
parseRange, // parse "bytes=start-end" against a size
toMegaRange, // RangeResult -> { start, end } for file.download()
rangeHeaders, // 206/200 response headers including Content-Range
SW_MARKER, // "/__mmstream__/" URL prefix used by the web worker
} from "mega-proxy";const { name, size } = await collectMegaFileStream(megaUrl, async (chunk) => {
// backpressure-aware: the download pauses until this resolves
console.log("received", chunk.byteLength, "bytes");
});Range parsing
const r = parseRange("bytes=100-199", size); // satisfiable/start/end/length/partial
const r2 = parseRange("bytes=-500", size); // suffix range (last 500 bytes)
const { start, end } = toMegaRange(r) ?? {}; // pass these to file.download({start, end})
const headers = rangeHeaders(r, size, "video/mp4"); // Content-Range, Accept-Ranges, ...Native streaming proxy ( mega-proxy/native)
Binds a singleton local HTTP-ish TCP server on 127.0.0.1 so the OS video
player can request decrypted byte ranges without touching device storage.
import {
ensureProxy, // bind the proxy once per app lifetime (idempotent)
createStream, // one MEGA link -> one playable local URL
createStreams, // ordered MEGA parts -> /part-N.mp4 URLs played in sequence
releaseStream, // tear down one session (others keep streaming)
stopProxy, // full teardown (tests / HMR)
downloadFile, // save a decrypted MEGA file to app storage (expo-file-system)
} from " mega-proxy/native";await ensureProxy();
const { url, sessionId } = await createStream("https://mega.nz/file/xxxx#key");
// url: "http://127.0.0.1:<port>/<sessionId>/My%20Video.mp4"
const { urls, sessionId: multiId } = await createStreams([
"https://mega.nz/file/part1#key",
"https://mega.nz/file/part2#key",
]);
// urls: ["http://127.0.0.1:<port>/<multiId>/part-0.mp4", ".../part-1.mp4"]
// ... later:
releaseStream(sessionId);Saving files to disk
const { uri, name, size } = await downloadFile(megaUrl, {
subdirectory: "movies", // saved under documentDirectory/movies/
fileName: "Episode 1.mp4", // defaults to the MEGA file's name
onProgress: ({ loaded, total }) =>
console.log(`downloaded ${loaded}/${total} bytes`), // byte-level progress
});Same decrypted pipeline as streaming, but the whole file is written to
documentDirectory via expo-file-system's native downloader (no JS
typed-array copies).
Notes:
createStreamsdoes not concatenate bytes — each part keeps its ownmoovand is played back-to-back; the player advances part by part.- Requires
react-native-tcp-socket. Must run on a device/emulator (the server binds127.0.0.1).
Web streaming proxy ( mega-proxy/web)
Same API shape as the native proxy. Registers the bundled Service Worker, which answers HTTP Range requests by streaming truncated, decrypted megajs bytes.
import {
ensureProxy,
createStream,
createStreams,
releaseStream,
} from " mega-proxy/web";await ensureProxy();
const { url } = await createStream(megaUrl); // "/__mmstream__/<session>/stream.mp4"
const { urls } = await createStreams(partUrls); // "/__mmstream__/<session>/part-N.mp4"Serving the Service Worker assets
The worker script and a vendored megajs browser build live in the package and
must be copied into your public/ (or equivalent) directory:
import { copySwAssets, SW_MARKER } from " mega-proxy/sw";
const { sw, megajs, marker } = copySwAssets("public");
// copies public/sw.js + public/megajs.browser.js; both must be served
// from the same path so the worker can importScripts("./megajs.browser.js").The Service Worker requires a secure context (
https:,localhost, or127.0.0.1). When one is unavailable, the web proxy automatically falls back to fully downloading + decrypting each part intoBlobobject URLs.
Player ( mega-proxy/player)
Cross-platform player (Android / iOS / web) wrapped around react-native-video.
Handles ordered multi-part sources, auto-advance, subtitles, resolutions,
intro/outro skip regions, fullscreen and custom controls.
import { useRef } from "react";
import { MegaVideoPlayer, type MegaVideoPlayerHandle } from " mega-proxy/player";
function WatchScreen() {
const ref = useRef<MegaVideoPlayerHandle>(null);
return (
<MegaVideoPlayer
ref={ref}
uri={firstUrl} // playable URL (from createStream / createStreams)
sources={orderedUrls} // ordered parts, auto-advanced on end
title="My Movie"
autoPlay
subtitleUrl="https://mega.nz/file/sub#key" // MEGA link, http(s), or local .srt/.ass/.vtt
subtitleLang="English"
onEnd={() => console.log("finished")}
onError={(msg) => console.error(msg)}
/>
);
}Key props:
uri/sources— the proxy URLs fromcreateStream(s).subtitleUrl/subtitleTracks/subtitleFormat/subtitleLang— subtitles (see below).subtitleOffset— shift cue timestamps by an offset in seconds.resolutions/resolution/onSelectResolution— quality switcher UI.introRegion/outroRegion/onSkipIntro/onSkipOutro— skip buttons.autoPlay,onEnd,onError,onProgress,onFullscreenChange.
Imperative handle: ref.current.add(url, { title }) loads a new source;
ref.current.stop() stops playback.
Lower-level components are also exported: PlayerControls and SubtitleDisplay.
Subtitles ( mega-proxy/subtitles)
Parses SRT, ASS/SSA and WebVTT into a flat cue list and loads subtitle files from MEGA links (downloaded + decrypted), http(s) hosts (CORS required on web), or the local filesystem.
import {
loadSubtitles, // url + options -> SubCue[]
parseSubtitles, // raw text + optional format -> SubCue[]
detectSubtitleFormat, // url -> "srt" | "ass" | "vtt"
} from " mega-proxy/subtitles";const cues = await loadSubtitles("https://mega.nz/file/subs#key");
// [{ start, end, text }, ...] — start/end in seconds
// local file (bring your own reader):
const cues2 = await loadSubtitles("file:///path/subs.srt", {
readLocalFile: (uri) => readAsStringAsync(uri),
});SubCue = { start: number; end: number; text: string }. MEGA-stored subtitles
are content-sniffed (ASS scripts are detected even though MEGA URLs carry no
extension). Format detection falls back to "srt" when a URL gives no hints.
Accounts ( mega-proxy/accounts)
Manage saved MEGA accounts (login, browse the cloud drive, generate shareable links) with a pluggable persistence store.
import {
MegaAccountManager,
createLocalStorageStore, // web: localStorage (+ cross-tab subscribe)
createAsyncStorageStore, // native: AsyncStorage
} from " mega-proxy/accounts";
const manager = new MegaAccountManager(createAsyncStorageStore()); // or createLocalStorageStore()
const account = await manager.addAccount({
email: "[email protected]",
password: "…",
label: "Personal",
});
const accounts = await manager.listAccounts();
const { name, folders, files } = await manager.testAccount(account); // login + drive count
const tree = await manager.loadTree(account); // filtered to folders/videos/subtitles
const link = await manager.exportLink(account, tree[0].handle); // create a shareable link
manager.subscribe((accounts) => /* re-render on external changes */);
manager.deleteAccount(account.id);
manager.closeAll(); // close live megajs Storage sessionsBringing your own store
Implement MegaAccountStore to persist anywhere (Firestore, SQLite, …):
import type { MegaAccountStore } from " mega-proxy/accounts";
const myStore: MegaAccountStore = {
list: async () => /* ... */,
save: async (account) => /* upsert by account.id */,
remove: async (id) => /* ... */,
subscribe: (cb) => /* optional: live updates; return an unsubscribe fn */,
};Note: credentials (including passwords) are persisted by the store you provide — secure storage is your responsibility.
Development
npm run build # tsc + copy sw.js / megajs.browser.js into dist
npm run typecheck # tsc --noEmit
npm test # build + node --test tests/*.test.mjs
npm pack # build then pack the tarball for publishing- Node >= 18 is required.
- The repo is structured as a library with the platform entry points declared in
package.jsonexports;dist/is generated bynpm run build. - Tests cover range parsing, subtitle parsing, and SW asset bundling.
License
MIT
