Skip to content

feat(moq-mux): catalog delay measures cross-rendition encoder lateness - #4170

Open
kixelated wants to merge 2 commits into
mainfrom
quest/m1/jitter-flush-clock
Open

kixelated wants to merge 2 commits into
mainfrom
quest/m1/jitter-flush-clock

Conversation

@kixelated

Copy link
Copy Markdown
Collaborator

Problem

A rendition's catalog jitter is its spread above its own minimum flush lateness, so a constant offset between encoders (video flushing 200 ms behind audio) is advertised nowhere. js/watch plays every track from one clock anchored on the earliest track, so a slower rendition's frames arrive late for it. Separately, the js/watch video decoder computed its sync floor from the decoder-only config, which carries neither jitter nor framerate, so the video rendition's catalog jitter never reached Sync.

Approach

  • Measurement (moq-mux). catalog::Producer owns one shared flush baseline: a common epoch plus a 10 s sliding minimum fed by every rendition that calls flush. Each rendition's delay is its own sliding minimum minus the broadcast's, reported as a lifetime maximum. Renditions that never flush (imports, gateways) advertise none. A discontinuity clears the broadcast window as well as the rendition's, so a paused-and-resumed broadcast does not read its pause as delay.
  • Decision: no subtraction (the quest's recommended option, chosen by the maintainer). Sliding baselines and never-lower are kept; the player holds max(delay + jitter) over the renditions it subscribes to, and the draft says a consumer MUST NOT subtract delay across renditions. This never under-buffers even when the earliest rendition changes; it over-buffers by min(delay) when the broadcast's earliest rendition is not subscribed.
  • Never-lower. Rendition::set/replace refuse a lowered delay with the new Error::DelayDecreased; measurements hold the published value. js/publish's CatalogProducer refuses a lowered or zero delay.
  • Player (js/watch). Each rendition's registered floor is now delay + jitter (video: read live from the full catalog entry by track name, fixing the bug above; audio: playbackJitter). Sync already recomputes on register, unregister, and a rising entry, so dropping a slow rendition lowers latency.
  • Docs. Draft delay field section and changelog, doc/concept/audio-jitter.md "Across renditions" (the normative rule), doc/concept/hang.md, moq-mux/OBS/GStreamer pages.

Tests inject the clock: a constant offset reports delay on the slower track only, a common drift stays bounded, the earliest rendition drifting behind still covers the real spread, a discontinuity does not become delay, a decrease is refused in Rust and JS, and Sync resizes when a delayed rendition is dropped.

Impact

  • hang: additive delay: Option<Duration> on VideoConfig, AudioConfig, TextConfig (MillisCeil, zero-as-absent).
  • moq-mux: additive Estimate::delay + Estimate::with_delay, VideoHint::delay, Error::DelayDecreased. Estimator::new() standalone never measures delay.
  • @moq/hang: additive optional delay on video, audio, and text rendition schemas.
  • @moq/publish: CatalogProducer.mutate also throws on a lowered or zero delay.
  • @moq/watch: no API change; video Decoder.out.jitter now reflects the catalog (it was always undefined), and both decoders include delay.
  • Wire: one optional catalog field per rendition, delay, in draft-lcurley-moq-hang (moq-hang-03 changelog). Additive: older consumers ignore it.

Alternatives

  • Fixed common origin, coordinated rebasing, or no delay field (receiver-measured): see the options in the removed quest. Rejected for unbounded growth, giving up never-lower, or a glitch on each new subscription respectively.
  • Renaming Sync.out.delay (quest plan): skipped. Sync never reads the catalog field by name, since delay is folded into each registered floor, and the rename would churn every consumer that quest(audio-jitter-target): Audio jitter target #3952 is also rewriting.

Follow-ups

  • Conflicts expected with quest(audio-jitter-target): Audio jitter target #3952 (audio-jitter-target): it replaces the registered jitter with a per-decoder target. On merge, add the rendition's catalog delay on top of target(...) in the audio decoder and on top of max(spread, jitter) in the video decoder; doc/concept/audio-jitter.md already states playout = max(delay + target).
  • New quest quest/m1/publish-delay.md: the browser publisher advertises delay like moq-mux. Completes quest/m1/jitter-flush-clock.md otherwise.
  • Sync.received only ever lowers its reference, so after the earliest subscribed track leaves, playback stays anchored to it. Receiver-side; belongs with the arrival minimum the audio jitter target expires.
  • main does not compile moq-ffi (E0283 from feat(ffi): advertise JSON tracks in the catalog, add binary data tracks #4137), so the Rust CI here fails until fix(ffi): name the binary config conversion so moq-ffi compiles #4157 lands. Local just check passes with that fix applied.
  • Not exercised in a real browser (just dev); the video decoder change is covered only by unit tests of the helpers it calls.

(Written by Claude Opus 5.5)

🤖 Generated with Claude Code

kixelated and others added 2 commits September 25, 2026 09:33
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Renditions advertise delay, how far their minimum flush lateness trails the
broadcast's earliest rendition, measured against one baseline shared by the
catalog and never lowered. js/watch holds the largest delay + jitter over the
renditions it subscribes to, and the video decoder now reads its floor from
the full catalog entry.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kixelated
kixelated marked this pull request as ready for review September 26, 2026 00:16
@coderabbitai

coderabbitai Bot commented Sep 26, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

Next included review available in 1 minute.

Check out review usage here.

View limit details

Limit details: You’ve used all 4 included reviews currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: 36cee78c-d126-420c-b4b5-a493208f077e

📥 Commits

Reviewing files that changed from the base of the PR and between e173ddd and 9d47840.

📒 Files selected for processing (35)
  • doc/bin/gstreamer.md
  • doc/bin/obs.md
  • doc/concept/audio-jitter.md
  • doc/concept/hang.md
  • doc/lib/rs/moq-mux.md
  • drafts/draft-lcurley-moq-hang.md
  • js/hang/src/catalog/audio.ts
  • js/hang/src/catalog/root.test.ts
  • js/hang/src/catalog/text.ts
  • js/hang/src/catalog/video.ts
  • js/publish/src/catalog.test.ts
  • js/publish/src/catalog.ts
  • js/watch/src/audio/config.test.ts
  • js/watch/src/audio/config.ts
  • js/watch/src/sync.test.ts
  • js/watch/src/video/decoder.ts
  • js/watch/src/video/playhead.test.ts
  • js/watch/src/video/playhead.ts
  • quest/m0/audio-jitter-target/README.md
  • quest/m1/README.md
  • quest/m1/data-jitter.md
  • quest/m1/jitter-flush-clock.md
  • quest/m1/publish-delay.md
  • quest/m2/watch-data-sync.md
  • rs/hang/src/catalog/audio/mod.rs
  • rs/hang/src/catalog/root.rs
  • rs/hang/src/catalog/text/mod.rs
  • rs/hang/src/catalog/video/mod.rs
  • rs/moq-hls/src/export/rendition.rs
  • rs/moq-mux/src/catalog/estimate.rs
  • rs/moq-mux/src/catalog/producer.rs
  • rs/moq-mux/src/catalog/tracks.rs
  • rs/moq-mux/src/container/producer.rs
  • rs/moq-mux/src/error.rs
  • rs/moq-video/src/encode/producer.rs

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 26, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-09-26T00:21:08.325395Z 9d47840 Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9d47840096

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +246 to +248
#[serde_as(as = "MillisCeil")]
#[serde(default)]
pub delay: Option<std::time::Duration>,

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 Round-trip delay through the MSF catalog

When a consumer selects CatalogFormat::Msf, this new field is lost: to_msf_media in rs/moq-mux/src/catalog/producer.rs copies only config.jitter into moq_msf::Track, whose schema has no delay, and catalog/msf/consumer.rs consequently restores only jitter. Thus both audio and video using the supported MSF catalog path still omit the measured cross-rendition offset, so a slower encoder remains late despite this change. Add the corresponding MSF extension and conversion in both directions.

Useful? React with 👍 / 👎.

Comment on lines +131 to +134
const renditions = effect.get(this.source.out.catalog)?.renditions;
const jitter = (track: string | undefined) => {
const config = track === undefined ? undefined : renditions?.[track];
return config && renditionJitter(config);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Keep the active rendition tied to its source catalog

During a broadcast or source switch, #active continues rendering the old subscription until the pending track catches up, but source.out.catalog already describes the new source. Looking up the active track name in this new map therefore makes its floor disappear when names differ, or substitutes the new rendition's floor when names match. If the old rendition has a larger delay or jitter, Sync shrinks prematurely and its remaining frames can arrive late during the handoff. Preserve the active rendition's catalog association or last required floor while still observing rises from its own catalog.

Useful? React with 👍 / 👎.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant