moqx
moqx is an Elixir Media over QUIC library with explicit, independent
implementations of standard MOQT draft-18 and MoQ Lite draft-05 over native
QUIC. Protocol selection is explicit; endpoints never imply a protocol and
there is no silent fallback.
The QUIC transport boundary is backed by
quicer. Protocol implementations depend on
MOQX.Transport, while tests can select the deterministic
MOQX.Testing.Transport without changing protocol code.
Protocol documents
- RFC 9000 — QUIC
- RFC 9001 — Using TLS to Secure QUIC
- RFC 9221 — QUIC DATAGRAM
- draft-ietf-moq-transport-18 — Media over QUIC Transport
- draft-lcurley-moq-lite-05 — Media over QUIC Lite
The draft-18 interoperability reference is MOQtail pinned at
0e265d8.
The Lite05 interoperability reference is Curley pinned at
fd47708.
Installation
def deps do
[{:moqx, "~> 0.11.0"}]
end
Standard MOQT draft-18
Draft-18 is the canonical standard implementation and negotiates native-QUIC
ALPN moqt-18:
{:ok, client} =
MOQX.connect("moqt://relay.moqtail.dev:443", protocol: :draft_18)
track = %MOQX.TrackRef{
namespace: ["moqtail", "testsrc"],
track: "catalog"
}
{:ok, subscription} =
MOQX.subscribe(client, track,
profile: :moqtail_cmsf,
start: :next_group,
priority: 127
)
receive do
{:moqx, ^client,
%MOQX.Event.CatalogReceived{
subscription: ^subscription,
catalog: %MOQX.Catalog{} = catalog
}} ->
catalog
end
Publishing has two readiness boundaries: PublicationReady accepts the
namespace, then PublicationSubscriberJoined accepts each PUBLISH track
request before objects may be sent.
{:ok, publication} = MOQX.publish(client, ["example", "camera"])
receive do
{:moqx, ^client, %MOQX.Event.PublicationReady{publication: ^publication}} -> :ok
end
{:ok, video} = MOQX.add_track(client, publication, "video", delivery: :subgroup)
receive do
{:moqx, ^client, %MOQX.Event.PublicationSubscriberJoined{track: ^video}} ->
:ok = MOQX.publish_object(client, video, object)
end
:ok = MOQX.finish_publication(client, publication)
Draft-18 supports subgroup and datagram delivery, all four subscription filter
forms, request updates, controlled inbound subscriptions, extension
preservation, and delivery draining through PUBLISH_DONE. Setup uses paired
unidirectional control streams; each subscription and publication request owns
a bidirectional request stream.
Exact object and subgroup-completion roundtrips were verified against MOQtail and Cloudflare public draft-18 relays on 2026-09-15. External services can change independently, so rerun the public integration test for current evidence.
MoQ Lite draft-05
Lite05 remains an independent protocol implementation selected as
:moq_lite_05. It supports timestamped groups, publication and subscription,
track provisioning, broadcast discovery, and the HANG application profile.
{:ok, client} = MOQX.connect("moqt://cdn.moq.dev:443", protocol: :moq_lite_05)
{:ok, publication} = MOQX.publish(client, ["anon", "example"])
{:ok, track} = MOQX.add_track(client, publication, "video")
See the pinned HANG/Lite05 interoperability evidence for the exact verified scope and playback limitations.
Application catalog profiles
Wire protocol selection is connection-scoped; catalog profiles are selected per subscription or published catalog. Raw objects are the default.
| Profile | MOQT draft-18 | MoQ Lite 05 | Catalog track |
|---|---|---|---|
:none |
yes | yes | any, opaque |
:cloudflare_cmsf |
yes | yes | .catalog |
:moqtail_cmsf |
yes | yes | catalog |
:hang |
rejected | yes | catalog.json / catalog.json.z |
The matrix describes codec composition, not certification against every relay. Cloudflare CMSF and MOQtail CMSF remain distinct application formats even though their retired transport drafts are no longer implemented.
Catalog values preserve advertised codec, packaging, dimensions, bitrate, timescale, and initialization metadata so downstream media libraries can use the tracks. MOQX does not select codecs, parse or mux media containers, manage playback, or perform media file IO; object payloads remain opaque.
Events and ordering
Application output uses typed MOQX.Event.* structs inside
{:moqx, client, event}. Events default to the connecting process; a shared
owner may pass events_to: router_pid.
Objects are emitted in normalized transport arrival order. One subgroup
preserves stream-local order and emits a typed SubgroupEnded boundary after
its objects. No global coordinate ordering is manufactured across streams.
Applications needing stronger ordering own and bound their reorder policy.
Development
mix deps.get
mix format
mix test
mix credo --strict
Default tests are fast and hermetic. Real QUIC checks are tagged as integration tests and excluded by default.
Run public draft-18 publisher/subscriber checks against MOQtail and Cloudflare:
mix test --only integration test/integration/draft_18_public_relays_test.exs
Run the pinned local MOQtail draft-18 relay harness:
scripts/run_moqtail_draft18_integration.sh
Run the pinned Curley Lite05 harness:
scripts/run_curley_moq_lite_05_integration.sh
ExUnit never starts Docker. The runner scripts own Compose startup and cleanup. The shared integration file also exposes the reference QUIC server:
docker compose -f docker-compose.integration.yml up -d --wait
mix test --only integration
docker compose -f docker-compose.integration.yml down
Loopback TLS certificates are generated under .tmp/integration-certs/ by
scripts/gen-loopback-certs.sh. They are deliberately long-lived and only
authenticate local test names.
License
MIT