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
4 changes: 4 additions & 0 deletions quest/m1/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,12 +133,15 @@ per-frame CPU growth since #5926.
## Required

- [Address groups by timestamp](/quest/m1/time-addressing/README.md) - group start timestamps replace sequence numbers in the moq-net API and lite-07 wire, while old subscribers, old publishers, and published JS clients keep working
- [@moq/net group IDs past 2^53](/quest/m1/js-group-u62.md) - JS carries every 62-bit group ID instead of refusing those past 2^53
- [Pre-cut audit findings](/quest/m1/cut-audit/README.md) - fix the crashes, hostile-input costs, and correctness bugs the 2026-10-24 audit of main found
- [Vendored code has every upstream fix it needs](/quest/m1/vendor-sync/README.md) - quinn, libv4l-rs, nvidia-video-codec-sdk, rust-media-libs, cros-codecs, and UniFFI generator fixes are ported or planned, once per release cycle
- [Mesh dials recover from a stuck flow](/quest/m1/mesh-fresh-port.md) - a relay redials a failing cluster peer from a fresh UDP port, so one dropped 5-tuple can't strand a link
- [Cluster routing](/quest/m1/cluster-routing/README.md) - prove shared routing contracts, implement per-origin routes, and connect outbound local clusters
- [Routing guide](/quest/m1/routing-guide.md) - concepts and practical outbound federation setup
- [moq-c at parity](/quest/m1/c/README.md) - the hand-written `moq-c` ships at the cut with every m1 binding feature moq-ffi gains
- [JSON writes take a broadcast-clock timestamp](/quest/m1/json-required-timestamp.md) - every binding's JSON write requires one, moq-c's NULL fails, and C gains `moq_publish_catalog_now()`, so C media stops advertising an 8 s delay
- [Catalog delay is the least a track trails](/quest/m1/catalog-delay-min.md) - the estimator keeps the minimum trail, so B-frame reorder never becomes `delay`
- [Bindings see a moq-transport run end as an end](/quest/m1/ffi-ietf-run-end.md) - an IETF publisher ending its run reaches moq-ffi consumers as an end, not `Internal`
- [Byte-stream renditions form a ladder](/quest/m1/stream-ladder.md) - moq-ffi stream producers link as `lower` and disable unfundable rungs like frame producers
- [A job's exact route continues its claim's readers](/quest/m1/announce-takeover.md) - a job's route inherits its claim's instance, replacing the ANNOUNCE_TAKEOVER predecessor design
Expand All @@ -157,6 +160,7 @@ per-frame CPU growth since #5926.
- [moq-crypto replaces moq-sign and moq-e2ee](/quest/m1/moq-crypto.md) - one Rust crate and one JS package hold signing and end-to-end encryption, sharing moq-net's timestamp check
- [moq-transport 18, 20, and 22 only](/quest/m1/ietf-drop/README.md) - drafts 14-17, 19, and 21 are deleted from Rust, JS, tests, interop lanes, and docs, removing the legacy control-stream adapter and its request limit
- [A moq-transport request token is one auth per distinct token](/quest/m1/ietf-token-auth.md) - requests naming the same token share one verification and grant, on any request type, ending with the last such request
- [A lapsed request token resets with EXPIRED_AUTH_TOKEN](/quest/m1/auth-expiry-reset.md) - group streams of a lapsed grant reset with the expiry code, so a client knows to renew
- [moq-quic's API improves on quinn's](/quest/m1/quic-api/README.md) - moq-quic and both QUIC drivers ship one mirrored API: `send::`/`recv::` namespaces, one error per direction, consuming terminal operations, plain configs, no quinn baggage
- [On-demand JavaScript FETCH](/quest/m1/js-fetch-on-demand.md) - browser publishers answer IETF FETCH for evicted or uncached groups through the JS ranges request surface
- [Native rung disable](/quest/m1/simulcast-rung-disable.md) - OBS stops sending the top renditions its grant cannot fund, as the Rust/FFI publishers do, and a shaped uplink through moq-ffi proves the resume
Expand Down
35 changes: 35 additions & 0 deletions quest/m1/auth-expiry-reset.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# [XS] A lapsed request token resets its groups with EXPIRED_AUTH_TOKEN

## Goal

When a moq-transport request token's grant lapses, the group streams of the
subscriptions it admitted reset with the draft's EXPIRED_AUTH_TOKEN code
instead of INTERNAL_ERROR. A client can then tell "renew and retry" from
"denied" without parsing PUBLISH_DONE's reason phrase.

## Plan

Point 1 of [moq-dev/moq#7646](https://github.com/moq-dev/moq/issues/7646)
(ksletmoe-aws), verified on `main`. A lapsed grant ends the subscription with
PUBLISH_DONE status UNAUTHORIZED and reason "token expired"
(`Served::status` in `rs/moq-net/src/ietf/publisher.rs`, which notes that
PUBLISH_DONE has no token-expiry status), and its group streams reset with
INTERNAL_ERROR.

Decided 2026-11-03 (maintainer, `/quest-import`): ✅ reset with
EXPIRED_AUTH_TOKEN; no wire change (the next request's REQUEST_ERROR already
says EXPIRED_AUTH_TOKEN); won't fix. PUBLISH_DONE keeps UNAUTHORIZED, since
its own EXPIRED means the subscription's lifetime ran out.

Check the reset code in drafts 18, 20, and 22. The report cites 0x07 for a
stream reset, while 0x5 is the request error code moq-net already sends
(`rs/moq-net/src/ietf/error.rs`). Use what each draft defines for stream
resets. Test on mocked time that a lapsed grant resets its groups with that
code and a revoked one doesn't, and note it in `doc/bin/relay/auth.md`.

Public API: none. Wire: moq-transport group resets on token lapse change
code.

## Related

- [moq-transport request tokens](/quest/m1/ietf-token-auth.md) - #7646's points 2 to 4 land there
46 changes: 46 additions & 0 deletions quest/m1/catalog-delay-min.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# [S] A track's catalog delay is the least it trails the others

## Goal

The catalog estimator's `delay` is the smallest amount a track's frames have
trailed the highest timestamp another track wrote, not the largest. A
constant offset between tracks still shows as `delay`. B-frame reorder, which
only some frames trail by, never does: not on the B-frame track, not on a
sibling rendition, and not on the audio beside it.

## Plan

Reported in [moq-dev/moq#7579](https://github.com/moq-dev/moq/issues/7579)
and verified on `main`. `Order` (`rs/moq-mux/src/catalog/estimate.rs`)
compares presentation timestamps written in decode order, and
`Estimator::order` keeps the maximum trail as `delay`. A B-frame track's high
runs ahead of the frames it writes, so every track written beside it
advertises the reorder as `delay` (40 to 50 ms in the report). The player
then counts it twice, because the measured arrival lateness already holds it:
130 ms of playout against 82 ms on libmoq 0.6.17.

Decided 2026-11-03 (maintainer, `/quest-import`):

- Fix: ✅ `delay` is the minimum trail; subtract each track's declared
reorder (containers only); something else. The reporter's prototype (decode
edges, with the reorder taken from the SPS depth through `DeclaredClock`)
was rejected: "I definitely don't want SPS depth".
- Why the minimum: `delay` exists for a constant offset between tracks, which
cancels out of the receiver's `measured` (`doc/concept/audio-jitter.md`,
"Across renditions"). Reference pictures, and the audio of the same decode
slot, trail by zero, so the minimum drops the reorder. A genuinely late
track, every frame of which trails, keeps its offset. Variable lateness is
left to `jitter` and the receiver's measurement, since playout is
`delay + max(measured, jitter)`.

A track that has never trailed keeps no `delay`. Keep the reporter's lockstep
test (two 50 fps renditions with two B-frames, plus audio, in decode order)
and assert no track gets a `delay`, with and without a declared reorder. Keep
the existing late-track tests. Update `Estimate::delay`'s doc and the `Order`
comment.

Public API: none. Wire: catalog `delay` values shrink for B-frame broadcasts.

## Closes

- [#7579](https://github.com/moq-dev/moq/issues/7579) - catalog `delay` counts B-frame reorder
28 changes: 28 additions & 0 deletions quest/m1/js-group-u62.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# [S] @moq/net group IDs past 2^53

## Goal

`@moq/net` decodes, holds, and re-encodes every group ID the wire allows (a
62-bit varint) on lite and moq-transport, instead of refusing any past 2^53.
A track whose group IDs are large, such as start ticks on a fine timescale
or a third-party moq-transport publisher's IDs, plays and relays through JS.

## Plan

Decided 2026-11-03 (maintainer, the `/quest-import` grill of
[moq-dev/moq#7573](https://github.com/moq-dev/moq/issues/7573)): "JS should
fix that regardless". Legacy group IDs now pass through verbatim as keys
([the model](/quest/m1/time-addressing/model.md)), so JS can no longer assume
they stay small. This replaces
[@moq/net by start](/quest/m1/time-addressing/js.md)'s plan to refuse a start
past 2^53.

Today `js/net/src/lite/group.ts` reads and writes the sequence with `u53`,
and the moq-transport codecs do the same. Carry group IDs as `bigint` at the
wire and in the model, or add a checked u62 type if `bigint` costs too much
on the hot path. Measure both with the JS benches and pick the simpler one
that holds. The API breaks where a group ID is a `number` today. Test the
decode and encode of 2^53 + 1 and 2^62 - 1 on every wire version, through a
JS relay hop.

Public API: breaking in `@moq/net` where group IDs are numbers. Wire: none.
49 changes: 49 additions & 0 deletions quest/m1/json-required-timestamp.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# [S] JSON writes take a broadcast-clock timestamp in every binding

## Goal

Every JSON snapshot update and stream append takes a required timestamp on
the broadcast clock: `moq_mux::json`'s `Snapshot` and `Stream`, moq-ffi's
JSON producers, and moq-c's `moq_publish_json_snapshot_update` and
`moq_publish_json_stream_append`, where a NULL timestamp fails instead of
being stamped with `now()`. moq-c gains `moq_publish_catalog_now()`, so a C
publisher stamps its media and its data in the broadcast clock's epoch. A C
publisher's JSON tracks no longer give its media an 8 s catalog `delay`.

## Plan

Reported in [moq-dev/moq#7573](https://github.com/moq-dev/moq/issues/7573)
and verified on `main`. moq-c stamps a NULL-timestamp JSON write with the
catalog clock's `now()` (`json_snapshot_update` and `json_stream_append` in
`rs/moq-c/src/publish.rs`). That write takes a seat in the catalog's write
`Order` against C media on the publisher's own timeline, so every media
track advertises the gap between the two timelines, about 8 s, as `delay`.
Rust and moq-ffi publish a null timestamp untimed instead, and moq-ffi
already exposes `MoqMediaCatalogProducer::now()`.

Decided 2026-11-03 (maintainer, `/quest-import`):

- Fix: require a timestamp, and expose `now()` to C, "because every track in
a broadcast needs to use the same epoch". Options were: NULL writes
unordered; ✅ require a timestamp (with C `now()`); won't fix.
- Scope: ✅ every binding in one quest; moq-c only; moq-c now and the others
later.
- This is the JSON part of
[time-addressing's required timestamps](/quest/m1/time-addressing/model.md),
which removes untimed writes everywhere. It lands first because #7573 is a
live bug.

`moq_publish_catalog_now(broadcast, moq_timestamp *dst)` mirrors moq-ffi's
`now()`. `doc/lib/c` tells a C publisher to stamp media and timed data from
it. A NULL timestamp returns the invalid-argument error and publishes
nothing. Test that a C broadcast with JSON tracks advertises no `delay` on
its media, and that NULL fails, and update the moq-ffi and Rust JSON tests to
pass a timestamp.

Public API: breaking in moq-c (NULL refused, `moq_publish_catalog_now`
added), moq-ffi (JSON `timestamp` required), and `moq_mux::json` (a write
takes a timestamp). Wire: none.

## Closes

- [#7573](https://github.com/moq-dev/moq/issues/7573) - moq-c JSON writes without a timestamp give every media track ~8 s of delay
2 changes: 1 addition & 1 deletion quest/m1/max-age-defaults.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ Before claiming, checked ready state with `quest unblocked` and searched open PR

### Current defaults

Publisher max-age is a freshness bound on **superseded groups**. A group expires when either its successor's arrival is old enough at this hop or the successor's first timestamp trails the live edge enough. Untimed tracks use the wall clock. The newest group is exempt. Byte reclamation has a separate cache policy. See [rs/moq-net/src/model/track.rs:120](https://github.com/moq-dev/moq/blob/c95a9b1201/rs/moq-net/src/model/track.rs#L120) and [js/net/src/track.ts:137](https://github.com/moq-dev/moq/blob/c95a9b1201/js/net/src/track.ts#L137).
Publisher max-age is a freshness bound on **superseded groups**. A group expires when either its successor's arrival is old enough at this hop or the successor's first timestamp trails the live edge enough. Untimed tracks use the wall clock today; once [time-addressing](/quest/m1/time-addressing/model.md) lands, a scale-less track counts max-age in bare key units instead (decided 2026-11-03). The newest group is exempt. Byte reclamation has a separate cache policy. See [rs/moq-net/src/model/track.rs:120](https://github.com/moq-dev/moq/blob/c95a9b1201/rs/moq-net/src/model/track.rs#L120) and [js/net/src/track.ts:137](https://github.com/moq-dev/moq/blob/c95a9b1201/js/net/src/track.ts#L137).

| Surface | Current default and meaning | Source |
| --- | --- | --- |
Expand Down
2 changes: 1 addition & 1 deletion quest/m1/time-addressing/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ compatibility.

- [Rust groups by start](/quest/m1/time-addressing/model.md) - the moq-net model and API address groups by start, and legacy wires carry the start ticks as the group ID
- [Lite-07 by start](/quest/m1/time-addressing/lite07.md) - lite-07 carries starts, previous-start links, GROUP_END, and time bounds instead of sequence numbers
- [moq-transport starts and ends](/quest/m1/time-addressing/ietf.md) - Prior Group ID Gap and TIMESTAMP carry links and PTS, END_OF_GROUP marks a whole group, and the end time needs an extension
- [moq-transport IDs verbatim](/quest/m1/time-addressing/ietf.md) - group IDs pass through as keys, Prior Group ID Gap signals drops best-effort, END_OF_GROUP marks a whole group, and our Timestamp property goes
- [@moq/net by start](/quest/m1/time-addressing/js.md) - JS matches Rust's model, wire, and legacy mapping
- [Consumers by start](/quest/m1/time-addressing/consumers.md) - hang drops duration and discontinuity markers, and every consumer that numbered groups uses starts
- [Bindings by start](/quest/m1/time-addressing/bindings.md) - moq-ffi, moq-c, and every binding expose starts, with migration docs
Expand Down
9 changes: 6 additions & 3 deletions quest/m1/time-addressing/bindings.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,12 @@ failover, with each `doc/lib` page and the PR's Impact section saying how to mig

Follow the cross-package sync table from `rs/moq-ffi` outward. Timestamps
already cross the bindings as `{value, scale}`, so a start reuses that
type. Decided 2026-10-27 (maintainer): every track is timed after the cut,
so reads drop the `Option` that exact timestamps keeps for untimed. Only
reads drop it here; producer arguments keep their optional timestamp.
type. Decided 2026-11-03 (maintainer, replacing 2026-10-27's "producer arguments
keep their optional timestamp"): every read and write carries a timestamp.
Reads drop the `Option` that exact timestamps keeps for untimed, producer
arguments take a required one, and nothing is wall-clock stamped. A
scale-less track's frames carry their group's key. See
[the model](/quest/m1/time-addressing/model.md).

Public API: breaking in every binding. Wire: none.

Expand Down
74 changes: 44 additions & 30 deletions quest/m1/time-addressing/ietf.md
Original file line number Diff line number Diff line change
@@ -1,39 +1,53 @@
# [M] moq-transport carries group starts and ends
# [S] moq-transport carries group IDs verbatim and gaps best-effort

## Goal

A moq-transport peer that understands the MOQ Properties sees each group's
predecessor link, each object's PTS, and each group's end, and moq-net reads
them back on ingest. A peer without them still plays live; only end
precision and gap detection degrade.
moq-net keeps a moq-transport group ID verbatim as the group's key in both
directions. It sends each group's predecessor gap in Prior Group ID Gap as a
best-effort drop signal, and marks a whole group with END_OF_GROUP. It sends
no Timestamp object property of its own. LOC object properties are proxied
only between two moq-transport peers.

## Plan

Decided 2026-10-25 (maintainer), from the spike:

- The Group ID is the start ticks ([the model](/quest/m1/time-addressing/model.md)
owns that mapping). The previous-start link rides in the Prior Group ID Gap
property.
- Object IDs stay +1 per frame, so no Prior Object ID Gap is sent. Each
object's PTS rides in TIMESTAMP (0x10), with TIMESCALE (0x08) on the track,
the MOQ Properties shared with draft-ietf-moq-loc, so it stays LOC
compatible.
- GROUP_END maps to END_OF_GROUP. The supported drafts refuse properties on
a status object (`check_status` in `rs/moq-net/src/ietf/object.rs`), so
END_OF_GROUP stays bare and the end needs a negotiated extension or is
lost, as the extension rule below allows.
- Rejected: Object ID as a PTS delta, because B-frames run backwards in
decode order and open-GOP leading frames present before the group's start.
- The properties are optional on both sides: moq-transport may carry them as
properties or a negotiated extension, and a peer sending none still works
for live playback.

Test both directions over drafts 18 and 22, with and without the properties.

Public API: none. Wire: moq-transport Prior Group ID Gap and TIMESTAMP are
sent and read, END_OF_GROUP marks a whole group, and the end time rides a
negotiated extension if one is defined.
Decided 2026-11-03 (maintainer, the `/quest-import` grill of
[moq-dev/moq#7573](https://github.com/moq-dev/moq/issues/7573)), replacing
the 2026-10-25 plan that carried each object's PTS in TIMESTAMP (0x10) with
TIMESCALE (0x08):

- **The Group ID is the key**, verbatim on ingest and egress
([the model](/quest/m1/time-addressing/model.md) owns that mapping). A
moq-transport track is in whatever unit its IDs are in; nothing infers
time from its objects.
- **No Timestamp property from us.** moq-net stops sending TIMESTAMP on
egress and doesn't lift it into the frame on ingest. LOC object properties
pass through unchanged when both sides speak moq-transport, and are dropped
toward moq-lite. A moq-transport publisher's LOC media relayed to a
moq-lite viewer therefore arrives without a frame PTS, which is accepted.
Today's TIMESTAMP code (`ietf/group.rs`, `ietf/object.rs`,
`ietf/max_age.rs`) is deleted or reduced to that pass-through.
- **Prior Group ID Gap stays** as a best-effort SUBSCRIBE_DROP signal:
egress sends the gap to the previous key, and ingest reads it as the
skipped IDs being dropped. A peer that sends none still works.
- **GROUP_END maps to END_OF_GROUP.** The supported drafts refuse properties
on a status object (`check_status` in `rs/moq-net/src/ietf/object.rs`), so
END_OF_GROUP stays bare and the end time is lost on this wire.
- Object IDs stay +1 per frame, so no Prior Object ID Gap is sent.

Decision prompts:

- Timestamp property: ✅ stop, with LOC proxied only between moq-transport
peers; keep reading but stop sending; keep the 2026-10-25 plan.
- Prior Group ID Gap: drop it; ✅ keep it as a best-effort SUBSCRIBE_DROP
signal.

Test both directions over drafts 18, 20, and 22: IDs round-trip exactly
through a relay, a gap reaches the subscriber as a drop, and LOC properties
survive moq-transport to moq-transport.

Public API: none. Wire: moq-transport loses our TIMESTAMP property, and
gains Prior Group ID Gap and END_OF_GROUP on whole groups.

## Required

- [Rust groups by start](/quest/m1/time-addressing/model.md) - the model whose links and ends these carry
- [Rust groups by start](/quest/m1/time-addressing/model.md) - the model whose keys and gaps these carry
5 changes: 3 additions & 2 deletions quest/m1/time-addressing/js.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@ Mirror [the model](/quest/m1/time-addressing/model.md) and
increasing starts, the three group ends, reach from a group's own end, and
`Relative(n)` counting groups. The gap machinery that assumes `sequence + 1`
(`delivered.ts`, `ledger.ts`, `track.ts`, `resume.ts`) follows
previous-start links instead. Starts stay JavaScript numbers, so a decoded
start past 2^53 is refused as today's group IDs are.
previous-start links instead. Starts are carried past 2^53, as
[@moq/net group IDs past 2^53](/quest/m1/js-group-u62.md) does for today's
group IDs (decided 2026-11-03, maintainer).

The spike found no sparse-ID breakage in direct `@moq/net` sessions over
lite-06 and lite-07; its relay fills follow Rust's once
Expand Down
Loading
Loading