fix(moq-mux): reject frames before the first keyframe instead of dropping#1766
fix(moq-mux): reject frames before the first keyframe instead of dropping#1766kixelated wants to merge 2 commits into
Conversation
…ping A MoQ group must start with a keyframe. The container `Producer` papered over this with a `with_lenient_start` opt-in that silently dropped non-keyframes arriving before the first group, which let a malformed stream produce an undecodable group without anyone noticing. Remove `with_lenient_start` and make the rule explicit: - The `Producer` stays strict: a delta with no open group is a protocol violation. Add `has_group()` so importers can check before writing. - Codec/format importers (h264 avc1+avc3, h265, flv) return a new typed `Error::MissingKeyframe` when a delta arrives before a keyframe anchors a group, plus `Error::is_missing_keyframe` to detect it through an anyhow chain. - The h264 avc3 path additionally errors on a keyframe it cannot configure (no inline SPS, no avcC seed), the genuinely undecodable case. - The TS importer is the sole exception: a transport stream can legitimately join mid-GOP, so `Stream::write` ignores `MissingKeyframe` (covering the post-seek discontinuity case too). FLV lets it propagate, since a valid FLV is expected to open on a keyframe. Tests: add `keyframe_without_sps_errors` and `delta_before_init_returns_missing_keyframe`; `survives_midstream_join` and `kyrion_dirtystart` continue to pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
WalkthroughThe pull request removes the 🚥 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 |
…_group The producer is the single owner of the keyframe invariant, so make `write` return `Error::MissingKeyframe` directly instead of having each importer re-derive the decision with a `has_group()` pre-check. `container::Producer::write` now returns `crate::Error` (the bound `crate::Error: From<C::Error>` holds for every container error, including `fmp4::Error` via the `Cmaf` variant), so the orphan-delta case returns `MissingKeyframe` and importers just propagate it. Removes `has_group()` and all the duplicated checks in the h264/h265/flv importers. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
@arielmol I missed I'll fix this on the I think on the dev branch I'll add a |
Summary
A MoQ group must start with a keyframe. The container
Producerpapered over this with awith_lenient_startopt-in that silently dropped non-keyframes arriving before the first group. That let a malformed stream produce an undecodable group with no signal, and the leniency was load-bearing in ways that weren't obvious at the call sites.This removes
with_lenient_startand makes the invariant explicit, with a single legitimate exception (MPEG-TS, which can join mid-GOP).What changed
Producerowns and enforces the invariant (producer.rs): a non-keyframe with no open group is rejected with a new typedError::MissingKeyframe.writenow returnscrate::Errordirectly (the boundcrate::Error: From<C::Error>holds for every container error, incl.fmp4::Errorvia theCmafvariant), so importers just?-propagate the error rather than re-deriving the decision. Removedwith_lenient_start.Error::MissingKeyframeplusError::is_missing_keyframe(&anyhow::Error)to detect it through an anyhow chain.MissingKeyframeleaves them ready for the next access unit. The h264 avc3 path additionally errors on a keyframe it cannot configure (no inline SPS, no avcC seed) — the genuinely undecodable case, kept distinct fromMissingKeyframeso it is never swallowed.Stream::writeignoresMissingKeyframe. This is the single choke point, so it also covers the post-seekdiscontinuity case.Why
The old behavior silently swallowed a real protocol violation. Making it a typed error that the
Producerreturns by default — with mid-stream tolerance opt-in at exactly one importer — means a group can never start without a keyframe, the one format where mid-GOP join is normal (TS) is explicitly the special case, and the keyframe decision lives in one place instead of being re-derived at every call site.Public API note
Removing
pub fn with_lenient_startfrommoq-mux's containerProduceris technically a breaking change, but the only callers were internal to the crate.Producer::write/finish/seek/finish_groupnow returncrate::Errorinstead of the (always-crate::Errorin practice) associatedC::Error.Error::MissingKeyframe(a#[non_exhaustive]enum) andError::is_missing_keyframeare additive. moq-mux isn't in the explicit breaking-change list, so this targetsmain— happy to retargetdevif reviewers prefer.Test plan
cargo test -p moq-mux— 236 passed, incl.survives_midstream_joinandkyrion_dirtystart_extracts_real_cueskeyframe_without_sps_errors,delta_before_init_returns_missing_keyframe; updatedfirst_frame_must_be_keyframeto expectMissingKeyframecargo clippy -p moq-mux --all-targetscleancargo fmt --checkclean (via nix toolchain)hang,moq-cli,moq-native)No cross-package sync needed: the JS side (
js/hang) has no equivalent leniency concept.(Written by Claude)