mega-stream-proxy
v0.2.0
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-stream-proxy
Stream decrypted MEGA files over HTTP byte ranges — exactly like a VOD server — on Android/iOS (a local TCP proxy) and the web (a Service Worker). Ships a ready-to-use Expo Video player + subtitle engine, and MEGA account management with pluggable storage.
No server required. No full-file downloads at playback time. The video element asks
for the exact range it needs and mega-stream-proxy decrypts and serves only those bytes.
mega-link ──> megajs File.fromURL ──┬─> Android/iOS: 127.0.0.1:PORT/… ──> VideoPlayer
└─> Web: Service Worker /__mmstream__/… ──> <video>Features
- Native proxy (
/native): runs a local HTTP server viareact-native-tcp-socket, answersRangerequests by piping decrypted megajs chunks (maxConnections: 4). - Web proxy (
/web+/sw): registers the bundled Service Worker, returns same-origin/__mmstream__/…URLs that the worker range-streams on demand. Graceful fallback: without a secure context it downloads+decrypts into Blob URLs. - Player (
/player): cross-platformMegaVideoPlayer(expo-video) with custom controls, multi-part auto-advance, subtitle overlay (srt/ass/vtt), subtitle track picker, text-size slider, intro/outro skip regions. - Subtitles (
/subtitles): SRT/ASS/VTT parser, format auto-detect including content sniffing for extension-less MEGA links. - Accounts (
/accounts):MegaAccountManagerwith CRUD, live megajs sessions, Cloud-Drive tree, file export-link, and a pluggableMegaAccountStore(createLocalStorageStorefor web,createAsyncStorageStorefor React Native, or your own backend).
Requirements
- Web: a modern browser at
https:(orlocalhost) for Service Worker streaming; Node 18+ for thecopySwAssetsdeploy step. - Android / iOS: React Native ≥ 0.71 (Expo SDK 52 recommended) — the native streaming module is auto-linked, no manual Gradle/Xcode edits.
- All platforms need
react,react-native,expo-video, and@expo/vector-icons(Expo apps already ship the last two).
for sdk 54 + support versions
visit url https://www.npmjs.com/package/mega-proxy?activeTab=readme
Install
# from npm (once published)
npm install mega-stream-proxy
# or from the packed tarball / a local folder
npm install ./mega-stream-proxy-0.1.0.tgz
npm install "file:../mega-stream-proxy"Runtime dependencies are regular dependencies, so installing the package pulls
in everything (megajs, buffer, react-native-tcp-socket, async-storage) with zero
extra setup:
- React Native apps:
react-native-tcp-socketis auto-linked by RN autolinking — you do not editandroid/build.gradle. The package ships anandroid/gradle module purely for clean autolinking; it contains no native code. - Web apps:
copySwAssets()copies the worker + vendored megajs browser build into yourpublic/directory. No bundler changes.
react, react-native, expo-video and @expo/vector-icons are peer/optional deps —
your Expo app already provides them.
Android / iOS (native streaming)
import { useEffect, useMemo } from "react";
import { View } from "react-native";
import { ensureProxy, createStream, releaseStream } from "mega-stream-proxy/native";
import { MegaVideoPlayer } from "mega-stream-proxy/player";
export function WatchScreen({ megaUrl, subtitleUrl }) {
const ref = useMemo(() => ({ url: null as string | null, sessionId: null as string | null }), []);
useEffect(() => {
let alive = true;
void ensureProxy(); // idempotent — starts the singleton server once per app lifetime
createStream(megaUrl).then(({ url, sessionId }) => {
if (!alive) {
releaseStream(sessionId);
return;
}
ref.url = url;
ref.sessionId = sessionId;
});
return () => {
alive = false;
if (ref.sessionId) releaseStream(ref.sessionId);
};
}, [megaUrl]);
return (
<View style={{ flex: 1 }}>
<MegaVideoPlayer uri={ref.url} subtitleUrl={subtitleUrl} autoPlay />
</View>
);
}ensureProxy(): Promise<void>— idempotent; binds the singleton local server once per app lifetime. Safe to call repeatedly; no-op untilstopProxy().createStream(megaUrl): Promise<{ url, sessionId }>— one keyed session per MEGA link; concurrent streams get independent routes on the same server.createStreams(megaUrls): Promise<{ urls, sessionId }>— ordered parts, played back-to-back by the player, under one session.releaseStream(sessionId)— tears down that session only; other sessions and the server keep running.stopProxy()— unbinds the server and clears all sessions (tests / HMR).
Android networking: loopback http://127.0.0.1 is allowed in Expo dev builds by
default — no permissions or cleartext config is needed. For a release APK keep
usesCleartextTraffic enabled (your RN app templates usually set it) or the
stream URL stays on loopback, which most players accept.
Web
1. Copy the Service Worker assets (build/deploy step, node)
import { copySwAssets } from "mega-stream-proxy/sw";
// e.g. Expo web: copy into ./public (before `expo export`)
const files = copySwAssets("public");
console.log("copied", files.sw, files.megajs);
// sw.js -> public/sw.js (the range-streaming worker)
// megajs.browser.js -> public/megajs.browser.js (vendored megajs UMD)Server from a host that allows a Service Worker (https: or localhost).
2. Stream + play
import { useEffect, useMemo } from "react";
import { View } from "react-native";
import { ensureProxy, createStream, releaseStream } from "mega-stream-proxy/web";
import { MegaVideoPlayer } from "mega-stream-proxy/player";
export function WatchScreen({ megaUrl, subtitleUrl }) {
const ref = useMemo(() => ({ url: null as string | null, sessionId: null as string | null }), []);
useEffect(() => {
let alive = true;
void ensureProxy(); // idempotent — ensures the Service Worker is registered once
createStream(megaUrl).then(({ url, sessionId }) => {
if (!alive) {
releaseStream(sessionId);
return;
}
ref.url = url; // e.g. /__mmstream__/abc/part-0.mp4
ref.sessionId = sessionId;
});
return () => {
alive = false;
if (ref.sessionId) releaseStream(ref.sessionId);
};
}, [megaUrl]);
return (
<View style={{ flex: 1 }}>
<MegaVideoPlayer uri={ref.url} subtitleUrl={subtitleUrl} autoPlay />
</View>
);
}The web API matches the native one exactly: ensureProxy(), createStream(megaUrl) →
{ url, sessionId }, createStreams(megaUrls) → { urls, sessionId },
releaseStream(sessionId), and stopProxy(). Each createStream/createStreams call
registers its own keyed session — several streams can coexist, and releasing one
session leaves the others untouched.
If no Service Worker is available the proxy silently falls back to full download into Blob object URLs (still plays).
Using the proxy URL with your own player (expo-video / expo-av)
You don't need MegaVideoPlayer — the proxy returns a plain, range-capable HTTP URL,
so it works with any player that speaks HTTP ranges. The only lifecycle to remember is:
call ensureProxy() once, hold the sessionId from createStream, and call
releaseStream(sessionId) in your unmount cleanup. Import from
mega-stream-proxy/native on Android/iOS or mega-stream-proxy/web on the web — the
API is identical.
expo-video (VideoView)
import { useEffect, useRef } from "react";
import { View } from "react-native";
import { createVideoPlayer, VideoView } from "expo-video";
import { ensureProxy, createStream, releaseStream } from "mega-stream-proxy/web"; // or /native
export function WatchScreen({ megaUrl }) {
const player = useRef(createVideoPlayer(null)).current;
const sessionId = useRef<string | null>(null);
useEffect(() => {
let alive = true;
void ensureProxy(); // idempotent
createStream(megaUrl).then(({ url, sessionId: id }) => {
if (!alive) {
releaseStream(id);
return;
}
sessionId.current = id;
player.replace({ uri: url });
});
return () => {
alive = false;
if (sessionId.current) releaseStream(sessionId.current);
player.release();
};
}, [megaUrl]);
return (
<View style={{ flex: 1 }}>
<VideoView player={player} style={{ flex: 1 }} contentFit="contain" />
</View>
);
}expo-av (Video)
import { useEffect, useRef } from "react";
import { View } from "react-native";
import { Video, ResizeMode } from "expo-av";
import { ensureProxy, createStream, releaseStream } from "mega-stream-proxy/web"; // or /native
export function WatchScreen({ megaUrl }) {
const uri = useRef<string | null>(null);
const sessionId = useRef<string | null>(null);
useEffect(() => {
let alive = true;
void ensureProxy(); // idempotent
createStream(megaUrl).then(({ url, sessionId: id }) => {
if (!alive) {
releaseStream(id);
return;
}
sessionId.current = id;
uri.current = url;
});
return () => {
alive = false;
if (sessionId.current) releaseStream(sessionId.current);
};
}, [megaUrl]);
return (
<View style={{ flex: 1 }}>
<Video
style={{ flex: 1 }}
source={{ uri: uri.current ?? undefined }}
resizeMode={ResizeMode.CONTAIN}
shouldPlay
/>
</View>
);
}The same pattern applies to multi-part episodes: createStreams(megaUrls) returns
{ urls, sessionId } — feed the ordered URLs to your player's queue or switch the
source when one part ends, then releaseStream(sessionId) once when you leave.
Player component
import { MegaVideoPlayer, PlayerControls, SubtitleDisplay } from "mega-stream-proxy/player";
This is the same player MyMovies uses (components/MegaVideoPlayer.web.tsx),
byte-for-byte — same expo-video engine, same custom controls (scrub bar, volume,
skip ±10s, subtitle picker + text-size slider, intro/outro regions), same subtitle
overlay style, same auto-hiding chrome, and the same multi-part advance logic. It
runs identically on Android, iOS and web.
Simple usage — pass the subtitle URL as a prop
<MegaVideoPlayer
uri="https://…/part-0.mp4" // or the proxy URL from /native or /web
title="Episode 1"
subtitleUrl="https://cdn.example.com/subs.srt" // <-- the subtitle file
subtitleFormat="srt" // optional (auto-detected from URL/extension/content)
subtitleLang="English" // language name shown in the player controls
autoPlay
/>Pass subtitleLang to control the language name the player displays in the
controls (the CC/language chip next to the progress bar). Without it the player
shows SRT.
Selecting subtitles from the list
The player shows a subtitle selector in the controls (same UI as MyMovies):
- The
CCbutton toggles subtitles on/off. - The language chip (e.g.
ENG) opens the subtitle menu — a dropdown with two tabs: Language (the subtitle list) and Text Size.
<MegaVideoPlayer
uri={url}
subtitleTracks={[
{ lang: "English", url: "https://cdn.example.com/subs.en.srt" },
{ lang: "Español", url: "https://cdn.example.com/subs.es.srt" },
{ lang: "Français", url: "https://mega.nz/file/xyz#key", format: "srt" },
]}
initialSubtitleLang="English"
autoPlay
/>- The Language tab lists every subtitle; tapping one selects it, marks it with a
checkmark, loads it, and starts showing it (with a single
subtitleUrlthe list shows that one track, selectable too). - The Text Size tab drags a 10–34px slider for the overlay font.
- Requires
initialSubtitleLang/subtitleTracks/subtitleUrl— see prop list below. - All these controls are the exact
PlayerControlsused in the MyMovies player.
subtitleUrl can be:
- a MEGA link (
https://mega.nz/file/…) — decrypted through the same pipeline as the video, so no CORS/extension issues; - a plain http(s) URL — the host must allow CORS;
- a local file URI (
file:,content:, or/) — provide a file reader with thereadLocalFileoption onloadSubtitles(see Subtitles section).
Format auto-detection order: subtitleFormat prop → URL extension → content sniffing
(.srt/.ass/.ssa/.vtt, plus ASS detected even from extension-less URLs).
Multi-language tracks
<MegaVideoPlayer
uri={url}
subtitleTracks={[
{ lang: "English", url: "https://cdn.example.com/subs.srt" },
{ lang: "Français", url: "https://mega.nz/file/xyz#key", format: "srt" },
]}
initialSubtitleLang="English"
subtitleOffset={0} // seconds to shift cues (can be negative)
autoPlay
onEnd={() => goToNextEpisode()}
onProgress={({ currentTime, duration }) => {}}
onError={(message) => {}}
/>Full prop list
MegaVideoPlayer props:
uri— playable URL (first part). Optional if onlysourcesis given.sources— ordered part URLs; the player auto-advances when a part ends (sequence advance, not concatenation).title— shown in the top control bar.subtitleUrl+subtitleFormat— single subtitle track (the common case).subtitleLang— language name shown in the controls for the singlesubtitleUrltrack (e.g."English"). Defaults to"SRT".subtitleTracks—{ lang, url, format? }[]for multi-language; the player shows the subtitle list in the controls and selecting one switches tracks (same behavior as MyMovies).initialSubtitleLang— preselected track language on load.subtitleOffset— seconds to shift cue timestamps (negative = earlier).autoPlay— auto-start (defaulttrue).onEnd— fired when the last part finishes.onProgress({ currentTime, duration }),onError(message),onFullscreenChange(boolean)— callbacks.
All subtitles render through SubtitleDisplay (dark pill, white text, centered,
resizable 10–34px via the controls' Text Size slider).
Subtitles
import { loadSubtitles, parseSubtitles, detectSubtitleFormat } from "mega-stream-proxy/subtitles";
const cues = await loadSubtitles("https://mega.nz/file/xyz#key");
// MEGA links are decrypted through the same pipeline as video (no CORS issues).
// Plain http(s) hosts must allow CORS.
const cues = await loadSubtitles("/data/sub/1.srt", {
readLocalFile: (uri) => FileSystem.readAsStringAsync(uri, { encoding: FileSystem.EncodingType.UTF8 }),
});- Supported formats: SRT, ASS/SSA, WebVTT. Extension-less URLs (MEGA links) are sniffed by content.
parseSubtitles(text, format?)is import-pure and tree-shakable — batch-process files anywhere.
Accounts
import { MegaAccountManager } from "mega-stream-proxy/accounts";Web — persist in localStorage (with cross-tab live updates):
import { createLocalStorageStore } from "mega-stream-proxy/accounts";
const accounts = new MegaAccountManager(createLocalStorageStore());
await accounts.addAccount({ email: "[email protected]", password: "…", label: "Work" });
const list = await accounts.listAccounts(); // MegaAccount[]
const tree = await accounts.loadTree(list[0]); // MegaTreeNode[] (folders, videos, subs)
const link = await accounts.exportLink(list[0], tree.find(n => n.isVideo).handle);
await accounts.deleteAccount(list[0].id);React Native — persist in AsyncStorage:
import { createAsyncStorageStore } from "mega-stream-proxy/accounts";
import AsyncStorage from "@react-native-async-storage/async-storage";
const accounts = new MegaAccountManager(createAsyncStorageStore(AsyncStorage));Bring your own backend (Firestore, your server…):
import type { MegaAccountStore, MegaAccount } from "mega-stream-proxy/accounts";
const myStore: MegaAccountStore = {
list: () => db.collection("accounts").get().then(q => q.docs.map(d => d.data())),
save: (a: MegaAccount) => db.doc(a.id).set(a),
remove: (id) => db.doc(id).delete(),
subscribe: (cb) => unsubFirestore(cb),
};
const accounts = new MegaAccountManager(myStore);Other helpers: testAccount() returns { name, folders, files } (login check),
subscribe(cb) streams account changes, closeSession(id) / closeAll() free sessions,
MegaAccount — you can enrich stores (encrypt passwords, etc.) before saving.
Export map
| Subpath | Contents |
| ------------------ | --------------------------------------------------------------- |
| mega-stream-proxy | core: loadMegaFile, collectMegaFileStream, parseRange, rangeHeaders, SW_MARKER |
| mega-stream-proxy/native | ensureProxy, createStream, createStreams, releaseStream, stopProxy (TCP, keyed sessions on a singleton server) |
| mega-stream-proxy/web | same API, Service-Worker backed (Blob fallback) |
| mega-stream-proxy/sw | copySwAssets(outDir), swAssetPaths() (deploy-time node helper) |
| mega-stream-proxy/player | MegaVideoPlayer, PlayerControls, SubtitleDisplay |
| mega-stream-proxy/subtitles| loadSubtitles, parseSubtitles, detectSubtitleFormat, types |
| mega-stream-proxy/accounts| MegaAccountManager, createLocalStorageStore, createAsyncStorageStore, types |
Android build
The package includes android/build.gradle (com.android.library, namespace
com.megastreamproxy) so RN autolinking resolves it without touching your Gradle
config. All native streaming is performed by react-native-tcp-socket, which is a
declared dependency and is auto-linked too. Build as usual:
npx expo run:android # Expo apps
./gradlew assembleDebug # bare RN appsDevelopment
npm install # installs everything (incl. dev deps for typechecking the player)
npm run build # tsc -> dist/ + copies sw assets
npm run test # build + node:test suite
npm pack # -> mega-stream-proxy-0.1.0.tgzNode 18+. TypeScript strict. ESM ("type": "module").
License
MIT — see LICENSE.
