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.
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.
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".
streamState · ViewSession · the WS; proxyconfigs.m3u8 manifestsNode 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.
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).
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.proxy log category — the same "View logs" drawer.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.
Gate → relay → resolve → fetch → bytes
?token= and the ?pl= playlist id. /api/v1 = in-app; /api/ext/v1 = external clients (the mount the composed M3U emits).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_exhaustedSame 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.
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.
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.
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 | |
|---|---|---|
| Shape | A playlist + many short segments | One continuous video/mp2t stream |
| Who drives timing | The player pulls each clip | The server pushes the stream |
| Requests after start | One per segment, forever | Exactly zero — one open socket |
| Upstream per viewer | One 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 bitrate | Yes — player picks a quality | No — server fixes the top variant |
| Which mount | /api/v1 and /api/ext/v1 | /api/ext/v1 only |
| Telemetry model | Poll-recency (each poll = heartbeat) | Socket (open · sbytes · close) |
| Upstream limits | Handles fMP4 & AES too | MPEG-TS, incl. AES-128 (fMP4 / SAMPLE-AES fall back to HLS; a demuxed source is woven into one program under origin) |
| Best for | Browsers, the in-app player, modern apps | Set-top / legacy clients that want one URL |
Keeping a stream alive on flaky upstreams
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).
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.
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.
Clean truncation
An idle read timeout (readTimeoutMs) turns a silent upstream into a clean end instead of a hang.
Smooth jitter
A bounded in-memory buffer (bufferSizeKb) smooths jitter and fixes the chunked / no-Content-Length byte undercount that used to fake buffering.
Off the hot path
Events are coalesced and posted off the byte path, so reporting never blocks the stream.
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.
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.
proxyconfigsTwo 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.
| Knob | Status | Effect |
|---|---|---|
| headerOverrides | live | Extra upstream headers, merged over the adapter's (operator wins). |
| connectTimeoutMs, maxRedirects | live | Per-config upstream HTTP client (cached in Rust). |
| readTimeoutMs, bufferSizeKb | live | Per-stream stall timeout + read-ahead buffer size. |
outputFormat (hls | ts) | live | Distribution shape (ts = continuous MPEG-TS, external mount only). |
| originEnabled | live | Local origin: republish from our own ring instead of proxying the upstream playlist. Default off — off is today's output byte-for-byte. |
| originRingMb | live | Per-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. |
| spliceNormalize | live | Splice 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. |
| streamInfRedux | live | Opt-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. |
| failoverEnabled | live | Walk a channel's ordered failover children on an establish failure. Default on — configuring a group is the real opt-in. |
| failoverOnDefiniteError | live | Also treat a definitive upstream 4xx/5xx as a failover trigger. Default off — it changes forward-verbatim semantics. |
| segmentCacheTtlSec | reserved | Shipped in the grant, not yet enforced. |
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.
originEnabledRewriting 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.
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.
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.
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.
MASQ_EDGETake 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.
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-*.
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.
masq-proxy · proxy.rs · tsmux.rs · origin.rs · tsnorm.rs · tsweave.rs · tsseg.rs
.claude/artifacts · HLS vs Raw TS explainer
next → Local origin