From ce4d3bf87dd7e65e65e3030ff09c82d1f888c603 Mon Sep 17 00:00:00 2001 From: OneTooMany Date: Fri, 2 Oct 2026 12:26:32 +0200 Subject: [PATCH 1/9] quest: claim quest/m1/plan-untimed-objects Co-Authored-By: Claude Opus 5.5 From 821bc2a9037cd50beb2eda55d108887aa2a02179 Mon Sep 17 00:00:00 2001 From: OneTooMany Date: Fri, 2 Oct 2026 12:46:01 +0200 Subject: [PATCH 2/9] quest: plan untimed frames as four implementation quests Co-Authored-By: Claude Opus 5.5 --- quest/m1/README.md | 5 +- quest/m1/cache-wall-eviction.md | 4 + quest/m1/data-consumer-timestamps.md | 8 +- quest/m1/js-data-consumer-timestamps.md | 2 +- quest/m1/js-publish-timestamp.md | 2 +- quest/m1/js-untimed-model.md | 38 +++++++++ quest/m1/lite-untimed.md | 40 +++++++++ quest/m1/plan-untimed-objects.md | 49 ----------- quest/m1/publish-timestamp.md | 11 ++- quest/m1/ts-pes-untimed.md | 29 +++++++ quest/m1/untimed-model.md | 109 ++++++++++++++++++++++++ quest/m2/cmaf-frame-timestamp.md | 3 + 12 files changed, 240 insertions(+), 60 deletions(-) create mode 100644 quest/m1/js-untimed-model.md create mode 100644 quest/m1/lite-untimed.md delete mode 100644 quest/m1/plan-untimed-objects.md create mode 100644 quest/m1/ts-pes-untimed.md create mode 100644 quest/m1/untimed-model.md diff --git a/quest/m1/README.md b/quest/m1/README.md index a29b80e3413..b73842af4e8 100644 --- a/quest/m1/README.md +++ b/quest/m1/README.md @@ -75,7 +75,10 @@ QUIC studies there on that rule. - [Go and Dart doc samples](/quest/m1/doc-samples-go-dart.md) - Go and Dart doc samples compile against their wrappers - [Publishing never invents a timestamp](/quest/m1/publish-timestamp.md) - no Rust or binding publish API fills in a timestamp; an untimed payload goes out untimed, and moq-mux data producers take broadcast-clock timestamps - [JS publishing never invents a timestamp](/quest/m1/js-publish-timestamp.md) - the same in @moq/json, @moq/binary, and @moq/net -- [Plan: untimed objects](/quest/m1/plan-untimed-objects.md) - map faithful timestamp absence, publisher to consumer, into implementation quests +- [Untimed model](/quest/m1/untimed-model.md) - on dev, moq-net frames and datagrams carry an absent timestamp to every subscriber; no receiver fills in arrival time +- [JS untimed model](/quest/m1/js-untimed-model.md) - the same in @moq/net +- [Untimed lite-07](/quest/m1/lite-untimed.md) - lite-07 encodes an absent timestamp in both languages; lite-05/06 write send time +- [Untimed TS PES](/quest/m1/ts-pes-untimed.md) - a PES without a PTS imports untimed instead of at 0 - [Data consumer timestamps](/quest/m1/data-consumer-timestamps.md) - json and binary consumers return each value's timestamp, in Rust and every binding; snapshots add `latest()` beside an in-order `next()` - [JS data consumer timestamps](/quest/m1/js-data-consumer-timestamps.md) - @moq/json and @moq/binary consumers return each value's timestamp, with snapshot `next()` and `latest()` - [Nested data configs](/quest/m1/data-config-nesting.md) - docs nest `BinaryConfig`/`JsonConfig` in an application section instead of flattening it diff --git a/quest/m1/cache-wall-eviction.md b/quest/m1/cache-wall-eviction.md index 7643c0075e4..53d42d7a4ca 100644 --- a/quest/m1/cache-wall-eviction.md +++ b/quest/m1/cache-wall-eviction.md @@ -34,6 +34,9 @@ instead of only on a write. The pool's idle expiry is out of scope. - Weigh the semantics too: `max_age` is media time on purpose, so a congestion stall cannot age content out (`track::Info::max_age`). A wall term changes that for a stalled but live publisher. +- Whatever this decides, an untimed group is never media-stale and only the + pool's expiry reclaims it, as decided in + [moq-net carries untimed frames faithfully](/quest/m1/untimed-model.md). - Record the numbers and the decision in the PR, then rewrite this quest into the implementation or delete it. @@ -43,3 +46,4 @@ Public API: none from the plan. Wire: none. - [Cache expiry growth](/quest/m1/cache-expiry-growth.md) - relay memory past the expiry window, in the same cache - [Cache shard](/quest/m2/cache-shard.md) - the pool's shared counters under many workers +- [moq-net carries untimed frames faithfully](/quest/m1/untimed-model.md) - untimed groups are never media-stale diff --git a/quest/m1/data-consumer-timestamps.md b/quest/m1/data-consumer-timestamps.md index 82e86a78fda..4fc513ef2f2 100644 --- a/quest/m1/data-consumer-timestamps.md +++ b/quest/m1/data-consumer-timestamps.md @@ -21,9 +21,9 @@ state carries the timestamp of the frame that produced it. Decided: `next()` and `poll_next()` return `Timed`, the type the producers take in [Publishing never invents a timestamp](/quest/m1/publish-timestamp.md). `at` is the frame's media timestamp on the track's timescale, and `None` for -an untimed frame: [Plan: untimed objects](/quest/m1/plan-untimed-objects.md) -settled (2026-10-01) that absence survives the wire rather than becoming -arrival time. A republisher passes `at` straight to a moq-mux data producer. +an untimed frame: absence survives the wire rather than becoming arrival +time (decided 2026-10-01, see +[moq-net carries untimed frames faithfully](/quest/m1/untimed-model.md)). A republisher passes `at` straight to a moq-mux data producer. Decided (2026-10-01): snapshot consumers get two reads, both returning `Timed`. Today the moq-json snapshot consumer applies every buffered delta @@ -48,4 +48,4 @@ Public API: breaking, on `dev`. Wire: none. ## Required - [Publishing never invents a timestamp](/quest/m1/publish-timestamp.md) - gives `Timed.at` its untimed meaning, the type this returns -- [Plan: untimed objects](/quest/m1/plan-untimed-objects.md) - the model must carry an absent timestamp to consumers +- [moq-net carries untimed frames faithfully](/quest/m1/untimed-model.md) - the model must carry an absent timestamp to consumers diff --git a/quest/m1/js-data-consumer-timestamps.md b/quest/m1/js-data-consumer-timestamps.md index 90657f35aa7..930be35e6db 100644 --- a/quest/m1/js-data-consumer-timestamps.md +++ b/quest/m1/js-data-consumer-timestamps.md @@ -27,7 +27,7 @@ Public API: breaking, on `dev`. Wire: none. ## Required - [JS publishing never invents a timestamp](/quest/m1/js-publish-timestamp.md) - adds `Timed` to `@moq/net` -- [Plan: untimed objects](/quest/m1/plan-untimed-objects.md) - js/net stops filling arrival time, so `at` can be absent +- [@moq/net carries untimed frames faithfully](/quest/m1/js-untimed-model.md) - js/net stops filling arrival time, so `at` can be absent ## Related diff --git a/quest/m1/js-publish-timestamp.md b/quest/m1/js-publish-timestamp.md index 388adbc49ff..bd13d099022 100644 --- a/quest/m1/js-publish-timestamp.md +++ b/quest/m1/js-publish-timestamp.md @@ -25,4 +25,4 @@ Public API: breaking, on `dev`. Wire: none. ## Required -- [Plan: untimed objects](/quest/m1/plan-untimed-objects.md) - an untimed payload must travel as untimed before producers stop filling in now +- [@moq/net carries untimed frames faithfully](/quest/m1/js-untimed-model.md) - the model must hold an untimed payload before producers stop filling in now diff --git a/quest/m1/js-untimed-model.md b/quest/m1/js-untimed-model.md new file mode 100644 index 00000000000..43cd946bee1 --- /dev/null +++ b/quest/m1/js-untimed-model.md @@ -0,0 +1,38 @@ +# [M] @moq/net carries untimed frames faithfully + +## Goal + +`@moq/net` mirrors [moq-net carries untimed frames +faithfully](/quest/m1/untimed-model.md). A frame or datagram without a +timestamp stays untimed from publisher to consumer, and js/net never fills in +`Timestamp.now()` on receive. Covers `@moq/net` and its in-repo callers +(`@moq/hang`, `@moq/loc`, `@moq/watch`). + +## Plan + +Decided (2026-10-02 12:43 +02:00): a hand-written change now, rather than +waiting for [Generated @moq/net](/quest/m1/rs2ts/README.md). That line is +long-running and would stall the JS timestamp quests. Whichever lands second +absorbs the other. The semantics, the end-marker rule and the reasons are in +the Rust quest; keep the two in step. + +Things to look out for: + +- Receive-side fills: lite frames on a track with no timescale, and IETF + objects without a timestamp, both default to now today. +- The track's drift, reach, staleness and expiry logic skips unstamped groups + the way Rust does. Check that untimed frames still count as frames, and + that an untimed track starts at the latest group. +- `Frame.timestamp` and `Datagram.timestamp` become optional. Update callers + in js/hang and js/loc (end markers) and anything in js/watch that reads + them. + +Test: an untimed frame survives a JS subscribe on each receive path. Run +`just test interop --all`. + +Public API: breaking, on `dev`. Wire: none. + +## Related + +- [moq-net carries untimed frames faithfully](/quest/m1/untimed-model.md) - the Rust side and the decisions +- [Generated @moq/net](/quest/m1/rs2ts/README.md) - will replace this code with generated code diff --git a/quest/m1/lite-untimed.md b/quest/m1/lite-untimed.md new file mode 100644 index 00000000000..fe631a3ada4 --- /dev/null +++ b/quest/m1/lite-untimed.md @@ -0,0 +1,40 @@ +# [S] lite-07 encodes an absent timestamp + +## Goal + +On moq-lite-07, an untimed frame or datagram crosses the wire as untimed in +both Rust and JS, so absence survives a lite relay hop. lite-05 and lite-06 +keep writing the encoder's send time, documented as a downgrade for old +peers. Covers `rs/moq-net`, `js/net` and `drafts/draft-lcurley-moq-lite.md`. + +## Plan + +Decided (2026-10-01, maintainer): shift the FRAME Timestamp Delta and the +DATAGRAM Timestamp by one, so 0 means absent. An absent frame doesn't move +the delta baseline. Rejected: a bare 0 as a sentinel, which collides with a +real pts of 0. + +Decided (2026-10-02 12:43 +02:00): one PR for both languages, after both +model quests. Shipping one language first would break Rust-JS interop on +lite-07-wip in between. + +Draft work: + +- The draft describes only the current wire. Add the lite-07 encoding, the + lite-05/06 downgrade, and a lite-07 changelog entry. +- Fix the text that assumes every frame is timed: "each frame carries a + presentation timestamp", the datagram's "any varint value (including 0) is + valid", and expiration's "reach" (the first frame timestamp of the next + group) for an untimed group. +- Check how a lite track that has no timescale says so on lite-07. + +Test: an untimed frame and an untimed datagram round-trip Rust-to-JS and +JS-to-Rust on lite-07. lite-06 still receives a timestamp. Run +`just drafts check` and `just test interop --all`. + +Public API: none. Wire: lite-07-wip only, which is unpublished. + +## Required + +- [moq-net carries untimed frames faithfully](/quest/m1/untimed-model.md) - the Rust model must hold an absent timestamp before the wire can carry one +- [@moq/net carries untimed frames faithfully](/quest/m1/js-untimed-model.md) - the same for JS diff --git a/quest/m1/plan-untimed-objects.md b/quest/m1/plan-untimed-objects.md deleted file mode 100644 index c495e94c703..00000000000 --- a/quest/m1/plan-untimed-objects.md +++ /dev/null @@ -1,49 +0,0 @@ -# [S] Plan: untimed objects - -## Goal - -Rewrite this quest into implementation quests that carry an absent timestamp -faithfully, from publisher to consumer, through every relay. That covers -untimed publishes, IETF objects on a track without TIMESCALE or without a -Timestamp property, and lite-01 to lite-04, which have no timestamp on the -wire. Nothing may invent a timeline that changes with the relay path. - -## Plan - -Today every receiver fills local arrival time (`Timestamp::from(runtime.now())` -in `rs/moq-net/src/{lite,ietf}/subscriber.rs`, `?? Timestamp.now()` in js/net), -as `drafts/draft-lcurley-moq-timestamp.md` mandates. On lite-05 and later, a -relay then forwards those stamps as if they were real. A failover to another -first hop changes them, so the timeline jumps, and they break the "one clock -per broadcast" property. The maintainer dislikes this. - -Decided (2026-10-01): faithful absence, on both the publish and the subscribe -side. The model and every consumer carry `Option`, a relay forwards -"none" as none, and no layer substitutes arrival or wall time. -[Publishing never invents a timestamp](/quest/m1/publish-timestamp.md) stops -the producers filling in now. Untimed groups are never media-stale; the -pool's wall-clock expiry still reclaims them, and start resolution picks the -latest group instead of replaying the cache. - -- lite-07-wip encodes absence by shifting the FRAME Timestamp Delta and the - DATAGRAM Timestamp by one (0 = absent); an absent frame doesn't move the - delta baseline. -- lite-05 and lite-06 (published) can't encode absence, so their encoder - writes its send time, documented in the lite draft as a downgrade for old - peers. -- Rejected: first-hop arrival (the failover jump stays), 0 as a sentinel - (collides with a real pts of 0), and `max_age` on max(wall, pts) (Rust keeps - `max_age` as media-time staleness so a congestion stall can't age content - out, with the pool's expiry as the wall-clock bound, - `rs/moq-net/src/model/cache.rs`). - -What remains is mapping the fallout into implementation quests. - -Facts to gather: which IETF implementations send TIMESCALE and Timestamp -(moxygen, the interop matrix), and every model decision that reads a -timestamp (`track.rs` staleness and `GroupExpiry`, `resume.rs` successors, -`group.rs` `poll_timestamp`, which treats "has a timestamp" as "has a frame"). - -Output: implementation quests (the model and wire carrying `Option`, lite-07 -encoding, and the timestamp and lite drafts), and their places in the -`Required` lists of the publish and consumer timestamp quests. diff --git a/quest/m1/publish-timestamp.md b/quest/m1/publish-timestamp.md index a749f771e72..1653528201e 100644 --- a/quest/m1/publish-timestamp.md +++ b/quest/m1/publish-timestamp.md @@ -14,8 +14,9 @@ Swift, Kotlin, Go, Dart). Not libmoq, which the Decided (2026-10-01), reversing the 2026-09-30 "timestamp required" plan: the timestamp is optional end to end, on the wire and in every API, and absent means untimed. A library never substitutes now, wall clock, or arrival time. -How absence travels on the wire is -[Plan: untimed objects](/quest/m1/plan-untimed-objects.md); this quest +How absence travels through the model and over the wire is +[moq-net carries untimed frames faithfully](/quest/m1/untimed-model.md) and +[lite-07 encodes an absent timestamp](/quest/m1/lite-untimed.md); this quest removes every place that fills one in. Today `Timed.at: None` means "stamp when written": moq-json and moq-binary @@ -48,7 +49,9 @@ fill `Timestamp::now()` (moq-net's clock, not the broadcast's), and moq-mux's - moq-ffi: data producers take an optional timestamp in microseconds on the broadcast clock, unchecked, as Rust does. moq-ffi exposes the broadcast clock's `now()` so callers have a value to stamp with. The raw frame and - datagram records' `timestamp_us` becomes optional instead of `default = 0`. + datagram records' `timestamp_us` is already optional after + [moq-net carries untimed frames faithfully](/quest/m1/untimed-model.md); publishing one without it + goes out untimed instead of at `default = 0`. - Go (`go/wrapper/moq`) and Python (`py/moq-rs`) wrap only the JSON producers; [#4137](https://github.com/moq-dev/moq/pull/4137) added `publish_binary_snapshot` / `publish_binary_stream` to moq-ffi without @@ -67,7 +70,7 @@ with the untimed implementation quests. ## Required - [JSON and flate namespaces](/quest/m1/ffi-shape/json.md) - moves the data producers this changes, so the two breaks land in order rather than colliding -- [Plan: untimed objects](/quest/m1/plan-untimed-objects.md) - an untimed payload must travel as untimed before producers stop filling in now +- [moq-net carries untimed frames faithfully](/quest/m1/untimed-model.md) - the model must hold an untimed payload before producers stop filling in now; until lite-07 encodes absence, a lite encoder writes its send time, as producers effectively do today ## Related diff --git a/quest/m1/ts-pes-untimed.md b/quest/m1/ts-pes-untimed.md new file mode 100644 index 00000000000..19e66a2a245 --- /dev/null +++ b/quest/m1/ts-pes-untimed.md @@ -0,0 +1,29 @@ +# [XS] A PES without a PTS imports untimed + +## Goal + +The MPEG-TS importer publishes a PES that carries no PTS as an untimed frame, +instead of stamping it 0. Asynchronous private data such as KLV is the usual +case. `drafts/draft-lcurley-moq-mpegts.md` says the same. + +## Plan + +Decided (2026-10-02 12:43 +02:00): its own quest, because it lands +independently in a different crate. 0 is the sentinel that +[moq-net carries untimed frames faithfully](/quest/m1/untimed-model.md) +rejected, because it collides with a real pts of 0. + +Check which stream types this applies to. A media elementary stream may +legitimately split an access unit across PES packets, or infer timing. +There, "no PTS" doesn't mean untimed, and the importer's existing handling +stays. Check the TS exporter for the reverse: an untimed frame should go out +as a PES with no PTS. + +Test: a private-data PES with no PTS imports untimed and exports back +without a PTS. + +Public API: none. Wire: the mpegts draft's mapping of a missing PTS changes. + +## Required + +- [moq-net carries untimed frames faithfully](/quest/m1/untimed-model.md) - a frame must be able to carry no timestamp diff --git a/quest/m1/untimed-model.md b/quest/m1/untimed-model.md new file mode 100644 index 00000000000..c08122b4828 --- /dev/null +++ b/quest/m1/untimed-model.md @@ -0,0 +1,109 @@ +# [L] moq-net carries untimed frames faithfully + +## Goal + +A frame or datagram published without a timestamp reaches every subscriber +without one, through any number of relays. moq-net's model carries +`Option`. No receiver fills in arrival time, and a relay forwards +absence as absence. IETF objects on a track without TIMESCALE or without a +Timestamp property, and lite-01 to lite-04 frames, arrive untimed. Covers +moq-net, moq-relay, every in-repo caller of the model, and +`drafts/draft-lcurley-moq-timestamp.md`. + +Out of scope: publishers that fill in now +([Publishing never invents a timestamp](/quest/m1/publish-timestamp.md)), the +lite-07 encoding of absence ([lite-07 encodes an absent +timestamp](/quest/m1/lite-untimed.md)), and JS +([@moq/net carries untimed frames faithfully](/quest/m1/js-untimed-model.md)). + +## Plan + +Decided (2026-10-01, maintainer): faithful absence on both the publish and +the subscribe side. Today every receiver stamps local arrival time, as the +timestamp draft mandates, and on lite-05 and later a relay forwards those +stamps as if they were real. A failover to another first hop then changes +them, so the timeline jumps, and the "one clock per broadcast" property +breaks. Rejected: first-hop arrival (the failover jump stays), 0 as a +sentinel (collides with a real pts of 0), and `max_age` on max(wall, pts) +(`max_age` stays media-time staleness, so a congestion stall can't age +content out, and the pool's wall-clock expiry is the bound). + +Decided (2026-10-02 12:43 +02:00, planning this split): + +- An IETF object that carries its own TIMESCALE and Timestamp properties is + timed, even when the track sent no TIMESCALE. The draft already allows an + object-scope TIMESCALE. Reading it is faithful, not invented. imquic's LOC + examples publish this way. +- A legacy or LOC end marker (an empty frame) that arrives untimed is ignored. + The group then ends without a precise end bound. Its frames still play + from their payload timestamps, and only the last frame's duration is + estimated. Refusing would turn streams that play today into failures. +- Until lite-07 encodes absence, a lite encoder writes its send time for an + untimed frame, on every lite version from lite-05. That's what producers + effectively do today, so nothing regresses. Document it in the lite draft + as a downgrade for lite-05 and lite-06. + +Untimed semantics, from the decision: + +- An untimed group is never media-stale. The pool's wall-clock expiry still + reclaims it. +- Start resolution on an untimed track picks the latest group rather than + replaying the cache, as pre-lite-06 sessions already do. + +Things to look out for: + +- Several model decisions read "has a timestamp" as "has a frame". + `group.rs`'s `poll_timestamp` is the clearest case, and `GroupExpiry` uses + it. An untimed frame must still count as a frame there. +- Live edge, reach and successor search (`track.rs`, `resume.rs`) skip + unstamped groups. Check that an untimed track neither stalls a cursor nor + replays everything. +- `track::Info.timescale` always has a value today. A relay subscribed to an + IETF track without TIMESCALE currently announces one downstream. It must + not claim a timeline the source never had. +- Model docs that recommend `Timestamp::now` for untimed data + (`model/{frame,group,track,subscription}.rs`) change with the type. +- In-repo callers follow the type change: + - moq-mux container consumers and end markers; + - moq-e2ee; + - libmoq; + - moq-ffi's frame and datagram records, whose `timestamp_us` becomes + optional; + - the binding wrappers. + + Media consumers that need a time refuse an untimed frame, except for the + end-marker rule above. +- FETCH_OK properties are discarded today + ([FETCH_OK properties](/quest/m1/fetch-ok-properties.md)), so fetched IETF + objects arrive untimed until that lands. That's correct, not a regression. + +Interop facts (2026-10-02): + +- The in-tree interop matrix has no external peers. +- In the community runner: + - moxygen sends neither TIMESCALE nor Timestamp, so all its objects become + untimed. + - imquic uses the object scope. + - libquicr sends neither. + - MOQtail is undetermined. + +Draft: rewrite the arrival-time mandates in +`draft-lcurley-moq-timestamp.md` (no TIMESCALE, before track properties +arrive, an object without a Timestamp) as "untimed". An untimed object has no +media time, is never media-stale, starts at the latest group, and is +forwarded as untimed. + +Tests: each receive path (lite before lite-05, IETF subgroup, fetch and +datagram) yields an untimed frame, and a relay forwards one untimed. + +Public API: breaking, on `dev` (`Frame`, `Datagram` and track info +timestamps become optional). Wire: no encoding change. Receive semantics on +published drafts change as the timestamp draft says. + +## Related + +- [IETF timestamp units](/quest/m1/ietf-timestamp-units.md) - drafts 14-16 stop sending timestamps without units, which then arrive untimed +- [Fetch without SUBSCRIBE](/quest/m1/ietf-fetch-only.md) - opting out of properties today falls back to arrival time +- [Publisher timeliness](/quest/m1/qos/publisher-timeliness.md) - relay ingest measures arrival minus timestamp, and must skip untimed frames +- [Plan: cache age-out](/quest/m1/cache-wall-eviction.md) - whatever it decides, untimed groups age out only through the pool's expiry +- [Translator](/quest/m1/rs2ts/translator.md) - flags the nested `Option` in first-start resolution this touches diff --git a/quest/m2/cmaf-frame-timestamp.md b/quest/m2/cmaf-frame-timestamp.md index 42abb97467f..f4fd8c18a0c 100644 --- a/quest/m2/cmaf-frame-timestamp.md +++ b/quest/m2/cmaf-frame-timestamp.md @@ -25,6 +25,9 @@ shifts `trun` data offsets. confirm with a test. - Check the TS exporter's verbatim carriage, which keeps PES PTS inside the payload. Rewrite those from the frame timestamp, or refuse a mismatch. +- An untimed CMAF frame is refused once the frame timestamp is the + fragment's timeline (decided 2026-10-02 12:43 +02:00, with + [moq-net carries untimed frames faithfully](/quest/m1/untimed-model.md)). - `main`: a behavior fix with no API change. Test: in Rust and JS, a fragment whose `tfdt` disagrees with its frame From 201d65f559ef5abe1ae4db65366a2d0524a5abd0 Mon Sep 17 00:00:00 2001 From: OneTooMany Date: Fri, 2 Oct 2026 12:53:30 +0200 Subject: [PATCH 3/9] quest: date the untimed decisions as the tree does Co-Authored-By: Claude Opus 5.5 --- quest/m1/js-untimed-model.md | 2 +- quest/m1/lite-untimed.md | 2 +- quest/m1/ts-pes-untimed.md | 2 +- quest/m1/untimed-model.md | 2 +- quest/m2/cmaf-frame-timestamp.md | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/quest/m1/js-untimed-model.md b/quest/m1/js-untimed-model.md index 43cd946bee1..0ef32d10d0e 100644 --- a/quest/m1/js-untimed-model.md +++ b/quest/m1/js-untimed-model.md @@ -10,7 +10,7 @@ timestamp stays untimed from publisher to consumer, and js/net never fills in ## Plan -Decided (2026-10-02 12:43 +02:00): a hand-written change now, rather than +Decided (2026-10-02): a hand-written change now, rather than waiting for [Generated @moq/net](/quest/m1/rs2ts/README.md). That line is long-running and would stall the JS timestamp quests. Whichever lands second absorbs the other. The semantics, the end-marker rule and the reasons are in diff --git a/quest/m1/lite-untimed.md b/quest/m1/lite-untimed.md index fe631a3ada4..a024e963b52 100644 --- a/quest/m1/lite-untimed.md +++ b/quest/m1/lite-untimed.md @@ -14,7 +14,7 @@ DATAGRAM Timestamp by one, so 0 means absent. An absent frame doesn't move the delta baseline. Rejected: a bare 0 as a sentinel, which collides with a real pts of 0. -Decided (2026-10-02 12:43 +02:00): one PR for both languages, after both +Decided (2026-10-02): one PR for both languages, after both model quests. Shipping one language first would break Rust-JS interop on lite-07-wip in between. diff --git a/quest/m1/ts-pes-untimed.md b/quest/m1/ts-pes-untimed.md index 19e66a2a245..05ec56aa107 100644 --- a/quest/m1/ts-pes-untimed.md +++ b/quest/m1/ts-pes-untimed.md @@ -8,7 +8,7 @@ case. `drafts/draft-lcurley-moq-mpegts.md` says the same. ## Plan -Decided (2026-10-02 12:43 +02:00): its own quest, because it lands +Decided (2026-10-02): its own quest, because it lands independently in a different crate. 0 is the sentinel that [moq-net carries untimed frames faithfully](/quest/m1/untimed-model.md) rejected, because it collides with a real pts of 0. diff --git a/quest/m1/untimed-model.md b/quest/m1/untimed-model.md index c08122b4828..0220dcfafcc 100644 --- a/quest/m1/untimed-model.md +++ b/quest/m1/untimed-model.md @@ -28,7 +28,7 @@ sentinel (collides with a real pts of 0), and `max_age` on max(wall, pts) (`max_age` stays media-time staleness, so a congestion stall can't age content out, and the pool's wall-clock expiry is the bound). -Decided (2026-10-02 12:43 +02:00, planning this split): +Decided (2026-10-02, planning this split): - An IETF object that carries its own TIMESCALE and Timestamp properties is timed, even when the track sent no TIMESCALE. The draft already allows an diff --git a/quest/m2/cmaf-frame-timestamp.md b/quest/m2/cmaf-frame-timestamp.md index f4fd8c18a0c..fb5f3a8d788 100644 --- a/quest/m2/cmaf-frame-timestamp.md +++ b/quest/m2/cmaf-frame-timestamp.md @@ -26,7 +26,7 @@ shifts `trun` data offsets. - Check the TS exporter's verbatim carriage, which keeps PES PTS inside the payload. Rewrite those from the frame timestamp, or refuse a mismatch. - An untimed CMAF frame is refused once the frame timestamp is the - fragment's timeline (decided 2026-10-02 12:43 +02:00, with + fragment's timeline (decided 2026-10-02, with [moq-net carries untimed frames faithfully](/quest/m1/untimed-model.md)). - `main`: a behavior fix with no API change. From ea150d032aff028078d8335af42a305f5bccd116 Mon Sep 17 00:00:00 2001 From: OneTooMany Date: Fri, 2 Oct 2026 13:02:20 +0200 Subject: [PATCH 4/9] quest: fold review findings into the untimed quests Co-Authored-By: Claude Opus 5.5 --- quest/m1/README.md | 2 +- quest/m1/plan-ts-pes-untimed.md | 40 +++++++++++++++++++++++++++++++++ quest/m1/ts-pes-untimed.md | 29 ------------------------ quest/m1/untimed-model.md | 19 +++++++++++----- 4 files changed, 54 insertions(+), 36 deletions(-) create mode 100644 quest/m1/plan-ts-pes-untimed.md delete mode 100644 quest/m1/ts-pes-untimed.md diff --git a/quest/m1/README.md b/quest/m1/README.md index b73842af4e8..8184d853a82 100644 --- a/quest/m1/README.md +++ b/quest/m1/README.md @@ -78,7 +78,7 @@ QUIC studies there on that rule. - [Untimed model](/quest/m1/untimed-model.md) - on dev, moq-net frames and datagrams carry an absent timestamp to every subscriber; no receiver fills in arrival time - [JS untimed model](/quest/m1/js-untimed-model.md) - the same in @moq/net - [Untimed lite-07](/quest/m1/lite-untimed.md) - lite-07 encodes an absent timestamp in both languages; lite-05/06 write send time -- [Untimed TS PES](/quest/m1/ts-pes-untimed.md) - a PES without a PTS imports untimed instead of at 0 +- [Plan: untimed verbatim PES](/quest/m1/plan-ts-pes-untimed.md) - decide how a verbatim TS track carries a PES that has no PTS, then write the implementation quest - [Data consumer timestamps](/quest/m1/data-consumer-timestamps.md) - json and binary consumers return each value's timestamp, in Rust and every binding; snapshots add `latest()` beside an in-order `next()` - [JS data consumer timestamps](/quest/m1/js-data-consumer-timestamps.md) - @moq/json and @moq/binary consumers return each value's timestamp, with snapshot `next()` and `latest()` - [Nested data configs](/quest/m1/data-config-nesting.md) - docs nest `BinaryConfig`/`JsonConfig` in an application section instead of flattening it diff --git a/quest/m1/plan-ts-pes-untimed.md b/quest/m1/plan-ts-pes-untimed.md new file mode 100644 index 00000000000..19e7079d02c --- /dev/null +++ b/quest/m1/plan-ts-pes-untimed.md @@ -0,0 +1,40 @@ +# [S] Plan: untimed verbatim PES + +## Goal + +Decide how a verbatim MPEG-TS track carries a PES that has no PTS without +inventing a time, from import through export. The usual case is asynchronous +private data such as KLV. The output is an implementation quest, or a +recorded reason to keep today's behaviour. + +## Plan + +Today the importer stamps a PES without a PTS at the track's live edge, or +at 0 before the first frame. `drafts/draft-lcurley-moq-mpegts.md` says 0. +Both are invented times, which +[moq-net carries untimed frames faithfully](/quest/m1/untimed-model.md) rules +out for moq-net itself. + +Making the moq-net timestamp optional isn't enough on its own. Verbatim +tracks use the legacy container, whose payload always starts with a +timestamp, and its decoder reads the time from there, not from the frame. So +the decision covers: + +- **Carriage:** either the legacy payload keeps a time, or verbatim tracks + move to a container that takes its time from the moq-net frame. Weigh what + the second costs existing catalogs and consumers of the `mpegts` section. +- **Export:** how the TS exporter schedules and writes an untimed PES, which + goes out with no PTS. +- **Stream types:** which ones this applies to. A media elementary stream + may split an access unit across PES packets or infer timing. There, a + missing PTS doesn't mean untimed. + +The maintainer decides the carriage, because it touches the catalog and +container contract. + +Test plan for the implementation quest: importer to encoded track to +exporter, for a PES without a PTS and for one with a real PTS of 0. + +## Required + +- [moq-net carries untimed frames faithfully](/quest/m1/untimed-model.md) - a frame must be able to carry no timestamp diff --git a/quest/m1/ts-pes-untimed.md b/quest/m1/ts-pes-untimed.md deleted file mode 100644 index 05ec56aa107..00000000000 --- a/quest/m1/ts-pes-untimed.md +++ /dev/null @@ -1,29 +0,0 @@ -# [XS] A PES without a PTS imports untimed - -## Goal - -The MPEG-TS importer publishes a PES that carries no PTS as an untimed frame, -instead of stamping it 0. Asynchronous private data such as KLV is the usual -case. `drafts/draft-lcurley-moq-mpegts.md` says the same. - -## Plan - -Decided (2026-10-02): its own quest, because it lands -independently in a different crate. 0 is the sentinel that -[moq-net carries untimed frames faithfully](/quest/m1/untimed-model.md) -rejected, because it collides with a real pts of 0. - -Check which stream types this applies to. A media elementary stream may -legitimately split an access unit across PES packets, or infer timing. -There, "no PTS" doesn't mean untimed, and the importer's existing handling -stays. Check the TS exporter for the reverse: an untimed frame should go out -as a PES with no PTS. - -Test: a private-data PES with no PTS imports untimed and exports back -without a PTS. - -Public API: none. Wire: the mpegts draft's mapping of a missing PTS changes. - -## Required - -- [moq-net carries untimed frames faithfully](/quest/m1/untimed-model.md) - a frame must be able to carry no timestamp diff --git a/quest/m1/untimed-model.md b/quest/m1/untimed-model.md index 0220dcfafcc..bbc62a4c3e0 100644 --- a/quest/m1/untimed-model.md +++ b/quest/m1/untimed-model.md @@ -55,9 +55,13 @@ Things to look out for: - Several model decisions read "has a timestamp" as "has a frame". `group.rs`'s `poll_timestamp` is the clearest case, and `GroupExpiry` uses it. An untimed frame must still count as a frame there. -- Live edge, reach and successor search (`track.rs`, `resume.rs`) skip - unstamped groups. Check that an untimed track neither stalls a cursor nor - replays everything. +- The live edge skips unstamped groups. Reach and successor search + (`track.rs`, `resume.rs`) deliberately stop at the immediate successor and + leave the bound unknown while it is unstamped, because skipping ahead + could expire content that is still valid. An untimed successor keeps that + bound unknown for good, so the timed group before it is kept. Preserve + that, and check that an untimed track neither stalls a cursor nor replays + everything. Cover tracks that mix timed and untimed groups. - `track::Info.timescale` always has a value today. A relay subscribed to an IETF track without TIMESCALE currently announces one downstream. It must not claim a timeline the source never had. @@ -73,9 +77,12 @@ Things to look out for: Media consumers that need a time refuse an untimed frame, except for the end-marker rule above. -- FETCH_OK properties are discarded today - ([FETCH_OK properties](/quest/m1/fetch-ok-properties.md)), so fetched IETF - objects arrive untimed until that lands. That's correct, not a regression. +- A joining or fill FETCH takes its units from SUBSCRIBE_OK and keeps its + timestamps. Only objects with neither track units nor object-level units + are untimed. A standalone FETCH has no units until + [FETCH_OK properties](/quest/m1/fetch-ok-properties.md) lands, so its + objects arrive untimed until then. Test that known units still yield + timestamps, alongside the untimed cases. Interop facts (2026-10-02): From 5b86fc11876686c8f3ef089eb0b18d9a7f47d90b Mon Sep 17 00:00:00 2001 From: OneTooMany Date: Fri, 2 Oct 2026 13:11:11 +0200 Subject: [PATCH 5/9] quest: keep object-level units on a standalone FETCH Co-Authored-By: Claude Opus 5.5 --- quest/m1/untimed-model.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/quest/m1/untimed-model.md b/quest/m1/untimed-model.md index bbc62a4c3e0..83b0f159597 100644 --- a/quest/m1/untimed-model.md +++ b/quest/m1/untimed-model.md @@ -79,10 +79,11 @@ Things to look out for: end-marker rule above. - A joining or fill FETCH takes its units from SUBSCRIBE_OK and keeps its timestamps. Only objects with neither track units nor object-level units - are untimed. A standalone FETCH has no units until - [FETCH_OK properties](/quest/m1/fetch-ok-properties.md) lands, so its - objects arrive untimed until then. Test that known units still yield - timestamps, alongside the untimed cases. + are untimed. A standalone FETCH has no track units until + [FETCH_OK properties](/quest/m1/fetch-ok-properties.md) lands, so until + then only its objects with object-level units stay timed. Test that known + units, from the track or the object, still yield timestamps, alongside the + untimed cases. Interop facts (2026-10-02): From 0e2b06998aadbb0f4d7314e8c879d619e23d8184 Mon Sep 17 00:00:00 2001 From: OneTooMany Date: Fri, 2 Oct 2026 13:16:32 +0200 Subject: [PATCH 6/9] quest: fold the latest review notes into the untimed quests Co-Authored-By: Claude Opus 5.5 --- quest/m1/js-untimed-model.md | 4 ++++ quest/m1/untimed-model.md | 20 ++++++++++++-------- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/quest/m1/js-untimed-model.md b/quest/m1/js-untimed-model.md index 0ef32d10d0e..032ae83c6c9 100644 --- a/quest/m1/js-untimed-model.md +++ b/quest/m1/js-untimed-model.md @@ -23,6 +23,10 @@ Things to look out for: - The track's drift, reach, staleness and expiry logic skips unstamped groups the way Rust does. Check that untimed frames still count as frames, and that an untimed track starts at the latest group. +- A track's `Info.timescale` is required today, with a default. Like Rust, + a track that never declared one must not claim a timeline downstream. +- A FETCH keeps timestamps whenever the track or the object gives units. + Only objects with neither are untimed. - `Frame.timestamp` and `Datagram.timestamp` become optional. Update callers in js/hang and js/loc (end markers) and anything in js/watch that reads them. diff --git a/quest/m1/untimed-model.md b/quest/m1/untimed-model.md index 83b0f159597..3a628e10e0c 100644 --- a/quest/m1/untimed-model.md +++ b/quest/m1/untimed-model.md @@ -54,7 +54,10 @@ Things to look out for: - Several model decisions read "has a timestamp" as "has a frame". `group.rs`'s `poll_timestamp` is the clearest case, and `GroupExpiry` uses - it. An untimed frame must still count as a frame there. + it. An untimed frame must still count as a frame there. Key "has + presented" off frames written (or fin), and keep the group's + `Option` for media time only, so an open group and a group of + untimed frames don't share one `None`. - The live edge skips unstamped groups. Reach and successor search (`track.rs`, `resume.rs`) deliberately stop at the immediate successor and leave the bound unknown while it is unstamped, because skipping ahead @@ -77,13 +80,14 @@ Things to look out for: Media consumers that need a time refuse an untimed frame, except for the end-marker rule above. -- A joining or fill FETCH takes its units from SUBSCRIBE_OK and keeps its - timestamps. Only objects with neither track units nor object-level units - are untimed. A standalone FETCH has no track units until - [FETCH_OK properties](/quest/m1/fetch-ok-properties.md) lands, so until - then only its objects with object-level units stay timed. Test that known - units, from the track or the object, still yield timestamps, alongside the - untimed cases. +- Only objects with neither track units nor object-level units are untimed. + A FETCH learns track units from SUBSCRIBE_OK (a joining or fill FETCH), + from TRACK_STATUS ([Fetch without SUBSCRIBE](/quest/m1/ietf-fetch-only.md)), + or from [FETCH_OK properties](/quest/m1/fetch-ok-properties.md), whichever + applies. Until those last two land, a standalone FETCH keeps timestamps + only on objects that carry their own units. Test that known units, from + the track or the object, still yield timestamps, alongside the untimed + cases. Interop facts (2026-10-02): From 18b7d935b98309a156ebf337b42917325fd01951 Mon Sep 17 00:00:00 2001 From: OneTooMany Date: Fri, 2 Oct 2026 13:28:15 +0200 Subject: [PATCH 7/9] quest: lite-07 refuses the timestamps the shift can't encode Co-Authored-By: Claude Opus 5.5 --- quest/m1/lite-untimed.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/quest/m1/lite-untimed.md b/quest/m1/lite-untimed.md index a024e963b52..b97f81ff8c3 100644 --- a/quest/m1/lite-untimed.md +++ b/quest/m1/lite-untimed.md @@ -27,6 +27,11 @@ Draft work: valid", and expiration's "reach" (the first frame timestamp of the next group) for an untimed group. - Check how a lite track that has no timescale says so on lite-07. +- lite-07 varints carry the full 64 bits, so the shift can't represent the + top value: a DATAGRAM Timestamp of 2^64-1, or a delta whose zigzag + encoding is 2^64-1. Wrapping would turn either into the absence marker. + Recommended: the encoder refuses those values and the draft says so, + rather than widening the encoding for timestamps no real track reaches. Test: an untimed frame and an untimed datagram round-trip Rust-to-JS and JS-to-Rust on lite-07. lite-06 still receives a timestamp. Run From 4d5ce38e82a8afd6db1981ae6c40790d92821c47 Mon Sep 17 00:00:00 2001 From: OneTooMany Date: Fri, 2 Oct 2026 14:09:45 +0200 Subject: [PATCH 8/9] quest: split the broadcast-clock input out of publish-timestamp Co-Authored-By: Claude Opus 5.5 --- quest/m1/README.md | 3 +- quest/m1/mux-data-timestamp.md | 58 ++++++++++++++++++++++++++++++++++ quest/m1/publish-timestamp.md | 28 ++++++---------- 3 files changed, 70 insertions(+), 19 deletions(-) create mode 100644 quest/m1/mux-data-timestamp.md diff --git a/quest/m1/README.md b/quest/m1/README.md index 8184d853a82..7b9ab914071 100644 --- a/quest/m1/README.md +++ b/quest/m1/README.md @@ -73,7 +73,8 @@ QUIC studies there on that rule. - [Auth client CA](/quest/m1/relay-auth-client-ca.md) - on dev, `auth::Config::validate` and `init` take the client-CA flag, so no caller can skip the check - [Data track clock](/quest/m1/data-track-clock.md) - JSON and binary data tracks stamp on the catalog's clock at write time, matching the media's anchored clock - [Go and Dart doc samples](/quest/m1/doc-samples-go-dart.md) - Go and Dart doc samples compile against their wrappers -- [Publishing never invents a timestamp](/quest/m1/publish-timestamp.md) - no Rust or binding publish API fills in a timestamp; an untimed payload goes out untimed, and moq-mux data producers take broadcast-clock timestamps +- [Mux data timestamp](/quest/m1/mux-data-timestamp.md) - on dev, moq-mux JSON and binary producers take a broadcast-clock timestamp, with no ahead-of-now refusal +- [Publishing never invents a timestamp](/quest/m1/publish-timestamp.md) - no Rust or binding publish API fills in a timestamp; an untimed payload goes out untimed - [JS publishing never invents a timestamp](/quest/m1/js-publish-timestamp.md) - the same in @moq/json, @moq/binary, and @moq/net - [Untimed model](/quest/m1/untimed-model.md) - on dev, moq-net frames and datagrams carry an absent timestamp to every subscriber; no receiver fills in arrival time - [JS untimed model](/quest/m1/js-untimed-model.md) - the same in @moq/net diff --git a/quest/m1/mux-data-timestamp.md b/quest/m1/mux-data-timestamp.md new file mode 100644 index 00000000000..20b2341d27a --- /dev/null +++ b/quest/m1/mux-data-timestamp.md @@ -0,0 +1,58 @@ +# [S] moq-mux data producers take a broadcast-clock timestamp + +## Goal + +A moq-mux JSON or binary data producer accepts a timestamp on the broadcast +clock and publishes it as given, with no ahead-of-now refusal. That timestamp +may come from a `clock::Lane` on the broadcast's `Anchor` (KLV next to video +from one MPEG-TS program), or from the `at` of a consumer's `Timed` the value +was derived from (MAVLink telemetry republished). Callers that hold a capture +`Instant` convert it with a public `Clock::capture`. + +Out of scope: what `at: None` means, which +[Publishing never invents a timestamp](/quest/m1/publish-timestamp.md) +changes, along with moq-json, moq-binary/moq-flate, moq-ffi and the +bindings. + +## Plan + +Requested by OneTooMany (Discord), who align KLV and MAVLink-derived +telemetry with video. Split out of `publish-timestamp` (2026-10-02) because +it needs neither untimed frames nor the FFI namespace move, so it can land +well before the rest. + +Decided (2026-10-02): + +- `Snapshot::update` / `Stream::append` in `json` and `binary` take + `Timed<_, Timestamp>` instead of `Timed<_, Instant>`. That's the same + contract as `container::Frame`, and the default `Timed` that moq-json and + moq-flate already take. `at: None` keeps stamping the broadcast clock's now + until `publish-timestamp` makes it untimed. +- `Clock::capture(Instant)` becomes public. It keeps refusing an instant + ahead of now or before the epoch (`Error::InvalidCapture`). +- Carried over from `publish-timestamp` (2026-10-01): a timestamp ahead of + now is recorded as zero delay rather than refused. A source clock running + slightly fast must not be rejected. + +Things to look out for: + +- The delay/jitter estimator takes a signed lateness. An ahead-of-now sample + can drag the broadcast-wide minimum down and inflate delay or jitter on + other tracks. Clamp it where it is sampled, and test it. +- Callers inside the repository (moq-ffi, moq-c) pass bare values and + should compile unchanged. The changes are moq-mux's own tests and doc + examples that use `.at(Instant)`. +- Document on the producers that a carried-over timestamp lines up with + media only on a broadcast sharing the source's clock mapping. Update + `doc/lib/rs/moq-mux.md`, which describes the `Instant` input and the + refusal. + +Test: a timestamp from a `Lane` on the catalog's `Anchor` is published +unchanged. A timestamp ahead of now is accepted, and other tracks' delay +doesn't move. + +Public API: breaking, on `dev`. Wire: none. + +## Related + +- [Publishing never invents a timestamp](/quest/m1/publish-timestamp.md) - changes what `at: None` means for these producers, after this lands diff --git a/quest/m1/publish-timestamp.md b/quest/m1/publish-timestamp.md index 1653528201e..14c8e6209a8 100644 --- a/quest/m1/publish-timestamp.md +++ b/quest/m1/publish-timestamp.md @@ -1,4 +1,4 @@ -# [L] Publishing never invents a timestamp +# [M] Publishing never invents a timestamp ## Goal @@ -29,21 +29,11 @@ fill `Timestamp::now()` (moq-net's clock, not the broadcast's), and moq-mux's (see [Data consumer timestamps](/quest/m1/data-consumer-timestamps.md)). - moq-json (snapshot, stream, and window, whose `push` always stamps now) and moq-binary producers take `Timed<_, Timestamp>` and write `None` as untimed. -- moq-mux data producers take `Timed<_, Timestamp>` on the broadcast clock, - the same contract as `container::Frame`, with no ahead-of-now check. A - caller already holding a broadcast-clock timestamp passes it as is: one - returned by a `clock::Lane` on the broadcast's `Anchor` (video + KLV from one - MPEG-TS program), or the `at` of a consumer's `Timed` it derived the value - from (MAVLink telemetry republished). A source clock running slightly fast - must not be refused. -- `Clock::capture(Instant)` becomes public for callers holding a capture - `Instant`; it keeps refusing one ahead of now or before the epoch - (`Error::InvalidCapture`). `Clock::stamp` is deleted. - `catalog::data::Listing::record` samples `delay` and `jitter` only for timed - writes, and records zero delay for a timestamp ahead of now rather than - refusing it. -- Document on the moq-mux producers that a carried-over timestamp lines up - with media only on a broadcast sharing the source's clock mapping. +- moq-mux data producers already take `Timed<_, Timestamp>` on the broadcast + clock after [moq-mux data producers take a broadcast-clock timestamp](/quest/m1/mux-data-timestamp.md). + Here `None` stops meaning the clock's now and goes out untimed. + `Clock::stamp` is deleted. `catalog::data::Listing::record` samples `delay` + and `jitter` only for timed writes. - Publishers inside the repository (hang catalog snapshots, MSF, stats, room chat, examples) pass their clock's now explicitly. - moq-ffi: data producers take an optional timestamp in microseconds on the @@ -61,14 +51,16 @@ fill `Timestamp::now()` (moq-net's clock, not the broadcast's), and moq-mux's - Docs: "stamped when written" in `doc/lib/rs/{moq-json,moq-binary}.md` and `rs/moq-net/src/model/timed.rs`, plus `doc/lib/{py,swift,kt,go,dart}`. -The broadcast-clock input was requested by OneTooMany, who align KLV and -MAVLink-derived telemetry with video. +The broadcast-clock input, requested by OneTooMany, was split out into +[moq-mux data producers take a broadcast-clock timestamp](/quest/m1/mux-data-timestamp.md) (2026-10-02), +because it needs neither blocker below. Public API: breaking, on `dev`. Wire: none here; absence on the wire lands with the untimed implementation quests. ## Required +- [moq-mux data producers take a broadcast-clock timestamp](/quest/m1/mux-data-timestamp.md) - changes the same producers' input type first, so the two signature changes land in order - [JSON and flate namespaces](/quest/m1/ffi-shape/json.md) - moves the data producers this changes, so the two breaks land in order rather than colliding - [moq-net carries untimed frames faithfully](/quest/m1/untimed-model.md) - the model must hold an untimed payload before producers stop filling in now; until lite-07 encodes absence, a lite encoder writes its send time, as producers effectively do today From 23c9af5da38519cf01a40306d771521594276a85 Mon Sep 17 00:00:00 2001 From: OneTooMany Date: Fri, 2 Oct 2026 14:36:56 +0200 Subject: [PATCH 9/9] quest: judge ahead-of-now data timestamps against the catalog clock Co-Authored-By: Claude Opus 5.5 --- quest/m1/mux-data-timestamp.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/quest/m1/mux-data-timestamp.md b/quest/m1/mux-data-timestamp.md index 20b2341d27a..fa8850e21f0 100644 --- a/quest/m1/mux-data-timestamp.md +++ b/quest/m1/mux-data-timestamp.md @@ -36,9 +36,11 @@ Decided (2026-10-02): Things to look out for: -- The delay/jitter estimator takes a signed lateness. An ahead-of-now sample - can drag the broadcast-wide minimum down and inflate delay or jitter on - other tracks. Clamp it where it is sampled, and test it. +- An ahead-of-now sample can drag the broadcast-wide delay minimum down and + inflate delay or jitter on other tracks. Decide "ahead of now" against the + catalog clock's current reading. Don't clamp the estimator's signed + lateness itself: it is measured from an arbitrary local epoch, where valid + samples can be negative. Test it. - Callers inside the repository (moq-ffi, moq-c) pass bare values and should compile unchanged. The changes are moq-mux's own tests and doc examples that use `.at(Instant)`.