Repository navigation
fix(audio): bound the jitter reference history - #6359
Conversation
The reference search scanned every arrival still inside the window, and the window is media time, which the publisher chooses. A track stamped a microsecond apart holds millions of entries inside two seconds, so every frame's scan grew with the peer's timestamp rate and a viewer's main thread stalled. Keep a monotonic deque of the prefix minimum beside the history, so an in-order frame reads its reference in O(1) amortized, and fall back to the scan only for a reordered frame, where the prefix is shorter than the history. Cap the history at 8192 arrivals, far above any real track's two-second window, so memory and the reordered scan stay bounded too. Both the browser and native estimators implement the page, so both change together, with the cap named in doc/concept/audio-jitter.md. Co-Authored-By: DeepSeek V4.1 Flash <noreply@opencode.ai>
|
Warning Review limit reachedYou'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 26 minutes. View limit details
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. Comment |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: acb5154c0e
ℹ️ 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".
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
A reordered frame's reference is a minimum over a prefix of the history, and the scan let a peer spend MAX_HISTORY comparisons per frame by sending only reordered frames. Replace the minimum deque and the scan with a segment tree over a ring, which answers in-order and reordered frames the same way in O(log n), and keeps the oldest entry on a tie as the scan did. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Summary before merge:
(Written by Claude Opus 5.5) |
Problem
The playout estimator measures each frame against the fastest arrival at or before it in the last
WINDOWof media time. The history is pruned only by media time, and the publisher chooses the timestamp rate, so a track stamped one microsecond apart keeps ~2,000,000 entries inside the two-second window. Every frame then scanned all of them, so per-frame cost grew with the attacker's timestamp rate and total cost grew quadratically.js/net/src/track.tsonly checks that group starts do not decrease, so frame timestamps inside a group are entirely peer-controlled.This is a viewer-side denial of service, and the audit rated it High: the attacker does not need to fill the window, since ~100k entries already makes the main thread unresponsive.
The native estimator in
rs/moq-audiohad the same scan.Change
(arrival, media)roundsdelaydifferently, so this is required for bit-identical results; the doc now states it.arrival - ref.arrival - (media - ref.media)keeps the normative arithmetic order.MAX_HISTORY(8192) arrivals. With logarithmic lookups the cap no longer bounds CPU, only memory, and it is still needed for that: the window is media time, so a stream that advances a nanosecond per frame would otherwise keep every frame it sends. 8192 stays 4x above the two-second window of a 1000 fps track; a stream dense enough to reach it measures against a shorter window rather than being refused. The ring grows by doubling, so a normal track allocates for the ~100 entries it holds, not the cap.doc/concept/audio-jitter.mdnamesMAX_HISTORY, the tie rule, and why the lookup must be sublinear.Public API: none (
Historyis exported from the internaljitter.tsmodule for its test only). Wire: none.Benchmark
just js jitter-benchsweeps held history against in-order and reordered frames (bun, ns per frame):In-order frames pay a logarithmic tree update instead of the deque's O(1), well under a microsecond; reordered frames go from linear to flat.
Verification
cargo nextest run -p moq-audio jitter: the full conformance corpus passes unchanged.bun test src/container/jitter.test.tsinjs/hang: 18 passed, including the same corpus.(arrival, media)included, after every frame of a 400-frame trace with reordering, timestamp jumps, and frequent exact ties. Flipping the tie rule to keep the newest fails them, which the previous deque did silently.just check: JS and Rust pass; the flake module's github-runner VM image cannot build in my sandbox (unsharedenied), which CI covers.(Written by DeepSeek V4.1 Flash and Claude Opus 5.5)