diff --git a/quest/m1/README.md b/quest/m1/README.md index c9111dbf1bd..19f269979f6 100644 --- a/quest/m1/README.md +++ b/quest/m1/README.md @@ -72,9 +72,13 @@ QUIC studies there on that rule. - [Capture by default](/quest/m1/capture-default.md) - moq-video and moq-audio build `capture` by default, so pre-merge checks test it and the capture gate goes away - [Ship capture and playback](/quest/m1/cli-packaging.md) - a released moq binary can capture and play, which no distribution currently enables - [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) - 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 -- [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) - 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 +- [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/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 aeefb0ec23e..ee2612975be 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. 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 bfd1b95fc43..7565dd92964 100644 --- a/quest/m1/js-data-consumer-timestamps.md +++ b/quest/m1/js-data-consumer-timestamps.md @@ -27,7 +27,7 @@ Public API: breaking. 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 c0b96672882..fd5fe7cc0d9 100644 --- a/quest/m1/js-publish-timestamp.md +++ b/quest/m1/js-publish-timestamp.md @@ -25,4 +25,4 @@ Public API: breaking. 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..c5d8dc6a5fb --- /dev/null +++ b/quest/m1/js-untimed-model.md @@ -0,0 +1,42 @@ +# [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): 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. +- 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. + +Test: an untimed frame survives a JS subscribe on each receive path. Run +`just test interop --all`. + +Public API: breaking. 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..b97f81ff8c3 --- /dev/null +++ b/quest/m1/lite-untimed.md @@ -0,0 +1,45 @@ +# [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): 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. +- 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 +`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/mux-data-timestamp.md b/quest/m1/mux-data-timestamp.md new file mode 100644 index 00000000000..e4bb41ddf27 --- /dev/null +++ b/quest/m1/mux-data-timestamp.md @@ -0,0 +1,60 @@ +# [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 be the source's own timestamp on a catalog clock anchored to that source +(KLV next to video from one MPEG-TS program), or 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-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: + +- 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)`. +- 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 source timestamp on a catalog clock anchored to that source is +published unchanged. A timestamp ahead of now is accepted, and other tracks' delay +doesn't move. + +Public API: breaking. 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/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/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 86349bc552f..e97782e94f9 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 @@ -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-flate @@ -28,27 +29,19 @@ 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-flate 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: the - source's own timestamp on a catalog clock anchored to that source (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 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` (now @@ -58,16 +51,18 @@ 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-flate}.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. 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 -- [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/untimed-model.md b/quest/m1/untimed-model.md new file mode 100644 index 00000000000..49eb30db460 --- /dev/null +++ b/quest/m1/untimed-model.md @@ -0,0 +1,121 @@ +# [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, 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. 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 + 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. +- 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. +- 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): + +- 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 (`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..fb5f3a8d788 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, 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