Engineering notes from Deviqon for Q-Stream
Q-Stream needs to distribute signed camera video over the public internet without letting the delivery infrastructure become a trusted component. Encoders, media servers and relays all sit between the camera and the viewer, and any of them can fail, be misconfigured or be compromised. Our answer is to make the viewer's browser the only thing that has to be trusted.
The trust model is simple to state. The camera signs every H.264 access unit with ML-DSA-44 over a BLAKE3 hash and carries the C2PA assertion in a SEI NAL unit. The browser verifies each unit before decoding it, and it accepts the camera's public key only if its fingerprint matches a pin the operator supplied out of band.
Until recently the only route to that browser was RTSP through a WebSocket proxy. That suits a local network but not internet-scale distribution, so we extended the delivery path with three pieces:
Delivery Paths (simplified):
camera -► c2pa-egress ─┬─ RTSP -► proxy -► WebSocket ─────────────────┐
(signed H.264 + SEI) │ |
└─ SRT / MPEG-TS -► media server -► MoQ relay-►|
|
┌------┘
▼
browser
(verify → gate → decode)
"Verified" can mean four different things in this system, and the article uses each term for exactly one of them.
| Property | Question it answers | Mechanism | Where |
|---|---|---|---|
| Camera authenticity | Who signed this? Is it the camera whose key we pinned? | ML-DSA-44 signature; public key accepted only on fingerprint match with the out-of-band pin | Mode A |
| Media-byte integrity | Are these the bytes that were signed? | Mode A: BLAKE3 over the reconstructed access unit. Mode B: C2PA BMFF hash, bound to the signed claim | A and B |
| Provenance after re-encoding | Who attests to this representation of the video? | Standard C2PA manifest signed at the distribution point, checked against a pinned CA | Mode B |
Integrity and authenticity are separate questions. "The bytes match" says nothing about who signed them, since a perfect hash match under an unpinned key proves nothing about origin. And "the signer is the camera" says nothing about whether the bytes were altered afterwards. Mode A answers both at once. Mode B can answer only integrity, plus a different authenticity claim made by a different signer. SRT and QUIC make delivery dependable, but nothing about them is trusted.
Mode A: passthrough. The media server re-frames the stream without re-encoding. The camera's signed elementary stream and its SEI arrive intact, so the player verifies camera authenticity and media-byte integrity end to end, exactly as it does on the RTSP path.
Mode B: ABR re-encode. Delivering an ABR ladder means re-encoding, and re-encoded pixels can never match the hash the camera signed. We don't pretend otherwise.
In Mode B, the camera's signature does not survive. What survives is the broadcaster's signature, applied at the distribution point. The broadcaster is attesting to the re-encoded representation of the video, not to the camera-signed pixel and hash identity. The player verifies that attestation against a separate pinned CA.
The UI shows a distinct verdict for each mode, so the two claims can't be confused. Mode B is a narrower claim than Mode A, but it is honest about its scope: provenance from distribution onwards.
The egress service previously ran CBOR serialisation, SEI injection, NAL splitting and RTP packetisation in one function. We split it into a shared prepare stage and a format-specific final step. RTSP and SRT now share the SEI logic, and the RTSP behaviour is unchanged.
The SRT sink muxes the same access units into MPEG-TS with a small in-house muxer, in keeping with the project's hand-rolled RTSP and RTP code. MPEG-TS is transparent to NAL content, so the C2PA SEI passes through byte for byte. Two details are worth noting:
The core design constraint is that a slow or failing media server must never affect local viewers. The sink owns its own broadcast receiver. When it falls behind, it drops whole access units and resyncs on the next keyframe, so it never applies backpressure to the sender. It dials out only once a source exists, retries with capped, jittered exponential backoff, and is respawned by a supervisor if it panics. Its endpoint is hot-reloadable through ArcSwap, so reconfiguration doesn't take a lock on the video path. The transport sits behind a small trait (SRTTransport, a single send method), so this isolation logic is unit-tested without an SRT stack, including against a transport whose send never returns.
Our first transport was srt-tokio, a pure-Rust SRT implementation that fits a static-binary deployment well, though its own README marks it as not production ready. In practice, our logs showed the caller session dying roughly 14 ms after a successful handshake. Log analysis pointed to a probabilistic interoperability race with the media server's libsrt. As a control, a GStreamer srtsink (libsrt to libsrt) was stable against the same media server.
So we moved to Haivision's libsrt, the same library the media server runs, through a minimal FFI layer. The surface is small enough that we don't need a binding crate:
#[link(name = "srt")]
extern "C" {
fn srt_startup() -> c_int;
fn srt_create_socket() -> Srtsocket;
fn srt_setsockopt(sock: Srtsocket, level: c_int, optname: c_int,
optval: *const c_void, optlen: c_int) -> c_int;
fn srt_connect(sock: Srtsocket, name: *const libc_sockaddr, namelen: c_int) -> c_int;
fn srt_send(sock: Srtsocket, buf: *const c_char, len: c_int) -> c_int;
// srt_close, srt_getlasterror_str
}
libsrt's send and connect calls block, so each runs on Tokio's blocking pool and is bounded twice: by libsrt's own socket timeouts, and by the pump's. The socket closes on Drop, so a failed session can't leak a handle across reconnects. The pure-Rust transport stays behind a feature flag for rollback.
The player's verifier was already transport-agnostic. Everything trust-related is driven by two entry points: handleInit(), which receives the public key, and onAccessUnit(), which receives one Annex-B access unit with its SEI. The WebSocket client is just glue around them. MoQ is therefore a second adapter that calls the same two sinks, and the crypto, key pinning, freshness checks and render gate are reused unchanged.
The library choice matters. MoQ's high-level playback layers decode and render on their own, which would bypass our verify-before-decode gate. In our testing, the high-level watcher also hard-coded a catalog track name that our media server doesn't publish. We use only the @moq/net transport layer, bundled locally with esbuild so the player stays self-contained, with no CDN:
conn = await Connection.connect(relayUrl(cfg.relay));
const broadcast = conn.consume(Path.from(cfg.namespace));
// The publisher exposes a streaming-format catalog; read track names, never hardcode them
const catalog = JSON.parse(
new TextDecoder().decode(await broadcast.subscribe('.catalog', 0).readFrame())
);
// → an init track (avcC) and a media track; each media object is one fMP4 fragment
The camera's public key arrives in-band, inside the C2PA assertion, and is trusted only on a fingerprint match with the out-of-band pin. The pin never travels over MoQ. Relay and namespace are set by URL parameter, server-provided config or built-in default, in that order, and the MoQ bundle is lazy-loaded so the default WebSocket path never depends on it.
The hard part of Mode A is that the media server publishes CMAF, so the player must rebuild the exact access unit the camera signed. Here is the journey of one keyframe (the NAL order is illustrative):
AUD | SPS | PPS | SEI(C2PA) | IDR slice. The signature covers this unit.avcC box, and each sample's NAL units get length prefixes in place of Annex-B start codes. The SEI stays inside the sample.00 00 00 01, reinserts SPS and PPS from avcC where the camera had them, and hands the rebuilt unit to the verifier.NAL payloads, including emulation-prevention bytes and the C2PA SEI, survive packaging untouched, so only framing and parameter-set placement need reconstructing. But that is exactly where the risk lies: if the rebuilt unit differs from the signed one by a single byte, the BLAKE3 check fails on that keyframe.
Because Mode B is standard C2PA (JUMBF, COSE_Sign1, X.509), it is a proper second verifier, held to a fail-closed standard:
c2pa.hash.bmff.v3 hash over the init segment must match the played bytes.For per-segment integrity, the player auto-detects the binding the stream uses: a full manifest per fragment (C2PA §19.3), or verifiable-segment-info checked against session keys committed in the signed manifest (§19.4). Once either is seen, the player latches. A later fragment without it is treated as a downgrade and rejected, rather than falling back to the weaker init-only verdict, so a stripped manifest can't silently demote a stream.
Together, the SRT sink, the MoQ adapter and Mode B verification give Q-Stream an internet-scale delivery path. Every hop is untrusted, and the browser decides what it plays.
Adding transports widens where Q-Stream can go without changing what it trusts.
Working on a video streaming project with similar security or delivery requirements? Get in touch with our engineering team to discuss your challenges: https://www.deviqon.com/contact-us