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.
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. The pusher hands its streams' origin
duties to its peers: it writes no broadcast_origins row of its own and
does not contend for transcoding (below), so a pusher on the same statedb
as its peer, out of the other nodes' reach, is never chosen over it.
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. The nodes that can do a job contend for it 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 that wins a job but cannot take it up (a failed lookup before
recording) releases the lease at once and asks again after 10 seconds; one
that will not do it for this stream (the streamer turned recording off)
releases it and stops asking. A node on its own statedb simply holds every
lease.
Any node with a session for a stream contends for its multistream targets.
Recording (S3 configured) and transcoding (a Livepeer gateway) are
contended for only by a node that is an origin for the stream, one that
ingests it without pushing it to --peers or that a peer pushes it to: both
want the stream at its live edge. Peers take a stream's renditions from its
origin, so renditions minted anywhere else would reach nobody but that
node's own viewers; and a pull replica, seconds behind, that took the
recording over would record again segments its predecessor had saved. A
recorder completes its current object when the stream stops being
published, so the recording can be finalized at once: an ingest node on its
first unpublished segment, a push receiver, which is pushed no unpublished
segments, on seeing the streamer's livestream record end.
A pusher whose --peers names two nodes of one station gives that station
two origins, of which one transcodes. The other gets the renditions only by
pulling the stream from it, so such a station needs --syndicate.