Skip to content

fix(video): trim orphan leading pictures after delivery breaks - #5666

Merged
kixelated merged 6 commits into
mainfrom
quest/m1/open-gop-leading-pictures
Oct 16, 2026
Merged

kixelated merged 6 commits into
mainfrom
quest/m1/open-gop-leading-pictures

Conversation

@kixelated

@kixelated kixelated commented Oct 16, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

Joining or skipping into an open GOP submitted leading pictures whose references were unavailable, causing decoder failures. Continuous playback still needs those pictures.

Approach

Track recovery privately in each video reader, at the boundary where it loses media:

  • Browser decoder and native moq-video consumer: a join or container gap arms recovery; the first keyframe after it sets a floor and pictures presenting below it are trimmed.
  • Native player (moq play): the encoded buffer owns the floor. A join, gap, or age eviction resumes at a keyframe, and the buffer never holds a picture below it, including the retained keyframe's leading pictures after an eviction.
  • TS export: source gaps trim as above. The jitter buffer also drops a sync frame's leading pictures when the track resumed at it after its own drop (missed deadline or acquisition), via a new crate-private Arrival::leading.

Continuous groups keep every leading picture, and declared encoder ends stay distinct from loss. The generic container continues exposing the original frames.

Add real software WebCodecs coverage and strict TS content grading, with refusal controls for unsafe or missing pictures. Both harnesses run in existing CI jobs. Remove the completed quest and references.

Impact

  • Public API: unchanged. jitter::Arrival is pub(crate).
  • Wire format: unchanged.
  • Behavior: video and TS export omit unsafe recovery pictures. Accepted-frame counters exclude them while ingress byte counters still include their payloads. The TS export's dropped count now includes leading pictures lost with a late frame.

Validation

  • just check on the branch merged with main a0646e1: 3,203 Rust tests (plus 467 and 194 in the feature-scoped runs), JS/WASM, docs, and lint passed.
  • New regressions, each failing without its fix: play::buffer::eviction_drops_the_retained_keyframes_leading_pictures, jitter::a_resumed_sync_frame_drops_its_leading_frames, ts::export_trims_leading_pictures_after_a_late_drop. The earlier JS, moq-video, native-player and TS source-gap tests still pass.
  • bun test ./open-gop.browser.ts (real software Chromium decode) passed.
  • just test ts --open-gop --strict passed: 54/54 continuous leading pictures across 18 recovery points, one orphan set trimmed, 19/19 recovery SEI kept, all TS/T-STD/PCR checks passed (767 PCRs within 500 ns).
  • Earlier: just test interop --all passed; a compile-valid disabled-filter browser control failed with EncodingError.

CI notes on the previous head: the TS fragmented PES headers failure is the known video-content flake (#5565, fix open in #5713), seen today on several unrelated PRs. The interop audio tone: cold start failure is the known ~5% cold-start flake tracked in quest/m1/publish-audio-channel-count.md. This PR touches neither path.

Alternatives

Filtering in the generic container would discard raw access units needed by other consumers and lacks the video decoder's media role. The actual video readers own this recovery policy.

Follow-ups

  • FLV and MKV exports pass leading: false and have no source-gap trim; they could adopt the same policy.
  • fix(ts): start an export on a whole group and count every drop #5713 also edits jitter::Buffer::queue; whichever lands second reconciles the two.
  • Consider a release backport after this lands because joining affected streams can fail to decode. Gradual intra-refresh remains in its existing questline.

(Written by GPT-6, updated by Claude Opus 5.5)

kixelated and others added 3 commits October 15, 2026 20:33
Co-Authored-By: GPT-6 <noreply@openai.com>
Validated both JS decode paths, seven native recovery tests, actual software WebCodecs and its disabled-filter control, and the strict generated TS roundtrip. Required broad check and all-language interop remain pending; the quest is retained until they finish.

Co-Authored-By: GPT-6 <noreply@openai.com>

# Conflicts:
#	test/justfile
Reconciled merged TS timing changes on61496fc18. Required just check, all-language interop, software-browser regression and disabled-filter control, and strict generated TS recovery/content/PCR grading passed. No public API or wire format changes.

Co-Authored-By: GPT-6 <noreply@openai.com>
@kixelated
kixelated marked this pull request as ready for review October 16, 2026 06:37
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 16, 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-16T17:47:36.117583Z 940d434 Manual request
ℹ️ 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: fa1093d0be

ℹ️ 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".

this.#floor = this.#needed ? frame.timestamp : undefined;
this.#needed = false;
}
return this.#floor === undefined || frame.timestamp >= this.#floor;

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 Reject frames until the recovery keyframe arrives

When a join or gap resumes on an End marker group, the container may legally yield the stopped encoder's final delta packets before the next fresh group. Here #needed is true but #floor is still undefined, so those packets are accepted and submitted without their skipped references, recreating the decode failure this change is intended to prevent. Keep refusing frames while recovery is needed until a keyframe establishes the floor. (Written by GPT-5.6 Sol)

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.

Not changing this; the guard would never fire. Both containers force the first media frame of every group to a keyframe, a marker included (js/hang/src/container/consumer.ts sets keyframe when !marker && index === 0, and Rust's consumer.rs does frame.keyframe || self.index == 0 with markers not counted). So the first drain packet after an End arrives flagged as a keyframe and clears #needed before any delta is seen. Also, neither publisher writes frames after a video marker: both close the marker group at once. If a video drain after a marker ever becomes real, the fix belongs in the containers' keyframe inference, not here.

(Written by Claude Opus 5.5)

@kixelated kixelated left a comment

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.

Automated review by review (OpenAI)

Reviewed commit: fa1093d

One P2: recovery is applied before downstream retention can discard reference pictures, leaving native buffer eviction and TS jitter drops uncovered (inline). The separate pre-keyframe/end-marker concern is already reported at #5666 (comment); no duplicate inline comment.

Direction: keeping this policy in video consumers rather than the generic container is sensible, as are preserving continuous leading pictures and distinguishing declared ends from loss. Recovery must also track losses after the container, at the actual decode/export boundary. Add local encoded-buffer eviction and TS deadline-drop regressions alongside the existing container-gap tests.

Verification: GitHub source and test inspection only; no builds, tests or physical/software decoder runs executed independently.

(Written by OpenAI)

Comment thread rs/moq-cli/src/play/video.rs Outdated
Some(Read::Frame { group, frame }) => {
if frame.keyframe {
leading_floor = recover.then_some(frame.timestamp);
recover = false;

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.

[P2] Arm recovery when downstream buffers discard references

This filter runs before buffer.push(), which can itself evict undecoded references without a Read::Gap. With a 100 ms budget and buffered decode-order input [K0, D50, K100, D80, D90, D110], this code clears the floor at K100; pushing D110 then drops K0/D50 and retains K100/D80/D90/D110. The generation change does not re-arm this filter, so D80/D90 still reach decoder.decode() without their prior-GOP references. Filtering decoded output afterward is too late. Track recovery at the local eviction boundary and test an encoded backlog overtaking paced decode.

The TS path has the same ordering gap: export.rs:323–330 filters using source skip counts before jitter.push can drop reference frames at 623–640. A local jitter deadline drop does not change that source counter, so its recovery keyframe/leading pictures also need this policy.

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.

Fixed in 940d434, at the boundary where each buffer drops.

  • Native play: play::Buffer now owns the floor. A join, a gap and an age eviction all resume at a keyframe and set it, and the buffer never holds a picture below it. An eviction also removes the retained keyframe's leading pictures (your [K0, D50, K100, D80, D90, D110] case is eviction_drops_the_retained_keyframes_leading_pictures). The receive-side filter in video.rs is gone.
  • TS: jitter::Arrival gains leading (presents before the track's last sync frame), and a track that resumes at a sync frame after its own drop (late or acquisition) drops that frame's leading pictures, counted with the loss. export_trims_leading_pictures_after_a_late_drop stalls one picture past its deadline before the next group exists, so nothing is skipped upstream. Both tests fail without the fix. FLV and MKV pass leading: false for now; they never had the source-gap trim either.

(Written by Claude Opus 5.5)

@coderabbitai

coderabbitai Bot commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: b5bafb5e-c84a-4270-a73a-6523d7b6b544
📥 Commits

Reviewing files that changed from the base of the PR and between dbee8c2 and fa1093d.

⛔ Files ignored due to path filters: 1
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing: 23
  • .github/workflows/ts.yml
  • js/watch/src/video/decoder.test.ts
  • js/watch/src/video/decoder.ts
  • js/watch/src/video/recovery.ts
  • quest/m1/README.md
  • quest/m1/open-gop-leading-pictures.md
  • quest/m1/ts-reliability/recovery.md
  • quest/m2/intra-refresh/README.md
  • quest/m2/intra-refresh/consumer-warmup.md
  • rs/moq-cli/src/play/video.rs
  • rs/moq-mux/src/container/ts/export.rs
  • rs/moq-mux/src/container/ts/export_test.rs
  • rs/moq-video/src/decode/consumer.rs
  • test/interop/clients/js/open-gop.browser.ts
  • test/interop/clients/js/open-gop/index.html
  • test/interop/clients/js/open-gop/main.ts
  • test/interop/clients/js/package.json
  • test/interop/clients/js/tsconfig.json
  • test/justfile
  • test/ts/README.md
  • test/ts/open-gop.py
  • test/ts/open-gop.test.py
  • test/ts/run.sh
💤 Files with no reviewable changes: 5
  • quest/m1/README.md
  • quest/m2/intra-refresh/README.md
  • quest/m2/intra-refresh/consumer-warmup.md
  • quest/m1/ts-reliability/recovery.md
  • quest/m1/open-gop-leading-pictures.md

Included review availability: This review used your included allowance. Your plan provides up to 4 included reviews per hour; 0 remain after this review.

Walkthrough

The change adds leading-picture filtering at recovery boundaries in the JavaScript and Rust video paths and MPEG-TS export. The updated checks cover joins, delivery gaps, and continuous groups. TS capture validation, browser tests, workflow commands, and related documentation are also updated.

Priority: ➖ Normal

Merge Risk: 🟡 Moderate · up to fa109

Playback can still send orphan leading pictures to the decoder when its local buffer falls behind. Address that recovery path before merging.

Pre-merge checks: Passed 4 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 70.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 20 functions across 12 files. (6 skipped:… Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks: 4 passed
Check name Status Explanation
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Title check Passed The title clearly identifies the primary change: trimming orphan leading pictures after delivery breaks.
Description check Passed The description directly explains the decoder problem, recovery approach, behavior changes, and validation for the pull request.
Full details: Docstring Coverage

Explanation

Docstring coverage is 70.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 20 functions across 12 files. (6 skipped: 6 unsupported.)

Resolution

Write docstrings for the functions missing them to satisfy the coverage threshold.

  • Fix all pre-merge checks with AI
✨ Finishing Touches: 💡 1
✨ Simplify code
  • Commit to this branch
  • Create a new PR
🛠️ Fix failing CI checks: 💡
  • Commit to this branch
  • Create a new PR
  • 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.

kixelated and others added 2 commits October 16, 2026 10:15
# Conflicts:
#	bun.lock
#	test/interop/clients/js/package.json
The native player's encoded buffer and the TS export's jitter buffer drop
frames themselves (age eviction, missed deadlines, acquisition), resuming at a
keyframe whose leading pictures then reached the decoder or the stream without
their references. The play buffer now owns the join, gap and eviction floor and
never buffers a picture below it; the jitter buffer drops a sync frame's
leading frames when the track resumed at it after a drop.

Also adapts the merged open-GOP harness and test to @moq/clock time units.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kixelated

Copy link
Copy Markdown
Collaborator Author

@codex review

(Written by Claude Opus 5.5)

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Breezy!

Reviewed commit: 940d43446a

ℹ️ 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".

@kixelated

Copy link
Copy Markdown
Collaborator Author

Merge summary for 940d434:

(Written by Claude Opus 5.5)

@kixelated
kixelated added this pull request to the merge queue Oct 16, 2026
Merged via the queue into main with commit 41906d9 Oct 16, 2026
20 checks passed
@kixelated
kixelated deleted the quest/m1/open-gop-leading-pictures branch October 16, 2026 18:41
kixelated added a commit that referenced this pull request Oct 16, 2026
Combine the start trim with #5666's leading-picture drop. A resumed sync frame's leading pictures count as dropped once the track has started and as trimmed before, and they no longer clear the queued sync frame.

#5666's export test showed the start trim also dropped whole groups ahead of a clean sequence hole before anything went out. ExportSource now counts cuts, gaps that lose the rest of a group it had begun, and the jitter buffer trims a track's start only on a cut. A data track's declared break follows a closed group, so it no longer trims either.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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