Skip to content

caprid_relay

From the caprid-relay package: import caprid_relay

Relay Python SDK.

Owns the Relay product API and authentication convention, built on moq_core. The default transport is native QUIC through aioquic (caprid_relay.quic), which reaches a relay natively with no browser; create_transport swaps in another implementation of the same seam.

The public names are the rows of spec/vocabulary.md, pinned by tests/test_exports.py. From the core this package re-exports exactly what @caprid/relay re-exports, plus Closed; every other protocol type (location filters, fetch, track status, the constants) is imported from moq_core, a declared dependency.

async def connect_relay_publisher(
url: str | None = None,
*,
namespace: Sequence[str],
get_token: TokenProvider | None = None,
cancellation: CancellationToken | None = None,
connect_timeout: float = DEFAULT_CONNECT_TIMEOUT,
create_transport: RelayTransportFactory | None = None,
) -> RelayPublisher

Open a dedicated Relay publisher connection.

Subscribers use connect_relay_subscriber; publishing does not share their transport. One publisher connection serves one namespace and any number of tracks. A relay must solicit the namespace and subscribe upstream before data can flow. With no url, the connection dials DEFAULT_ENDPOINT rooted at the token’s root claim, which get_token must then supply.

Source

async def connect_relay_subscriber(
url: str | None = None,
*,
get_token: TokenProvider | None = None,
cancellation: CancellationToken | None = None,
connect_timeout: float = DEFAULT_CONNECT_TIMEOUT,
request_timeout: float = DEFAULT_REQUEST_TIMEOUT,
create_transport: RelayTransportFactory | None = None,
) -> RelaySubscriber

Open a Relay connection for subscribing to tracks.

One deadline (connect_timeout) covers token resolution and both handshakes. cancellation controls the connection’s entire lifetime. An explicit jwt query parameter on url takes precedence over get_token, matching the Relay authentication convention; an empty one is a ValueError. With no url, the connection dials DEFAULT_ENDPOINT rooted at the token’s root claim, which get_token must then supply. Publishing uses a separate connection; see connect_relay_publisher.

Source

async def connect_supervised_publisher(
url: str | None = None,
*,
namespace: Sequence[str],
get_token: TokenProvider | None = None,
cancellation: CancellationToken | None = None,
connect_timeout: float = DEFAULT_CONNECT_TIMEOUT,
create_transport: RelayTransportFactory | None = None,
reconnect: SupervisionBackoff | None = None,
rotate_before: float | None = None,
on_generation: Callable[[PublisherGeneration], None] | None = None,
on_status: Callable[[SupervisedStatus, BaseException | None], None] | None = None,
) -> SupervisedPublisher

Open a publisher that reconnects: each connection serves namespace plus a fresh generation field, and the writers SupervisedPublisher.register_track returns outlive the connections.

Returns once the first generation is up. A first attempt that fails on its arguments, or because the relay refused the credentials (UNAUTHORIZED), raises that failure; any other failure is retried with backoff (reconnect, from 0.5 s doubling up to 10 s), bounded only by cancellation, whose reason is raised. The token must grant namespace, the root, since every generation publishes one field below it. on_generation runs once each generation is up and every track is bound to it; on_status on every status change, with what ended the previous connection or failed the last attempt. rotate_before (seconds, default 60) is how long before the connection’s credential expires (RelayPublisher.expires_at) the next generation is brought up to replace it, make-before-break; never earlier than halfway through the time left. With no url, every generation dials the endpoint the first one rooted at its token’s root. See spec/behavior.md, “Supervised connections”; connect_relay_publisher never reconnects.

Source

async def connect_supervised_subscriber(
url: str | None = None,
*,
get_token: TokenProvider | None = None,
cancellation: CancellationToken | None = None,
connect_timeout: float = DEFAULT_CONNECT_TIMEOUT,
request_timeout: float = DEFAULT_REQUEST_TIMEOUT,
create_transport: RelayTransportFactory | None = None,
reconnect: SupervisionBackoff | None = None,
rotate_before: float | None = None,
on_status: Callable[[SupervisedStatus, BaseException | None], None] | None = None,
) -> SupervisedSubscriber

Open a subscriber that reconnects and keeps every wanted track subscribed across connections, publisher restarts and generations.

Returns once the first connection is up. A first attempt that fails on its arguments, or because the relay refused the credentials (UNAUTHORIZED), raises that failure; any other failure is retried with backoff (reconnect, from 0.5 s doubling up to 10 s), bounded only by cancellation, whose reason is raised. on_status runs on every status change, with what ended the previous connection or failed the last attempt. rotate_before (seconds, default 60) is how long before the connection’s credential expires (RelaySubscriber.expires_at) a replacement connection is opened, make-before-break; never earlier than halfway through the time left. With no url, every connection dials the endpoint the first one rooted at its token’s root. See spec/behavior.md, “Supervised connections”; connect_relay_subscriber never reconnects or resubscribes.

Source

def read_json(obj: MoqtObject) -> Any

Parse one UTF-8 JSON payload without schema validation; invalid input raises ValueError.

Source

def read_text(obj: MoqtObject) -> str

Decode one complete object as UTF-8; malformed bytes raise UnicodeDecodeError.

Source

class CancellationToken

Cancel every connection that was handed this token, whether still connecting or established.

Source

Attributes

  • cancelled: bool (property) — Whether cancel has been called.
  • reason: BaseException | None (property) — The exception recorded by cancel; None until then.
def cancel(self, reason: BaseException | None = None) -> None

Cancel once. Later calls are ignored. Registered callbacks run synchronously, in order.

def throw_if_cancelled(self) -> None

Raise the recorded reason if the token has been cancelled; return otherwise.

def add_callback(self, callback: Callable[[BaseException], None]) -> None

Run callback with the reason when the token is cancelled. No-op once already cancelled.

def remove_callback(self, callback: Callable[[BaseException], None]) -> None

Unregister a callback added with add_callback. No-op if it is not registered.

class Closed

How a session or connection ended. error is None for a clean close.

Source

Attributes

  • error: BaseException | None
class ConnectingTransport

What a transport factory hands back: the transport, an awaitable for its handshake, and its framing.

Source

Attributes

  • transport: MoqtTransport
  • ready: Awaitable[None]
  • framing: Framing — "native-quic" makes the connection send the endpoint’s PATH and AUTHORITY in SETUP (TA-1, TA-6).
class Goaway

GOAWAY (§9.2): the peer intends to close the session soon and asks for no new requests.

Source

Attributes

  • kind: Literal['goaway']
  • new_session_uri: str — Where to reconnect; empty means the current URI. A client always sends it empty.
  • timeout: int — Milliseconds until the peer closes the session; 0 when it gave none.
class GoawayEvent

Request relocation details; no automatic retry.

Source

Attributes

  • value: Goaway
  • type: Literal['goaway']
class KeyValuePair

A generic MOQT Key-Value-Pair.

Even types carry a varint value, odd types a byte field. Used for Setup Options and Track/Object Properties (message parameters use per-type encodings instead; see MessageParameters).

Source

Attributes

  • type: int
  • value: int | bytes
class MessageParameters

Decoded message parameters.

Unlike Setup Options, message parameters use per-type encodings (uint8 / varint / length-prefixed), so the codec is registry-driven. Per §9.20 an unknown parameter type is a PROTOCOL_VIOLATION (parameters cannot be skipped; the block is bounded by count, not length).

fill_parameters (§9.20.15) asks the publisher to deliver a fill range on a fetch stream alongside the subscription; its presence alone requests the fill. It is a nested parameter scope: only the fill-scoped subset (Table 7) is legal inside, and omitted entries inherit the subscription’s own values.

Source

Attributes

  • object_delivery_timeout: int | None

  • subgroup_delivery_timeout: int | None

  • rendezvous_timeout: int | None

  • fill_timeout: int | None

  • authorization_tokens: list[bytes] | None — Serialized Token structures (§9.20.2). May repeat.

  • expires: int | None

  • largest_object: Location | None

  • forward: int | None — 0 or 1.

  • subscriber_priority: int | None

  • location_filter: LocationFilter | None

  • group_order: GroupOrder | None

  • fill_parameters: MessageParameters | None

  • new_group_request: int | None

  • range_filters: list[RangeFilter] | None

  • track_namespace_prefix: TrackNamespace | None

  • include_properties: int | None — INCLUDE_PROPERTIES (§9.20.21): 0 asks the responder to send empty Track Properties. Default 1.

  • max_age: int | None — MAX_AGE (0x915C6, the MoQ Timestamp extension; not a draft-22 parameter).

    The subscriber’s max age in milliseconds, 0 to 2^53-1. A publisher that honors it skips, for this subscription only, a group once a newer group supersedes it and either that successor arrived at the publisher’s hop at least this long ago or the group’s media time trails the live edge by at least this much; the newest group never expires. moq-dev main turns it into the subscription’s max_delay. A session sends it on SUBSCRIBE and REQUEST_UPDATE only when the peer’s SETUP declared SETUP_OPTION.MAX_AGE, and never on FETCH, TRACK_STATUS or SUBSCRIBE_NAMESPACE; see MoqtSession.max_age_supported.

class MoqtObject

An object delivered on a subscription’s subgroup stream.

Source

Attributes

  • group_id: int
  • object_id: int
  • subgroup_id: int
  • priority: int | None — Publisher priority; None inherits the subscription’s priority.
  • status: ObjectStatus
  • properties: list[KeyValuePair]
  • payload: bytes
class NamespaceEvent

One change in which namespaces under the prefix are announced.

Source

Attributes

  • type: Literal['announced', 'unannounced'] — announced: a publisher announced the namespace, or it was already announced when the request was accepted. unannounced: its announce was withdrawn. Each namespace alternates between the two; the relay’s repeats are not passed on.
  • namespace: tuple[str, ...] — The full namespace: the prefix’s fields followed by the rest, without splitting or joining.
class NamespaceEvents(BoundedEvents[NamespaceEvent])

Single-consumer iterator owning one namespace request, never the shared connection.

Create one with caprid_relay.RelaySubscriber.namespace_events and use async with around async for. At most capacity events are retained; overflow discards the queue, cancels the request and raises SubscriptionOverflowError on reads. A relay rejection raises SubscriptionRequestError, the connection ending SubscriptionConnectionClosedError; a relay that finishes the request, close(), or break ends iteration cleanly.

Source

class ObjectEvent

An object, with its payload and protocol metadata.

Source

Attributes

  • value: MoqtObject
  • type: Literal['object']
class PublishDone

PUBLISH_DONE (§9.9): the publisher finished a subscription.

status_code is one of the PUBLISH_DONE status codes; stream_count is how many data streams it opened for the subscription, so a subscriber can tell when all of them have arrived.

Source

Attributes

  • kind: Literal['publish-done']
  • status_code: int
  • stream_count: int | None — None when the publisher could not count its streams (the 2^64-1 sentinel, §9.9).
  • reason: str
class PublisherDoneEvent

Publisher completion; trailing objects may still arrive.

Source

Attributes

  • value: PublishDone
  • type: Literal['publisher_done']
class PublisherGeneration

One publisher generation: a connection and the namespace it serves.

Source

Attributes

  • generation: str — The generation field: start time in fixed-width base 36, then four random characters, so it sorts by age.
  • namespace: tuple[str, ...] — The namespace this generation serves: the root followed by generation.
class RelayAwaitable(Generic[_T])

A fact the SDK settles once: a subscription’s subscribed or ended, a publisher’s announced, or a connection’s closed.

Await it as often as needed. Each waiter is isolated: a task cancelled while awaiting, or an asyncio.wait_for that times out, never touches the underlying result, where handing out the future itself would let one impatient waiter destroy the signal for every later one. done(), result(), exception() and add_done_callback() read the result directly and allocate nothing, so polling is free. It is not an asyncio.Future: it cannot be cancelled or set. Wrap it with asyncio.ensure_future for an API that needs a future.

Source

def done(self) -> bool

Whether the result is in.

def result(self) -> _T

The result; raises what awaiting would, or asyncio.InvalidStateError while pending.

def exception(self) -> BaseException | None

The exception awaiting would raise, None for a plain result.

def add_done_callback(
self,
callback: Callable[[asyncio.Future[_T]], object],
*,
context: contextvars.Context | None = None,
) -> None

Call callback with the settled future once the result is in.

def remove_done_callback(self, callback: Callable[[asyncio.Future[_T]], object]) -> int

Forget a callback; returns how many registrations were removed.

class RelayCredential

A token together with when the relay stops honoring it.

moq-relay ends a session at its grant’s deadline, which is the token’s expiry, and a live session cannot take a new token; supervised connections replace theirs before then.

Source

Attributes

  • token: str — The token, carried in the endpoint’s jwt query parameter. An empty one sends no token.

  • expires_at: datetime — When the relay ends a session the token admitted: the JWT’s exp, or the grant’s deadline if earlier.

    Must be timezone-aware.

class RelayGroupWriter

One open multi-object group on a track. Create one with RelayTrackWriter.open_group.

Objects are appended in order with increasing object IDs starting at 0, so the first object is the group’s random-access point in LOC/MSF terms. close() FINs the stream, which is how a complete group is announced; abort() resets it unless it already closed cleanly. A group that fails part-way is reset, and the track stays open for the next group.

Source

Attributes

  • group_id
  • closed: bool (property) — True once the group was FINed or reset; no further objects can be written.
  • object_count: int (property) — Objects accepted by the transport so far.
async def write_text(
self,
text: str,
*,
properties: Sequence[KeyValuePair] | None = None,
status: ObjectStatus = 'normal',
) -> None

Encode UTF-8 text and write one object with the same semantics as write.

async def write_json(
self,
value: object,
*,
properties: Sequence[KeyValuePair] | None = None,
status: ObjectStatus = 'normal',
) -> None

Serialize JSON and write one object; serialization errors leave the writer usable.

async def write(
self,
payload: bytes,
*,
properties: Sequence[KeyValuePair] | None = None,
status: ObjectStatus = 'normal',
) -> None

Append one object. Returns when the transport accepts it. Bytes are copied.

Properties are encoded as supplied; the relay may transform them before delivery. Each call has its own write_timeout deadline; expiry resets the group and leaves the track open. status is only meaningful for an empty payload (an end-of-group or end-of-track marker). On the group’s first object, a 0x02 or 0x06 property sets the group’s delivery timeout instead of the track’s; on a later object it is only sent. An object older than the group’s OBJECT_DELIVERY_TIMEOUT when it reaches the transport raises DELIVERY_TIMEOUT and resets the group.

async def close(self) -> None

FIN within write_timeout; expiry resets the group. Idempotent once closed cleanly.

def abort(self, reason: BaseException | None = None) -> None

Reset the group’s stream; objects already sent may be incomplete at the peer.

After a clean close() this has no effect: the FIN already ended the stream, and a reset after complete delivery is ignorable (draft-ietf-moq-transport-22 section 11.3.2).

class RelayPublisher

A connection that can only publish, serving one namespace. Create one with connect_relay_publisher.

Subscribing uses a separate connection; see connect_relay_subscriber.

Source

Attributes

  • expires_at: datetime | None (property) — When the relay will end this connection for its credential’s expiry, as an aware UTC datetime.

    The RelayCredential’s expires_at, or the JWT’s exp claim, read without verifying it; None when neither is known. The relay closes the session then with UNAUTHORIZED in phase connection; open a replacement before it. Supervised connections do.

  • namespace: list[str] (property) — The namespace every track on this connection is published under.

  • announced: RelayAwaitable[None] (property) — Settles once the namespace announce has reached the wire on a solicitation that covers it.

    That is no earlier than the first register_track, and it is the strongest fact the publisher has: the relay acknowledges an announce with nothing and routes subscribes for the namespace about a round trip later, so a subscribe that races it is rejected (request error 16 unroutable on moq-relay 0.15.8 and later; see spec/behavior.md). It stays pending while no solicitation covers the namespace, where the first write reports NAMESPACE_NOT_SOLICITED, and the connection’s closure terminates it if that comes first: it raises the closure’s error, or a CLOSED RelayError after a clean shutdown. Every waiter is isolated, as for closed.

  • closed: RelayAwaitable[Closed] (property) — Resolves when the connection shuts down. Errors are returned as data; this never raises.

    Every waiter is isolated: a timed-out asyncio.wait_for or a cancelled task leaves the result intact for every other waiter.

def register_track(
self,
track: str,
*,
track_properties: Sequence[KeyValuePair] | None = None,
write_timeout: float = DEFAULT_WRITE_TIMEOUT,
) -> RelayTrackWriter

Register a track this publisher will serve and return its writer.

Nothing is sent: the relay learns of the track when a subscriber asks for it, and bytes move when the writer’s write() or open_group() runs, after waiting for that relay demand. No capture starts and no media catalog is published. Track names are unique per publisher connection, including after their writer closes. track_properties is encoded as supplied; the SDK adds no default timescale. It rides in every SUBSCRIBE_OK (and TRACK_STATUS_OK), so it must encode to at most 65525 bytes, hold at most 64 pairs (the relay’s limit) and keep 0x0E at most 255 and 0x22 at 1 or 2 (draft-22); otherwise the relay would refuse every subscriber, so register_track raises ValueError instead. The relay may add, drop, or rewrite properties before delivery to subscribers; see README. Properties 0x02 (OBJECT_DELIVERY_TIMEOUT) and 0x06 (SUBGROUP_DELIVERY_TIMEOUT), in milliseconds, are also the publisher’s delivery timeouts (draft-ietf-moq-transport-22 section 5.2), which the writer enforces; 0 means none, and values above 2147483647 are clamped. write_timeout (seconds) bounds relay demand and the transport write together.

async def close(self) -> None

End all tracks, retract the namespace, and wait for transport shutdown. Idempotent.

Before the session ends it waits, for up to the largest write_timeout among the tracks, for the relay to acknowledge the groups that completed and each track’s end; any still unacknowledged after that may be lost, and the close still succeeds. Cancelling the caller does not stop the shutdown.

async def __aenter__(self) -> Self
class RelaySubscriber

A connection that can only subscribe. Create one with connect_relay_subscriber.

Publishing uses a separate connection and token; see connect_relay_publisher.

Source

Attributes

  • closed: RelayAwaitable[Closed] (property) — Resolves once for local shutdown, peer closure, or failure. It never raises.

    Every waiter is isolated: a timed-out asyncio.wait_for or a cancelled task leaves the result intact for every other waiter.

  • expires_at: datetime | None (property) — When the relay will end this connection for its credential’s expiry, as an aware UTC datetime.

    The RelayCredential’s expires_at, or the JWT’s exp claim, read without verifying it; None when neither is known. The relay closes the session then with UNAUTHORIZED in phase connection; open a replacement before it. Supervised connections do.

  • max_age_supported: bool (property) — Whether the relay’s SETUP declared subscriber max age (the MoQ Timestamp extension’s MAX_AGE, 0x915C4).

    When it did, a parameters.max_age on subscribe, subscribe_switchable, subscribe_events, update() or track_source.subscribe reaches it. While False the SDK leaves max_age off every request, as the extension requires. moq-dev main declares it; moq-relay 0.17.x does not. Fixed for the connection’s life.

  • track_source: MoqTrackSource (property) — This connection seen through the generic MoqTrackSource contract, for integrations such as players.

    subscribe, fetch, and track_status in protocol terms, plus closed. It lists no tracks and cannot close the connection; the application that opened the connection owns shutdown.

def subscribe(
self,
namespace: Sequence[str],
track: str,
handlers: RelaySubscriptionHandlers | None = None,
*,
parameters: MessageParameters | None = None,
) -> RelaySubscription

Ask the relay for a track’s objects, in any payload format, without discovering or loading a catalog.

Returns at once with a cancellable handle whose subscribed settles when the relay answers and whose ended settles once the subscription can no longer deliver; handlers.on_subscribed and handlers.on_error report the same answer as callbacks. namespace fields are sent verbatim, without splitting or joining on slashes. parameters carries protocol subscription controls such as the location filter and priority.

def subscribe_switchable(
self,
namespace: Sequence[str],
track: str,
handlers: SwitchableHandlers | None = None,
*,
parameters: MessageParameters | None = None,
) -> SwitchableSubscription

Subscribe to a track through a handle that can move to another track in the same namespace make-before-break.

SwitchableSubscription.switch_to opens the new subscription while this one keeps delivering, and cuts over once the new one’s object passes until. Use it to change layers (simulcast, ABR), or to change a subscription’s filter or priority, which update() cannot do against moq-relay. Handlers see only the active subscription, with its track name.

def subscribe_events(
self,
namespace: Sequence[str],
track: str,
*,
parameters: MessageParameters | None = None,
capacity: int = 64,
cancellation: CancellationToken | None = None,
) -> SubscriptionEvents

Create a bounded event iterator. Use async with around async for.

Owns only this request. publisher_done permits trailing objects; it is not EOF.

def namespace_events(
self,
prefix: Sequence[str],
*,
capacity: int = 64,
cancellation: CancellationToken | None = None,
) -> NamespaceEvents

Discover which namespaces under prefix are announced, as a bounded event iterator.

Same ownership rules as subscribe_events: the iterator owns one SUBSCRIBE_NAMESPACE request, and close(), leaving async with or cancellation cancels it. It first yields announced for each namespace already announced, then one event per announce and withdrawal; the relay sends no marker between the two. A relay rejection ends it with SubscriptionRequestError, the connection ending with SubscriptionConnectionClosedError, a full queue with SubscriptionOverflowError; a relay that finishes the request ends it cleanly, after the queued events and an unannounced for each namespace still announced. See spec/behavior.md, “Discovery”, for the timing. prefix follows the namespace rules; one that equals, contains or falls under a prefix this connection is already discovering raises a RelayError with code PREFIX_OVERLAP before anything is sent.

def close(self) -> None

Close this connection and all requests on it. Idempotent.

async def __aenter__(self) -> Self
class RelaySubscription(Subscription)

The handle caprid_relay.RelaySubscriber.subscribe returns.

The core handle (request_id, update(), cancel()) plus two awaitables. subscribed settles when the relay answers: it resolves with the acceptance info on_subscribed receives and raises SubscriptionRequestError when the relay rejected the request or it timed out, SubscriptionConnectionClosedError when the connection ended first, or the reason given to cancel() (a CLOSED RelayError when none was given). ended settles once, when the subscription can no longer deliver, with a SubscriptionEnd; it never raises. Publisher completion settles neither, and neither implies exactly-once or gap-free delivery.

It subclasses the core handle so subscribe() keeps returning a Subscription; every inherited member is overridden and the core’s own slots are never set.

Source

Attributes

  • subscribed: RelayAwaitable[SubscribeOk] (property) — Settles when the relay answers. See the class docstring for what it raises.

    Every waiter is isolated: cancelling one, as asyncio.wait_for does on timeout, leaves the subscription’s readiness intact for every other.

  • ended: RelayAwaitable[SubscriptionEnd] (property) — Settles once, when the subscription can no longer deliver, with how it ended. Never raises.

    Every waiter is isolated, as for subscribed.

  • request_id: int (property) — The Request ID the SUBSCRIBE carried.

    Raises when the subscription ended before its request was sent (the connection had already ended): the error it ended with, or a CLOSED RelayError.

def update(self, parameters: MessageParameters) -> None

Send REQUEST_UPDATE, or queue it in order until the relay accepts the subscription.

def cancel(self, reason: object = None) -> None
class RelaySubscriptionHandlers

Callbacks for one subscription made through RelaySubscriber.subscribe.

Each is optional and runs synchronously as the protocol layer delivers the event; the SDK adds no queue. The names say what happened between this connection and the relay. The borrowed RelaySubscriber.track_source keeps the core’s protocol names (on_ok, on_done).

Source

Attributes

  • on_subscribed: Callable[[SubscribeOk], None] | None — The relay accepted the subscription (SUBSCRIBE_OK) and will forward objects.

    info carries the track alias, the relay’s parameters (largest_object when the track already has content), and the track properties as the relay sends them, which can differ from what the publisher declared.

  • on_error: Callable[[RequestError], None] | None — The relay answered the request with REQUEST_ERROR.

    It rejected the subscription, or the request failed before acceptance (request timeout, or the request stream failing). A REQUEST_ERROR can also arrive after acceptance, for example when the relay refuses an update(), and it ends the subscription then too (issue #94). Connection shutdown and GOAWAY never arrive here: a pending subscription ends through subscribed and ended, and an accepted one surfaces them as subgroup resets and connection closure.

  • on_object: Callable[[MoqtObject], None] | None — One object arrived. Payloads are uninterpreted bytes. Objects can arrive after on_publisher_done.

  • on_subgroup_end: Callable[[SubgroupEnd], None] | None — A subgroup stream finished cleanly (FIN).

  • on_subgroup_reset: Callable[[SubgroupReset], None] | None — A subgroup stream was reset; objects from it may be missing.

    Called for every reset the relay forwards. The relay may drop a reset for a subgroup it had not begun forwarding, so an abandoned group can arrive as nothing at all, only a gap in group IDs.

  • on_publisher_done: Callable[[PublishDone], None] | None — The publisher finished with the track (PUBLISH_DONE).

    A control notification, not the end of the subscription: objects still in transit can arrive after it, and the request stays open until cancel() or connection shutdown. The SDK does not resubscribe, and resubscribing cannot bring back a finished track: against moq-relay, a subscribe after the publisher has gone is refused (request error 16 unroutable), and one while its namespace stays announced replays the track’s cached last group and another done one round trip later. Back off, and do not resubscribe to a track the publisher ended. A done that follows this subscription’s own update() is different: moq-relay ends the subscription on any update, the track is still live, and resubscribing is how to apply the change. After a publisher crash the done arrives only at the relay’s QUIC idle timeout, about 11 s later on moq-relay 0.17.0. See spec/relay-interop.md, “Publisher restarts”.

  • on_update_ok: Callable[[], None] | None — The relay acknowledged an update() on this subscription (REQUEST_OK).

    moq-relay never does: it ends the subscription with PUBLISH_DONE on any update (spec/behavior.md, relay interop deviation 4).

  • on_state_notify: Callable[[MessageParameters], None] | None — The publisher changed the subscription’s state on its own (PUBLISH_STATE_NOTIFY).

    The parameters carry only what changed. Informational.

  • on_goaway: Callable[[Goaway], None] | None — GOAWAY on this request stream: the relay wants this request re-issued, possibly elsewhere.

    A GOAWAY on the connection itself ends the connection instead; observe closed for that.

class RelayTrackWriter

One registered track on a publisher connection. Create one with RelayPublisher.register_track.

Registration sends nothing; write and open_group send, after waiting for relay demand. The behaviour behind this class is the track’s write slot (see CONTEXT.md and the “Write phases and interruptions” table in spec/behavior.md).

Source

Attributes

  • name

  • has_demand: bool (property) — Whether the relay currently has an upstream subscription for this track, so a write would proceed at once.

    Demand is the relay’s own subscription, shared by every downstream viewer and ended when the relay chooses: it is neither a viewer count nor a liveness signal. Always False once the track has ended, whatever the relay asks for; read closed to tell a track that ended apart from one the relay has not subscribed to.

  • closed: bool (property) — True once the application closed the track or the publisher connection ended.

    Individual write failures reset their stream and leave the track open.

def demand_changes(self) -> AsyncIterator[bool]

Iterate relay demand: the current state first, then one value per relay subscription change.

Yields True when the relay subscribes or replaces its subscription (so True can repeat) and False when its subscription ends, and stops once the track ends, however it ended. This is how a catalog re-sends its document on every relay subscription. Values are per relay subscription, never per downstream viewer.

def close(self) -> None

End the track and cancel an outstanding write or group. The publisher connection stays open.

async def write_text(
self,
text: str,
*,
properties: Sequence[KeyValuePair] | None = None,
) -> None

Encode UTF-8 text and write one object with the same semantics as write.

async def write_json(
self,
value: object,
*,
properties: Sequence[KeyValuePair] | None = None,
) -> None

Serialize JSON and write one object; serialization errors leave the writer usable.

async def write(
self,
payload: bytes,
*,
properties: Sequence[KeyValuePair] | None = None,
) -> None

Send one independent object/group.

Waits for relay demand (see has_demand). Returns when the transport accepts the complete group, not when a downstream viewer acknowledges it. Only one write or group may be pending; await it before writing again. Bytes are copied. Failure resets this group and leaves the track usable while the connection is open. Data may already have reached the relay; choose the next object explicitly (no automatic retry). Properties are encoded as supplied; the relay may transform them before delivery. A 0x02 or 0x06 property sets this group’s delivery timeout instead of the track’s. An object older than its OBJECT_DELIVERY_TIMEOUT when it would reach the transport, counted from this call and including the wait for demand, raises DELIVERY_TIMEOUT: the data is stale, so write fresh data rather than this object again.

async def open_group(
self,
payload: bytes | None = None,
*,
properties: Sequence[KeyValuePair] | None = None,
) -> RelayGroupWriter

Open a multi-object group: one stream carrying objects 0, 1, 2, … until close().

Waits for relay demand, for room under the connection’s settling-group limit and for the stream within write_timeout. Only one write or group may be pending per track. If the relay’s subscription changes or ends while the group is open, the group is reset and its writer raises; the track stays open, so start the next group at the next random-access point.

With payload, the group opens with it as object 0 and returns once the transport takes it (object_count is 1). Its delivery timeout counts from this call, so every wait before the stream opens counts (draft-ietf-moq-transport-22 section 5.2), and a 0x02 or 0x06 property in properties sets the group’s delivery timeouts. If it expires this raises DELIVERY_TIMEOUT and the group is reset. Without payload there is no object to time, so only write_timeout bounds the wait; properties without payload is a TypeError.

class RequestError

REQUEST_ERROR (§9.4): the peer refused a request.

The session also reports a request that fails on its stream (FIN before any response, a reset, or the response timeout) in this shape, so a consumer has one failure path.

Source

Attributes

  • kind: Literal['request-error']
  • error_code: int
  • retry_interval: int
  • reason: str
  • redirect: Redirect | None
class StateNotifyEvent

Publisher-initiated subscription state changes.

Source

Attributes

  • value: MessageParameters
  • type: Literal['state_notify']
class SubgroupEndEvent

Clean FIN of one subgroup, not the whole subscription.

Source

Attributes

  • value: SubgroupEnd
  • type: Literal['subgroup_end']
class SubgroupResetEvent

A subgroup reset; objects may be missing.

Source

Attributes

  • value: SubgroupReset
  • type: Literal['subgroup_reset']
class SubscribeOk

SUBSCRIBE_OK (§9.7): the publisher accepted a subscription.

track_alias is the alias its data streams carry; parameters and track_properties are as the publisher sent them.

Source

Attributes

  • kind: Literal['subscribe-ok']
  • track_alias: int
  • parameters: MessageParameters
  • track_properties: list[KeyValuePair]
class SubscribedEvent

Subscription acceptance.

Source

Attributes

  • value: SubscribeOk
  • type: Literal['subscribed']
class Subscription

A live subscription request.

MOQT has no UNSUBSCRIBE message: teardown is the request stream’s lifecycle (§6.4.2.3).

Source

Attributes

  • request_id: int (property) — The Request ID the SUBSCRIBE carried (§6.4.2.1).
def update(self, parameters: MessageParameters) -> None

Modify the subscription (REQUEST_UPDATE, §9.5).

def cancel(self, reason: object = None) -> None

Tear the subscription down.

class SubscriptionEnd

How a subscription ended.

kind is the lifecycle name; cause is what happened on the wire or locally; accepted says whether SUBSCRIBE_OK had arrived; error is the moq_core.RequestError for a request error, the connection’s failure (None after a clean shutdown) for closure, and the caller’s reason for a cancel.

Source

Attributes

  • kind: SubscriptionEndKind
  • cause: Literal['request-error', 'connection-closed', 'cancelled']
  • accepted: bool
  • error: object
class SubscriptionEvents(BoundedEvents[SubscriptionEvent])

Single-consumer iterator. Use async with for deterministic cleanup on break/errors.

Create one with caprid_relay.RelaySubscriber.subscribe_events. At most capacity events are retained, including lifecycle events. Overflow discards the queue, cancels only this request and raises on reads. No transport backpressure is implied. Publisher completion is an event, never EOF. Explicit close produces EOF; failures raise.

Source

class SubscriptionHandlers

Callbacks for one subscription. Every one is optional and must be synchronous.

An async callback raises TypeError at subscribe time; a callback that returns an awaitable has it discarded with a RuntimeWarning. Relay’s subscribe_events() is the async way in.

Source

Attributes

  • on_ok: Callable[[SubscribeOk], None] | None — The publisher accepted the subscription.

  • on_error: Callable[[RequestError], None] | None — The request failed: a REQUEST_ERROR, or a stream failure or timeout in the same shape.

  • on_object: Callable[[MoqtObject], None] | None — An object arrived on one of the subscription’s data streams.

  • on_subgroup_end: Callable[[SubgroupEnd], None] | None — A subgroup stream finished cleanly.

  • on_subgroup_reset: Callable[[SubgroupReset], None] | None — A subgroup stream was reset; see SubgroupReset.

  • on_done: Callable[[PublishDone], None] | None — PUBLISH_DONE arrived: the publisher finished the subscription.

  • on_update_ok: Callable[[], None] | None — Response to a REQUEST_UPDATE sent on this subscription.

  • on_state_notify: Callable[[MessageParameters], None] | None — PUBLISH_STATE_NOTIFY (§9.10): the publisher changed subscription state on its own.

    The parameters carry only what changed, with LARGEST_OBJECT marking where. Informational; the session sends no reply.

  • on_goaway: Callable[[Goaway], None] | None — GOAWAY on this request stream: re-issue the request (possibly elsewhere).

class SupervisedPublisher

A publisher that reconnects. Create one with connect_supervised_publisher.

Each connection is a new generation with its own namespace; tracks are registered once and rebound to every generation.

Source

Attributes

  • generation: PublisherGeneration | None (property) — The live generation, or None between connections and after closing.
  • status: SupervisedStatus (property) — Where the supervisor stands; see SupervisedStatus.
  • closed: RelayAwaitable[Closed] (property) — Resolves once the supervisor stops, with the failure that stopped it or the cancellation’s reason as error (None after close). Never raises.
def register_track(
self,
track: str,
*,
track_properties: Sequence[KeyValuePair] | None = None,
write_timeout: float = DEFAULT_WRITE_TIMEOUT,
) -> RelayTrackWriter

Register a track every generation serves, and return the writer that outlives the generations.

The same rules as RelayPublisher.register_track, checked at once: track names are unique per supervised publisher, including after their writer closes. Writes wait for demand on the live generation; while none is up they raise RECONNECTING, and an operation the connection’s end interrupts raises RECONNECTING too, never CLOSED, so a frame writer treats it as an interruption and resumes at the next keyframe.

def rotate(self) -> None

Replace the live generation make-before-break, as happens on its own rotate_before before the credential expires.

The next generation connects, with a fresh token, and every track is registered on it. Each track’s writer then moves to it at its next write or group, once the track has demand there or has none on the old one. The old generation closes once every track has moved, after 10 seconds at most. A no-op while no generation is up.

async def close(self) -> None

Stop reconnecting and close the live generation as RelayPublisher.close does. Idempotent.

async def __aenter__(self) -> Self
class SupervisedSubscriber

A subscriber that reconnects and keeps wanted tracks subscribed. Create one with connect_supervised_subscriber.

Source

Attributes

  • status: SupervisedStatus (property) — Where the supervisor stands; see SupervisedStatus.
  • closed: RelayAwaitable[Closed] (property) — Resolves once the supervisor stops, with the failure that stopped it or the cancellation’s reason as error (None after close). Never raises.
def want(
self,
key: str,
namespace: Sequence[str],
track: str,
handlers: WantHandlers | None = None,
*,
parameters: MessageParameters | None = None,
generations: bool = False,
stale_after: float | None = None,
retry: SupervisionBackoff | None = None,
until: Until = 'first-object',
drain: float = 0.0,
) -> None

Keep a subscription to a track open until unwant(key).

generations=True makes namespace a supervised publisher’s root: the want follows the newest generation announced one field below it, and the subscriber’s token must allow discovering the root. stale_after (seconds) resubscribes when no object arrived for that long, for tracks that are never silent while live. parameters is sent on every SUBSCRIBE; retry is the backoff between resubscriptions (from 0.25 s, doubling up to 8 s).

A move (a newer generation, or a re-want with another track or parameters) is made before the break, as switch_to makes one: the current subscription keeps delivering until an object of the replacement passes until ("first-object", the default, or a synchronous predicate), and on_switched reports the cutover. Compare the object’s own timeline with what was delivered, never group IDs: each generation and track numbers its own groups. drain (seconds, default 0) keeps the old subscription delivering its in-flight groups that long after the cutover.

Wanting a key again with the same namespace and generations updates the want in place: the handlers, retry, stale_after, until and drain are swapped, and another track or parameters moves the subscription. Another namespace or generations replaces the want. Arguments are checked at once.

def unwant(self, key: str) -> None

Cancel a want’s subscription and forget it. Unknown keys are ignored.

def rotate(self) -> None

Replace the connection make-before-break, as happens on its own rotate_before before the credential expires.

A new connection opens, with a fresh token, and every want moves its subscription to it as a move would, keeping the old one delivering until the cutover. A want whose target is unchanged cuts over on the first object the old one did not deliver, with no callback: nothing is lost or repeated. The old connection closes once every want has moved, after 10 seconds at most. A no-op while no connection is up.

def close(self) -> None

Stop reconnecting, cancel every want and close the connection. Idempotent.

async def __aenter__(self) -> Self
class SupervisionBackoff

The delay before each retry: it starts at initial_delay and doubles up to max_delay, with jitter.

Both are seconds; None takes the default for what is being retried.

Source

Attributes

  • initial_delay: float | None — The delay before the first retry, in seconds.
  • max_delay: float | None — The longest delay between retries, in seconds.
class SwitchableHandlers

Callbacks for a switchable subscription.

Each runs synchronously, for the active subscription’s events only, and receives the active track’s name as its last argument. A pending subscription reaches no handler before its cutover.

Source

Attributes

  • on_subscribed: Callable[[SubscribeOk, str], None] | None — The first subscription accepted (SUBSCRIBE_OK). A later subscription’s acceptance arrives with its cutover, in on_switched.
  • on_error: Callable[[RequestError, str], None] | None — The active subscription ended with REQUEST_ERROR: the first one was rejected, or an accepted one was refused later. The switchable has ended. A pending subscription’s rejection fails its switch_to instead.
  • on_object: Callable[[MoqtObject, str], None] | None — One object from the active subscription. Payloads are uninterpreted bytes.
  • on_subgroup_end: Callable[[SubgroupEnd, str], None] | None — One of the active subscription’s subgroup streams finished cleanly (FIN).
  • on_subgroup_reset: Callable[[SubgroupReset, str], None] | None — One of the active subscription’s subgroup streams was reset; objects from it may be missing.
  • on_publisher_done: Callable[[PublishDone, str], None] | None — The active track’s publisher finished with it (PUBLISH_DONE). Not an ending, as on RelaySubscriptionHandlers.on_publisher_done; a pending switch carries on.
  • on_state_notify: Callable[[MessageParameters, str], None] | None — The publisher changed the active subscription’s state (PUBLISH_STATE_NOTIFY).
  • on_goaway: Callable[[Goaway, str], None] | None — GOAWAY on the active subscription’s request stream. Informational; nothing is retried.
  • on_switched: Callable[[SwitchedInfo], None] | None — A cutover: to_track is now active and from_track has been cancelled (or is draining). Runs before the first object of to_track, which is the one that passed until. Decoders drop state that assumed continuity here.
class SwitchableSubscription

The handle caprid_relay.RelaySubscriber.subscribe_switchable returns.

Handlers see one subscription at a time, the active one. switch_to moves them to another track in the same namespace, or the same track with other parameters, make-before-break.

Source

Attributes

  • track: str (property) — The active subscription’s track name.

  • pending: str | None (property) — The pending subscription’s track name, while a switch is pending.

  • subscribed: RelayAwaitable[SubscribeOk] (property) — Settles with the acceptance info of the first subscription to be active and accepted.

    That is the first one, or the one a switch made before the first one was answered. Raises as RelaySubscription.subscribed does if the switchable ends before that.

  • ended: RelayAwaitable[SubscriptionEnd] (property) — Settles once with the active subscription’s ending, when it ends other than by a cutover, or when cancel runs. Any pending switch is cancelled with it. Never raises.

def switch_to(
self,
track: str,
*,
parameters: MessageParameters | None = None,
until: Until = 'first-object',
drain: float = 0.0,
cancellation: CancellationToken | None = None,
) -> RelayAwaitable[SwitchedInfo]

Subscribe to track in the same namespace and cut over once one of its objects passes until.

The active subscription keeps delivering until then, and is cancelled at the cutover. The track may be the active one, to change parameters. parameters are not inherited from the subscription being replaced: omitted means none.

until is "first-object" (the default), which promotes on the new subscription’s first object, or a synchronous predicate that promotes on the first object it accepts. Objects before the cutover are discarded, except those of groups later than the one that cuts over (arrived early, out of order), which follow it. Compare the object’s own timeline (a timestamp property, say) with what the active subscription delivered, so the cutover repeats media rather than skipping it: group IDs are no such timeline, since each track numbers its own groups. A new subscription can also start with a stale cached group, or lose its first object through another relay (issue #243); a timeline predicate absorbs both.

drain keeps the old subscription open that many seconds after the cutover, from 0 to MAX_TIMEOUT (2147483.647, as for every SDK deadline). Groups arrive out of order across streams, most of all through another relay, so an older group of the old track can still be in flight at the cutover. While it drains, its objects, subgroup ends and resets from groups no later than the largest it had delivered still reach the handlers, with the old track’s name, after on_switched; everything else from it is discarded.

cancellation abandons the switch while it is pending: the new subscription is cancelled and the active one stays. A newer switch_to also cancels a pending one, which raises CLOSED. The returned awaitable raises as RelaySubscription.subscribed does if the new subscription ends before its cutover, the token’s reason when it is cancelled, or what until raised; the active subscription carries on. Raises at once for an invalid track or options, a closed connection, or an ended switchable.

def cancel(self, reason: object = None) -> None

Cancel the active subscription and any pending one. Idempotent.

class SwitchedInfo

What on_switched receives and SwitchableSubscription.switch_to resolves with.

Source

Attributes

  • from_track: str — The track that was active before the cutover.
  • to_track: str — The track now active; the same as from_track when only the parameters changed.
  • subscribed: SubscribeOk — The new subscription’s acceptance info.
class UpdateOkEvent

Acknowledgement of a subscription update.

Source

Attributes

  • value: None
  • type: Literal['update_ok']
class WantHandlers

The callbacks a want delivers to. Like RelaySubscriptionHandlers, they must be synchronous.

Source

Attributes

  • on_subscribed: Callable[[SubscribeOk, tuple[str, ...]], None] | None — A subscription was accepted. The second argument is the namespace it was made on: the generation’s, when following one. A move’s replacement reports its acceptance in on_switched instead.
  • on_object: Callable[[MoqtObject], None] | None — One object, from whichever subscription is current.
  • on_subgroup_end: Callable[[SubgroupEnd], None] | None — A subgroup stream ended cleanly.
  • on_subgroup_reset: Callable[[SubgroupReset], None] | None — A subgroup stream was reset.
  • on_publisher_done: Callable[[PublishDone], None] | None — The relay sent PUBLISH_DONE on the current subscription; the want resubscribes after its backoff.
  • on_restarted: Callable[[RestartReason, tuple[str, ...]], None] | None — An accepted subscription ended and the want will replace it: drop decoder state that assumed continuity. Receives the reason and the namespace the ended subscription was made on. Called before any object of the replacement; the gap is never filled.
  • on_switched: Callable[[WantSwitchedInfo], None] | None — A move’s cutover: the want now delivers from the to_ subscription, and the from_ one was cancelled or drains its in-flight groups. Runs before the replacement’s first object, the one that passed until. Decoders drop state that assumed continuity here; no on_restarted or on_subscribed runs for a move.
  • on_error: Callable[[BaseException], None] | None — A failure the want does not retry, after which it stops and is removed: a REQUEST_ERROR no wait fixes (credentials, an unsupported or malformed request) as a SubscriptionRequestError, or discovery refusing the root.
class WantSwitchedInfo

What on_switched receives at a want’s cutover.

Source

Attributes

  • from_namespace: tuple[str, ...] — The namespace the want delivered from before the cutover: the generation’s, when following one.
  • from_track: str — The track the want delivered from before the cutover.
  • to_namespace: tuple[str, ...] — The namespace it delivers from now.
  • to_track: str — The track it delivers from now.
  • subscribed: SubscribeOk — The new subscription’s acceptance info.
class MoqTrackSource(Protocol)

The subscribe / fetch / track-status subset of a session, plus closure observation, without shutdown.

Source

Attributes

  • closed: asyncio.Future[Closed] (property) — Resolves when the owning connection ends; an error denotes failure.
def subscribe(
self,
options: MoqtSubscribeOptions,
handlers: SubscriptionHandlers | None = None,
) -> Subscription

Subscribe to a track, as MoqtSession.subscribe does; the caller owns the request.

def fetch(
self,
options: MoqtSubscribeOptions,
handlers: FetchHandlers | None = None,
) -> FetchHandle

Fetch a range of a track, as MoqtSession.fetch does; the caller owns the request.

def track_status(
self,
options: MoqtSubscribeOptions,
handlers: TrackStatusHandlers | None = None,
) -> None

Ask for a track’s status, as MoqtSession.track_status does.

class MoqtTransport(Protocol)

Structural subset of a WebTransport session the session drivers need.

Source

MoqtTransport.incoming_unidirectional_streams

Section titled “MoqtTransport.incoming_unidirectional_streams”
def incoming_unidirectional_streams(self) -> AsyncIterator[ReceiveStream]

Iterate peer-initiated unidirectional streams until the transport ends.

MoqtTransport.incoming_bidirectional_streams

Section titled “MoqtTransport.incoming_bidirectional_streams”
def incoming_bidirectional_streams(self) -> AsyncIterator[BidirectionalStream]

Iterate peer-initiated bidirectional streams until the transport ends.

MoqtTransport.create_unidirectional_stream

Section titled “MoqtTransport.create_unidirectional_stream”
async def create_unidirectional_stream(self) -> SendStream

Open a unidirectional stream to the peer.

async def create_bidirectional_stream(self) -> BidirectionalStream

Open a bidirectional stream to the peer.

def close(self, close_code: int = 0, reason: str = '') -> None

Close the session. Idempotent; a transport that already ended ignores it.

async def wait_closed(self) -> None

Return once the session ended cleanly (either side); raise the error that ended it otherwise.

class Cancelled(Exception)

The default reason recorded when a token is cancelled without one.

Source

class RelayError(Exception)

An SDK failure with a stable code and origin phase; neither promises safe replay.

Branch on code (see spec/behavior.md), never on the message. The underlying exception, when there is one, is chained as __cause__.

Source

Attributes

  • code: RelayErrorCode
  • phase: RelayErrorPhase
class SubscriptionConnectionClosedError(RelayError)

The connection ended, without proving that subscription delivery was drained.

Source

class SubscriptionOverflowError(RelayError)

A stalled consumer exceeded the event capacity; the subscription was cancelled.

Source

class SubscriptionRequestError(RelayError)

Request rejection, timeout, or request failure, with the protocol details preserved.

Source

Attributes

  • request_error
LocationFilter: TypeAlias = ...

Which Objects a subscription or fetch covers.

Draft-22 opens the LOCATION_FILTER value with a Location Filter Type that selects the fields after it; draft-20 length-prefixes up to four varints and the number present selects the meaning.

Source

RestartReason: TypeAlias = ...

Why a want replaced its subscription.

publisher-done: the relay sent PUBLISH_DONE. stale: no object arrived for stale_after. generation: the current generation was withdrawn with none newer, or a move to a newer one could not be made before the break. connection: the subscriber connection ended and was replaced. terminated: the relay ended the subscription with REQUEST_ERROR after accepting it.

Source

SubscriptionEndKind: TypeAlias = Literal['rejected', 'terminated', 'cancelled']

The lifecycle name for how a subscription ended.

rejected: the relay answered the request with REQUEST_ERROR before accepting it (including the request timeout). terminated: an accepted subscription can no longer deliver (the connection ended, or a REQUEST_ERROR arrived after acceptance, which is how a refused update() is answered, issue #94), or the connection ended before any answer. cancelled: the application cancelled it.

Source

SubscriptionEvent: TypeAlias = ...

Typed callback arrivals; no ordering across streams or drain guarantee is implied.

Source

SupervisedStatus: TypeAlias = Literal['connecting', 'live', 'reconnecting', 'closed']

Where a supervised connection stands: connecting until the first connection is up, live while one is, reconnecting between connections, and closed once the application closed it, its cancellation token was cancelled, or a first attempt failed in a way that is not retried.

Source

DEFAULT_ENDPOINT = 'https://relay.mux.global'

The default Relay endpoint pinned in spec/protocol.json.

Source

RelayErrorCode = ...

Source

RelayErrorPhase = ...

Source

RelayTransportFactory = Callable[[str, Sequence[str]], ConnectingTransport]

Supply a transport for (url, protocols).

protocols are the MOQT protocol IDs to offer, in preference order: moqt-22, then moqt-20 as a fallback. A transport reports the one the peer selected as protocol_id (TA-8).

Native QUIC on aioquic (caprid_relay.quic.connect_quic) by default; tests supply in-memory pairs.

Source