Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
157e541
quest: claim m1/catalog-enabled
kixelated Oct 6, 2026
f8d4cc4
feat(hang)!: one enabled flag replaces stalled
kixelated Oct 6, 2026
b289566
Merge origin/main into quest/m1/catalog-enabled
kixelated Oct 6, 2026
b9dec43
fix(watch): show black when no video rendition is selectable
kixelated Oct 6, 2026
20aa72b
test(interop): disable and re-enable the fixture's video rendition
kixelated Oct 6, 2026
5dfbb50
Merge origin/main into quest/m1/catalog-enabled
kixelated Oct 6, 2026
107fde0
fix(transcode): prefer an enabled source rendition
kixelated Oct 6, 2026
cbbd5f7
fix(hang): rank enabled video renditions first
kixelated Oct 6, 2026
cfa4b9e
fix(publish): keep a re-enabled rendition disabled until it resolves
kixelated Oct 6, 2026
dda7e5b
docs(hang): define enabled with the common rendition fields
kixelated Oct 6, 2026
8d01683
Merge origin/main into quest/m1/catalog-enabled
kixelated Oct 6, 2026
92d377e
docs(hang): note text renditions skip enabled; a capture that never r…
kixelated Oct 6, 2026
301b831
Merge remote-tracking branch 'origin/main' into HEAD
kixelated Oct 7, 2026
a5a7c21
quest: remove completed catalog-enabled prerequisites
kixelated Oct 7, 2026
d2e2dad
fix(watch): release the audio graph when its broadcast is disabled
kixelated Oct 7, 2026
17ddfd1
Merge remote-tracking branch 'origin/main' into codex/merge-4915
kixelated Oct 7, 2026
ccfa825
Preserve early-demand regression after catalog completion
kixelated Oct 7, 2026
d03d028
Merge branch 'main-latest' into m4915
kixelated Oct 7, 2026
7a2192c
Merge remote-tracking branch 'origin/main' into mq4915
kixelated Oct 8, 2026
e181f86
Merge remote-tracking branch 'origin/main' into mq4915
kixelated Oct 8, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions dart/moq/lib/src/aliases.dart
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ typedef Frame = MoqFrame;
/// A media [Frame] whose keyframe flag marks group starts or video keyframes; audio flags only group starts.
typedef MediaFrame = MoqMediaFrame;

/// The catalog description of a video track, including whether the publisher recommends temporarily avoiding it.
/// The catalog description of a video track, including whether it is enabled (a disabled one has no frames coming).
typedef Video = MoqVideo;

/// Caller-provided catalog fields for a video track.
Expand All @@ -164,7 +164,7 @@ typedef VideoProperties = MoqVideoProperties;
/// A single video codec an importer can parse.
typedef VideoFormat = MoqVideoFormat;

/// The catalog description of an audio track: codec, sample rate, channels, and container.
/// The catalog description of an audio track: codec, sample rate, channels, whether it is enabled, and container.
typedef Audio = MoqAudio;

/// An audio codec, its required init bytes, and an optional label.
Expand Down
32 changes: 23 additions & 9 deletions dart/moq_ffi/lib/src/moq.dart
Original file line number Diff line number Diff line change
Expand Up @@ -412,6 +412,7 @@ class MoqAudio {
final int sampleRate;
final int channelCount;
final int? bitrate;
final bool enabled;
final MoqContainer container;
MoqAudio({
this.label = null,
Expand All @@ -421,6 +422,7 @@ class MoqAudio {
required this.sampleRate,
required this.channelCount,
this.bitrate,
this.enabled = true,
required this.container,
});
}
Expand Down Expand Up @@ -467,6 +469,11 @@ class FfiConverterMoqAudio {
);
final bitrate = bitrate_lifted.value;
new_offset += bitrate_lifted.bytesRead;
final enabled_lifted = FfiConverterBool.read(
Uint8List.view(buf.buffer, new_offset),
);
final enabled = enabled_lifted.value;
new_offset += enabled_lifted.bytesRead;
final container_lifted = FfiConverterMoqContainer.read(
Uint8List.view(buf.buffer, new_offset),
);
Expand All @@ -481,6 +488,7 @@ class FfiConverterMoqAudio {
sampleRate: sampleRate,
channelCount: channelCount,
bitrate: bitrate,
enabled: enabled,
container: container,
),
new_offset - buf.offsetInBytes,
Expand All @@ -496,6 +504,7 @@ class FfiConverterMoqAudio {
FfiConverterUInt32.allocationSize(value.sampleRate) +
FfiConverterUInt32.allocationSize(value.channelCount) +
FfiConverterOptionalUInt64.allocationSize(value.bitrate) +
FfiConverterBool.allocationSize(value.enabled) +
FfiConverterMoqContainer.allocationSize(value.container) +
0;
final buf = Uint8List(total_length);
Expand Down Expand Up @@ -533,6 +542,10 @@ class FfiConverterMoqAudio {
value.bitrate,
Uint8List.view(buf.buffer, new_offset),
);
new_offset += FfiConverterBool.write(
value.enabled,
Uint8List.view(buf.buffer, new_offset),
);
new_offset += FfiConverterMoqContainer.write(
value.container,
Uint8List.view(buf.buffer, new_offset),
Expand All @@ -548,6 +561,7 @@ class FfiConverterMoqAudio {
FfiConverterUInt32.allocationSize(value.sampleRate) +
FfiConverterUInt32.allocationSize(value.channelCount) +
FfiConverterOptionalUInt64.allocationSize(value.bitrate) +
FfiConverterBool.allocationSize(value.enabled) +
FfiConverterMoqContainer.allocationSize(value.container) +
0;
}
Expand Down Expand Up @@ -1105,7 +1119,7 @@ class MoqVideo {
final MoqDimensions? coded;
final MoqDimensions? displayAspect;
final int? bitrate;
final bool stalled;
final bool enabled;
final double? framerate;
final MoqContainer container;
MoqVideo({
Expand All @@ -1116,7 +1130,7 @@ class MoqVideo {
this.coded,
this.displayAspect,
this.bitrate,
this.stalled = false,
this.enabled = true,
this.framerate,
required this.container,
});
Expand Down Expand Up @@ -1164,11 +1178,11 @@ class FfiConverterMoqVideo {
);
final bitrate = bitrate_lifted.value;
new_offset += bitrate_lifted.bytesRead;
final stalled_lifted = FfiConverterBool.read(
final enabled_lifted = FfiConverterBool.read(
Uint8List.view(buf.buffer, new_offset),
);
final stalled = stalled_lifted.value;
new_offset += stalled_lifted.bytesRead;
final enabled = enabled_lifted.value;
new_offset += enabled_lifted.bytesRead;
final framerate_lifted = FfiConverterOptionalDouble64.read(
Uint8List.view(buf.buffer, new_offset),
);
Expand All @@ -1188,7 +1202,7 @@ class FfiConverterMoqVideo {
coded: coded,
displayAspect: displayAspect,
bitrate: bitrate,
stalled: stalled,
enabled: enabled,
framerate: framerate,
container: container,
),
Expand All @@ -1205,7 +1219,7 @@ class FfiConverterMoqVideo {
FfiConverterOptionalMoqDimensions.allocationSize(value.coded) +
FfiConverterOptionalMoqDimensions.allocationSize(value.displayAspect) +
FfiConverterOptionalUInt64.allocationSize(value.bitrate) +
FfiConverterBool.allocationSize(value.stalled) +
FfiConverterBool.allocationSize(value.enabled) +
FfiConverterOptionalDouble64.allocationSize(value.framerate) +
FfiConverterMoqContainer.allocationSize(value.container) +
0;
Expand Down Expand Up @@ -1245,7 +1259,7 @@ class FfiConverterMoqVideo {
Uint8List.view(buf.buffer, new_offset),
);
new_offset += FfiConverterBool.write(
value.stalled,
value.enabled,
Uint8List.view(buf.buffer, new_offset),
);
new_offset += FfiConverterOptionalDouble64.write(
Expand All @@ -1267,7 +1281,7 @@ class FfiConverterMoqVideo {
FfiConverterOptionalMoqDimensions.allocationSize(value.coded) +
FfiConverterOptionalMoqDimensions.allocationSize(value.displayAspect) +
FfiConverterOptionalUInt64.allocationSize(value.bitrate) +
FfiConverterBool.allocationSize(value.stalled) +
FfiConverterBool.allocationSize(value.enabled) +
FfiConverterOptionalDouble64.allocationSize(value.framerate) +
FfiConverterMoqContainer.allocationSize(value.container) +
0;
Expand Down
2 changes: 1 addition & 1 deletion doc/concept/hang.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ A few things the catalog can express beyond decoder config:
- **Renditions in another broadcast.** A rendition may point at a relative broadcast path, so a transcoder can publish a ladder that adds low rungs and references the source's original rendition without re-publishing its bytes. The path resolves against where the consumer found the catalog, so a reference that escapes above the root names nothing and the catalog is rejected.
- **Jitter.** A rendition can say how far its frames fell behind the media clock before the publisher flushed them, in whole milliseconds rounded up. Encoders report the spread of lateness above each rendition's own recent minimum, so a constant encoder delay is not jitter; container imports estimate batch spans without counting ingest delay. It describes the publisher, never the network, only grows over the life of a stream, and a player sizes its buffer to at least this much. A `0` is read as absent.
- **Delay.** A rendition can also say how far its frames reach the transport behind the broadcast's earliest rendition, measured the same way from each rendition's minimum lateness, so a video encoder running 200 ms behind audio advertises `delay: 200` on video. It follows the same rules as jitter. A player holds the largest `delay + jitter` among the renditions it subscribes to, and never subtracts one rendition's `delay` from another's.
- **Stalled renditions.** A publisher can flag a rendition as temporarily bad so players prefer another one without the track disappearing. First-party video publishers set this flag after more than three frame intervals of source silence or encoding lag while subscribed, and clear it after three on-time completed frames or when idle. Browser and native capture poll while waiting; FLV and MPEG-TS importers observe video silence as container data arrives. The shared detector is `hang::catalog::stalled::Detector` in Rust and `Catalog.Stalled.Detector` in JavaScript. It is a playback diagnostic, not an authorization or routing signal.
- **Disabled renditions.** An audio or video rendition with `enabled: false` has no frames coming, and a player must not select it. Pausing a rendition in `@moq/publish` keeps it in the catalog this way rather than removing it, so a mute is a one-field update; a disabled video rendition encodes one black keyframe first, so a player that predates the field shows black instead of a frozen picture. The field is written only when false. The older video `stalled` field is no longer written, and readers ignore it.
- **Archive.** A broadcast may advertise an `archive` entry mapping each track, the catalog included, to its own timeline track (a small index of the track's spans: timing plus group and frame positions) and, if recorded, the replay MoQ path, object-store URL, and format version. Every track is cut by one rule, at group boundaries between 2s and 10s (a zero minimum for sparse data such as the catalog), and tracks commit and expire independently. The timelines are what let the [HLS gateway](/bin/hls) build playlists without subscribing to media.
- **Clock.** The optional root `clock` maps PTS zero to wall time so every media track and the archive index share one fixed epoch after timescale conversion. It is independent of `archive`, so a live-only publisher can expose wall-clock timing without creating a timeline.
- **Extensions.** The root is a loose object. Applications add their own sections (`scte35`, for example) next to the ones hang defines, optionally naming a track that carries the data. Every library exposes a way to write your section without clobbering the built-in ones, and readers ignore what they don't know.
Expand Down
1 change: 0 additions & 1 deletion doc/concept/standard.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,6 @@ The MoQ Streaming Format is a catalog, playing the role HLS playlists and SDP
do elsewhere. It overlaps with the [hang catalog](/concept/hang) and the two
will likely converge. The tools track draft-01 and hide the version on the
wire, so draft-00 catalogs still decode and init data always arrives inline.
The `stalled` rendition hint is shared between the two formats.

## LOC

Expand Down
2 changes: 1 addition & 1 deletion doc/lib/c/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ pkg-config file included.
- **Server.** `moq_server_listen` binds before it returns (a bad address or certificate fails there) and hands each incoming session to `on_request` as a request handle. Read `moq_session_request_path` and `_query` to route and authenticate, then `moq_session_request_accept` (a session handle, with origins like `moq_session_connect`) or `moq_session_request_reject` with an HTTP-style code (401 and 403 become the protocol's unauthorized close). An accepted session reports `1` once SETUP completes and never reconnects. `moq_server_addr` reports an ephemeral port and `moq_server_fingerprints` the hashes a client pins for a `tls_generate` certificate. `moq_server_close` stops listening; its terminal callback fires once the sockets are released.
- **Demand.** A watcher on a published track (`moq_publish_track_demand`, `moq_publish_media_demand`, `moq_encode_video_demand`, `moq_encode_audio_demand`) calls `on_demand` with `MOQ_DEMAND_USED` or `MOQ_DEMAND_UNUSED` right away and again on every change, so an encoder on a battery-powered device runs only while someone is watching. The first call is the current state, so a track that went unused before the watcher existed still reports it. `moq_publish_demand_cancel` stops it; the terminal callback still fires. A container has no single demand and is refused. Demand follows the last real subscriber: an origin that served the track drops its source copy on the unused edge, so a cache linger never delays it. Only a relay keeps what it already delivered warm for 30 seconds, and a returning subscriber is served that cache only once the publisher confirms it is still current.
- **Requests.** `moq_publish_dynamic` serves subscriptions to tracks the broadcast never declared: each arrives as a request handle, read its name with `moq_track_request_name`, then `moq_track_request_accept` (a raw track handle), `moq_track_request_video` / `_audio` (the media handle `moq_publish_video` / `_audio` return), or `moq_track_request_abort` with an application code the subscriber sees. Without a live handler an unknown name is refused. `moq_publish_track_dynamic` does the same for fetches of groups a track no longer has cached, delivered as `moq_group_request_*` (`sequence`, `priority`, `frame_start`); `moq_group_request_accept` starts the producer at `frame_start` so written frames keep their group indices. Register it with `moq_track_request_dynamic` before accepting a track that was itself requested by a fetch, so that pending group survives the transition. Both handlers stop with `moq_publish_dynamic_cancel`.
- **Everything the bindings can do** ([list](/lib/#what-every-binding-can-do)): media publish and consume with the catalog managed for you, raw pixels and PCM with the codec inside (`moq_encode_video`, `moq_encode_audio`, and the `moq_decode_*` mirrors), raw tracks with timestamps and datagrams (one read from an untimed track has `timestamp_present` false), JSON and binary data tracks (snapshot or stream, each advertised in the catalog for as long as it lives), group fetch, catalog sections, shared video properties, and stalled hints. The three advertising operations are `moq_origin_create_broadcast` (unannounced producer, invisible to everyone), `moq_publish_announce` / `moq_publish_unannounce` (exact-path advertisement), `moq_publish_close` (ends the broadcast for good and releases its handle), and `moq_origin_dynamic` (a claim over a path prefix and everything beneath it; `""` for everything). A route is a capability, not an inventory. `moq_origin_announced` takes a `moq_announce_config` (NULL or zeroed for everything): a literal prefix and an optional relative pattern filter; each `moq_announce_event` has a `kind` (`MOQ_ANNOUNCE_KIND_START`, `_UPDATE`, or `_END`); `prefix` stays relative to the origin, while `captures` reports what each wildcard matched when `has_captures` is true. Paths with a `.`-prefixed segment below the prefix are [hidden](/concept/moq-lite#hidden-broadcasts); set `hidden` or name the dot segment in `prefix` to list them.
- **Everything the bindings can do** ([list](/lib/#what-every-binding-can-do)): media publish and consume with the catalog managed for you, raw pixels and PCM with the codec inside (`moq_encode_video`, `moq_encode_audio`, and the `moq_decode_*` mirrors), raw tracks with timestamps and datagrams (one read from an untimed track has `timestamp_present` false), JSON and binary data tracks (snapshot or stream, each advertised in the catalog for as long as it lives), group fetch, catalog sections, shared video properties, and whether a video rendition is enabled (`moq_consume_video_enabled`). The three advertising operations are `moq_origin_create_broadcast` (unannounced producer, invisible to everyone), `moq_publish_announce` / `moq_publish_unannounce` (exact-path advertisement), `moq_publish_close` (ends the broadcast for good and releases its handle), and `moq_origin_dynamic` (a claim over a path prefix and everything beneath it; `""` for everything). A route is a capability, not an inventory. `moq_origin_announced` takes a `moq_announce_config` (NULL or zeroed for everything): a literal prefix and an optional relative pattern filter; each `moq_announce_event` has a `kind` (`MOQ_ANNOUNCE_KIND_START`, `_UPDATE`, or `_END`); `prefix` stays relative to the origin, while `captures` reports what each wildcard matched when `has_captures` is true. Paths with a `.`-prefixed segment below the prefix are [hidden](/concept/moq-lite#hidden-broadcasts); set `hidden` or name the dot segment in `prefix` to list them.

```c
moq_client_config config;
Expand Down
2 changes: 1 addition & 1 deletion doc/lib/rs/hang.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ The [hang media format](/concept/hang) on top of `moq-net`: a live catalog
describing renditions with WebCodecs-style decoder configs, and containers
that carry a timestamp with every frame.

- **Catalog producer and consumer.** Typed `VideoConfig`, `AudioConfig` and text renditions, the `json` and `binary` data track sections, shared video properties (display size, rotation, flip), stalled hints, cross-broadcast rendition references, and the `archive` entry (each track's timeline track, optional replay path, store URL, and format version). Extend it with your own sections through a lock or `#[serde(flatten)]`, or read unknown sections as raw JSON.
- **Catalog producer and consumer.** Typed `VideoConfig`, `AudioConfig` and text renditions, the `json` and `binary` data track sections, shared video properties (display size, rotation, flip), per-rendition `enabled` flags, cross-broadcast rendition references, and the `archive` entry (each track's timeline track, optional replay path, store URL, and format version). Extend it with your own sections through a lock or `#[serde(flatten)]`, or read unknown sections as raw JSON.
- **Containers.** `legacy`, `cmaf`, and `loc`, decoded for you; unknown kinds pass through untouched.
- **`Ordered`** reads a media track as timestamped frames, reorders groups, and skips ones that fall past your max delay.
- **Codecs described**: H.264, H.265, VP8, VP9, AV1, AAC, Opus, PCM.
Expand Down
Loading
Loading