Skip to content

fix(ts): start an export on a whole group and count every drop - #5713

Merged
kixelated merged 13 commits into
mainfrom
quest/m1/test-flakes-2/ts-video-content
Oct 16, 2026
Merged

kixelated merged 13 commits into
mainfrom
quest/m1/test-flakes-2/ts-video-content

Conversation

@kixelated

@kixelated kixelated commented Oct 16, 2026 •

Copy link
Copy Markdown
Collaborator

Closes #5565

Problem

The TS compliance video-content check flaked on unrelated PRs: the capture kept pictures 25 to ~33, then 50 onward. Root cause (confirmed in #5565 from the #5553 and #5566 runs): ExportSource::live() subscribes with max_delay = 0 to pick the live edge, then raises the budget after the first frame. When the next group lands before the raised budget crosses the relay, the publisher resets the joined group Old. The export already held that group's first pictures and wrote them out ahead of a hole. The loss went unreported apart from the consumer's eviction warning.

Changes

  • Start on a whole group (rs/moq-mux/src/jitter.rs). Until a frame of a track has gone out, a loss takes everything the track holds, and the track starts at its next sync frame. A loss here is a cut (a gap that loses the rest of a group the source had begun) or a frame the buffer drops (stale at acquisition, late). ExportSource counts cuts (cuts(), crate-private) and they reach the buffer as jitter::Arrival::cut. Whole groups missing after one that ended are not a cut, so they trim nothing. The same goes for a data track's declared break, which follows a closed group. fix(video): trim orphan leading pictures after delivery breaks #5666's leading-picture drop combines with it: a resumed sync frame's leading pictures count as dropped once the track has started, and as trimmed before. This covers both candidates in the quest's hypothesis 1: the upstream cut, and a held IDR that passes while the rest of its group is stale at acquisition. The zero-budget live-edge pick stays as it is, as the quest recommends.
  • Tune-in follows the moved start (ts/export.rs). The stream still begins on the video keyframe, with one exception below. Until video goes out, audio that presents before the earliest frame the video holds is dropped. Without this, a second of audio would lead the moved keyframe.
  • Every drop counted and logged. stats::Export gains trimmed (dropped before a stream's output began) and gaps (media skipped upstream). dropped now also counts frames that fall due before a new generation's acquisition ends after output began; those were silent before. ExportSource warns on every gap with its position and span. A data track's declared break is a skipped sequence, so it also counts as a gap and gets the warning; the doc and the warning say so (quest/m1/ts-reliability/data-break.md tracks telling them apart). moq export ts logs the new counts in its TS export release clock line.
  • A dropped release leaves its track unstarted (jitter.rs, from Codex review). jitter::Ready says whether it is its track's first frame out. A release the export drops ahead of the keyframe goes through Buffer::discard, so a later loss on that track is still trimmed rather than counted as dropped.
  • Leading pictures orphaned by a hole are counted (ts/export.rs, from Codex review). fix(video): trim orphan leading pictures after delivery breaks #5666 drops the leading pictures below a recovery keyframe before they reach the jitter buffer. They now count through Buffer::lose: as dropped when the output already holds frames ahead of the hole, and as trimmed when they lead the track's first keyframe.
  • Harness (test/ts/run.sh). A failing decode.py now prints the exporter's release-clock counts and loss warnings beside the failure, where it used to exit with no context.

Regressions, all on the paused clock:

  • a_start_group_cut_short_starts_at_the_next (export): reproduces the CI signature, pictures 0..4 then 15+.
  • a_start_group_cut_by_a_gap_is_dropped_whole (jitter).
  • a_join_drops_its_group_whole (jitter): the held-IDR case.
  • audio_dropped_ahead_of_a_moved_start_is_not_started (export): audio dropped ahead of a moved start, then late, is trimmed, not dropped.

Each one fails without the fix.

  • only_a_gap_inside_a_group_cuts_it (source): an eviction mid-group is a cut; a missing whole group is not.

Existing tests: export_drops_an_ac3_frame_cut_by_a_marker keeps main's expectation, and its doc now says why the break trims nothing. a_resumed_sync_frame_drops_its_leading_frames (#5666) now lets the first frame go out before the late one, so its leading-frame loss still counts as dropped.

Public API / wire

  • moq_mux::container::ts::stats::Export (#[non_exhaustive]): new trimmed and gaps fields. dropped is now documented as "after the stream's output began", and it also counts frames that fall due before a new generation's acquisition ends.
  • ExportSource::cuts, jitter::Arrival::cut, jitter::Ready::first, jitter::Buffer::discard and jitter::Buffer::lose are crate-private.
  • No wire change.

Checks

Decisions

Settled with the maintainer during iteration.

  1. What a frame dropped under host starvation should fail. The late check uses arrived, which is when the source was last found empty. So starving only the exporter cannot make a frame late; only late delivery upstream can, and that is real loss.
    • ✅ (a) decode.py keeps failing on any loss after output began, and the harness prints the exporter's counts beside the failure.
    • (b) A separate export-drops check, with video-content and audio-content excusing holes when dropped > 0. CI fails either way, and this could hide corruption that coincides with a drop.
    • (c) Tolerate reported drops. This goes against failing loud.
  2. Scope of the start trim. A data track (verbatim PES: AC-3, teletext, SCTE-35, ID3) declares a break by skipping a sequence, which reads as Read::Gap. So every declared break before such a track starts trims what it holds and counts in gaps.
    • ✅ (a) Keep the trim uniform. Reword the gaps doc and the "media skipped upstream" warning for data-track breaks, and add a quest to tell the break apart on the wire (quest/m1/ts-reliability/data-break.md). Since the merge with fix(video): trim orphan leading pictures after delivery breaks #5666, the trim follows only cuts, so a declared data-track break no longer trims anything. It still counts in gaps.
    • (b) Exempt the sparse tracks the acquisition does not expect.
    • (c) Keep it uniform with no rewording.
  3. Naming of the new stats::Export fields.
    • ✅ (a) trimmed and gaps.
    • (b) startup for trimmed.
    • (c) skips for gaps. Rejected because ExportSource::skips includes markers.
  4. Audio leading the first keyframe after a trimmed video start (found in review, not reproduced).
    • ✅ (a) Drop non-video frames while no video has gone out and none is queued, landing it only if a bounded attempt at a regression test reproduces the problem. Three paused-clock attempts did not, so this PR names the exception above and quest/m1/ts-reliability/audio-lead.md tracks the fix.
    • (b) Only correct the description.
    • (c) Only file a quest.

Follow-ups

  • Hypothesis 3, the silent AAC loss under load. Gaps and new-generation acquisition drops are now logged and counted, so a loaded test/ts/run.sh loop should name the cause. I recommend a quest to run that loop and fix what it shows.
  • Local runs logged drift_ppm of -100 to -250 with out_of_tolerance rising during the default round. fix(moq-mux): judge a source's clock on slack the floors saw #5714 (merged) addressed that start-up drift; this branch is merged with it.

The quest file is deleted in this PR.

(Written by Claude Opus 5.5)

🤖 Generated with Claude Code

kixelated and others added 3 commits October 16, 2026 07:44
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A live-edge export subscribes with a zero budget, so the publisher can reset
the group it joined at (`Old`) before the raised budget reaches it, and the
export emitted that group's cut-off start ahead of a hole (#5565). The
jitter buffer now holds a track's start group whole: until a frame of the
track has gone out, a loss (a source gap, or a frame it drops) takes
everything the track holds, and it starts at its next sync frame. The TS
export's tune-in follows the video's start when it moves.

Drops are counted by cause: `dropped` (after a track's output began,
including a new generation's acquisition, previously uncounted), `trimmed`
(before it), and `gaps` (media skipped upstream, each logged with its span).
The TS compliance harness prints them beside a failing decoded-content check.

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

Copy link
Copy Markdown
Collaborator Author

Quest outcome: the confirmed CI cause (#5565, a zero-budget live-edge join whose start group the publisher resets Old) is fixed at the export. A track now starts on a whole group, so a start group that was cut before anything of it went out is trimmed instead of written ahead of a hole. Every drop is counted as dropped, trimmed or gaps, and the harness prints those counts when decode.py fails. The quest file is deleted.

Left as draft for the open decisions in the description: a separate check for host-starvation drops (recommended), the trim's scope for sparse tracks (recommend keeping it uniform), and the field names. Suggested follow-up: a loaded test/ts/run.sh loop to name the cause of the silent AAC loss (hypothesis 3), which is now logged and counted.

(Written by Claude Opus 5.5)

kixelated and others added 2 commits October 16, 2026 09:21
A frame late before its track's first went out counts as trimmed, not dropped, so the warning carries both counts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
#	quest/m1/ts-drift-startup.md
…es-2/ts-video-content

# Conflicts:
#	quest/m1/test-flakes-2/README.md
#	quest/m1/test-flakes-2/ts-resume-late-frames.md
…es-2/ts-video-content

# Conflicts:
#	quest/m1/test-flakes-2/README.md
A data track marks a discontinuity by skipping a sequence, which the consumer reads as media skipped upstream. Document that in stats::Export::gaps and the warning, and add quests for telling the break apart on the wire and for audio leading a keyframe after a trimmed video start.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kixelated
kixelated marked this pull request as ready for review October 16, 2026 18:00
@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-16T19:21:14.958101Z a9bd0e6 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.

@coderabbitai

coderabbitai Bot commented Oct 16, 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 3 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
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: f007cd08-c03a-4f02-b635-48339ccf153f
📥 Commits

Reviewing files that changed from the base of the PR and between 2c57810 and a9bd0e6.

📒 Files selected for processing: 18
  • quest/m1/test-flakes-2/README.md
  • quest/m1/test-flakes-2/ts-video-content.md
  • quest/m1/ts-reliability/README.md
  • quest/m1/ts-reliability/audio-lead.md
  • quest/m1/ts-reliability/data-break.md
  • rs/moq-cli/src/subscribe.rs
  • rs/moq-mux/benches/jitter.rs
  • rs/moq-mux/src/container/consumer.rs
  • rs/moq-mux/src/container/flv/export.rs
  • rs/moq-mux/src/container/mkv/export.rs
  • rs/moq-mux/src/container/source.rs
  • rs/moq-mux/src/container/ts/export.rs
  • rs/moq-mux/src/container/ts/export_test.rs
  • rs/moq-mux/src/container/ts/release.rs
  • rs/moq-mux/src/container/ts/stats.rs
  • rs/moq-mux/src/jitter.rs
  • test/ts/README.md
  • test/ts/run.sh
  • 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: 765f99b2f5

ℹ️ 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/container/ts/export.rs Outdated
The jitter buffer marked a track started on release, before the export dropped the frame for leading the video keyframe. A later loss on that track then counted as dropped from the output and skipped the start trim. Buffer::discard now counts the dropped release and leaves a track whose first frame it was not started.

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

kixelated commented Oct 16, 2026 •

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. Chef's kiss.

Reviewed commit: cb64c6def1

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

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>
@kixelated

Copy link
Copy Markdown
Collaborator Author

@codex review

The merge with #5666 changed the start trim: it now follows only gaps that cut a group short (ExportSource::cuts).

(Written by Claude Opus 5.5)

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

ℹ️ 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/container/ts/stats.rs
A delivery gap arms the leading floor, and the pictures below it were
discarded before the jitter buffer saw them, so they counted nowhere.
They are now dropped when the output holds frames ahead of the hole,
and trimmed when they lead the track's first keyframe.

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 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: a9bd0e6182

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

.filter_map(|track| self.jitter.peek(&track.pid))
.map(|queued| queued.frame.timestamp)
.min();
timestamp < next.map_or(start, |next| next.max(start))

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 Hold non-video frames until a start video is queued

When acquisition trims every frame from the initial video group before the next keyframe arrives, peek returns None and this fallback compares audio/data against the stale video_start. Frames at or after that timestamp are therefore muxed before any video, potentially leaving a long audio-only preamble that prevents live decoders from acquiring the in-band video configuration. While video_out is false and no video frame is queued, non-video releases should remain trimmed until the next video keyframe is available.

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.

Known, and deliberately deferred. This is decision 4 in the PR description. The maintainer chose to land the fix (trim non-video releases while no video went out and none is queued) only with a regression test that reproduces it. Three bounded paused-clock attempts did not get the acquisition to drop the whole start group before the next keyframe arrived, partly because a frame's arrived is when its source was last found empty. The exception is named in the description, and quest/m1/ts-reliability/audio-lead.md tracks the fix together with the fixture it needs.

(Written by Claude Opus 5.5)

@kixelated

Copy link
Copy Markdown
Collaborator Author

Ready to land at a9bd0e618.

Changes: an export now starts each track on a whole group. Until a track has sent a frame, a gap that cut its group short, or a frame dropped as late or stale, takes everything the track holds, and the track starts at its next sync frame. That fixes the #5565 flake, where a zero-budget live-edge join had its start group reset Old and wrote the head of the group ahead of a hole. Whole groups missing after one that ended trim nothing. Every drop is counted as dropped (after output began), trimmed (before it) or gaps (media skipped upstream), and each gap is logged. When decode.py fails, the harness prints these counts. #5666's leading-picture drop is merged in and counted the same way.

Decisions (details in the description):

  1. decode.py keeps failing on any loss after output began. There is no separate starvation check.
  2. The start trim stays uniform. The gaps doc and the warning say that a data track's declared break reads as a gap. quest/m1/ts-reliability/data-break.md tracks telling the two apart on the wire.
  3. The new fields keep the names trimmed and gaps.
  4. Audio can lead the keyframe after a stale start group. This was not reproduced in bounded attempts, so the description names the exception and quest/m1/ts-reliability/audio-lead.md tracks it. Codex's finding on the final head is this item.

Checks: CI is green on the final head, and cargo nextest -p moq-mux passes 1406 tests locally. There is no public API or wire change beyond the two new stats::Export fields.

Follow-up worth a quest: a loaded test/ts/run.sh loop to name the cause of the silent AAC loss (hypothesis 3), which is now counted.

(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 11bfa30 Oct 16, 2026
13 checks passed
@kixelated
kixelated deleted the quest/m1/test-flakes-2/ts-video-content branch October 16, 2026 20:35
kixelated added a commit that referenced this pull request Oct 17, 2026
#5713 merged with the cold-join regressions this draft gated on, so drop
the prerequisite (its quest is gone) and record what the recovery tests
reuse instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
kixelated added a commit that referenced this pull request Oct 17, 2026
Resolve quest/m2/qos-publisher-timeliness.md by taking main's m2 paths and
dropping its Related link to the schema quest this PR completes. Record the
export re-timing section decision in the TS health stats quest, reconciled
with ts::stats::Export's trimmed and gaps from #5713.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
shermerL pushed a commit to shermerL/moq that referenced this pull request Oct 17, 2026
…-dev#5746)

Co-authored-by: Luke Curley <432854+kixelated@users.noreply.github.com>
Co-authored-by: GPT-6 <noreply@openai.com>
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.

flaky: just test ts loses most of the first video group when it is evicted as Old just after subscribing (4 of 51 CI runs)

1 participant