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

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