Media over QUIC #
Streamplace nodes hand live streams to each other over Media over QUIC,
using the official implementation: moq.dev/moq, the Go wrapper over the
Rust moq-ffi that Meson builds from source into the node (see
rust/streamplace-native). pkg/moq holds only what Streamplace adds, the
listener's certificate handling. The listener and both directions of
transfer live in the websocketrep replicator
(pkg/replication/websocketrep/moq.go). A node pulls a stream from its
origin, preferring the origin's MoQ URL over its websocket when one is
advertised, and pushes its own streams to the nodes named in --peers.
Wire convention #
Every byte a broadcast carries is signed MUXL, laid out the way the rest of
the MoQ ecosystem reads media: a hang catalog and one CMAF track per media
track. The MoQ tools (moq export, moq play) and the @moq/watch player
can play a Streamplace broadcast, and a Streamplace node reassembles the
exact signed segments and verifies them as before.
| MoQ | Streamplace |
|---|---|
| broadcast path | the streamer's DID (did:plc:...) |
catalog.json, catalog.json.z, catalog |
the hang and MSF catalogs, one CMAF rendition per track |
| track | one MUXL track, named by its MUXL rendition (video1, audio2, audio3; renditions video100 up) |
| group | that track's canonical segment for one GoP |
| frame | one moof+mdat, the first also carrying the segment's leading uuid boxes (C2PA manifest, MUXL catalog) |
| timestamp | the fragment's first presentation time, on the track's timescale |
A group's frames concatenate back to the track's canonical segment, byte
for byte. Each catalog rendition's CMAF init is the track's own init
segment, synthesized from its MUXL catalog (muxl wrap --init-only); the
hang codec fields are derived from that init by moq-mux. The catalog is
unsigned: a node uses it only to find the tracks, and verifies the bytes.
The source segment's tracks (MUXL track ids below
media.RenditionTrackID(0)) share one group sequence per segment. A
subscriber reads each in sequence order, concatenates one segment's
groups in track-id order, and validates the result with
MediaManager.ValidateReplicatedMP4: the websocket path's validation,
plus a check that the segment's signed creator is the streamer it asked
for. A source track with no group at a segment's sequence has no part in
that segment. Each rendition track's group is a single-track addendum,
verified with muxl verify and folded into the live window on its own.
Anything that is not MUXL ends the session. So does a catalog listing more
than --moq-max-tracks tracks (default 32) or a group over
--moq-max-group-mb (default 16 MiB): a subscriber buffers a whole group
before it can verify it, so these bound what one peer's stream can make a
node hold. Either is off at 0.
A node's listener advertises one route covering every path and serves a
broadcast when a peer requests it: a request for a stream that is not live
on its segment bus is refused, and a served broadcast ends with the
stream's session, or when a track's codec config changes (its MUXL catalog
box differs), since the catalog entry describes the old one; the next
request starts afresh. The transport shares one served broadcast among all
its subscribers. A track is published the first time a segment carries it,
and a new subscriber starts from the newest segment, like
place.stream.live.subscribeSegments's priming. The source tracks have the
higher subscriber priority, and a subscriber that falls behind skips ahead.
To watch or dump a broadcast with the MoQ CLI (pin a self-signed node's
certificate with the certhash from its MoQ URL):
moq --connect moqt://host:38443 --connect-tls-fingerprint <certhash> \
--broadcast did:plc:... export fmp4 > stream.mp4
Bindings #
One UDP port serves raw QUIC (moqt://host:port) and WebTransport, which
is the binding a browser uses. The transport negotiates the protocol
version by ALPN.
Configuration #
| Flag | Env | Default | Meaning |
|---|---|---|---|
--moq-addr |
SP_MOQ_ADDR |
:38443 |
UDP listen address; empty disables the listener. |
--moq-url |
SP_MOQ_URL |
Override for the advertised moqt:// URL (default: --server-host on --moq-addr's port). |
|
--peers |
SP_PEERS |
Nodes to push this node's live streams to: each an https:// node origin or a moqt:// URL, comma-separated. |
The listener terminates TLS itself, even on a node behind an HTTPS proxy,
reading its certificate from files (the data directory's moq-tls) that
Go keeps current:
- with
--secure, it serves the node's certificate (ACME, rewritten as it renews, or--tls-cert/--tls-keyas given); - otherwise it mints a self-signed ECDSA certificate, valid for two weeks
and rotated weekly (the limits a browser puts on a pinned certificate),
and advertises its SHA-256 as the
certhashquery parameter of the MoQ URL. A subscriber pins exactly that certificate.
Discovery #
The advertised URL travels two ways, like the websocket URL:
- the
moqURLfield of the streamer'splace.stream.broadcast.originrecord on the firehose; - the
moq_urlcolumn of the station's sharedbroadcast_originstable, written by the ingest node on every local segment, so nodes sharing a statedb find each other without a relay.
Push #
A node that cannot be reached, one beside a streamer's encoder behind NAT,
pushes instead of waiting to be pulled from. For each --peers entry it
keeps one session open for as long as it runs, publishing the streams it
is ingesting: each is announced, one broadcast per streamer DID, once its
first published segment's tracks are in the catalog, until the stream
session times out, its tracks served from the bus exactly as the listener
serves them. The peer gives every incoming session its own origin, so a
pushed announcement never becomes a route it serves; it pulls each
announced stream's source tracks from that session and validates them
exactly as it would a pull, and ignores
announcements past 32 streams or that are not DIDs. It takes no renditions
from a pusher, whose signer could be anyone: as the stream's origin it mints
its own. A pushed stream is a replicated stream on
the peer (Local: false); while it flows the peer writes itself into the
station's broadcast_origins table for it, so the nodes sharing its
statedb pull the stream from the peer rather than from a pusher they
cannot reach.
An https:// peer is asked for its current MoQ URL on every dial, through
place.stream.broadcast.getBroadcaster's moqURL: a node behind an HTTPS
proxy serves MoQ with a self-signed certificate it rotates, so a fixed
moqt://…?certhash= entry would go stale. A moqt:// entry is dialled as
given. A listener accepts any pushed stream its --allowed-streams would
accept over RTMP; a frame that fails validation ends the session.
Validation refuses a replicated segment whose start time is more than ten
minutes from the node's clock, so a pusher cannot replay a public
streamer's old signed segments to make them appear live (the same window
applies to every replicated segment, /api/segment included).
Who records, multistreams and transcodes #
Recording a stream to S3, driving its multistream targets and transcoding
its renditions must each happen on exactly one node, however many carry the
stream. Every node with a session for a stream, whether it ingested the
segments or received them from a peer, contends for each job it is equipped
to do (S3 configured for recording, always for multistream) through a lease
in the station's shared statedb (leases; pkg/director/roles.go). The
holder renews the lease every 2 seconds; a node that dies stops, and the
next contender takes the job over once the 6 second TTL has lapsed. A node
that ends a session cleanly releases its leases, so a peer takes over at
once. A node on its own statedb simply holds every lease.
Transcoding is contended for only by a node that is an origin for the stream, one that ingests it or that a peer pushes it to, and that has a Livepeer gateway: peers take a stream's renditions from its origin, so renditions minted on a pull-only replica would reach nobody but its own viewers.