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
9 changes: 5 additions & 4 deletions quest/m1/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,10 +72,11 @@ 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 requires a timestamp](/quest/m1/publish-timestamp.md) - every Rust and binding publish API takes a timestamp; nothing fills in now for the caller
- [JS publishing requires a timestamp](/quest/m1/js-publish-timestamp.md) - the same in @moq/json, @moq/binary, and @moq/net
- [Plan: untimed peer objects](/quest/m1/plan-untimed-objects.md) - decide what a frame's timestamp is when a moq-transport or pre-lite-05 peer sends none
- [Data consumer timestamps](/quest/m1/data-consumer-timestamps.md) - json and binary consumers return each value's timestamp, in Rust and every binding
- [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
- [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
- [Omit empty catalog sections](/quest/m1/catalog-omit-empty.md) - a Rust catalog with no video or audio leaves those keys out, as JS does
- [Delete SourceMap](/quest/m1/source-map-removal.md) - on dev, `Anchor` with its `Lane`s is the one way onto the broadcast clock
Expand Down
37 changes: 25 additions & 12 deletions quest/m1/data-consumer-timestamps.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,24 +15,37 @@ sync with video.

Every `moq_net::Frame` has a timestamp, but the consumers decode only
`frame.payload` and drop it: `moq_mux::{json,binary}::Consumer::next` and the
moq-json and moq-binary snapshot and stream consumers they wrap. A snapshot
consumer returns the timestamp of the frame it decoded last.
moq-json and moq-binary snapshot and stream consumers they wrap. Each snapshot
state carries the timestamp of the frame that produced it.

Decided: `next()` and `poll_next()` return `Timed<T>`, the type the producers
take, whose `at` becomes required in
[Publishing requires a timestamp](/quest/m1/publish-timestamp.md). The
timestamp is the frame's media timestamp on the track's timescale.
[Plan: untimed peer objects](/quest/m1/plan-untimed-objects.md) decides
whether a peer can deliver a frame without one; if it can, `at` becomes an
`Option` here, so this waits for it and breaks once.
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.

Decided (2026-10-01): snapshot consumers get two reads, both returning
`Timed<T>`. Today the moq-json snapshot consumer applies every buffered delta
but yields only the newest state, and moq-binary jumps to the newest group, so
a 9s state is lost when 11s is already buffered. A caller syncing to a
playhead needs the newest state at or before it.

- `next()` (and `poll_next()`) yields every state in order, like a stream
consumer. A reader that falls behind the drift budget (`Lagged`) still
jumps to the newest group, so a slow reader stays bounded.
- `latest()` (and `poll_latest()`) skips to the newest state, today's
behavior, kept as the optimization for callers that only want the current
value.
- Same names in the moq-mux wrappers, moq-ffi, and every binding.

moq-ffi's json/binary consumers return the timestamp too, and the py, swift,
kt, go, and dart wrappers and `doc/lib/*` follow. JS readers stay with
[Data sync in watch](/quest/m3/watch-data-sync.md).
kt, go, and dart wrappers and `doc/lib/*` follow. JS is
[JS data consumer timestamps](/quest/m1/js-data-consumer-timestamps.md).

Public API: breaking, on `dev`. Wire: none.

## Required

- [Publishing requires a timestamp](/quest/m1/publish-timestamp.md) - makes `Timed.at` required, the type this returns
- [Plan: untimed peer objects](/quest/m1/plan-untimed-objects.md) - decides whether the returned timestamp can be absent
- [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
34 changes: 34 additions & 0 deletions quest/m1/js-data-consumer-timestamps.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# [M] JS data consumers return each value's timestamp

## Goal

`@moq/json` and `@moq/binary` snapshot and stream consumers return each value
with its frame's timestamp, so a browser can hold telemetry back until the
video playhead reaches it. Mirrors
[Data consumer timestamps](/quest/m1/data-consumer-timestamps.md) in Rust.

## Plan

Requested by OneTooMany, whose web frontend shows KLV and MAVLink telemetry
beside video and today reads raw frames to recover the timestamps.

Decided (2026-10-01): `next()` and the async iterator yield `@moq/net`'s
`Timed<T>` (`{ value, at? }`), the type the producers take after
[JS publishing never invents a timestamp](/quest/m1/js-publish-timestamp.md).
`at` is the frame's timestamp, absent for an untimed frame. Snapshot
consumers get the same two reads as Rust: `next()` yields every state in
order (`@moq/json` stops draining to the latest, `@moq/binary` reads groups in
order), and `latest()` skips to the newest state, today's behavior. A reader
that falls behind still jumps to the newest group. Update
`doc/lib/js/{json,binary}.md`.

Public API: breaking, on `dev`. Wire: none.

## Required

- [JS publishing never invents a timestamp](/quest/m1/js-publish-timestamp.md) - adds `Timed<T>` to `@moq/net`
- [Plan: untimed objects](/quest/m1/plan-untimed-objects.md) - js/net stops filling arrival time, so `at` can be absent

## Related

- [Synced data playback](/quest/m2/watch-data-sync.md) - the js/watch reader built on these timestamps
26 changes: 18 additions & 8 deletions quest/m1/js-publish-timestamp.md
Original file line number Diff line number Diff line change
@@ -1,18 +1,28 @@
# [M] JS publishing requires a timestamp
# [M] JS publishing never invents a timestamp

## Goal

`@moq/json`, `@moq/binary`, and `@moq/net` publish APIs take a timestamp and
never fill in `Timestamp.now()` for the caller, mirroring
[Publishing requires a timestamp](/quest/m1/publish-timestamp.md).
`@moq/json`, `@moq/binary`, and `@moq/net` publish APIs never fill in
`Timestamp.now()` for the caller, mirroring
[Publishing never invents a timestamp](/quest/m1/publish-timestamp.md). A
payload published without a timestamp goes out untimed.

## Plan

Today the json and binary snapshot and stream producers default
`at = Timestamp.now()`, the json window producer always uses now, and js/net's
`writeString`, `writeJson`, and `writeBool` stamp now. Make the timestamp a
required argument on each, with the same shape as Rust's `Timed`. Callers in
the repository (js/hang catalog, js/publish, js/room) pass their clock's now
explicitly. Update `doc/lib/js/{json,binary,net}.md`.
`writeString`, `writeJson`, and `writeBool` stamp now.

Decided (2026-10-01): `@moq/net` exports `Timed<T>`
(`{ value: T; at?: Time.Timestamp }`), the Rust name and shape. Each producer
takes a `Timed<T>`; an absent `at` is untimed, never now. The consumers return
the same type in
[JS data consumer timestamps](/quest/m1/js-data-consumer-timestamps.md).
Callers in the repository (js/hang catalog, js/publish, js/room) pass their
clock's now explicitly. Update `doc/lib/js/{json,binary,net}.md`.

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
57 changes: 30 additions & 27 deletions quest/m1/plan-untimed-objects.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
# [S] Plan: untimed peer objects
# [S] Plan: untimed objects

## Goal

Decide what a frame's timestamp is when a peer sends none, and rewrite this
quest into implementation quests. That covers 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. The decision must not invent a timeline that
changes with the relay path.
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

Expand All @@ -17,30 +17,33 @@ 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.

Settled elsewhere: publishing always requires a timestamp
([Publishing requires a timestamp](/quest/m1/publish-timestamp.md)), and
lite-05 and lite-06 (published) can't encode an absent one.

Candidates:

- Faithful absence (leaning): the model and consumers carry
`Option<Timestamp>` so a relay forwards "none" as none. lite-07-wip encodes
it by shifting the FRAME Timestamp Delta and the DATAGRAM Timestamp by one
(0 = absent); an absent frame doesn't move the delta baseline. A lite-05/06
encoder writes its send time. 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.
- First-hop arrival, documented: no change, but the failover jump stays.
- 0 as a sentinel: rejected. It collides with a real pts of 0.
- `max_age` on max(wall, pts): the maintainer asked. 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`).
Decided (2026-10-01): faithful absence, on both the publish and the subscribe
side. The model and every consumer carry `Option<Timestamp>`, 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, and updates to
[Data consumer timestamps](/quest/m1/data-consumer-timestamps.md), whose
return type depends on this, and to the timestamp and lite drafts.
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.
77 changes: 45 additions & 32 deletions quest/m1/publish-timestamp.md
Original file line number Diff line number Diff line change
@@ -1,61 +1,74 @@
# [L] Publishing requires a timestamp
# [L] Publishing never invents a timestamp

## Goal

Every publish API takes a timestamp, and nothing below it invents one. A
frame's timestamp is the publisher's statement of when the content happened.
A library never fills in "now" on the caller's behalf. Covers moq-net,
No publish API fills in a timestamp the caller didn't give. A frame's
timestamp is the publisher's statement of when the content happened; a payload
published without one goes out untimed and arrives untimed. Covers moq-net,
moq-json, moq-binary, moq-mux, moq-ffi, and every binding wrapper (Python,
Swift, Kotlin, Go, Dart). Not libmoq, which the
[Generated C bindings](/quest/m1/c/README.md) replace.

## Plan

Decided (2026-09-30): the wire timestamp stays required, so the publish side
must be too. Today `Timed.at: None` means "stamp when written": moq-json and
moq-binary fill `Timestamp::now()` (moq-net's clock, not the broadcast's), and
moq-mux's `Clock::stamp` fills the broadcast clock's `now()`. The FFI raw
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
removes every place that fills one in.

Today `Timed.at: None` means "stamp when written": moq-json and moq-binary
fill `Timestamp::now()` (moq-net's clock, not the broadcast's), and moq-mux's
`Clock::stamp` fills the broadcast clock's `now()`. The FFI raw
`MoqFrame.timestamp_us` and `MoqDatagram.timestamp_us` default to 0.

- `moq_net::Timed<P, T>` keeps its name and gets a required `at: T`, with no
`From<bare payload>`. The json/binary consumers return the same type (see
[Data consumer timestamps](/quest/m1/data-consumer-timestamps.md)).
- `moq_net::Timed<P, T>` keeps its name and its `at: Option<T>`; only the
meaning of `None` changes. The json/binary consumers return the same type
(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>`. The moq-mux data producers
take `Timed<_, Instant>` and map it onto the broadcast clock, refusing one
ahead of now (`Error::InvalidCapture`), as today; `Clock::stamp` stops
filling `None`. `catalog::data::Listing::record` then always has a capture
time for `delay` and `jitter`.
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.
- Publishers inside the repository (hang catalog snapshots, MSF, stats, room
chat, examples) pass their clock's now explicitly.
- moq-ffi: the
capture time is a media timestamp on the broadcast's timeline. moq-ffi
exposes the broadcast clock's `now()` as a timestamp; callers stamp payloads
with values taken from it, and moq refuses one ahead of now. That keeps a
device or process clock out, as the Rust `Instant` mapping does. Either map
the timestamp back through the clock inside moq-ffi or give the clock a
typed timestamp moq-mux accepts; keep a raw `Timestamp` from compiling
there. The json/binary `update` and `append` take it as a required
argument, and the raw frame and datagram records lose their `default = 0`.
- 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`.
- 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
them. Add hand-written binary wrappers there. The maintainer asked for this.
Each wrapper gets a test that a past capture time is accepted and a future
one refused.
Each wrapper gets a test that a timestamp round-trips and an untimed payload
arrives untimed.
- 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}`.

What a receiver fills in when a peer sends no timestamp is out of scope:
[Plan: untimed peer objects](/quest/m1/plan-untimed-objects.md).
The broadcast-clock input was requested by OneTooMany, who align KLV and
MAVLink-derived telemetry with video.

Public API: breaking, on `dev`. Wire: none.
Public API: breaking, on `dev`. Wire: none here; absence on the wire lands
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

## Related

- [Data track clock](/quest/m1/data-track-clock.md) - the moq-mux mapping this keeps reads the catalog's clock at write time
- [JS publishing requires a timestamp](/quest/m1/js-publish-timestamp.md) - the same change in the JS packages
- [JS publishing never invents a timestamp](/quest/m1/js-publish-timestamp.md) - the same change in the JS packages
1 change: 1 addition & 0 deletions quest/m2/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ waiting on an upstream release waits in [m4](/quest/m4/README.md).
- [One port](/quest/m2/one-port/README.md) - a relay speaks QUIC and STUN on one UDP port and HTTP, RTMP, and RTMPS on one TCP port; WebRTC media is an embedder hook
- [Ladder](/quest/m2/ladder/README.md) - a transcode ladder adapts to the uplink it publishes over, instead of encoding every live rung at its ceiling
- [Processor](/quest/m2/processor/README.md) - a customer-run worker publishes an on-demand contribution under its own service prefix with scoped access
- [Synced data playback](/quest/m2/watch-data-sync.md) - js/watch releases JSON and binary payloads on the media playhead, and a slow data track holds media back
- [Stream sessions](/quest/m2/uring-tcp/README.md) - serve WebSocket and HTTP from the io_uring workers, where io_uring pays off most
- [fMP4 emsg](/quest/m2/emsg.md) - settles the shared framing and missing-data semantics before this section adopts them
- [ID3 catalog section](/quest/m2/id3.md) - timed ID3 as a first-class container-neutral catalog section
Expand Down
Loading
Loading