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.

one word of this page has changed meaning

Remux-free is still true; “passthrough” no longer is. Nothing is ever re-encoded — but with local origin switched on, the engine stops forwarding the provider's playlist and publishes one it wrote itself, from segments it ingested, decrypted, normalized and cached in RAM. Everything on this page describes the rewriting proxy — the default, and still the whole story with originEnabled off. Where origin mode changes an answer below, it is called out inline.

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) can't be welded, so it falls back to HLS. AES-128 no longer does — the engine decrypts it in-process (RustCrypto, no OpenSSL, still no ffmpeg), so an encrypted source concatenates like any other. Only fMP4 and SAMPLE-AES / FairPlay still decline. See Local origin → what declines.

● 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
Upstream per viewerOne fetch per viewer on this page's rewriting path — or one shared ingest for all of them with originEnabled on, whichever shape is served.
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 tooMPEG-TS, incl. AES-128 (fMP4 / SAMPLE-AES fall back to HLS; a demuxed source is woven into one program under origin)
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 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). AES-128 is decrypted in-engine; only fMP4 and SAMPLE-AES fall back to HLS. A demuxed source needs more than concatenation — origin mode interleaves the pair into one program rather than declining.

● local origin

Republish from RAM

Opt-in per playlist. One shared ingest per channel fills a RAM ring — whose entries carry both lanes on a demuxed source — and both output shapes are rendered from it, so a second viewer costs no extra upstream. See Local origin.

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 → Video Config) 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).
originEnabledliveLocal origin: republish from our own ring instead of proxying the upstream playlist. Default off — off is today's output byte-for-byte.
originRingMblivePer-channel ring cap in MiB (default 25, range 1–4096); a 3-segment floor still wins over it, and on a demuxed source both lanes count. Applies only while originEnabled is on.
spliceNormalizeliveSplice normalization — “Smooth ad transitions” in the UI. Republishes every ingested segment onto one timeline with canonical pids, so a provider that moves its video pid between ads cannot make a demuxer stop rendering. A kill switch, not an opt-in: default on, and an older config document that lacks the field reads as on. Applies only while originEnabled is on.
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 The other topology switch · originEnabled

Rewriting proxy, or local origin

Everything above describes a rewriting proxy: the upstream playlist is fetched, its URIs are rewritten to point back through masqueradarr, and the rest is passed through. That hides hostnames — but the client is still looking at the provider's timeline: their media sequence, their #EXT-X-KEY, even their vendor tags. And every viewer of a channel is a separate trip upstream.

● default · originEnabled off

Forward their playlist

One upstream fetch per viewer. The player follows the provider's timeline through us. This is the path the whole page above describes, and it is unchanged.

● opt-in · originEnabled on

Publish our own

One ingest per channel decrypts and normalizes segments into a RAM ring, and both output shapes are rendered from it. A second viewer costs no extra upstream, and the manifest is one we wrote.

it is a per-playlist switch, not a mode

Both paths live in the same binary and the choice is made per request, from the playlist's proxy config. You can run one exported playlist as an origin and leave every other playlist — and the in-app player — on the rewriting path. The full mechanism, the ring, the two renderers and the new iop/oop observability are on the Local origin page.

08 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). There is still no ffmpeg and no remux / transcode — but that no longer implies what it used to. AES-128 is decrypted in-engine, and a bare MPEG-TS socket is now segmented in-engine (PAT → PMT → cut at a random-access point, durations from the stream's own 90 kHz PCR clock), so a direct / hdhomerun upstream can be republished as ordinary HLS — with originEnabled on. That path is opt-in and newer than the rest of the engine; the honest caveat is that it has had far less live mileage than the HLS proxy path. fMP4 and SAMPLE-AES / FairPlay genuinely still decline — though a declining channel now drops back to the rewriting path rather than failing. 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 · proxy.rs · tsmux.rs · origin.rs · tsnorm.rs · tsweave.rs · tsseg.rs .claude/artifacts · HLS vs Raw TS explainer next → Local origin
masqueradarr · video proxy engine same video · different packaging