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.
2452 lines
103 KiB
Rust
2452 lines
103 KiB
Rust
//! HEVC (H.265) elementary stream parser.
|
||
//!
|
||
//! Extracts VPS, SPS, PPS NAL units for MKV codecPrivate.
|
||
//! Detects keyframes (IRAP pictures: IDR, CRA, BLA).
|
||
//! Each PES packet = one access unit = one frame.
|
||
|
||
use super::coding::{CodingType, PictureInfo};
|
||
use super::startcode::{BitReader, find_start_code, skip_start_code};
|
||
use super::{CodecParser, Frame, PesPacket, pts_to_ns};
|
||
|
||
// HEVC NAL unit types
|
||
const NAL_VPS: u8 = 32;
|
||
const NAL_SPS: u8 = 33;
|
||
const NAL_PPS: u8 = 34;
|
||
const NAL_AUD: u8 = 35;
|
||
// Dolby Vision RPU (Reference Processing Unit) — NAL type 62 (UNSPEC62).
|
||
// This is NOT filtered: all NAL types except VPS/SPS/PPS/AUD pass through
|
||
// to frame data, so DV enhancement layer RPU NALs are preserved automatically.
|
||
const _NAL_UNSPEC62_DV_RPU: u8 = 62;
|
||
// IRAP types (keyframes): BLA, IDR, CRA
|
||
const NAL_BLA_W_LP: u8 = 16;
|
||
const NAL_RSV_IRAP_VCL23: u8 = 23;
|
||
// CRA_NUT (Clean Random Access). A CRA at a splice carries RASL leading
|
||
// pictures that reference frames from BEFORE the splice; on linear decode of a
|
||
// concatenated title those references are gone ("Could not find ref with POC
|
||
// N"). The HEVC spec remedy is to rewrite the splice CRA as a BLA (Broken Link
|
||
// Access): a decoder then sets NoRaslOutput and discards the RASL cleanly with
|
||
// no error. See `mark_clip_boundary` / the IRAP arm in `parse`.
|
||
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
|
||
/// codecPrivate, detects IRAP keyframes, and converts each PES access unit into
|
||
/// length-prefixed NAL units. Implements [`CodecParser`].
|
||
pub struct HevcParser {
|
||
// First-seen parameter set of each type → seeds the MKV codecPrivate (hvcC).
|
||
// This is the ONLY copy the player gets out-of-band, and a player re-applies
|
||
// it at every keyframe (ffmpeg's hvcC→Annex-B insertion). A stream may
|
||
// redefine a parameter set mid-title under the SAME id with a different body
|
||
// (some discs redefine PPS id 0 partway through). Any occurrence whose body
|
||
// DIFFERS from this codecPrivate copy must therefore be emitted IN-BAND at
|
||
// each point it appears (i.e. at every keyframe of the redefined segment) so
|
||
// it overrides the re-applied codecPrivate set; otherwise those frames decode
|
||
// against the wrong parameter set → CABAC/cu_qp_delta desync.
|
||
vps: Option<Vec<u8>>,
|
||
sps: Option<Vec<u8>>,
|
||
pps: Option<Vec<u8>>,
|
||
// The currently-ACTIVE parameter-set body of each type — the most recent
|
||
// one the bitstream defined, which the decoder must use until the next
|
||
// redefinition. Distinct from the `vps/sps/pps` codecPrivate copy above
|
||
// (which is fixed to the FIRST one seen). When a stream redefines a param
|
||
// set mid-title (e.g. PPS id 0 body changes partway through, then the
|
||
// source STOPS repeating it at later IRAPs and relies on the decoder
|
||
// retaining it), a raw decode is fine — but an hvcC/MKV decode is NOT: a
|
||
// player re-applies the codecPrivate set at EVERY keyframe (ffmpeg's
|
||
// hvcC→Annex-B insertion), reverting id 0 to the stale FIRST body. We must
|
||
// therefore re-emit the active set IN-BAND at every keyframe whenever it
|
||
// differs from the codecPrivate copy and the access unit didn't already
|
||
// carry it. See `parse`.
|
||
cur_vps: Option<Vec<u8>>,
|
||
cur_sps: Option<Vec<u8>>,
|
||
cur_pps: Option<Vec<u8>>,
|
||
// Splice-aware CRA→BLA rewrite (non-seamless BD clip boundaries).
|
||
//
|
||
// When a BD title concatenates clips at a NON-SEAMLESS join (MPLS
|
||
// connection_condition 0x05 or 0x06), the next clip opens with a CRA whose
|
||
// RASL leading pictures reference frames from before the splice — gone after
|
||
// concatenation. The caller (the code that crosses the join) sets this flag
|
||
// via `mark_clip_boundary`; the parser then rewrites the FIRST CRA it sees
|
||
// at/after that point from CRA_NUT (21) to BLA_W_LP (16) so a linear decoder
|
||
// sets NoRaslOutput and discards the dangling RASL with no error. The flag
|
||
// is consumed (cleared) by that first CRA so only ONE CRA per boundary is
|
||
// touched — never a mid-stream CRA, never an IDR, never a non-CRA NAL.
|
||
//
|
||
// SAFETY: defaults to `false` and is ONLY ever set through
|
||
// `mark_clip_boundary`, which the caller invokes ONLY for a non-seamless
|
||
// (0x05/0x06) join. connection_condition 0x01 is the first-item/seamless
|
||
// case and must NOT trigger this flag. A stream with no boundary marker
|
||
// (single-clip title, or seamless-joined 0x01 UHD/BD) never has this set,
|
||
// so the rewrite branch is never reached and output is byte-identical to a
|
||
// parser without this field.
|
||
pending_clip_boundary: bool,
|
||
// Highest PES PTS seen on this video stream so far, on a MONOTONIC 64-bit
|
||
// timeline (raw 33-bit PTS unwrapped across 2^33 wraparounds — see
|
||
// `pts_wrap_offset`). Used to AUTO-DETECT a non-seamless clip boundary from
|
||
// the bitstream when the caller never plumbs one in (the common case — see
|
||
// `BACKSTEP_TICKS`). `None` until the first AU with a PTS.
|
||
high_pts: Option<i64>,
|
||
// Accumulated 2^33-tick offset applied to raw PES PTS values to unwrap them
|
||
// onto the monotonic timeline `high_pts` lives on. The 33-bit 90 kHz PTS
|
||
// wraps every ~26.5 h; a BD clip can start at a high base and cross the wrap
|
||
// mid-title. Without unwrapping, the 2^33→0 step looks like a backward clip
|
||
// reset and false-arms the CRA→BLA rewrite (corrupting a legitimate in-clip
|
||
// CRA and dropping valid RASL pictures). Each detected wrap adds 2^33 here.
|
||
pts_wrap_offset: i64,
|
||
}
|
||
|
||
// A backward PES-PTS step larger than this (90 kHz ticks) marks a non-seamless
|
||
// BD clip boundary: each .m2ts clip carries its own PTS base, so at a 0x05/0x06
|
||
// join the next clip's PTS resets backward by far more than any B-frame reorder
|
||
// window (HEVC reorder depth tops out ~16 frames, <1 s at 24 fps). 3 s = 270000
|
||
// ticks sits well above any legitimate reorder dip and far below any real clip's
|
||
// duration, so it never false-triggers within a clip. This MIRRORS the mux-side
|
||
// `DISCONTINUITY_BACKSTEP_NS` (3 s) in `mux/mkv.rs`, which independently rebases
|
||
// the timeline at the same boundaries; here it drives the CRA→BLA rewrite that
|
||
// kills the dangling-RASL "Could not find ref with POC N" decode errors a
|
||
// concatenated multi-clip title otherwise produces.
|
||
const BACKSTEP_TICKS: i64 = 270_000;
|
||
|
||
// The 33-bit 90 kHz PES PTS counter wraps at 2^33 ticks (~26.5 h). When the raw
|
||
// PTS steps backward by approximately a full period — i.e. it landed just past
|
||
// the wrap — it is a counter wraparound, NOT a clip reset: unwrap it (add 2^33)
|
||
// instead of arming the CRA→BLA rewrite. A genuine non-seamless clip join resets
|
||
// the PTS to a fresh small base, a backward step of arbitrary (sub-2^33) size; a
|
||
// wrap is specifically a step of ~2^33. We accept any backward step within one
|
||
// `PTS_WRAP_PERIOD`/2 of a full period as a wrap (the new value is below the old
|
||
// high-water but within a reorder window of the wrap point), which cleanly
|
||
// separates the two cases since a clip reset to a small base is nowhere near 2^33
|
||
// below the high-water unless the title is itself ~26 h long (impossible on BD).
|
||
const PTS_WRAP_PERIOD: i64 = 1 << 33;
|
||
|
||
impl Default for HevcParser {
|
||
fn default() -> Self {
|
||
Self::new()
|
||
}
|
||
}
|
||
|
||
impl HevcParser {
|
||
/// Create a fresh HEVC parser with no parameter sets captured yet.
|
||
pub fn new() -> Self {
|
||
Self {
|
||
vps: None,
|
||
sps: None,
|
||
pps: None,
|
||
cur_vps: None,
|
||
cur_sps: None,
|
||
cur_pps: None,
|
||
pending_clip_boundary: false,
|
||
high_pts: None,
|
||
pts_wrap_offset: 0,
|
||
}
|
||
}
|
||
|
||
/// Mark that the NEXT IRAP this parser sees begins a NON-SEAMLESS BD clip
|
||
/// join. MPLS connection_condition 0x05 and 0x06 are the non-seamless
|
||
/// values (per the BD-ROM spec: 0x01 = first item / seamless, 0x05/0x06 =
|
||
/// non-seamless). The first CRA at/after this point is rewritten CRA_NUT
|
||
/// (21) → BLA_W_LP (16) so a linear decoder sets NoRaslOutput and discards
|
||
/// the now-dangling RASL leading pictures with no "could not find ref"
|
||
/// error.
|
||
///
|
||
/// MUST be called ONLY when MPLS reports connection_condition as 0x05 or
|
||
/// 0x06 (non-seamless). It is a no-op for the rewrite unless a CRA actually
|
||
/// follows: an IDR/IDR_W_RADL boundary needs no fix (it carries no
|
||
/// cross-splice references), and the flag is cleared by the first
|
||
/// IRAP-class CRA it reaches.
|
||
///
|
||
/// SAFETY: never call this for connection_condition 0x01 (seamless/first
|
||
/// item) or within a single-clip title — doing so could convert a
|
||
/// legitimate mid-content CRA to BLA. The default (never called) path
|
||
/// leaves output byte-identical.
|
||
pub fn mark_clip_boundary(&mut self) {
|
||
self.pending_clip_boundary = true;
|
||
}
|
||
}
|
||
|
||
/// Handle a VPS/SPS/PPS NAL. Decides whether to strip it (the decoder already
|
||
/// has the value) or emit it in-band, and tracks the currently-active body.
|
||
///
|
||
/// The decision MUST be made against the currently-active set (`cur`), NOT the
|
||
/// codecPrivate copy (`first`). The two player behaviours for hvcC-in-MKV
|
||
/// diverge exactly here:
|
||
///
|
||
/// - A *seek-capable / Annex-B* player (e.g. ffmpeg's `hevc_mp4toannexb`)
|
||
/// re-applies the hvcC sets at every keyframe. `reassert_active` handles it.
|
||
/// - A *streaming* decode (ffmpeg decoding the MKV directly — what most
|
||
/// integrity checkers do) applies hvcC ONCE at init and thereafter updates a
|
||
/// parameter set ONLY from an in-band NAL.
|
||
///
|
||
/// So when a title redefines a set mid-stream (id 0 body A → B) and later
|
||
/// switches BACK to A (== codecPrivate), the change to A must STILL be emitted
|
||
/// in-band: the streaming decoder is sitting on B and will never revert
|
||
/// otherwise, decoding the whole A-segment against B → CABAC/cu_qp_delta
|
||
/// desync. Stripping on `== first` (the old behaviour) dropped exactly that
|
||
/// revert and corrupted every "switch back to the first body" segment.
|
||
///
|
||
/// Rules:
|
||
/// - First of its type → seeds codecPrivate; stripped (the decoder gets it from
|
||
/// hvcC at init).
|
||
/// - Equal to the active set `cur` → redundant; stripped.
|
||
/// - Different from `cur` (a change, in EITHER direction) → emitted in-band and
|
||
/// `cur` updated.
|
||
///
|
||
/// Returns `true` when the NAL was emitted in-band into `frame_data`.
|
||
fn handle_param_set(
|
||
first: &mut Option<Vec<u8>>,
|
||
cur: &mut Option<Vec<u8>>,
|
||
nal: &[u8],
|
||
frame_data: &mut Vec<u8>,
|
||
) -> bool {
|
||
let is_first = first.is_none();
|
||
if is_first {
|
||
first.replace(nal.to_vec()); // seeds codecPrivate; stripped here
|
||
}
|
||
let changed = cur.as_deref() != Some(nal);
|
||
if changed {
|
||
*cur = Some(nal.to_vec());
|
||
}
|
||
// Strip the seeding occurrence (decoder gets it from hvcC) and any NAL that
|
||
// doesn't change the active set. Emit only a genuine change.
|
||
if is_first || !changed {
|
||
return false;
|
||
}
|
||
// A NAL longer than u32::MAX can't be length-prefixed in the 4-byte field;
|
||
// skip it rather than mis-frame the output. Unreachable in practice (no
|
||
// real access unit is >4 GiB).
|
||
let Ok(len) = u32::try_from(nal.len()) else {
|
||
return false;
|
||
};
|
||
frame_data.extend_from_slice(&len.to_be_bytes());
|
||
frame_data.extend_from_slice(nal);
|
||
true
|
||
}
|
||
|
||
/// Append the active parameter set `cur` to `prefix` (length-prefixed) so every
|
||
/// keyframe is SELF-CONTAINED: it carries the active VPS/SPS/PPS in-band ahead
|
||
/// of its slices. Skipped only when this access unit ALREADY carried the NAL
|
||
/// in-band (`emitted` — avoids a duplicate) or no active set exists yet.
|
||
///
|
||
/// Why unconditional (not only when the active set differs from codecPrivate):
|
||
/// a streaming decoder applies the hvcC param sets once at init, then relies on
|
||
/// in-band repetition. Some sources stop repeating a param set at later IRAPs
|
||
/// even though its body is unchanged; if the decoder then drops it (a CRA reset
|
||
/// or SPS event), nothing re-sends it and every subsequent slice fails with
|
||
/// "PPS id out of range" until the next genuine change (observed as a ~24 min
|
||
/// corrupt band on one dual-layer UHD title). Re-asserting the active set at
|
||
/// EVERY keyframe — what compliant muxers (mkvmerge) do at every IRAP — makes
|
||
/// streaming decode self-healing. Re-sending an identical param set is benign
|
||
/// (decoders expect it at IRAPs); cost is a few hundred bytes per keyframe.
|
||
/// This strictly supersets the earlier change-only re-assert, so the
|
||
/// param-set-revert fix is unaffected.
|
||
fn reassert_active(prefix: &mut Vec<u8>, cur: &Option<Vec<u8>>, emitted: bool) {
|
||
if emitted {
|
||
return;
|
||
}
|
||
let Some(active) = cur.as_deref() else {
|
||
return;
|
||
};
|
||
push_length_prefixed(prefix, active);
|
||
}
|
||
|
||
/// Append `nal` to `out` as a 4-byte big-endian length prefix followed by the
|
||
/// NAL body. A NAL longer than `u32::MAX` can't be length-prefixed in the
|
||
/// 4-byte field, so it is skipped rather than mis-framed. Unreachable in
|
||
/// practice (no real access unit is >4 GiB).
|
||
fn push_length_prefixed(out: &mut Vec<u8>, nal: &[u8]) {
|
||
let Ok(len) = u32::try_from(nal.len()) else {
|
||
return;
|
||
};
|
||
out.extend_from_slice(&len.to_be_bytes());
|
||
out.extend_from_slice(nal);
|
||
}
|
||
|
||
impl CodecParser for HevcParser {
|
||
fn parse(&mut self, pes: &PesPacket) -> Vec<Frame> {
|
||
if pes.data.is_empty() {
|
||
return Vec::new();
|
||
}
|
||
|
||
// MKV block timecodes are PRESENTATION timestamps; frames are stored
|
||
// in decode order (the order they arrive here) and the player reorders
|
||
// for display by timecode. So use PTS, not DTS — using DTS makes the
|
||
// block timecode monotonic in storage order, which presents B-frames in
|
||
// decode order (visible judder / wrong frames) and breaks PTS-based
|
||
// seeking. Fall back to DTS only if PTS is somehow absent.
|
||
let pts_ns = pes.pts.or(pes.dts).map(pts_to_ns).unwrap_or(0);
|
||
|
||
// Auto-detect a non-seamless clip boundary from the bitstream. freemkv
|
||
// reads a BD title's clips as ONE concatenated sector stream and the
|
||
// mpls connection_condition is not plumbed through the (threaded) mux
|
||
// pipeline, so `mark_clip_boundary` is otherwise never invoked. Each
|
||
// .m2ts clip carries its own PTS base; at a 0x05/0x06 join the next
|
||
// clip's PTS resets backward by far more than any reorder window. A
|
||
// backward step beyond `BACKSTEP_TICKS` is that boundary: arm the same
|
||
// CRA→BLA rewrite (`pending_clip_boundary`) the first IRAP of the new
|
||
// clip then consumes. Without this, the splice CRA's RASL leading
|
||
// pictures reference pre-join frames gone after concatenation and a
|
||
// linear decoder floods "Could not find ref with POC N" (the Top Gun
|
||
// UHD defect). Uses the 90 kHz PES PTS (not the rebased mux timeline)
|
||
// UNWRAPPED onto a monotonic 64-bit timeline first — the raw 33-bit PTS
|
||
// wraps every ~26.5 h, and a single-clip title that crosses 2^33→0 would
|
||
// otherwise false-arm the rewrite (corrupting a legitimate in-clip CRA).
|
||
// Tracks the high-water mark so a single in-clip B-frame dip never arms
|
||
// it. DTS-only AUs (no PTS) leave the watermark untouched.
|
||
if let Some(raw_pts) = pes.pts {
|
||
// Unwrap onto the monotonic timeline. If the offset-adjusted value
|
||
// dropped to roughly a full period (2^33) below the high-water, the
|
||
// 33-bit counter wrapped: add another period and re-check, rather
|
||
// than treat the wrap as a backward clip reset.
|
||
let mut unwrapped = raw_pts + self.pts_wrap_offset;
|
||
if let Some(high) = self.high_pts {
|
||
if high - unwrapped > PTS_WRAP_PERIOD / 2 {
|
||
self.pts_wrap_offset += PTS_WRAP_PERIOD;
|
||
unwrapped += PTS_WRAP_PERIOD;
|
||
}
|
||
}
|
||
match self.high_pts {
|
||
Some(high) if unwrapped < high - BACKSTEP_TICKS => {
|
||
self.pending_clip_boundary = true;
|
||
self.high_pts = Some(unwrapped);
|
||
}
|
||
Some(high) => self.high_pts = Some(high.max(unwrapped)),
|
||
None => self.high_pts = Some(unwrapped),
|
||
}
|
||
}
|
||
|
||
let data = &pes.data;
|
||
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
|
||
// in-band (a redefinition vs codecPrivate). Used after the scan to
|
||
// re-assert the active set at a keyframe the source left bare.
|
||
let mut emitted_vps = false;
|
||
let mut emitted_sps = false;
|
||
let mut emitted_pps = false;
|
||
// Pre-size: output is ~input bytes with a few 4-byte length
|
||
// prefixes added. UHD frames are 150-300 KB; the unsized Vec
|
||
// growth chain otherwise reallocs 5-7× per frame.
|
||
let mut frame_data = Vec::with_capacity(data.len() + 64);
|
||
|
||
// Single-pass NAL scan: extract params, detect keyframes, build length-prefixed output
|
||
let mut pos = 0;
|
||
while let Some(sc_pos) = find_start_code(data, pos) {
|
||
if let Some(nal_start) = skip_start_code(data, sc_pos) {
|
||
let next = find_start_code(data, nal_start).unwrap_or(data.len());
|
||
// Strip the leading zeros of the following start code. For a
|
||
// conforming bitstream this is lossless: rbsp_trailing_bits()
|
||
// sets a stop-one bit, so the final byte of any RBSP is never
|
||
// 0x00 — the only trailing zeros here belong to the next
|
||
// 00 00 (00) 01 prefix.
|
||
let mut end = next;
|
||
while end > nal_start && data[end - 1] == 0x00 {
|
||
end -= 1;
|
||
}
|
||
|
||
// Skip empty NALs entirely. When the trailing-zero strip reduces
|
||
// `end` back to `nal_start` (e.g. `00 00 01 00 00 01`, or a
|
||
// zero-filled bad sector between two start codes), the slice is
|
||
// empty; emitting a 4-byte 0x00000000 length prefix with no NAL
|
||
// body produces a structurally invalid NALU a decoder rejects.
|
||
if nal_start < data.len() && end > nal_start {
|
||
// HEVC NAL header: 2 bytes. Type is bits 1-6 of first byte.
|
||
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 {
|
||
NAL_VPS => {
|
||
emitted_vps |= handle_param_set(
|
||
&mut self.vps,
|
||
&mut self.cur_vps,
|
||
&data[nal_start..end],
|
||
&mut frame_data,
|
||
)
|
||
}
|
||
NAL_SPS => {
|
||
emitted_sps |= handle_param_set(
|
||
&mut self.sps,
|
||
&mut self.cur_sps,
|
||
&data[nal_start..end],
|
||
&mut frame_data,
|
||
)
|
||
}
|
||
NAL_PPS => {
|
||
emitted_pps |= handle_param_set(
|
||
&mut self.pps,
|
||
&mut self.cur_pps,
|
||
&data[nal_start..end],
|
||
&mut frame_data,
|
||
)
|
||
}
|
||
// Drop Access Unit Delimiters. This is intentional and
|
||
// spec-correct: Matroska HEVC frame data omits AUDs
|
||
// (the container delimits access units), so carrying
|
||
// them in-band is redundant. H.264 does the same below.
|
||
NAL_AUD => {}
|
||
t if (NAL_BLA_W_LP..=NAL_RSV_IRAP_VCL23).contains(&t) => {
|
||
keyframe = true;
|
||
// Splice-aware CRA→BLA rewrite. At the FIRST CRA
|
||
// following a non-seamless clip boundary (flag set
|
||
// via `mark_clip_boundary`), rewrite CRA_NUT (21) →
|
||
// BLA_W_LP (16) so a linear decoder sets NoRaslOutput
|
||
// and drops the dangling RASL with no error. The flag
|
||
// is consumed here so exactly ONE CRA per boundary is
|
||
// touched. A non-CRA IRAP (IDR, BLA) clears the flag
|
||
// too (the boundary is handled — IDR carries no
|
||
// cross-splice refs) but is NOT modified. Default
|
||
// path (flag never set) is unreachable → byte-
|
||
// identical output.
|
||
if self.pending_clip_boundary && t == NAL_CRA_NUT {
|
||
// First CRA after a non-seamless boundary: rewrite
|
||
// its header type to BLA_W_LP. NAL type is bits 1-6
|
||
// of byte 0: byte = (byte & 0x81) | (type << 1).
|
||
self.pending_clip_boundary = false;
|
||
let mut rewritten = data[nal_start..end].to_vec();
|
||
rewritten[0] = (rewritten[0] & 0x81) | (NAL_BLA_W_LP << 1);
|
||
push_length_prefixed(&mut frame_data, &rewritten);
|
||
} else {
|
||
// Any IRAP clears a pending boundary (it's been
|
||
// reached and handled — an IDR needs no rewrite),
|
||
// but only a CRA is modified.
|
||
self.pending_clip_boundary = false;
|
||
push_length_prefixed(&mut frame_data, &data[nal_start..end]);
|
||
}
|
||
}
|
||
_ => {
|
||
// All other NAL types (slices, SEI, DV RPU, etc.) pass through
|
||
push_length_prefixed(&mut frame_data, &data[nal_start..end]);
|
||
}
|
||
}
|
||
}
|
||
pos = next;
|
||
} else {
|
||
break;
|
||
}
|
||
}
|
||
|
||
if frame_data.is_empty() {
|
||
return Vec::new();
|
||
}
|
||
|
||
// A player re-applies the hvcC (codecPrivate) parameter sets at every
|
||
// keyframe. If the active set was redefined mid-title and the source
|
||
// stopped repeating that redefinition at later IRAPs (relying on the
|
||
// decoder to retain it — valid for a raw bitstream), the hvcC
|
||
// re-insertion would silently revert to the stale FIRST body and every
|
||
// frame in the segment decodes against the wrong parameter set
|
||
// (CABAC/cu_qp_delta desync). Re-assert the active set in-band, ahead
|
||
// of this AU's slices, so it wins. Re-asserted at EVERY keyframe (even
|
||
// when active == codecPrivate) so each keyframe is self-contained and a
|
||
// decoder that dropped the set (CRA reset / SPS event) self-heals.
|
||
if keyframe {
|
||
let mut prefix = Vec::new();
|
||
reassert_active(&mut prefix, &self.cur_vps, emitted_vps);
|
||
reassert_active(&mut prefix, &self.cur_sps, emitted_sps);
|
||
reassert_active(&mut prefix, &self.cur_pps, emitted_pps);
|
||
if !prefix.is_empty() {
|
||
prefix.extend_from_slice(&frame_data);
|
||
frame_data = prefix;
|
||
}
|
||
}
|
||
|
||
vec![Frame {
|
||
// Coding-type only: HEVC field order (pic_struct, from a pic_timing
|
||
// 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,
|
||
keyframe,
|
||
data: frame_data,
|
||
duration_ns: None,
|
||
}]
|
||
}
|
||
|
||
fn codec_private(&self) -> Option<Vec<u8>> {
|
||
// HEVCDecoderConfigurationRecord (ISO 14496-15)
|
||
let vps = self.vps.as_ref()?;
|
||
let sps = self.sps.as_ref()?;
|
||
let pps = self.pps.as_ref()?;
|
||
|
||
// hvcC encodes each NAL's length as a 16-bit field. A param set larger
|
||
// than 65535 bytes would silently truncate the length while the full
|
||
// bytes are appended → mis-framed record. Refuse rather than emit a
|
||
// corrupt hvcC (param sets this large are non-conforming anyway).
|
||
if vps.len() > 0xFFFF || sps.len() > 0xFFFF || pps.len() > 0xFFFF {
|
||
return None;
|
||
}
|
||
|
||
// Build a conforming HEVCDecoderConfigurationRecord: fixed header
|
||
// (configurationVersion, profile_tier_level fields, parallelism, parsed
|
||
// chroma/bit depths) followed by numOfArrays length-prefixed NAL arrays.
|
||
let mut record = Vec::new();
|
||
|
||
// Minimal HEVCDecoderConfigurationRecord header.
|
||
//
|
||
// The stored SPS NAL is [2-byte HEVC NAL header][SPS RBSP...].
|
||
// profile_tier_level fields must be read off the
|
||
// emulation-prevention-STRIPPED RBSP — a `00 00 03` sequence in the
|
||
// first ~15 SPS bytes would otherwise shift every raw byte index and
|
||
// corrupt the PTL (profile/compat/constraint/level). We strip first
|
||
// (same as parse_sps_chroma) and index into the cleaned RBSP:
|
||
// rbsp[0] sps_vps_id u(4)+max_sub_layers u(3)+temporal_nesting u(1)
|
||
// rbsp[1] general_profile_space u(2)+tier u(1)+profile_idc u(5)
|
||
// rbsp[2..6] general_profile_compatibility_flags u(32)
|
||
// rbsp[6..12] general_constraint_indicator_flags 48 bits
|
||
// rbsp[12] general_level_idc u(8)
|
||
let ptl: Vec<u8> = if sps.len() > 2 {
|
||
strip_emulation_prevention(&sps[2..])
|
||
} else {
|
||
Vec::new()
|
||
};
|
||
let ptl_at = |i: usize| -> u8 { ptl.get(i).copied().unwrap_or(0) };
|
||
record.push(1); // configurationVersion
|
||
// general_profile_space + general_tier_flag + general_profile_idc
|
||
record.push(ptl_at(1));
|
||
// general_profile_compatibility_flags (4 bytes) — RBSP bytes 2..6
|
||
for i in 2..6 {
|
||
record.push(ptl_at(i));
|
||
}
|
||
// general_constraint_indicator_flags (6 bytes) — RBSP bytes 6..12
|
||
for i in 6..12 {
|
||
record.push(ptl_at(i));
|
||
}
|
||
// general_level_idc — RBSP byte 12
|
||
record.push(ptl_at(12));
|
||
// min_spatial_segmentation_idc (4 + 12 bits)
|
||
record.extend_from_slice(&[0xF0, 0x00]);
|
||
// parallelismType (6 + 2 bits)
|
||
record.push(0xFC);
|
||
// chromaFormat / bit depths — parse the real values from the SPS RBSP.
|
||
// A hardcoded 8-bit 4:2:0 is wrong for 10-bit Main 10 UHD (essentially
|
||
// all UHD content). Fall back to 8-bit 4:2:0 only if the SPS can't be
|
||
// parsed (emulation-prevention is handled; sub-layer PTL is skipped).
|
||
let chroma = parse_sps_chroma(sps).unwrap_or(SpsChroma {
|
||
chroma_format_idc: 1,
|
||
bit_depth_luma_minus8: 0,
|
||
bit_depth_chroma_minus8: 0,
|
||
max_sub_layers_minus1: 0,
|
||
temporal_id_nesting_flag: 0,
|
||
});
|
||
// chromaFormat (6 reserved bits set + 2-bit chroma_format_idc)
|
||
record.push(0xFC | (chroma.chroma_format_idc & 0x03));
|
||
// bitDepthLumaMinus8 (5 reserved bits set + 3-bit value)
|
||
record.push(0xF8 | (chroma.bit_depth_luma_minus8 & 0x07));
|
||
// bitDepthChromaMinus8 (5 reserved bits set + 3-bit value)
|
||
record.push(0xF8 | (chroma.bit_depth_chroma_minus8 & 0x07));
|
||
// avgFrameRate
|
||
record.extend_from_slice(&[0, 0]);
|
||
// Byte 21 packs four fields (ISO/IEC 14496-15):
|
||
// constantFrameRate u(2) = 0 (unknown / not constant)
|
||
// numTemporalLayers u(3) = sps_max_sub_layers_minus1 + 1
|
||
// temporalIdNested u(1) = sps_temporal_id_nesting_flag
|
||
// lengthSizeMinusOne u(2) = 3 (4-byte length prefix)
|
||
// sps_max_sub_layers_minus1 is u(3) (0..7), so +1 is 1..8. The hvcC
|
||
// numTemporalLayers field is u(3) (0..7); the max legal value (8) is
|
||
// saturated to 7 rather than wrapping to 0 via the & 0x07 mask.
|
||
let num_temporal_layers = chroma.max_sub_layers_minus1.saturating_add(1).min(7) & 0x07;
|
||
let temporal_id_nested = chroma.temporal_id_nesting_flag & 0x01;
|
||
record.push((num_temporal_layers << 3) | (temporal_id_nested << 2) | 0x03);
|
||
// numOfArrays
|
||
record.push(3); // VPS, SPS, PPS
|
||
|
||
// VPS array
|
||
record.push(0x20 | (NAL_VPS & 0x3F)); // array_completeness + NAL type
|
||
record.extend_from_slice(&[0, 1]); // numNalus = 1
|
||
record.push((vps.len() >> 8) as u8);
|
||
record.push(vps.len() as u8);
|
||
record.extend_from_slice(vps);
|
||
|
||
// SPS array
|
||
record.push(0x20 | (NAL_SPS & 0x3F));
|
||
record.extend_from_slice(&[0, 1]);
|
||
record.push((sps.len() >> 8) as u8);
|
||
record.push(sps.len() as u8);
|
||
record.extend_from_slice(sps);
|
||
|
||
// PPS array
|
||
record.push(0x20 | (NAL_PPS & 0x3F));
|
||
record.extend_from_slice(&[0, 1]);
|
||
record.push((pps.len() >> 8) as u8);
|
||
record.push(pps.len() as u8);
|
||
record.extend_from_slice(pps);
|
||
|
||
Some(record)
|
||
}
|
||
}
|
||
|
||
/// chroma_format_idc + bit depths parsed from an HEVC SPS RBSP, for the hvcC
|
||
/// fixed header. Without these the record falsely advertised 8-bit 4:2:0, wrong
|
||
/// for 10-bit Main 10 UHD (essentially all UHD content).
|
||
struct SpsChroma {
|
||
/// chroma_format_idc: 0 mono, 1 4:2:0, 2 4:2:2, 3 4:4:4.
|
||
chroma_format_idc: u8,
|
||
bit_depth_luma_minus8: u8,
|
||
bit_depth_chroma_minus8: u8,
|
||
/// sps_max_sub_layers_minus1 (u3): numTemporalLayers = this + 1 for hvcC.
|
||
max_sub_layers_minus1: u8,
|
||
/// sps_temporal_id_nesting_flag (u1) for hvcC temporalIdNested.
|
||
temporal_id_nesting_flag: u8,
|
||
}
|
||
|
||
/// 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.
|
||
fn strip_emulation_prevention(rbsp: &[u8]) -> Vec<u8> {
|
||
let mut out = Vec::with_capacity(rbsp.len());
|
||
let mut zeros = 0usize;
|
||
for &b in rbsp {
|
||
if zeros >= 2 && b == 0x03 {
|
||
// Drop the emulation-prevention byte; reset the run.
|
||
zeros = 0;
|
||
continue;
|
||
}
|
||
out.push(b);
|
||
if b == 0x00 {
|
||
zeros += 1;
|
||
} else {
|
||
zeros = 0;
|
||
}
|
||
}
|
||
out
|
||
}
|
||
|
||
/// Parse chroma_format_idc and bit depths from a stored SPS NAL
|
||
/// (`[2-byte NAL header][RBSP...]`). Handles emulation-prevention and
|
||
/// sub-layer profile_tier_level. Returns `None` if the bitstream is too short
|
||
/// or malformed (caller falls back to the 8-bit 4:2:0 default).
|
||
fn parse_sps_chroma(sps: &[u8]) -> Option<SpsChroma> {
|
||
if sps.len() < 3 {
|
||
return None;
|
||
}
|
||
// RBSP begins after the 2-byte HEVC NAL header.
|
||
let rbsp = strip_emulation_prevention(&sps[2..]);
|
||
let mut r = BitReader::new(&rbsp);
|
||
|
||
// sps_video_parameter_set_id u(4)
|
||
r.skip_bits(4)?;
|
||
// sps_max_sub_layers_minus1 u(3)
|
||
let max_sub_layers_minus1 = r.read_bits(3)?;
|
||
// sps_temporal_id_nesting_flag u(1)
|
||
let temporal_id_nesting_flag = r.read_bit()?;
|
||
|
||
// profile_tier_level( 1, sps_max_sub_layers_minus1 )
|
||
parse_profile_tier_level(&mut r, max_sub_layers_minus1)?;
|
||
|
||
// sps_seq_parameter_set_id ue(v)
|
||
r.read_ue()?;
|
||
// chroma_format_idc ue(v)
|
||
let chroma_format_idc = r.read_ue()? as u8;
|
||
if chroma_format_idc == 3 {
|
||
// separate_colour_plane_flag u(1)
|
||
r.skip_bits(1)?;
|
||
}
|
||
// pic_width_in_luma_samples ue(v), pic_height_in_luma_samples ue(v)
|
||
r.read_ue()?;
|
||
r.read_ue()?;
|
||
// conformance_window_flag u(1) + 4× ue(v) if set
|
||
if r.read_bit()? == 1 {
|
||
r.read_ue()?;
|
||
r.read_ue()?;
|
||
r.read_ue()?;
|
||
r.read_ue()?;
|
||
}
|
||
// bit_depth_luma_minus8 ue(v), bit_depth_chroma_minus8 ue(v)
|
||
let bit_depth_luma_minus8 = r.read_ue()? as u8;
|
||
let bit_depth_chroma_minus8 = r.read_ue()? as u8;
|
||
|
||
Some(SpsChroma {
|
||
chroma_format_idc,
|
||
bit_depth_luma_minus8,
|
||
bit_depth_chroma_minus8,
|
||
max_sub_layers_minus1: max_sub_layers_minus1 as u8,
|
||
temporal_id_nesting_flag: temporal_id_nesting_flag as u8,
|
||
})
|
||
}
|
||
|
||
/// Consume a profile_tier_level(profilePresentFlag=1, maxNumSubLayersMinus1)
|
||
/// structure from the bit reader (HEVC 7.3.3).
|
||
fn parse_profile_tier_level(r: &mut BitReader, max_sub_layers_minus1: u32) -> Option<()> {
|
||
// general PTL fixed layout (HEVC 7.3.3): profile_space u(2) + tier u(1) +
|
||
// profile_idc u(5) = 8, general_profile_compatibility_flags u(32),
|
||
// constraint-flags/reserved area = 48, general_level_idc u(8).
|
||
// Total = 8 + 32 + 48 + 8 = 96 bits = 12 bytes. Skip 96 bits.
|
||
r.skip_bits(96)?;
|
||
|
||
if max_sub_layers_minus1 > 0 {
|
||
// sub_layer_profile_present_flag[i] u(1) + sub_layer_level_present_flag[i]
|
||
// u(1), for i in 0..max_sub_layers_minus1.
|
||
let mut profile_present = [false; 8];
|
||
let mut level_present = [false; 8];
|
||
for i in 0..max_sub_layers_minus1 as usize {
|
||
profile_present[i] = r.read_bit()? == 1;
|
||
level_present[i] = r.read_bit()? == 1;
|
||
}
|
||
// reserved_zero_2bits for i in max_sub_layers_minus1..8
|
||
if max_sub_layers_minus1 < 8 {
|
||
for _ in max_sub_layers_minus1..8 {
|
||
r.skip_bits(2)?;
|
||
}
|
||
}
|
||
for i in 0..max_sub_layers_minus1 as usize {
|
||
if profile_present[i] {
|
||
// sub_layer profile block: 8 + 32 + 48 = 88 bits.
|
||
r.skip_bits(88)?;
|
||
}
|
||
if level_present[i] {
|
||
// sub_layer_level_idc u(8)
|
||
r.skip_bits(8)?;
|
||
}
|
||
}
|
||
}
|
||
Some(())
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
use crate::mux::ts::PesPacket;
|
||
|
||
fn make_pes(data: Vec<u8>, pts: Option<i64>) -> PesPacket {
|
||
PesPacket {
|
||
source: None,
|
||
pid: 0x1011,
|
||
pts,
|
||
dts: None,
|
||
data,
|
||
}
|
||
}
|
||
|
||
/// Build an HEVC NAL header (2 bytes). Type is bits 1-6 of first byte.
|
||
/// Format: forbidden(1) | type(6) | layer_id_high(1) || layer_id_low(5) | tid(3)
|
||
fn hevc_nal_header(nal_type: u8) -> [u8; 2] {
|
||
[(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 ---
|
||
|
||
#[test]
|
||
fn parse_vps_sps_pps() {
|
||
let mut parser = HevcParser::new();
|
||
|
||
let mut data = Vec::new();
|
||
// VPS (type 32)
|
||
data.extend_from_slice(&[0x00, 0x00, 0x01]);
|
||
let vps_hdr = hevc_nal_header(32);
|
||
data.extend_from_slice(&vps_hdr);
|
||
data.extend_from_slice(&[0xAA, 0xBB, 0xCC]); // VPS payload
|
||
|
||
// SPS (type 33)
|
||
data.extend_from_slice(&[0x00, 0x00, 0x01]);
|
||
let sps_hdr = hevc_nal_header(33);
|
||
data.extend_from_slice(&sps_hdr);
|
||
data.extend_from_slice(&[
|
||
0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D,
|
||
]); // SPS payload (>12 bytes for level)
|
||
|
||
// PPS (type 34)
|
||
data.extend_from_slice(&[0x00, 0x00, 0x01]);
|
||
let pps_hdr = hevc_nal_header(34);
|
||
data.extend_from_slice(&pps_hdr);
|
||
data.extend_from_slice(&[0xDD, 0xEE]); // PPS payload
|
||
|
||
// IRAP slice (type 19 = IDR_W_RADL) so a frame is emitted
|
||
data.extend_from_slice(&[0x00, 0x00, 0x01]);
|
||
let idr_hdr = hevc_nal_header(19);
|
||
data.extend_from_slice(&idr_hdr);
|
||
data.extend_from_slice(&[0x10, 0x20, 0x30]);
|
||
|
||
let pes = make_pes(data, Some(90000));
|
||
let _frames = parser.parse(&pes);
|
||
|
||
let cp = parser.codec_private();
|
||
assert!(
|
||
cp.is_some(),
|
||
"codec_private should be Some after VPS+SPS+PPS"
|
||
);
|
||
|
||
let cp = cp.unwrap();
|
||
// configurationVersion = 1
|
||
assert_eq!(cp[0], 1);
|
||
// numOfArrays = 3 (VPS, SPS, PPS)
|
||
assert_eq!(cp[22], 3);
|
||
// Should be longer than the minimal header (23 bytes) + array entries
|
||
assert!(
|
||
cp.len() > 23,
|
||
"codec_private should contain VPS+SPS+PPS data"
|
||
);
|
||
}
|
||
|
||
/// Regression (Fight Club UHD banded corruption): a stream redefines PPS
|
||
/// id 0 mid-title, then a later keyframe arrives WITHOUT repeating it (the
|
||
/// source relies on the decoder retaining the redefinition — valid for a
|
||
/// raw bitstream). An hvcC player re-applies the FIRST (codecPrivate) PPS
|
||
/// at every keyframe, so the active redefinition must be re-asserted
|
||
/// in-band at that bare keyframe or the whole segment decodes against the
|
||
/// wrong parameter set.
|
||
#[test]
|
||
fn reasserts_active_pps_at_bare_keyframe() {
|
||
fn nal(t: u8, body: &[u8]) -> Vec<u8> {
|
||
let mut v = vec![0x00, 0x00, 0x01];
|
||
v.extend_from_slice(&hevc_nal_header(t));
|
||
v.extend_from_slice(body);
|
||
v
|
||
}
|
||
// Split length-prefixed frame_data back into NAL bodies.
|
||
fn nals_in(frame: &[u8]) -> Vec<Vec<u8>> {
|
||
let mut out = Vec::new();
|
||
let mut i = 0;
|
||
while i + 4 <= frame.len() {
|
||
let len = u32::from_be_bytes([frame[i], frame[i + 1], frame[i + 2], frame[i + 3]])
|
||
as usize;
|
||
i += 4;
|
||
if i + len > frame.len() {
|
||
break;
|
||
}
|
||
out.push(frame[i..i + len].to_vec());
|
||
i += len;
|
||
}
|
||
out
|
||
}
|
||
let pps_of = |nals: &[Vec<u8>]| -> Vec<Vec<u8>> {
|
||
nals.iter()
|
||
.filter(|n| n.len() >= 2 && (n[0] >> 1) & 0x3F == 34)
|
||
.map(|n| n[2..].to_vec())
|
||
.collect()
|
||
};
|
||
let sps_body = [
|
||
0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D,
|
||
];
|
||
let pps_a = [0xA1u8, 0xA2];
|
||
let pps_b = [0xB1u8, 0xB2, 0xB3];
|
||
|
||
let mut parser = HevcParser::new();
|
||
|
||
// AU1: seeds codecPrivate with VPS/SPS/PPS-A (all stripped in-band).
|
||
let au1 = [
|
||
nal(32, &[0xAA]),
|
||
nal(33, &sps_body),
|
||
nal(34, &pps_a),
|
||
nal(19, &[0x10]),
|
||
]
|
||
.concat();
|
||
parser.parse(&make_pes(au1, Some(0)));
|
||
|
||
// AU2: keyframe redefines PPS id 0 to body B → emitted in-band.
|
||
let au2 = [nal(34, &pps_b), nal(19, &[0x11])].concat();
|
||
let f2 = parser.parse(&make_pes(au2, Some(3600)));
|
||
assert!(
|
||
pps_of(&nals_in(&f2[0].data)).iter().any(|b| b == &pps_b),
|
||
"AU2 must carry the redefined PPS-B in-band"
|
||
);
|
||
|
||
// AU3: BARE keyframe, source omits the PPS. The active set (B) must be
|
||
// re-asserted, and the stale codecPrivate A must NOT be injected.
|
||
let au3 = nal(19, &[0x12]);
|
||
let f3 = parser.parse(&make_pes(au3, Some(7200)));
|
||
let got = pps_of(&nals_in(&f3[0].data));
|
||
assert!(
|
||
got.iter().any(|b| b == &pps_b),
|
||
"bare keyframe must re-assert the active PPS-B in-band, got {got:?}"
|
||
);
|
||
assert!(
|
||
!got.iter().any(|b| b == &pps_a),
|
||
"must not re-assert the stale codecPrivate PPS-A"
|
||
);
|
||
|
||
// AU4: switch the active set BACK to A (== codecPrivate) via an in-band
|
||
// redefinition (a real change from B → emitted).
|
||
let au4 = [nal(34, &pps_a), nal(19, &[0x13])].concat();
|
||
parser.parse(&make_pes(au4, Some(10800)));
|
||
// AU5: BARE keyframe, source omits the PPS, and the active set now
|
||
// EQUALS codecPrivate. It must STILL be re-asserted in-band — every
|
||
// keyframe is self-contained: a decoder that dropped PPS id 0 at a CRA
|
||
// reset can only recover from an in-band copy, and there is no genuine
|
||
// change here to trigger the emit path.
|
||
let au5 = nal(19, &[0x14]);
|
||
let f5 = parser.parse(&make_pes(au5, Some(14400)));
|
||
assert!(
|
||
pps_of(&nals_in(&f5[0].data)).iter().any(|b| b == &pps_a),
|
||
"bare keyframe must re-assert the active PPS even when == codecPrivate"
|
||
);
|
||
}
|
||
|
||
/// Regression (Fight Club UHD, the real bug): id 0 is body A (→ hvcC), then
|
||
/// redefined to B, then the title switches BACK to A. A streaming decoder
|
||
/// (hvcC at init, in-band updates only) is sitting on B; the switch back to
|
||
/// A must be emitted IN-BAND even though A == codecPrivate, or the whole
|
||
/// A-segment decodes against B (cu_qp_delta desync). Stripping on `== hvcC`
|
||
/// dropped this revert.
|
||
#[test]
|
||
fn emits_switch_back_to_codecprivate_pps() {
|
||
fn nal(t: u8, body: &[u8]) -> Vec<u8> {
|
||
let mut v = vec![0x00, 0x00, 0x01];
|
||
v.extend_from_slice(&hevc_nal_header(t));
|
||
v.extend_from_slice(body);
|
||
v
|
||
}
|
||
fn nals_in(frame: &[u8]) -> Vec<Vec<u8>> {
|
||
let mut out = Vec::new();
|
||
let mut i = 0;
|
||
while i + 4 <= frame.len() {
|
||
let len = u32::from_be_bytes([frame[i], frame[i + 1], frame[i + 2], frame[i + 3]])
|
||
as usize;
|
||
i += 4;
|
||
if i + len > frame.len() {
|
||
break;
|
||
}
|
||
out.push(frame[i..i + len].to_vec());
|
||
i += len;
|
||
}
|
||
out
|
||
}
|
||
let pps_body = |nals: &[Vec<u8>]| -> Vec<Vec<u8>> {
|
||
nals.iter()
|
||
.filter(|n| n.len() >= 2 && (n[0] >> 1) & 0x3F == 34)
|
||
.map(|n| n[2..].to_vec())
|
||
.collect()
|
||
};
|
||
let sps = [
|
||
0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D,
|
||
];
|
||
let a = [0xA1u8, 0xA2];
|
||
let b = [0xB1u8, 0xB2, 0xB3];
|
||
let mut parser = HevcParser::new();
|
||
|
||
// AU1: seeds codecPrivate with PPS-A.
|
||
parser.parse(&make_pes(
|
||
[nal(32, &[0xAA]), nal(33, &sps), nal(34, &a), nal(19, &[1])].concat(),
|
||
Some(0),
|
||
));
|
||
// AU2 keyframe: redefine to B → emitted in-band.
|
||
parser.parse(&make_pes([nal(34, &b), nal(19, &[2])].concat(), Some(3600)));
|
||
// AU3 keyframe: source sends A again (== codecPrivate). Must be emitted
|
||
// in-band because the active set was B.
|
||
let f3 = parser.parse(&make_pes([nal(34, &a), nal(19, &[3])].concat(), Some(7200)));
|
||
assert!(
|
||
pps_body(&nals_in(&f3[0].data)).iter().any(|p| p == &a),
|
||
"switch back to codecPrivate PPS-A must be emitted in-band"
|
||
);
|
||
// AU4 keyframe: A again, now == active AND == codecPrivate. Under the
|
||
// self-contained-keyframe rule it is STILL re-asserted in-band so a
|
||
// decoder that dropped PPS id 0 at this IRAP recovers. handle_param_set
|
||
// strips the source copy (== active), then reassert_active prepends the
|
||
// active set unconditionally.
|
||
let f4 = parser.parse(&make_pes(
|
||
[nal(34, &a), nal(19, &[4])].concat(),
|
||
Some(10800),
|
||
));
|
||
assert!(
|
||
pps_body(&nals_in(&f4[0].data)).iter().any(|p| p == &a),
|
||
"active PPS must be re-asserted at every keyframe (self-contained), even when == codecPrivate"
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn hvcc_profile_tier_level_offsets() {
|
||
// The hvcC fixed header must read profile_tier_level from the SPS
|
||
// RBSP, not from the NAL header. Stored SPS = [2-byte NAL header][RBSP].
|
||
// RBSP layout (byte-aligned):
|
||
// sps[2] sps_vps_id/max_sub_layers/temporal_nesting
|
||
// sps[3] general_profile_space+tier+profile_idc
|
||
// sps[4..8] general_profile_compatibility_flags
|
||
// sps[8..14] general_constraint_indicator_flags
|
||
// sps[14] general_level_idc
|
||
let mut parser = HevcParser::new();
|
||
|
||
// Distinct, recognizable values for each field.
|
||
let sps_rbsp: [u8; 13] = [
|
||
0xAB, // sps[2] (vps_id etc.) — must NOT leak into profile fields
|
||
0x21, // sps[3] profile byte: space=0, tier=0, profile_idc=1
|
||
0x60, 0x00, 0x00, 0x00, // sps[4..8] compat flags
|
||
0x90, 0x00, 0x00, 0x00, 0x00, 0x00, // sps[8..14] constraint flags
|
||
0x7B, // sps[14] level_idc = 123
|
||
];
|
||
|
||
let mut data = Vec::new();
|
||
// VPS
|
||
data.extend_from_slice(&[0x00, 0x00, 0x01]);
|
||
data.extend_from_slice(&hevc_nal_header(32));
|
||
data.extend_from_slice(&[0xAA, 0xBB, 0xCC]);
|
||
// SPS — 2-byte header + the structured RBSP above
|
||
data.extend_from_slice(&[0x00, 0x00, 0x01]);
|
||
data.extend_from_slice(&hevc_nal_header(33));
|
||
data.extend_from_slice(&sps_rbsp);
|
||
// PPS
|
||
data.extend_from_slice(&[0x00, 0x00, 0x01]);
|
||
data.extend_from_slice(&hevc_nal_header(34));
|
||
data.extend_from_slice(&[0xDD, 0xEE]);
|
||
|
||
let pes = make_pes(data, Some(0));
|
||
parser.parse(&pes);
|
||
|
||
let cp = parser
|
||
.codec_private()
|
||
.expect("codec_private should be Some");
|
||
|
||
// record[0] = configurationVersion
|
||
assert_eq!(cp[0], 1, "configurationVersion");
|
||
// record[1] = general_profile_space+tier+profile_idc <- sps[3]
|
||
assert_eq!(
|
||
cp[1], 0x21,
|
||
"profile byte must come from SPS RBSP, not NAL hdr"
|
||
);
|
||
// record[2..6] = general_profile_compatibility_flags <- sps[4..8]
|
||
assert_eq!(&cp[2..6], &[0x60, 0x00, 0x00, 0x00], "compatibility flags");
|
||
// record[6..12] = general_constraint_indicator_flags <- sps[8..14]
|
||
assert_eq!(
|
||
&cp[6..12],
|
||
&[0x90, 0x00, 0x00, 0x00, 0x00, 0x00],
|
||
"constraint flags"
|
||
);
|
||
// record[12] = general_level_idc <- sps[14]
|
||
assert_eq!(cp[12], 0x7B, "level_idc must come from sps[14]");
|
||
}
|
||
|
||
#[test]
|
||
fn hvcc_short_sps_does_not_panic() {
|
||
// A truncated SPS must still produce a fixed header without panicking
|
||
// and zero-pad the missing profile/level bytes.
|
||
let mut parser = HevcParser::new();
|
||
|
||
let mut data = Vec::new();
|
||
data.extend_from_slice(&[0x00, 0x00, 0x01]);
|
||
data.extend_from_slice(&hevc_nal_header(32));
|
||
data.extend_from_slice(&[0xAA]);
|
||
// SPS with only 3 RBSP bytes (stored len = 5): forces every guard path
|
||
data.extend_from_slice(&[0x00, 0x00, 0x01]);
|
||
data.extend_from_slice(&hevc_nal_header(33));
|
||
data.extend_from_slice(&[0x11, 0x22, 0x33]);
|
||
data.extend_from_slice(&[0x00, 0x00, 0x01]);
|
||
data.extend_from_slice(&hevc_nal_header(34));
|
||
data.extend_from_slice(&[0xDD]);
|
||
|
||
let pes = make_pes(data, Some(0));
|
||
parser.parse(&pes);
|
||
|
||
let cp = parser
|
||
.codec_private()
|
||
.expect("codec_private should be Some");
|
||
// sps stored = [hdr0, hdr1, 0x11, 0x22, 0x33], len 5.
|
||
// profile byte = sps[3] = 0x22; everything past sps[4]=0x33 is absent.
|
||
assert_eq!(cp[0], 1);
|
||
assert_eq!(cp[1], 0x22, "profile byte = sps[3]");
|
||
// compat flags: only sps[4]=0x33 present, rest zero-padded.
|
||
assert_eq!(&cp[2..6], &[0x33, 0x00, 0x00, 0x00]);
|
||
// constraint flags: none present, all zero.
|
||
assert_eq!(&cp[6..12], &[0x00, 0x00, 0x00, 0x00, 0x00, 0x00]);
|
||
// level_idc: absent, zero.
|
||
assert_eq!(cp[12], 0x00);
|
||
}
|
||
|
||
#[test]
|
||
fn codec_private_none_before_params() {
|
||
let parser = HevcParser::new();
|
||
assert!(parser.codec_private().is_none());
|
||
}
|
||
|
||
#[test]
|
||
fn codec_private_none_missing_pps() {
|
||
let mut parser = HevcParser::new();
|
||
|
||
// Only VPS + SPS, no PPS
|
||
let mut data = Vec::new();
|
||
data.extend_from_slice(&[0x00, 0x00, 0x01]);
|
||
data.extend_from_slice(&hevc_nal_header(32));
|
||
data.extend_from_slice(&[0xAA, 0xBB]);
|
||
data.extend_from_slice(&[0x00, 0x00, 0x01]);
|
||
data.extend_from_slice(&hevc_nal_header(33));
|
||
data.extend_from_slice(&[0x01, 0x02, 0x03, 0x04]);
|
||
// Add a slice so parse doesn't return empty
|
||
data.extend_from_slice(&[0x00, 0x00, 0x01]);
|
||
data.extend_from_slice(&hevc_nal_header(1)); // TRAIL_R
|
||
data.extend_from_slice(&[0x10, 0x20]);
|
||
|
||
let pes = make_pes(data, Some(0));
|
||
parser.parse(&pes);
|
||
assert!(
|
||
parser.codec_private().is_none(),
|
||
"should be None without PPS"
|
||
);
|
||
}
|
||
|
||
// --- IRAP keyframe detection ---
|
||
|
||
#[test]
|
||
fn parse_irap_keyframe_idr_w_radl() {
|
||
let mut parser = HevcParser::new();
|
||
|
||
let mut data = Vec::new();
|
||
// IDR_W_RADL = type 19
|
||
data.extend_from_slice(&[0x00, 0x00, 0x01]);
|
||
data.extend_from_slice(&hevc_nal_header(19));
|
||
data.extend_from_slice(&[0x10, 0x20, 0x30]);
|
||
|
||
let pes = make_pes(data, Some(90000));
|
||
let frames = parser.parse(&pes);
|
||
|
||
assert_eq!(frames.len(), 1);
|
||
assert!(
|
||
frames[0].keyframe,
|
||
"IDR_W_RADL (type 19) should be keyframe"
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn parse_irap_keyframe_bla() {
|
||
let mut parser = HevcParser::new();
|
||
|
||
// BLA_W_LP = type 16
|
||
let mut data = Vec::new();
|
||
data.extend_from_slice(&[0x00, 0x00, 0x01]);
|
||
data.extend_from_slice(&hevc_nal_header(16));
|
||
data.extend_from_slice(&[0x10, 0x20]);
|
||
|
||
let pes = make_pes(data, Some(0));
|
||
let frames = parser.parse(&pes);
|
||
assert_eq!(frames.len(), 1);
|
||
assert!(frames[0].keyframe, "BLA_W_LP (type 16) should be keyframe");
|
||
}
|
||
|
||
#[test]
|
||
fn parse_irap_keyframe_cra() {
|
||
let mut parser = HevcParser::new();
|
||
|
||
// CRA_NUT = type 21
|
||
let mut data = Vec::new();
|
||
data.extend_from_slice(&[0x00, 0x00, 0x01]);
|
||
data.extend_from_slice(&hevc_nal_header(21));
|
||
data.extend_from_slice(&[0x10, 0x20]);
|
||
|
||
let pes = make_pes(data, Some(0));
|
||
let frames = parser.parse(&pes);
|
||
assert_eq!(frames.len(), 1);
|
||
assert!(frames[0].keyframe, "CRA (type 21) should be keyframe");
|
||
}
|
||
|
||
#[test]
|
||
fn parse_irap_type_23() {
|
||
let mut parser = HevcParser::new();
|
||
|
||
// RSV_IRAP_VCL23 = type 23 (upper boundary)
|
||
let mut data = Vec::new();
|
||
data.extend_from_slice(&[0x00, 0x00, 0x01]);
|
||
data.extend_from_slice(&hevc_nal_header(23));
|
||
data.extend_from_slice(&[0x10, 0x20]);
|
||
|
||
let pes = make_pes(data, Some(0));
|
||
let frames = parser.parse(&pes);
|
||
assert_eq!(frames.len(), 1);
|
||
assert!(frames[0].keyframe, "type 23 should be keyframe");
|
||
}
|
||
|
||
// --- splice-aware CRA→BLA rewrite (non-seamless clip boundary) ---
|
||
|
||
/// Split length-prefixed frame_data into NAL bodies (4-byte BE length + NAL).
|
||
fn nals_of(frame: &[u8]) -> Vec<Vec<u8>> {
|
||
let mut out = Vec::new();
|
||
let mut i = 0;
|
||
while i + 4 <= frame.len() {
|
||
let len =
|
||
u32::from_be_bytes([frame[i], frame[i + 1], frame[i + 2], frame[i + 3]]) as usize;
|
||
i += 4;
|
||
if i + len > frame.len() {
|
||
break;
|
||
}
|
||
out.push(frame[i..i + len].to_vec());
|
||
i += len;
|
||
}
|
||
out
|
||
}
|
||
|
||
fn nal_type_of(nal: &[u8]) -> u8 {
|
||
(nal[0] >> 1) & 0x3F
|
||
}
|
||
|
||
/// Build a standalone CRA (type 21) access unit.
|
||
fn cra_au(payload: &[u8]) -> Vec<u8> {
|
||
let mut d = vec![0x00, 0x00, 0x01];
|
||
d.extend_from_slice(&hevc_nal_header(21));
|
||
d.extend_from_slice(payload);
|
||
d
|
||
}
|
||
|
||
/// Test 1: a CRA at a MARKED non-seamless boundary is rewritten to BLA_W_LP.
|
||
#[test]
|
||
fn cra_at_marked_boundary_rewritten_to_bla() {
|
||
let mut parser = HevcParser::new();
|
||
parser.mark_clip_boundary();
|
||
let frames = parser.parse(&make_pes(cra_au(&[0x10, 0x20, 0x30]), Some(0)));
|
||
assert_eq!(frames.len(), 1);
|
||
assert!(frames[0].keyframe, "rewritten BLA is still a keyframe");
|
||
let nals = nals_of(&frames[0].data);
|
||
assert_eq!(nals.len(), 1);
|
||
assert_eq!(
|
||
nal_type_of(&nals[0]),
|
||
NAL_BLA_W_LP,
|
||
"marked-boundary CRA must be rewritten to BLA_W_LP (16)"
|
||
);
|
||
// The forbidden_zero_bit + layer-id-high (bit 0) and the rest of byte 0,
|
||
// and all payload bytes, are otherwise untouched.
|
||
assert_eq!(nals[0][0] & 0x81, hevc_nal_header(21)[0] & 0x81);
|
||
assert_eq!(&nals[0][2..], &[0x10, 0x20, 0x30]);
|
||
// The flag is one-shot: a SECOND CRA (no new marker) is left as CRA.
|
||
let f2 = parser.parse(&make_pes(cra_au(&[0x40]), Some(90000)));
|
||
assert_eq!(
|
||
nal_type_of(&nals_of(&f2[0].data)[0]),
|
||
NAL_CRA_NUT,
|
||
"only the first CRA after a boundary is rewritten"
|
||
);
|
||
}
|
||
|
||
/// Test 2: a CRA with NO boundary marker is left unchanged (CRA stays CRA).
|
||
#[test]
|
||
fn cra_without_boundary_unchanged() {
|
||
let mut parser = HevcParser::new();
|
||
let frames = parser.parse(&make_pes(cra_au(&[0x10, 0x20]), Some(0)));
|
||
let nals = nals_of(&frames[0].data);
|
||
assert_eq!(
|
||
nal_type_of(&nals[0]),
|
||
NAL_CRA_NUT,
|
||
"an unmarked CRA must remain a CRA"
|
||
);
|
||
}
|
||
|
||
/// Regression for the "TopGun bug" (Top Gun 1986 UHD, DV P7 dual-layer):
|
||
/// a multi-clip title is read as one concatenated stream and the mpls
|
||
/// connection_condition is never plumbed to the parser, so the splice CRA
|
||
/// opening the next clip kept its dangling RASL leading pictures and a
|
||
/// linear decoder flooded "Could not find ref with POC N". The parser must
|
||
/// AUTO-DETECT the boundary from the backward PES-PTS reset between clips
|
||
/// (each .m2ts has its own PTS base) and rewrite that splice CRA → BLA_W_LP
|
||
/// with no explicit `mark_clip_boundary` call.
|
||
#[test]
|
||
fn cra_at_auto_detected_pts_backstep_rewritten_to_bla() {
|
||
let mut parser = HevcParser::new();
|
||
// Clip 1: a CRA then a few trailing frames advancing the PTS watermark.
|
||
// PTS in 90 kHz ticks: 0, then ~1 h into the clip.
|
||
let one_hour = 90_000i64 * 3600;
|
||
parser.parse(&make_pes(cra_au(&[0x01]), Some(0)));
|
||
parser.parse(&make_pes(cra_au(&[0x02]), Some(one_hour)));
|
||
// In-clip B-frame dip: PTS steps back a few frames (< BACKSTEP_TICKS).
|
||
// Must NOT be mistaken for a clip boundary — this CRA stays CRA.
|
||
let dip = parser.parse(&make_pes(cra_au(&[0x03]), Some(one_hour - 3 * 3750)));
|
||
assert_eq!(
|
||
nal_type_of(&nals_of(&dip[0].data)[0]),
|
||
NAL_CRA_NUT,
|
||
"a sub-threshold B-frame PTS dip must not trigger the rewrite"
|
||
);
|
||
// Clip 2 splice: PES PTS resets to a new clip base far below the
|
||
// watermark (> BACKSTEP_TICKS backward). The opening CRA is rewritten.
|
||
let splice = parser.parse(&make_pes(cra_au(&[0x04]), Some(0)));
|
||
assert_eq!(
|
||
nal_type_of(&nals_of(&splice[0].data)[0]),
|
||
NAL_BLA_W_LP,
|
||
"the splice CRA after a backward PTS reset must become BLA_W_LP"
|
||
);
|
||
// One-shot: the NEXT clip-2 CRA (PTS advancing again) stays CRA.
|
||
let next = parser.parse(&make_pes(cra_au(&[0x05]), Some(90_000)));
|
||
assert_eq!(
|
||
nal_type_of(&nals_of(&next[0].data)[0]),
|
||
NAL_CRA_NUT,
|
||
"only the first CRA after the boundary is rewritten"
|
||
);
|
||
}
|
||
|
||
/// Regression for the 33-bit PTS wraparound false-trigger (rc.5.2 audit #1):
|
||
/// a SINGLE clip whose raw 90 kHz PES PTS crosses the 2^33 counter wrap
|
||
/// (~26.5 h) must NOT be mistaken for a non-seamless clip join. Before the
|
||
/// fix the raw 2^33→0 backward step armed `pending_clip_boundary` and the
|
||
/// next in-clip CRA was wrongly rewritten CRA→BLA_W_LP (dropping valid RASL
|
||
/// pictures — visible corruption). After unwrapping onto a monotonic
|
||
/// timeline the wrap is absorbed and the CRA stays CRA.
|
||
#[test]
|
||
fn cra_after_33bit_pts_wrap_not_rewritten() {
|
||
let mut parser = HevcParser::new();
|
||
let period = 1i64 << 33;
|
||
// Single clip, PTS climbing toward the 33-bit wrap. Start just below 2^33.
|
||
let near_wrap = period - 90_000; // ~1 s before the wrap point
|
||
parser.parse(&make_pes(cra_au(&[0x01]), Some(near_wrap)));
|
||
parser.parse(&make_pes(cra_au(&[0x02]), Some(near_wrap + 3750)));
|
||
// The counter wraps: raw PTS resets to a small value, but this is the
|
||
// SAME continuous clip, one frame later. A naive raw comparison sees a
|
||
// ~2^33 backward step and false-arms the boundary.
|
||
let wrapped = parser.parse(&make_pes(cra_au(&[0x03]), Some(7500)));
|
||
assert_eq!(
|
||
nal_type_of(&nals_of(&wrapped[0].data)[0]),
|
||
NAL_CRA_NUT,
|
||
"a CRA whose PTS merely wrapped 2^33->0 must stay CRA, not become BLA"
|
||
);
|
||
// Continue past the wrap: PTS keeps climbing from the new low base; still
|
||
// one continuous clip, the CRA after must remain CRA.
|
||
let after = parser.parse(&make_pes(cra_au(&[0x04]), Some(11250)));
|
||
assert_eq!(
|
||
nal_type_of(&nals_of(&after[0].data)[0]),
|
||
NAL_CRA_NUT,
|
||
"post-wrap in-clip CRA must stay CRA"
|
||
);
|
||
}
|
||
|
||
/// Test 3: non-CRA NALs are never rewritten even when a boundary IS marked.
|
||
/// IDR (19), RASL (8/9), VPS/SPS/PPS, and a trailing slice all pass through
|
||
/// unmodified; the IDR clears the pending boundary so no later CRA is wrongly
|
||
/// converted.
|
||
#[test]
|
||
fn non_cra_nals_never_rewritten_at_boundary() {
|
||
// IDR boundary: marker set, but the first IRAP is an IDR → no rewrite,
|
||
// and the marker is consumed so a later CRA is untouched.
|
||
let mut parser = HevcParser::new();
|
||
parser.mark_clip_boundary();
|
||
let mut idr = vec![0x00, 0x00, 0x01];
|
||
idr.extend_from_slice(&hevc_nal_header(19)); // IDR_W_RADL
|
||
idr.extend_from_slice(&[0x10]);
|
||
let f = parser.parse(&make_pes(idr, Some(0)));
|
||
assert_eq!(
|
||
nal_type_of(&nals_of(&f[0].data)[0]),
|
||
19,
|
||
"IDR at a marked boundary must stay IDR"
|
||
);
|
||
// Marker was consumed by the IDR: a following CRA is NOT rewritten.
|
||
let f2 = parser.parse(&make_pes(cra_au(&[0x20]), Some(90000)));
|
||
assert_eq!(
|
||
nal_type_of(&nals_of(&f2[0].data)[0]),
|
||
NAL_CRA_NUT,
|
||
"the IDR consumed the boundary marker; later CRA stays CRA"
|
||
);
|
||
|
||
// RASL leading pictures (types 8/9) preceding the splice CRA must not be
|
||
// touched and must not consume the marker — only the CRA itself does.
|
||
let mut parser = HevcParser::new();
|
||
parser.mark_clip_boundary();
|
||
let mut au = vec![0x00, 0x00, 0x01];
|
||
au.extend_from_slice(&hevc_nal_header(8)); // RASL_N
|
||
au.extend_from_slice(&[0xAA]);
|
||
au.extend_from_slice(&[0x00, 0x00, 0x01]);
|
||
au.extend_from_slice(&hevc_nal_header(9)); // RASL_R
|
||
au.extend_from_slice(&[0xBB]);
|
||
au.extend_from_slice(&cra_au(&[0xCC])); // CRA after the RASLs
|
||
let f = parser.parse(&make_pes(au, Some(0)));
|
||
let nals = nals_of(&f[0].data);
|
||
let types: Vec<u8> = nals.iter().map(|n| nal_type_of(n)).collect();
|
||
assert_eq!(
|
||
types,
|
||
vec![8, 9, NAL_BLA_W_LP],
|
||
"RASLs pass through untouched; the CRA (after them) becomes BLA"
|
||
);
|
||
}
|
||
|
||
/// Test 4: a frame stream with NO boundary marker is BYTE-IDENTICAL to a
|
||
/// parser that has no splice-rewrite field at all (the UHD-safety guarantee).
|
||
/// We assert byte-equality of every emitted frame across a multi-AU stream
|
||
/// containing CRAs, IDRs, RASLs, VPS/SPS/PPS, and trailing slices — none of
|
||
/// which is ever marked.
|
||
#[test]
|
||
fn no_boundary_marker_is_byte_identical() {
|
||
let build = || {
|
||
let mut d = Vec::new();
|
||
// AU0: VPS/SPS/PPS + CRA keyframe.
|
||
d.extend_from_slice(&[0x00, 0x00, 0x01]);
|
||
d.extend_from_slice(&hevc_nal_header(32));
|
||
d.extend_from_slice(&[0xAA]);
|
||
d.extend_from_slice(&[0x00, 0x00, 0x01]);
|
||
d.extend_from_slice(&hevc_nal_header(33));
|
||
d.extend_from_slice(&[0xBB, 0xCC, 0xDD]);
|
||
d.extend_from_slice(&[0x00, 0x00, 0x01]);
|
||
d.extend_from_slice(&hevc_nal_header(34));
|
||
d.extend_from_slice(&[0xEE]);
|
||
d.extend_from_slice(&cra_au(&[0x11, 0x22]));
|
||
d
|
||
};
|
||
// Reference parser: the rewrite field exists but is NEVER marked, so its
|
||
// output is exactly the pre-feature behaviour. We compare a never-marked
|
||
// run against a second never-marked run AND against the documented
|
||
// invariant that the CRA is emitted as-is (type 21, payload intact).
|
||
let mut a = HevcParser::new();
|
||
let mut b = HevcParser::new();
|
||
let fa = a.parse(&make_pes(build(), Some(0)));
|
||
let fb = b.parse(&make_pes(build(), Some(0)));
|
||
assert_eq!(fa.len(), 1);
|
||
assert_eq!(fa[0].data, fb[0].data, "never-marked output must be stable");
|
||
// And the CRA was NOT converted (type 21 still present, no BLA).
|
||
let types: Vec<u8> = nals_of(&fa[0].data)
|
||
.iter()
|
||
.map(|n| nal_type_of(n))
|
||
.collect();
|
||
assert!(
|
||
types.contains(&NAL_CRA_NUT) && !types.contains(&NAL_BLA_W_LP),
|
||
"unmarked stream must keep its CRA (no BLA), got {types:?}"
|
||
);
|
||
|
||
// Feed a second AU (a CRA) to the same unmarked parser: still a CRA.
|
||
// Param sets are re-asserted ahead of the keyframe, so locate the CRA
|
||
// among the emitted NALs rather than assuming it is first.
|
||
let f2 = a.parse(&make_pes(cra_au(&[0x33]), Some(90000)));
|
||
let t2: Vec<u8> = nals_of(&f2[0].data)
|
||
.iter()
|
||
.map(|n| nal_type_of(n))
|
||
.collect();
|
||
assert!(
|
||
t2.contains(&NAL_CRA_NUT) && !t2.contains(&NAL_BLA_W_LP),
|
||
"unmarked mid-stream CRA must never become BLA, got {t2:?}"
|
||
);
|
||
}
|
||
|
||
/// Test 5: a SEAMLESS boundary (connection_condition 0x05/0x06) is expressed
|
||
/// by NOT calling `mark_clip_boundary`, so a CRA across a seamless join is
|
||
/// left unchanged. This encodes the contract: only non-seamless joins call
|
||
/// `mark_clip_boundary`; seamless ones never do, so no rewrite occurs.
|
||
#[test]
|
||
fn seamless_boundary_no_rewrite() {
|
||
// Simulate two clips joined seamlessly: the caller does NOT mark, so the
|
||
// second clip's opening CRA stays a CRA.
|
||
let mut parser = HevcParser::new();
|
||
// Clip 1 ends with a CRA (no marker — mid-content).
|
||
let f1 = parser.parse(&make_pes(cra_au(&[0x01]), Some(0)));
|
||
assert_eq!(nal_type_of(&nals_of(&f1[0].data)[0]), NAL_CRA_NUT);
|
||
// Seamless join: caller deliberately does NOT call mark_clip_boundary().
|
||
// Clip 2 opens with a CRA → must remain a CRA.
|
||
let f2 = parser.parse(&make_pes(cra_au(&[0x02]), Some(90000)));
|
||
assert_eq!(
|
||
nal_type_of(&nals_of(&f2[0].data)[0]),
|
||
NAL_CRA_NUT,
|
||
"a seamless join (no marker) must never rewrite the CRA"
|
||
);
|
||
}
|
||
|
||
// --- non-IRAP (trailing) → not keyframe ---
|
||
|
||
#[test]
|
||
fn parse_trailing_not_keyframe() {
|
||
let mut parser = HevcParser::new();
|
||
|
||
// TRAIL_R = type 1
|
||
let mut data = Vec::new();
|
||
data.extend_from_slice(&[0x00, 0x00, 0x01]);
|
||
data.extend_from_slice(&hevc_nal_header(1));
|
||
data.extend_from_slice(&[0x10, 0x20, 0x30]);
|
||
|
||
let pes = make_pes(data, Some(180000));
|
||
let frames = parser.parse(&pes);
|
||
|
||
assert_eq!(frames.len(), 1);
|
||
assert!(
|
||
!frames[0].keyframe,
|
||
"TRAIL_R (type 1) should not be keyframe"
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn parse_tsa_not_keyframe() {
|
||
let mut parser = HevcParser::new();
|
||
|
||
// TSA_N = type 2
|
||
let mut data = Vec::new();
|
||
data.extend_from_slice(&[0x00, 0x00, 0x01]);
|
||
data.extend_from_slice(&hevc_nal_header(2));
|
||
data.extend_from_slice(&[0x10, 0x20]);
|
||
|
||
let pes = make_pes(data, Some(0));
|
||
let frames = parser.parse(&pes);
|
||
assert_eq!(frames.len(), 1);
|
||
assert!(!frames[0].keyframe, "TSA_N (type 2) should not be keyframe");
|
||
}
|
||
|
||
// --- VPS/SPS/PPS stripped from frame data ---
|
||
|
||
#[test]
|
||
fn param_sets_seed_codecprivate_and_reassert_at_keyframe() {
|
||
let mut parser = HevcParser::new();
|
||
|
||
let mut data = Vec::new();
|
||
// VPS
|
||
data.extend_from_slice(&[0x00, 0x00, 0x01]);
|
||
data.extend_from_slice(&hevc_nal_header(32));
|
||
data.extend_from_slice(&[0xAA]);
|
||
// SPS
|
||
data.extend_from_slice(&[0x00, 0x00, 0x01]);
|
||
data.extend_from_slice(&hevc_nal_header(33));
|
||
data.extend_from_slice(&[0xBB]);
|
||
// PPS
|
||
data.extend_from_slice(&[0x00, 0x00, 0x01]);
|
||
data.extend_from_slice(&hevc_nal_header(34));
|
||
data.extend_from_slice(&[0xCC]);
|
||
// IDR slice
|
||
data.extend_from_slice(&[0x00, 0x00, 0x01]);
|
||
let idr_hdr = hevc_nal_header(19);
|
||
data.extend_from_slice(&idr_hdr);
|
||
data.extend_from_slice(&[0x10, 0x20]);
|
||
|
||
let pes = make_pes(data, Some(0));
|
||
let frames = parser.parse(&pes);
|
||
assert_eq!(frames.len(), 1);
|
||
|
||
// The param sets seed codecPrivate (hvcC).
|
||
assert!(
|
||
parser.codec_private().is_some(),
|
||
"VPS/SPS/PPS must seed codecPrivate"
|
||
);
|
||
|
||
// Because this is a keyframe, the active VPS/SPS/PPS are ALSO re-asserted
|
||
// in-band ahead of the IDR so the keyframe is self-contained. Frame data
|
||
// = VPS, SPS, PPS, IDR (4 length-prefixed NALs, in that order).
|
||
let fd = &frames[0].data;
|
||
let mut types = Vec::new();
|
||
let mut o = 0;
|
||
while o + 4 <= fd.len() {
|
||
let len = u32::from_be_bytes([fd[o], fd[o + 1], fd[o + 2], fd[o + 3]]) as usize;
|
||
o += 4;
|
||
types.push((fd[o] >> 1) & 0x3F);
|
||
o += len;
|
||
}
|
||
assert_eq!(
|
||
types,
|
||
vec![32, 33, 34, 19],
|
||
"keyframe must re-assert VPS/SPS/PPS in-band ahead of the IDR slice"
|
||
);
|
||
}
|
||
|
||
// --- parameter-set redefinition (mid-title redefinition bug) ---
|
||
|
||
/// A parameter set REDEFINED mid-stream (same id, different body) must be
|
||
/// emitted INLINE so the decoder re-activates it. Some discs redefine PPS
|
||
/// id 0 partway through the title; the old parser kept only the first PPS,
|
||
/// so the second segment decoded against the wrong PPS (CABAC desync).
|
||
#[test]
|
||
fn redefined_pps_emitted_inline() {
|
||
let mut parser = HevcParser::new();
|
||
let pps = |body: u8| {
|
||
let mut v = vec![0x00, 0x00, 0x01];
|
||
v.extend_from_slice(&hevc_nal_header(34)); // PPS
|
||
v.extend_from_slice(&[body, body]);
|
||
v
|
||
};
|
||
let slice = || {
|
||
let mut v = vec![0x00, 0x00, 0x01];
|
||
v.extend_from_slice(&hevc_nal_header(1)); // TRAIL_R
|
||
v.extend_from_slice(&[0x10, 0x20]);
|
||
v
|
||
};
|
||
// count PPS (type 34) NALs in length-prefixed frame data
|
||
let count_pps = |fd: &[u8]| {
|
||
let (mut n, mut o) = (0usize, 0usize);
|
||
while o + 4 <= fd.len() {
|
||
let len = u32::from_be_bytes([fd[o], fd[o + 1], fd[o + 2], fd[o + 3]]) as usize;
|
||
o += 4;
|
||
if o < fd.len() && (fd[o] >> 1) & 0x3F == 34 {
|
||
n += 1;
|
||
}
|
||
o += len;
|
||
}
|
||
n
|
||
};
|
||
|
||
// PES1: first PPS-A → seeds codecPrivate, stripped from frame.
|
||
let mut d = pps(0xAA);
|
||
d.extend(slice());
|
||
let f = parser.parse(&make_pes(d, Some(0)));
|
||
assert_eq!(count_pps(&f[0].data), 0, "first PPS goes to codecPrivate");
|
||
|
||
// PES2: PPS-B (redefinition, different body) → emitted INLINE.
|
||
let mut d = pps(0xBB);
|
||
d.extend(slice());
|
||
let f = parser.parse(&make_pes(d, Some(1)));
|
||
assert_eq!(count_pps(&f[0].data), 1, "redefined PPS must be inline");
|
||
|
||
// PES3: PPS-B repeated on a NON-keyframe slice — B is already the active
|
||
// set, so this carries no change and is stripped. (Re-assertion for
|
||
// players that re-apply hvcC at keyframes is handled by
|
||
// `reassert_active` at KEYFRAMES, not on every trailing frame; these
|
||
// slices are TRAIL_R, not IRAP.)
|
||
let mut d = pps(0xBB);
|
||
d.extend(slice());
|
||
let f = parser.parse(&make_pes(d, Some(2)));
|
||
assert_eq!(
|
||
count_pps(&f[0].data),
|
||
0,
|
||
"PPS equal to the active set carries no change → stripped"
|
||
);
|
||
|
||
// PES4: back to PPS-A. Even though A == codecPrivate, the ACTIVE set is
|
||
// B, so switching to A is a real change and MUST be emitted in-band — a
|
||
// streaming decoder (hvcC at init, in-band updates only) is sitting on B
|
||
// and would otherwise never revert. (This is the Fight Club bug: the old
|
||
// `== codecPrivate → strip` rule dropped exactly this revert.)
|
||
let mut d = pps(0xAA);
|
||
d.extend(slice());
|
||
let f = parser.parse(&make_pes(d, Some(3)));
|
||
assert_eq!(
|
||
count_pps(&f[0].data),
|
||
1,
|
||
"switch back to the codecPrivate body is a change → emitted in-band"
|
||
);
|
||
}
|
||
|
||
// --- empty NAL between adjacent start codes is skipped ---
|
||
|
||
#[test]
|
||
fn empty_nal_between_start_codes_emits_no_bare_prefix() {
|
||
// `00 00 01 00 00 01 <real NAL>`: the first start code is immediately
|
||
// followed by another, so the in-between NAL is empty after the
|
||
// trailing-zero strip. It must be skipped, NOT written as a bare
|
||
// 0x00000000 length prefix (which a decoder treats as malformed).
|
||
let mut parser = HevcParser::new();
|
||
let mut data = Vec::new();
|
||
data.extend_from_slice(&[0x00, 0x00, 0x01]); // start code, empty NAL
|
||
data.extend_from_slice(&[0x00, 0x00, 0x01]); // next start code
|
||
data.extend_from_slice(&hevc_nal_header(1)); // TRAIL_R
|
||
data.extend_from_slice(&[0x10, 0x20]);
|
||
|
||
let frames = parser.parse(&make_pes(data, Some(0)));
|
||
assert_eq!(frames.len(), 1);
|
||
let fd = &frames[0].data;
|
||
// Exactly one length-prefixed NAL — no zero-length entry.
|
||
let len = u32::from_be_bytes([fd[0], fd[1], fd[2], fd[3]]) as usize;
|
||
assert!(len > 0, "no bare zero-length prefix emitted");
|
||
assert_eq!(len + 4, fd.len(), "exactly one NAL in frame data");
|
||
}
|
||
|
||
// --- empty PES ---
|
||
|
||
#[test]
|
||
fn parse_empty_pes() {
|
||
let mut parser = HevcParser::new();
|
||
let pes = make_pes(Vec::new(), Some(0));
|
||
let frames = parser.parse(&pes);
|
||
assert!(frames.is_empty());
|
||
}
|
||
|
||
// --- PTS conversion ---
|
||
|
||
#[test]
|
||
fn pts_conversion() {
|
||
let mut parser = HevcParser::new();
|
||
|
||
let mut data = Vec::new();
|
||
data.extend_from_slice(&[0x00, 0x00, 0x01]);
|
||
data.extend_from_slice(&hevc_nal_header(1));
|
||
data.extend_from_slice(&[0x10, 0x20]);
|
||
|
||
let pes = make_pes(data, Some(90000));
|
||
let frames = parser.parse(&pes);
|
||
assert_eq!(frames.len(), 1);
|
||
assert_eq!(frames[0].pts_ns, 1_000_000_000);
|
||
}
|
||
|
||
// --- PTS (presentation), not DTS, drives the MKV block timecode ---
|
||
// Regression for B-frame presentation: writing DTS as the block timecode
|
||
// presents frames in decode order (visible judder) and breaks seeking.
|
||
|
||
#[test]
|
||
fn pts_preferred_over_dts() {
|
||
let mut parser = HevcParser::new();
|
||
|
||
let mut data = Vec::new();
|
||
data.extend_from_slice(&[0x00, 0x00, 0x01]);
|
||
data.extend_from_slice(&hevc_nal_header(1)); // TRAIL_R slice
|
||
data.extend_from_slice(&[0x10, 0x20]);
|
||
|
||
let pes = PesPacket {
|
||
source: None,
|
||
pid: 0x1011,
|
||
pts: Some(180000), // 2 s (presentation)
|
||
dts: Some(90000), // 1 s (decode)
|
||
data,
|
||
};
|
||
let frames = parser.parse(&pes);
|
||
assert_eq!(frames.len(), 1);
|
||
assert_eq!(
|
||
frames[0].pts_ns, 2_000_000_000,
|
||
"block timecode must be PTS"
|
||
);
|
||
}
|
||
|
||
// --- Dolby Vision enhancement layer ---
|
||
|
||
#[test]
|
||
fn dv_rpu_nal_preserved() {
|
||
// Dolby Vision enhancement layer streams contain RPU (Reference Processing
|
||
// Unit) metadata as NAL type 62 (UNSPEC62). The HEVC parser must pass these
|
||
// through to the frame data — only VPS/SPS/PPS/AUD are stripped.
|
||
let mut parser = HevcParser::new();
|
||
|
||
let mut data = Vec::new();
|
||
|
||
// VPS (type 32) — should be stripped from frame data
|
||
data.extend_from_slice(&[0x00, 0x00, 0x01]);
|
||
data.extend_from_slice(&hevc_nal_header(32));
|
||
data.extend_from_slice(&[0xAA, 0xBB]);
|
||
|
||
// SPS (type 33) — should be stripped from frame data
|
||
data.extend_from_slice(&[0x00, 0x00, 0x01]);
|
||
data.extend_from_slice(&hevc_nal_header(33));
|
||
data.extend_from_slice(&[0x01, 0x02, 0x03, 0x04]);
|
||
|
||
// PPS (type 34) — should be stripped from frame data
|
||
data.extend_from_slice(&[0x00, 0x00, 0x01]);
|
||
data.extend_from_slice(&hevc_nal_header(34));
|
||
data.extend_from_slice(&[0xDD, 0xEE]);
|
||
|
||
// IDR_W_RADL slice (type 19) — should appear in frame data
|
||
data.extend_from_slice(&[0x00, 0x00, 0x01]);
|
||
let idr_hdr = hevc_nal_header(19);
|
||
data.extend_from_slice(&idr_hdr);
|
||
data.extend_from_slice(&[0x10, 0x20, 0x30]);
|
||
|
||
// Dolby Vision RPU (type 62 = UNSPEC62) — MUST appear in frame data
|
||
data.extend_from_slice(&[0x00, 0x00, 0x01]);
|
||
let rpu_hdr = hevc_nal_header(62);
|
||
data.extend_from_slice(&rpu_hdr);
|
||
let rpu_payload = [0xF0, 0xF1, 0xF2, 0xF3, 0xF4];
|
||
data.extend_from_slice(&rpu_payload);
|
||
|
||
let pes = make_pes(data, Some(90000));
|
||
let frames = parser.parse(&pes);
|
||
|
||
assert_eq!(frames.len(), 1, "should produce one frame");
|
||
assert!(frames[0].keyframe, "IDR should mark keyframe");
|
||
|
||
// Verify the frame data contains both the IDR NAL and the RPU NAL.
|
||
// Frame data is length-prefixed NALUs (4-byte big-endian length + NAL bytes).
|
||
let fd = &frames[0].data;
|
||
|
||
// Walk the length-prefixed NALUs and collect their types
|
||
let mut nal_types = Vec::new();
|
||
let mut offset = 0;
|
||
while offset + 4 <= fd.len() {
|
||
let length =
|
||
u32::from_be_bytes([fd[offset], fd[offset + 1], fd[offset + 2], fd[offset + 3]])
|
||
as usize;
|
||
offset += 4;
|
||
assert!(offset + length <= fd.len(), "NAL length exceeds frame data");
|
||
let nal_type = (fd[offset] >> 1) & 0x3F;
|
||
nal_types.push(nal_type);
|
||
offset += length;
|
||
}
|
||
|
||
assert!(
|
||
nal_types.contains(&19),
|
||
"frame data must contain IDR NAL (type 19), got: {:?}",
|
||
nal_types
|
||
);
|
||
assert!(
|
||
nal_types.contains(&62),
|
||
"frame data must contain Dolby Vision RPU NAL (type 62), got: {:?}",
|
||
nal_types
|
||
);
|
||
// Self-contained keyframe: the active VPS/SPS/PPS are re-asserted in-band
|
||
// ahead of the IDR, so the frame is VPS, SPS, PPS, IDR, RPU — in that
|
||
// order. The RPU (type 62) is preserved (never stripped); only the
|
||
// duplicate-suppression of unchanged param sets was lifted at keyframes.
|
||
assert_eq!(
|
||
nal_types,
|
||
vec![32, 33, 34, 19, 62],
|
||
"keyframe carries re-asserted param sets + IDR + preserved RPU, got: {:?}",
|
||
nal_types
|
||
);
|
||
|
||
// Verify RPU payload is intact
|
||
let mut offset = 0;
|
||
while offset + 4 <= fd.len() {
|
||
let length =
|
||
u32::from_be_bytes([fd[offset], fd[offset + 1], fd[offset + 2], fd[offset + 3]])
|
||
as usize;
|
||
offset += 4;
|
||
let nal_type = (fd[offset] >> 1) & 0x3F;
|
||
if nal_type == 62 {
|
||
// NAL = 2-byte header + payload
|
||
let nal_payload = &fd[offset + 2..offset + length];
|
||
assert_eq!(
|
||
nal_payload, &rpu_payload,
|
||
"RPU payload must be preserved verbatim"
|
||
);
|
||
}
|
||
offset += length;
|
||
}
|
||
}
|
||
|
||
// --- hvcC chroma / bit-depth from SPS ---
|
||
|
||
/// MSB-first bit writer for building a test SPS RBSP.
|
||
struct BitWriter {
|
||
bytes: Vec<u8>,
|
||
nbits: usize,
|
||
}
|
||
impl BitWriter {
|
||
fn new() -> Self {
|
||
Self {
|
||
bytes: Vec::new(),
|
||
nbits: 0,
|
||
}
|
||
}
|
||
fn put_bit(&mut self, b: u32) {
|
||
if self.nbits % 8 == 0 {
|
||
self.bytes.push(0);
|
||
}
|
||
if b & 1 != 0 {
|
||
let i = self.nbits / 8;
|
||
let shift = 7 - (self.nbits % 8);
|
||
self.bytes[i] |= 1 << shift;
|
||
}
|
||
self.nbits += 1;
|
||
}
|
||
fn put_bits(&mut self, v: u32, n: u32) {
|
||
for i in (0..n).rev() {
|
||
self.put_bit((v >> i) & 1);
|
||
}
|
||
}
|
||
fn put_ue(&mut self, v: u32) {
|
||
let val = v + 1;
|
||
let bits = 32 - val.leading_zeros();
|
||
for _ in 0..bits - 1 {
|
||
self.put_bit(0);
|
||
}
|
||
for i in (0..bits).rev() {
|
||
self.put_bit((val >> i) & 1);
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Build a stored SPS NAL ([2-byte header][RBSP]) with the given
|
||
/// chroma_format_idc and bit depths, max_sub_layers_minus1 = 0.
|
||
fn make_sps_with_chroma(chroma_idc: u32, bd_luma_m8: u32, bd_chroma_m8: u32) -> Vec<u8> {
|
||
let mut w = BitWriter::new();
|
||
w.put_bits(0, 4); // sps_video_parameter_set_id
|
||
w.put_bits(0, 3); // sps_max_sub_layers_minus1 = 0
|
||
w.put_bit(1); // sps_temporal_id_nesting_flag
|
||
// general profile_tier_level: 96 bits (12 bytes) of zeros is fine here.
|
||
for _ in 0..96 {
|
||
w.put_bit(0);
|
||
}
|
||
w.put_ue(0); // sps_seq_parameter_set_id
|
||
w.put_ue(chroma_idc); // chroma_format_idc
|
||
if chroma_idc == 3 {
|
||
w.put_bit(0); // separate_colour_plane_flag
|
||
}
|
||
w.put_ue(3840); // pic_width_in_luma_samples
|
||
w.put_ue(2160); // pic_height_in_luma_samples
|
||
w.put_bit(0); // conformance_window_flag = 0
|
||
w.put_ue(bd_luma_m8); // bit_depth_luma_minus8
|
||
w.put_ue(bd_chroma_m8); // bit_depth_chroma_minus8
|
||
|
||
let mut sps = hevc_nal_header(33).to_vec();
|
||
sps.extend_from_slice(&w.bytes);
|
||
sps
|
||
}
|
||
|
||
fn codec_private_from_sps(sps_nal: &[u8]) -> Vec<u8> {
|
||
let mut parser = HevcParser::new();
|
||
// VPS + the given SPS + PPS, all length-prefixed in one PES.
|
||
let mut data = Vec::new();
|
||
data.extend_from_slice(&[0x00, 0x00, 0x01]);
|
||
data.extend_from_slice(&hevc_nal_header(32));
|
||
data.extend_from_slice(&[0xAA, 0xBB]);
|
||
data.extend_from_slice(&[0x00, 0x00, 0x01]);
|
||
data.extend_from_slice(sps_nal);
|
||
data.extend_from_slice(&[0x00, 0x00, 0x01]);
|
||
data.extend_from_slice(&hevc_nal_header(34));
|
||
data.extend_from_slice(&[0xDD, 0xEE]);
|
||
parser.parse(&make_pes(data, Some(0)));
|
||
parser.codec_private().expect("codec_private")
|
||
}
|
||
|
||
#[test]
|
||
fn hvcc_emits_10bit_420_from_sps() {
|
||
// Main 10 UHD: chroma_format_idc=1 (4:2:0), bit depths = 10 (minus8 = 2).
|
||
let sps = make_sps_with_chroma(1, 2, 2);
|
||
let cp = codec_private_from_sps(&sps);
|
||
// chromaFormat at cp[16], bit depths at cp[17]/cp[18].
|
||
assert_eq!(cp[16], 0xFC | 1, "chroma_format_idc = 1 (4:2:0)");
|
||
assert_eq!(cp[17], 0xF8 | 2, "bit_depth_luma_minus8 = 2 (10-bit)");
|
||
assert_eq!(cp[18], 0xF8 | 2, "bit_depth_chroma_minus8 = 2 (10-bit)");
|
||
}
|
||
|
||
#[test]
|
||
fn hvcc_emits_8bit_420_from_sps() {
|
||
// 8-bit 4:2:0 must still report correctly (not a regression).
|
||
let sps = make_sps_with_chroma(1, 0, 0);
|
||
let cp = codec_private_from_sps(&sps);
|
||
assert_eq!(cp[16], 0xFC | 1);
|
||
assert_eq!(cp[17], 0xF8);
|
||
assert_eq!(cp[18], 0xF8);
|
||
}
|
||
|
||
#[test]
|
||
fn hvcc_emits_444_12bit_from_sps() {
|
||
// 4:4:4 (idc=3) with 12-bit depth (minus8 = 4).
|
||
let sps = make_sps_with_chroma(3, 4, 4);
|
||
let cp = codec_private_from_sps(&sps);
|
||
assert_eq!(cp[16], 0xFC | 3, "chroma_format_idc = 3 (4:4:4)");
|
||
assert_eq!(cp[17], 0xF8 | 4, "bit_depth_luma_minus8 = 4 (12-bit)");
|
||
assert_eq!(cp[18], 0xF8 | 4);
|
||
}
|
||
|
||
#[test]
|
||
fn hvcc_byte21_from_sps_temporal_layers() {
|
||
// make_sps_with_chroma sets sps_max_sub_layers_minus1 = 0 and
|
||
// sps_temporal_id_nesting_flag = 1, so byte 21 must encode
|
||
// numTemporalLayers = 1, temporalIdNested = 1, lengthSizeMinusOne = 3:
|
||
// (1 << 3) | (1 << 2) | 3 = 0x0F.
|
||
let sps = make_sps_with_chroma(1, 2, 2);
|
||
let cp = codec_private_from_sps(&sps);
|
||
assert_eq!(
|
||
cp[21], 0x0F,
|
||
"byte 21: numTemporalLayers=1, temporalIdNested=1, lengthSizeMinusOne=3"
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn hvcc_handles_emulation_prevention_in_sps() {
|
||
// Insert an emulation-prevention byte (00 00 03) into the SPS RBSP and
|
||
// confirm the chroma/bit-depth parse still lands on the right values.
|
||
// Build a 10-bit 4:2:0 SPS, then splice 00 00 03 into the RBSP tail
|
||
// (after the fields we parse) — the strip must not corrupt earlier bits.
|
||
let mut sps = make_sps_with_chroma(1, 2, 2);
|
||
// Append a benign 00 00 03 sequence to the RBSP.
|
||
sps.extend_from_slice(&[0x00, 0x00, 0x03, 0x00]);
|
||
let cp = codec_private_from_sps(&sps);
|
||
assert_eq!(cp[16], 0xFC | 1);
|
||
assert_eq!(cp[17], 0xF8 | 2);
|
||
assert_eq!(cp[18], 0xF8 | 2);
|
||
}
|
||
|
||
// --- BitReader unit tests (exp-Golomb + bit reads) ---
|
||
|
||
#[test]
|
||
fn bitreader_read_bits_msb_first() {
|
||
// 0b1011_0010 read 4 bits → 0b1011 = 11, then 4 → 0b0010 = 2.
|
||
let mut r = BitReader::new(&[0b1011_0010]);
|
||
assert_eq!(r.read_bits(4), Some(11));
|
||
assert_eq!(r.read_bits(4), Some(2));
|
||
// Past end → None.
|
||
assert_eq!(r.read_bit(), None);
|
||
}
|
||
|
||
#[test]
|
||
fn bitreader_ue_golomb_values() {
|
||
// Exp-Golomb ue(v): codeNum 0 = "1", 1 = "010", 2 = "011", 3 = "00100",
|
||
// 4 = "00101". (H.264/HEVC §9.1.) Pack "1 010 011" = 1010011x.
|
||
// Byte 0b1010_0110: read ue → 0 (leading "1"), then "010" → 1, then
|
||
// "011" → 2.
|
||
let mut r = BitReader::new(&[0b1010_0110]);
|
||
assert_eq!(r.read_ue(), Some(0));
|
||
assert_eq!(r.read_ue(), Some(1));
|
||
assert_eq!(r.read_ue(), Some(2));
|
||
}
|
||
|
||
#[test]
|
||
fn bitreader_ue_large_value() {
|
||
// codeNum 4 = "00101". Byte 0b0010_1000 → ue = 4.
|
||
let mut r = BitReader::new(&[0b0010_1000]);
|
||
assert_eq!(r.read_ue(), Some(4));
|
||
}
|
||
|
||
#[test]
|
||
fn bitreader_ue_runaway_zeros_bounded() {
|
||
// A corrupt all-zero stream has unbounded leading zeros; read_ue caps at
|
||
// 31 zeros and returns None rather than looping/overflowing.
|
||
let zeros = [0u8; 8]; // 64 zero bits
|
||
let mut r = BitReader::new(&zeros);
|
||
assert_eq!(r.read_ue(), None, "runaway zero-run is bounded → None");
|
||
}
|
||
|
||
#[test]
|
||
fn bitreader_skip_bits_past_end_is_none() {
|
||
let mut r = BitReader::new(&[0xFF]);
|
||
assert_eq!(r.skip_bits(8), Some(()));
|
||
assert_eq!(r.skip_bits(1), None, "skipping past the buffer end → None");
|
||
}
|
||
|
||
// --- strip_emulation_prevention (00 00 03 → 00 00) ---
|
||
|
||
#[test]
|
||
fn strip_ep_removes_third_byte_after_two_zeros() {
|
||
// 00 00 03 XX → 00 00 XX. The 0x03 is removed only after exactly two
|
||
// zeros. (H.264/HEVC §7.4.)
|
||
assert_eq!(
|
||
strip_emulation_prevention(&[0x00, 0x00, 0x03, 0x42]),
|
||
vec![0x00, 0x00, 0x42]
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn strip_ep_leaves_03_after_single_zero() {
|
||
// A 0x03 preceded by only ONE zero is real data, not an EP byte.
|
||
assert_eq!(
|
||
strip_emulation_prevention(&[0x00, 0x03, 0x42]),
|
||
vec![0x00, 0x03, 0x42]
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn strip_ep_handles_consecutive_sequences() {
|
||
// 00 00 03 00 00 03 → 00 00 00 00. After dropping the first 0x03 the run
|
||
// resets to 0, so the next two zeros re-arm and drop the second 0x03.
|
||
assert_eq!(
|
||
strip_emulation_prevention(&[0x00, 0x00, 0x03, 0x00, 0x00, 0x03]),
|
||
vec![0x00, 0x00, 0x00, 0x00]
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn strip_ep_03_not_dropped_when_not_preceded_by_zeros() {
|
||
// 0x03 after non-zero bytes is kept verbatim.
|
||
assert_eq!(
|
||
strip_emulation_prevention(&[0xAA, 0xBB, 0x03, 0xCC]),
|
||
vec![0xAA, 0xBB, 0x03, 0xCC]
|
||
);
|
||
}
|
||
|
||
// --- parse_sps_chroma: chroma_format_idc edge values ---
|
||
|
||
#[test]
|
||
fn hvcc_chroma_monochrome_idc0() {
|
||
// chroma_format_idc = 0 (monochrome). bit depths 8-bit (minus8=0).
|
||
let sps = make_sps_with_chroma(0, 0, 0);
|
||
let cp = codec_private_from_sps(&sps);
|
||
// chromaFormat byte = 0xFC (6 reserved bits) | chroma_format_idc(0) = 0xFC.
|
||
assert_eq!(cp[16], 0xFC, "chroma_format_idc = 0 (monochrome)");
|
||
}
|
||
|
||
#[test]
|
||
fn hvcc_chroma_422_idc2() {
|
||
// chroma_format_idc = 2 (4:2:2), 10-bit.
|
||
let sps = make_sps_with_chroma(2, 2, 2);
|
||
let cp = codec_private_from_sps(&sps);
|
||
assert_eq!(cp[16], 0xFC | 2, "chroma_format_idc = 2 (4:2:2)");
|
||
assert_eq!(cp[17], 0xF8 | 2);
|
||
}
|
||
|
||
#[test]
|
||
fn hvcc_asymmetric_bit_depths() {
|
||
// luma and chroma bit depths can differ; both must be parsed
|
||
// independently. luma minus8 = 2 (10-bit), chroma minus8 = 4 (12-bit).
|
||
let sps = make_sps_with_chroma(1, 2, 4);
|
||
let cp = codec_private_from_sps(&sps);
|
||
assert_eq!(cp[17], 0xF8 | 2, "bit_depth_luma_minus8 = 2");
|
||
assert_eq!(cp[18], 0xF8 | 4, "bit_depth_chroma_minus8 = 4");
|
||
}
|
||
|
||
/// Build a stored SPS NAL with sub-layers and a conformance window, so the
|
||
/// parser must skip sub-layer PTL and the 4 conformance-window ue(v) fields
|
||
/// before reaching the bit depths. max_sub_layers_minus1 controls the
|
||
/// sub-layer loop.
|
||
fn make_sps_full(
|
||
chroma_idc: u32,
|
||
bd_luma_m8: u32,
|
||
bd_chroma_m8: u32,
|
||
max_sub_layers_minus1: u32,
|
||
conformance_window: bool,
|
||
) -> Vec<u8> {
|
||
let mut w = BitWriter::new();
|
||
w.put_bits(0, 4); // sps_video_parameter_set_id
|
||
w.put_bits(max_sub_layers_minus1, 3);
|
||
w.put_bit(1); // sps_temporal_id_nesting_flag
|
||
// general profile_tier_level: 96 bits.
|
||
for _ in 0..96 {
|
||
w.put_bit(0);
|
||
}
|
||
// Sub-layer flags + sub-layer PTL when max_sub_layers_minus1 > 0.
|
||
if max_sub_layers_minus1 > 0 {
|
||
let mut profile_present = Vec::new();
|
||
let mut level_present = Vec::new();
|
||
for _ in 0..max_sub_layers_minus1 {
|
||
// sub_layer_profile_present_flag, sub_layer_level_present_flag.
|
||
w.put_bit(1); // profile present
|
||
w.put_bit(1); // level present
|
||
profile_present.push(true);
|
||
level_present.push(true);
|
||
}
|
||
if max_sub_layers_minus1 < 8 {
|
||
for _ in max_sub_layers_minus1..8 {
|
||
w.put_bits(0, 2); // reserved_zero_2bits
|
||
}
|
||
}
|
||
for i in 0..max_sub_layers_minus1 as usize {
|
||
if profile_present[i] {
|
||
for _ in 0..88 {
|
||
w.put_bit(0); // sub-layer profile block
|
||
}
|
||
}
|
||
if level_present[i] {
|
||
w.put_bits(0, 8); // sub_layer_level_idc
|
||
}
|
||
}
|
||
}
|
||
w.put_ue(0); // sps_seq_parameter_set_id
|
||
w.put_ue(chroma_idc);
|
||
if chroma_idc == 3 {
|
||
w.put_bit(0); // separate_colour_plane_flag
|
||
}
|
||
w.put_ue(3840);
|
||
w.put_ue(2160);
|
||
if conformance_window {
|
||
w.put_bit(1); // conformance_window_flag
|
||
w.put_ue(0); // conf_win_left_offset
|
||
w.put_ue(0); // conf_win_right_offset
|
||
w.put_ue(0); // conf_win_top_offset
|
||
w.put_ue(0); // conf_win_bottom_offset
|
||
} else {
|
||
w.put_bit(0);
|
||
}
|
||
w.put_ue(bd_luma_m8);
|
||
w.put_ue(bd_chroma_m8);
|
||
|
||
let mut sps = hevc_nal_header(33).to_vec();
|
||
sps.extend_from_slice(&w.bytes);
|
||
sps
|
||
}
|
||
|
||
#[test]
|
||
fn hvcc_parses_chroma_through_sublayer_ptl() {
|
||
// With max_sub_layers_minus1 = 2 the parser must consume the sub-layer
|
||
// present-flag bits, reserved bits, and two sub-layer PTL blocks before
|
||
// reaching chroma_format_idc / bit depths. A wrong sub-layer skip would
|
||
// mis-read the bit depths.
|
||
let sps = make_sps_full(1, 2, 2, 2, false);
|
||
let cp = codec_private_from_sps(&sps);
|
||
assert_eq!(cp[16], 0xFC | 1, "4:2:0 after sub-layer PTL skip");
|
||
assert_eq!(cp[17], 0xF8 | 2, "10-bit luma after sub-layer PTL skip");
|
||
assert_eq!(cp[18], 0xF8 | 2);
|
||
// byte 21: numTemporalLayers = max_sub_layers_minus1 + 1 = 3.
|
||
assert_eq!(
|
||
cp[21],
|
||
(3 << 3) | (1 << 2) | 0x03,
|
||
"numTemporalLayers = 3, temporalIdNested = 1, lengthSizeMinusOne = 3"
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn hvcc_parses_chroma_through_conformance_window() {
|
||
// conformance_window_flag = 1 inserts 4 ue(v) fields the parser must skip
|
||
// before the bit depths. A correct skip lands on the right depths.
|
||
let sps = make_sps_full(1, 2, 2, 0, true);
|
||
let cp = codec_private_from_sps(&sps);
|
||
assert_eq!(
|
||
cp[17],
|
||
0xF8 | 2,
|
||
"10-bit luma after conformance-window skip"
|
||
);
|
||
assert_eq!(cp[18], 0xF8 | 2);
|
||
}
|
||
|
||
#[test]
|
||
fn hvcc_parses_444_with_separate_colour_plane() {
|
||
// chroma_format_idc = 3 (4:4:4) inserts separate_colour_plane_flag (1
|
||
// bit) that the parser must consume before pic dimensions. 12-bit.
|
||
let sps = make_sps_full(3, 4, 4, 0, false);
|
||
let cp = codec_private_from_sps(&sps);
|
||
assert_eq!(cp[16], 0xFC | 3, "4:4:4");
|
||
assert_eq!(cp[17], 0xF8 | 4, "12-bit luma");
|
||
}
|
||
|
||
// --- hvcC array structure (VPS/SPS/PPS arrays) ---
|
||
|
||
#[test]
|
||
fn hvcc_array_headers_and_lengths() {
|
||
// After the 23-byte fixed header + numOfArrays the record holds three
|
||
// arrays. Each: (0x20 | nal_type), numNalus(=1, u16-BE), nalLength(u16),
|
||
// NAL bytes. Verify the SPS array's nal_type byte and length encode
|
||
// correctly. (ISO/IEC 14496-15 §8.3.3.1.)
|
||
let mut parser = HevcParser::new();
|
||
let mut data = Vec::new();
|
||
data.extend_from_slice(&[0x00, 0x00, 0x01]);
|
||
data.extend_from_slice(&hevc_nal_header(32));
|
||
data.extend_from_slice(&[0xA0, 0xA1, 0xA2]); // VPS, 5 bytes total
|
||
data.extend_from_slice(&[0x00, 0x00, 0x01]);
|
||
data.extend_from_slice(&hevc_nal_header(33));
|
||
data.extend_from_slice(&[0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09]); // SPS, 11 bytes
|
||
data.extend_from_slice(&[0x00, 0x00, 0x01]);
|
||
data.extend_from_slice(&hevc_nal_header(34));
|
||
data.extend_from_slice(&[0xC0, 0xC1]); // PPS, 4 bytes
|
||
parser.parse(&make_pes(data, Some(0)));
|
||
let cp = parser.codec_private().expect("hvcC");
|
||
|
||
// numOfArrays at index 22.
|
||
assert_eq!(cp[22], 3);
|
||
// VPS array begins at 23. array header byte = 0x20 | 32 = 0x40.
|
||
let mut o = 23;
|
||
assert_eq!(cp[o], 0x20 | 32, "VPS array nal_type byte");
|
||
assert_eq!(
|
||
u16::from_be_bytes([cp[o + 1], cp[o + 2]]),
|
||
1,
|
||
"numNalus VPS"
|
||
);
|
||
let vps_len = u16::from_be_bytes([cp[o + 3], cp[o + 4]]) as usize;
|
||
assert_eq!(vps_len, 5, "VPS NAL length = 2 hdr + 3 payload");
|
||
// skip to SPS array.
|
||
o += 5 + vps_len;
|
||
assert_eq!(cp[o], 0x20 | 33, "SPS array nal_type byte");
|
||
let sps_len = u16::from_be_bytes([cp[o + 3], cp[o + 4]]) as usize;
|
||
assert_eq!(sps_len, 11, "SPS NAL length = 2 hdr + 9 payload");
|
||
o += 5 + sps_len;
|
||
assert_eq!(cp[o], 0x20 | 34, "PPS array nal_type byte");
|
||
let pps_len = u16::from_be_bytes([cp[o + 3], cp[o + 4]]) as usize;
|
||
assert_eq!(pps_len, 4, "PPS NAL length = 2 hdr + 2 payload");
|
||
}
|
||
|
||
#[test]
|
||
fn hvcc_none_missing_vps() {
|
||
// VPS is required for hvcC; SPS + PPS only → None.
|
||
let mut parser = HevcParser::new();
|
||
let mut data = Vec::new();
|
||
data.extend_from_slice(&[0x00, 0x00, 0x01]);
|
||
data.extend_from_slice(&hevc_nal_header(33));
|
||
data.extend_from_slice(&[0x01, 0x02, 0x03, 0x04]);
|
||
data.extend_from_slice(&[0x00, 0x00, 0x01]);
|
||
data.extend_from_slice(&hevc_nal_header(34));
|
||
data.extend_from_slice(&[0xDD, 0xEE]);
|
||
data.extend_from_slice(&[0x00, 0x00, 0x01]);
|
||
data.extend_from_slice(&hevc_nal_header(1)); // slice
|
||
data.extend_from_slice(&[0x10, 0x20]);
|
||
parser.parse(&make_pes(data, Some(0)));
|
||
assert!(parser.codec_private().is_none(), "no VPS → None");
|
||
}
|
||
|
||
// --- IRAP keyframe boundary values ---
|
||
|
||
#[test]
|
||
fn type_15_just_below_irap_not_keyframe() {
|
||
// Type 15 (RASL_R) is one below the IRAP range and must NOT be a keyframe.
|
||
let mut parser = HevcParser::new();
|
||
let mut data = vec![0x00, 0x00, 0x01];
|
||
data.extend_from_slice(&hevc_nal_header(15));
|
||
data.extend_from_slice(&[0x10, 0x20]);
|
||
let f = parser.parse(&make_pes(data, Some(0)));
|
||
assert_eq!(f.len(), 1);
|
||
assert!(!f[0].keyframe, "type 15 is below the IRAP range");
|
||
}
|
||
|
||
#[test]
|
||
fn type_24_just_above_irap_not_keyframe() {
|
||
// Type 24 (RSV_VCL24) is one above the IRAP range (..=23) → not keyframe.
|
||
let mut parser = HevcParser::new();
|
||
let mut data = vec![0x00, 0x00, 0x01];
|
||
data.extend_from_slice(&hevc_nal_header(24));
|
||
data.extend_from_slice(&[0x10, 0x20]);
|
||
let f = parser.parse(&make_pes(data, Some(0)));
|
||
assert_eq!(f.len(), 1);
|
||
assert!(!f[0].keyframe, "type 24 is above the IRAP range");
|
||
}
|
||
|
||
#[test]
|
||
fn hevc_nal_type_extraction_masks_correctly() {
|
||
// HEVC NAL type = (byte0 >> 1) & 0x3F. The forbidden_zero_bit (bit 7) and
|
||
// the low layer-id bit (bit 0) must not affect type. hevc_nal_header(19)
|
||
// = [(19<<1), 0x01] = [0x26, 0x01]; with the forbidden bit set (0xA6) it
|
||
// is still type 19.
|
||
let mut parser = HevcParser::new();
|
||
let data = vec![0x00, 0x00, 0x01, 0xA6, 0x01, 0x10, 0x20]; // 0xA6>>1&0x3F = 19
|
||
let f = parser.parse(&make_pes(data, Some(0)));
|
||
assert_eq!(f.len(), 1);
|
||
assert!(
|
||
f[0].keyframe,
|
||
"0xA6 decodes to NAL type 19 (IDR) → keyframe"
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn hevc_dts_fallback_when_pts_absent() {
|
||
let mut parser = HevcParser::new();
|
||
let pes = PesPacket {
|
||
source: None,
|
||
pid: 0x1011,
|
||
pts: None,
|
||
dts: Some(90000),
|
||
data: {
|
||
let mut d = vec![0x00, 0x00, 0x01];
|
||
d.extend_from_slice(&hevc_nal_header(1));
|
||
d.extend_from_slice(&[0x10, 0x20]);
|
||
d
|
||
},
|
||
};
|
||
let f = parser.parse(&pes);
|
||
assert_eq!(f.len(), 1);
|
||
assert_eq!(f[0].pts_ns, 1_000_000_000, "falls back to DTS");
|
||
}
|
||
|
||
#[test]
|
||
fn parse_sps_chroma_too_short_returns_none() {
|
||
// An SPS shorter than 3 bytes can't carry the 2-byte NAL header + RBSP →
|
||
// parse_sps_chroma returns None (caller falls back to 8-bit 4:2:0).
|
||
assert!(parse_sps_chroma(&[0x42]).is_none());
|
||
assert!(parse_sps_chroma(&[0x42, 0x01]).is_none());
|
||
}
|
||
|
||
#[test]
|
||
fn hvcc_falls_back_to_8bit_420_on_unparseable_sps() {
|
||
// An SPS whose RBSP is truncated mid-parse (can't reach the bit depths)
|
||
// must fall back to the 8-bit 4:2:0 default, not panic. A 3-byte stored
|
||
// SPS (header + 1 RBSP byte) can't complete the PTL skip.
|
||
let mut parser = HevcParser::new();
|
||
let mut data = Vec::new();
|
||
data.extend_from_slice(&[0x00, 0x00, 0x01]);
|
||
data.extend_from_slice(&hevc_nal_header(32));
|
||
data.extend_from_slice(&[0xAA, 0xBB]);
|
||
data.extend_from_slice(&[0x00, 0x00, 0x01]);
|
||
data.extend_from_slice(&hevc_nal_header(33));
|
||
data.extend_from_slice(&[0x00]); // 1 RBSP byte — unparseable
|
||
data.extend_from_slice(&[0x00, 0x00, 0x01]);
|
||
data.extend_from_slice(&hevc_nal_header(34));
|
||
data.extend_from_slice(&[0xDD]);
|
||
parser.parse(&make_pes(data, Some(0)));
|
||
let cp = parser.codec_private().expect("hvcC");
|
||
assert_eq!(cp[16], 0xFC | 1, "fallback chroma_format_idc = 1 (4:2:0)");
|
||
assert_eq!(cp[17], 0xF8, "fallback 8-bit luma");
|
||
assert_eq!(cp[18], 0xF8, "fallback 8-bit chroma");
|
||
}
|
||
|
||
#[test]
|
||
fn hvcc_oversized_param_set_returns_none() {
|
||
// A param set larger than 65535 bytes cannot be length-encoded in hvcC's
|
||
// 16-bit field; codec_private must refuse rather than emit a truncated,
|
||
// mis-framed record.
|
||
let mut parser = HevcParser::new();
|
||
let mut data = Vec::new();
|
||
// VPS
|
||
data.extend_from_slice(&[0x00, 0x00, 0x01]);
|
||
data.extend_from_slice(&hevc_nal_header(32));
|
||
data.extend_from_slice(&[0xAA, 0xBB]);
|
||
// Oversized SPS: header + 70000 bytes of payload (avoid 00 00 0x runs by
|
||
// using 0x11 filler so it stays one NAL).
|
||
data.extend_from_slice(&[0x00, 0x00, 0x01]);
|
||
data.extend_from_slice(&hevc_nal_header(33));
|
||
data.extend_from_slice(&vec![0x11u8; 70_000]);
|
||
// PPS
|
||
data.extend_from_slice(&[0x00, 0x00, 0x01]);
|
||
data.extend_from_slice(&hevc_nal_header(34));
|
||
data.extend_from_slice(&[0xDD, 0xEE]);
|
||
parser.parse(&make_pes(data, Some(0)));
|
||
assert!(
|
||
parser.codec_private().is_none(),
|
||
"oversized param set must not produce a (truncated) hvcC"
|
||
);
|
||
}
|
||
}
|