What is QuicSRT? | The Library | Features | Getting Started | Build Instructions | Applications | Architecture | License
NOTE: NOT PRODUCTION READY. Experimental: the protocol works, streams play, and NAK/retransmit recovers from packet loss — but it has not been hardened, audited, or tested at scale.
QuicSRT is a Rust + WebAssembly library that implements SRT over WebTransport
datagrams — the same wire format, HSv5 handshake, NAK/ACK/retransmit
semantics, and TSBPD latency management as SRT/UDP, running natively on the
server and inside the browser via WASM. JS is glue only: the browser runs the
same srt-protocol and mpeg2ts Rust crates the gateway does, compiled to
WASM.
It bridges native SRT sources (OBS or any SRT sender) to browsers — and both directions: browsers can publish too, and a browser can even view one stream while publishing another.
| SRT | The real protocol: same wire format, HSv5 handshake, NAK/ACK retransmit, TSBPD |
| Rust | Native server and browser WASM run the same srt-protocol state machines |
| Transport | WebTransport (HTTP/3) datagrams carry the SRT packets — TLS and connection migration included |
┌─────── Rust gateway ───────────────┐
[OBS] --SRT/UDP─▶│ srt-tokio listener (ingest) │ ┌── Browser (viewer) ──────────────┐
│ ↓ (Instant, Bytes) │ │ JS: WebTransport datagram I/O │
│ broadcaster (broadcast channel) │ │ ↓ bytes │
[Browser] ─WT───▶│ SrtInitiator (ingest, publish) │──WT──▶│ WASM: srt-protocol::receiver │
(publisher) │ ↓ (Instant, Bytes) │ │ ↓ (Instant, Bytes) messages │
│ ↓ │ │ WASM: mpeg2ts demux │
│ srt-protocol::sender (per viewer) │ │ ↓ PES / NAL / Opus / PCM │
│ ↓ SRT packets (bytes) │ │ JS: WebCodecs decode + render │
│ wtransport datagram driver │ └──────────────────────────────────┘
│ ↑ ACK/NAK (bytes) │
└────────────────────────────────────┘
This repository also ships applications built on the library — a native gateway binary (desktop GUI or headless CLI) and web viewer/publisher pages. See Applications.
Six artifacts: two native crates, three WASM crates, and a TypeScript player SDK.
| Crate / package | Target | What it provides |
|---|---|---|
quicsrt |
native (tokio) | Gateway builder + run loop, Broadcaster fanout, SrtInitiator, Ingester trait (SRT / file / channel), StreamRegistry, SessionPolicy hooks, GatewayLimits, Cert / CertSource |
srt-wasm |
wasm32 | SrtReceiver — the browser-side SRT listener (recv and send), driven by handle_datagram() / poll() / sendMessage() |
mpeg2ts-wasm |
wasm32 | TsDemuxer — TS demux emitting PES / PCM / PMT events, plus DebugSnapshot stream analysis (CC errors, PCR jitter, NAL breakdown) |
ts-muxer |
native + wasm32 (via the ts-muxer-wasm shim) |
TsMuxer — MPEG-TS muxing logic (H.264/HEVC/AV1 video, Opus audio, raw PCM → 188-byte TS packets) |
ts-muxer-wasm |
wasm32 | wasm-bindgen shim over the native ts-muxer crate — the browser binding for publisher-side TS muxing |
web/ player SDK |
TypeScript | mountPlayer() — framework-agnostic viewer SDK; see docs/embedding.md |
[dependencies]
quicsrt = { git = "https://github.com/maxolgi/QuicSRT-rust.git", branch = "master" }quicsrt builds on two patched forks, wired via [patch.crates-io]:
maxolgi/srt-rs (12 patches: WASM
compat, skip-induction handshake, stats population, buffer-underflow fixes,
RTT seeding, CC-aware retransmit — all documented in AGENTS.md, drafted for
upstream in PR.md) and maxolgi/mpeg2ts
(1 patch: unknown-PID tolerance for mid-stream joins).
Note: cargo only honors [patch] sections from the workspace root, so a
git dependency does not carry QuicSRT's patches with it — mirror them in your
own root Cargo.toml:
[patch.crates-io]
srt-protocol = { git = "https://github.com/maxolgi/srt-rs.git", branch = "main" }
srt-tokio = { git = "https://github.com/maxolgi/srt-rs.git", branch = "main" }
mpeg2ts = { git = "https://github.com/maxolgi/mpeg2ts.git", branch = "master" }Embeddable gateway in ~30 lines:
use quicsrt::cert::{Cert, CertSource};
use quicsrt::hooks::{auth_token_policy, chain, path_policy};
use quicsrt::ingest::srt::SrtIngester;
use quicsrt::limits::GatewayLimits;
use quicsrt::Gateway;
# async fn run() -> anyhow::Result<()> {
let cert = Cert::build(CertSource::SelfSigned {
sans: vec!["localhost".into()],
}).await?;
let gateway = Gateway::builder()
.bind_addr("127.0.0.1:4433".parse::<std::net::SocketAddr>()?)
.identity(cert.identity.clone_identity())
.latency_ms(1000)
.max_viewers(16)
// Pluggable accept policy: chain built-ins (path / origin allowlist /
// auth token) or implement SessionPolicy for per-stream tokens, JWTs,
// rate limits… Replaces the built-in checks when set.
.session_policy(chain(
path_policy("/wt".into()),
auth_token_policy("s3cret".into()),
))
// Per-IP (IPv6 collapsed to /64) + global session caps, timeouts.
.limits(GatewayLimits::builder().max_sessions_per_ip(5).build()?)
.build()?;
// Live stats for your own metrics endpoint (cheap, no I/O):
let _stats = gateway.stats_handle();
// Deferred ingester: connect OBS in the background
let source = gateway.source_handle();
tokio::spawn(async move {
let ingester = SrtIngester::bind(
"0.0.0.0:9000",
None,
std::time::Duration::from_millis(120),
None,
).await.unwrap();
source.publish_stream("default", ingester);
});
gateway.run(async {
let _ = tokio::signal::ctrl_c().await;
}).await?;
# Ok(())
# }Ingester is the input abstraction: implement next_message() returning
(Instant, Bytes) TS messages and the library handles fanout, per-viewer SRT
senders, retransmit, and backpressure. Built-in: SrtIngester (OBS, listener
or caller, with reconnect), SrtListenerService (multi-publisher SRT accept
loop), FileIngester (fixture loop with real-time pacing), ChannelIngester
(mpsc-backed — this is what browser publishers feed).
The WASM crates are not published; consume them by cloning this repo and
running wasm-pack build --target web (see
Build Instructions), then importing the generated
pkg output. The browser is the SRT listener: it feeds raw WebTransport
datagrams into the state machine and executes the returned actions.
import init, { SrtReceiver } from './wasm/srt-wasm/srt_wasm.js';
await init(); // wasm-pack --target web output
const epoch = performance.now();
const nowUs = () => (performance.now() - epoch) * 1000;
// TSBPD latency 120 ms; RTT seeded from QUIC's smoothed RTT
// (await wt.getStats()).smoothedRtt — accurate cold-start retransmit timing.
const rx = SrtReceiver.newWithLatencyAndRtt(120, quicSmoothedRttMs);
// Incoming WebTransport datagram → SRT state machine → actions
function onDatagram(bytes) {
for (const a of rx.handle_datagram(bytes, nowUs())) dispatch(a);
}
// ~5 ms tick advances timers (TSBPD release, ACK, keepalive)
setInterval(() => { for (const a of rx.poll(nowUs())) dispatch(a); }, 5);
function dispatch(a) {
switch (a.kind) {
case 0: wtDatagramWriter.write(a.takeData()); break; // ACK / NAK / handshake
case 1: onTsMessage(a.takeData(), a.schedUs); break; // TSBPD-released TS message
case 2: onHandshakeComplete(); break; // data plane is live
case 4: onClose(); break;
}
a.free(); // wasm-bindgen structs are freed manually
}Demux and mux are equally small surfaces:
import { TsDemuxer } from './wasm/mpeg2ts-wasm/mpeg2ts_wasm.js';
const demux = new TsDemuxer();
for (const ev of demux.feed(tsBytes)) { /* PES / PCM / PMT / RA events */ }
import { TsMuxer } from './wasm/ts-muxer-wasm/ts_muxer_wasm.js';
const muxer = new TsMuxer(); // publisher side
muxer.push_video(nalBytes, ptsUs, dtsUs, isKeyframe);
muxer.push_pcm(f32Samples, ptsUs); // SMPTE 302M audioFor embedding the browser-side player (canvas + WebCodecs SDK, including
the direct pcmPort PCM delivery API), see
docs/embedding.md — a guide covering the mountPlayer()
SDK, config options, and event surface.
The sim-loss feature enables a probabilistic datagram dropper for testing
NAK/retransmit (data packets only; control packets always pass). Without the
feature, the rand dependency is excluded.
[dependencies]
quicsrt = { git = "https://github.com/maxolgi/QuicSRT-rust.git", branch = "master", features = ["sim-loss"] }- Dumb SRT repeater. The gateway terminates OBS's SRT connection, takes
the resulting
(Instant, Bytes)messages, and re-originates them as a new SRT sender to each browser. TS bytes are never modified server-side. - Browser publishing. A browser can publish upstream via WebTransport.
Published streams are fanned out to viewers exactly like OBS streams —
same broadcaster, same per-viewer SRT sender. The publisher muxes TS
locally (
ts-muxer-wasm), sends SRT over WT datagrams to the gateway. - Each browser gets its own SRT sender instance (independent seq numbers,
independent retransmit buffer) via
tokio::sync::broadcastfanout. - Browser runs the same Rust state machines the gateway does, compiled to WASM.
- First-wins stream occupancy. A live stream name cannot be replaced by another publisher (browser or SRT); challengers are rejected with 403.
- SRT crypto disabled between gateway and browser. WebTransport TLS
replaces it. The OBS-to-gateway link supports optional AES encryption via
--srt-passphrase. - Multi-codec support. H.264 (avcC + SPS parsing), HEVC/H.265 (hvcC +
VPS/SPS/PPS), and AV1 (OBU Sequence Header parsing) video; Opus, AAC/ADTS,
and uncompressed PCM (SMPTE 302M) audio. The browser publisher supports
H.264 and AV1 encode via WebCodecs
VideoEncoder, plus Opus and raw-PCM audio. - PCM / SMPTE 302M audio. AES3 (s302m) PES payloads are decoded to f32
samples inside the WASM demuxer and delivered as per-PID
pcmmessages tagged with channel count, PTS, and the SRT TSBPD release deadline. Audio-only streams (no video PID) and multi-PID stacks (broadcaster-style N×stereo) work in both directions. Embedders can receive raw PCM on a transferredMessagePort(pcmPortoption) — samples flow worker → consumer directly, bypassing the main-thread hop. - Web Worker architecture. The SRT receiver and TS demuxer run in a Web
Worker off the main thread. The poll loop is adaptive: it sleeps until the
protocol's next timed event (SRT
WaitForData), clamped to sane bounds, with a dedicated timer worker for fine-grained ticks. Only WebCodecs decode and canvas rendering happen on the main thread. - PTS-paced video presentation. Decoded frames are queued in a small
bounded ring and drawn on
requestAnimationFramewhen their PTS is due, measured against a wall-clock ↔ PTS mapping that resets on large gaps (seek, stream restart, backgrounded tab). SRT's TSBPD paces delivery; PTS pacing absorbs downstream bursts (decoder reorder, worker→main batching) so the canvas updates at the source frame rate. Opt out via the player SDK'ssetRenderPacing(false)to fall back to "latest frame only" skip-ahead. - Bounded audio buffering. The AudioWorklet paths use fixed-size ring buffers with drop-oldest and skip-ahead to prevent latency accumulation.
| The Library | Applications quickstart | Embedding the player |
| Crate overview + Rust and JS examples | Run the gateway app with OBS or the test fixture | mountPlayer() SDK: options, events, pcmPort |
| Build Instructions | Architecture | SRT-over-QUIC draft |
Toolchain setup, ./build.sh menu |
Data flow, browser pipeline | The IETF draft QuicSRT implements |
- Rust stable (>=1.75), with
wasm32-unknown-unknowntarget andwasm-pack - Node.js >=18 (build-time only — bundles the web UI that the gateway serves)
- ffmpeg (only for the live publisher scripts in
fixtures/) - System C/C++ build tools:
build-essential/cmake/pkg-config(Linux/macOS), or Visual Studio with the "Desktop development with C++" workload (Windows; supplies the MSVC compiler + Windows SDK thatring/aws-lc-syscompile viacmake). Thex86_64-pc-windows-msvcRust toolchain (the default on Windows) auto-discovers it — no manual env setup.
./install-prereqs.shDetects what's already installed and only installs what's missing. Run
./install-prereqs.sh --check to verify without installing. Can also be
curl'd directly on a fresh machine before cloning the repo:
curl -sSf https://raw.githubusercontent.com/maxolgi/QuicSRT-rust/master/install-prereqs.sh | bash# rustup + stable toolchain + wasm32 target + wasm-pack (user-local, ~/.cargo)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal
source "$HOME/.cargo/env"
rustup target add wasm32-unknown-unknown
curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
# Node.js >= 18, ffmpeg, and C/C++ build tools via your system package manager.
# one-time: build all 3 WASM modules, copy to web/wasm/, install web-UI deps
./build.sh setup
# one-time: bundle the web UI → web/dist/ (served by the gateway binary)
./build.sh web build./build.sh setup runs the equivalent of:
mkdir -p web/wasm/srt-wasm web/wasm/mpeg2ts-wasm web/wasm/ts-muxer-wasm
(cd crates/srt-wasm && wasm-pack build --target web --release)
cp crates/srt-wasm/pkg/* web/wasm/srt-wasm/
(cd crates/mpeg2ts-wasm && wasm-pack build --target web --release)
cp crates/mpeg2ts-wasm/pkg/* web/wasm/mpeg2ts-wasm/
(cd crates/ts-muxer-wasm && wasm-pack build --target web --release)
cp crates/ts-muxer-wasm/pkg/* web/wasm/ts-muxer-wasm/
(cd web && npm install)Run ./build.sh --help for the full menu (per-crate WASM builds, gateway,
library, web bundle, check, test, clean, etc.).
The project builds natively on Windows (verified on Rust 1.96 MSVC + VS 2026).
build.sh is bash, so either run it through Git Bash unchanged, or run the
raw commands below in PowerShell/cmd. Two Windows-specific notes:
- Visual Studio C++ tools are required — install Visual Studio (2022+) with
the "Desktop development with C++" workload. The MSVC toolchain
(
x86_64-pc-windows-msvc, Rust's Windows default) needs it forring/aws-lc-sys. Rust auto-discovers the linker; novcvarsallsetup needed. - PowerShell blocks
npm.ps1by default execution policy — usenpm.cmd(andnpx.cmd) instead ofnpm/npx.
# Prerequisites (one-time)
rustup target add wasm32-unknown-unknown
# Install wasm-pack: https://rustwasm.github.io/wasm-pack/installer/
# Install Visual Studio with the C++ workload, then:
# Build all 3 WASM crates + copy to web/wasm/
foreach ($c in 'srt-wasm','mpeg2ts-wasm','ts-muxer-wasm') {
wasm-pack build --target web --release "crates/$c"
New-Item -ItemType Directory -Force "web/wasm/$c" | Out-Null
Copy-Item "crates/$c/pkg/*" "web/wasm/$c/" -Force
}
# Web-UI deps + bundle
npm.cmd install --prefix web
npm.cmd run build --prefix web
# Gateway binary (release) → target\release\quicsrt-gateway.exe
cargo build --release -p quicsrt-gateway
.\target\release\quicsrt-gateway.exe # GUI: only the egui window, no console window
.\target\release\quicsrt-gateway.exe --no-gui # headless: reattaches to the terminal for log outputEverything else in this README (cargo run, CLI flags, cert modes, browser
flow) works identically on Windows. The only Linux-only steps are the
supervisord deployment (./build.sh restart, quicsrt.conf) and the
fixtures/stream*.sh publishers (bash+ffmpeg scripts — adapt them or run OBS
instead).
The test fixture (fixtures/test.ts, H.264+Opus, 10 s loop) is generated by
./fixtures/make-fixture.sh (gitignored — media isn't committed; run it after a
fresh clone, the smoke test needs it). fixtures/stream.sh is a live ffmpeg
publisher (NVENC/VAAPI) that streams to the gateway's SRT listener instead of
writing a file.
./build.sh setup # one-time: WASM + web-UI deps (run after fresh clone)
./build.sh wasm # rebuild all 3 WASM crates + copy to web/wasm/
./build.sh wasm srt # rebuild just srt-wasm + copy
./build.sh gateway # cargo build --release -p quicsrt-gateway
./build.sh gateway --sim-loss # add the sim-loss feature
./build.sh lib # cargo build --release -p quicsrt (the library)
./build.sh web build # bundle the web UI → web/dist/
./build.sh check # cargo check + tsc --noEmit
./build.sh test # cargo test --workspace + node web/smoke.mjs
./build.sh srt-protocol # rule 1: rebuild gateway + srt-wasm after editing forked srt-rs
./build.sh restart # sudo supervisorctl restart quicsrt (production only)
./build.sh clean # rm -rf web/wasm web/dist target
./build.sh all # full clean rebuild: clean → setup → gateway → web buildRaw form (what the script runs):
# Demo gateway binary (release, with sim-loss feature)
cargo build --release -p quicsrt-gateway --features sim-loss
# Library only (for use as a dependency)
cargo build --release -p quicsrt
# WASM crates — must rebuild + copy to web/ after changes
(cd crates/srt-wasm && wasm-pack build --target web --release)
cp crates/srt-wasm/pkg/* web/wasm/srt-wasm/
(cd crates/mpeg2ts-wasm && wasm-pack build --target web --release)
cp crates/mpeg2ts-wasm/pkg/* web/wasm/mpeg2ts-wasm/
(cd crates/ts-muxer-wasm && wasm-pack build --target web --release)
cp crates/ts-muxer-wasm/pkg/* web/wasm/ts-muxer-wasm/
# Web UI bundle (debug builds of the gateway serve dist/ from disk;
# release builds embed it at compile time)
(cd web && npm run build)
# TypeScript typecheck (no emit)
cd web && npx tsc --noEmit
# Live publishers (need ffmpeg; h264 uses NVENC, av1 uses VAAPI)
./fixtures/stream.sh h264|av1
./fixtures/stream_pcm.sh [mono|stereo|surround|<channel_count>]- Forked
srt-protocol(maxolgi/srt-rs) change → run./build.sh srt-protocol(rebuilds BOTH the gateway binary AND srt-wasm + copies pkg toweb/wasm/). - Changing only
web/src/*.ts/*.tsx→ rebuild the bundle (./build.sh web build). Debugcargo runpicks it up on page reload; release builds re-embed at compile time, so rebuild the binary too. - Changing
crates/srt-wasm/src/lib.rs→./build.sh wasm srt+ browser reload. - Changing
crates/mpeg2ts-wasm/orcrates/ts-muxer-wasm/→./build.sh wasm mpeg2ts(orts-muxer) + browser reload. - Changing
crates/quicsrt/(library) →./build.sh gateway+./build.sh restart(production only; dev just reruns the binary).
The repo ships two applications built entirely on the library.
Like SRT's srt-live-transmit, they are provided for instructional purposes —
demonstrations of the crate surfaces, not ends in themselves.
quicsrt-gateway— native binary wrappingGateway: CLI parse, cert setup, ingester wiring, health endpoint. Launches a desktop GUI (eframe/egui) by default — config form (persisted to~/.config/quicsrt/gateway-config.json), Start/Stop, live stats, scrolling log panel.--no-guifor headless CLI mode (required under supervisord); auto-falls-back to CLI on headless servers. Single-binary deployment: the built-in HTTPS web server embeds the web UI at compile time (rust-embed), so the release binary serves the viewer UI with no external file server. Debug builds serveweb/dist/from disk so UI iterations don't require recompiling.web/pages — viewer page (player SDK + optional debug overlay with 8 analysis tabs), publisher page (screen capture → WebCodecs encode →ts-muxer-wasm→ SRT over WT).
cargo run -p quicsrt-gatewayThe gateway launches a GUI window by default (config form + live stats + logs).
Add --no-gui for headless mode. The gateway writes web/public/cert-hash.js
on startup (the self-signed cert DER hash for the browser).
Open https://127.0.0.1:5173 — the gateway's built-in HTTPS server serves the
web UI from the embedded bundle (or from web/dist/ on disk in debug builds).
The cert hash is auto-loaded from cert-hash.js — no manual entry needed.
Click connect.
If the page returns "web UI not built", run ./build.sh web build once and
reload.
cargo run -p quicsrt-gateway -- --input srt --srt-port 9000In OBS, add a Media Source (or your camera), then add an SRT output:
- Mode:
Call - IP:
127.0.0.1 - Port:
9000 - No passphrase needed by default. Add
--srt-passphrase <key>to enable AES encryption on the OBS leg (10–79 chars).
If OBS disconnects (crash, restart, network drop), the gateway automatically waits for a reconnection — no restart required. Existing browser viewers will auto-reconnect via the exponential-backoff logic.
cargo run -p quicsrt-gateway -- --input srt --srt-mode caller --srt-call 192.168.1.50:9000A browser can publish a stream to the gateway, which fans it out to other
browsers. The publisher encodes video (WebCodecs VideoEncoder) and audio,
muxes to MPEG-TS (ts-muxer-wasm), and sends via SRT-over-WebTransport.
Viewers connect to ?stream=<name> as usual.
The publish URL pattern is ?publish=<name> — the gateway creates a
broadcaster for that stream name, and viewers subscribe via ?stream=<name>.
A live stream name cannot be taken over by another publisher (first-wins);
challengers are rejected with 403.
cargo run -p quicsrt-gateway # listener mode, any SRT sourcePublisher connects to https://127.0.0.1:4433/wt?publish=mystream.
Viewer connects to https://127.0.0.1:5173/?stream=mystream.
The publisher-side SRT uses the same forked srt-protocol compiled to WASM
(srt-wasm). The gateway's publish path runs an SrtInitiator (SRT receiver
over WT datagrams) and releases TSBPD-paced messages to the broadcaster.
Note: Browser publishing works best with low TSBPD latency (20–120 ms). High latency (e.g., 300 ms) is fine for viewing but adds unnecessary buffering on the publish side. The gateway services publish sessions on every ticker cycle (every 2 ms) to keep the TSBPD release path responsive.
Uncompressed AES3 audio rides MPEG-TS as SMPTE 302M (s302m). The WASM
demuxer decodes s302m PES payloads to interleaved f32 samples in Rust; the
worker posts per-PID pcm messages (pid, channelCount, samples, pts,
plus the SRT TSBPD deadline and actual release time for pacing telemetry).
The reference viewer plays them via an AudioWorklet (PcmPlayer); the debug
overlay's AudioTab adds per-PID meters (peak/LUFS/phase/scope/spectrum) and
release-pacing rows.
Publishing side: ts-muxer-wasm exposes push_pcm / push_pcm_pid so a
browser (or embedder) can originate PCM audio — audio-only (no video PID) and
multi-PID (N×stereo) layouts included.
Test source (needs an ffmpeg built with --enable-gpl for the s302m encoder):
./fixtures/stream_pcm.sh # stereo two-tone sine
./fixtures/stream_pcm.sh surround # 5.1 bed on one PID
./fixtures/stream_pcm.sh 128 # 64 stereo PIDs at different frequenciescargo run -p quicsrt-gateway --features sim-loss -- --sim-loss 5 # 5% random drop of data datagrams
cargo run -p quicsrt-gateway --features sim-loss -- --sim-loss 20 # 20% — NAK/retransmit recoversOnly data packets are dropped; control packets (handshake, ACK, NAK, KeepAlive) always pass through so the SRT reliability machinery stays functional.
Open more browser tabs — each gets its own independent SRT sender. Viewer cap
defaults to 16 (enforced in Broadcaster::subscribe).
Full CLI reference, certificate-mode details (self-signed vs mkcert), and
production deployment (supervisord) live in
docs/reference-gateway.md.
OBS ──SRT/UDP──► SrtIngester ──► Broadcaster (broadcast channel, depth 32768)
▲
Browser ──WT──► SrtInitiator ────────┘ (publish path: WT dgrams → SRT receiver → ReleaseData)
(publisher) (recv_pump + ticker)
│
┌───────────┴───────────────┐
▼ ▼
BrowserSession A BrowserSession B
├── recv_pump ├── recv_pump
│ (WT dgram → SrtInitiator)│ (WT dgram → SrtInitiator)
└── ticker (shared) └── ticker (shared)
(viewer.recv → (viewer.recv →
SrtInitiator → WT dgram) SrtInitiator → WT dgram)
The gateway is a dumb SRT repeater: it terminates OBS's SRT/UDP connection (or a browser's SRT-over-WT publish session), re-originates TS bytes as a new SRT sender to each browser over WebTransport datagrams. TS bytes are never inspected server-side.
Each browser session runs a recv_pump task (drains incoming WT datagrams — ACK/NAK from browser — into the SRT initiator state machine) and is serviced by a single centralized ticker (one task drives all sessions' SRT state machines every ~2 ms, eliminating N separate timer tasks). The ticker pushes TS messages from each viewer's broadcast subscriber into the initiator and sends resulting SRT packets as WT datagrams.
main thread │ Web Worker
│
WT datagram ──────────────────────────────────────────►│ SrtReceiver (WASM)
(batched up to 16, fresh clock per datagram) │ ↓ TSBPD-paced
│ SrtAction::DeliverMessage
│ ↓ raw TS bytes
│ Demuxer (WASM, mpeg2ts)
│ ↓ PES packets / PCM
◄────────────────── postMessage ────────────────────│ (pid, pts, payload)
│ │ (s302m decoded to f32
│ │ in the worker)
├── VideoPipeline │
│ H.264 SPS parse → avcC → VideoDecoder │
│ ↓ VideoFrame │
│ CanvasRenderer (PTS-paced, rAF-driven) │
│ │
├── OpusAudioPipeline / AacAudioPipeline │
│ AudioDecoder → MediaStreamTrackGenerator │
│ or AudioWorklet (Firefox fallback) │
│ │
├── PcmPlayer (s302m) — AudioWorklet ring │
│ or pcmPort: raw PCM → embedder's MessagePort │
│ │
└── datagramWriter (ACK/NAK → WT) │
The SRT receiver and TS demuxer run in a Web Worker (worker.ts) to keep
the main thread free for decoding and rendering. The worker's tick is
adaptive: the SRT state machine reports how long until its next timed
event (WaitForData actions) and the loop sleeps exactly that long (clamped),
using a dedicated timer worker for sub-millisecond ticks — no fixed 5 ms
quantization on audio release. Incoming datagrams are batched (up to 16) but
each gets a fresh timestamp, so packets processed late in a batch don't
see a stale clock. Outgoing messages (PES packets, PCM batches, stats, logs)
coalesce into a single batched postMessage to the main thread; PCM on a
pcmPort bypasses the main thread entirely.
Video presentation: Decoded frames are queued in a small bounded ring and
drawn on requestAnimationFrame when their PTS is due (PTS-paced presentation,
on by default). A wall-clock ↔ PTS mapping established on the first frame and
reset on large gaps (seek, restart, backgrounded tab) gates each draw. Frames
that missed their slot are dropped as late (the newest is always kept so the
canvas never freezes). Opt out via setRenderPacing(false) to fall back to
"latest frame only" skip-ahead drawing.
Audio output: On Chrome, MediaStreamTrackGenerator provides implicit
pacing for decoded codecs. The AudioWorklet paths (Firefox fallback, and the
s302m PCM player) use bounded ring buffers with drop-oldest and skip-ahead
when buffered data exceeds the playout target.
Backpressure: Both video and audio decoders check decodeQueueSize before
submitting new chunks. Video skips delta frames when queue depth > 8; audio
skips when queue depth > 20. Keyframes always pass to allow resync.
QuicSRT implements draft-sharabayko-srt-over-quic:
- §4.2 Packet integrity — satisfied. Each SRT packet is sent as exactly one WebTransport datagram; the underlying transport preserves packet boundaries.
- §4.3 Connection establishment — 1-RTT handshake. Because WebTransport already provides TLS-level authentication and return-routability, the SRT induction phase (whose only purpose is DoS protection via cookie) is redundant. QuicSRT skips induction and runs a 2-packet handshake: the gateway sends CONCLUSION directly, the browser responds with CONCLUSION-RESP. This saves one RTT (~50-200 ms) on every viewer join.
- §4.5 Congestion control — transport-aware retransmit decisions. SRT's RTT is seeded from QUIC's smoothed RTT (
wtransport::Connection::rtt()on the gateway,WebTransport.getStats().smoothedRtton the browser). The SRT sender skips NAK-triggered retransmits whose predicted arrival time exceeds the TSBPD deadline, preventing wasted bandwidth on packets the receiver will drop as too-late. - §4.7 Connection migration — inherited from QUIC via WebTransport. Mobile viewers can hand off between networks (cellular → WiFi) without rejoining the stream.
- §4.8 Datagram vs H3 Datagram — WebTransport uses H3 Datagram semantics, the load-balancer-compatible choice the draft recommends.
# SRT handshake + TS continuity-counter probe (tests NAK/retransmit under sim-loss)
cargo run -p quicsrt-gateway --bin wt_hs_probe
# Sends fixture over SRT to test ingester without real OBS
cargo run -p quicsrt-gateway --bin mock_obs
# WT datagram round-trip test
cargo run -p quicsrt-gateway --bin wt_echo_client# Both WASM modules end-to-end
node web/smoke.mjs
# PCM round-trip + MessagePort handoff semantics
node web/pcm-roundtrip.mjs
# PCM delivery jitter: worker→main→consumer relay vs direct MessagePort
node web/pcm-port-bench.mjs- Start gateway:
cargo run -p quicsrt-gateway -- --input srt --srt-port 9000 - Open
https://127.0.0.1:5173, click connect - In OBS: SRT output to
127.0.0.1:9000, modecaller - Kill OBS (Ctrl-C) — gateway should log "waiting for reconnect"
- Restart OBS — gateway reconnects, browser auto-reconnects
QuicSRT/
Cargo.toml # workspace (5 crates, 2 forked deps via [patch.crates-io])
Cargo.lock
AGENTS.md # build commands, architecture, gotchas
build.sh # build orchestrator (./build.sh --help for the menu)
install-prereqs.sh # toolchain installer (./install-prereqs.sh --check to verify)
quicsrt.conf # supervisord config (production)
packaging/ # Debian package scaffolding + packaged supervisord config
docs/
embedding.md # mountPlayer() SDK: embedding the browser player, pcmPort API
reference-gateway.md # reference gateway: CLI reference, cert modes, supervisord deployment
fixtures/
stream.sh # live ffmpeg publisher (h264 NVENC / av1 VAAPI) → SRT 9000
stream_pcm.sh # live SMPTE 302M PCM publisher (mono…N×stereo PIDs) → SRT 9000
make-fixture.sh # generates test.ts (gitignored; 10 s H.264+Opus loop)
certs/
README.md # mkcert setup instructions
LICENSE # MPL-2.0
crates/
quicsrt/ # library crate: SRT-over-WebTransport gateway core
src/
lib.rs # pub re-exports + crate docs
gateway.rs # Gateway builder: WT accept loop, session spawn, fanout, health
session.rs # per-browser session: recv_pump (ticker drives the sender half)
registry.rs # centralized SessionRegistry + 2 ms ticker (replaces per-session sender_pump)
stream_registry.rs # multi-stream name → Broadcaster map (?stream= / ?publish=)
srt_sender.rs # SrtInitiator: wraps srt-protocol Connect → DuplexConnection
broadcaster.rs # broadcast fanout with alive-flag + per-stream viewer cap
nocc.rs # QUIC congestion-control bypass for the WT listener
cert.rs # self-signed / mkcert / in-memory PEM cert management
hooks.rs # pluggable SessionPolicy (path/origin/auth-token, chainable)
limits.rs # GatewayLimits: per-IP (/64) + global session caps, timeouts
ingest/
mod.rs # Ingester trait + TsMessage type
channel.rs # ChannelIngester: mpsc-backed ingester (browser publish path)
srt.rs # SrtIngester: srt-tokio listener/caller with reconnect
srt_listener.rs # SrtListenerService: multi-publisher SRT accept loop
file.rs # FileIngester: fixture loop with real-time pacing
continuity.rs # TsContinuityChecker: read-only MPEG-TS CC gap probe
quicsrt-gateway/ # application: native GUI + CLI wrapper around the library
src/
main.rs # CLI parsing, cert setup, cert-hash.js writing, Gateway::run()
gui.rs # eframe/egui app: config form (persisted), Start/Stop, stats, logs
log_buffer.rs # LogBuffer ring buffer + tracing writer for the GUI log panel
web_server.rs # built-in HTTPS server, web/dist/ embedded via rust-embed
bin/
wt_hs_probe.rs # SRT handshake + TS continuity probe
mock_obs.rs # Streams fixture over SRT
wt_echo_client.rs # WT datagram round-trip test
tests/
broadcaster.rs # fanout / viewer cap / lag integration tests
timebase_drift.rs # diagnostic: confirms forked TimeBase::adjust sign-flip fix
srt-wasm/ # wasm-bindgen wrapper around srt-protocol (receiver + sender)
mpeg2ts-wasm/ # wasm-bindgen wrapper around mpeg2ts::TsDemuxer (+ nal.rs + aes3.rs + DebugSnapshot)
ts-muxer-wasm/ # wasm-bindgen wrapper around the publisher-side TS muxer (video, Opus/AAC, PCM push)
web/
index.html # default page — loads pages/viewer.ts (player SDK + lazy debug panel)
stream.html # publisher page — loads stream.tsx
package.json # web-UI toolchain (TypeScript, Preact, Chart.js)
vite.config.ts # multi-page HTTPS build config
tsconfig.json
smoke.mjs # Node smoke test for WASM modules
pcm-roundtrip.mjs # PCM round-trip + MessagePort semantics test
pcm-port-bench.mjs # PCM relay vs direct-MessagePort jitter benchmark
src/
pages/
viewer.ts # default-page entry: mountPlayer + opt-in debug panel (lazy-loaded)
player/
index.ts # mountPlayer(): framework-agnostic player SDK → PlayerHandle (EventTarget)
shared/
viewer.ts # viewer lifecycle: WT URL build, worker mgmt, backoff reconnect, audio wiring
pmt.ts # PMT summarization (video/audio PID + codec from stream types + descriptors)
av1.ts # AV1 OBU content-probe (disambiguate 0x06 AV1 vs Opus)
stream.tsx # publisher page: screen capture → VideoEncoder → TsMuxer → SRT
stream-worker.ts # publisher Web Worker: VideoEncoder/AudioEncoder → TsMuxer → SrtReceiver.sendMessage
worker.ts # viewer Web Worker: WebTransport + SrtReceiver + Demuxer (adaptive tick)
demux.ts # Demuxer: wraps mpeg2ts-wasm, dispatches PES/PCM events
decode.ts # H.264/HEVC/AV1 parsers, VideoPipeline, Opus/AAC audio pipelines
pcm-player.ts # PcmPlayer: AudioWorklet ring for s302m PCM
render.ts # CanvasRenderer: PTS-paced presentation (bounded ring, rAF-driven)
wasm.d.ts # Type declarations for MediaStreamTrackGenerator
debug/ # debug panel (Preact + signals)
store.ts # DebugStore: reactive signals consumed by all tabs
sampler.ts # main-thread sampler for decoder/renderer stats
types.ts # shared TS contracts (DemuxStats, VideoStats, etc.)
diagnostics.ts # "Download/Copy Info" JSON exporter
gpu-info.ts, media-capabilities.ts
components/ # Panel, StreamTab, CodecTab, AudioTab, GpuTab, SrtTab, DemuxTab,
# ConsoleTab, TestTab, PacketTimeline, packetUtils, streamTypes
components/charts/ # BitrateChart, PidDonutChart, CcHeatmap, RaTimeline, PtsJumpSparkline,
# PcrChart, NalStackedBar, LossCorrelation, LossHeatmap, QueueSparkline, …
public/
cert-hash.js # runtime-generated (gitignored)
favicon.ico
wasm/ # pre-built wasm-pack pkg output (gitignored)
srt-wasm/
mpeg2ts-wasm/
ts-muxer-wasm/
dist/ # built web-UI bundle (gitignored; embedded into the gateway binary)
There are two independent SRT TSBPD latencies in the reference binary:
--latency(default 120 ms) — controls the OBS → gateway ingester link (passed toSrtIngester::bind_with_latency). Raise it if OBS is on a high-latency network.- Browser latency slider (default 120 ms in the UI) — controls the
gateway → browser link. The gateway-side floor is 10 ms
(
SrtConfig::default().send_latency); the browser's requested latency wins viamax(sender, receiver)during HSv5 handshake.
For high-bitrate streams, cap the SRT send bandwidth with
--max-bandwidth <kbps> (~125% of the stream bitrate) so retransmit headroom
doesn't oversubscribe the uplink.
The renderer does not add its own playout delay — SRT's TSBPD is the only latency buffer.
web/public/cert-hash.jsis runtime-generated (gitignored). Don't commit it. The gateway writes it on boot.web/wasm/andweb/dist/contents are gitignored. Fresh clones must run the build steps from Build Instructions before the page will work.- WASM camelCase warnings in
srt-wasmare required by wasm-bindgen — don't "fix" them. performance.now()epoch mismatch: browser usesweb_time::Instant(Performance API), gateway usesstd::time::Instant. SRT protocol handles this via timestamp fields in packets + clock sync during handshake.- The self-signed cert is regenerated on every boot — never persisted,
so the cert hash rotates on every restart. Reload the browser page
after a gateway restart to pick up the fresh hash from
cert-hash.js. - Don't raise
PAYLOAD_SIZE(1128 = 6×188, TS-aligned). Chrome silently drops browser→gateway datagrams larger than ~1200 bytes — the write resolves with no error, so it fails invisibly. 1128 + SRT header stays under the cap. - The built-in web server uses the same self-signed cert as the WebTransport listener — click through the browser's "not private" warning on first load.
Anyone with the cert hash can connect. Default --cert-mode self binds to
127.0.0.1 (localhost only). Do not bind 0.0.0.0 with self-signed mode
unless you add an auth layer. Use mkcert mode for LAN access (Firefox
compatible, PKI-validated).
Stream-name occupancy is first-wins (a live stream cannot be replaced by
another publisher), but there is no per-publisher authentication beyond the
optional ?token= gate — any session that passes the policy can claim a free
name.
- Chrome/Edge only for self-signed mode — Firefox lacks
serverCertificateHashessupport. Use mkcert mode for Firefox. - Codec support: H.264, HEVC/H.265, and AV1 video; Opus, AAC/ADTS, and
PCM (SMPTE 302M) audio. Browser publishing supports H.264 and AV1 encode
via WebCodecs; the muxer also accepts raw PCM (
push_pcm/push_pcm_pid). - Opus-in-MPEG-TS — supported via 2-byte control header strip. Each PES payload is treated as one Opus packet (ffmpeg's default). AAC/ADTS is the default for OBS and is fully supported.
- 2-week cert validity (self-signed mode) —
serverCertificateHashesimposes a 14-day cap. The cert is regenerated on every boot (never persisted) and the hash rotates with it — reload the page after a gateway restart. Use mkcert mode (or a real CA) for a stable identity. - No SRT encryption between gateway and browser (WebTransport TLS replaces
it). The OBS-to-gateway link supports optional AES encryption via
--srt-passphrase(10–79 chars); disabled by default.