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:
MOQTRequestErrorexception 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_receivedcallback - 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 resolvecerts/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.pytestauto-generatescerts/on first run; theopensslline 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)
| File | Size | Uploaded | |
|---|---|---|---|
| aiomoqt-0.11.0rc1.tar.gz | 414.4 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| 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
|