Skip to content
Merged
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
97 changes: 51 additions & 46 deletions quest/m1/jitter-flush-clock.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,67 +38,72 @@ media span of each emitted batch and advertise no `delay`.
`flush` but no discontinuity, so a binding publisher that pauses and resumes
on a re-anchored PTS within the window would count the pause; add one when
such a caller appears.
- **Measurement.** `catalog::Estimator` gains an additive `flush(timestamp,
now)` observation next to the clock-free `write`. Lateness is
`now - timestamp`. Each rendition keeps its own baseline, the minimum
lateness over a sliding window (about 10 s) rather than the lifetime
minimum, so a media clock that drifts slower than wall time does not ratchet
forever. The broadcast baseline, on `catalog::Producer` and shared by every
rendition, is the minimum of those. `delay` is the rendition baseline minus
the broadcast baseline; `jitter` is `lateness - rendition baseline`. Each is
reported as its lifetime maximum.
- **Measurement.** Lateness is `now - timestamp`, observed by the existing
`flush` calls, so no call site changes. Each rendition keeps its own
baseline, the minimum lateness over a sliding window (about 10 s), so a media
clock that drifts slower than wall time does not ratchet forever. The
broadcast baseline, on `catalog::Producer` and shared by every rendition
that flushes, is the minimum of those; the per-rendition `Baseline` epochs
must become one shared epoch for the subtraction to mean anything. `delay` is
the rendition baseline minus the broadcast baseline, reported as its lifetime
maximum. Renditions that never flush advertise no `delay`.
- **Open:** lifetime maxima taken against a sliding baseline stop sharing an
origin when the earliest rendition changes. If A starts at 0 and B at
200 ms, B keeps `delay: 200`; if A then drifts to 500 ms, A advertises 300
and `Sync` computes `300 - 200 = 100` while the tracks are 300 ms apart.
Settle a fixed common origin, coordinated rebasing, or no subtraction
before implementing.
Options:
- *No subtraction (recommended).* Keep the sliding baselines and never-lower,
and change the player rule to `max(delay + jitter)` over the subscribed
renditions, dropping `- min(delay)`. The subscribed renditions' true spread
is measured from an earliest subscribed baseline no earlier than the
broadcast baseline, so it never exceeds the largest advertised `delay`,
and the catalog alone can never under-buffer. The cost is over-buffering by
`min(delay)` when the broadcast's earliest rendition is not subscribed
(a video-only viewer of a broadcast whose audio leads by 200 ms pays
200 ms). Dropping a slow track still lowers latency. The draft says a
consumer MUST NOT subtract `delay` values across renditions.
- *Fixed common origin.* Measure every `delay` from the broadcast's first
lateness. Exact subtraction, but common drift raises every rendition
together, so values grow without bound and the catalog republishes for the
life of the broadcast; the problem the sliding window exists to avoid.
- *Coordinated rebasing.* Advertise each `delay` as its current value against
the current broadcast baseline and let it fall. Exact, but `delay` gives up
never-lower, the catalog churns as baselines move, and the player must
shrink safely.
- *No `delay` field.* The player measures each subscribed track's own
arrival baseline and sizes by their spread. No wire change, and it also
covers gateway and ingest offsets, but a track's offset is unknown until its
first frames arrive, so subscribing to a slower rendition glitches once.
Every option also needs the player's reference to follow the earliest
subscribed track's current arrival rather than its lifetime minimum, since
`Sync.received` only ever lowers it; that is receiver-side and belongs with
the arrival minimum the [audio jitter target](/quest/m0/audio-jitter-target/README.md)
already expires.
Comment on lines +77 to +81

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Make the receiver reference fix a prerequisite

When the currently leading track drifts later, the catalog buffer alone cannot prevent late playback because Sync.received() retains the old, earlier reference. These lines say every option needs the audio-jitter quest's expiring arrival minimum, but this quest has no Required section and later says to build on whichever lands first, so it can ship first without that fix and under-buffer exactly this scenario. Keep the reference update in this quest or add the audio-jitter target as a prerequisite. quest/CLAUDE.mdL63-L66

Useful? React with 馃憤聽/ 馃憥.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Leaving this as written.

The receiver reference stays on the audio jitter target. Its watch quest replaces the lifetime arrival minimum with a media-time window, which is the fix for Sync.received only ever lowering. Pulling that update into this quest would duplicate it.

A Required entry would block the whole quest, including the catalog delay field, until both the watch and native jitter buffers land. The measurement does not depend on the player reference.

"Build on whichever lands first" is only about #3954 changing what register() takes. That PR is merged on the audio jitter target line, not on main, so the player half still follows whichever register() shape is on the base when it starts. It is not permission to ship the player without the expiring arrival minimum.

(Written by Grok 4.7)

- A faster-than-real-time source flushes early; each frame becomes the new
minimum and both stay at zero, which is correct for something that is not
live.
- **Wire.** Add optional `delay` beside `jitter` on video, audio, and text
renditions in `rs/hang`, `js/hang`, and `drafts/draft-lcurley-moq-hang.md`,
serialized with `MillisCeil` and zero-as-absent like `jitter`. Additive:
today's `jitter` never included a cross-track offset. Extend the draft's
never-lower rule to `delay`, and define both in terms of lateness.
- **Call sites:** the `moq-video` and `moq-audio` encode producers, the capture
path in `moq import capture`, `moq-gst`, and `libmoq` (so OBS). `js/publish`
mirrors the measurement in the video and audio encoders and replaces its
fixed `ceil(1000 / framerate)` and frame-duration hints. `container::Producer`
does not call it on its own.
- Replace the provisional PTS-gap floor so a decode-order sequence such as
`0, 120, 40, 80` ms does not permanently advertise its first 120 ms gap
when the reorder delay is only 80 ms. Preserve the never-lower rule for
measurements already advertised.
- **Enforce never-lower at the publisher**, not only by convention.
`js/publish/src/catalog.ts:25-29` already refuses a decrease and a zero for
audio and video jitter; Rust does not. Add a `moq_mux::Error` for a
decreased estimate (covering both fields) and return it from
`Rendition::set`, `Rendition::replace`, and `Rendition::estimate`
(`rs/moq-mux/src/catalog/tracks.rs`). Extend the `MillisCeil` serialization
(`rs/hang/src/catalog/millis.rs`) to `TextConfig`
(`rs/hang/src/catalog/text/mod.rs:124`), and mirror the zero-as-absent
normalization in `js/hang/src/catalog/text.ts` and the text section of
`js/publish/src/catalog.ts`. The `js/publish` check covers `delay` as well
as `jitter`, in every section that carries them.
never-lower rule to `delay` (unless rebasing wins), along with
`moq_mux::Error::JitterDecreased` and the `js/publish/src/catalog.ts` check.
- **Player.** `Sync` registers each subscribed rendition's `delay` and
`jitter` and computes `max(delay + jitter) - min(delay)`; it recomputes when
a rendition registers, unregisters, or its catalog entry rises. When the
earliest subscribed rendition's `delay` rises, `Sync` re-anchors its
reference later by that amount instead of leaving it at the old earliest
arrival, or every other track under-buffers. Rename `Sync`'s own `delay`
output (the resolved playout total) so it does not collide with the field.
`jitter` and recomputes when a rendition registers, unregisters, or its
catalog entry rises. Rename `Sync`'s own `delay` output (the resolved
playout total) so it does not collide with the field. #3954 on the
[audio jitter target](/quest/m0/audio-jitter-target/README.md) line changes
what `register()` takes, so build on whichever lands first.
- **Docs.** Update `doc/concept/audio-jitter.md`, the normative playout page,
with the cross-track rule and the catalog floor it reads.
- **Tests** inject the clock. Cover: a batch flushed at its end reports the
batch as jitter, a constant offset reports as `delay` on the slower track
and nothing on the faster, a slow drift stays bounded, a decrease is refused
on every site, and `Sync` resizes on a subscription change and re-anchors on
a rising earliest `delay`.
- **Tests** inject the clock. Cover: a constant offset reports as `delay` on
the slower track and nothing on the faster, a slow common drift stays
bounded, the earliest rendition changing does not under-buffer, a decrease is
refused on every site, and `Sync` resizes on a subscription change.

Public API: additive `delay` field in `hang` and `@moq/hang`, a new
`moq_mux::Error` variant, `Sync` output rename in `@moq/watch`. Wire: one
optional catalog field.
Public API: additive `delay` field in `hang` and `@moq/hang`, the
decreased-estimate error extended to `delay`, `Sync` output rename in
`@moq/watch`. Wire: one optional catalog field.

## Related

Expand Down
Loading