mux/hevc: measure coding type from slice_type; reuse shared BitReader

Honest PictureInfo population for HEVC, on the same principle as H.264.

- Consolidate the bit reader: hevc.rs had its own BitReader (used by the SPS
  parser); h264 just gained one in startcode. Promote startcode's to the single
  shared pub(crate) reader (adds read_bits) and delete hevc's copy — one proven
  primitive, reused (SPS parse + both slice-type decoders).
- hevc: decode slice_type from the first coded slice's slice_segment_header
  (H.265 §7.3.6.1) → I/P/B (§7.4.7.1). The offset to slice_type depends on
  num_extra_slice_header_bits, which lives in the PPS — so we parse it from the
  ACTIVE PPS (§7.3.2.3) and only measure slice_type when that PPS is known.
  With no active PPS we decline rather than guess: coding stays None, honestly
  absent. Set coding = coding_type_only(...) and source = pes.source; field
  order (pic_struct SEI) is not decoded, so field_order() is honestly None.
- Tests: I/P/B from real slice headers, source carry, field-order absence, and
  the no-PPS honest-omission case.
This commit is contained in:
Matthew Jackson
2026-06-25 20:44:15 -07:00
parent 534eca502c
commit 6acc26a802
2 changed files with 148 additions and 58 deletions
+138 -57
View File
@@ -4,7 +4,8 @@
//! Detects keyframes (IRAP pictures: IDR, CRA, BLA). //! Detects keyframes (IRAP pictures: IDR, CRA, BLA).
//! Each PES packet = one access unit = one frame. //! Each PES packet = one access unit = one frame.
use super::startcode::{find_start_code, skip_start_code}; use super::coding::{CodingType, PictureInfo};
use super::startcode::{BitReader, find_start_code, skip_start_code};
use super::{CodecParser, Frame, PesPacket, pts_to_ns}; use super::{CodecParser, Frame, PesPacket, pts_to_ns};
// HEVC NAL unit types // HEVC NAL unit types
@@ -26,6 +27,59 @@ const NAL_RSV_IRAP_VCL23: u8 = 23;
// Access): a decoder then sets NoRaslOutput and discards the RASL cleanly with // Access): a decoder then sets NoRaslOutput and discards the RASL cleanly with
// no error. See `mark_clip_boundary` / the IRAP arm in `parse`. // no error. See `mark_clip_boundary` / the IRAP arm in `parse`.
const NAL_CRA_NUT: u8 = 21; const NAL_CRA_NUT: u8 = 21;
/// Highest VCL (coded-slice) NAL type. Rec. ITU-T H.265 Table 7-1: types 0..=31
/// are VCL, 32..=63 non-VCL. A coded slice carries a `slice_type`.
const NAL_VCL_MAX: u8 = 31;
/// `num_extra_slice_header_bits` from a HEVC PPS NAL (H.265 §7.3.2.3): after the
/// 2-byte NAL header, skip `pps_pic_parameter_set_id` + `pps_seq_parameter_set_id`
/// (both `ue(v)`) and `dependent_slice_segments_enabled_flag` +
/// `output_flag_present_flag` (`u(1)` each), then read `u(3)`. `None` if the PPS
/// is too short to parse — the caller then declines to guess a slice type.
fn hevc_num_extra_slice_header_bits(pps_nal: &[u8]) -> Option<u32> {
let mut br = BitReader::new(pps_nal.get(2..)?);
br.read_ue()?; // pps_pic_parameter_set_id
br.read_ue()?; // pps_seq_parameter_set_id
br.skip_bits(2)?; // dependent_slice_segments_enabled_flag, output_flag_present_flag
let mut n = 0u32;
for _ in 0..3 {
n = (n << 1) | br.read_bit()?;
}
Some(n)
}
/// Map a HEVC `slice_type` (H.265 §7.4.7.1, Table 7-7) to a coding type:
/// 0 = B, 1 = P, 2 = I. `None` for any other value (malformed header).
fn hevc_slice_coding_type(slice_type: u32) -> Option<CodingType> {
match slice_type {
0 => Some(CodingType::B),
1 => Some(CodingType::P),
2 => Some(CodingType::I),
_ => None,
}
}
/// Measure the coding type from the FIRST coded slice of an access unit
/// (H.265 §7.3.6.1 `slice_segment_header`). Reads only the leading fields of the
/// first slice segment: `first_slice_segment_in_pic_flag` u(1), the IRAP
/// `no_output_of_prior_pics_flag` u(1), `slice_pic_parameter_set_id` ue(v), the
/// `num_extra_slice_header_bits` reserved bits, then `slice_type` ue(v). Returns
/// `None` for a non-first slice or on truncation — never a guess. `num_extra`
/// MUST come from the active PPS so the bit offset to `slice_type` is exact.
fn hevc_first_slice_coding_type(nal: &[u8], nal_type: u8, num_extra: u32) -> Option<CodingType> {
let mut br = BitReader::new(nal.get(2..)?); // RBSP after the 2-byte NAL header
if br.read_bit()? != 1 {
return None; // not the first slice segment of the picture
}
if (NAL_BLA_W_LP..=NAL_RSV_IRAP_VCL23).contains(&nal_type) {
br.skip_bits(1)?; // no_output_of_prior_pics_flag (IRAP only)
}
br.read_ue()?; // slice_pic_parameter_set_id
// First slice → no slice_segment_address and dependent_slice_segment_flag is
// 0, so slice_type follows the reserved bits directly.
br.skip_bits(num_extra)?; // slice_reserved_flag[i]
hevc_slice_coding_type(br.read_ue()?)
}
/// HEVC (H.265) Annex B → MKV codec parser: extracts VPS/SPS/PPS for the hvcC /// HEVC (H.265) Annex B → MKV codec parser: extracts VPS/SPS/PPS for the hvcC
/// codecPrivate, detects IRAP keyframes, and converts each PES access unit into /// codecPrivate, detects IRAP keyframes, and converts each PES access unit into
@@ -314,6 +368,8 @@ impl CodecParser for HevcParser {
let data = &pes.data; let data = &pes.data;
let mut keyframe = false; let mut keyframe = false;
// Picture coding type, MEASURED from the first coded slice's header.
let mut coding_type: Option<CodingType> = None;
// Track whether THIS access unit already carried each param-set type // Track whether THIS access unit already carried each param-set type
// in-band (a redefinition vs codecPrivate). Used after the scan to // in-band (a redefinition vs codecPrivate). Used after the scan to
// re-assert the active set at a keyframe the source left bare. // re-assert the active set at a keyframe the source left bare.
@@ -349,6 +405,25 @@ impl CodecParser for HevcParser {
// HEVC NAL header: 2 bytes. Type is bits 1-6 of first byte. // HEVC NAL header: 2 bytes. Type is bits 1-6 of first byte.
let nal_type = (data[nal_start] >> 1) & 0x3F; let nal_type = (data[nal_start] >> 1) & 0x3F;
// Measure the coding type from the FIRST coded slice (VCL NAL
// 0..=31). Only attempted once the active PPS is known, so
// `num_extra_slice_header_bits` — and thus the bit offset to
// `slice_type` — is EXACT. With no PPS we decline rather than
// guess, leaving coding `None` (honestly absent).
if coding_type.is_none() && nal_type <= NAL_VCL_MAX {
if let Some(num_extra) = self
.cur_pps
.as_deref()
.and_then(hevc_num_extra_slice_header_bits)
{
coding_type = hevc_first_slice_coding_type(
&data[nal_start..end],
nal_type,
num_extra,
);
}
}
match nal_type { match nal_type {
NAL_VPS => { NAL_VPS => {
emitted_vps |= handle_param_set( emitted_vps |= handle_param_set(
@@ -446,8 +521,11 @@ impl CodecParser for HevcParser {
} }
vec![Frame { vec![Frame {
coding: None, // Coding-type only: HEVC field order (pic_struct, from a pic_timing
source: None, // SEI) is not decoded here, so field_order() stays None — honestly
// absent, never guessed.
coding: coding_type.map(PictureInfo::coding_type_only),
source: pes.source,
pts_ns, pts_ns,
keyframe, keyframe,
data: frame_data, data: frame_data,
@@ -582,60 +660,6 @@ struct SpsChroma {
temporal_id_nesting_flag: u8, temporal_id_nesting_flag: u8,
} }
/// Minimal MSB-first bit reader over a byte slice.
struct BitReader<'a> {
data: &'a [u8],
bit_pos: usize,
}
impl<'a> BitReader<'a> {
fn new(data: &'a [u8]) -> Self {
Self { data, bit_pos: 0 }
}
fn read_bit(&mut self) -> Option<u32> {
let byte = self.bit_pos / 8;
if byte >= self.data.len() {
return None;
}
let shift = 7 - (self.bit_pos % 8);
self.bit_pos += 1;
Some(((self.data[byte] >> shift) & 1) as u32)
}
fn read_bits(&mut self, n: u32) -> Option<u32> {
let mut v = 0u32;
for _ in 0..n {
v = (v << 1) | self.read_bit()?;
}
Some(v)
}
fn skip_bits(&mut self, n: u32) -> Option<()> {
for _ in 0..n {
self.read_bit()?;
}
Some(())
}
/// Exp-Golomb unsigned, ue(v). Bounded leading-zero count to avoid runaway
/// on corrupt input.
fn read_ue(&mut self) -> Option<u32> {
let mut zeros = 0u32;
while self.read_bit()? == 0 {
zeros += 1;
if zeros > 31 {
return None;
}
}
if zeros == 0 {
return Some(0);
}
let rest = self.read_bits(zeros)?;
Some((1u32 << zeros) - 1 + rest)
}
}
/// Strip HEVC/H.264 emulation-prevention bytes (00 00 03 → 00 00) from a NAL /// Strip HEVC/H.264 emulation-prevention bytes (00 00 03 → 00 00) from a NAL
/// RBSP so a bit reader sees the true coded values. /// RBSP so a bit reader sees the true coded values.
fn strip_emulation_prevention(rbsp: &[u8]) -> Vec<u8> { fn strip_emulation_prevention(rbsp: &[u8]) -> Vec<u8> {
@@ -769,6 +793,63 @@ mod tests {
[(nal_type & 0x3F) << 1, 0x01] // tid=1 [(nal_type & 0x3F) << 1, 0x01] // tid=1
} }
#[test]
fn hevc_populates_measured_coding_type_and_source() {
use super::super::coding::CodingType;
// PPS body 0xC0 = pps_id 0, sps_id 0, dependent_slice 0, output_flag 0,
// num_extra_slice_header_bits 0 → slice_type follows pps_id directly.
// Slice body (TRAIL_R, non-IRAP VCL type 1) = first_slice 1, pps_id 0,
// slice_type: 0xD8 → 2 (I); 0xD0 → 1 (P); 0xE0 → 0 (B).
let nal = |t: u8, body: u8| {
let mut v = vec![0x00, 0x00, 0x01];
v.extend_from_slice(&hevc_nal_header(t));
v.push(body);
v
};
let src = crate::pes::SourcePos::at_byte(16384);
let run = |slice_body: u8| {
let mut p = HevcParser::new();
let mut data = nal(NAL_PPS, 0xC0); // active PPS first (sets num_extra)
data.extend_from_slice(&nal(1, slice_body)); // then the coded slice
let mut pe = make_pes(data, Some(0));
pe.source = Some(src);
p.parse(&pe)
};
let fi = run(0xD8);
assert_eq!(fi.len(), 1);
let ci = fi[0].coding.expect("HEVC frame carries PictureInfo");
assert_eq!(ci.coding_type(), CodingType::I, "slice_type 2 → I");
assert!(
ci.field_order().is_none(),
"HEVC field order undecoded → None, never faked"
);
assert_eq!(
fi[0].source.unwrap().byte,
16384,
"source provenance carried"
);
assert_eq!(
run(0xD0)[0].coding.unwrap().coding_type(),
CodingType::P,
"slice_type 1 → P"
);
assert_eq!(
run(0xE0)[0].coding.unwrap().coding_type(),
CodingType::B,
"slice_type 0 → B"
);
// No PPS seen → num_extra is unknown, so slice_type is NOT guessed; the
// coding stays None (honestly absent) rather than risk a wrong offset.
let mut p = HevcParser::new();
let bare = p.parse(&make_pes(nal(1, 0xD8), Some(0)));
assert!(
bare[0].coding.is_none(),
"no active PPS → coding omitted, never a guessed type"
);
}
// --- VPS+SPS+PPS → codec_private --- // --- VPS+SPS+PPS → codec_private ---
#[test] #[test]
+10 -1
View File
@@ -70,8 +70,17 @@ impl<'a> BitReader<'a> {
Some(b as u32) Some(b as u32)
} }
/// Read `n` bits, MSB-first, into the low bits of a `u32`.
pub fn read_bits(&mut self, n: u32) -> Option<u32> {
let mut v = 0u32;
for _ in 0..n {
v = (v << 1) | self.read_bit()?;
}
Some(v)
}
/// Skip `n` bits; `None` if that would run past the end. /// Skip `n` bits; `None` if that would run past the end.
pub fn skip_bits(&mut self, n: usize) -> Option<()> { pub fn skip_bits(&mut self, n: u32) -> Option<()> {
for _ in 0..n { for _ in 0..n {
self.read_bit()?; self.read_bit()?;
} }