moq
Python bindings for Media over QUIC: real-time pub/sub with built-in caching, fan-out, and prioritization, on top of QUIC.
Installed as moq-rs (the moq name is taken on PyPI), imported as moq.
It wraps the auto-generated moq-ffi UniFFI bindings with a Pythonic API: no Moq prefixes, async iterators, context managers, and simplified connection setup. At session setup it negotiates either the moq-lite or moq-transport wire protocol.
Installation
pip install moq-rs
# or with uv
uv add moq-rs
This pulls in the moq-ffi native bindings automatically. moq-rs is pure Python and is versioned independently of moq-ffi; it floats to the latest compatible moq-ffi patch.
Quick Start
Subscribe to a stream
import asyncio
import moq
async def main():
async with moq.connect("https://cdn.moq.dev/anon") as client:
async for announcement in client.announced():
catalog = await announcement.broadcast.catalog()
for name, track in catalog.audio.items():
frames = await announcement.broadcast.subscribe_media(name, track)
async with frames:
async for frame in frames:
print(f"Got frame: {len(frame.payload)} bytes, ts={frame.timestamp_us}")
asyncio.run(main())
Publish a stream
import asyncio
import moq
async def main():
async with moq.Client("https://cdn.moq.dev/anon") as client:
broadcast = client.create_broadcast("my-stream")
# Publish an Opus audio track (init bytes from your encoder)
audio = broadcast.publish_media("opus", opus_init_bytes)
# Write frames
audio.write_frame(payload, timestamp_us=0)
audio.write_frame(payload, timestamp_us=20000)
# Clean up
audio.finish()
broadcast.finish()
asyncio.run(main())
Host a server
import asyncio
import moq
async def main():
async with moq.Server("127.0.0.1:4443", tls_generate=["localhost"]) as server:
broadcast = server.create_broadcast("hello")
track = broadcast.publish_track("events")
print(f"listening on https://{server.local_addr}")
sessions = []
async for request in server:
safe_path = request.path.split("?", 1)[0]
safe_url = request.url.split("?", 1)[0] if request.url else None
print(f" + {request.transport} {safe_path} from {safe_url}")
sessions.append(await request.accept())
asyncio.run(main())
Reject a request instead of accepting it with await request.reject(403).
Advanced: Manual origin wiring
For full control over the origin topology:
import moq
origin = moq.OriginProducer()
client = moq.Client(
"https://cdn.moq.dev/anon",
publish=origin,
subscribe=origin,
)
API
Connection
connect(url, *, tls_verify=True, tls_roots=None, tls_system_roots=None, tls_fingerprints=None, tls_cert=None, tls_key=None, bind=None, publish=None, subscribe=None). Shorthand forClient(...); use asasync with moq.connect(url) as client:.Client(url, *, tls_verify=True, tls_roots=None, tls_system_roots=None, tls_fingerprints=None, tls_cert=None, tls_key=None, bind=None, publish=None, subscribe=None). Async context manager for connecting to a relay.tls_roots. PEM root certificate file path(s) to trust instead of the system roots.tls_system_roots. Whether to trust platform roots in addition to custom roots.tls_fingerprints. Hex SHA-256 fingerprint(s) to pin the peer's certificate to, the native equivalent ofserverCertificateHashes. Accepts the values a server reports viacert_fingerprints(), so you can trust a self-signed certificate withouttls_verify=False.tls_cert,tls_key. Paired PEM certificate chain and private key paths for mTLS..session. The establishedSession(orNonebefore connecting / after exit).
Server(bind="[::]:443", *, tls_cert=(), tls_key=(), tls_generate=(), publish=None, subscribe=None). Async context manager + async iterator of incomingRequests..local_addr. The bound address (useful when binding to port0)..cert_fingerprints(). SHA-256 fingerprints of the configured TLS certificates, forserverCertificateHashesbrowser cert pinning..create_broadcast(path) → BroadcastProducer. Create a live broadcast served to incoming sessions;finish()unpublishes it.
Request. An incoming session, yielded byasync for request in server..url,.path,.query,.transport. The query-free path is uniform across transports; the root or missing path is"". The encoded query may contain credentials..set_publish(origin),.set_consume(origin). Per-request overrides.await .accept() → Session. Complete the handshake (hold the result to keep the connection alive).await .reject(code). Reject with an HTTP status code..cancel(). Cancel an in-flightaccept()/reject()call.
Session. An established connection. Holding it keeps the connection alive; it is also anasync withcontext manager that shuts down on exit.await .closed(). Wait until the session closes..cancel(code),.shutdown(). Close with an error code, or gracefully (code 0)..publisher() → OriginProducer,.consumer() → OriginConsumer. The wired origin sides..stats() → ConnectionStats. Snapshot RTT, bandwidth estimates, and byte/packet counters.
Publishing
BroadcastProducer(). Create a broadcast to publish tracks into..dynamic() → BroadcastDynamic.publish_media(format, init=b"", video=None) → MediaProducer. Pass aVideoHintto pin catalog fields the stream can't reveal (bitrate) or publish the catalog before the first keyframe; audio formats resolve from their init bytes..finish()
BroadcastDynamic. Async source of tracks requested by subscribers.await .requested_track() → TrackRequest. Call.accept()on it for aTrackProducer, or.abort(code)to reject.- Async iterator yielding
TrackRequest
MediaProducer. Write frames to a track..write_frame(payload, timestamp_us=0).finish()
TrackProducer/GroupProducer. Write raw payloads with no codec parsing..write_frame(payload, timestamp_us=0)writes a payload with a presentation timestamp in microseconds..create_group(sequence)creates a sparse or replayed group at an explicit sequence..finish_at(final_sequence)declares the first group that will never be produced while leaving lower groups writable..abort(error_code)terminates the track or group with an application error..append_datagram(payload, timestamp_us=0) -> sequence(TrackProducer) sends a best-effort datagram. Payloads are capped at 1200 bytes and there is no stream fallback.
Subscribing
BroadcastConsumer. Subscribe to tracks within a broadcast..route_updates() → RouteWatch(async iterator; current route, then changes)await .subscribe_catalog() → CatalogConsumerawait .subscribe_track(name, subscription=None) → TrackConsumerawait .subscribe_media(name, track, subscription=None) → MediaConsumer.trackis the catalog record (e.g.catalog.video[name]); its container tells the decoder how to parse the bitstream.await .catalog() → Catalog(convenience)
RouteWatch. Async iterator ofRoutethat ends with the broadcast.CatalogConsumer. Async iterator ofCatalog.MediaConsumer. Async iterator ofMediaFrame.TrackConsumer. Async iterator of raw groups, in sequence order.await .next_group() → GroupConsumer | None. Sequence order; what the default iteration yields.await .recv_group() → GroupConsumer | None. Arrival order, which may be out of sequence. Prefer it when latency matters more than order..groups_as_arrived(). Async iterator overrecv_group()..read_frame() -> Frame | Nonereturns a timestamped raw frame.await .recv_datagram() -> Datagram | Nonefor best-effort raw track datagrams..info() → TrackInfo.update(subscription). Change delivery priority, group ordering priority, staleness, or group range after subscribing.
GroupConsumer. Async iterator of timestampedFrames..read_frame() -> Frame | Nonereturns a timestamped raw frame.
All consumers and route watches (CatalogConsumer, MediaConsumer, TrackConsumer, AudioConsumer, GroupConsumer, RouteWatch) are async context managers; exiting async with cancels the subscription or watch.
Origin (advanced)
OriginProducer(cache_capacity_bytes=None). Manage broadcast announcements. Setcache_capacity_bytesto bound cached groups under this origin..consume() → OriginConsumer.dynamic() → OriginDynamic.create_broadcast(path) → BroadcastProducer
OriginDynamic. Async source of broadcasts requested by consumers.await .requested_broadcast() → BroadcastRequest. Call.accept(broadcast)to serve it, or.abort(code)to fail the requester.- Async iterator yielding
BroadcastRequest
OriginConsumer. Discover broadcasts..announced(prefix) → Announced(async iterator).announced_broadcast(path) → AnnouncedBroadcast(awaitable, waits for a future announcement).request_broadcast(path) → BroadcastConsumer(awaitable; announced now or a dynamic fallback, else raises)
Types
Catalog..audio: dict[str, Audio],.video: dict[str, Video],.display,.rotation,.flip.Frame..payload: bytes,.timestamp_us: int. The unit of every write and every raw read.MediaFrame..payload: bytes,.timestamp_us: int,.keyframe: bool. Returned by media subscriptions.Datagram..sequence: int,.timestamp_us: int,.payload: bytes. Delivered only on datagram-capable transports and lite-05 or newer moq-lite.Audio..codec,.sample_rate,.channel_count,.bitrate,.description.Video..codec,.coded: Dimensions,.display_aspect,.bitrate,.stalled,.framerate,.description. A true.stalledrecommends temporarily avoiding the rendition without making it unavailable.Subscription. Subscriber delivery preferences: priority, ordering priority, staleness, and optional group range.TrackInfo. Publisher track properties: priority, ordering priority, cache window, and timescale.Dimensions..width: int,.height: int.Container. The catalog container enum, carried on eachVideo/Audiorecord.
For both Subscription and TrackInfo, ordered controls prioritization only. When true, groups are prioritized in sequence order. Groups may always arrive out-of-order (or not at all) over the network.
Logging and errors
log_level(level="info"). Initialize logging for the underlying Rust layer ("error","warn","info","debug","trace"). Call once per process.Error. The exception raised by all operations. Catch a specific case via its variants, e.g.except moq.Error.AlreadyResponded:orexcept moq.Error.Cancelled:.is_shutdown(err). True forCancelledandClosed, which arise from graceful shutdown rather than an actual failure. Use it to break out of anasync forwithout treating the expected end-of-stream error as a problem.is_auth(err). True forUnauthorized(HTTP 401) andForbidden(HTTP 403). Retrying without new credentials won't help, so surface these rather than reconnect.
See Also
moq-ffi. The raw UniFFI bindings this package wraps. Use it directly only if you need the unwrappedMoq-prefixed API.- MoQ project. Full monorepo with Rust server, TypeScript browser lib, and more.
Release files for moq-rs 0.4.7
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| moq_rs-0.4.7.tar.gz | 32.1 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| moq_rs-0.4.7-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 57.3 kB
Release files / moq_rs-0.4.7.tar.gz
| Download URL | moq_rs-0.4.7.tar.gz |
|---|---|
| Size | 32.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
d400b3b62ebc625d92fb01b7105eae1d24501b030bf887c9f3272f77ebfc603c
|
|
BLAKE2b-256 checksum How to use checksums |
629d2aa6b1c075f9c4527ae0c279c2ae472882ede68768bf1f16ba2419301a1c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 17, 2026.
Transparency logRelease files / moq_rs-0.4.7-py3-none-any.whl
| Download URL | moq_rs-0.4.7-py3-none-any.whl |
|---|---|
| Size | 25.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
65a18026ddb9cff63a7ece1d0e79f085a66418158cbc7bf5606e545f9db9f64a
|
|
BLAKE2b-256 checksum How to use checksums |
16732618cf508fd266ea3373303fa522c59a094e74b51237f1a82799a4af8e16
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 17, 2026.
Transparency log