mirror of
https://github.com/freemkv/libfreemkv.git
synced 2026-10-07 18:13:10 +00:00
mux(h264): gate open-GOP promotion on all-intra slices and base view
The open-GOP keyframe promotion trusted only the first slice's coding type and wasn't gated on the base view. A picture whose first slice is I but whose later slices are P/B is not a clean random-access point, and an intra MVC dependent view is never an independent anchor. Track every VCL slice's coding type (all_vcl_intra) and require a real VCL slice, then promote only when all slices are intra AND !mvc. Add mixed-slice and MVC non-promotion tests.
This commit is contained in:
+69
-7
@@ -208,6 +208,13 @@ impl CodecParser for H264Parser {
|
||||
let mut keyframe = false;
|
||||
// Picture coding type, MEASURED from the first coded slice's header.
|
||||
let mut coding_type: Option<CodingType> = None;
|
||||
// Open-GOP promotion needs EVERY VCL slice to be intra, not just the
|
||||
// first: a picture whose first slice is I but whose later slices are
|
||||
// P/B is not a clean random-access point. `all_vcl_intra` stays true
|
||||
// only while every slice header parses AND codes as I/SI; `saw_vcl`
|
||||
// guards against promoting a param-set-only access unit.
|
||||
let mut all_vcl_intra = true;
|
||||
let mut saw_vcl = false;
|
||||
// Did this access unit already carry each param-set type in-band?
|
||||
let mut emitted_sps = false;
|
||||
let mut emitted_pps = false;
|
||||
@@ -243,16 +250,26 @@ impl CodecParser for H264Parser {
|
||||
if nal_type == NAL_SLICE_IDR {
|
||||
keyframe = true;
|
||||
}
|
||||
// Measure coding type from the first slice header (§7.3.3:
|
||||
// Measure coding type from each slice header (§7.3.3:
|
||||
// first_mb_in_slice, slice_type, both ue(v)) after unescaping
|
||||
// EBSP (§7.3.1) — large first_mb_in_slice needs escaped 0x00 0x00.
|
||||
if (nal_type == NAL_SLICE_NON_IDR || nal_type == NAL_SLICE_IDR)
|
||||
&& coding_type.is_none()
|
||||
{
|
||||
// The FIRST slice sets the picture's `coding_type`; EVERY slice
|
||||
// feeds `all_vcl_intra` for the open-GOP promotion below.
|
||||
if nal_type == NAL_SLICE_NON_IDR || nal_type == NAL_SLICE_IDR {
|
||||
saw_vcl = true;
|
||||
let header = unescape_ebsp_prefix(&nal[1..]);
|
||||
let mut br = BitReader::new(&header);
|
||||
if let (Some(_first_mb), Some(slice_type)) = (br.read_ue(), br.read_ue()) {
|
||||
coding_type = h264_slice_coding_type(slice_type);
|
||||
let ct = h264_slice_coding_type(slice_type);
|
||||
if coding_type.is_none() {
|
||||
coding_type = ct;
|
||||
}
|
||||
if ct != Some(CodingType::I) {
|
||||
all_vcl_intra = false;
|
||||
}
|
||||
} else {
|
||||
// An unparseable slice header cannot be proven intra.
|
||||
all_vcl_intra = false;
|
||||
}
|
||||
}
|
||||
// A NAL longer than u32::MAX can't be length-prefixed in the
|
||||
@@ -273,8 +290,11 @@ impl CodecParser for H264Parser {
|
||||
|
||||
// Open-GOP resync anchor: BD titles use open GOPs whose random-access
|
||||
// point is a non-IDR I-frame, not always SEI-tagged. Without treating it
|
||||
// as a keyframe, the resync gate (`mux/resync.rs`) can miss it and drop frames to EOF.
|
||||
if coding_type == Some(CodingType::I) {
|
||||
// as a keyframe, the resync gate (`mux/resync.rs`) can miss it and drop
|
||||
// frames to EOF. Promote only when EVERY VCL slice is intra (a picture
|
||||
// with a P/B slice is not a random-access point) and only on the base
|
||||
// view (`!mvc`): a dependent MVC view is never an independent anchor.
|
||||
if saw_vcl && all_vcl_intra && !mvc {
|
||||
keyframe = true;
|
||||
}
|
||||
|
||||
@@ -603,6 +623,48 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
// A picture whose FIRST slice is I but whose LATER slice is P/B is not a
|
||||
// clean random-access point: the open-GOP promotion must require EVERY VCL
|
||||
// slice to be intra, not trust the first slice alone.
|
||||
#[test]
|
||||
fn a_mixed_slice_picture_with_a_p_slice_is_not_promoted() {
|
||||
// AU with two coded slices: slice 1 is I (0x88, slice_type=7), slice 2
|
||||
// is P (0xC0, slice_type=0). Both NAL type 1, nal_ref_idc 3 -> 0x61.
|
||||
let au = vec![
|
||||
0x00, 0x00, 0x01, 0x61, 0x88, // I slice
|
||||
0x00, 0x00, 0x01, 0x61, 0xC0, // P slice
|
||||
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 picture's coding type is still the first slice's (I)",
|
||||
);
|
||||
assert!(
|
||||
!frames[0].keyframe,
|
||||
"a picture with a P slice is not a random-access point and must not promote",
|
||||
);
|
||||
}
|
||||
|
||||
// MVC dependent-view (Blu-ray 3D right-eye) passthrough: an intra dependent
|
||||
// view is not an independent random-access anchor, so it must NOT be
|
||||
// promoted to a keyframe even when every slice is intra.
|
||||
#[test]
|
||||
fn an_intra_mvc_dependent_view_is_not_promoted() {
|
||||
// Single non-IDR I slice, as in open_gop_intra_access_unit_is_a_keyframe.
|
||||
let au = vec![0x00, 0x00, 0x01, 0x61, 0x88, 0x00];
|
||||
let mut parser = H264Parser::new().with_mvc_passthrough(true);
|
||||
let frames = parser.parse(&make_pes(au, Some(0)));
|
||||
assert_eq!(frames.len(), 1);
|
||||
assert!(
|
||||
!frames[0].keyframe,
|
||||
"an MVC dependent view is never an independent anchor, intra or not",
|
||||
);
|
||||
}
|
||||
|
||||
// The slice-header parse must remove emulation-prevention bytes first
|
||||
// (ISO/IEC 14496-10 §7.3.1/§7.4.1) before decoding ue(v) fields.
|
||||
// See docs/h264.md — slice_header_parse_removes_emulation_prevention_bytes.
|
||||
|
||||
Reference in New Issue
Block a user