feat(moq-mux): cap pending AU growth and add discontinuity()#1770
feat(moq-mux): cap pending AU growth and add discontinuity()#1770arielmol wants to merge 1 commit into
Conversation
Two additive, internal hardening changes to the codec importers (no wire or breaking API change). Bound the in-progress access unit: H.264/H.265/AV1 append every NAL/OBU to a pending `chunks` buffer and only flush it on a slice/frame, so a stream that never sends one (repeated AUD/SEI/SPS/PPS, or non-frame OBUs) grows it without limit. Every append now goes through a checked helper that bails past MAX_PENDING_FRAME_BYTES (16 MiB), including the cached SPS/PPS/VPS re-inserted before a keyframe. Add `discontinuity()` to signal a timeline boundary (e.g. a GStreamer FLUSH): it closes the open group via `Producer::finish_group()` so the next frame starts a fresh group, and for H.264/H.265/AV1 also drops the partial access unit. Exposed on `Framed` and on every framed codec importer; the moq-gst caller lands with the moqsink promotion (it lives on another branch). CONTEXT Discarded paths: - `seek(0)` as the discontinuity primitive: it rewrites the group sequence explicitly and fabricates false semantics. `finish_group()` closes the group without inventing a sequence. - No-op discontinuity for codecs without a partial AU (VP8/VP9/AAC/Opus): they still `finish_group()` so a FLUSH closes a clean group on every track, not only the video ones. - Toggling lenient_start or dropping deltas until a keyframe inside discontinuity(): that revives `with_lenient_start` under another name. discontinuity() defers to the Producer keyframe contract, forward compatible with the stricter contract in PR moq-dev#1766. - Leaving the SPS/PPS/VPS re-insertion on a raw `extend_from_slice`: a large cached parameter set would allocate past the cap before the final append checked it. Routing it through the helper validates before mutating. - Including the moq-gst `Pad::flush()` caller here: moqsinkspike lives on the gstsink branch, not main, so the caller ships with the promotion PR. - A `Stream` dispatcher discontinuity(): moqsinkspike only uses `Framed`. Key decisions: - discontinuity() means "timeline boundary", not "clear partial AU if any": all codecs close the group, only H.264/H.265/AV1 also clear the partial AU. - The cap is hardening against malformed input (a normal stream flushes every frame). moq-gst's per-buffer 16 MiB limit does not bound accumulation across buffers. - A discontinuity test followed by a keyframe does not prove finish_group() (the keyframe closes the group anyway), so the Framed-level test feeds a delta after discontinuity() and asserts it does not extend the closed group. Verified faithful by deleting finish_group() and watching it fail. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (8)
WalkthroughA 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches✨ Simplify code
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 |
|
Well the rabbit left no comments? 🐰 |
| /// Signal a timeline discontinuity: close the current group so the next | ||
| /// frame starts a fresh group. | ||
| pub fn discontinuity(&mut self) -> anyhow::Result<()> { | ||
| self.track.finish_group()?; |
There was a problem hiding this comment.
? each frame should be its own group
There was a problem hiding this comment.
Right, for AAC/Opus each frame already calls finish_group() after write() so discontinuity(..) here just refinishes a closed group already (no-op), I'll drop discontinuuty from the audio importers and keep it only on video
| /// Cap on a single in-progress temporal unit. Without a frame OBU the importer | ||
| /// never flushes `chunks`, so a stream of non-frame OBUs (sequence headers, | ||
| /// metadata) would grow it unboundedly; bail instead. | ||
| const MAX_PENDING_FRAME_BYTES: usize = 16 * 1024 * 1024; |
There was a problem hiding this comment.
Maybe we should export moq_net::MAX_FRAME_SIZE and use that instead?
There was a problem hiding this comment.
Maybe we should export
moq_net::MAX_FRAME_SIZEand use that instead?
Agreed
| /// Signal a timeline discontinuity: drop the in-progress temporal unit and | ||
| /// close the current group so the next frame starts a fresh group with a | ||
| /// keyframe. Tolerant before the track exists. | ||
| pub fn discontinuity(&mut self) -> anyhow::Result<()> { |
There was a problem hiding this comment.
IDK I really don't like this TS stuff leaking everywhere. Partially writing a frame is just gross.
I'm architecting moq-mux on dev now. There's a separate av1::Split module that finds frame boundaries, while the dumb av1::Import takes entire frames (not a stream). TS and stdin can use av1::Split, while everybody else uses av1::Import directly instead.
There was a problem hiding this comment.
Your call, just two questions:
- is it ok if I land just the pending AU cap here for now?
- once Split exists, where should a FLUSH hook in? SPlit or still surfaced via Framed? (moqsink will need this)
Summary
Two additive, internal hardening changes to the moq-mux codec importers.
No wire-protocol or breaking API change, so this targets
main.NAL/OBU to a pending buffer and only flush on a slice/frame; a stream
that never sends one (repeated AUD/SEI/SPS/PPS, or non-frame OBUs) grew
it without limit. Every append, including the SPS/PPS/VPS re-inserted
before a keyframe, now goes through a checked helper that bails past
MAX_PENDING_FRAME_BYTES(16 MiB). moq-gst's per-buffer limit does notbound accumulation across buffers, so this is the right layer for it.
discontinuity()to signal a timeline boundary (e.g. aGStreamer FLUSH). It closes the open group via
Producer::finish_group()so the next frame starts a fresh group, andfor H.264/H.265/AV1 also drops the partial access unit. It does not use
seek(0)(which would rewrite the group sequence) and does not touchlenient_start, so it stays forward compatible with the stricter keyframe
contract in fix(moq-mux): reject frames before the first keyframe instead of dropping #1766.
Public API (all additive, non-breaking)
Framed::discontinuity()discontinuity()on theh264/h265/av1/vp8/vp9/aac/opuscodec importers.
Not in this PR
The moq-gst
Pad::flush()caller that will invokeFramed::discontinuity()ships with the moqsink promotion (it lives on a separate branch). This PR is
the moq-mux half only.
Test plan
cargo test -p moq-mux: 243 passed (10 new: cap + discontinuity percodec, an end-to-end no-leak test, and a group-boundary test).
finish_group()fromdiscontinuity()makes it fail (2 frames vs 1).cargo clippy -p moq-mux --all-targetsclean,cargo fmt --checkclean.(Written by Claude)