Skip to content

fix(moq-mux): codec/container correctness fixes from #1918 review (#1923)#1925

Merged
kixelated merged 2 commits into
mainfrom
claude/nostalgic-haibt-30556f
Jun 26, 2026
Merged

fix(moq-mux): codec/container correctness fixes from #1918 review (#1923)#1925
kixelated merged 2 commits into
mainfrom
claude/nostalgic-haibt-30556f

Conversation

@kixelated

Copy link
Copy Markdown
Collaborator

Fixes the deferred findings in #1923 — pre-existing moq-mux codec/container bugs surfaced by CodeRabbit's review of the #1918 backport. Each finding gets a focused fix and a regression test.

Findings & fixes

  • codec/h264/split.rs + codec/h265/split.rs — consecutive bare IDRs merged. A bare IDR arriving right after a delta picture in the same decode chunk folded two access units into one frame (and mis-flagged it a keyframe), because the IDR arm never closed an already-open slice on the first slice of a new picture. It now does, mirroring the non-keyframe slice path's first_mb_in_slice / first_slice_segment_in_pic_flag check.

  • codec/aac/mod.rs — explicit (index 15) sample rate misparsed. AudioSpecificConfig is bit-packed; an explicit 24-bit sample rate pushes channelConfiguration off byte boundaries, so the byte-aligned parser decoded both sample_rate and channel_count wrong. Rewritten over an MSB-first bit reader. The previously-impossible explicit-rate round-trip now passes.

  • container/consumer.rs — decode errors treated as evictions. Any poll_read error skipped the group as if it had aged out of the relay cache. A malformed-payload decode error is not an eviction and now propagates. The moq_net group's own terminal state (poll_finished) is the source of truth: an aborted/evicted group reports the transport error, while a decode failure leaves the group live or cleanly finished.

  • container/fmp4/mod.rs + container/producer.rs — duration-less multi-sample fragments lost timestamps. A CMAF fragment with several samples where the non-final samples carried no duration decoded them all at the same DTS. Per the issue's two-pronged suggestion (chosen in discussion): decode() now rejects such fragments (MissingSampleDuration) instead of silently collapsing, and the latency-batching Producer backfills every batched sample's duration (gap to the next sample in decode order, or the rolling keyframe for the last) so CMAF batches stay decodable end-to-end. Frames that already carry a duration (e.g. fMP4 passthrough) keep it; a backwards gap (B-frame) is left unset. This changes CMAF encoded bytes for the batched-from-duration-less-source path, which is the intended fix. Related to Support negative / signed presentation timestamps (audio priming, pacing, B-frame reorder) #1917 signed-timestamp work.

  • catalog/filter.rs + catalog/target.rs — never returned EOF. After upstream EOF the streams kept the final snapshot and parked in Pending forever. Decision (per discussion): end with upstream — once the final filtered/selected snapshot is emitted and inner is exhausted, the stream finishes.

Public API changes

  • moq_mux::container::fmp4::Error gains a MissingSampleDuration variant. The enum is #[non_exhaustive], so this is additive.
  • No trait signatures or other pub shapes changed.

Behavior changes (not API)

  • CMAF fragments produced by the latency-batching Producer from a duration-less source now carry per-sample durations (byte change vs. before; required so they decode correctly).
  • catalog::Filter / catalog::Target now end when their upstream ends, rather than staying alive for post-EOF retargeting.

Test plan

  • cargo test -p moq-mux (306 passing, incl. 10 new)
  • just rs check, just js check, remark markdown lint
  • Not e2e-tested on live streams

The remaining just check failures in my environment are confined to the worktree's git-ignored .direnv/ generated flake-input sources (shellcheck/taplo), not this change.

Targets main since the #1918 backport landed there; reviewers can redirect to dev if preferred.

🤖 Generated with Claude Code

(Written by Claude)

)

Five pre-existing findings in the moq-mux codec/container lineage, surfaced
by CodeRabbit's review of the #1918 backport. Each gets a focused fix plus a
regression test.

- h264/h265 split: a bare IDR arriving right after a delta picture in the
  same decode chunk folded both access units into one frame (and mis-flagged
  it a keyframe). The IDR arm now closes an already-open slice on the first
  slice of a new picture, mirroring the non-keyframe slice path.

- aac: AudioSpecificConfig is bit-packed, so an explicit (index 15) 24-bit
  sample rate pushes channelConfiguration off byte boundaries. The old
  byte-aligned parser misread both fields. Parse via an MSB-first bit reader.

- container/consumer: a group read error was always treated as a relay
  eviction and skipped. A malformed-payload decode error is not an eviction;
  it now propagates. The group's own terminal state (poll_finished) is the
  source of truth: an aborted/evicted group reports the transport error,
  while a decode failure leaves the group live or cleanly finished.

- container/fmp4: a multi-sample fragment whose non-final samples carry no
  duration left every following sample stuck at the same DTS, silently
  collapsing timestamps. decode() now rejects such fragments, and the
  latency-batching producer backfills every batched sample's duration (from
  the next sample in decode order, or the rolling keyframe for the last) so
  CMAF batches stay decodable. Frames that already carry a duration keep it.

- catalog/filter + catalog/target: after upstream EOF the streams retained
  the final snapshot and parked in Pending forever. They now end with
  upstream once the final filtered/selected snapshot is emitted.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@kixelated kixelated marked this pull request as ready for review June 26, 2026 20:43
@coderabbitai

coderabbitai Bot commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@kixelated, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 26 minutes and 57 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits.

🚦 How do rate limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f471b612-b3ab-4054-94cc-3c8fd5c5f43e

📥 Commits

Reviewing files that changed from the base of the PR and between d27593e and a784c17.

📒 Files selected for processing (5)
  • rs/moq-mux/src/catalog/filter.rs
  • rs/moq-mux/src/catalog/msf/consumer.rs
  • rs/moq-mux/src/catalog/msf/mod.rs
  • rs/moq-mux/src/catalog/target.rs
  • rs/moq-mux/src/container/consumer.rs

Walkthrough

The PR updates EOF handling in catalog Filter and Target, rewrites AAC AudioSpecificConfig parsing with a bit reader, changes H.264 and H.265 splitters to close access units on bare IDR starts, refines consumer error handling so decode failures propagate separately from aborts, adds fMP4 missing-duration validation, and makes producer flush backfill missing sample durations across buffered frames. Tests were added or updated for each area.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the PR as moq-mux codec/container correctness fixes from review #1923.
Description check ✅ Passed The description matches the changeset and summarizes the codec, container, and catalog fixes with tests.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
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.
✨ Finishing Touches
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch claude/nostalgic-haibt-30556f

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
rs/moq-mux/src/catalog/target.rs (1)

143-158: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Make EOF terminal after the final selected snapshot.

Like Filter, Target keeps last_input after EOF. After poll_next returns None, a later retarget can reuse that cached catalog and produce Some(...) again. Clear the cached input when EOF is reached/finalized, and add a post-EOF retarget assertion.

Suggested fix direction
 			Poll::Ready(Ok((emit, epoch))) => {
 				self.last_epoch = epoch;
 				self.fresh_input = false;
+				if inner_eof {
+					self.last_input = None;
+				}
 				Poll::Ready(Ok(Some(emit)))
 			}
@@
 				if inner_eof {
+					self.last_input = None;
+					self.fresh_input = false;
 					Poll::Ready(Ok(None))
 				} else {
 					Poll::Pending

Also applies to: 416-418

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@rs/moq-mux/src/catalog/target.rs` around lines 143 - 158, Make EOF terminal
in Target::poll_next by clearing the cached input when the stream reaches final
EOF, so a later retarget cannot reuse stale catalog state and emit items again.
Update the Target polling logic around the Poll::Ready(Ok(None)) path (and any
finalize/EOF handling nearby) to drop last_input/fresh_input once EOF is
determined, then add an assertion in the retarget path to verify retargeting
after EOF does not reopen the stream.
rs/moq-mux/src/catalog/filter.rs (1)

131-143: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Clear the cached snapshot when EOF becomes terminal.

last_input remains populated after the final emit/EOF path. A later set_video or set_audio can make state_consumer.poll return Ready and re-emit Some(...) after this stream already returned None, which violates the catalog Stream EOF contract. Please clear the cached input when the final snapshot has been emitted or when returning EOF, and extend this test with a post-EOF filter change.

Suggested fix direction
 			Poll::Ready(Ok((emit, epoch))) => {
 				self.last_epoch = epoch;
 				self.fresh_input = false;
+				if inner_eof {
+					self.last_input = None;
+				}
 				Poll::Ready(Ok(Some(emit)))
 			}
@@
 				if inner_eof {
+					self.last_input = None;
+					self.fresh_input = false;
 					Poll::Ready(Ok(None))
 				} else {
 					Poll::Pending

Also applies to: 299-307

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@rs/moq-mux/src/catalog/filter.rs` around lines 131 - 143, The EOF handling in
the catalog filter stream leaves the cached input state alive after the final
emit/terminal None path, so later updates can cause `state_consumer.poll` to
produce another item after EOF. Update the stream logic in the filtering path
that handles `Poll::Ready(Ok((emit, epoch)))`, `Poll::Pending`, and the
`inner_eof`/`Poll::Ready(Ok(None))` branches to clear the cached snapshot/input
state once the final snapshot has been emitted or when returning EOF, and make
sure `set_video`/`set_audio` cannot revive output after termination. Also extend
the existing EOF test around the filter stream to cover a post-EOF filter change
and verify no additional item is emitted.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@rs/moq-mux/src/codec/aac/mod.rs`:
- Around line 77-81: `Config::parse` is draining the remaining AAC config bytes,
which makes the trailing-byte validation in `derive_from_codec_config`
ineffective. Update `Config::parse` to stop consuming unread bytes so callers
can detect malformed init data, or if the new contract intentionally ignores
extras, remove/rework the `AacTrailingBytes` check to match that behavior. Keep
the fix centered around `Config::parse` and `derive_from_codec_config`.

In `@rs/moq-mux/src/container/consumer.rs`:
- Around line 1371-1389: The async test decode_error_propagates relies on
tokio::time::timeout, so it must explicitly pause Tokio time before any timed
await. Add tokio::time::pause() at the start of the test, before creating the
Consumer or calling consumer.read(), so the timeout behavior is deterministic
and consistent with the async test guidelines.

---

Outside diff comments:
In `@rs/moq-mux/src/catalog/filter.rs`:
- Around line 131-143: The EOF handling in the catalog filter stream leaves the
cached input state alive after the final emit/terminal None path, so later
updates can cause `state_consumer.poll` to produce another item after EOF.
Update the stream logic in the filtering path that handles
`Poll::Ready(Ok((emit, epoch)))`, `Poll::Pending`, and the
`inner_eof`/`Poll::Ready(Ok(None))` branches to clear the cached snapshot/input
state once the final snapshot has been emitted or when returning EOF, and make
sure `set_video`/`set_audio` cannot revive output after termination. Also extend
the existing EOF test around the filter stream to cover a post-EOF filter change
and verify no additional item is emitted.

In `@rs/moq-mux/src/catalog/target.rs`:
- Around line 143-158: Make EOF terminal in Target::poll_next by clearing the
cached input when the stream reaches final EOF, so a later retarget cannot reuse
stale catalog state and emit items again. Update the Target polling logic around
the Poll::Ready(Ok(None)) path (and any finalize/EOF handling nearby) to drop
last_input/fresh_input once EOF is determined, then add an assertion in the
retarget path to verify retargeting after EOF does not reopen the stream.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 77277292-d2a4-4679-a4c6-6304c6470e14

📥 Commits

Reviewing files that changed from the base of the PR and between 97e7914 and d27593e.

📒 Files selected for processing (9)
  • rs/moq-mux/src/catalog/filter.rs
  • rs/moq-mux/src/catalog/target.rs
  • rs/moq-mux/src/codec/aac/mod.rs
  • rs/moq-mux/src/codec/h264/split.rs
  • rs/moq-mux/src/codec/h265/split.rs
  • rs/moq-mux/src/container/consumer.rs
  • rs/moq-mux/src/container/fmp4/import_test.rs
  • rs/moq-mux/src/container/fmp4/mod.rs
  • rs/moq-mux/src/container/producer.rs

Comment thread rs/moq-mux/src/codec/aac/mod.rs
Comment thread rs/moq-mux/src/container/consumer.rs
- catalog filter/target: make EOF terminal. Once the final filtered/selected
  snapshot is emitted and upstream has ended, drop the retained input so a
  later set_video/set_audio can't revive the stream after it returned None
  (the Stream once-None-always-None contract). The retarget-after-snapshot
  test now uses a still-live upstream to keep exercising the live retarget path.

- msf consumer: drop the unreachable AacTrailingBytes check (and its unused
  error variant). AudioSpecificConfig carries valid variable-length SBR/PS
  extensions after the core fields, so Config::parse intentionally drains the
  buffer; the trailing-byte check could never fire (the parser drained before
  this PR too) and wrongly implied HE-AAC configs are malformed.

- consumer test: pause Tokio time in decode_error_propagates (uses timeout).

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

Copy link
Copy Markdown
Collaborator Author

Addressed the two outside-diff CodeRabbit findings (Filter/Target EOF) in a784c17:

  • Both catalog::Filter and catalog::Target now make EOF terminal: once the final filtered/selected snapshot is emitted and upstream has ended, last_input is dropped, so a later set_video/set_audio can no longer make the closure re-emit Some(...) after the stream returned None. This upholds the once-None-always-None Stream contract.
  • Extended both ends_after_upstream_eof tests with a post-EOF retarget that asserts the stream stays ended. The pre-EOF retarget test (set_video_after_snapshot_reemits) now uses a still-live upstream so it keeps exercising live retargeting.

The two inline comments (AAC trailing-bytes, test tokio::time::pause()) are replied to and resolved.

(Written by Claude)

@kixelated kixelated enabled auto-merge (squash) June 26, 2026 20:58
@kixelated kixelated merged commit 1dac0a5 into main Jun 26, 2026
2 checks passed
@kixelated kixelated deleted the claude/nostalgic-haibt-30556f branch June 26, 2026 21:04
@moq-bot moq-bot Bot mentioned this pull request Jun 26, 2026
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