English | Español | Français | Italiano | 日本語 | Português | 中文
moqxr is a C++20 OpenMOQ publisher for Linux, macOS, and Windows.
It packages file and live media for Media over QUIC Transport (MOQT), builds MSF/CMSF catalogs and publish plans, and publishes through picoquic-backed Raw QUIC or WebTransport connections.
- Parses fragmented MP4 (
ftyp+moov+moof/mdat) and remuxes progressive MP4 into fragmented media objects. - Extracts track metadata and RFC 6381 codec identifiers, including HEVC signaling and
hev1tohvc1normalization. - Builds MSF/CMSF version 1 catalogs, initialization data, optional media timelines, and SAP event timelines.
- Detects and signals existing CMAF CENC content protection for batch input, live fragmented MP4 on stdin, and CTE LL-DASH ingest. It does not encrypt or decrypt media.
- Emits catalog, initialization, media, probe, and publish-plan files for local inspection.
- Publishes with the main CLI's supported MOQT draft profiles: draft 16, draft 18 (default), draft 21, and draft 22.
- Publishes over Raw QUIC or WebTransport when picoquic and picotls are available.
- Accepts live fragmented MP4 from stdin, MPEG-TS over SRT when libsrt is available, and CMAF over HTTP/1.1 CTE LL-DASH ingest (chunked or fixed-length requests).
- Parses MSF URLs with
--urland prints the catalog discovery URL with--print-msf-urls. - Supports ordered relay failover with repeated
--endpointoptions and configurable same-endpoint retries with--retry. - Provides C++ Publisher API examples for FFmpeg-generated live media, CAT4MOQ authorization, and MPEG-2 TS/M2TS packaging.
- Optionally routes publishing through the moq5 C11 Media-over-QUIC library for drafts 16 and 18.
Build and test:
cmake -S . -B build -DOPENMOQ_RUN_PICOQUIC_SMOKE_TESTS=OFF
cmake --build build
ctest --test-dir build --output-on-failureThe default build creates the openmoq-publisher executable and the static Publisher library: build/libopenmoq_publisher.a on Linux/macOS, or build\<config>\openmoq_publisher.lib with Visual Studio generators on Windows.
Check which build you are running:
./build/openmoq-publisher --versionThis prints openmoq-publisher <version> (commit <hash>). The version is project(VERSION) from CMakeLists.txt; a build made from the matching v<version> release tag reports it as-is, while any other build appends -dev+g<commit> (plus .dirty when there are uncommitted changes) so a bug report identifies the exact source. --help prints the same banner. Embedders get the same values from openmoq/publisher/version.h (version(), version_full(), git_commit()).
Inspect a publish plan:
./build/openmoq-publisher --input sample.mp4 --dump-planEmit the catalog and packaged media objects:
./build/openmoq-publisher --input sample.mp4 --emit-dir out/CMAF remains the default. Add --packaging locmaf to opt into
LOCMAF-01, packaging version 0.3, for
file, live stdin, SRT, or CTE LL-DASH publishing on the default backend. The
publisher retains CMAF initialization and encoded sample bytes, signals LOCMAF in the
catalog, and emits full headers so late joins and queue drops do not depend on
earlier delta objects. File input retains original CMAF chunks, including
encryption and auxiliary boxes; source constraints can keep an entire track on
CMAF. A live track that becomes ineligible fails instead of changing its
advertised packaging. --stream-per-object, --coalesce-cmaf-chunks, and the
libmoq publishing backend are currently incompatible with LOCMAF.
Multiplexed fragments must be demuxed first (for FFmpeg, use separate_moof).
--emit-dir writes LOCMAF media as .locmafobj alongside the usual catalog and
initialization segments. For a playback equivalence check against a moq-playa
checkout with LOCMAF support, run
bun scripts/test-locmaf-playa.mjs build/openmoq-publisher ../moq-playa
(requires FFmpeg and ffprobe). See the LOCMAF quickstart,
C++ API configuration, and
validation coverage.
LOC-04 publishing is available with --packaging loc --draft 18 (or --draft 21 / --draft 22) on the native
backend. It sends individual H.264/AAC frames with timestamp, timescale, and
codec configuration properties; CMAF remains the default. See the
LOC quickstart for scope and constraints.
Publish to a relay with the default draft-18 profile:
OPENMOQ_PICOQUIC_TRACE=1 ./build/openmoq-publisher \
--input sample.mp4 \
--endpoint moqt://relay.example.com:443/moq \
--namespace media \
--draft 18 \
--forward 0 \
--timeout 30 \
--pacedFor file input, --forward 0 announces the namespace and waits for inbound
SUBSCRIBE requests for catalog and media tracks. The catalog is available
without --publish-catalog; that option proactively sends catalog PUBLISH
and requires the relay to accept it. --forward 1 proactively publishes both
catalog and media tracks and sends objects when the relay permits forwarding.
If the relay reports PUBLISH is not supported, use --forward 0 without
--publish-catalog. See publishing modes and troubleshooting.
A printed connection_id= confirms transport and MOQT setup only; it does not
confirm namespace acceptance or a downstream subscription.
Add endpoints in preferred order and use --retry N to retry each current
endpoint N times before advancing. For example, --endpoint primary:4433 --endpoint backup:4433 --retry 2 makes up to three attempts at the primary,
then up to three attempts at the backup. See the
CLI quick start for failure
handling and live-source details.
On Windows, replace ./build/openmoq-publisher with build\Release\openmoq-publisher.exe or the path for the selected build configuration.
The CLI exposes one live source at a time:
| Source | CLI selection | Input | Notes |
|---|---|---|---|
| Fragmented MP4 | --live-source stdin --input - |
CMAF/fMP4 on standard input | Available on all supported platforms |
| SRT | --live-source srt --srt-config FILE |
MPEG-TS over SRT | Requires libsrt; CENC metadata is unavailable in this path |
| CTE LL-DASH | --live-source dash --dash-listen HOST:PORT |
CMAF POST or PUT requests, chunked or with Content-Length |
Listener currently requires a Unix-like platform |
The publisher reads fragmented MP4 from standard input. The input must contain ftyp + moov + moof/mdat fragments, and the publisher will emit MSF/CMSF catalogs and media objects.
ffmpeg -hide_banner -stream_loop -1 -re \
-i fragmented.mp4 \
-map 0:v:0 -map 0:a:0 \
-c:v libx264 -preset veryfast \
-r 30 -g 60 -keyint_min 60 -sc_threshold 0 -bf 0 \
-c:a aac -b:a 128k -ar 48000 -ac 2 \
-movflags +frag_keyframe+empty_moov+default_base_moof+separate_moof \
-f mp4 pipe:1 | ./build/openmoq-publisher \
--transport webtransport \
--endpoint https://127.0.0.1:4433/moq \
--namespace live/stdin \
--draft 16 \
--publish-catalog \
--live-source stdin \
--input -The publisher is an SRT caller. Create /tmp/srt_callers.json with the SRT listener address and MPEG-TS/CMAF settings:
{
"srt_callers": [
{
"id": "cam1",
"srt": {
"mode": "caller",
"host": "127.0.0.1",
"port": 9000,
"latency_ms": 120
},
"mpegts": {
"auto_detect_program": true,
"program_number": null,
"video_pid": null,
"audio_pid": null
},
"cmaf": {
"fragment_on_keyframe": true,
"empty_moov": true,
"default_base_moof": true,
"separate_moof_per_track": true,
"target_fragment_duration_ms": 1000
}
}
]
}In the first terminal, start an FFmpeg SRT listener that sends MPEG-TS after the publisher connects:
ffmpeg -hide_banner -stream_loop -1 -re \
-i input.mp4 \
-map 0:v:0 -map 0:a:0 \
-c:v libx264 -preset veryfast -r 30 -g 60 -keyint_min 60 -sc_threshold 0 -bf 0 \
-c:a aac -b:a 128k -ar 48000 -ac 2 \
-f mpegts "srt://0.0.0.0:9000?mode=listener&pkt_size=1316"In the second terminal, start the SRT caller and MoQ publisher:
./build/openmoq-publisher \
--live-source srt \
--srt-config /tmp/srt_callers.json \
--endpoint 127.0.0.1:4443 \
--transport raw \
--namespace live/srt \
--draft 16 \
--timeout 120 \
--forward 0Set srt.mode to caller to connect to an existing SRT listener, or to listener to bind the configured host and port and accept an incoming SRT source. Listener mode accepts one connection per configured entry and uses the same MPEG-TS ingest pipeline. Use --forward 1 for an immediate relay smoke test, or keep --forward 0 to wait for subscriber interest.
Start the publisher with an HTTP/1.1 CMAF listener and a MoQ relay target:
./build/openmoq-publisher \
--live-source dash \
--dash-listen 0.0.0.0:8080 \
--dash-path /ingest \
--endpoint https://127.0.0.1:4433/moq \
--transport webtransport \
--namespace live/dash \
--draft 18 \
--publish-catalog \
--forward 1 \
--insecureSend an existing CMAF/fMP4 stream with HTTP/1.1 chunked transfer encoding:
curl -X PUT \
-H 'Transfer-Encoding: chunked' \
-H 'Content-Type: video/iso.segment' \
--data-binary @live-video.cmaf \
http://127.0.0.1:8080/ingest/videoRequests that carry a Content-Length instead of chunked transfer encoding are accepted too, as the DASH-IF ingest specification only recommends chunking for low latency. This is how livesim2 pushes init segments, so its CMAF ingest output can be pointed straight at the listener:
curl -X PUT \
-H 'Content-Type: video/mp4' \
--data-binary @init.cmfv \
http://127.0.0.1:8080/ingest/video/init.cmfvWhen the final path segment has a file extension, such as /ingest/video/init.cmfv followed by /ingest/video/1.cmfv, the enclosing directory (/ingest/video) is the representation path, so media segments find the init segment that preceded them. Paths without an extension, like the FFmpeg recipe below, are used as-is. A request with neither framing header is refused with 411 Length Required, and a declared length above the configured cap (256 MiB by default) with 413 Content Too Large.
FFmpeg can instead create two video representations plus audio and push them directly to the ingest prefix:
ffmpeg -re \
-f lavfi -i "testsrc2=size=1280x720:rate=25" \
-f lavfi -i "anullsrc=r=48000:cl=stereo" \
-filter_complex "[0:v]split=2[v1][v2];[v1]scale=1280:720[v720];[v2]scale=640:360[v360]" \
-map "[v720]" -c:v:0 libx264 -b:v:0 1500k -g 50 -keyint_min 50 -sc_threshold 0 \
-map "[v360]" -c:v:1 libx264 -b:v:1 500k -g 50 -keyint_min 50 -sc_threshold 0 \
-map 1:a -c:a aac -b:a 128k \
-f dash -seg_duration 2 -use_template 1 -use_timeline 0 \
-init_seg_name 'video$RepresentationID$' \
-media_seg_name 'video$RepresentationID$' \
-adaptation_sets "id=0,streams=v id=1,streams=a" \
-multiple_requests 1 -streaming 1 -remove_at_exit 0 \
-window_size 20 -extra_window_size 20 \
http://127.0.0.1:8080/ingest/Each representation path below /ingest maintains independent parser state and produces path-prefixed MoQ track names. Use --forward 1 to send objects immediately, or --forward 0 to wait for subscriber interest. The DASH listener currently requires a Unix-like platform.
See the CLI quick start, FFmpeg recipes, and SRT technical note for additional details.
Each live publisher process numbers its Group IDs from 0. If the process restarts and publishes the same namespace and track names again, its new groups reuse Group IDs that the previous run already published, so two different objects share one Location. MoqTransport section 2.5 suggests that a resuming publisher set its initial Group ID to a value larger than every group it used before, for example one based on the current time. Use --initial-group-id to do that:
./build/openmoq-publisher --input - --initial-group-id time ...--initial-group-id timereads the system clock once at startup and starts at the number of milliseconds since the Unix epoch. This is safe if the previous run published fewer groups than the milliseconds that passed between its start and the restart, and the system clock did not step backwards in between.--initial-group-id Nstarts at the unsigned integerN, which must not exceed 2^62-1.- The default is 0. The option applies to the media tracks and the catalog of the live sources (stdin, SRT, and CTE LL-DASH) on every draft; it is rejected for file input. On draft 22, a GOAWAY migration still resumes above the groups the previous session sent. The moq5 publishing backend numbers Group IDs itself and rejects a non-zero value.
- The value is fixed for the process. A
--retryor endpoint failover reconnect inside the same process starts again from it, so those attempts can reuse Group IDs; the option covers a process restart, not an in-process reconnect.
The opt-in moq5 backend routes batch, live stdin, live SRT, and live-object publishing through moq5's service tier. The service tier handles catalog publication, CMSF/CMAF validation, subscriber-demand gating, bounded backpressure, and graceful transport draining.
CMake fetches current openmoq/moq5 main when this backend is enabled. Set
OPENMOQ_LIBMOQ_SOURCE_DIR only to use a local or offline source override:
cmake -S . -B build-libmoq \
-DOPENMOQ_USE_LIBMOQ_PUBLISHER=ON
cmake --build build-libmoq
ctest --test-dir build-libmoq --output-on-failureThe default build keeps the built-in transport path. See docs/build.md for backend status, dependency discovery, and configuration details.
CAT4MoQ on this backend requires moq5's MOQ_SERVICE_AUTH_API_VERSION >= 1.
Use -DOPENMOQ_LIBMOQ_SOURCE_DIR=../moq5 with the corresponding
feature/cat4moq checkout while testing the coordinated change. Older moq5
dependencies reject configured credentials before connecting. See the
auth design for backend limits and compatibility profiles.
Enable examples explicitly with cmake -S . -B build -DOPENMOQ_BUILD_EXAMPLES=ON,
then build them with cmake --build build.
| Example | Target | Purpose |
|---|---|---|
| Psychedelic live publisher | openmoq-publisher-psychedelic-example |
Runs one FFmpeg audio/video pipeline through Publisher::publish_live(...) |
| CAT4MOQ authorization | openmoq-publisher-auth-example |
Publishes deterministic live objects with token files or a Catapult token command |
| MSFTS publisher | openmoq-publisher-msfts-example |
Publishes packet-aligned MPEG-2 TS or M2TS objects through Publisher::publish_live_objects(...) |
The MSFTS example follows the local text draft in examples/msfts-publisher/docs/, discovers PAT/PMT data, selects one program, filters unrelated PIDs, and emits an MSF version 1 catalog with packaging: "m2ts".
This example discovers single-packet PAT/PMT sections within the first 4096
source packets. It uses fixed 10 ms object pacing rather than PCR-derived
timing, publishes one group, and declares m2tsRandomAccess: false; it does
not identify random-access boundaries or track program changes.
./build/examples/msfts-publisher/openmoq-publisher-msfts-example \
--input sample.m2ts \
--endpoint https://relay.example.com:443/moq \
--namespace media.msfts \
--track transport \
--draft 17Add --program NUMBER to select a program, --packets-per-object COUNT to change object sizing, or --insecure only when the relay certificate is intentionally untrusted.
| Topic | Link |
|---|---|
| Build and dependencies | docs/build.md |
| CLI and live-ingest quick start | docs/quickstart.md |
| Testing | docs/testing.md |
| Design overview | docs/design.md |
| FFmpeg input recipes | docs/ffmpeg.md |
| SRT ingest technical note | docs/srt-ingest-technical-note.md |
| Relay interoperability | docs/relay-interop.md |
| C++ Publisher API | docs/publisher-api.md |
| CAT4MOQ auth example | examples/auth/README.md |
| CAT4MoQ profiles and implementation status | docs/cat4moq-design.md, docs/cat4moq-plan.md |
| MSFTS text draft | examples/msfts-publisher/docs/draft-gregoire-moq-msfts.txt |
| Protocol mapping | docs/protocol-mapping.md |
| WebTransport compliance | docs/webtransport-compliance.md |
| Local MSF version 1 draft | docs/draft-ietf-moq-msf-01.txt |
| Local CMSF version 1 draft | docs/draft-ietf-moq-cmsf-01.txt |
| macOS DASH shutdown behavior | docs/macos-accept-shutdown-quirk.txt |
| Project status and roadmap | docs/status.md |
Localized Publisher API guides are available in Spanish, French, Italian, Japanese, Portuguese, and Chinese.
include/openmoq/publisher: public C++ headerssrc: static library and CLI implementationtests: CTest-based unit and integration coveragedocs: local draft text, protocol notes, integration guides, and design referencesexamples: Publisher API integrations.github/workflows/ci.yml: Linux, macOS, and Windows CI.github/workflows/release.yml: CLI, header, and static-library release artifacts
The main openmoq-publisher CLI accepts drafts 16, 18, 21, and 22; draft 18 is the default and provides the newer request-stream profile, and draft 21 extends it (LOCATION_FILTER, INCLUDE_PROPERTIES, FILL_PARAMETERS). Draft 22 adds explicit LOCATION_FILTER types while retaining draft-21 publishing semantics. Drafts 21 and 22 are native-backend only; the moq5 backend stays on drafts 16 and 18. Text for drafts 14, 17, 19, and 20 remains in docs/ for implementation history and protocol review, but those versions are not selectable in the main CLI. The separate MSFTS example retains draft 14/16/17/18 selection for draft-specific testing.
The default picoquic backend and the opt-in moq5 backend are both under active interoperability testing. For detailed feature coverage, limitations, and roadmap work, see docs/status.md and docs/protocol-mapping.md.
Originally created by Paul Gregoire for OpenMOQ