Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,11 @@ grant; `Options::bandwidth` documents that
`opus::bitrate_floor(codec_rate, frame_size).max(500)` to
`300_000 * channels` (`backend/libopus.rs`, `rs/moq-audio/src/opus.rs`). `Policy::min` defaults to a tenth of
the ceiling (`rs/moq-mux/src/rate.rs`); for Opus it is the codec floor. A
grant below it disables the rendition until the grant recovers (decided
2026-10-04 with the enabled flag); the encoder never errors. The reservation's ceiling is
grant below it disables the rendition until bandwidth recovers (decided
2026-10-04 with the enabled flag); the encoder never errors. Once viewers
deselect it, its demand and so its grant go away, so recovery evaluates a
hypothetical share against the current estimate, as the ladder controller
does. The reservation's ceiling is
the configured bitrate; only the policy target moves.
- PCM: `pcm::bitrate(sample_rate, channels)` is `pub(crate)`
(`rs/moq-audio/src/pcm.rs`), `Settings::bitrate` is refused for it
Expand All @@ -58,8 +61,8 @@ grant; `Options::bandwidth` documents that
Tests: retain the shared `Control` tests; an Opus Producer whose
grant drops below its configured bitrate reports the lower `bitrate()` after
one policy step, holds it on a `None` grant, and ramps back when the grant
returns; a grant below the Opus floor disables the rendition and a recovered grant
enables it; a PCM Producer
returns; a grant below the Opus floor disables the rendition, and recovered
bandwidth enables it again even after the last viewer unsubscribed; a PCM Producer
ignores every grant.

## Closes
Expand Down
1 change: 1 addition & 0 deletions quest/m1/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ QUIC studies there on that rule.
- [Demand everywhere](/quest/m1/demand-everywhere.md) - requests and producers at every level watch subscribers through `demand()` alone, ahead of the bindings exposing it
- [Request linger](/quest/m1/request-linger.md) - an upstream FETCH or SUBSCRIBE outlives its last reader by a short linger, so quick re-requests don't churn upstream
- [Merge queue](/quest/m1/merge-queue.md) - the required checks run on `merge_group`, so a stale green check can no longer break main
- [Flat questlines](/quest/m1/quest-flat-lines.md) - moq pins the current quest CLI, lands its questline branches on main, and retires them
- [Resampler tail](/quest/m1/resampler-tail.md) - `moq play` plays a finite track to its last sample when its rate differs from the device's
- [Binding audio delay](/quest/m1/binding-surface.md) - moq-ffi and every wrapper configure and observe audio playout delay
- [FFI shape](/quest/m1/ffi-shape/README.md) - the bindings mirror Rust's layers: net at the root, then media, json, flate, audio, and video namespaces built from the handle below
Expand Down
5 changes: 3 additions & 2 deletions quest/m1/auth/ws-unauthorized.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,9 @@ upgrades, reads SETUP, and then admits; the relay is the outlier.
Decided (2026-10-04): the relay hands the upgraded socket to moq-tokio's
`Request`, so `Connection::run` serves WebSocket as it serves QUIC, and the
relay's duplicate admit and supervise loop is deleted. Every refusal becomes
an in-band close, mapped as the io_uring path does (403 to `Unauthorized`,
502 to `App(502)`). It admits after SETUP, which
an in-band close, mapped as the io_uring path does: 401 and 403 to
`Unauthorized`, 502 to `App(502)`. An expired token keeps its own
`Error::Expired` path ([expired error](/quest/m1/auth/expired-error.md)). It admits after SETUP, which
[token in band](/quest/m1/auth/token-in-band.md) needs anyway. It needs a new
public moq-tokio constructor for an already-upgraded socket; keep it minimal.

Expand Down
14 changes: 9 additions & 5 deletions quest/m1/catalog-enabled.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@ instead of a removal.
Decided (2026-10-04):

- Wire: an optional `enabled` boolean on every rendition, audio and video,
written only when false. Writers stop writing `stalled`; readers treat a
legacy `stalled: true` as `enabled: false`. Update
written only when false. Writers stop writing `stalled`, and readers ignore
it: released publishers still flap it, and following it would keep #4772
alive for them. Update
`drafts/draft-lcurley-moq-hang.md` (replacing the `stalled` section, with a
changelog entry, since `stalled` shipped in -03), `doc/concept/hang.md`, and
run `just drafts check`. MSF stops mirroring `stalled` and does not mirror
Expand All @@ -37,7 +38,10 @@ Decided (2026-10-04):
graph ([audio graph lifetime](/quest/m1/watch-audio-graph.md)).
- Bandwidth: a rendition is enabled only once its reservation is granted, and
disabled, with encoding stopped, when the grant falls below its floor,
until the grant recovers. The [ladder](/quest/m2/ladder/README.md) and
until the grant recovers. A disabled rendition loses its subscribers, and
the allocator grants nothing to an undemanded track, so recovery evaluates
a hypothetical share against the current estimate instead of waiting for a
grant. The [ladder](/quest/m2/ladder/README.md) and
[audio grant following](/quest/m1/2848-follow-the-bandwidth-grant-in-moq-audio-instead-of.md)
adopt that rule; this quest only defines the field.
- Everything else that names `stalled` moves to `enabled`: moq-transcode's
Expand All @@ -48,8 +52,8 @@ Decided (2026-10-04):

Tests: disabling an audio rendition publishes a catalog with the same
rendition and `enabled: false`, and a viewer deselects it and keeps one
AudioContext across disable and enable; a legacy `stalled: true` reads as
disabled; nothing in the tree writes `stalled`.
AudioContext across disable and enable; a legacy `stalled: true` changes
nothing; nothing in the tree writes `stalled`.

## Required

Expand Down
17 changes: 11 additions & 6 deletions quest/m1/fmp4-export-tracks.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,14 @@ that is not in the moov, and ffprobe rejects the file.

Decided (2026-10-04):

- Init from the catalog: write avc3 and hev1 sample entries from the
catalog's profile and level, with SPS and PPS in-band, so the init is ready
at the first catalog and the wait disappears for H.264 and H.265. Apple
prefers hvc1 for HEVC and some editors handle avc3 poorly; that is the
accepted trade.
- Init from the catalog: write avc3 and hev1 sample entries with SPS and PPS
in-band, so the init is ready at the first catalog and the wait disappears
for H.264 and H.265. Only when the catalog carries everything the entry's
configuration record needs: hvcC (and avcC for high profiles) also holds
chroma format and bit depths, which `build_hvcc` reads from the SPS and the
codec string does not carry. Otherwise the track waits for its SPS in the
bounded queue below. Apple prefers hvc1 for HEVC and some editors handle
avc3 poorly; that is the accepted trade.
- For a codec whose entry still needs the bitstream, drain every ready track
into a per-track fragment queue before the init, keeping fragment
boundaries, and flush after the moov. The queue is bounded; exceeding it
Expand All @@ -41,7 +44,9 @@ Decided (2026-10-04):
Tests: an H.264 Annex-B export whose audio starts 2 s before the first
keyframe keeps that audio; a rendition removed and re-added after the init
writes under its original id and the output parses; a new name, an
incompatible config, and a backwards replay each fail the export.
incompatible config, and a backwards replay each fail the export; a 10-bit
HEVC source whose catalog lacks bit depth waits for its SPS and is described
correctly.

## Closes

Expand Down
37 changes: 37 additions & 0 deletions quest/m1/quest-flat-lines.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# [M] moq adopts flat questlines from the current quest CLI

## Goal

moq pins a quest release where questlines are planning groups with no branch
of their own: every quest, including a questline README's remaining work, is
its own PR against `main`. The child work already merged into questline
branches reaches `main`, and those branches retire.

## Plan

kixelated/quest#44 dropped questline branches, `quest branch`, and
`quest ready --remote`; kixelated/quest#51 changed the import skill to treat a
report's fix as a claim and plan each issue fully. moq still pins the quest
input in `flake.nix` from before both.

Decided (2026-10-04): a quest of its own, not a routine pin bump, because 16
questline branches carry merged child work that is not on `main`
(`git ls-remote origin 'refs/heads/quest/*README'`).

- Bump the `quest` input and run `quest check` with the new CLI; fix what it
reports.
- For each questline branch, land its accumulated work on `main` through its
existing umbrella PR, or a new one, then delete the branch. A branch whose
work cannot land yet is decided with the maintainer, not left orphaned.
- Add the `quest-takeover` skill stub beside the other `.claude/skills/quest-*`
stubs, and decide with the maintainer whether it replaces the existing
`takeover` skill.
- Update `AGENTS.md` and `CONTRIBUTING.md` only where they describe questline
branches, with the maintainer's approval, as both files require.

Done when `flake.nix` pins the new quest, no `quest/*README` branch remains,
and `quest check` passes.

## Related

- [Merge queue](/quest/m1/merge-queue.md) - the other trunk workflow change
21 changes: 14 additions & 7 deletions quest/m1/ts-export-rewind.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@

`moq export ts` keeps its program clock running through gaps and sets the PCR
`discontinuity_indicator` only when the clock really restarts: a `--linger`
resume or a replaced broadcast. One resume flags once. A timestamp that goes
backwards within one broadcast fails the export.
resume or a replaced broadcast. One resume flags once. A track whose timestamps
restart within one broadcast fails the export.

## Plan

Expand All @@ -23,17 +23,24 @@ Decided (2026-10-04):
export already emits null packets through quiet periods.
- Restart the clock and set the flag only on a resume or a replaced
broadcast.
- Time going backwards within one broadcast is a publisher bug (a name always
means the same content), so the export fails with an error rather than
rewinding.
- Timestamps restarting within one broadcast are a publisher bug (a name
always means the same content), so the export fails with an error rather
than rewinding. The only input timestamp is the frame's PTS, and it legally
moves backwards in decode order with B-frames (0, 120, 40, 80 ms); the
authored DTS cannot show a reset, since `author_dts` clamps every backwards
candidate to `prev + 1`. So the check runs on PTS before that clamp, and is
group-aware: reordering, including open-GOP leading pictures, stays inside
a group, so a frame whose PTS is below the largest PTS of the track's
previous group is a reset.
Comment on lines +32 to +34

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Use the previous group's start, not its maximum PTS

HANG explicitly permits frames below the previous group's content, provided they do not precede its start (group rules). The existing overlap regression accepts group 0 at 0/33/66 ms followed by group 1 at 50/83 ms. The new maximum-based rule rejects the valid 50 ms keyframe and terminates that recording. Align the plan with the producer/consumer's existing previous-group-start floor, and retain this cross-group overlap case alongside the reset and within-group B-frame cases.

- No new public API: the generation-change handling lives behind the
crate-private `ExportSource`.
- [Fixed-delay release](/quest/m1/tstd/delay.md) rewrites the same release
path and re-anchors on each rewind; land this inside or after it.

Tests: a resume that walks gaps on two tracks sets the indicator once; a 10 s
forward gap on one track keeps the PCR continuous with no flag; a backwards
timestamp fails the export.
forward gap on one track keeps the PCR continuous with no flag; a source whose
timestamps restart at zero after 10 s fails the export, while a B-frame
sequence and open-GOP leading pictures are still accepted.

## Closes

Expand Down
10 changes: 5 additions & 5 deletions quest/m2/ladder/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,10 @@ state shipped in [moq#2865](https://github.com/moq-dev/moq/pull/2865):
rendition. Routing, decoder, and presentation identities are split so a
metadata-only change cannot rebuild WebCodecs.

Decided (2026-10-04): a rung is enabled only once its reservation is granted
at or above its boundary, and disabled, with encoding stopped, when the grant
falls below it, until the grant recovers. Demand loss alone never changes the
flag.
Decided (2026-10-04): a rung's `enabled` follows its last applied target. It
is disabled, with encoding stopped, when its share falls below its boundary,
and enabled once a target at or above the boundary is applied. Demand loss
alone never changes the flag.

What remains is the publisher side. The allocator
([moq#2854](https://github.com/moq-dev/moq/pull/2854)) divides a connection's
Expand Down Expand Up @@ -61,7 +61,7 @@ stall = (max + 2 * lower) / 3
The lowest rendition takes `lower = 0`, so its boundary is `max / 3`. An
encoder may adapt within `[stall, max]`; below the boundary the rung is
disabled (`enabled: false`) and stops encoding, and it is enabled again only
once a target above the same boundary is successfully applied. Catalog state follows the last target the encoder
once a target at or above the same boundary is successfully applied. Catalog state follows the last target the encoder
*accepted*, not the one the controller requested, so a transient rate-control
failure retains the last applied target rather than lying.

Expand Down
18 changes: 13 additions & 5 deletions quest/m2/mp4-export.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,19 @@ Decided (2026-10-04):
- A new `mp4` format that requires `--output` (it must seek); `export fmp4`
stays a stream to stdout.

Open for the PR: a graceful-finish path in `moq-cli` (today SIGINT drops the
task set), and what a second Ctrl+C or SIGTERM does; edit lists or stretched
durations for gaps and track start offsets; co64 and a 64-bit mdat header for
files over 4 GB; whether a returning rendition with a new config gets an
extra sample description.
- Finishing is required, not best effort: the first SIGINT or SIGTERM
finalizes the file, which needs a graceful-finish path in `moq-cli` (today
SIGINT drops the task set). A second signal aborts at once and leaves the
crash-safe fragmented file.
- No size limit: always write co64 chunk offsets and a 64-bit (largesize) mdat
header.

Tests: SIGINT during a recording produces a regular MP4 that parses with its
moov at the end; a second SIGINT leaves a playable fragmented file.

Open for the PR: edit lists or stretched durations for gaps and track start
offsets; whether a returning rendition with a new config gets an extra sample
description.

## Related

Expand Down
Loading