masqueradarr
video proxy engine · 07 how bytes actually move

The remux-free Rust data plane

The old always-on ffmpeg engine is gone, replaced by a durable Rust sidecar (masq-proxy) that resolves each stream on demand and pipes it — with retry, mirror rotation, failover groups and read-ahead buffering — to the player. Node stays the brains; Rust moves the muscle.

01 Two planes

Node control plane · Rust data plane

Video is split across two processes that ship in the same container. Everything stateful and provider-specific stays in TypeScript; the byte work is a small standalone Rust binary driven per-stream by a "grant".

the split the brains · the muscle
Node — control plane · the brains
Per-source auth & scraping
dulo's Supabase session + device fingerprint; dlhd's rotating-mirror 3-hop resolve
The SSRF allow-set + stream-token gate
who's allowed to reach what
Telemetry authority + config storage
streamState · ViewSession · the WS; proxyconfigs
Rust — data plane · masq-proxy
Fetch upstream · follow redirects
fast, multi-threaded, near-zero-copy
Rewrite .m3u8 manifests
every child URL routes back through the proxy
Pipe segments
counted, buffered, stall-guarded — never re-derives provider logic
crash-tolerant by design

Node spawns and supervises masq-proxy as a child (auto-restart with backoff). A missing or crashed sidecar is non-fatal — the app keeps managing playlists / EPG / channels / users and serving M3U / XMLTV downloads; only live playback pauses until it's back.

02 The internal seams

One private loopback channel, four jobs

Node and Rust talk over one loopback channel — POST /api/internal/*, guarded by a shared x-masq-secret (the SPA never calls it; only the Rust engine does).

/api/internal/* · shared-secret seams
resolve → grant
Rust asks Node to resolve a stream; Node runs the adapter and returns a per-stream grant (masterUrl, upstreamHeaders, allowHosts, relabel, resolved proxyConfig) that Rust replays for the whole stream. The request carries an attempt number — 0 is the channel itself, N is its Nth failover child — and the grant names the serving candidate's policySource. A 410 failover_exhausted ends the walk.
telemetry · batched
Rust measures the true byte edge and reports batched viewer / byte / phase / close events; Node stays the telemetry authority.
log · batched
Rust ships level-gated, request-tagged engine logs into the dedicated proxy log category — the same "View logs" drawer.
authorize · edge only
The per-request stream-token check when Rust owns the public socket (edge mode, below).

The telemetry + log responses both echo the current log level, so changing verbosity on the Settings screen reaches the sidecar within one flush — no restart.

03 How a stream request flows

Gate → relay → resolve → fetch → bytes

a single stream request player → gate → rust → upstream
1
Player → /api/(ext/)v1/<source>/<enc-url>
Request a channel
Carries the per-user ?token= and the ?pl= playlist id. /api/v1 = in-app; /api/ext/v1 = external clients (the mount the composed M3U emits).
2
streamGate
Check access first
Valid token? enabled? (for a non-admin) is this source in the user's allow-list? denials are plain text so a media player surfaces them
3
proxyRelay → 127.0.0.1:8787
Relay to the sidecar
On allow, Node relays to Rust and injects the client identity it can see (IP, UA, username) plus the shared secret.
4
Rust · ENTRY or HOP?
Resolve once, reuse after
ENTRY → resolve seam → grant + masterUrl HOP → reuse cached policy The first request resolves; child requests (variant playlists, segments, keys) replay the grant.
5
fetch upstream
Fetch with durability
Retry 502/503/504 with bounded backoff; a dead master forces a fresh resolve → mirror rotation. still won't establish → walk the channel's failover children, attempt=1,2,… until one answers or the seam replies 410 failover_exhausted
6
manifest or segment?
Rewrite or pipe
manifest → rewrite child URIs, re-embed token + pl, grow the SSRF allow-set segment → relabel + pipe through a bounded read-ahead buffer
→ Player
Bytes out
Nothing upstream is ever exposed to the player directly.
04 HLS vs. raw TS

Same video, different packaging

The engine can hand a stream to a player two ways. Same bytes underneath — different packaging. HLS is the universal default; raw TS is an opt-in for players that want one continuous pipe.

● HLS · the default

A list of short clips

The server hands the player a menu of tiny clips; the player fetches them one at a time, re-checking the menu every few seconds. The player drives timing and can adapt quality.

● Raw TS · opt-in

One continuous pour

The server opens one never-ending video/mp2t pipe and pours the video down it — segments welded by #EXT-X-MEDIA-SEQUENCE, no remux. The player just receives. Like tuning to a channel.

signal flow · player ← proxy ← cdn
Playeryour app / TV
Proxymasqueradarr
CDNsource
HLSChunks arrive in bursts — pull a clip, pause, ask again. Many small requests.
Playeryour app / TV
Proxymasqueradarr
CDNsource
Raw TSOne unbroken flow — the proxy follows the source and keeps pushing bytes. One request, ever.
mpeg-ts · self-framing packets welded across a segment boundary
188B
188B
188B
seg 42 │ seg 43
188B
188B
188B
seg 43 │ seg 44
188B
188B
188B
why no ffmpeg is needed

MPEG-TS is a river of self-describing 188-byte packets — you can glue two chunks end-to-end and it still plays. That's the property that lets masqueradarr build the raw-TS stream by pure concatenation: no re-encoding, no ffmpeg. fMP4 (#EXT-X-MAP) and AES-encrypted sources can't be welded, so they auto-fall-back to HLS.

● HLS● Raw TS
ShapeA playlist + many short segmentsOne continuous video/mp2t stream
Who drives timingThe player pulls each clipThe server pushes the stream
Requests after startOne per segment, foreverExactly zero — one open socket
Adaptive bitrateYes — player picks a qualityNo — server fixes the top variant
Which mount/api/v1 and /api/ext/v1/api/ext/v1 only
Telemetry modelPoll-recency (each poll = heartbeat)Socket (open · sbytes · close)
Upstream limitsHandles fMP4 & AES tooPure MPEG-TS only (else falls back to HLS)
Best forBrowsers, the in-app player, modern appsSet-top / legacy clients that want one URL
05 Durability

Keeping a stream alive on flaky upstreams

● retry

Transient failures

Transport errors and 502/503/504 retry with bounded backoff; definitive 4xx/5xx are forwarded verbatim (unless failoverOnDefiniteError routes them into the walk).

● mirror rotation

Rotate within one provider

A dead resolved master forces a fresh resolve, driving dlhd / dami to re-probe and rotate to a live mirror without dropping the viewer. Same channel, same provider.

● failover groups

Fall through to a backup

When a channel has configured backups and still won't establish, the engine walks its ordered children (attempt=1,2,… against the resolve seam) and serves the first live one under the parent's identity, then sticks to it for the session. Backups may be on other providers. See Playlist Failover.

● stall detection

Clean truncation

An idle read timeout (readTimeoutMs) turns a silent upstream into a clean end instead of a hang.

● read-ahead buffer

Smooth jitter

A bounded in-memory buffer (bufferSizeKb) smooths jitter and fixes the chunked / no-Content-Length byte undercount that used to fake buffering.

● batched telemetry

Off the hot path

Events are coalesced and posted off the byte path, so reporting never blocks the stream.

● raw MPEG-TS

Flat TS pipe

With outputFormat: 'ts', the external mount serves one continuous video/mp2t (no remux); fMP4 / AES auto-fall back to HLS.

06 Tuning knobs · proxyconfigs

Two tiers, doc-level fallback

The engine's knobs live in the proxyconfigs collection, edited in the UI and resolved by Node into each grant — Rust never reads MongoDB. A per-install (Default) (_id:'app', Settings → Advanced) and an optional per-playlist (Custom) (_id:'app_<playlistId>', the playlist drawer) that fully replaces the Default for that playlist.

KnobStatusEffect
headerOverridesliveExtra upstream headers, merged over the adapter's (operator wins).
connectTimeoutMs, maxRedirectslivePer-config upstream HTTP client (cached in Rust).
readTimeoutMs, bufferSizeKblivePer-stream stall timeout + read-ahead buffer size.
outputFormat (hls | ts)liveDistribution shape (ts = continuous MPEG-TS, external mount only).
streamInfReduxliveOpt-in (default off) non-destructive reorder of the HLS master so the first #EXT-X-STREAM-INF lands inside a strict player's manifest probe window. External mount only; off = byte-identical output.
failoverEnabledliveWalk a channel's ordered failover children on an establish failure. Default on — configuring a group is the real opt-in.
failoverOnDefiniteErrorliveAlso treat a definitive upstream 4xx/5xx as a failover trigger. Default off — it changes forward-verbatim semantics.
segmentCacheTtlSecreservedShipped in the grant, not yet enforced.
STREAM-INF Redux, in one line

Some players (VLC among them) only peek at the first few kilobytes of an HLS master before deciding it isn't video. When a source pads its master with long comments or many audio renditions, the first #EXT-X-STREAM-INF can fall outside that window. streamInfRedux reorders the master — moving the marker in, changing nothing else and deleting nothing — so a strict player recognizes the stream.

07 Public edge mode · MASQ_EDGE

Take Node's event loop out of the byte path

By default Node is the public front door and Rust a loopback sidecar. MASQ_EDGE=1 inverts the topology: Rust binds the public port and serves streams in-process, reverse-proxying everything else (SPA, /api/*, downloads, WebSockets) back to Node on a loopback port. Same public port and DOMAIN; fully reversible.

default · MASQ_EDGE off Node is the front door
clients
Clients
public :3000
all traffic
front door
Node — public front door
0.0.0.0:3000 · SPA · /api/* · WS · gate · relay
relay bytes · seams
sidecar
masq-proxy
127.0.0.1:8787 · engine
inverted · MASQ_EDGE=1 Rust is the front door
clients
Clients
public :3000
all traffic
public edge
masq-proxy — public edge
0.0.0.0:3000 · streams in-process · token-gated
reverse-proxy · authorize (30s TTL)
internal
Node — internal
127.0.0.1:8080 · control plane
when to turn it on

Flip it for many concurrent or high-bitrate viewers (the main reason), to keep the management UI snappy under streaming load, or on constrained hardware (Pi / small VPS). Leave it off for a personal setup — the default sidecar path is simpler, keeps strictly per-request token revocation, and has a smaller blast radius. In edge mode the token gate becomes a per-request check against a Rust auth cache (revocation within a 30-second TTL), and the edge synthesizes client identity server-side, ignoring inbound x-masq-*.

honest scope

Playback is live over the Rust engine (HLS + raw-TS). The part still pending is remux / transcode (e.g. HDHomeRun TS→HLS) — there is no ffmpeg, which is exactly why fMP4/AES fall back to HLS and HDHomeRun playback is dormant. The ?fmt=ts per-request override and the HDHomeRun-tuner / Xtream-panel consumers are on the roadmap; segmentCacheTtlSec ships in the grant but isn't enforced yet. Edge mode is build- and unit-verified; a full live-stack E2E pass is still pending — treat it as an opt-in scale topology.

README · Video Proxy Engine proxy/ crate · masq-proxy · tsmux.rs · proxy.rs .claude/artifacts · HLS vs Raw TS explainer next → Users & access
masqueradarr · video proxy engine same video · different packaging