A minimal end-to-end demo of the MOQT Streaming Format (draft-ietf-moq-msf-01) layered on top of LOC (draft-ietf-moq-loc-02) and MoQ Transport (draft-ietf-moq-transport-18).
One binary, two modes:
msfdemo publish— emits an MSF catalog declaring one LOC-packagedvideotrack, then pushes a synthetic LOC video frame every second.msfdemo subscribe— joins the broadcast, decodes the catalog, subscribes to the video track, and prints every frame's LOC metadata (timestamp, timescale) and payload size.
Payloads are filler bytes, not real codec data — the point is to exercise the catalog, LOC object packaging, and the relay's SUBSCRIBE / FETCH semantics together. For a working media player you would feed the per-object payload into a WebCodecs (or equivalent) decoder per the codec declared in the catalog.
msfdemo [-addr host:port] publish | subscribe
| Flag | Default | Description |
|---|---|---|
-addr |
localhost:4433 |
Relay address to dial |
Connections use InsecureSkipVerify: true so they work against the
relay's ephemeral self-signed cert. SIGINT / SIGTERM triggers a
clean shutdown: the publisher sends PUBLISH_DONE on both tracks; the
subscriber closes its request streams. A second signal force-exits.
Both tracks live under the namespace moq-example/msf:
| Track name | Packaging | Contents |
|---|---|---|
catalog |
(MSF catalog) | One JSON object describing the broadcast. |
video |
loc |
One LOC object per tick, 1 Hz, opaque payload. |
The catalog object is emitted once when the publisher starts and is not re-published — §5 of the MSF draft says a catalog object SHOULD be published only when track availability changes.
msfdemo publish relay
│
│ SETUP │
│ ───────────────────────────► │
│ │
│ PUBLISH (moq-example/msf, catalog)
│ ───────────────────────────► │
│ REQUEST_OK │
│ ◄─────────────────────────── │
│ SUBGROUP_HEADER {group=0} │
│ OBJECT (catalog JSON), FIN │
│ ───────────────────────────► │ cached at entry.Cache
│ │
│ PUBLISH (moq-example/msf, video)
│ ───────────────────────────► │
│ REQUEST_OK │
│ ◄─────────────────────────── │
│ │
├─ tick (1s) ──────────────────│
│ SUBGROUP_HEADER {group=N, Properties=1}
│ OBJECT { │
│ Properties: LOC KV pairs │ Timestamp = t.UnixMilli()
│ │ Timescale = 1000
│ Payload: "synthetic-frame-N"
│ } │
│ FIN │
│ ───────────────────────────► │── fanout ──► subgroup streams
│ │── cache ──► replay buffer
│ … │
│ │
│ SIGINT / SIGTERM │
│ PUBLISH_DONE (catalog) │
│ ───────────────────────────► │
│ PUBLISH_DONE (video) │
│ ───────────────────────────► │
Group IDs come from
msf.GroupSequencer, which seeds
from time.Now().UnixMilli() per §6.1 of the MSF draft so a
republish after restart cannot reuse a Group ID.
Per-object LOC properties are encoded with
loc.Object.Encode, which returns
the bytes to drop into message.SubgroupObject.Properties and
.Payload. The Subgroup header carries Properties: true so the
relay knows the per-object Properties block is present (§11.4.2).
The subscriber is catalog-driven: it subscribes to the catalog track first, parses the cached catalog object, picks a track based on its declared properties, and only then subscribes to media. This mirrors how a real MSF player would discover tracks rather than hardcoding names.
Flow:
- SUBSCRIBE on the catalog track with
FilterLargestObject— delivers any future catalog (delta) updates. - Relative Joining FETCH with
JoiningStart=0on the catalog subscription — backfills the cached catalog object the publisher emitted at broadcast start. The relay'shandleSubscribedoes not replay cached objects on a fresh SUBSCRIBE (the draft permits both patterns; §5.1.3 documents this one), so the cached catalog arrives via the FETCH stream. - Wait for the first catalog object, then parse it with
msf.Catalog.Validateand pick the first track whosepackagingislocandroleisvideo(seeselectVideoTrack). The track'sNamespacefield is honoured if present, otherwise the subscriber inherits the catalog track's namespace per §5.1.10. - SUBSCRIBE on the discovered track with
FilterLargestObject— delivers every video frame that arrives after the subscription is established. The demo skips a video-track Joining FETCH because the publisher emits a frame every second; the next live tick lands almost immediately.
msfdemo subscribe relay
│
│ SETUP │
│ ───────────────────────────► │
│ │
│ SUBSCRIBE catalog (filter=LargestObject)
│ ───────────────────────────► │
│ SUBSCRIBE_OK {alias_c, LARGEST_OBJECT={0,0}}
│ ◄─────────────────────────── │
│ FETCH (RelativeJoining, JoiningStart=0,
│ JoiningRequestID = SUBSCRIBE.RequestID)
│ ───────────────────────────► │
│ FETCH_OK │
│ ◄─────────────────────────── │
│ FETCH_HEADER stream │
│ OBJECT (catalog JSON), FIN │
│ ◄─────────────────────────── │
│ │
├─ catalog parsed, pick track ─│
│ (selectVideoTrack: first track with packaging=loc, role=video)
│ │
│ SUBSCRIBE <track-name> (filter=LargestObject)
│ ───────────────────────────► │
│ SUBSCRIBE_OK {alias_v, …} │
│ ◄─────────────────────────── │
│ │
├─ live (each publisher tick) ─│
│ SUBGROUP_HEADER {alias_v, group=N+1, Properties=1}
│ OBJECT {Properties: …, Payload: …}, FIN
│ ◄─────────────────────────── │
On each received catalog object the subscriber runs
msf.Catalog.Validate and logs the
declared tracks. On each video object it calls
loc.Decode to recover the LOC
properties and log them alongside the (group, object) location.
The accept loop dispatches by stream type and TrackAlias:
| Stream type | Routed to |
|---|---|
*IncomingSubgroupStream (catalog alias) |
live catalog updates |
*IncomingSubgroupStream (video alias) |
live video frames |
*IncomingFetchStream |
catalog backfill from the Joining FETCH |
In three terminals:
# 1. Start the relay (ephemeral self-signed cert, port 4433)
go run ./cmd/relay
# 2. Publish the catalog + a synthetic video frame every second
go run ./cmd/msfdemo publish
# 3. Subscribe — prints the cached catalog, then a video frame each tick
go run ./cmd/msfdemo subscribeA late-joining subscriber sees the catalog immediately (via the Joining FETCH), then every subsequent video frame on the live subscription.
- Real codec data. Payloads are ASCII strings, not encoded
frames. A real publisher feeds
EncodedVideoChunk.copyTooutput (per WebCodecs) intoloc.Object.Payload. - Multiple tracks / ABR. The catalog declares one video track
with no alternates. The MSF library supports simulcast/SVC catalogs
(
altGroup,depends,temporalId,spatialId) but the demo doesn't exercise them. - Delta updates. The publisher emits a single independent catalog
at startup and never updates it. The MSF library supports
add/remove/clone deltas (
msf.Apply) but the demo doesn't use them. - End-to-end encryption. LOC §3 and MSF §4.3 are explicit TODOs in the drafts; this implementation defers them.