moqlivemock
August 6, 2026 · View on GitHub
moqlivemock is a simple media test service for MOQ Transport
and the MSF/CMSF streaming format by providing a server which
publishes an asset with wall-clock synchronized multi-bitrate video,
audio tracks, and dynamically-generated subtitle tracks (WVTT and STPP),
as well as a client that can receive these streams and even multiplex
video and audio for playback with ffplay like mlmsub -muxout - | ffplay -.
Video tracks use avc1 (H.264) and hvc1 (HEVC) sample descriptors with
parameter sets stored in the init segment, which is required for FairPlay
DRM support in Safari 26.4+.
The input media is 10s of video and audio which is then disassembled
into frames. One or more frames are then combined into a MoQ object as a CMAF chunk.
How many frames are combined is configurable via the -audiobatch and -videobatch options.
Subtitles are generated on the fly and delivered as 1s groups with 1 object per group. That object is published at the start of each second in order to not increase the latency.
Wall-clock alignment
All streams are aligned to UTC wall-clock time at two levels:
- The 10-second asset loop is aligned to UTC modulo 10 seconds.
The first sample of the clip maps to epoch times where
seconds % 10 == 0. This means every subscriber joining at the same wall-clock time receives the same content, regardless of when the publisher was started. - MoQ groups are aligned to full UTC seconds. Each group number is
Unix_epoch_ms / 1000, so group boundaries fall on exact second boundaries. Audio is typically not compatible with integral seconds, so minimal displacement is applied without accumulated drift over time.
In addition to CMSF, mlmpub also announces an LOC (Low Overhead Container) namespace and a moq-mi (MoQ Media Interop) namespace. Each CMSF catalog additionally offers a LOCMAF (Low Overhead CMAF) variant of every track. See the Namespaces section below for details.
This project uses moqtransport for the MoQ transport layer,
supporting both draft-14 and draft-16 of MOQT. Draft-16 uses ALPN-based version
negotiation (moqt-16) and WT-Available-Protocols for WebTransport. Draft-14
(moq-00) is supported for backward compatibility.
Namespaces
mlmpub announces one or more namespaces depending on the configured packaging and protection modes. Each CMSF/MSF namespace has its own catalog containing only the relevant tracks; moq-mi is catalogless and uses fixed track names by convention.
| Namespace | Packaging | Condition | Track suffix | Description |
|---|---|---|---|---|
cmsf/clear | CMSF (CMAF chunks) | Always | (none) | Unencrypted tracks |
cmsf/drm-{scheme} | CMSF (CMAF chunks) | -drmpath set | _drm | Commercial DRM (Widevine/PlayReady/FairPlay via CPIX) |
cmsf/eccp-{scheme} | CMSF (CMAF chunks) | -kid/-iv set | _eccp | ClearKey/ECCP (explicit key over HTTP) |
msf/clear | LOC (draft-mzanaty-moq-loc) | Always | (none) | AVC video + AAC/Opus audio, clear only |
moq-mi/clear | moq-mi (draft-cenzano-moq-media-interop) | When asset has AVC + AAC-LC/Opus | (none) | Catalogless, fixed track names video0 / audio0 |
There is no separate locmaf/* namespace. The cmsf/* catalogs are
unified: each rendition is listed twice — as a CMAF track (packaging: "cmaf")
and as a LOCMAF track <name>_locmaf (packaging: "locmaf") sharing one
init-data entry. See LOCMAF below.
Both DRM and ECCP can be active simultaneously — they use independent encryption keys and produce separate sets of protected tracks.
Subtitle tracks are only included in the CMSF namespaces; LOC and moq-mi carry video and audio only.
LOC (msf/clear)
The LOC namespace uses MSF with packaging=loc per
draft-ietf-moq-msf-00 and draft-mzanaty-moq-loc. Objects carry
raw codec bitstream — AVC as length-prefixed NALUs and AAC as raw frames —
without any container framing.
On the subscriber side, mlmsub reframes LOC video (length-prefixed NALUs
→ AnnexB) and LOC audio (raw AAC → ADTS) so the output can be piped directly
to ffplay. Only AAC-LC (mp4a.40.2) is supported for LOC audio at the
moment; HE-AAC and other object types are rejected.
moq-mi (moq-mi/clear)
The moq-mi namespace implements
draft-cenzano-moq-media-interop. It has no catalog: the subscriber
uses fixed track names (video0, audio0) and parses per-object moqmi
extension headers to learn the media type and codec metadata. Payloads are
the codec bitstream as defined by moqmi (AVCC length-prefixed NALUs for
video, raw frames for AAC/Opus) and are written through unchanged by mlmsub
— this namespace is intended for interop testing, not direct ffplay playback.
LOCMAF (within the cmsf/* namespaces)
LOCMAF (Low Overhead CMAF) is a compact CMAF packaging in which only the
non-derivable moof fields are sent on the wire; the receiver reconstructs
standard CMAF media fragments so the normal CMAF playback path is reused
unchanged. LOCMAF is not a separate namespace: within each cmsf/* catalog
every rendition is offered both as a CMAF track <name> (packaging: "cmaf")
and as a LOCMAF track <name>_locmaf (packaging: "locmaf"), listed as
alternates in the same altGroup. The two variants share one init-data entry
(the raw CMAF init segment) referenced by initRef. Because all fields needed
for playback — including per-sample encryption metadata — are carried, the
LOCMAF variant is offered for the encrypted cmsf/drm-{scheme} and
cmsf/eccp-{scheme} catalogs too (<name>_drm_locmaf / <name>_eccp_locmaf).
The codec is not implemented in this repository. Encode/decode comes from
the reusable Go module
github.com/Eyevinn/locmaf, and the catalog
advertises locmafVersion from locmaf.Version (currently 0.3). mlmpub
encodes each chunk with locmaf.EncodeCanonical; mlmsub expands received
Objects back to CMAF with locmaf.Decode + locmaf.ReconstructCanonical and
rejects tracks whose locmafVersion it does not implement. One packaging
version is supported at a time — v0.2 remains reachable at the locmaf-v0.2
tag.
The normative wire format is specified by the IETF draft
draft-einarsson-moq-locmaf;
see docs/LOCMAF.md for how moqlivemock uses the module. The
reference test-asset generator (golden-vector corpus) and the round-trip
fidelity/overhead tool live in the locmaf module's CLI, alongside the codec.
Session setup
After session establishment, the server announces all configured namespaces. For CMSF and LOC namespaces the client retrieves the catalog track first, then subscribes to the media tracks listed in that catalog. For moq-mi there is no catalog, so the client subscribes directly to the fixed track names.
By default the catalog is retrieved with a SUBSCRIBE (Filter Type = Largest
Object) plus a relative joining FETCH at offset 0, per
[draft-ietf-moq-msf-01][msf-01] §5, so the client gets the latest catalog group
aligned to the live edge in a single round-trip. The mlmsub -catalog-mode flag
selects the strategy: joining (default), subscribe (legacy plain SUBSCRIBE),
or fetch (legacy standalone FETCH).
The bundled mlmsub client connects to a single namespace (default: cmsf/clear,
configurable via -namespace). It subscribes to the first video and audio track
from the catalog or tracks that match -videoname, -audioname.
For subtitles, see below.
Subtitle Tracks
The publisher generates subtitle tracks dynamically, showing UTC timestamp and group number. Two subtitle formats are supported:
- WVTT (WebVTT in CMAF) - codec:
wvtt - STPP (TTML in CMAF) - codec:
stpp.ttml.im1t
By default, one Swedish WVTT track (subs_wvtt_sv) and one English STPP track (subs_stpp_en) are created.
You can configure multiple languages:
# Multiple languages for both formats
./mlmpub -subswvtt "en,sv,de" -subsstpp "en,fr"
# Only WVTT subtitles
./mlmpub -subswvtt "en,sv" -subsstpp ""
# No subtitles
./mlmpub -subswvtt "" -subsstpp ""
Subtitle track names follow the pattern subs_wvtt_{lang} and subs_stpp_{lang}.
To receive subtitles with the mlmsub subscriber:
# Subscribe to WVTT subtitles
./mlmsub -subsout subs.mp4 -subsname wvtt
# Subscribe to a specific language
./mlmsub -subsout subs_sv.mp4 -subsname subs_wvtt_sv
In-band CTA-608 Captions
With -cc608, the publisher injects auto-generated CTA-608 CC1 captions into
the video elementary stream itself, so they travel inside the coded frames
rather than as a separate track:
./mlmpub -cc608
One self-contained caption is generated per MoQ group (one wall-clock second), with the UTC time on row 13 (white) and the group number on row 14 (yellow) — the same self-describing clock as the subtitle tracks, which makes caption timing directly verifiable against the picture.
Caption mode
-cc608mode selects how the caption reaches the screen, covering all three
CTA-608 presentation styles. Every mode keeps every group independent: a group's
whole caption is derivable from that group's samples alone, so a subscriber
joining at an arbitrary group is correct immediately.
-cc608mode | On screen | Frame it lands on (25 fps, 1 s group) |
|---|---|---|
paint-on (default) | Screen clears on the group's first frame, then the caption grows two characters per frame and stands for the rest of the group | first characters at frame 4, complete at frame 17 |
roll-up2, roll-up3, roll-up4 | Types onto the base row the same way, but scrolls the window up before each line instead of clearing the screen. The digit is the window size (RU2/RU3/RU4); bare roll-up means roll-up2 | first characters at frame 5, complete at frame 19 |
pop-on | Built invisibly, then the whole caption flips on at once — the classic broadcast style | nothing until frame 18, then displayed into the next group |
./mlmpub -cc608 # paint-on
./mlmpub -cc608 -cc608mode roll-up3 # roll-up, 3-row window
./mlmpub -cc608 -cc608mode pop-on # classic pop-on
Paint-on is the default because a MoQ group is one second and carries exactly one
cue, so the visible transition decides the display interval. Its caption is
displayed over the second it names; pop-on's build drains one byte pair per frame,
so the flip lands three-quarters through the group and the caption reading
GRP n is mostly on screen during group n+1. Aligning pop-on would mean
spending the previous group's frames on this group's build (go-608's
WithFlipAtCueStart), which is exactly the cross-group dependency that low
latency and independent groups rule out. The progressive typing doubles as a
liveness tell.
Roll-up animates like paint-on and finishes two frames later, the cost of the extra carriage return it sends per line. Two details are worth knowing:
- The window size changes the wire, not the picture. With one cue per group
and the window reset at each group's first frame, only the two rows a group
writes for itself are ever filled, so
roll-up2,roll-up3androll-up4display identically here. What differs is the RU2/RU3/RU4 mode code the decoder receives, which is the point for exercising a decoder against all three. - The window is reset per group, not carried. go-608 can carry a roll-up
window across units (
WithRollUpCarry), which is how broadcast roll-up fills to its full depth — but the displayed rows would then depend on the preceding groups, and a subscriber joining mid-stream would see a partly filled window. moqlivemock always resets, keeping groups independent in display as well as in data.
All three video codecs carry the captions, in every packaging (CMAF, LOCMAF, LOC and moq-mi — note that moq-mi video is AVC-only) and including the encrypted namespaces, where the captions are injected before encryption and so ride inside the ciphertext:
| Codec | Carriage |
|---|---|
AVC (avc1/avc3) | user_data_registered_itu_t_t35 SEI NAL unit before the first VCL NALU |
HEVC (hvc1/hev1) | prefix-SEI NAL unit before the first VCL NALU |
AV1 (av01) | metadata_itu_t_t35 OBU before the first frame OBU |
The cc_data() payload and its T.35/GA94 header are identical in all three;
only the envelope differs. Captions are generated by
Eyevinn/go-608.
When (and only when) -cc608 is set, the MSF/CMSF catalogs advertise the
captions on each video track with an accessibility descriptor
(urn:scte:dash:cc:cea-608:2015, value CC1=eng). The catalogless moq-mi
namespace has nowhere to signal this, so its captions are in-band only.
Requirements
- Go 1.25 or later
Installation and Usage
As usual with Go, run
go mod tidy
to get up and running.
There are three commands
mlmpubis the server and publishermlmsubis the client and subscribermlmtestis an interop test client for the moq-interop-runner
The content used is in the assets/test10s directory, and was
generated using the tools in utils/contentgen.
To run the system, first start the publisher
cd cmd/mlmpub
go run .
You can also build the binary and then run it
cd cmd/mlmpub
go build .
./mlmpub
You can also specify options for the publisher:
./mlmpub -audiobatch 4 -videobatch 2
In another shell, start the subscriber and choose if the video, the audio, or a muxed combination should be output, e.g.
cd cmd/mlmsub
go run . -muxout - | ffplay -
or build it similarly to mlmpub before you run it. This time with some other options
cd cmd/mlmsub
go build .
./mlmsub -videoname 600 -audioname scale -loglevel debug -muxout - | ffplay -
to directly play with ffplay. There are more options to change the loglevel, choose track etc.
The subscriber will connect to the publisher and start receiving video and audio frames if some tracks are selected.
Use with Eyevinn's browser player
The browser player warp-player has been created to match the mlmpub publisher. It will subscribe to and read a catalog. One can then choose video and audio tracks and start playing synchronized video and audio with configurable latency.
For that to work, one either need certificates or use of the fingerprint mechanism.
Using mkcert (recommended for development)
One way to do that is with mkcert:
> mkcert -key-file key.pem -cert-file cert.pem localhost 127.0.0.1 ::1
> mkcert -install
> go run . -cert cert.pem -key key.pem
Using certificate fingerprint
For browsers that support WebTransport certificate fingerprints (e.g., Chrome), you can use self-signed certificates without installing them. This is especially useful when running the server locally.
Run mlmpub with fingerprint support:
> go run . -sideport 8081
This will automatically generate a WebTransport-compatible certificate with:
- ECDSA algorithm (not RSA)
- 14-day validity (WebTransport maximum)
- Self-signed
Alternatively, you can use your own certificate (e.g., generated with the included generate-webtransport-cert.sh script):
cd cmd/mlmpub
./generate-webtransport-cert.sh
go run . -cert cert-fp.pem -key key-fp.pem -sideport 8081
This will:
- Start the MoQ server on port 4443 (default address is
0.0.0.0:4443, listening on all interfaces) - Start an HTTP side server on port 8081 serving
/fingerprintand/clearkey - Validate that the certificate meets WebTransport requirements
The warp-player can then connect using:
- Server URL:
https://localhost:4443/moqorhttps://127.0.0.1:4443/moq - Fingerprint URL:
http://localhost:8081/fingerprintorhttp://127.0.0.1:8081/fingerprint
Notes:
- The side server is disabled by default (
-sideport 0). Enable it when using certificate fingerprints or ClearKey/ECCP encryption. - If no certificate files are provided, mlmpub will generate WebTransport-compatible certificates automatically.
Using DRM
moqlivemock supports two independent content protection modes that can run simultaneously:
ClearKey / ECCP (explicit key)
Use -kid, -iv, and optionally -cenckey flags. If no cenc key is provided, the
key-id is used as the key. The ClearKey license endpoint is served at /clearkey on
the side server, so -sideport must be set. For production behind a reverse proxy,
use -laurl to specify the external license URL announced in the catalog.
/clearkey serves the configured content key — -cenckey when set, otherwise the
key-id — for the configured -kid, and answers 404 for any other key-id. -kid
and -cenckey may therefore differ.
# Local development
go run . -kid 39112233445566778899aabbccddeeff -iv 41112233445566778899aabbccddeeff -scheme cbcs -sideport 8081
# Behind a reverse proxy (e.g. Caddy forwarding /clearkey → localhost:8081/clearkey)
go run . -kid 39112233445566778899aabbccddeeff -iv 41112233445566778899aabbccddeeff -scheme cbcs \
-sideport 8081 -laurl https://moqlivemock.demo.osaas.io/clearkey
This announces the cmsf/eccp-cbcs namespace with tracks like video_400kbps_avc_eccp (each also offered as a _locmaf variant).
Commercial DRM (CPIX)
Use -drmpath pointing to a config JSON file in the same format as assets/testdrm/drm_config_test.json.
Supported systems: Widevine, PlayReady, FairPlay.
go run . -drmpath ../../assets/testdrm/drm_config_test.json
This announces the cmsf/drm-{scheme} namespace with tracks like video_400kbps_avc_drm (each also offered as a _locmaf variant).
Both simultaneously
Both modes can be active at the same time, each with independent encryption keys:
go run . -drmpath ../../assets/drm/drm_config.json \
-kid 39112233445566778899aabbccddeeff -iv 41112233445566778899aabbccddeeff -scheme cbcs \
-sideport 8081 -laurl https://moqlivemock.demo.osaas.io/clearkey
This announces three CMSF namespaces: cmsf/clear, cmsf/drm-cbcs, and cmsf/eccp-cbcs. Each catalog carries both the CMAF and the LOCMAF (_locmaf) track variants.
Subscriber examples
The subscriber uses information from the catalog to make license requests, so no extra flags are needed except choosing the right namespace and track names:
# Clear content (default namespace)
go run . -muxout - | ffplay -
# ECCP-protected content
go run . -namespace cmsf/eccp-cbcs -videoname _eccp -audioname _eccp -muxout - | ffplay -
# DRM-protected content
go run . -namespace cmsf/drm-cbcs -videoname _drm -audioname _drm -muxout - | ffplay -
# LOC packaging — AVC reframed to AnnexB, AAC reframed to ADTS
go run . -namespace msf/clear -videoout video.h264 -audioout audio.aac
ffplay video.h264
ffplay audio.aac
# moq-mi packaging — raw moqmi payloads written through unchanged
go run . -namespace moq-mi/clear -videoout video0.bin -audioout audio0.bin
# LOCMAF variant (inside cmsf/clear) — select the _locmaf tracks
go run . -namespace cmsf/clear -videoname _avc_locmaf -audioname _aac_locmaf -muxout - | ffplay -
LOCMAF test assets and round-trip tooling
The reference test-asset generator (golden-vector corpus) and the round-trip
fidelity/overhead tool moved out of this repository together with the codec.
They now live in the locmaf CLI in the
github.com/Eyevinn/locmaf module; see that
repository for usage.
QUIC / WebTransport Configuration
Since quic-go v0.59.0 and webtransport-go v0.10.0, the QUIC config must enable
EnableStreamResetPartialDelivery in addition to EnableDatagrams. Without it,
WebTransport connections will fail with ERR_METHOD_NOT_SUPPORTED in the browser.
For WebTransport servers, webtransport.ConfigureHTTP3Server(h3Server) must also be
called before serving connections. This sets the ENABLE_WEBTRANSPORT HTTP/3 setting
that browsers require during the WebTransport handshake.
Example QUIC config:
&quic.Config{
EnableDatagrams: true,
EnableStreamResetPartialDelivery: true,
}
Development
Use plain Go environment, with go 1.25 or later. The Makefile helps out with some tasks.
Contributing
See CONTRIBUTING
License
This project is licensed under the MIT License, see LICENSE. Some code is based on [moqtransport][moqtransport which is also licensed under MIT]
Support
Join our community on Slack where you can post any questions regarding any of our open source projects. Eyevinn's consulting business can also offer you:
- Further development of this component
- Customization and integration of this component into your platform
- Support and maintenance agreement
Contact sales@eyevinn.se if you are interested.
About Eyevinn Technology
Eyevinn Technology is an independent consultant firm specialized in video and streaming. Independent in a way that we are not commercially tied to any platform or technology vendor. As our way to innovate and push the industry forward we develop proof-of-concepts and tools. The things we learn and the code we write we share with the industry in blogs and by open sourcing the code we have written.
Want to know more about Eyevinn and how it is to work here. Contact us at work@eyevinn.se!