Skip to main content
Pre-release

This release is a pre-release and may not be stable for production use.

aiomoqt - Media over QUIC Transport (MoQT)

aiomoqt is an implementation of MoQT for asyncio, layered on aiopquic.

Overview

aiomoqt implements the MoQT protocol with draft-14 / draft-16 / draft-18 (beta) support, both publish and subscribe roles, H3/WebTransport and raw QUIC transports, and ALPN-based draft negotiation. The architecture extends aiopquic.asyncio.QuicConnectionProtocol. The package ships publisher / subscriber example clients, a benchmark suite, a relay version probe, and a moq-interop-runner-compatible test client.

Features

  • H3/WebTransport and raw QUIC transports
  • Draft-14 / draft-16 / draft-18 negotiation (ALPN moq-00 / moqt-16 / moqt-18, plus WT-Protocol over H3)
  • Draft-16 wire format: delta-encoded param keys, track extensions, unified request/response
  • Draft-18 (beta): vi64 varints, uni-stream control pair, per-request bidi streams, Request-ID-less replies — over both raw QUIC and WebTransport
  • SubgroupHeader / ObjectDatagram flag encoding, delta-encoded object IDs
  • Version-independent control: MOQTRequestError exception across drafts
  • Async context manager for session lifecycle
  • Sync / async response handling on every control message via wait_response
  • High-level publisher: PublishedTrack — stream setup, subgroup writing, pacing
  • High-level subscriber: SubscribedTrack — object reassembly, FETCH / JOIN handling
  • Pluggable message handlers via register_handler()
  • Data publishing via SubgroupHeader streams (ObjectDatagram RX + codec complete; datagram publishing in progress)
  • Data reception via on_object_received callback
  • Low-level message serialization / deserialization for custom protocol work

Installation

Pure Python, requires Python 3.12+ (tested on 3.12, 3.13, 3.14). For a clean, uv-managed .venv, run ./bootstrap_python.sh; otherwise install into your current environment:

uv pip install aiomoqt    # or: pip install aiomoqt

aiopquic (the QUIC transport) installs as a binary wheel automatically. Only Linux (glibc 2.34+, RHEL 9 / Ubuntu 22.04+) and macOS arm64 have prebuilt wheels; other systems pull aiopquic via sdist and need a C build toolchain — see aiopquic install notes.

Reporting issues

Include the full version report in any issue. It captures aiomoqt, aiopquic, and the picoquic + picotls submodule SHAs aiopquic was built from — useful for diagnosing version-pair mismatches across the stack:

python -m aiomoqt.versions   # or the console script: aiomoqt-versions

Sample output:

aiomoqt:   0.9.7.dev7+g0fa185338 (~/src/aiomoqt/aiomoqt) [2026-06-11 11:57]
aiopquic:  0.3.7.dev12+g6eef9caf6 (~/src/aiopquic/src/aiopquic) [2026-06-11 11:58]
  - picoquic:  1.1.49.2 (d6c5653d) [2026-06-05]
  - picotls:   master (bfa67875) [2026-04-20]

Quick Start

Verify install + relay liveness

Confirm the stack and reach a relay before writing any code (probe exits 0 if any draft handshakes):

python -m aiomoqt.versions

python -m aiomoqt.tools.relay_probe --url moqt://moqx-main.ci.openmoq.org:4433
moqt://moqx-main.ci.openmoq.org:4433              QUIC   ✓  draft-14,draft-16,draft-18  (540ms)

python -m aiomoqt.tools.relay_probe --url https://moqx-main.ci.openmoq.org:4433/moq-relay
https://moqx-main.ci.openmoq.org:4433/moq-relay   H3/WT  ✓  draft-14,draft-16,draft-18  (435ms)

Subscriber

import asyncio
from aiomoqt.client import MOQTClient

def on_object(msg, size, recv_time_ms, group_id=None, subgroup_id=None):
    print(f"g={group_id} obj={msg.object_id} {size}B payload={msg.payload}")

async def main():
    client = MOQTClient('relay.example.com', 443, path='moq',
                         use_quic=True, supported_drafts=16)
    async with client.connect() as session:
        await session.client_session_init()
        session.on_object_received = on_object
        await session.subscribe('ns', 'track', wait_response=True)
        await session.async_closed()

asyncio.run(main())

Publisher

import asyncio
from aiomoqt.client import MOQTClient
from aiomoqt.types import MOQTMessageType
from aiomoqt.messages import SubgroupHeader

async def main():
    client = MOQTClient('relay.example.com', 443, path='moq',
                         use_quic=True, supported_drafts=16)
    client.register_handler(MOQTMessageType.SUBSCRIBE, on_subscribe)
    async with client.connect() as session:
        await session.client_session_init()
        await session.publish_namespace('ns', wait_response=True)
        await session.async_closed()  # serve until closed

async def on_subscribe(session, msg):
    """Called when a subscriber requests a track."""
    ok = session.subscribe_ok(request_msg=msg)
    stream_id = session.open_uni_stream()
    hdr = SubgroupHeader(track_alias=ok.track_alias, group_id=0, subgroup_id=0, publisher_priority=0)
    session.stream_write(stream_id, hdr.serialize().data)
    session.stream_write(stream_id, hdr.next_object(payload=b"hello").data)

asyncio.run(main())

The on_subscribe handler above — stream setup, header serialization, object writing — is wrapped by the higher-level PublishedTrack / SubscribedTrack classes in aiomoqt.track. See *_bench.py examples for typical usage.

Control Message API

Control messages support both sync and async patterns via wait_response:

# Blocking — awaits and returns the response message
resp = await session.subscribe('ns', 'track', wait_response=True)

# Non-blocking — returns request, response arrives via handler
req = await session.subscribe('ns', 'track')

Auth

AUTH_TOKEN rides the SETUP handshake (session-level) and any request message (namespace- or track-level). Values are arbitrary bytes — the codec wraps them in the spec Token structure. Read a peer's token back from the message's parameters.

from aiomoqt.types import SetupParamType, ParamType

# session auth — on the SETUP handshake
await session.client_session_init(parameters={SetupParamType.AUTH_TOKEN: b"session-tok"})

# namespace auth — on PUBLISH_NAMESPACE (or SUBSCRIBE_NAMESPACE)
await session.publish_namespace('ns', parameters={ParamType.AUTH_TOKEN: b"ns-tok"}, wait_response=True)

# track auth — on SUBSCRIBE (also FETCH / PUBLISH)
ok = await session.subscribe('ns', 'track', parameters={ParamType.AUTH_TOKEN: b"track-tok"}, wait_response=True)
print(ok.parameters.get(ParamType.AUTH_TOKEN))   # token echoed by the peer, if any

Examples

Publisher / Subscriber

# Publish (SubgroupHeader streams)
python -m aiomoqt.examples.pub_example moqt://relay.ex.com

# Subscribe
python -m aiomoqt.examples.sub_example moqt://relay.ex.com

# Subscribe + FETCH (join mid-stream)
python -m aiomoqt.examples.join_example moqt://relay.ex.com

Common options: --namespace, --trackname, --path, --debug, --keylogfile. Every tool prints its full option set with -? / --help (note: -h is --host, not help).

Benchmarks

Bench tools take a positional relay URL — moqt://host[:port] for raw QUIC, https://host[:port]/[endpoint] for H3/WebTransport — except loopback_bench, which needs no relay at all:

# Local loopback (canonical stack benchmark, no relay)
python -m aiomoqt.tools.loopback_bench -s 4096 -P 4 -t 20

# Publisher / subscriber through a relay
python -m aiomoqt.tools.pub_bench moqt://relay.ex.com -s 4096 -P 4 -r 120 -t 60
python -m aiomoqt.tools.sub_bench moqt://relay.ex.com

The full tool matrix (two-process, fanout, adaptive ramp), all options, latency methodology (paced vs unpaced, TX budgets), and observed numbers live in PERFORMANCE.md.

Interop Testing

# All tests (draft-14, auto-detected)
python -m aiomoqt.tools.moq_interop_client -r "moqt://relay.ex.com:4433"

# All tests (draft-16)
python -m aiomoqt.tools.moq_interop_client -r "moqt://relay.ex.com:4433" --draft 16

# Single test case
python -m aiomoqt.tools.moq_interop_client -r "moqt://relay.ex.com:4433" -t subscribe-error

# List test cases
python -m aiomoqt.tools.moq_interop_client -l

Relay Probe

Batch liveness + draft-version check: reads a relay list, does a real CLIENT_SETUP / SERVER_SETUP handshake per (endpoint × draft), and writes a JSON status report. Accepts CLI flags, environment variables, or both (CLI overrides env). A single endpoint can also be probed inline with --url (see Quick Start above).

# Probe a relay list once and write a status report (CLI form)
python -m aiomoqt.tools.relay_probe -f relays.json -o status.json

# Same, env form (typical container/daemon deployment)
RELAYS_FILE=relays.json OUTPUT_FILE=status.json python -m aiomoqt.tools.relay_probe

# Long-running monitor: re-probe every 300s
python -m aiomoqt.tools.relay_probe -f relays.json -o status.json --interval 300
CLI flag Env var Default Meaning
-f / --relays-file RELAYS_FILE /app/relays.json input relay list
-o / --output-file OUTPUT_FILE /output/relay-status.json status report destination
--timeout PROBE_TIMEOUT 8 per-probe handshake timeout (s)
--interval PROBE_INTERVAL 0 re-probe cadence (s); 0 probes once and exits
--draft — all draft(s) to probe: --draft 18 or --draft 18,16 (add --offer to offer the list in one session)

WebTransport Server

python -m aiomoqt.examples.server_example --cert cert.pem --key key.pem -p 4433

Example Reference

Example Description
pub_example.py Publisher — SubgroupHeader streams
sub_example.py Subscriber — receives data from a relay
join_example.py SUBSCRIBE + FETCH (join mid-stream)
pub_bench.py Publisher benchmark, configurable parameters
sub_bench.py Subscriber with latency/jitter/loss stats
loopback_bench.py Local loopback (no relay)
adaptive_bench.py Ramps rate or subscriber count; loopback (--mp for proc-isolated pub/sub) or relay
server_example.py WebTransport server (origin)
relay_probe.py Relay version probe (draft-14/16/18)
moq_interop_client.py Interop test client (TAP v14 out; 6 standard + fetch/join)
moq_interop_relay.py Minimal interop relay (control plane; experimental)

Interop

Validated against live public relays — OpenMoQ moqx, Meta moxygen, Cloudflare moq-rs, Quicr libquicr, Meetecho imquic, OzU moqtail, Nokia — across draft-14/draft-16/draft-18 and both transports, using the moq-interop-runner cases plus a multi-subscriber pub-sub bench. The full point-in-time matrix lives in PERFORMANCE.md; the relay catalog with per-endpoint notes is tests/relays.json. Red5 Pro was interop-tested in earlier cycles and is currently untested/unverified.

Performance

aiomoqt sits on aiopquic, which sits on picoquic + the kernel UDP path; throughput at this layer is bounded by the layers below. Observed on commodity hardware over loopback: multi-Gbps sustained throughput at ~4 KiB objects with bounded memory under stream churn, and sub-millisecond to low-millisecond latency when paced below saturation. Numbers vary substantially by platform — methodology, observed figures, the paced-vs-unpaced distinction, and TX budget tuning are in PERFORMANCE.md.

Development

git clone https://github.com/gmarzot/aiomoqt.git
cd aiomoqt
python3 -m venv .venv && source .venv/bin/activate
uv pip install -e ".[test]"    # or: pip install -e ".[test]"

# One-time self-signed cert for the loopback server + test_loopback_* suites (skipped if certs/ is absent)
mkdir -p certs && openssl req -x509 -newkey rsa:2048 -nodes -days 3650 -keyout certs/key.pem -out certs/cert.pem -subj "/CN=localhost" -addext "subjectAltName=DNS:localhost,IP:127.0.0.1"

pytest aiomoqt/tests/

Install editable (-e). The standalone bench scripts and the cross-importing loopback tests resolve certs/ and sibling modules relative to the working tree. A non-editable copy in site-packages silently skips the loopback suites with "TLS certs not found in certs/" even when the certs exist. pytest auto-generates certs/ on first run; the openssl line above is only needed for the standalone bench scripts.

Developing against a locally built aiopquic

The PyPI aiopquic wheel is portable (any CPU of the architecture). A locally compiled aiopquic is host-tuned (-O3 -march=native -flto, plus picotls Fusion AES-GCM on x86_64) and is measurably faster — build from source when benchmarking, optimizing, or targeting bare metal.

# Build aiopquic from source (host-tuned by default; AIOPQUIC_WHEEL_BUILD=1 for a portable build)
git clone https://github.com/gmarzot/aiopquic.git
cd aiopquic && git submodule update --init --recursive && ./build_picoquic.sh

With a separate venv per repo, install the local aiopquic into the aiomoqt venv first, so the dependency is already satisfied and no wheel is fetched (re-run it any time the wheel sneaks back in — it replaces the wheel install):

# in the aiomoqt venv:
uv pip install -e ~/aiopquic    # local source, editable — BEFORE aiomoqt
uv pip install -e '.[test]'     # aiopquic already satisfied → no wheel

Confirm the local build is actually in use:

python -c "import aiopquic; print(aiopquic.__file__)"   # must be <repo>/src/aiopquic/…, not …/site-packages/
python -m aiomoqt.versions                              # paths + picoquic/picotls SHAs match your build

Both venvs must use the same Python version. C/Cython changes need a rebuild (./build_picoquic.sh then uv pip install -e ~/aiopquic); pure-Python edits are live.

Known Limitations

  • draft-18 is beta. Negotiated by default ((18, 16, 14), newest-first) over raw QUIC and WebTransport, with control, FETCH, and SUBGROUP object delivery validated end to end (including subscribers that join a track which already has objects). Not yet complete: the SUBSCRIBE / PUBLISH subscription-filter still uses the d16 nested form; Track-Properties extensions encode as RFC9000 varints (correct for the small values in use). draft-14 / draft-16 are unaffected and remain the stable path.
  • WebTransport fetch / join routing -- four [wt] test variants of FETCH and JOINING_SUBSCRIBE return empty results when the underlying transport is WebTransport. Raw-QUIC variants of the same tests pass and cover the MoQT-level invariant. Tracked separately; affects WT-only consumers of fetch/join.

TODO

  • Fix WebTransport fetch / join routing (the 4 [wt] tests currently skipped)
  • Track data modules:
    • File transfer (or MOQT File Format?)
    • Interactive chat
    • MSF/LOC media packaging
    • CMSF media packaging
  • Simple relay implementation

Contributing

Contributions are welcome! Please fork the repository, create a branch, and submit a pull request. For major changes, open an issue first.

Resources


Author

Giovanni Marzot — gmarzot@marzresearch.net

A Marz Research project.

Acknowledgements

This project takes inspiration from, and has benefited from the great work done by the OpenMOQ/moqx team, and the continued efforts of the MOQ IETF WG.

Metadata

Release files for aiomoqt 0.11.0rc1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for aiomoqt 0.11.0rc1
File Size Uploaded
aiomoqt-0.11.0rc1.tar.gz 414.4 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for aiomoqt 0.11.0rc1
File Interpreter ABI Platform
aiomoqt-0.11.0rc1-py3-none-any.whl Python 3 none any Details

Total release size: 794.9 kB

Release files / aiomoqt-0.11.0rc1.tar.gz

Download URL aiomoqt-0.11.0rc1.tar.gz
Size 414.4 kB
Tags Source
SHA-256 checksum
How to use checksums
3bc522ceadb97abf12e3bfbb7fd325f194853c9c63aec1a6939cf9c5020e264c
BLAKE2b-256 checksum
How to use checksums
034ca804a335be89f4271043dbdf6a05432505ef04f3cc428a8fd24c5e7a3c21
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.14

Release files / aiomoqt-0.11.0rc1-py3-none-any.whl

Download URL aiomoqt-0.11.0rc1-py3-none-any.whl
Size 380.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
f39d4b029c8dc66e39d72f4a8a49ce9829fdd7fcbe6dbf3bb45ee2b27f4911f6
BLAKE2b-256 checksum
How to use checksums
43647f4d53f5ecb77e31dbc1b48ee21ece32c98bf6bda932690c9cd27016ed2a
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.14

Release history Release notifications | RSS feed

0.11.1

2 release files

0.11.0

2 release files

This release

0.11.0rc1 This release

2 release files

0.10.5

2 release files

0.10.4

2 release files

0.10.3

2 release files

0.10.2

2 release files

0.10.1

2 release files

0.10.0

2 release files

0.9.12

2 release files

0.9.11

2 release files

0.9.10

2 release files

0.9.9

2 release files

0.9.8

2 release files

0.9.7

2 release files

0.9.6

2 release files

0.9.5

2 release files

0.9.4

2 release files

0.9.3

2 release files

0.9.2

2 release files

0.9.1

2 release files

0.9.0

2 release files

0.8.2

2 release files

0.8.1

2 release files

0.8.0

2 release files

0.7.0

2 release files

0.6.2

2 release files

0.6.1

2 release files

0.6.0

2 release files

0.5.12

2 release files

0.5.11

2 release files

0.5.10

2 release files

0.5.9

2 release files

0.5.8

2 release files

0.5.7

2 release files

0.5.6

2 release files

0.5.5

2 release files

0.5.3

2 release files

0.5.2

2 release files

0.5.1

2 release files

0.5.0

2 release files

0.3.9

2 release files

0.3.7

2 release files

0.3.2

2 release files

0.3.1

2 release files

0.2.3

2 release files

0.2.2

2 release files

0.2.1

2 release files

0.1.7

2 release files

0.1.6

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page