Skip to content

floatdrop/moq-go

moq

CI Go Reference Go Report Card License

A Go implementation of the Media over QUIC IETF drafts: a transport-agnostic session library, a single-instance reference relay, media packaging libraries, and demo publisher/subscriber CLIs.

This is library + reference-relay code, not a media player. Payloads are opaque to every layer — applications plug their own codec stack in at the LOC boundary.

Status: tracks moving IETF drafts. The wire format follows the draft versions above and the API is pre-1.0 — both change as the specs evolve.

Install

go get github.com/floatdrop/moq-go

Requires Go 1.26 or newer.

Quick start

Run the stack locally, each command in its own terminal:

go run ./cmd/relay              # ephemeral self-signed cert on :4433
go run ./cmd/msfdemo publish    # MSF catalog + LOC video frames
go run ./cmd/msfdemo subscribe  # discovers the video track from the catalog

For the simpler raw-MOQT case (no LOC/MSF), swap msfdemo for clock.

Using the library

The mental model:

  • A Session is one MOQT connection after the SETUP handshake. You get one from session.Client or session.Server over a transport Conn.
  • A publisher opens a PUBLISH request stream, then pushes objects on subgroup uni-streams.
  • A subscriber opens a SUBSCRIBE request stream, then reads objects via Session.AcceptDataStream.
  • A track is named by a (Namespace, Name) pair; a per-session Track Alias is the compact integer that data streams carry.

A minimal publisher looks like this:

sess, err := session.Client(ctx, quicconn.New(qconn),
	session.WithImplementation("my-app/0.1"))
if err != nil {
	return err
}
defer sess.Close(moqt.SessionNoError, "bye")

pub, err := sess.Publish(ctx, &message.Publish{
	Namespace: wire.Namespace("moq-example"),
	Name:      []byte("clock"),
})
if err != nil {
	return err
}
defer pub.Close()

// Publish assigned the Track Alias; the returned Publication carries it, so
// pub.OpenSubgroup fills it in for you. To manage aliases yourself, set
// Publish's TrackAlias (via sess.AllocOutboundTrackAlias) and use
// sess.OpenSubgroup directly.
sg, _ := pub.OpenSubgroup(message.SubgroupHeader{
	SubgroupIDMode: message.SubgroupIDImplicitZero,
	GroupID:        0,
})
// WriteObjectAt takes absolute Object IDs and computes the §11.4.2 delta
// encoding for you; WriteObject is the lower-level form that takes the delta.
_ = sg.WriteObjectAt(0, &message.SubgroupObject{Payload: []byte("hello")})
_ = sg.Close()

A subscriber reads objects off inbound data streams via Session.AcceptDataStream, type-switching the result to *IncomingSubgroupStream / *IncomingFetchStream. When you subscribe to several tracks on one session, session.Demux removes the hand-rolled accept loop: register a handler per track by its Track Alias (and per FETCH by Request ID), then call Demux.Run.

// sess here is the subscriber's session (from session.Client/Server).
sub, err := sess.Subscribe(ctx, &message.Subscribe{
	Namespace:  wire.Namespace("moq-example"),
	Name:       []byte("clock"),
	Parameters: message.Parameters{message.LargestObjectFilter()},
})
if err != nil {
	return err
}
defer sub.Close()

demux := session.NewDemux()
demux.HandleTrack(sub.TrackAlias(), func(s *session.IncomingSubgroupStream) {
	for {
		obj, err := s.ReadDecoded() // absolute IDs; deltas resolved for you
		if err != nil {
			return // io.EOF on clean FIN
		}
		_ = obj
	}
})
go demux.Run(ctx, sess) // HandleTrack is safe to call after Run starts

Examples

Worked, compile-checked examples for each part of the API live as Go example functions — browse them on pkg.go.dev or read the source, grouped here by the file they live in:

session

Topic Example function(s)
Open a session ExampleClient
Publish a track ExampleSession_Publish
Subscribe to a track ExampleSession_Subscribe
Route many tracks' data streams ExampleDemux
Joining / standalone FETCH ExampleSession_Fetch, ExampleSession_Fetch_standalone, ExampleIncomingFetchStream
Update a live request ExampleSession_UpdateRequest
End a publication Example_endingAPublication
Stream exhaustion (PUBLISH_BLOCKED) ExampleSession_OpenPublish, ExampleSession_ReadPublishBlocked
Announce / discover namespaces ExampleSession_PublishNamespace, ExampleSession_SubscribeNamespace
Accept requests (server side) ExampleSession_AcceptRequest
Graceful shutdown (GOAWAY) ExampleSession_SendGoaway, ExampleSession_OnGoaway

relay

Topic Example function(s)
Run / authorize the relay ExampleNew, ExampleNew_authorizer

loc

Topic Example function(s)
LOC media packaging ExampleObject_Encode

msf

Topic Example function(s)
MSF catalogs (build / parse / delta) ExampleBeginBroadcast, Example_subscribeCatalog, ExampleApply

The two demo commands — cmd/clock and cmd/msfdemo — are complete, runnable versions of these patterns end to end; each has its own README with sequence diagrams.

Repo layout

pkg/moqt/                 MOQT protocol implementation
├── wire/                 Wire-format primitives (varint, KV pairs, namespaces, framing)
├── message/              Typed control- and request-stream messages
├── track/                Full track name + canonical map keys
├── session/              SETUP handshake, control multiplexing, GOAWAY, alias mgmt
│   ├── quicconn/         Native-QUIC Conn adapter (quic-go)
│   ├── wtconn/           WebTransport Conn adapter (webtransport-go)
│   └── sessiontest/      In-process pipe-backed Conn for tests
├── loc/                  Low-Overhead Container per draft-ietf-moq-loc-02
├── msf/                  MOQT Streaming Format per draft-ietf-moq-msf-01
└── errors.go             Session / request / publish-done / stream-reset codes

pkg/relay/                Single-instance MOQT relay
├── cache/                Per-track object cache
└── discovery/            Cross-instance discovery interface + in-memory impl

cmd/
├── relay/                MOQT relay binary
├── interop-client/       moq-interop-runner test client (drives the session library)
├── clock/                Wall-clock publish/subscribe demo (raw MOQT)
└── msfdemo/              MSF catalog + LOC video frame demo

apps/
└── tlmst/                Wails3 desktop app (separate Go module, isolated deps)

What's implemented

  • wire — byte-level codec: MoQT leading-ones varints (§1.4.1, distinct from QUIC's RFC 9000 varints), length-prefixed bytes, KV pairs with delta-encoded types (§1.4.3), track namespaces (§2.4.1), reason phrases, and both an in-memory Reader and a streaming Decoder over the same control-frame interface.
  • message — typed control, request-stream, and data-stream messages with parameter negotiation: SETUP, GOAWAY, SUBSCRIBE, PUBLISH (+ DONE/BLOCKED), FETCH (standalone, relative/absolute joining), TRACK_STATUS, REQUEST_UPDATE, the namespace messages, the §11 object framing (subgroup, fetch, datagram) with absence markers, subscription filters, GREASE handling, and a Validate hook the decoder runs on parse to reject structurally-malformed messages (FETCH End < Start, REQUEST_ERROR redirect consistency, object status/flags, …).
  • session — the SETUP handshake with version negotiation, control multiplexing and request-ID allocation, Track-Alias management with §3.5 collision detection, the request openers (Publish/Subscribe/Fetch/…) and the AcceptRequest responder, typed inbound data streams that resolve §11.4.2/§11.4.4 deltas to absolute IDs, GOAWAY, the §10.20 token cache, and pluggable transport via the Conn interface (quicconn + wtconn adapters).
  • locObject.Encode/Decode producing the bytes that drop into a SubgroupObject: typed Timestamp/Timescale/VideoConfig/VideoFrameMarking/ AudioLevel properties with Extras passthrough for unknown IDs, an RFC 6464 audio-level codec, and AVC/HEVC NAL framing detection.
  • msfCatalog/Track JSON (independent and delta catalogs, with Apply replaying delta operations in document order), group-ID sequencing, the Media and Event Timeline record formats, the BeginBroadcast / EndBroadcast* workflow helpers, and Catalog.Validate enforcing the §5.1/ §5.2 invariants.
  • relay — accepts publisher and subscriber sessions, routes objects through a track registry with per-subscription live fanout under a §8 latency-window slow-reader policy, merges multiple upstream publishers per track (§9.5) into one outbound subgroup stream per subscriber with §2.1 {Group, Object} deduplication and survivor-continues failover, serves FETCHes from a per-track object cache and stitches the evicted part of a range from an upstream FETCH, issues on-demand upstream SUBSCRIBEs to every matching publisher (a local publisher and/or, via a DiscoveryStore FindNamespace lookup + a pluggable Dialer, each advertising relay instance), reflects namespaces other relays advertise to local subscribers by consuming WatchNamespaces, forwards namespace interest, gates each request through an Authorizer hook, emits telemetry through a Metrics hook, and drains sessions with GOAWAY.
  • CLIscmd/relay (with cert flags + self-signed fallback), cmd/clock (raw subgroup demo), and cmd/msfdemo (the LOC + MSF stack end to end).

Limitations

This is a single-instance reference relay, though cross-relay routing works when wired up: set Config.Discovery + Config.Dialer and the relay follows a FindNamespace lookup to dial and subscribe upstream on another instance (and reflects remote namespaces to local subscribers via WatchNamespaces). What remains out of scope: multi-hop loop detection (the only guard is skipping the relay's own RelayAddr), an upstream connection-health / redial policy beyond dial-on-demand, production DiscoveryStore backends (only the in-process MemoryStore ships), GOAWAY cascading, and a Dialer for cmd/relay itself (the binary stays single-instance; cross-relay is library-level). Known gaps in the current code, roughly ordered by how load-bearing they are:

  • Late publisher pickup (§9.5) — multiple publishers per track are merged and deduplicated, and an on-demand SUBSCRIBE subscribes to every publisher matching at that moment. A publisher (or remote relay) that begins advertising after a track's upstream set is established is not retroactively pulled in until that set drains and a fresh SUBSCRIBE re-establishes it; publishers that PUBLISH proactively are always merged regardless.
  • Subscriber-priority scheduling (§10.2.7) — fully plumbed but not yet enforced on the wire. SUBSCRIBER_PRIORITY is parsed and stored, the §7.2 four-rule composite key is computed (EffectiveStreamPriority) and pushed through the session.PrioritizedSendStream interface on every stream open/reopen (propagation is end-to-end test-covered). The missing piece is a transport adapter that honors the knob: quic-go and webtransport-go expose no per-stream priority API today, so the bundled adapters silently absorb it and quic-go round-robins instead. Lights up with a one-line adapter change once quic-go#437 lands. A REQUEST_UPDATE that changes the priority mid-stream applies to subsequently opened subgroup streams, not in-flight ones.
  • LOC encryption / SecureObjects (LOC §3) and Private Properties — intentionally out of scope pending a chosen SecureObjects revision. Property IDs are draft-tentative (PropAudioLevel = 0x0A deviates from the draft's unassigned suggested 6, which collides with the registered PropTimestamp (0x06); pending IANA assignment).
  • MSF — no timeline GZIP compression, content protection (§4.3), token authorization, or logs/analytics (each is a TODO or unspecified in the draft). There is no built-in ABR helper: the library surfaces every catalog field a selector needs (AltGroup, Width/Height, Bitrate, RenderGroup, Depends, TemporalID, SpatialID), but variant-selection policy is the application's job.

Building and testing

go build ./...
go test ./...                          # full suite — hermetic, no fixtures or network
go test -race ./pkg/moqt/session/...   # race detector for goroutine/stream code
golangci-lint run                      # lint + format check (.golangci.yml)

go test ./... from the root does not include apps/tlmst (a separate module with CGO/WebKit deps). For the benchmark suite and the benchstat regression-comparison workflow, see benchmarks/README.md.

Interoperability tests

Interop is tested in both directions against independent implementations:

  • Relay directionmake interop runs a third-party MoQT test client (from the moq-interop-runner) against our relay over both transports; make interop-matrix runs several clients and prints a pass/skip/fail matrix.
  • Client directionmake interop-client runs our own test client (cmd/interop-client) against a relay (loopback by default; override CLIENT_RELAY_IMAGE/CLIENT_RELAY_URL for a third-party relay).

See cmd/relay/README.md for the targets and options; current results are tracked in STATUS.md.

CI runs on every push and pull request (.github/workflows/ci.yml): go build ./..., go test ./..., go test -race ./..., golangci-lint run, a govulncheck scan, and the interop suite (make interop and make interop-client). The interop run is not redundant with go test: the unit tests round-trip through our own codec, so a wire-encoding regression (e.g. emitting QUIC varints instead of the §1.4.1 leading-ones encoding) passes every unit test yet breaks interop — only a run against an independent implementation catches it. The advisory make interop-matrix is not gated, as it has known cross-implementation divergences (see STATUS.md).

License

Licensed under either:

About

Go implementation of the IETF MoQ Transport protocol

Topics

Resources

License

Apache-2.0, MIT licenses found

Licenses found

Apache-2.0
LICENSE-APACHE
MIT
LICENSE-MIT

Contributing

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors