Skip to content

feat(catalog)!: measure jitter and delay from timestamps, not the wall clock - #6595

Merged
kixelated merged 8 commits into
mainfrom
catalog-estimates-from-timestamps
Oct 24, 2026
Merged

kixelated merged 8 commits into
mainfrom
catalog-estimates-from-timestamps

Conversation

@kixelated

@kixelated kixelated commented Oct 23, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

The catalog jitter and delay estimates came from the wall clock: each frame's flush time minus its media timestamp, a lifetime maximum. Any clock that does not track the wall clock exactly looks like ever-growing lateness. Every rise republishes the catalog, and the viewer turns each republish into a SUBSCRIBE_UPDATE on every track.

I measured this with a physical camera and mic published from moq.dev/publish (release) and watched from a second tab. These are the viewer's subscribe update: id= lines:

Window Track Updates
60 s after warmup video 146
60 s after warmup audio 176
next 67 s both ~451, rising

Over 110 s, the advertised audio jitter went from about 3 s to 7.6 s, and the audio delay reached 26 s: audio output fell about 0.25 s/s behind the wall clock, and the estimator reported that as lateness.

Cause of the camera lag

#6599 measured the same camera and mic once a second (worklet stamp minus now(), AudioContext clock minus wall clock, empty and dropped quanta, encodeQueueSize, encoder input and output counts):

  • Release moq.dev/publish (6f204fd): input stamps track the clock and encodeQueueSize stays 0, so no samples are lost and there is no encoder backlog. Release configures voice with usedtx: true, and Chromium stamps Opus output by counting the samples it emits, so every frame DTX suppresses pulls later audio 20 ms earlier. (frames in - frames out) x 20 ms equals the gap exactly (1860, 4460, then 9380 ms at 175, 190, and 202 s), and DTX's 400 ms keep-alive matches the 400 ms catalog steps.
  • This branch built locally, about 130 s: the stamp gap stayed flat, no lost quanta, every input produced an output, and the audio catalog delay was 13 ms.

main already removed DTX in #4808 and #4919 (a test checks it never reaches the encoder), and #6598 backports that to release. So this PR deletes the camera check quest, quest/m1/subscribe-update-churn.md, instead of replacing it: its chatter came from DTX's timestamps plus the wall-clock estimator, and both are gone. Thanks to #6599 for the measurement; its quest deletion is folded in here.

Approach

Jitter and delay are now measured only from media timestamps, in Rust (moq-mux) and JS (@moq/publish):

  • jitter: only reorder delay (PTS minus DTS) and the media span of a batch or fragment written together.
  • delay: the most a frame's timestamp has trailed the highest timestamp another track of the same catalog has already written. For example, video at 0/20/40/60 and audio at 0/15/30/45/60, written in the order v0 v20 a0 a15 v40 a30 v60 a45 a60, gives audio delay 20 and video none.
    • A lone track measures nothing.
    • B-frames within a track count as jitter, not delay.
    • A discontinuity, or a track finishing or ending, stops holding the other tracks back. A finished producer that is kept around (as moq-ffi keeps a finished media track) counts as ended.
    • A buffered container::Producer (with_buffer) takes its place in the order when the buffer flushes, since that is when the transport has its frames. Rates are still counted on the way in.
    • A stopped JS video rendition leaves the order after its teardown's black keyframe, so that frame cannot seat it again.
    • Rust keeps the top two highs, so a write costs O(1) whatever the track count. The catalog_write_order bench sweeps 1, 16, and 256 tracks.
    • Data tracks order only payloads that carry a capture time, clamped to now.
  • No wall-clock measurement at all: flush and the gst encoder pad property are removed, and importers now measure delay too.

Impact

  • Wire: none in moq-lite. The hang catalog fields keep their shape, but their meaning changed: drafts/draft-lcurley-moq-hang.md field-jitter, field-delay, and the data estimates are rewritten, with the moq-hang-04 changelog updated.
  • Breaking: every flush that reported a frame's write time is removed, since nothing reads the wall clock any more. To migrate, delete the call (and regenerate bindings for moq-ffi); delay is now measured from the order of the timestamps you write, so importers advertise it too.
    • moq-mux: catalog::Estimator::flush, container::Producer::flush, import::Track::flush, and every codec importer's flush.
    • moq-c: moq_publish_media_flush.
    • moq-ffi: MoqMediaTrackProducer::flush. The checked-in Dart bindings are regenerated.
    • Python, Swift, and Go wrappers: the matching media.TrackProducer.flush, Media.TrackProducer.flush(timestampUs:), and media.TrackProducer.Flush.
  • Breaking (moq-gst): the moqsink sink_%u::encoder pad property is removed. To migrate, drop encoder=true from pipeline descriptions; every pad now advertises delay from its buffer timestamps against the other pads.
  • Behavior: jitter now covers only reorder delay and batch spans, so a source whose catalog advertised wall-clock lateness advertises much less.
  • @moq/publish: internal only. Broadcast.baseline (@internal) becomes Broadcast.order.

Per #6610, these migration notes land in each package's MIGRATE.md at the breaking cut rather than here.

Alternatives

  • Keep the wall-clock measurement but cap or decay it. Any clock that does not track the wall clock still reads as growing lateness, just slower, and the cap would be a constant whose only effect is a refusal.
  • Measure delay against the minimum lateness across tracks (the previous Baseline). That still needs a wall clock per frame, which is what failed.

Follow-ups

None from this PR. #6599 suggests an hour-long run to see whether the AudioContext clock keeps stepping behind the wall clock; it no longer affects the catalog, since nothing here reads that clock.

Checks

  • just check: passes after merging main.
  • bun test in js/publish: passes. The new black-keyframe assertion fails without the video teardown fix.
  • just test interop --all: every pairing passes, including the js-native-node/js-native-bun ones that failed on the previous worktree.
  • quest check --base origin/main: passes.
  • Not rerun here: the physical camera (quest: drop audio-capture-lag, fixed on main by removing Opus DTX #6599 ran this branch locally on one: audio delay 13 ms, no drift).

(Written by Claude Opus 5.5)

🤖 Generated with Claude Code

kixelated and others added 2 commits October 23, 2026 16:03
…l clock

The catalog estimator measured each frame's flush time against its media
timestamp, so any capture clock drifting from the wall clock read as
ever-rising lateness. A physical camera on moq.dev/publish ratcheted audio
jitter and delay without bound, republishing the catalog and driving
hundreds of SUBSCRIBE_UPDATEs a minute.

jitter now covers only reorder delay and batch spans. delay is how far a
frame's timestamp trails the highest one another track of the catalog
already wrote, so importers measure it too. flush, moq_publish_media_flush,
the binding wrappers, and moqsink's encoder pad property are removed.

The camera's audio still drifts behind video by PTS; the subscribe update
quest becomes the audio capture clock fix.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 23, 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-10-24T10:36:55.243179Z 65385f3 New commits
ℹ️ 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.

@coderabbitai

coderabbitai Bot commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

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

Next included review available in 15 minutes.

Check out review usage here.

View limit details

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

Learn how review limits work.

Review configuration:

Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: a8469a94-6aab-4683-a5ee-787ac30ad3b6
Commits

Reviewing files that changed from the base of the PR and between d71a947 and 65385f3.

Files selected for processing: 62
  • cpp/obs/src/moq-output.cpp
  • dart/moq_ffi/lib/src/moq.dart
  • doc/bin/gstreamer.md
  • doc/concept/audio-jitter.md
  • doc/concept/hang.md
  • doc/lib/c/index.md
  • doc/lib/dart/index.md
  • doc/lib/go/index.md
  • doc/lib/kt/index.md
  • doc/lib/py/index.md
  • doc/lib/swift/index.md
  • drafts/draft-lcurley-moq-hang.md
  • go/wrapper/media/media.go
  • js/publish/src/audio/capture.test.ts
  • js/publish/src/audio/encoder.test.ts
  • js/publish/src/audio/encoder.ts
  • js/publish/src/broadcast.ts
  • js/publish/src/jitter.test.ts
  • js/publish/src/jitter.ts
  • js/publish/src/order.test.ts
  • js/publish/src/order.ts
  • js/publish/src/video/capture.test.ts
  • js/publish/src/video/encoder-stall.test.ts
  • js/publish/src/video/encoder.test.ts
  • js/publish/src/video/encoder.ts
  • js/watch/src/audio/latency.ts
  • py/moq-rs/README.md
  • py/moq-rs/moq/media.py
  • quest/m1/README.md
  • quest/m1/subscribe-update-churn.md
  • rs/moq-audio/src/encode/producer.rs
  • rs/moq-c/README.md
  • rs/moq-c/src/api.rs
  • rs/moq-c/src/publish.rs
  • rs/moq-c/src/test.rs
  • rs/moq-ffi/src/media.rs
  • rs/moq-ffi/src/test.rs
  • rs/moq-gst/src/sink/imp.rs
  • rs/moq-gst/src/sink/pad.rs
  • rs/moq-gst/src/sink/request_pad.rs
  • rs/moq-gst/tests/element.rs
  • rs/moq-mux/benches/catalog.rs
  • rs/moq-mux/src/binary.rs
  • rs/moq-mux/src/catalog/data.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/codec/aac/import.rs
  • rs/moq-mux/src/codec/av1/import.rs
  • rs/moq-mux/src/codec/flac/import.rs
  • rs/moq-mux/src/codec/h264/import.rs
  • rs/moq-mux/src/codec/h265/import.rs
  • rs/moq-mux/src/codec/mp3.rs
  • rs/moq-mux/src/codec/opus/import.rs
  • rs/moq-mux/src/codec/vp8/import.rs
  • rs/moq-mux/src/codec/vp9/import.rs
  • rs/moq-mux/src/container/fmp4/import.rs
  • rs/moq-mux/src/container/producer.rs
  • rs/moq-mux/src/import/track.rs
  • rs/moq-mux/src/json.rs
  • rs/moq-video/src/encode/producer.rs
  • swift/Sources/Moq/Media.swift
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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 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: 58d410a0a3

ℹ️ 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 thread rs/moq-mux/src/catalog/estimate.rs Resolved
Comment thread js/publish/src/video/encoder.ts Resolved
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@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: b8fee30cdd

ℹ️ 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 thread rs/moq-mux/src/catalog/estimate.rs Resolved
kixelated and others added 4 commits October 23, 2026 22:56
…m-timestamps

# Conflicts:
#	go/wrapper/MIGRATE.md
#	py/moq-rs/MIGRATE.md
#	rs/moq-audio/src/encode/producer.rs
#	rs/moq-c/MIGRATE.md
#	rs/moq-c/src/publish.rs
#	rs/moq-ffi/MIGRATE.md
#	rs/moq-gst/src/sink/imp.rs
#	rs/moq-gst/src/sink/pad.rs
#	rs/moq-mux/MIGRATE.md
#	rs/moq-video/src/encode/producer.rs
#	swift/MIGRATE.md
…sees them

A buffered container::Producer takes its place in the write order when its
buffer flushes, a finished producer or fMP4 track stops holding the others
back, and a stopped JS video rendition leaves the order after its black
keyframe instead of before it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Folds in #6599's measurement: release's DTX made Chromium's
sample-counted Opus stamps fall behind, and main already removed DTX.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#6610 writes MIGRATE.md at the breaking cut, so this PR's
per-package entries become its Impact section.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kixelated
kixelated added this pull request to the merge queue Oct 24, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to a conflict with the base branch Oct 24, 2026
# Conflicts:
#	rs/moq-mux/src/import/track.rs
@kixelated
kixelated added this pull request to the merge queue Oct 24, 2026
Merged via the queue into main with commit fc7d35e Oct 24, 2026
9 checks passed
@kixelated
kixelated deleted the catalog-estimates-from-timestamps branch October 24, 2026 13:31
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