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.
- Media over QUIC Transport (MoQT) —
draft-ietf-moq-transport-18 - Low Overhead Media Container (LOC) —
draft-ietf-moq-loc-02 - MoQ Streaming Format (MSF) —
draft-ietf-moq-msf-01
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.
go get github.com/floatdrop/moq-goRequires Go 1.26 or newer.
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 catalogFor the simpler raw-MOQT case (no LOC/MSF), swap msfdemo for clock.
The mental model:
- A
Sessionis one MOQT connection after the SETUP handshake. You get one fromsession.Clientorsession.Serverover a transportConn. - A publisher opens a
PUBLISHrequest stream, then pushes objects on subgroup uni-streams. - A subscriber opens a
SUBSCRIBErequest stream, then reads objects viaSession.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 startsWorked, 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:
| 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 |
| Route inbound requests (server side) | ExampleRequestMux |
| 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 + reply (Accept* helpers) |
ExampleSession_AcceptRequest |
| Graceful shutdown (GOAWAY) | ExampleSession_SendGoaway, ExampleSession_OnGoaway |
| Topic | Example function(s) |
|---|---|
| Run / authorize the relay | ExampleNew, ExampleNew_authorizer |
| Topic | Example function(s) |
|---|---|
| LOC media packaging | ExampleObject_Encode |
| 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.
A per-feature breakdown of draft-18 completeness, the full list of what's
implemented per package, and known limitations live in
STATUS.md.
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.
This implementation is registered (as moq-go) in the
moq-interop-runner, which
exercises it in both directions against independent draft-18 implementations.
See cmd/relay/README.md for the local make interop
targets.
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. 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.
Licensed under either:
- Apache License, Version 2.0 (LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0)
- MIT license (LICENSE-MIT or http://opensource.org/licenses/MIT)