Skip to content
floatdropPublic

About

Go implementation of the IETF MoQ Transport protocol and relay

Topics

Resources

Contributing

Stars

3 stars

Watchers

0 watching

Forks

Repository files navigation

moq-go

CI Go Reference Coverage License Discord Awesome Go

A Go implementation of the Media over QUIC IETF drafts: a transport-agnostic session library, a reference relay that runs standalone or as several instances routing across each other, 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.27 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/video -in clip.mp4 publish        # MSF catalog + CMAF-packaged video
go run ./cmd/video -out /tmp/recv.mp4 subscribe # reassembles it and reports delivery

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

cmd/relay is one instance. Several instances routing across each other need a distributed DiscoveryStore.

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
}
// Done ends the publication gracefully (PUBLISH_DONE, then FIN). pub.Close()
// cancels it instead (§6.4.2.3).
defer pub.Done(moqt.PublishDoneTrackEnded, "")

// 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.3.1 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

A relay's own program publishes on Relay.Connect's session what it would have the relay originate (a figure it computes per subscriber, say), and every subscriber reaches the track as it reaches a peer's. The session runs over an in-process connection (session/memconn), served as LegInProcess with every request authorized, and ends with the relay's Stop:

own, err := r.Connect(ctx) // r is a started *relay.Relay
if err != nil {
	return err
}
pub, err := own.Publish(ctx, &message.Publish{
	Namespace: wire.Namespace("room"),
	Name:      []byte("link/p1"),
})
if err != nil {
	return err
}
_ = own.SendDatagram(&message.ObjectDatagram{
	TrackAlias: pub.TrackAlias(), GroupID: 0, ObjectPayload: []byte("rtt=42"),
})

Observing media at the relay

Implement relay.Metrics.ObjectReceived and set relay.Config.Metrics to observe complete objects from both subgroup streams and datagrams. relay.ReceivedObject includes the full track name, absolute Group/Object IDs, Status, receipt time (HeaderAt), Properties, and Payload. Datagram distinguishes objects with no subgroup; EndOfGroup carries a datagram's end bit. The hook runs after deduplication, before downstream filtering, including when no subscriber is present. Cached FETCH delivery does not trigger it again.

Callbacks run concurrently and must return promptly. Treat all slices as read-only, copy the properties and payload before queuing them, and use a bounded queue with a non-blocking send. Perform decoding and analysis in a worker. Order packets by their media timestamps; callback order is not a playout order. Use the full track identity to separate sources, and retire worker state when a source ends or goes idle; subscription lifecycle metrics describe downstream subscriptions, not publisher tracks.

For gosqa, consumer code still needs to select audio tracks from the MSF catalog, parse LOC properties, decode Opus into normalized mono float32 PCM at 48 kHz, and feed a gosqa stream per source. Preserve the media timeline through packet reordering, loss concealment, and intentional silence suppression. Report analysis queue overflow and reset decoder/analysis state after a discontinuity. Run inference outside the callback, share the model across workers, and ship the required ONNX Runtime library with the application. Gosqa's default frame buffers two seconds of PCM before producing a score. Scores at this hook describe audio received by the relay; downstream losses need receiver-side measurement.

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
Route inbound requests (server side) ExampleRequestMux
Joining / standalone FETCH ExampleSession_Fetch, ExampleSession_Fetch_standalone, ExampleIncomingFetchStream
Update a live request ExampleSession_UpdateRequest
Serve a long-lived request stream (updates + follow-ups) ExampleRequestBroker
End a publication Example_endingAPublication
Stream exhaustion (PUBLISH_SKIPPED) ExampleSession_OpenPublish, ExampleTrackSubscription_ReadPublishSkipped
Announce / discover namespaces ExampleSession_PublishNamespace, ExampleSession_SubscribeNamespace
Accept requests + reply (Accept* helpers) ExampleSession_AcceptRequest
Graceful shutdown (GOAWAY) ExampleSession_SendGoaway, ExampleSession_OnGoaway

relay

Topic Example function(s)
Run the relay / graceful shutdown ExampleNew
Authorize requests ExampleNew_authorizer
Relay metrics ExampleMetrics

loc

Topic Example function(s)
LOC media packaging ExampleObject_Encode

msf

Topic Example function(s)
MSF catalogs (build / parse / delta) ExampleBeginBroadcast, Example_subscribeCatalog, ExampleApply
CMSF CMAF packaging + DRM (CMSF) Example_contentProtection, ExampleSAPRecord

The two demo commands — cmd/clock and cmd/video — are complete, runnable versions of these patterns end to end; each has its own README. cmd/video streams a real media file as a CMSF broadcast and reports what the transport did to it, which is also the tool to reach for when a player is showing artefacts and the cause is unclear.

A per-feature breakdown of draft-22 completeness, the full list of what's implemented per package, and known limitations live in STATUS.md.

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)

For the benchmark suite and the benchstat regression-comparison workflow, see benchmarks/README.md.

Development environment (devenv + direnv)

The repo ships a devenv config (devenv.nix) that pins the Go toolchain (matching go.mod) plus golangci-lint, golines, goimports, gopls, and dlv — so everyone builds against the same versions. It is optional: a plain go install works fine. With Nix installed:

# One-time: install devenv and direnv.
nix profile add nixpkgs#devenv nixpkgs#direnv

Then either enter the shell on demand:

devenv shell        # drops you into a shell with the full toolchain on PATH
devenv test         # sanity-check the toolchain wiring

…or let direnv load it automatically on cd (recommended). Hook direnv into your shell once (see the direnv setup guide), e.g. for zsh:

echo 'eval "$(direnv hook zsh)"' >> ~/.zshrc

then trust this repo's .envrc once:

direnv allow        # run from the repo root; re-run if .envrc changes

After that the environment activates whenever you enter the directory. Inside the shell, convenience scripts wrap the canonical commands: build, test, test-race, lint, bench, and modernize.

Interoperability tests

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 §8.1 leading-ones encoding) passes every unit test yet breaks interop — only a run against an independent implementation catches it.

License

Licensed under either:

About

Go implementation of the IETF MoQ Transport protocol and relay

Topics

Resources

Contributing

Stars

3 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages