# moq_media

> Python API reference for moq_media: Pure LOC/MSF and H.264 format helpers.

URL: https://www.caprid.com/docs/reference/python/moq_media/

From the `caprid-moq-media` package: `import moq_media`

Pure LOC/MSF format helpers; transport belongs to the application.

The one encoder, `moq_media.opus`, needs the `opus` extra and is not imported here.

## Functions

### annexb_to_avcc

```python
def annexb_to_avcc(data: bytes, *, drop_parameter_sets: bool = True) -> bytes
```

Re-frame an Annex B access unit as 4-byte-length-prefixed NAL units.

`avc1` promises parameter sets out of band, so SPS/PPS (and access-unit delimiters) are dropped
by default; they travel in the avcC instead.

<small>[Source](https://github.com/caprids/relay-sdks/blob/main/python/packages/moq-media/src/moq_media/h264.py#L40)</small>

### avc_codec_string

```python
def avc_codec_string(avcc: bytes) -> str
```

The `avc1.PPCCLL` WebCodecs codec string an avcC declares (RFC 6381 §3.3).

<small>[Source](https://github.com/caprids/relay-sdks/blob/main/python/packages/moq-media/src/moq_media/h264.py#L104)</small>

### build_avcc

```python
def build_avcc(sps: Sequence[bytes], pps: Sequence[bytes]) -> bytes
```

Build an AVCDecoderConfigurationRecord (avcC) with 4-byte NAL length prefixes.

<small>[Source](https://github.com/caprids/relay-sdks/blob/main/python/packages/moq-media/src/moq_media/h264.py#L65)</small>

### build_interrupt

```python
def build_interrupt(
    *,
    until_us: int,
    from_us: int | None = None,
    track: str = AUDIO_TRACK,
) -> bytes
```

An interrupt object's payload: compact JSON, `{"track":…,"fromUs":…,"untilUs":…}`, `fromUs` only if given.

Raises `ValueError` on an empty track, a timestamp that is not a nonnegative safe integer, or `from_us` not
before `until_us`.

<small>[Source](https://github.com/caprids/relay-sdks/blob/main/python/packages/moq-media/src/moq_media/interrupt.py#L33)</small>

### build_media_timeline

```python
def build_media_timeline(entries: Iterable[MediaTimelineEntry]) -> bytes
```

Return a timeline object's payload: the records in the order given, as compact JSON.

The same form serves an independent document (every retained record) and an incremental one (only new ones).

<small>[Source](https://github.com/caprids/relay-sdks/blob/main/python/packages/moq-media/src/moq_media/timeline.py#L55)</small>

### build_msf_catalog

```python
def build_msf_catalog(
    namespace: Sequence[str],
    *,
    video: VideoTrackInfo | None = None,
    audio: AudioTrackInfo | None = None,
    media: Sequence[VideoTrackInfo | AudioTrackInfo] = (),
    data: Sequence[DataTrackInfo] = (),
    media_timeline: MediaTimelineTrackInfo | None = None,
    generated_at_ms: int | None = None,
) -> bytes
```

The independent MSF catalog JSON for a live LOC broadcast, byte-compatible with the Video.js parser.

The catalog leaves `isComplete` out: msf-01 §5.1.3 forbids it while false, and once present it can
never be removed. End a broadcast with [`build_msf_end_catalog`](#build_msf_end_catalog).

`video` and `audio` are shorthand for one-entry `media` lists, listed first. `media` lists
further video and audio tracks in order: simulcast layers sharing an `alt_group`, or several
sources in their own render groups. Every media entry carries its render group (default 1); the
builder sets no target latency or buffer, which §5.2.8 and §5.2.9 require to be identical across
a render group.

<small>[Source](https://github.com/caprids/relay-sdks/blob/main/python/packages/moq-media/src/moq_media/catalog.py#L103)</small>

### build_msf_end_catalog

```python
def build_msf_end_catalog(*, generated_at_ms: int | None = None) -> bytes
```

The catalog that ends a broadcast (msf-01 §11.3): `isComplete: true` and no tracks.

It commits that no track will be added and no content published, so send it only for a broadcast
that ends on purpose, once every media track has ended (closing a track sends PUBLISH_DONE Track
Ended), and never on a crash or lost connection.

<small>[Source](https://github.com/caprids/relay-sdks/blob/main/python/packages/moq-media/src/moq_media/catalog.py#L241)</small>

### build_transcript_segment

```python
def build_transcript_segment(
    *,
    seq: int,
    from_us: int,
    text: str,
    until_us: int | None = None,
    final: bool = False,
    track: str = AUDIO_TRACK,
) -> bytes
```

A transcript object's payload: compact JSON, `{"seq":…,"track":…,"fromUs":…,"untilUs":…,"text":…,"final":…}`.

`untilUs` only if given. Raises `ValueError` on a number that is not a nonnegative safe integer, an empty
track, text that is not valid Unicode, `until_us` not after `from_us`, or a final segment without `until_us`.

<small>[Source](https://github.com/caprids/relay-sdks/blob/main/python/packages/moq-media/src/moq_media/transcript.py#L51)</small>

### compose_player_url

```python
def compose_player_url(
    relay_url: str,
    namespace: Sequence[str],
    *,
    token: str | None = None,
    track_name: str = CATALOG_TRACK,
) -> str
```

Compose the `moqt://` MSF source URL a Video.js MoQ player takes, the inverse of its parser.

The relay origin may be written as `https://` or `moqt://`; an existing path and query
survive. The token, when given, rides the `c4m` fragment parameter, which the player moves onto
the connect URL's `jwt` query parameter.

<small>[Source](https://github.com/caprids/relay-sdks/blob/main/python/packages/moq-media/src/moq_media/source.py#L32)</small>

### encode_name_component

```python
def encode_name_component(value: str) -> str
```

MSF namespace/name string form: `[a-zA-Z0-9_]` literal, every other UTF-8 byte as `.xx` lowercase hex.

<small>[Source](https://github.com/caprids/relay-sdks/blob/main/python/packages/moq-media/src/moq_media/source.py#L10)</small>

### encode_namespace_name

```python
def encode_namespace_name(namespace: Sequence[str], track_name: str) -> str
```

The `ns1-ns2--name` identifier that follows `#msf:` in a source URL (msf-01 §11.1.2).

<small>[Source](https://github.com/caprids/relay-sdks/blob/main/python/packages/moq-media/src/moq_media/source.py#L25)</small>

### extract_parameter_sets

```python
def extract_parameter_sets(data: bytes) -> tuple[list[bytes], list[bytes]]
```

SPS and PPS NAL units found in an Annex B byte stream (an encoder's extradata or a keyframe).

<small>[Source](https://github.com/caprids/relay-sdks/blob/main/python/packages/moq-media/src/moq_media/h264.py#L58)</small>

### find_replacement_track

```python
def find_replacement_track(
    current: MsfCatalogTrack,
    tracks: Sequence[MsfCatalogTrack],
) -> MsfCatalogTrack | None
```

Find the track in a newer catalog that replaces `current`, a track of an older one.

A publisher that replaces a track lists a new name in its place (msf-01 §5.3: a changed track is a
new Namespace|Name tuple), as a voice agent does to interrupt its own reply. While `tracks` still
lists `current`'s namespace and name the answer is `None`. Otherwise it is the first track listed
with `current`'s namespace, role and render group, or `None` when there is none (the track was
removed, not replaced).

<small>[Source](https://github.com/caprids/relay-sdks/blob/main/python/packages/moq-media/src/moq_media/catalog.py#L406)</small>

### group_msf_tracks

```python
def group_msf_tracks(tracks: Sequence[MsfCatalogTrack]) -> tuple[MsfTrackGroup, ...]
```

Group catalog tracks into the sets a viewer picks one track from, in order of each set's first track.

Tracks sharing a render group and an alternate group form one set, and every other track is a
set of its own. A viewer plays one track from each set of a render group, and moves between
alternates with `want(..., until=...)`.

<small>[Source](https://github.com/caprids/relay-sdks/blob/main/python/packages/moq-media/src/moq_media/catalog.py#L384)</small>

### iter_annexb_nal_units

```python
def iter_annexb_nal_units(data: bytes) -> Iterator[bytes]
```

Split an Annex B byte stream (3- or 4-byte start codes) into NAL units without their start codes.

<small>[Source](https://github.com/caprids/relay-sdks/blob/main/python/packages/moq-media/src/moq_media/h264.py#L17)</small>

### loc_properties

```python
def loc_properties(
    timestamp_us: int,
    *,
    timescale: int = MICROSECONDS_PER_SECOND,
    video_config: bytes | None = None,
    audio_config: bytes | None = None,
) -> list[KeyValuePair]
```

Package a microsecond timestamp; pass decoder configuration only on keyframes.

Other timescales round to the nearest tick, with ties rounded upward.

<small>[Source](https://github.com/caprids/relay-sdks/blob/main/python/packages/moq-media/src/moq_media/loc.py#L40)</small>

### loc_timestamp_us

```python
def loc_timestamp_us(
    properties: Iterable[KeyValuePair],
    *,
    track_properties: Iterable[KeyValuePair] = (),
) -> int | None
```

Return an object's LOC Timestamp in microseconds, or `None` when it has no usable one.

The Timestamp is scaled by the first Timescale that is a positive safe integer: the object's, then the track's
(`track_properties`, as `SUBSCRIBE_OK` carries them). The MSF catalog's `timescale` is not consulted: MSF
never ties it to the LOC Timestamp. With none, the Timestamp already is wall-clock microseconds since the Unix
epoch (LOC §2.3.1.1). Ties round upward; a result past 2^53 - 1 is `None`.

<small>[Source](https://github.com/caprids/relay-sdks/blob/main/python/packages/moq-media/src/moq_media/loc.py#L72)</small>

### loc_track_properties

```python
def loc_track_properties(timescale: int = MICROSECONDS_PER_SECOND) -> list[KeyValuePair]
```

Declare timestamp units on a LOC track; returns fresh properties on every call.

<small>[Source](https://github.com/caprids/relay-sdks/blob/main/python/packages/moq-media/src/moq_media/loc.py#L22)</small>

### media_timeline_entry

```python
def media_timeline_entry(
    timestamp_us: int,
    group: int,
    *,
    object: int = 0,
    wallclock_us: int | None = None,
) -> MediaTimelineEntry
```

Return the record for an object whose LOC timestamp on the broadcast timeline is `timestamp_us`.

`wallclock_us` is when that media was captured, as Unix-epoch microseconds. It defaults to `timestamp_us`,
since the broadcast timeline is anchored at the epoch; pass 0 for unknown, as msf-01 allows.

<small>[Source](https://github.com/caprids/relay-sdks/blob/main/python/packages/moq-media/src/moq_media/timeline.py#L37)</small>

### nal_unit_type

```python
def nal_unit_type(nal: bytes) -> int
```

The `nal_unit_type` of a NAL unit without its start code: 7 for SPS, 8 for PPS, 5 for IDR.

<small>[Source](https://github.com/caprids/relay-sdks/blob/main/python/packages/moq-media/src/moq_media/h264.py#L35)</small>

### opus_packet_duration_us

```python
def opus_packet_duration_us(packet: bytes) -> int | None
```

An Opus packet's duration in microseconds from its TOC byte (RFC 6716 §3.1), or `None` if truncated.

Code 0 holds one frame, codes 1 and 2 two, and code 3 the count in the next byte's low six bits.

<small>[Source](https://github.com/caprids/relay-sdks/blob/main/python/packages/moq-media/src/moq_media/audio.py#L15)</small>

### parse_interrupt

```python
def parse_interrupt(payload: bytes) -> Interrupt
```

Read an interrupt object's payload.

Raises `ValueError` on a payload that is not a UTF-8 JSON object with a non-empty string `track` and a
nonnegative safe-integer `untilUs`, or whose `fromUs`, when present, is not one before `untilUs`. Other
fields are ignored.

<small>[Source](https://github.com/caprids/relay-sdks/blob/main/python/packages/moq-media/src/moq_media/interrupt.py#L47)</small>

### parse_media_timeline

```python
def parse_media_timeline(payload: bytes) -> tuple[MediaTimelineEntry, ...]
```

Read a timeline object's payload into its records, in document order.

Raises `ValueError` on a payload that is not a UTF-8 JSON array. Records that do not read as a nonnegative
safe-integer `[number, [number, number], number]` are skipped, a §7.4 template among them; extra items are
ignored. MSF_COMPRESSION payloads are not decompressed.

<small>[Source](https://github.com/caprids/relay-sdks/blob/main/python/packages/moq-media/src/moq_media/timeline.py#L107)</small>

### parse_msf_catalog

```python
def parse_msf_catalog(payload: bytes, *, catalog_namespace: Sequence[str]) -> MsfCatalog
```

Read an independent MSF catalog object's payload into its track descriptions.

`catalog_namespace` is the namespace of the catalog track itself, which tracks that declare
none inherit (§5.2.2). Raises `ValueError` on a payload that is not a UTF-8 JSON object, a
missing or unsupported `version`, a missing `tracks` array, or a delta update
(`deltaUpdate`, not supported). Everything else is lenient, as msf-01 requires: unknown fields
and fields of the wrong type are ignored, track entries without a string `name` are skipped,
and packaging is returned as declared, not interpreted.

<small>[Source](https://github.com/caprids/relay-sdks/blob/main/python/packages/moq-media/src/moq_media/catalog.py#L327)</small>

### parse_transcript_segment

```python
def parse_transcript_segment(payload: bytes) -> TranscriptSegment
```

Read a transcript object's payload.

Raises `ValueError` on a payload that is not a UTF-8 JSON object with a nonnegative safe-integer `seq` and
`fromUs`, a non-empty string `track`, a string `text` and a boolean `final`, or whose `untilUs`, when
present, is not one after `fromUs`, or that is final without `untilUs`. Other fields are ignored.

<small>[Source](https://github.com/caprids/relay-sdks/blob/main/python/packages/moq-media/src/moq_media/transcript.py#L77)</small>

## Classes

### AudioTrackInfo

```python
class AudioTrackInfo
```

The audio track [`build_msf_catalog`](#build_msf_catalog) describes. Fields left as `None` are omitted.

<small>[Source](https://github.com/caprids/relay-sdks/blob/main/python/packages/moq-media/src/moq_media/catalog.py#L47)</small>

**Attributes**

- **`name`**: `str`
- **`codec`**: `str`
- **`samplerate`**: `int` — Opus always declares 48000, the rate it decodes at, whatever is given here.
- **`channel_config`**: `str`
- **`bitrate`**: `int | None`
- **`init_data`**: `bytes | None` — Decoder configuration, emitted inline as `initDataList`.
- **`render_group`**: `int` — Render group (msf-01 §5.2.11): tracks sharing one play together on one timeline.

  Give a second source, such as a screen share and its audio, its own.

- **`alt_group`**: `int | None` — Alternate group (msf-01 §5.2.12): alternate versions of the same content, such as simulcast layers.

  A viewer subscribes to one member. Members share a role and a render group.


### BroadcastClock

```python
class BroadcastClock
```

A publisher's single clock: every track in the broadcast is stamped from it, in Unix-epoch microseconds.

`now_us` is the monotonic time source in microseconds, `time.monotonic_ns() // 1000` by default.
`origin_us` is a moment on that source in integer microseconds; it defaults to the source's time now, rounded
to whole microseconds. `origin_epoch_us` is that moment as Unix-epoch microseconds; it defaults to
`time.time_ns()` at creation less the source time elapsed since the origin, read once. A time `t` on the
source maps to `origin_epoch_us + t - origin_us`, so the timeline advances at the source's rate.

<small>[Source](https://github.com/caprids/relay-sdks/blob/main/python/packages/moq-media/src/moq_media/clock.py#L122)</small>

**Attributes**

- **`origin_us`**: `int` *(property)* — The origin, on the source clock.
- **`origin_epoch_us`**: `int` *(property)* — The origin as microseconds since the Unix epoch.

#### BroadcastClock.now_us

```python
def now_us(self) -> int
```

Return the timeline now: microseconds since the Unix epoch.

#### BroadcastClock.from_source_us

```python
def from_source_us(self, source_us: int) -> int
```

Map a timestamp on this clock's own source to Unix-epoch microseconds, rounded to whole microseconds.

For a capture clock with another origin, use `source_timeline`.

#### BroadcastClock.source_timeline

```python
def source_timeline(
    self,
    *,
    tolerance_us: int = 5000,
    capture_latency_us: int = 0,
) -> SourceTimeline
```

Map a clock with its own origin and rate, kept on the broadcast timeline by calibration.

An `AudioContext`-style clock, or capture timestamps from another origin. `tolerance_us` is how far a
calibration may disagree with the current anchor before it replaces it. `capture_latency_us` is the
capture device's input delay, subtracted from every timestamp; no helper knows it, so the application
supplies it and sets it again when the device changes.

#### BroadcastClock.sample_timeline

```python
def sample_timeline(
    self,
    sample_rate: int,
    *,
    first_sample_us: int | None = None,
) -> SampleTimeline
```

Anchor a sample counter at `first_sample_us` on the timeline (default: now).

The device clock that drives the counter drifts against the source, so re-anchor with a fresh timeline
when the two disagree by more than a frame.

### DataTrackInfo

```python
class DataTrackInfo
```

A non-media track [`build_msf_catalog`](#build_msf_catalog) lists by name and role only.

<small>[Source](https://github.com/caprids/relay-sdks/blob/main/python/packages/moq-media/src/moq_media/catalog.py#L69)</small>

**Attributes**

- **`name`**: `str`
- **`role`**: `str | None` — Must not be a media role (`video`, `audio`, `caption` and the like).

### FrameSequencer

```python
class FrameSequencer
```

One track's frame sequencer. Feed it the subscription's events, then `poll()` for frames.

`clock` is the broadcast's playout clock: it holds the target latency and is where lateness is measured.
`grouping` is `"keyframe"` (a keyframe opens each group and later objects depend on it, as video) or `"frame"`
(every group is independently decodable, as audio). `track_properties` are SUBSCRIBE_OK's, which may declare the
Timescale.

<small>[Source](https://github.com/caprids/relay-sdks/blob/main/python/packages/moq-media/src/moq_media/sequencer.py#L83)</small>

**Attributes**

- **`track_properties`**: `Sequence[KeyValuePair]` — The track properties used to read timestamps; set from each SUBSCRIBE_OK.
- **`stats`**

#### FrameSequencer.object

```python
def object(self, obj: MoqtObject) -> None
```

An object arrived.

#### FrameSequencer.group_ended

```python
def group_ended(self, group_id: int) -> None
```

A group's stream ended cleanly: no further objects of it will come.

#### FrameSequencer.group_reset

```python
def group_reset(self, group_id: int) -> None
```

A group's stream was reset: its remaining objects will not come.

#### FrameSequencer.restart

```python
def restart(self) -> None
```

The subscription was replaced (a Restart or a Move): forget everything and reset the clock; group IDs start
over.

#### FrameSequencer.poll

```python
def poll(self, max_frames: int | None = None) -> SequencerPoll
```

Take the frames that are ready, at most `max_frames` (default all), and when to poll again.

A reader that cannot take frames as they come polls one at a time, so each is judged late when it is taken.

### FrameSequencerStats

```python
class FrameSequencerStats
```

Counters, cumulative across restarts.

<small>[Source](https://github.com/caprids/relay-sdks/blob/main/python/packages/moq-media/src/moq_media/sequencer.py#L58)</small>

**Attributes**

- **`delivered`**: `int` — Frames returned by `poll()`.
- **`late`**: `int` — Frames discarded: too late, or behind what was already delivered.
- **`skipped_groups`**: `int` — Groups abandoned or passed over without being delivered whole.
- **`resyncs`**: `int` — Resyncs: deliveries after nothing arrived within the target latency.
- **`untimed`**: `int` — Objects without a LOC timestamp, which cannot be sequenced.

### H264Config

```python
class H264Config
```

An avc1 decoder record and the codec string derived from it.

<small>[Source](https://github.com/caprids/relay-sdks/blob/main/python/packages/moq-media/src/moq_media/h264.py#L112)</small>

**Attributes**

- **`avcc`**: `bytes`
- **`codec`**: `str` *(property)* — The `avc1.PPCCLL` codec string the record declares.

#### H264Config.from_annexb

```python
def from_annexb(cls, data: bytes) -> H264Config
```

Build the record from the SPS and PPS in Annex B bytes (an encoder's extradata or a keyframe).

Raises `ValueError` as [`build_avcc`](#build_avcc) does.

#### H264Config.to_avcc

```python
def to_avcc(self, data: bytes) -> bytes
```

Re-frame an Annex B access unit for this configuration, dropping in-band SPS/PPS and AUDs.

### Interrupt

```python
class Interrupt
```

One interrupt: drop the named audio track's audio in a timeline range, in microseconds.

<small>[Source](https://github.com/caprids/relay-sdks/blob/main/python/packages/moq-media/src/moq_media/interrupt.py#L18)</small>

**Attributes**

- **`track`**: `str` — The audio track in the same namespace whose audio to drop.
- **`until_us`**: `int` — Drop before this timestamp: the end of the last audio the talker sent.

  Frames that arrive later inside the range are dropped too, so the talker's next audio must start at or after it.

- **`from_us`**: `int | None` — Drop from this timestamp on; `None` drops from the oldest audio held.

### MediaTimeline

```python
class MediaTimeline
```

The retained entries of one broadcast's media timeline, and the independent document they make.

`max_entries` bounds the document: past it, the oldest entry is dropped. The default 512 is about 18 KB, 17
minutes of two-second GOPs. A supervised publisher's next generation is a new broadcast and starts a new timeline.

<small>[Source](https://github.com/caprids/relay-sdks/blob/main/python/packages/moq-media/src/moq_media/timeline.py#L71)</small>

**Attributes**

- **`entries`**: `tuple[MediaTimelineEntry, ...]` *(property)* — The retained entries, oldest first.
- **`document`**: `bytes` *(property)* — The independent document of the retained entries; `[]` before the first.

#### MediaTimeline.add

```python
def add(
    self,
    timestamp_us: int,
    group: int,
    *,
    object: int = 0,
    wallclock_us: int | None = None,
) -> bytes
```

Record the object at `group` and `object` whose LOC timestamp is `timestamp_us`; return the document.

Call it once per group of the indexed track, for its first object, when that object was sent. `wallclock_us`
is as for [`media_timeline_entry`](#media_timeline_entry): the timestamp itself by default. A rejected argument raises
`ValueError` and leaves the timeline unchanged.

### MediaTimelineEntry

```python
class MediaTimelineEntry
```

One media timeline record, `[media_time_ms, [group, object], wallclock_ms]` on the wire (msf-01 §7.1.1).

<small>[Source](https://github.com/caprids/relay-sdks/blob/main/python/packages/moq-media/src/moq_media/timeline.py#L18)</small>

**Attributes**

- **`media_time_ms`**: `int` — Media time of the first sample in the referenced object, floored to whole milliseconds.
- **`group`**: `int`
- **`object`**: `int` — The referenced object's location in the track the timeline indexes.
- **`wallclock_ms`**: `int` — Milliseconds since the Unix epoch when that media was captured; 0 when unknown.

### MediaTimelineTrackInfo

```python
class MediaTimelineTrackInfo
```

The Media Timeline track (msf-01 §7) [`build_msf_catalog`](#build_msf_catalog) announces.

The entry is listed only while a track it depends on is listed; see `spec/media-layer.md`.

<small>[Source](https://github.com/caprids/relay-sdks/blob/main/python/packages/moq-media/src/moq_media/catalog.py#L78)</small>

**Attributes**

- **`name`**: `str`
- **`depends`**: `Sequence[str] | None` — The video and audio tracks the timeline applies to (msf-01 §7.2); its locations point into the first.

  Default: the first video track followed by the first audio track, whichever the catalog has.


### MsfCatalog

```python
class MsfCatalog
```

An independent MSF catalog (msf-01 §5.1), as `parse_msf_catalog` reads it.

<small>[Source](https://github.com/caprids/relay-sdks/blob/main/python/packages/moq-media/src/moq_media/catalog.py#L316)</small>

**Attributes**

- **`version`**: `str`
- **`tracks`**: `tuple[MsfCatalogTrack, ...]`
- **`generated_at_ms`**: `int | None`
- **`is_complete`**: `bool` — `True` only when the catalog declares the broadcast complete (§5.1.3).

### MsfCatalogTrack

```python
class MsfCatalogTrack
```

One track object of an independent MSF catalog (msf-01 §5.2), as `parse_msf_catalog` reads it.

<small>[Source](https://github.com/caprids/relay-sdks/blob/main/python/packages/moq-media/src/moq_media/catalog.py#L285)</small>

**Attributes**

- **`namespace`**: `tuple[str, ...]` — The track's own slash-joined `namespace` split into fields, else the catalog track's.
- **`name`**: `str`
- **`packaging`**: `str` — Payload encapsulation as declared (`loc`, or e.g. moq-rs `cmaf`/`legacy`); empty when absent.
- **`is_live`**: `bool` — `False` when absent.
- **`role`**: `str | None`
- **`codec`**: `str | None`
- **`width`**: `int | None`
- **`height`**: `int | None`
- **`framerate`**: `float | None`
- **`samplerate`**: `int | None`
- **`channel_config`**: `str | None`
- **`bitrate`**: `int | None`
- **`render_group`**: `int | None` — Render group (§5.2.11): tracks sharing one are meant to play together.
- **`alt_group`**: `int | None` — Alternate group (§5.2.12): tracks sharing one are alternate versions of one content.
- **`mimetype`**: `str | None` — MIME type (§5.2.19), e.g. `application/json` on a media timeline track.
- **`depends`**: `tuple[str, ...] | None` — Names of the tracks this one depends on (§5.2.14), its string items only.
- **`init_data`**: `bytes | None` — Decoder init data, resolved from `initRef` through the catalog's inline `initDataList`.

### MsfTrackGroup

```python
class MsfTrackGroup
```

A set of catalog tracks a viewer picks one of.

The members of one alternate group (§5.2.12) within one render group, or a single track that
names no alternate group.

<small>[Source](https://github.com/caprids/relay-sdks/blob/main/python/packages/moq-media/src/moq_media/catalog.py#L369)</small>

**Attributes**

- **`tracks`**: `tuple[MsfCatalogTrack, ...]` — The members, in catalog order.
- **`render_group`**: `int | None`
- **`alt_group`**: `int | None` — `None` for a group holding exactly one track that names no alternate group.

### PlayoutClock

```python
class PlayoutClock
```

A player's clock for one broadcast's timeline. Audio is the master; video asks it when each frame is due.

`late_drop_us` (default 40 ms, EBU R37's audio-early limit) is how late a frame may be shown;
`max_video_delay_us` (default 1 s) is how far a frame may lead before the clock re-syncs;
`audio_timeout_us` (default 500 ms) is how long audio may stay silent before wall-clock pacing takes over.
`target_latency_us` is how far behind the position a frame may still be delivered for decoding, and how late a
frame must be before wall pacing re-anchors on it; by default twice the longest of the last four keyframe
intervals reported, or 2 s before any is.
`now_us` is the time source, `time.monotonic_ns() // 1000` by default.

<small>[Source](https://github.com/caprids/relay-sdks/blob/main/python/packages/moq-media/src/moq_media/clock.py#L211)</small>

**Attributes**

- **`mode`**: `PlayoutMode` *(property)* — `idle` before anything anchors it, `audio` while audio reports arrive, `wall` otherwise.
- **`late_drop_us`**: `int` *(property)* — How late a frame may be shown, in microseconds: the `late_drop_us` option.
- **`target_latency_us`**: `float` *(property)* — The target latency now: the option, or twice the longest of the last four keyframe intervals reported.
- **`resets`**: `int` *(property)* — How many times `reset()` has been called: a frame reader sees from it that the timeline changed.

#### PlayoutClock.report_audio

```python
def report_audio(self, timestamp_us: int, *, at_us: int | None = None) -> None
```

Report that the audio with this timeline timestamp is audible at local time `at_us` (default: now).

That is what was handed to the output, less the jitter-buffer depth, the output latency, and any extra path.

#### PlayoutClock.stop_audio

```python
def stop_audio(self) -> None
```

Audio is muted or ended: pace by wall clock from the current position.

#### PlayoutClock.due

```python
def due(self, timestamp_us: int) -> PlayoutDecision
```

Decide whether the frame with this timeline timestamp should wait, be shown now, or be dropped.

#### PlayoutClock.position_us

```python
def position_us(self) -> float | None
```

Return the timeline position now, or `None` while idle.

#### PlayoutClock.report_keyframe_interval

```python
def report_keyframe_interval(self, interval_us: float) -> None
```

Report the spacing between two consecutive keyframes on the timeline; a frame reader does this.

#### PlayoutClock.reset

```python
def reset(self) -> None
```

Forget every anchor, at a Restart, a Move, a resync, or a new catalog: the next frame anchors wall pacing.

The keyframe intervals reported so far are kept.

#### PlayoutClock.local_now_us

```python
def local_now_us(self) -> float
```

The clock's local time source now, in microseconds: what a frame reader measures its waits in.

### PlayoutDecision

```python
class PlayoutDecision
```

What to do with one video frame.

`offset_us` is the frame's timestamp minus the clock's position (positive: early). `wait_us` is how long to
hold the frame before asking again, and is 0 unless `action` is `"wait"`.

<small>[Source](https://github.com/caprids/relay-sdks/blob/main/python/packages/moq-media/src/moq_media/clock.py#L191)</small>

**Attributes**

- **`action`**: `PlayoutAction`
- **`offset_us`**: `int`
- **`wait_us`**: `int`

### SampleTimeline

```python
class SampleTimeline
```

Maps an audio sample counter that starts at zero onto the broadcast timeline.

<small>[Source](https://github.com/caprids/relay-sdks/blob/main/python/packages/moq-media/src/moq_media/clock.py#L52)</small>

**Attributes**

- **`sample_rate`**: `int`
- **`first_sample_us`**: `int`

#### SampleTimeline.timestamp_us

```python
def timestamp_us(self, sample_index: int) -> int
```

Return the broadcast time of the sample at this index: whole microseconds after the first sample.

### SequencedFrame

```python
class SequencedFrame
```

One frame in decode order.

<small>[Source](https://github.com/caprids/relay-sdks/blob/main/python/packages/moq-media/src/moq_media/sequencer.py#L31)</small>

**Attributes**

- **`group_id`**: `int`
- **`object_id`**: `int`
- **`timestamp_us`**: `int` — The LOC timestamp in microseconds on the broadcast timeline.
- **`key`**: `bool` — Whether the frame starts its group: a keyframe under keyframe grouping, every frame under frame grouping.
- **`payload`**: `bytes`
- **`properties`**: `list[KeyValuePair]`
- **`discontinuity`**: `FrameDiscontinuity | None` — Set on the first frame after a skip, a resync, or a restart.

### SequencerPoll

```python
class SequencerPoll
```

What `FrameSequencer.poll` returns.

<small>[Source](https://github.com/caprids/relay-sdks/blob/main/python/packages/moq-media/src/moq_media/sequencer.py#L47)</small>

**Attributes**

- **`frames`**: `list[SequencedFrame]` — The frames ready now, in decode order.
- **`wait_us`**: `int | None` — Poll again after this many microseconds, when a wait can end; 0 when more is ready now (past `max_frames`, or a resync that takes a poll of its own); `None` when only new objects can change it.

### SourceTimeline

```python
class SourceTimeline
```

Maps timestamps from a clock with its own origin and rate onto the broadcast timeline.

Calibrate it with measured pairs of source time and time on the broadcast clock's own source; without a
calibration, the first timestamp mapped is anchored at the clock's `now_us()`. `capture_latency_us` is
subtracted from every timestamp.

<small>[Source](https://github.com/caprids/relay-sdks/blob/main/python/packages/moq-media/src/moq_media/clock.py#L65)</small>

**Attributes**

- **`capture_latency_us`**: `int` *(property)* — The capture device's input delay in integer microseconds, subtracted from every timestamp.

  Set it again when the input device changes; it applies from the next timestamp and never to calibration.

- **`anchor_source_us`**: `float | None` *(property)* — The source time of the current anchor; `None` until the first calibration or timestamp.

#### SourceTimeline.calibrate

```python
def calibrate(self, source_us: int, *, at_us: int | None = None) -> bool
```

Record that source time `source_us` happened at `at_us` on the clock's own source (default: now).

The pair becomes the anchor when there is none, or when the current anchor predicts it more than the
tolerance off: the source drifted, or jumped. Returns whether the anchor changed.

#### SourceTimeline.timestamp_us

```python
def timestamp_us(self, source_us: int) -> int
```

Return the broadcast time of this source timestamp, rounded to whole microseconds, less the capture latency.

Without an anchor, the first call anchors this timestamp at the clock's `now_us()`.

### Transcript

```python
class Transcript
```

What a listener has read of one audio track's transcript: each segment's latest version, less what was cut.

`apply` each segment read for the track and `cut` on each interrupt for it; `lines_at` gives what to show as
the audio plays. A segment's final version is kept against later ones. Cuts are remembered, so a version that
arrives after one is clipped as if it had arrived before. A segment is known by its `seq` within one run of the
talker's track, so use a new `Transcript` when the talker starts over.

<small>[Source](https://github.com/caprids/relay-sdks/blob/main/python/packages/moq-media/src/moq_media/transcript.py#L90)</small>

**Attributes**

- **`lines`**: `list[TranscriptLine]` *(property)* — Every segment held, in timeline order.

#### Transcript.apply

```python
def apply(self, segment: TranscriptSegment) -> bool
```

Keep a segment's version, clipped by the cuts so far; `False` if it changed nothing.

Ignored: a version of a segment the talker already sent final, a segment that starts inside a range cut
earlier (as audio arriving there late is), and an interim version of a segment a cut ended. A cut segment
takes the talker's final version, clipped: one whose audio ended before the cut is no longer cut.

#### Transcript.cut

```python
def cut(
    self,
    *,
    until_us: int,
    from_us: int | None = None,
    position_us: float | None = None,
) -> None
```

Apply an interrupt's range, with the playout position now (`PlayoutClock.position_us`).

Audio before the position has been heard, so the cut starts at the later of `from_us` and the position.
It drops the segments that start in the cut, and ends the one playing there at its start, final and cut.
Raises `ValueError` on a range an interrupt could not have, or a negative or non-finite position.

#### Transcript.lines_at

```python
def lines_at(self, position_us: float | None) -> list[TranscriptLine]
```

The segments whose audio has started playing at this position, in timeline order; none while idle.

### TranscriptLine

```python
class TranscriptLine
```

A segment as a `Transcript` holds it: its latest version, and whether an interrupt cut it short.

<small>[Source](https://github.com/caprids/relay-sdks/blob/main/python/packages/moq-media/src/moq_media/transcript.py#L37)</small>

**Attributes**

- **`seq`**: `int`
- **`track`**: `str`
- **`from_us`**: `int`
- **`until_us`**: `int | None`
- **`text`**: `str`
- **`final`**: `bool`
- **`cut`**: `bool` — An interrupt cut the segment's audio short at `until_us`: the listener heard only the start of the text.

### TranscriptSegment

```python
class TranscriptSegment
```

One version of a transcript segment: the text of the named audio track's audio from `from_us` on.

<small>[Source](https://github.com/caprids/relay-sdks/blob/main/python/packages/moq-media/src/moq_media/transcript.py#L19)</small>

**Attributes**

- **`seq`**: `int` — The segment's number; a later version of the same segment repeats it.
- **`track`**: `str` — The audio track in the same namespace whose audio this is the text of.
- **`from_us`**: `int` — Where the segment's audio starts, in microseconds on the broadcast timeline.
- **`until_us`**: `int | None` — Where it ends; `None` while the talker does not know yet. A final segment has it.
- **`text`**: `str` — The text, which a later version replaces whole.
- **`final`**: `bool` — No later version follows.

### VideoTrackInfo

```python
class VideoTrackInfo
```

The video track [`build_msf_catalog`](#build_msf_catalog) describes.

An `avc1` or `avc3` track needs `init_data`: without it the track is left out of the
catalog, and with it the codec string's profile and level come from the avcC. Fields left as
`None` are omitted from the catalog.

<small>[Source](https://github.com/caprids/relay-sdks/blob/main/python/packages/moq-media/src/moq_media/catalog.py#L19)</small>

**Attributes**

- **`name`**: `str`
- **`codec`**: `str` — WebCodecs registry codec string, e.g. `avc1.42E01F`.
- **`width`**: `int | None`
- **`height`**: `int | None`
- **`framerate`**: `float | None`
- **`bitrate`**: `int | None`
- **`init_data`**: `bytes | None` — Decoder configuration (avcC for `avc1`), emitted inline as `initDataList`.
- **`render_group`**: `int` — Render group (msf-01 §5.2.11): tracks sharing one play together on one timeline.

  Give a second source, such as a screen share and its audio, its own.

- **`alt_group`**: `int | None` — Alternate group (msf-01 §5.2.12): alternate versions of the same content, such as simulcast layers.

  A viewer subscribes to one member. Members share a role and a render group.


## Constants

### AUDIO_ONLY_TARGET_LATENCY_US

```python
AUDIO_ONLY_TARGET_LATENCY_US = 500000
```

The target latency for a broadcast with no video: pass it as `target_latency_us`, since the 2 s default waits for
keyframe intervals an audio-only track never reports. It covers a player's 400 ms jitter-buffer ceiling plus a wired
output delay, and a frame sequencer resyncs after half a second of nothing on time instead of two.

<small>[Source](https://github.com/caprids/relay-sdks/blob/main/python/packages/moq-media/src/moq_media/clock.py#L23)</small>

### AUDIO_TRACK

```python
AUDIO_TRACK = 'audio'
```

<small>[Source](https://github.com/caprids/relay-sdks/blob/main/python/packages/moq-media/src/moq_media/loc.py#L15)</small>

### CATALOG_TRACK

```python
CATALOG_TRACK = 'catalog'
```

<small>[Source](https://github.com/caprids/relay-sdks/blob/main/python/packages/moq-media/src/moq_media/loc.py#L13)</small>

### INTERRUPT_TRACK

```python
INTERRUPT_TRACK = 'interrupt'
```

The interrupt track's default name, listed in the catalog as a data track.

<small>[Source](https://github.com/caprids/relay-sdks/blob/main/python/packages/moq-media/src/moq_media/interrupt.py#L12)</small>

### LOC_AUDIO_CONFIG

```python
LOC_AUDIO_CONFIG = 15
```

<small>[Source](https://github.com/caprids/relay-sdks/blob/main/python/packages/moq-media/src/moq_media/loc.py#L10)</small>

### LOC_DOCUMENT

```python
LOC_DOCUMENT = 'draft-ietf-moq-loc-04'
```

<small>[Source](https://github.com/caprids/relay-sdks/blob/main/python/packages/moq-media/src/moq_media/loc.py#L18)</small>

### LOC_TIMESCALE

```python
LOC_TIMESCALE = 8
```

<small>[Source](https://github.com/caprids/relay-sdks/blob/main/python/packages/moq-media/src/moq_media/loc.py#L8)</small>

### LOC_TIMESTAMP

```python
LOC_TIMESTAMP = 16
```

<small>[Source](https://github.com/caprids/relay-sdks/blob/main/python/packages/moq-media/src/moq_media/loc.py#L7)</small>

### LOC_VIDEO_CONFIG

```python
LOC_VIDEO_CONFIG = 13
```

<small>[Source](https://github.com/caprids/relay-sdks/blob/main/python/packages/moq-media/src/moq_media/loc.py#L9)</small>

### MEDIA_TIMELINE_TRACK

```python
MEDIA_TIMELINE_TRACK = 'timeline'
```

<small>[Source](https://github.com/caprids/relay-sdks/blob/main/python/packages/moq-media/src/moq_media/loc.py#L16)</small>

### MICROSECONDS_PER_SECOND

```python
MICROSECONDS_PER_SECOND = 1000000
```

<small>[Source](https://github.com/caprids/relay-sdks/blob/main/python/packages/moq-media/src/moq_media/loc.py#L11)</small>

### MSF_CATALOG_VERSION

```python
MSF_CATALOG_VERSION = 'draft-01'
```

<small>[Source](https://github.com/caprids/relay-sdks/blob/main/python/packages/moq-media/src/moq_media/catalog.py#L16)</small>

### MSF_DOCUMENT

```python
MSF_DOCUMENT = 'draft-ietf-moq-msf-01'
```

<small>[Source](https://github.com/caprids/relay-sdks/blob/main/python/packages/moq-media/src/moq_media/loc.py#L19)</small>

### OPUS_FRAME_US

```python
OPUS_FRAME_US = 20000
```

The frame length [`moq_media.opus.OpusEncoder`](../moq_media-opus/#opusencoder) emits, and the LOC mapping's one frame per group.

<small>[Source](https://github.com/caprids/relay-sdks/blob/main/python/packages/moq-media/src/moq_media/audio.py#L6)</small>

### OPUS_SAMPLE_RATE

```python
OPUS_SAMPLE_RATE = 48000
```

The rate every Opus stream decodes at (RFC 7845 §5.1), whatever rate the encoder was fed.

<small>[Source](https://github.com/caprids/relay-sdks/blob/main/python/packages/moq-media/src/moq_media/audio.py#L3)</small>

### TRACK_TIMESCALE_PROPERTIES

```python
TRACK_TIMESCALE_PROPERTIES = tuple(loc_track_properties())
```

Track properties declared on every media track: timestamps are microseconds.

<small>[Source](https://github.com/caprids/relay-sdks/blob/main/python/packages/moq-media/src/moq_media/loc.py#L36)</small>

### TRANSCRIPT_TRACK

```python
TRANSCRIPT_TRACK = 'transcript'
```

The transcript track's default name, listed in the catalog as a data track.

<small>[Source](https://github.com/caprids/relay-sdks/blob/main/python/packages/moq-media/src/moq_media/transcript.py#L13)</small>

### VIDEO_TRACK

```python
VIDEO_TRACK = 'video'
```

<small>[Source](https://github.com/caprids/relay-sdks/blob/main/python/packages/moq-media/src/moq_media/loc.py#L14)</small>

### FrameDiscontinuity

```python
FrameDiscontinuity = Literal['gap', 'resync', 'restart']
```

Why a frame is not the one decoded after the frame before it: `gap` (frames before it were skipped: missing,
reset, or too late), `resync` (nothing arrived within the target latency for one target latency; the sequencer reset
the clock), `restart` (the subscription was replaced: a want's Restart or Move). A decoder resets there.

<small>[Source](https://github.com/caprids/relay-sdks/blob/main/python/packages/moq-media/src/moq_media/sequencer.py#L18)</small>

### PlayoutAction

```python
PlayoutAction = Literal['wait', 'show', 'drop']
```

What a video renderer does with one frame.

<small>[Source](https://github.com/caprids/relay-sdks/blob/main/python/packages/moq-media/src/moq_media/clock.py#L16)</small>

### PlayoutMode

```python
PlayoutMode = Literal['idle', 'audio', 'wall']
```

What a playout clock is paced by.

<small>[Source](https://github.com/caprids/relay-sdks/blob/main/python/packages/moq-media/src/moq_media/clock.py#L18)</small>
