forked from unom/punktfunk
H265Planner mirrors the H.264 layer's contract exactly: plan_au -> AuPlan
{ picture, slices with ref lists by stable PicId, DpbUpdate, warnings },
same concealment posture (warnings never abort, in-place reference
substitution preserving ref_idx positions, outputs survive failed AUs,
flush gates on AwaitingIdr, any IRAP resumes). Ported logic: RPS 8.3.2
(short-term AND long-term incl. PocLsbLt/MSB-cycle - the hosts' RFI
recovery rides long-term refs), ref lists 8.3.3/8.3.4, DPB C.5.2.2/C.5.2.3
via the vendored dpb; POC 8.3.1 from the vendored PictureData. Written
fresh: the plan surface, AU walk, envelope gates (multilayer, interlaced,
SCC self-reference, DPB>16, conf-window overflow - checked at EVERY
activation, not just parse), HEVC recovery-point SEI (prefix NALU 39,
se(v) recovery_poc_cnt), VUI colour with E.3.1 inference, and a test-only
HEVC bitstream synthesizer (upstream has none).
Upstream deviations worth naming (all in-code with spec anchors): the
empty-RPS inter slice cannot infinite-loop (upstream bug); RASL behind a
joined CRA refuses BEFORE any state change (PlanError::RaslSkipped - the
WP-2 wiring must map it to skip, not reanchor; module docs carry the
contract note); MaxPicOrderCntLsb reads from the ACTIVATING SPS (upstream
latches at parse - a latent multi-SPS bug); C.5.2.2's exemption is
picture 0 of the BITSTREAM (EobNut), never first-after-EOS.
Vendored parser gained PROVENANCE deviation 7 (report upstream): hostile
slice headers with num_long_term_sps+num_long_term_pics > 16 indexed out
of bounds of SliceHeader's [_;16] arrays - a production panic on exactly
the long-term-reference path, now a parse error.
Port review round 7: 10 findings (3 blocking: the vendor panic, an
EOS-boundary output interleave, an envelope bypass through PPS-only SPS
rebind reaching wrapping crop arithmetic) - 9 fixed with a regression
test each, 1 documented as the WP-2 contract note. Known follow-up: the
h264 AU-tail truncation detector shares h265's dead-arm shape (its arm
also cuts reserved NALU types, so the fix is not identical - deferred).
Tests: 29 h265 planner + 2 HEVC SEI + full test-25fps.h265/bear/bbb clip
walks with real invariants (every stored id output exactly once,
ascending POC per IRAP period). Gates: fmt clean; clippy -D warnings zero
(mac + pf-lxcheck2 incl. pf-client-core/pf-presenter); tests 45+69 mac,
69+121+53 container.
347 lines
12 KiB
Rust
347 lines
12 KiB
Rust
//! SEI payload parsing — the piece the vendored parser layer lacks: upstream classifies
|
||
//! SEI NALUs but never reads a payload. punktfunk needs exactly one payload type per
|
||
//! codec: the recovery point SEI, which hosts emit on RFI recovery so the client knows
|
||
//! where a decode-from-here point lands. Every other payload type is skipped by its
|
||
//! declared size.
|
||
//!
|
||
//! Both codecs put the recovery point at payload type 6 with the same D.1 message
|
||
//! framing, but the payload syntax differs: H.264 (D.1.8/D.2.8) counts recovery in
|
||
//! `frame_num` increments (`recovery_frame_cnt`, ue(v)) and carries a slice-group bit
|
||
//! pair; H.265 (D.2.8/D.3.8) counts in picture order (`recovery_poc_cnt`, se(v) — it
|
||
//! can be negative) and has no slice-group field. Hence two parsers over one shared
|
||
//! message walk.
|
||
|
||
/// Recovery point SEI (D.2.8).
|
||
///
|
||
/// `recovery_frame_cnt` counts in `frame_num` increments from the AU carrying the SEI to
|
||
/// the picture at which output is exact (`exact_match`) or approximate. `broken_link` set
|
||
/// means pictures before the recovery point may be visually broken and must not be shown.
|
||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||
pub struct RecoveryPoint {
|
||
pub recovery_frame_cnt: u32,
|
||
pub exact_match: bool,
|
||
pub broken_link: bool,
|
||
}
|
||
|
||
/// Recovery point SEI, H.265 flavour (D.3.8).
|
||
///
|
||
/// `recovery_poc_cnt` is the POC delta from the picture carrying the SEI to the
|
||
/// recovery-point picture — se(v)-coded, so unlike H.264's `recovery_frame_cnt` it can
|
||
/// be NEGATIVE (a recovery point among leading pictures). `exact_match`/`broken_link`
|
||
/// keep their H.264 semantics.
|
||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||
pub struct RecoveryPointHevc {
|
||
pub recovery_poc_cnt: i32,
|
||
pub exact_match: bool,
|
||
pub broken_link: bool,
|
||
}
|
||
|
||
/// Parse the first recovery point SEI message out of an H.264 SEI NALU.
|
||
///
|
||
/// `sei_payload` are the bytes of the NALU after its one-byte NAL header, emulation
|
||
/// prevention bytes still in place (they are removed here — 7.4.1 RBSP extraction).
|
||
/// `Ok(None)` means the NALU parsed cleanly but carries no recovery point.
|
||
pub fn parse_recovery_point(sei_payload: &[u8]) -> Result<Option<RecoveryPoint>, String> {
|
||
let rbsp = strip_emulation_prevention(sei_payload);
|
||
let Some(payload) = first_recovery_point_payload(&rbsp)? else {
|
||
return Ok(None);
|
||
};
|
||
|
||
let mut r = BitCursor::new(payload);
|
||
let recovery_frame_cnt = r.read_ue()?;
|
||
let exact_match = r.read_bit()? != 0;
|
||
let broken_link = r.read_bit()? != 0;
|
||
// changing_slice_group_idc u(2): parsed to keep the reader honest, unused —
|
||
// slice groups are outside every profile punktfunk hosts emit.
|
||
let _changing_slice_group_idc = r.read_bits(2)?;
|
||
Ok(Some(RecoveryPoint {
|
||
recovery_frame_cnt,
|
||
exact_match,
|
||
broken_link,
|
||
}))
|
||
}
|
||
|
||
/// Parse the first recovery point SEI message out of an H.265 prefix SEI NALU.
|
||
///
|
||
/// `sei_payload` are the bytes of the NALU after its TWO-byte NAL header (H.265 NALU
|
||
/// headers are 16 bits), emulation prevention still in place. Only prefix SEI NALUs
|
||
/// (type 39) can carry a recovery point — D.2.1 lists it as prefix-only, so suffix SEI
|
||
/// NALUs (type 40) need never reach here.
|
||
pub fn parse_recovery_point_hevc(sei_payload: &[u8]) -> Result<Option<RecoveryPointHevc>, String> {
|
||
let rbsp = strip_emulation_prevention(sei_payload);
|
||
let Some(payload) = first_recovery_point_payload(&rbsp)? else {
|
||
return Ok(None);
|
||
};
|
||
|
||
let mut r = BitCursor::new(payload);
|
||
let recovery_poc_cnt = r.read_se()?;
|
||
let exact_match = r.read_bit()? != 0;
|
||
let broken_link = r.read_bit()? != 0;
|
||
Ok(Some(RecoveryPointHevc {
|
||
recovery_poc_cnt,
|
||
exact_match,
|
||
broken_link,
|
||
}))
|
||
}
|
||
|
||
/// Walk the D.1 SEI message framing (shared verbatim between H.264 and H.265) and
|
||
/// return the payload bytes of the first recovery point message (payload type 6 in
|
||
/// both codecs), if any. `rbsp` is already emulation-prevention-stripped.
|
||
fn first_recovery_point_payload(rbsp: &[u8]) -> Result<Option<&[u8]>, String> {
|
||
let mut i = 0usize;
|
||
while i < rbsp.len() && !is_rbsp_trailing(rbsp, i) {
|
||
// D.1: payload type and size are ff-coded — 0xFF bytes each add 255 until a
|
||
// non-0xFF byte terminates the value. The run length is unbounded, so the type
|
||
// accumulates saturating: an adversarial ~16M-byte 0xFF run must not overflow
|
||
// (a saturated type simply never matches 6). The size accumulator is a usize
|
||
// whose use is bounds-checked below.
|
||
let mut payload_type = 0u32;
|
||
while i < rbsp.len() && rbsp[i] == 0xFF {
|
||
payload_type = payload_type.saturating_add(255);
|
||
i += 1;
|
||
}
|
||
if i >= rbsp.len() {
|
||
return Err("truncated SEI payload type".into());
|
||
}
|
||
payload_type = payload_type.saturating_add(u32::from(rbsp[i]));
|
||
i += 1;
|
||
|
||
let mut payload_size = 0usize;
|
||
while i < rbsp.len() && rbsp[i] == 0xFF {
|
||
payload_size += 255;
|
||
i += 1;
|
||
}
|
||
if i >= rbsp.len() {
|
||
return Err("truncated SEI payload size".into());
|
||
}
|
||
payload_size += usize::from(rbsp[i]);
|
||
i += 1;
|
||
|
||
let end = i
|
||
.checked_add(payload_size)
|
||
.filter(|&end| end <= rbsp.len())
|
||
.ok_or_else(|| "SEI payload overruns the NALU".to_string())?;
|
||
|
||
if payload_type == 6 {
|
||
return Ok(Some(&rbsp[i..end]));
|
||
}
|
||
|
||
i = end;
|
||
}
|
||
|
||
Ok(None)
|
||
}
|
||
|
||
/// 7.4.1: within the RBSP, `00 00 03` encodes two zero bytes; the `03` is the emulation
|
||
/// prevention byte and is dropped.
|
||
fn strip_emulation_prevention(data: &[u8]) -> Vec<u8> {
|
||
let mut out = Vec::with_capacity(data.len());
|
||
let mut zeros = 0usize;
|
||
for &byte in data {
|
||
if zeros >= 2 && byte == 0x03 {
|
||
zeros = 0;
|
||
continue;
|
||
}
|
||
zeros = if byte == 0 { zeros + 1 } else { 0 };
|
||
out.push(byte);
|
||
}
|
||
out
|
||
}
|
||
|
||
/// `more_rbsp_data()` at a byte-aligned message boundary: the remainder is trailing bits
|
||
/// iff it is the stop bit (0x80) followed by nothing but zero bytes.
|
||
fn is_rbsp_trailing(rbsp: &[u8], i: usize) -> bool {
|
||
rbsp[i] == 0x80 && rbsp[i + 1..].iter().all(|&b| b == 0)
|
||
}
|
||
|
||
/// Minimal MSB-first bit reader over an already-unescaped RBSP slice. The vendored
|
||
/// `BitReader` is `pub(crate)` to the vendored crate, so this crate carries its own.
|
||
struct BitCursor<'a> {
|
||
data: &'a [u8],
|
||
/// Position in bits from the start of `data`.
|
||
pos: usize,
|
||
}
|
||
|
||
impl<'a> BitCursor<'a> {
|
||
fn new(data: &'a [u8]) -> Self {
|
||
Self { data, pos: 0 }
|
||
}
|
||
|
||
fn read_bit(&mut self) -> Result<u32, String> {
|
||
let byte = *self
|
||
.data
|
||
.get(self.pos / 8)
|
||
.ok_or("SEI payload out of bits")?;
|
||
let bit = (byte >> (7 - self.pos % 8)) & 1;
|
||
self.pos += 1;
|
||
Ok(u32::from(bit))
|
||
}
|
||
|
||
fn read_bits(&mut self, count: usize) -> Result<u32, String> {
|
||
debug_assert!(count <= 31);
|
||
let mut out = 0u32;
|
||
for _ in 0..count {
|
||
out = (out << 1) | self.read_bit()?;
|
||
}
|
||
Ok(out)
|
||
}
|
||
|
||
/// ue(v), spec 9.1.
|
||
fn read_ue(&mut self) -> Result<u32, String> {
|
||
let mut leading_zeros = 0usize;
|
||
while self.read_bit()? == 0 {
|
||
leading_zeros += 1;
|
||
if leading_zeros > 31 {
|
||
return Err("invalid exp-Golomb code in SEI payload".into());
|
||
}
|
||
}
|
||
let suffix = self.read_bits(leading_zeros)?;
|
||
((1u32 << leading_zeros) - 1)
|
||
.checked_add(suffix)
|
||
.ok_or_else(|| "exp-Golomb value overflows u32".to_string())
|
||
}
|
||
|
||
/// se(v), spec 9.1.1: the ue(v) code point k maps to (−1)^(k+1) · ⌈k/2⌉.
|
||
fn read_se(&mut self) -> Result<i32, String> {
|
||
let k = self.read_ue()?;
|
||
let magnitude = k.div_ceil(2);
|
||
let magnitude =
|
||
i32::try_from(magnitude).map_err(|_| "exp-Golomb value overflows i32".to_string())?;
|
||
Ok(if k % 2 == 1 { magnitude } else { -magnitude })
|
||
}
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
|
||
#[test]
|
||
fn a_minimal_recovery_point_message_parses_to_its_field_values() {
|
||
// Message: type 6, size 1. Payload bits: ue(0)='1', exact=0, broken=0, csg=00,
|
||
// then payload alignment '1' + zeros -> 0b1000_0100. NALU trailing 0x80.
|
||
let sei = [0x06, 0x01, 0x84, 0x80];
|
||
assert_eq!(
|
||
parse_recovery_point(&sei).unwrap(),
|
||
Some(RecoveryPoint {
|
||
recovery_frame_cnt: 0,
|
||
exact_match: false,
|
||
broken_link: false
|
||
})
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn recovery_frame_cnt_and_both_flags_round_trip_through_the_bit_reader() {
|
||
// ue(5)='00110', exact=1, broken=1, csg=00, alignment -> 0b0011_0110 0b0100_0000.
|
||
let sei = [0x06, 0x02, 0x36, 0x40, 0x80];
|
||
assert_eq!(
|
||
parse_recovery_point(&sei).unwrap(),
|
||
Some(RecoveryPoint {
|
||
recovery_frame_cnt: 5,
|
||
exact_match: true,
|
||
broken_link: true
|
||
})
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn earlier_messages_and_ff_coded_types_are_skipped_to_reach_the_recovery_point() {
|
||
// First message: ff-coded payload type 255 (0xFF 0x00), size 1, payload 0x55.
|
||
// Second message: type 5 (user data), size 3. Third: the recovery point.
|
||
let sei = [
|
||
0xFF, 0x00, 0x01, 0x55, // type 255
|
||
0x05, 0x03, 0xAA, 0xBB, 0xCC, // type 5
|
||
0x06, 0x01, 0x84, // recovery point
|
||
0x80,
|
||
];
|
||
assert_eq!(
|
||
parse_recovery_point(&sei).unwrap(),
|
||
Some(RecoveryPoint {
|
||
recovery_frame_cnt: 0,
|
||
exact_match: false,
|
||
broken_link: false
|
||
})
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn emulation_prevention_bytes_inside_the_payload_are_removed_before_reading() {
|
||
// Unescaped payload (7 bytes): ue with a 22-zero prefix => recovery_frame_cnt
|
||
// 2^22-1 = 4194303, exact=1, broken=0, csg=00, alignment. Its first bytes are
|
||
// 00 00 02, which the escaper must have written as 00 00 03 02 on the wire.
|
||
let sei = [
|
||
0x06, 0x07, 0x00, 0x00, 0x03, 0x02, 0x00, 0x00, 0x04, 0x40, 0x80,
|
||
];
|
||
assert!(sei.windows(3).any(|w| w == [0x00, 0x00, 0x03]));
|
||
assert_eq!(
|
||
parse_recovery_point(&sei).unwrap(),
|
||
Some(RecoveryPoint {
|
||
recovery_frame_cnt: 4194303,
|
||
exact_match: true,
|
||
broken_link: false
|
||
})
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn a_sei_nalu_without_a_recovery_point_yields_none_not_an_error() {
|
||
let sei = [0x05, 0x01, 0x00, 0x80];
|
||
assert_eq!(parse_recovery_point(&sei).unwrap(), None);
|
||
}
|
||
|
||
#[test]
|
||
fn a_payload_size_overrunning_the_nalu_is_a_parse_error() {
|
||
let sei = [0x06, 0x0A, 0x00];
|
||
assert!(parse_recovery_point(&sei).is_err());
|
||
}
|
||
|
||
#[test]
|
||
fn the_hevc_recovery_point_parses_its_se_coded_poc_count() {
|
||
// recovery_poc_cnt se(0) = '1', exact = 0, broken = 0, payload alignment:
|
||
// 0b1001_0000.
|
||
let sei = [0x06, 0x01, 0x90, 0x80];
|
||
assert_eq!(
|
||
parse_recovery_point_hevc(&sei).unwrap(),
|
||
Some(RecoveryPointHevc {
|
||
recovery_poc_cnt: 0,
|
||
exact_match: false,
|
||
broken_link: false
|
||
})
|
||
);
|
||
|
||
// se(-1) = '011' (ue code point 2), exact = 1, broken = 0, alignment:
|
||
// 0b0111_0100 — the negative range H.264's ue(v) syntax cannot express.
|
||
let sei = [0x06, 0x01, 0x74, 0x80];
|
||
assert_eq!(
|
||
parse_recovery_point_hevc(&sei).unwrap(),
|
||
Some(RecoveryPointHevc {
|
||
recovery_poc_cnt: -1,
|
||
exact_match: true,
|
||
broken_link: false
|
||
})
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn the_hevc_parser_skips_earlier_messages_and_reports_absence_as_none() {
|
||
// User-data message first, then the recovery point (poc_cnt se(3): ue code
|
||
// point 5 = '00110', exact = 1, broken = 1, alignment: 0b0011_0111).
|
||
let sei = [
|
||
0x05, 0x02, 0xAA, 0xBB, // type 5
|
||
0x06, 0x01, 0x37, // recovery point
|
||
0x80,
|
||
];
|
||
assert_eq!(
|
||
parse_recovery_point_hevc(&sei).unwrap(),
|
||
Some(RecoveryPointHevc {
|
||
recovery_poc_cnt: 3,
|
||
exact_match: true,
|
||
broken_link: true
|
||
})
|
||
);
|
||
|
||
let sei = [0x05, 0x01, 0x00, 0x80];
|
||
assert_eq!(parse_recovery_point_hevc(&sei).unwrap(), None);
|
||
}
|
||
}
|