mux/h264: treat open-GOP intra access unit as a resync keyframe

The B1 resync gate (mux/resync.rs) arms on a concealed-gap / clip-seam
discontinuity and drops video frames until it sees a keyframe. The H.264
parser flagged a keyframe only on an IDR slice (NAL type 5), so on a BD
open-GOP tail whose only random-access point is a non-IDR I-frame (the
recovery point) the gate never disarmed and dropped every remaining frame
to EOF — truncating the video by up to a GOP-plus tail and failing the
cli-acceptance timeline_sound strict span check for BD.

Promote an intra-coded access unit (measured coding_type == I) to a
keyframe, alongside the existing IDR-by-NAL-type flag. This is the H.264
equivalent of HEVC's open-GOP IRAP (CRA) handling, which the HEVC parser
already flags by NAL type — H.264 has no such NAL tag, so the measured
intra coding type is the signal. Keying off coding type rather than a
recovery_point SEI covers every signalling: an SEI-tagged recovery point
(which is intra by construction) and a disc that marks the recovery point
by GOP structure alone. Consistent with videomap.rs's model that a
keyframe IS the intra decode-restart point; the open-GOP anchor now also
gets the self-contained SPS/PPS re-assert, mirroring HEVC at CRA. IDR
stays flagged by NAL type, so a slice whose header fails to parse
(coding_type == None) is unaffected. HEVC, DVD/MPEG-2 unchanged.

Tests: open_gop_intra_access_unit_is_a_keyframe (red before this change),
inter_coded_access_unit_is_not_a_keyframe (precision guard).
This commit is contained in:
Matthew Jackson
2026-08-19 13:32:30 -07:00
parent 040bc8b14d
commit e7b6f727d9
+73 -1
View File
@@ -1,7 +1,8 @@
//! H.264 (AVC) elementary stream parser. //! H.264 (AVC) elementary stream parser.
//! //!
//! Extracts SPS and PPS NAL units for MKV codecPrivate. //! Extracts SPS and PPS NAL units for MKV codecPrivate.
//! Detects keyframes (IDR slices). //! Detects keyframes: IDR slices, and open-GOP intra-coded access units (the
//! non-IDR recovery points BD titles use — see the intra promotion in `parse`).
//! Each PES packet = one access unit = one frame. //! Each PES packet = one access unit = one frame.
use super::coding::{CodingType, PictureInfo}; use super::coding::{CodingType, PictureInfo};
@@ -351,6 +352,26 @@ impl CodecParser for H264Parser {
return Vec::new(); return Vec::new();
} }
// Open-GOP resync anchor: an intra-coded access unit is a keyframe even
// when it is NOT an IDR. BD H.264 titles use open GOPs whose only
// random-access point is a non-IDR I-frame (the recovery point, which an
// encoder MAY additionally tag with a recovery_point SEI). Keying off the
// already-measured picture coding type covers every signalling — a
// recovery_point SEI, or a disc that marks the recovery point only by GOP
// structure with no SEI at all — where SEI parsing alone would miss the
// latter. Without this, the B1 resync gate (`mux/resync.rs`), once armed
// by a concealed-gap / clip-seam discontinuity, never sees a keyframe on
// such a tail and drops every frame to EOF, truncating the video. HEVC
// does not hit this: its open-GOP IRAP (CRA) is a distinct NAL type
// already flagged a keyframe (`mux/codec/hevc.rs`); H.264 has no such NAL
// tag, so the intra coding type is the equivalent signal. Consistent with
// `videomap.rs`'s model that a keyframe IS the intra decode-restart point.
// IDR is still flagged by NAL type in the loop above, so a slice whose
// header failed to parse (`coding_type == None`) is unaffected here.
if coding_type == Some(CodingType::I) {
keyframe = true;
}
// Every keyframe is self-contained: re-assert the active SPS/PPS in-band // Every keyframe is self-contained: re-assert the active SPS/PPS in-band
// ahead of the slices (even when unchanged vs codecPrivate) so a decoder // ahead of the slices (even when unchanged vs codecPrivate) so a decoder
// that dropped the set at a reset recovers, and a stale avcC re-apply // that dropped the set at a reset recovers, and a stale avcC re-apply
@@ -694,6 +715,57 @@ mod tests {
} }
} }
/// Open-GOP resync fix: a non-IDR intra-coded access unit MUST be flagged a
/// keyframe so the B1 resync gate (`mux/resync.rs`) can disarm on a BD open-
/// GOP tail instead of dropping every frame to EOF and truncating the video.
/// BD H.264 titles reach random access at a non-IDR I-frame (the recovery
/// point); HEVC gets this free from the CRA NAL type, H.264 does not, so the
/// measured intra coding type is the signal. Red before the intra promotion
/// in `parse` — a non-IDR I-slice used to yield `keyframe == false`.
#[test]
fn open_gop_intra_access_unit_is_a_keyframe() {
// Annex B AU: one non-IDR coded slice (NAL type 1, nal_ref_idc 3 → 0x61)
// whose header decodes first_mb_in_slice = 0 (ue = "1") and slice_type = 7
// (ue = "0001000"), i.e. an all-I slice. The two ue(v) fields pack into
// one byte: "1" + "0001000" = 0b1000_1000 = 0x88.
let au = vec![0x00, 0x00, 0x01, 0x61, 0x88, 0x00];
let mut parser = H264Parser::new();
let frames = parser.parse(&make_pes(au, Some(0)));
assert_eq!(frames.len(), 1, "one PES access unit → one frame");
assert_eq!(
frames[0].coding.map(|c| c.coding_type()),
Some(CodingType::I),
"the slice header measures an I picture",
);
assert!(
frames[0].keyframe,
"an open-GOP intra access unit is a resync-safe keyframe even without an IDR",
);
}
/// Guard the promotion's precision: a non-intra (P) access unit must NOT be
/// promoted to a keyframe, or the resync gate would resume on a frame that
/// carries inter-references to dropped data and the MKV cue index would gain
/// bogus random-access points.
#[test]
fn inter_coded_access_unit_is_not_a_keyframe() {
// Same NAL type 1, but slice_type = 0 (P): ue "1", so with first_mb = 0
// the byte is "1" + "1" padded = 0b1100_0000 = 0xC0.
let au = vec![0x00, 0x00, 0x01, 0x61, 0xC0, 0x00];
let mut parser = H264Parser::new();
let frames = parser.parse(&make_pes(au, Some(0)));
assert_eq!(frames.len(), 1);
assert_eq!(
frames[0].coding.map(|c| c.coding_type()),
Some(CodingType::P),
"the slice header measures a P picture",
);
assert!(
!frames[0].keyframe,
"an inter-coded access unit must never be a keyframe",
);
}
/// The slice-header parse must remove emulation-prevention bytes first /// The slice-header parse must remove emulation-prevention bytes first
/// (ISO/IEC 14496-10 §7.3.1/§7.4.1): the payload after the NAL header is /// (ISO/IEC 14496-10 §7.3.1/§7.4.1): the payload after the NAL header is
/// EBSP, and a 0x03 following 0x00 0x00 is not part of the RBSP. /// EBSP, and a 0x03 following 0x00 0x00 is not part of the RBSP.