Skip to content

dmorn/moqx

Repository files navigation

moqx

moqx is an Elixir Media over QUIC library.

It provides a QUIC transport boundary backed by quicer for building MOQT implementations in Elixir. The transport boundary keeps protocol code independent from the concrete QUIC backend and allows tests to use deterministic support transports.

Protocol documents

moqx currently implements Cloudflare's deployed MOQT draft-14 protocol over native QUIC. Additional protocol implementations are deferred until this path is ready for production consumers such as Membrane plugins.

Core references:

Installation

# mix.exs
{:moqx, "~> 0.7.1"}

Cloudflare draft-14 subscriber

Protocol selection is explicit; the endpoint never selects an implementation implicitly. Cloudflare's public Big Buck Bunny catalog can be requested with:

{:ok, client} =
  MOQX.connect("moqt://draft-14.cloudflare.mediaoverquic.com:443",
    protocol: :cloudflare_draft_14
  )

catalog_track = %MOQX.TrackRef{namespace: ["bbb"], track: ".catalog"}
{:ok, subscription} = MOQX.subscribe(client, catalog_track)

receive do
  {:moqx, ^client,
   %MOQX.Event.CatalogReceived{catalog: %MOQX.Catalog{} = catalog}} ->
    catalog.tracks
end

This path uses native QUIC with ALPN moq-00, negotiates MOQT draft-14, subscribes with LargestObject, and decodes the CMSF catalog delivered on a subgroup stream. It does not use FETCH.

Catalog tracks can be subscribed directly. Delivered objects retain their subscription, group, subgroup, object, and priority coordinates:

{:ok, video} = MOQX.Catalog.select_h264(catalog)
{:ok, subscription} = MOQX.subscribe(client, MOQX.Catalog.Track.track_ref(video))

receive do
  {:moqx, ^client,
   %MOQX.Event.ObjectReceived{
     object: %MOQX.Object{subscription: ^subscription} = object
   }} ->
    object.payload
end

For CMAF H.264, MOQX.CMAF.capture/4 subscribes to the advertised initialization and media tracks, orders received objects by their protocol coordinates, writes a fragmented MP4 atomically, and unsubscribes its temporary subscriptions:

{:ok, report} =
  MOQX.CMAF.capture(client, catalog, "/tmp/cloudflare-bbb.mp4",
    objects: 120,
    timeout: 30_000
  )

The runnable external example performs the complete flow:

mix run scripts/cloudflare_h264_capture.exs /tmp/cloudflare-bbb.mp4 120

ffprobe -v error -show_streams /tmp/cloudflare-bbb.mp4
ffmpeg -y -i /tmp/cloudflare-bbb.mp4 -map 0:v:0 -c:v copy \
  -bsf:v h264_mp4toannexb -an -f h264 /tmp/cloudflare-bbb.h264
ffmpeg -v error -f h264 -i /tmp/cloudflare-bbb.h264 -f null -

MOQX.unsubscribe/2 sends the selected protocol's unsubscribe message; MOQX.close/2 closes the connection. Relay rejections are delivered as MOQX.Event.SubscriptionFailed, while MOQX.Event.SubscriptionDone is emitted only after every stream advertised by PUBLISH_DONE has been processed or the subscription's :delivery_timeout has elapsed.

All application-facing output uses typed MOQX.Event.* structs inside the stable {:moqx, client, event} envelope. By default events go to the process that calls MOQX.connect/2; shared connection owners can choose a router:

{:ok, client} =
  MOQX.connect(endpoint,
    protocol: :cloudflare_draft_14,
    events_to: router_pid
  )

Downstream projects can run hermetic protocol tests with the packaged in-memory transport. It must be selected explicitly and is never chosen by production facade code:

{:ok, network} = MOQX.Testing.Transport.start_network()

MOQX.connect("moqt://localhost:443",
  protocol: :cloudflare_draft_14,
  transport: {MOQX.Testing.Transport, network: network, profile: :draft_14}
)

Cloudflare draft-14 publisher

Publishing uses the same explicitly selected client. Applications declare a namespace and tracks, then supply protocol-neutral objects; Cloudflare request IDs, track aliases, and inbound relay subscriptions remain implementation details:

{:ok, publication} = MOQX.publish(client, ["live", "camera-1"])

{:ok, video} =
  MOQX.add_track(client, publication, "video.m4s", retention: :live)

:ok =
  MOQX.publish_object(client, video, %MOQX.Object{
    group_id: 42,
    subgroup_id: 0,
    object_id: 0,
    publisher_priority: 127,
    payload: fragment
  })

:ok = MOQX.finish_publication(client, publication)

Retention is application policy: :live discards objects when no subscriber is active, :latest retains one object for catalog or initialization tracks, and :all replays bounded static content.

Inbound subscriptions are accepted automatically by default. A publisher can instead inspect, authorize, and provision each request before deciding it:

{:ok, publication} =
  MOQX.publish(client, ["live", "camera-1"],
    inbound_subscriptions: :controlled,
    subscription_decision_timeout: 5_000,
    max_pending_subscriptions: 128
  )

receive do
  {:moqx, ^client,
   %MOQX.Event.PublicationSubscriptionRequested{request: request}} ->
    {:ok, video} =
      MOQX.add_track(client, publication, request.track.track, retention: :live)

    :ok = MOQX.accept_subscription(client, request, video)
end

MOQX.reject_subscription/3 accepts a protocol-neutral MOQX.SubscriptionRejection. Pending requests are connection-scoped, bounded by the configured count and timeout, and are invalidated by unsubscribe, publication termination, or connection closure. Request events preserve priority, forward state, group order, all four draft-14 filters, repeated authorization parameters, delivery timeout, and unknown extensions.

Controlled acceptance supports ascending delivery. A request for descending delivery remains pending and accept_subscription/4 returns {:error, :unsupported_group_order}; the application should reject it with :not_supported. Publisher-selected order defaults to ascending and can be confirmed explicitly with group_order: :ascending in the acceptance options.

MOQX.CMAF.publish_file/3 prepares a fragmented MP4 as a CMSF .catalog, an initialization track, and retained media fragments:

{:ok, published} =
  MOQX.CMAF.publish_file(client, "/tmp/input.mp4",
    namespace: ["live", "camera-1"]
  )

Managed relay credentials are explicit caller input. The credential value is wrapped so both its value and the resulting sensitive wire actions have redacted inspection:

{:ok, client} =
  MOQX.connect(endpoint,
    protocol: :cloudflare_draft_14,
    authorization: MOQX.Secret.new(token)
  )

MOQX encodes that value using draft-14's standard AUTHORIZATION TOKEN parameter. Token acquisition, permissions, storage, and rotation remain relay and application concerns; MOQX does not read process or application configuration for credentials.

The manual publisher/subscriber roundtrip accepts a token file so the token is not placed in shell history. Omit it for Cloudflare's public relay:

mix run scripts/cloudflare_h264_publish.exs /tmp/input.mp4 \
  --endpoint moqt://draft-14.cloudflare.mediaoverquic.com:443 \
  --namespace moqx-test/unique-publisher \
  --output /tmp/roundtrip.mp4 \
  --timeout 120000

# For a managed relay, additionally pass:
# --authorization-file /path/to/temporarily-mounted-token

ffprobe -v error -show_streams /tmp/roundtrip.mp4
ffmpeg -v error -i /tmp/roundtrip.mp4 -map 0:v:0 -f null -

Development

mix deps.get
mix test
mix ci

Default tests are fast and hermetic. Real QUIC checks are tagged as ExUnit integration tests and are excluded by default.

The public Cloudflare interop check is independently selectable and depends on the availability of an external service:

mix test --only integration test/integration/cloudflare_catalog_test.exs

The repo-owned Cloudflare draft-14 roundtrip runs both MOQX and a real relay in Docker. It publishes a catalog and media object through the public API, subscribes through a second public client, verifies delivery, and exercises graceful publication completion:

scripts/run_moq_rs_integration.sh

The harness builds Cloudflare's moq-rs draft-ietf-moq-transport-14 branch at the immutable revision 69302d3dc2422e93b8a1d62f853a6759aa9e5468. Do not replace that pin with main: upstream main has moved to a later MOQT draft and no longer negotiates the draft-14 moq-00 ALPN. The MOQX test runner joins the Compose network directly so the QUIC path is identical on Docker Desktop and Linux CI rather than depending on host UDP forwarding.

ExUnit never starts Docker. The script owns Compose startup and cleanup, and the same script is the Cloudflare draft-14 relay roundtrip CI job. Future relay variants should add separately pinned Compose services, tagged public-API tests, and runner scripts following this boundary; they must not add an implicit protocol fallback or overload this Cloudflare test.

To run the caller-managed QUIC integration harness:

docker compose -f docker-compose.integration.yml up -d --wait
mix test --only integration

ExUnit does not start Docker. Stop the harness when finished:

docker compose -f docker-compose.integration.yml down

The harness provisions self-signed certificates under .tmp/integration-certs/ (via scripts/gen-loopback-certs.sh) and runs the repo-owned reference QUIC server from bench/quicprobe on UDP port 4433. The generated CA/server certificate is valid for ~100 years — it only authenticates a localhost QUIC handshake, so it is intentionally long-lived to avoid expiry friction. To (re)generate the loopback certificates outside the harness:

scripts/gen-loopback-certs.sh .tmp/integration-certs

The script is idempotent: it reuses an existing certificate unless it is missing or nearly expired.

For manual debugging, run the reference CLI directly:

go run ./bench/quicprobe server --addr :4433 \
  --cert .tmp/integration-certs/server.pem \
  --key .tmp/integration-certs/server-key.pem \
  --alpn moqx-test

go run ./bench/quicprobe client --addr 127.0.0.1:4433 \
  --ca .tmp/integration-certs/ca.pem \
  --alpn moqx-test \
  --bidi-echo hello

For reference stream-pressure experiments, the client can emit structured quicprobe-v1 JSON:

go run ./bench/quicprobe client --addr 127.0.0.1:4433 \
  --ca .tmp/integration-certs/ca.pem \
  --alpn moqx-test \
  --json \
  --stream-direction bidirectional \
  --stream-count 2 \
  --payload-size 1200 \
  --payload-count 100

License

MIT

About

No description, website, or topics provided.

Resources

License

Stars

0 stars

Watchers

0 watching

Forks

Packages

 
 
 

Contributors

Languages