use crate::codec::h264::bitstream::parse_nal_units_annexb;
use crate::codec::h264::cabac::context::CabacInitSlot;
use crate::codec::h264::cabac::neighbor::{
block_pos_to_chroma_ac_idx,
compute_cbf_ctx_idx_inc_chroma_ac, compute_cbf_ctx_idx_inc_chroma_dc,
compute_cbf_ctx_idx_inc_luma_4x4, compute_cbf_ctx_idx_inc_luma_ac,
compute_cbf_ctx_idx_inc_luma_dc, compute_mvd_ctx_idx_inc_bin0,
CabacNeighborMB, CurrentMbCbf, CurrentMbMvdAbs, MbTypeClass,
};
use crate::codec::h264::stego::{
Axis, DomainCover, MvdSlot, NullLogger, ResidualPathKind,
};
use crate::codec::h264::macroblock::BLOCK_INDEX_TO_POS;
use crate::codec::h264::slice::{parse_slice_header, SliceHeader, SliceType};
use crate::codec::h264::sps::{parse_pps, parse_sps, Pps, Sps};
use crate::codec::h264::{H264Error, NalType, NalUnit};
use super::syntax::{
decode_coded_block_pattern, decode_end_of_slice_flag,
decode_intra_chroma_pred_mode, decode_mb_qp_delta, decode_mb_skip_flag,
decode_mb_skip_flag_b,
decode_mb_type_b, decode_mb_type_i, decode_mb_type_p,
decode_mvd_with_bin0_inc,
decode_prev_intra4x4_pred_mode_flag, decode_rem_intra4x4_pred_mode,
decode_residual_block_cabac, decode_sub_mb_type_p,
decode_transform_size_8x8_flag, PositionCtx,
};
use super::decoder::CabacDecoder;
use super::engine::DecodeError;
use super::positions::PositionRecorder;
#[derive(Debug)]
pub enum WalkError {
H264(H264Error),
Cabac(DecodeError),
MissingParameterSet,
NotCabac,
UnsupportedSliceType(SliceType),
NoSlices,
}
impl From<H264Error> for WalkError {
fn from(e: H264Error) -> Self { Self::H264(e) }
}
impl From<DecodeError> for WalkError {
fn from(e: DecodeError) -> Self { Self::Cabac(e) }
}
impl std::fmt::Display for WalkError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::H264(e) => write!(f, "h264 parse: {e}"),
Self::Cabac(e) => write!(f, "cabac decode: {e:?}"),
Self::MissingParameterSet => write!(f, "missing SPS or PPS"),
Self::NotCabac => write!(f, "PPS is CAVLC; bin decoder is CABAC-only"),
Self::UnsupportedSliceType(t) => write!(f, "unsupported slice type: {t}"),
Self::NoSlices => write!(f, "no slice NALs in bitstream"),
}
}
}
impl std::error::Error for WalkError {}
#[derive(Debug, Default)]
pub struct CoverWalkOutput {
pub cover: DomainCover,
pub n_mb: usize,
pub n_slices: usize,
pub mvd_meta: Vec<crate::codec::h264::stego::encoder_hook::MvdPositionMeta>,
pub mb_w: u32,
pub mb_h: u32,
}
#[derive(Debug, Clone, Copy, Default)]
pub struct WalkOptions {
pub record_mvd: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WalkAction {
Continue,
StopWalk,
}
#[derive(Debug)]
pub struct GopContext {
pub gop_idx: u32,
pub cover: DomainCover,
pub n_mb: usize,
pub n_slices: usize,
pub mvd_meta: Vec<crate::codec::h264::stego::encoder_hook::MvdPositionMeta>,
pub mb_w: u32,
pub mb_h: u32,
}
#[derive(Debug, Default)]
pub struct StreamingWalkOutput {
pub n_gops: usize,
pub n_mb: usize,
pub n_slices: usize,
}
pub fn walk_annex_b_for_cover(annex_b: &[u8]) -> Result<CoverWalkOutput, WalkError> {
walk_annex_b_for_cover_with_options(annex_b, WalkOptions::default())
}
pub fn walk_annex_b_for_cover_with_options(
annex_b: &[u8],
opts: WalkOptions,
) -> Result<CoverWalkOutput, WalkError> {
let nalus = parse_nal_units_annexb(annex_b)?;
walk_nalus_for_cover_with_options(&nalus, opts)
}
pub fn walk_nalus_for_cover(nalus: &[NalUnit]) -> Result<CoverWalkOutput, WalkError> {
walk_nalus_for_cover_with_options(nalus, WalkOptions::default())
}
pub fn walk_nalus_for_cover_with_options(
nalus: &[NalUnit],
opts: WalkOptions,
) -> Result<CoverWalkOutput, WalkError> {
let mut acc_cover = DomainCover::default();
let mut acc_mvd_meta: Vec<crate::codec::h264::stego::encoder_hook::MvdPositionMeta>
= Vec::new();
let mut acc_mb_w = 0u32;
let mut acc_mb_h = 0u32;
let streaming_out = walk_nalus_streaming_with_options(
nalus,
opts,
|gop_ctx| {
acc_cover.extend_from(gop_ctx.cover);
acc_mvd_meta.extend(gop_ctx.mvd_meta);
if gop_ctx.mb_w != 0 { acc_mb_w = gop_ctx.mb_w; }
if gop_ctx.mb_h != 0 { acc_mb_h = gop_ctx.mb_h; }
Ok(WalkAction::Continue)
},
)?;
Ok(CoverWalkOutput {
cover: acc_cover,
n_mb: streaming_out.n_mb,
n_slices: streaming_out.n_slices,
mvd_meta: acc_mvd_meta,
mb_w: acc_mb_w,
mb_h: acc_mb_h,
})
}
pub fn walk_annex_b_streaming<F>(
annex_b: &[u8],
opts: WalkOptions,
on_gop: F,
) -> Result<StreamingWalkOutput, WalkError>
where
F: FnMut(GopContext) -> Result<WalkAction, WalkError>,
{
let nalus = parse_nal_units_annexb(annex_b)?;
walk_nalus_streaming_with_options(&nalus, opts, on_gop)
}
pub fn walk_nalus_streaming_with_options<F>(
nalus: &[NalUnit],
opts: WalkOptions,
mut on_gop: F,
) -> Result<StreamingWalkOutput, WalkError>
where
F: FnMut(GopContext) -> Result<WalkAction, WalkError>,
{
let mut active_sps: Option<Sps> = None;
let mut active_pps: Option<Pps> = None;
let mut recorder = PositionRecorder::new();
let mut frame_idx: u32 = 0;
let mut current_gop_idx: u32 = 0;
let mut gop_n_mb: usize = 0;
let mut gop_n_slices: usize = 0;
let mut had_vcl_in_current_gop = false;
let mut total_n_mb: usize = 0;
let mut total_n_slices: usize = 0;
let mut total_n_gops: usize = 0;
for nal in nalus {
match nal.nal_type {
NalType::SPS => {
active_sps = Some(parse_sps(&nal.rbsp)?);
}
NalType::PPS => {
active_pps = Some(parse_pps(&nal.rbsp)?);
}
t if t.is_vcl() => {
let sps = active_sps.as_ref().ok_or(WalkError::MissingParameterSet)?;
let pps = active_pps.as_ref().ok_or(WalkError::MissingParameterSet)?;
if !pps.entropy_coding_mode_flag {
return Err(WalkError::NotCabac);
}
let header = parse_slice_header(&nal.rbsp, sps, pps, t, nal.nal_ref_idc)?;
if t.is_idr() && had_vcl_in_current_gop {
let cover = recorder.take_cover();
let mvd_meta = recorder.take_mvd_meta();
let (closing_mb_w, closing_mb_h) = active_sps
.as_ref()
.map(|s| (
s.pic_width_in_mbs,
s.pic_height_in_map_units
* if s.frame_mbs_only_flag { 1 } else { 2 },
))
.unwrap_or((0, 0));
let action = on_gop(GopContext {
gop_idx: current_gop_idx,
cover,
n_mb: gop_n_mb,
n_slices: gop_n_slices,
mvd_meta,
mb_w: closing_mb_w,
mb_h: closing_mb_h,
})?;
total_n_gops += 1;
if matches!(action, WalkAction::StopWalk) {
return Ok(StreamingWalkOutput {
n_gops: total_n_gops,
n_mb: total_n_mb,
n_slices: total_n_slices,
});
}
current_gop_idx = current_gop_idx.wrapping_add(1);
gop_n_mb = 0;
gop_n_slices = 0;
had_vcl_in_current_gop = false;
}
let mb_w = sps.pic_width_in_mbs as usize;
let mb_h = sps.pic_height_in_map_units as usize
* if sps.frame_mbs_only_flag { 1 } else { 2 };
let mb_count = mb_w * mb_h;
let cabac_byte_off = cabac_data_byte_offset(header.data_bit_offset);
if cabac_byte_off > nal.rbsp.len() {
return Err(WalkError::H264(H264Error::UnexpectedEof));
}
let cabac_bytes = &nal.rbsp[cabac_byte_off..];
let slot = pick_init_slot(&header);
let mut dec = CabacDecoder::new_slice(
cabac_bytes, slot, header.slice_qp, mb_w,
)?;
let n_mb = walk_slice_mbs(
&mut dec, &header, pps, mb_count, frame_idx, mb_w,
&mut recorder, &opts,
)?;
gop_n_mb += n_mb;
gop_n_slices += 1;
total_n_mb += n_mb;
total_n_slices += 1;
had_vcl_in_current_gop = true;
frame_idx = frame_idx.wrapping_add(1);
}
_ => { }
}
}
if had_vcl_in_current_gop {
let cover = recorder.take_cover();
let mvd_meta = recorder.take_mvd_meta();
let (closing_mb_w, closing_mb_h) = active_sps
.as_ref()
.map(|s| (
s.pic_width_in_mbs,
s.pic_height_in_map_units
* if s.frame_mbs_only_flag { 1 } else { 2 },
))
.unwrap_or((0, 0));
on_gop(GopContext {
gop_idx: current_gop_idx,
cover,
n_mb: gop_n_mb,
n_slices: gop_n_slices,
mvd_meta,
mb_w: closing_mb_w,
mb_h: closing_mb_h,
})?;
total_n_gops += 1;
}
if total_n_slices == 0 {
return Err(WalkError::NoSlices);
}
Ok(StreamingWalkOutput {
n_gops: total_n_gops,
n_mb: total_n_mb,
n_slices: total_n_slices,
})
}
fn pick_init_slot(header: &SliceHeader) -> CabacInitSlot {
match header.slice_type {
SliceType::I | SliceType::SI => CabacInitSlot::ISI,
SliceType::P | SliceType::SP | SliceType::B => match header.cabac_init_idc {
0 => CabacInitSlot::PIdc0,
1 => CabacInitSlot::PIdc1,
2 => CabacInitSlot::PIdc2,
_ => CabacInitSlot::PIdc0, },
}
}
fn cabac_data_byte_offset(data_bit_offset: usize) -> usize {
data_bit_offset.div_ceil(8)
}
fn walk_slice_mbs(
dec: &mut CabacDecoder<'_>,
header: &SliceHeader,
pps: &Pps,
mb_count: usize,
frame_idx: u32,
mb_w: usize,
recorder: &mut PositionRecorder,
opts: &WalkOptions,
) -> Result<usize, WalkError> {
let is_intra_slice = matches!(header.slice_type, SliceType::I | SliceType::SI);
let is_p_slice = matches!(header.slice_type, SliceType::P | SliceType::SP);
let is_b_slice = matches!(header.slice_type, SliceType::B);
if !is_intra_slice && !is_p_slice && !is_b_slice {
return Err(WalkError::UnsupportedSliceType(header.slice_type));
}
let mut mbs_walked = 0usize;
let mut prev_mb_qp = header.slice_qp;
let mut mb_addr = header.first_mb_in_slice as usize;
let mut prev_mb_y: Option<usize> = None;
while mb_addr < mb_count {
let mb_x = mb_addr % mb_w;
let mb_y = mb_addr / mb_w;
if let Some(prev_y) = prev_mb_y
&& mb_y != prev_y {
dec.neighbors.new_row();
}
prev_mb_y = Some(mb_y);
let is_last = if is_b_slice {
walk_b_mb(
dec, pps, mb_x, mb_y, mb_w, &mut prev_mb_qp,
frame_idx, recorder, opts,
)?
} else if is_p_slice {
walk_p_mb(
dec, pps, mb_x, mb_y, mb_w, &mut prev_mb_qp,
frame_idx, recorder, opts,
)?
} else {
walk_i_mb(
dec, pps, mb_x, mb_y, mb_w, &mut prev_mb_qp,
frame_idx, recorder,
)?
};
mbs_walked += 1;
mb_addr += 1;
if is_last {
break;
}
}
Ok(mbs_walked)
}
fn walk_i_mb(
dec: &mut CabacDecoder<'_>,
pps: &Pps,
mb_x: usize,
mb_y: usize,
mb_w: usize,
prev_mb_qp: &mut i32,
frame_idx: u32,
recorder: &mut PositionRecorder,
) -> Result<bool, WalkError> {
let mb_type = decode_mb_type_i(dec, mb_x)?;
if mb_type == 25 {
return Err(WalkError::H264(H264Error::Unsupported(
"I_PCM (mb_type=25) not yet wired".into(),
)));
}
if mb_type == 0 {
return walk_inxn_mb(
dec, pps, mb_x, mb_y, mb_w, prev_mb_qp, frame_idx, recorder,
);
}
walk_i16x16_mb(
dec, mb_x, mb_y, mb_w, prev_mb_qp, frame_idx, recorder,
mb_type,
)
}
fn walk_p_mb(
dec: &mut CabacDecoder<'_>,
pps: &Pps,
mb_x: usize,
mb_y: usize,
mb_w: usize,
prev_mb_qp: &mut i32,
frame_idx: u32,
recorder: &mut PositionRecorder,
opts: &WalkOptions,
) -> Result<bool, WalkError> {
let is_skip = decode_mb_skip_flag(dec, mb_x)?;
if is_skip {
let is_last = decode_end_of_slice_flag(dec)?;
let nb = CabacNeighborMB {
mb_type: MbTypeClass::PSkip,
mb_skip_flag: true,
..CabacNeighborMB::default()
};
dec.neighbors.commit(mb_x, nb);
return Ok(is_last);
}
let mb_type_p = decode_mb_type_p(dec, mb_x)?;
if mb_type_p <= 3 {
return walk_p_partition_mb(
dec, pps, mb_x, mb_y, mb_w, prev_mb_qp, frame_idx, recorder,
mb_type_p, opts,
);
}
let i_mb_type = mb_type_p - 5;
if i_mb_type == 25 {
return Err(WalkError::H264(H264Error::Unsupported(
"I_PCM-in-P (mb_type_p=30) not yet wired".into(),
)));
}
if i_mb_type == 0 {
return walk_inxn_mb(
dec, pps, mb_x, mb_y, mb_w, prev_mb_qp, frame_idx, recorder,
);
}
walk_i16x16_mb(
dec, mb_x, mb_y, mb_w, prev_mb_qp, frame_idx, recorder,
i_mb_type,
)
}
fn walk_i16x16_mb(
dec: &mut CabacDecoder<'_>,
mb_x: usize,
mb_y: usize,
mb_w: usize,
prev_mb_qp: &mut i32,
frame_idx: u32,
recorder: &mut PositionRecorder,
i_mb_type: u32,
) -> Result<bool, WalkError> {
let v = i_mb_type - 1;
let _luma_pred_mode = v % 4;
let cbp_chroma = (v / 4) % 3;
let cbp_luma_flag = v / 12;
let chroma_pred_mode = decode_intra_chroma_pred_mode(dec, mb_x)?;
let qp_delta = decode_mb_qp_delta(dec)?;
*prev_mb_qp += qp_delta;
let mb_addr_u32 = (mb_y * mb_w + mb_x) as u32;
let mut current_cbf = CurrentMbCbf::new();
let current_is_intra = true;
let dc_inc = compute_cbf_ctx_idx_inc_luma_dc(&dec.neighbors, mb_x);
let dc_scan = decode_residual_block_with_null_logger(
dec, 0, 15, 0, dc_inc,
frame_idx, mb_addr_u32, ResidualPathKind::LumaDcIntra16x16,
)?;
let dc_coded = dc_scan.iter().any(|&v| v != 0);
current_cbf.set(0, 0, dc_coded);
recorder.on_residual_block(
frame_idx, mb_addr_u32, &dc_scan, 0, 15,
ResidualPathKind::LumaDcIntra16x16,
);
if cbp_luma_flag != 0 {
for k in 0..16usize {
let (bx, by) = BLOCK_INDEX_TO_POS[k];
let ac_inc = compute_cbf_ctx_idx_inc_luma_ac(
¤t_cbf, &dec.neighbors,
mb_x, bx, by, current_is_intra,
);
let path = ResidualPathKind::Luma4x4 { block_idx: k as u8 };
let ac_scan = decode_residual_block_with_null_logger(
dec, 0, 14, 1, ac_inc,
frame_idx, mb_addr_u32, path,
)?;
let coded = ac_scan.iter().any(|&v| v != 0);
current_cbf.set(1, k, coded);
recorder.on_residual_block(
frame_idx, mb_addr_u32, &ac_scan, 0, 14, path,
);
}
}
if cbp_chroma >= 1 {
for plane in 0u8..2 {
let inc = compute_cbf_ctx_idx_inc_chroma_dc(
&dec.neighbors, mb_x, plane, current_is_intra,
);
let path = ResidualPathKind::ChromaDc { plane };
let dc_flat = decode_residual_block_with_null_logger(
dec, 0, 3, 3, inc,
frame_idx, mb_addr_u32, path,
)?;
let coded = dc_flat.iter().any(|&v| v != 0);
current_cbf.set(3, plane as usize, coded);
recorder.on_residual_block(
frame_idx, mb_addr_u32, &dc_flat, 0, 3, path,
);
}
}
if cbp_chroma == 2 {
for plane in 0u8..2 {
for sub in 0..4usize {
let bx = (sub % 2) as u8;
let by = (sub / 2) as u8;
let inc = compute_cbf_ctx_idx_inc_chroma_ac(
¤t_cbf, &dec.neighbors,
mb_x, plane, bx, by, current_is_intra,
);
let path = ResidualPathKind::ChromaAc {
plane, block_idx: sub as u8,
};
let ac_scan = decode_residual_block_with_null_logger(
dec, 0, 14, 4, inc,
frame_idx, mb_addr_u32, path,
)?;
let coded = ac_scan.iter().any(|&v| v != 0);
current_cbf.set(
4, block_pos_to_chroma_ac_idx(plane, bx, by), coded,
);
recorder.on_residual_block(
frame_idx, mb_addr_u32, &ac_scan, 0, 14, path,
);
}
}
}
let is_last = decode_end_of_slice_flag(dec)?;
let mut nb = CabacNeighborMB {
mb_type: MbTypeClass::I16x16,
..CabacNeighborMB::default()
};
nb.intra_chroma_pred_mode = chroma_pred_mode as u8;
nb.cbp_luma = if cbp_luma_flag != 0 { 0x0F } else { 0 };
nb.cbp_chroma = cbp_chroma as u8;
nb.mb_qp_delta = qp_delta;
nb.coded_block_flag_cat = current_cbf.to_neighbor_cbf();
dec.neighbors.commit(mb_x, nb);
Ok(is_last)
}
fn walk_b_mb(
dec: &mut CabacDecoder<'_>,
_pps: &Pps,
mb_x: usize,
_mb_y: usize,
_mb_w: usize,
prev_mb_qp: &mut i32,
_frame_idx: u32,
_recorder: &mut PositionRecorder,
_opts: &WalkOptions,
) -> Result<bool, WalkError> {
let is_skip = decode_mb_skip_flag_b(dec, mb_x)?;
if is_skip {
let is_last = decode_end_of_slice_flag(dec)?;
let nb = CabacNeighborMB {
mb_type: MbTypeClass::BSkipOrDirect,
mb_skip_flag: true,
..CabacNeighborMB::default()
};
dec.neighbors.commit(mb_x, nb);
return Ok(is_last);
}
let mb_type = decode_mb_type_b(dec, mb_x)?;
if mb_type != 0 {
return Err(WalkError::H264(H264Error::Unsupported(
format!("B-slice non-direct mb_type {mb_type} (Phase 6E-A4-full)"),
)));
}
let cbp_byte = decode_coded_block_pattern(dec, mb_x)?;
let cbp_luma = cbp_byte & 0x0F;
let cbp_chroma = (cbp_byte >> 4) & 0x03;
if cbp_luma != 0 || cbp_chroma != 0 {
return Err(WalkError::H264(H264Error::Unsupported(
format!(
"B_Direct_16x16 with non-zero CBP {cbp_byte:#x} \
(Phase 6E-A4-full residual decoding)"
),
)));
}
let _ = prev_mb_qp;
let is_last = decode_end_of_slice_flag(dec)?;
let nb = CabacNeighborMB {
mb_type: MbTypeClass::BSkipOrDirect,
mb_skip_flag: false,
cbp_luma: 0,
cbp_chroma: 0,
..CabacNeighborMB::default()
};
dec.neighbors.commit(mb_x, nb);
Ok(is_last)
}
fn walk_inxn_mb(
dec: &mut CabacDecoder<'_>,
pps: &Pps,
mb_x: usize,
mb_y: usize,
mb_w: usize,
prev_mb_qp: &mut i32,
frame_idx: u32,
recorder: &mut PositionRecorder,
) -> Result<bool, WalkError> {
let mb_addr_u32 = (mb_y * mb_w + mb_x) as u32;
let current_is_intra = true;
if pps.transform_8x8_mode_flag {
let use_8x8 = decode_transform_size_8x8_flag(dec, mb_x)?;
if use_8x8 {
return Err(WalkError::H264(H264Error::Unsupported(
"I_8x8 (transform_size_8x8_flag=1) not yet wired".into(),
)));
}
}
for _ in 0..16 {
let prev_flag = decode_prev_intra4x4_pred_mode_flag(dec)?;
if !prev_flag {
let _rem = decode_rem_intra4x4_pred_mode(dec)?;
}
}
let chroma_pred_mode = decode_intra_chroma_pred_mode(dec, mb_x)?;
let cbp_byte = decode_coded_block_pattern(dec, mb_x)?;
let cbp_luma = cbp_byte & 0x0F;
let cbp_chroma = (cbp_byte >> 4) & 0x03;
let qp_delta_emitted = if cbp_byte != 0 {
let delta = decode_mb_qp_delta(dec)?;
*prev_mb_qp += delta;
delta
} else {
0
};
let _ = qp_delta_emitted;
let mut current_cbf = CurrentMbCbf::new();
if cbp_luma != 0 {
for k in 0..16usize {
let (bx, by) = BLOCK_INDEX_TO_POS[k];
if cbp_luma & (1 << (k / 4)) != 0 {
let inc = compute_cbf_ctx_idx_inc_luma_4x4(
¤t_cbf, &dec.neighbors,
mb_x, bx, by, current_is_intra,
);
let path = ResidualPathKind::Luma4x4 { block_idx: k as u8 };
let scan = decode_residual_block_with_null_logger(
dec, 0, 15, 2, inc,
frame_idx, mb_addr_u32, path,
)?;
let coded = scan.iter().any(|&v| v != 0);
current_cbf.set(2, k, coded);
recorder.on_residual_block(
frame_idx, mb_addr_u32, &scan, 0, 15, path,
);
}
}
}
if cbp_chroma >= 1 {
for plane in 0u8..2 {
let inc = compute_cbf_ctx_idx_inc_chroma_dc(
&dec.neighbors, mb_x, plane, current_is_intra,
);
let path = ResidualPathKind::ChromaDc { plane };
let dc_flat = decode_residual_block_with_null_logger(
dec, 0, 3, 3, inc,
frame_idx, mb_addr_u32, path,
)?;
let coded = dc_flat.iter().any(|&v| v != 0);
current_cbf.set(3, plane as usize, coded);
recorder.on_residual_block(
frame_idx, mb_addr_u32, &dc_flat, 0, 3, path,
);
}
}
if cbp_chroma == 2 {
for plane in 0u8..2 {
for sub in 0..4usize {
let bx = (sub % 2) as u8;
let by = (sub / 2) as u8;
let inc = compute_cbf_ctx_idx_inc_chroma_ac(
¤t_cbf, &dec.neighbors,
mb_x, plane, bx, by, current_is_intra,
);
let path = ResidualPathKind::ChromaAc {
plane, block_idx: sub as u8,
};
let ac_scan = decode_residual_block_with_null_logger(
dec, 0, 14, 4, inc,
frame_idx, mb_addr_u32, path,
)?;
let coded = ac_scan.iter().any(|&v| v != 0);
current_cbf.set(
4, block_pos_to_chroma_ac_idx(plane, bx, by), coded,
);
recorder.on_residual_block(
frame_idx, mb_addr_u32, &ac_scan, 0, 14, path,
);
}
}
}
let is_last = decode_end_of_slice_flag(dec)?;
let mut nb = CabacNeighborMB {
mb_type: MbTypeClass::INxN,
..CabacNeighborMB::default()
};
nb.intra_chroma_pred_mode = chroma_pred_mode as u8;
nb.cbp_luma = cbp_luma;
nb.cbp_chroma = cbp_chroma;
nb.mb_qp_delta = qp_delta_emitted;
nb.coded_block_flag_cat = current_cbf.to_neighbor_cbf();
dec.neighbors.commit(mb_x, nb);
Ok(is_last)
}
#[allow(clippy::too_many_arguments)]
fn walk_p_partition_mb(
dec: &mut CabacDecoder<'_>,
pps: &Pps,
mb_x: usize,
mb_y: usize,
mb_w: usize,
prev_mb_qp: &mut i32,
frame_idx: u32,
recorder: &mut PositionRecorder,
mb_type_p: u32,
opts: &WalkOptions,
) -> Result<bool, WalkError> {
let mb_addr_u32 = (mb_y * mb_w + mb_x) as u32;
let mut current_mvd = CurrentMbMvdAbs::new();
let sub_types: Option<[u32; 4]> = if mb_type_p == 3 {
let mut sm = [0u32; 4];
for s in sm.iter_mut() {
*s = decode_sub_mb_type_p(dec)?;
}
Some(sm)
} else {
None
};
decode_p_partition_mvds(
dec, &mut current_mvd, mb_x, mb_addr_u32, frame_idx,
mb_type_p, sub_types.as_ref(), recorder, opts,
)?;
decode_p_residuals_and_finish(
dec, pps, mb_x, mb_y, mb_w, prev_mb_qp, frame_idx,
mb_addr_u32, recorder, ¤t_mvd,
)
}
#[allow(clippy::too_many_arguments)]
fn decode_p_partition_mvds(
dec: &mut CabacDecoder<'_>,
current_mvd: &mut CurrentMbMvdAbs,
mb_x: usize,
mb_addr_u32: u32,
frame_idx: u32,
mb_type_p: u32,
sub_types: Option<&[u32; 4]>,
recorder: &mut PositionRecorder,
opts: &WalkOptions,
) -> Result<(), WalkError> {
match mb_type_p {
0 => {
decode_one_mvd_pair_p(
dec, current_mvd, mb_x, 0, 0, 4, 4,
0,
frame_idx, mb_addr_u32, recorder, opts,
)?;
}
1 => {
decode_one_mvd_pair_p(
dec, current_mvd, mb_x, 0, 0, 4, 2, 0,
frame_idx, mb_addr_u32, recorder, opts,
)?;
decode_one_mvd_pair_p(
dec, current_mvd, mb_x, 0, 2, 4, 2, 1,
frame_idx, mb_addr_u32, recorder, opts,
)?;
}
2 => {
decode_one_mvd_pair_p(
dec, current_mvd, mb_x, 0, 0, 2, 4, 0,
frame_idx, mb_addr_u32, recorder, opts,
)?;
decode_one_mvd_pair_p(
dec, current_mvd, mb_x, 2, 0, 2, 4, 1,
frame_idx, mb_addr_u32, recorder, opts,
)?;
}
3 => {
let sm = sub_types.expect("P_8x8 requires sub_types");
const SUB_ORIGINS: [(u8, u8); 4] = [(0, 0), (2, 0), (0, 2), (2, 2)];
for (i, &sub_t) in sm.iter().enumerate() {
let (sub_bx, sub_by) = SUB_ORIGINS[i];
decode_sub_mb_mvds(
dec, current_mvd, mb_x, sub_bx, sub_by, sub_t,
i as u8,
frame_idx, mb_addr_u32, recorder, opts,
)?;
}
}
_ => unreachable!("mb_type_p > 3 routed elsewhere"),
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn decode_sub_mb_mvds(
dec: &mut CabacDecoder<'_>,
current_mvd: &mut CurrentMbMvdAbs,
mb_x: usize,
sub_bx: u8,
sub_by: u8,
sub_mb_type: u32,
sub_mb_idx: u8,
frame_idx: u32,
mb_addr_u32: u32,
recorder: &mut PositionRecorder,
opts: &WalkOptions,
) -> Result<(), WalkError> {
let p = |sub_part_idx: u8| sub_mb_idx * 4 + sub_part_idx;
match sub_mb_type {
0 => {
decode_one_mvd_pair_p(
dec, current_mvd, mb_x, sub_bx, sub_by, 2, 2, p(0),
frame_idx, mb_addr_u32, recorder, opts,
)?;
}
1 => {
decode_one_mvd_pair_p(
dec, current_mvd, mb_x, sub_bx, sub_by, 2, 1, p(0),
frame_idx, mb_addr_u32, recorder, opts,
)?;
decode_one_mvd_pair_p(
dec, current_mvd, mb_x, sub_bx, sub_by + 1, 2, 1, p(1),
frame_idx, mb_addr_u32, recorder, opts,
)?;
}
2 => {
decode_one_mvd_pair_p(
dec, current_mvd, mb_x, sub_bx, sub_by, 1, 2, p(0),
frame_idx, mb_addr_u32, recorder, opts,
)?;
decode_one_mvd_pair_p(
dec, current_mvd, mb_x, sub_bx + 1, sub_by, 1, 2, p(1),
frame_idx, mb_addr_u32, recorder, opts,
)?;
}
3 => {
for oy in 0u8..2 {
for ox in 0u8..2 {
let sp = oy * 2 + ox;
decode_one_mvd_pair_p(
dec, current_mvd, mb_x,
sub_bx + ox, sub_by + oy, 1, 1, p(sp),
frame_idx, mb_addr_u32, recorder, opts,
)?;
}
}
}
_ => unreachable!("sub_mb_type > 3 cannot be decoded"),
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn decode_one_mvd_pair_p(
dec: &mut CabacDecoder<'_>,
current_mvd: &mut CurrentMbMvdAbs,
mb_x: usize,
part_bx: u8,
part_by: u8,
part_w4: u8,
part_h4: u8,
partition: u8,
frame_idx: u32,
mb_addr_u32: u32,
recorder: &mut PositionRecorder,
opts: &WalkOptions,
) -> Result<(), WalkError> {
let mut null = NullLogger;
let bin0_inc_x = compute_mvd_ctx_idx_inc_bin0(
current_mvd, &dec.neighbors, mb_x, part_bx, part_by, 0,
);
let mvd_x = {
let mut pc = PositionCtx {
frame_idx, mb_addr: mb_addr_u32, logger: &mut null,
};
decode_mvd_with_bin0_inc(
dec, 0, 0,
0, bin0_inc_x, &mut pc,
)?
};
let bin0_inc_y = compute_mvd_ctx_idx_inc_bin0(
current_mvd, &dec.neighbors, mb_x, part_bx, part_by, 1,
);
let mvd_y = {
let mut pc = PositionCtx {
frame_idx, mb_addr: mb_addr_u32, logger: &mut null,
};
decode_mvd_with_bin0_inc(
dec, 1, 0, 0, bin0_inc_y, &mut pc,
)?
};
current_mvd.fill_region(
part_bx, part_by, part_w4, part_h4,
mvd_x.unsigned_abs().min(i16::MAX as u32) as i16,
mvd_y.unsigned_abs().min(i16::MAX as u32) as i16,
);
if opts.record_mvd {
let sx = MvdSlot {
list: 0, partition, axis: Axis::X, value: mvd_x,
};
let sy = MvdSlot {
list: 0, partition, axis: Axis::Y, value: mvd_y,
};
recorder.on_mvd_slot(frame_idx, mb_addr_u32, &sx);
recorder.on_mvd_slot(frame_idx, mb_addr_u32, &sy);
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn decode_p_residuals_and_finish(
dec: &mut CabacDecoder<'_>,
pps: &Pps,
mb_x: usize,
_mb_y: usize,
_mb_w: usize,
prev_mb_qp: &mut i32,
frame_idx: u32,
mb_addr_u32: u32,
recorder: &mut PositionRecorder,
current_mvd: &CurrentMbMvdAbs,
) -> Result<bool, WalkError> {
let current_is_intra = false;
let cbp_byte = decode_coded_block_pattern(dec, mb_x)?;
let cbp_luma = cbp_byte & 0x0F;
let cbp_chroma = (cbp_byte >> 4) & 0x03;
if pps.transform_8x8_mode_flag && cbp_luma != 0 {
let use_8x8 = decode_transform_size_8x8_flag(dec, mb_x)?;
if use_8x8 {
return Err(WalkError::H264(H264Error::Unsupported(
"P-partition with transform_size_8x8_flag=1 not yet wired".into(),
)));
}
}
let qp_delta = if cbp_byte != 0 {
let d = decode_mb_qp_delta(dec)?;
*prev_mb_qp += d;
d
} else {
0
};
let mut current_cbf = CurrentMbCbf::new();
if cbp_luma != 0 {
for k in 0..16usize {
let (bx, by) = BLOCK_INDEX_TO_POS[k];
if cbp_luma & (1 << (k / 4)) != 0 {
let inc = compute_cbf_ctx_idx_inc_luma_4x4(
¤t_cbf, &dec.neighbors,
mb_x, bx, by, current_is_intra,
);
let path = ResidualPathKind::Luma4x4 { block_idx: k as u8 };
let scan = decode_residual_block_with_null_logger(
dec, 0, 15, 2, inc,
frame_idx, mb_addr_u32, path,
)?;
let coded = scan.iter().any(|&v| v != 0);
current_cbf.set(2, k, coded);
recorder.on_residual_block(
frame_idx, mb_addr_u32, &scan, 0, 15, path,
);
}
}
}
if cbp_chroma >= 1 {
for plane in 0u8..2 {
let inc = compute_cbf_ctx_idx_inc_chroma_dc(
&dec.neighbors, mb_x, plane, current_is_intra,
);
let path = ResidualPathKind::ChromaDc { plane };
let dc_flat = decode_residual_block_with_null_logger(
dec, 0, 3, 3, inc,
frame_idx, mb_addr_u32, path,
)?;
let coded = dc_flat.iter().any(|&v| v != 0);
current_cbf.set(3, plane as usize, coded);
recorder.on_residual_block(
frame_idx, mb_addr_u32, &dc_flat, 0, 3, path,
);
}
}
if cbp_chroma == 2 {
for plane in 0u8..2 {
for sub in 0..4usize {
let bx = (sub % 2) as u8;
let by = (sub / 2) as u8;
let inc = compute_cbf_ctx_idx_inc_chroma_ac(
¤t_cbf, &dec.neighbors,
mb_x, plane, bx, by, current_is_intra,
);
let path = ResidualPathKind::ChromaAc {
plane, block_idx: sub as u8,
};
let ac_scan = decode_residual_block_with_null_logger(
dec, 0, 14, 4, inc,
frame_idx, mb_addr_u32, path,
)?;
let coded = ac_scan.iter().any(|&v| v != 0);
current_cbf.set(
4, block_pos_to_chroma_ac_idx(plane, bx, by), coded,
);
recorder.on_residual_block(
frame_idx, mb_addr_u32, &ac_scan, 0, 14, path,
);
}
}
}
let is_last = decode_end_of_slice_flag(dec)?;
let mut nb = CabacNeighborMB {
mb_type: MbTypeClass::PInter,
mb_skip_flag: false,
..CabacNeighborMB::default()
};
nb.cbp_luma = cbp_luma;
nb.cbp_chroma = cbp_chroma;
nb.mb_qp_delta = qp_delta;
nb.coded_block_flag_cat = current_cbf.to_neighbor_cbf();
nb.abs_mvd_comp = current_mvd.to_neighbor();
nb.transform_size_8x8_flag = false;
dec.neighbors.commit(mb_x, nb);
Ok(is_last)
}
#[allow(clippy::too_many_arguments)]
fn decode_residual_block_with_null_logger(
dec: &mut CabacDecoder<'_>,
start_idx: usize,
end_idx: usize,
ctx_block_cat: u8,
cbf_ctx_idx_inc: u32,
frame_idx: u32,
mb_addr: u32,
path_kind: ResidualPathKind,
) -> Result<Vec<i32>, WalkError> {
let mut logger = NullLogger;
let mut pos_ctx = PositionCtx {
frame_idx,
mb_addr,
logger: &mut logger,
};
let coeffs = decode_residual_block_cabac(
dec, start_idx, end_idx, ctx_block_cat, cbf_ctx_idx_inc,
&mut pos_ctx,
|ci, kind| path_kind.path(ci, kind),
)?;
Ok(coeffs)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_annex_b_returns_no_slices() {
let r = walk_annex_b_for_cover(&[]);
assert!(matches!(r, Err(WalkError::NoSlices)));
}
#[test]
fn rejects_stream_with_no_parameter_sets() {
let nal_byte = (3u8 << 5) | 5;
let mut bytes = vec![0, 0, 0, 1, nal_byte];
bytes.extend_from_slice(&[0xff; 8]); let r = walk_annex_b_for_cover(&bytes);
match r {
Err(WalkError::MissingParameterSet) => {}
Err(_) => {}
Ok(_) => panic!("expected error on missing SPS/PPS"),
}
}
#[test]
fn walks_chunk5_empty_message_output_without_error() {
use crate::codec::h264::stego::encode_pixels::h264_stego_encode_i_frames_only;
let frame_size = 32 * 32 * 3 / 2;
let mut yuv = Vec::with_capacity(frame_size);
let mut s: u32 = 0xCAFE_F00D;
for _ in 0..frame_size {
s = s.wrapping_mul(1664525).wrapping_add(1013904223);
yuv.push((s >> 16) as u8);
}
let bytes = h264_stego_encode_i_frames_only(
&yuv, 32, 32, 1, &[], "test", 4, Some(26),
).expect("encode");
let walk = walk_annex_b_for_cover(&bytes).expect("walk");
assert_eq!(walk.n_slices, 1, "expected exactly one slice");
assert_eq!(walk.n_mb, 4, "32x32 has exactly 4 MBs");
assert!(
walk.cover.coeff_sign_bypass.len() > 0,
"expected non-empty coeff sign cover for random YUV",
);
}
#[test]
fn walks_uniform_yuv_through_i16x16_path() {
use crate::codec::h264::stego::encode_pixels::h264_stego_encode_i_frames_only;
let frame_size = 16 * 16 * 3 / 2;
let mut yuv = vec![128u8; frame_size];
for (i, b) in yuv.iter_mut().enumerate() {
*b = (*b as i32 + ((i as i32) % 3 - 1)) as u8;
}
let bytes = h264_stego_encode_i_frames_only(
&yuv, 16, 16, 1, &[], "test", 4, Some(26),
).expect("encode");
match walk_annex_b_for_cover(&bytes) {
Ok(walk) => {
assert_eq!(walk.n_slices, 1);
assert_eq!(walk.n_mb, 1, "16x16 has exactly one MB");
let total = walk.cover.coeff_sign_bypass.len()
+ walk.cover.coeff_suffix_lsb.len();
let _ = total;
}
Err(WalkError::H264(H264Error::Unsupported(s)))
if s.contains("I_NxN") || s.contains("I_PCM") =>
{
}
Err(e) => panic!("unexpected walk error: {e}"),
}
}
#[test]
fn encode_walk_parity_chunk5_empty_message() {
use crate::codec::h264::stego::encode_pixels::{
h264_stego_encode_i_frames_only, pass1_count,
};
let frame_size = 32 * 32 * 3 / 2;
let mut yuv = Vec::with_capacity(frame_size);
let mut s: u32 = 0xCAFE_F00D;
for _ in 0..frame_size {
s = s.wrapping_mul(1664525).wrapping_add(1013904223);
yuv.push((s >> 16) as u8);
}
let encoder_gop = pass1_count(
&yuv, 32, 32, 1, frame_size, Some(26),
).expect("pass1_count");
let encoder_cover = encoder_gop.cover;
let bytes = h264_stego_encode_i_frames_only(
&yuv, 32, 32, 1, &[], "test", 4, Some(26),
).expect("encode");
let walk = walk_annex_b_for_cover(&bytes).expect("walk");
let decoder_cover = walk.cover;
assert_eq!(
decoder_cover.coeff_sign_bypass.positions,
encoder_cover.coeff_sign_bypass.positions,
"coeff_sign_bypass positions must match"
);
assert_eq!(
decoder_cover.coeff_sign_bypass.bits,
encoder_cover.coeff_sign_bypass.bits,
"coeff_sign_bypass bits must match"
);
assert_eq!(
decoder_cover.coeff_suffix_lsb.positions,
encoder_cover.coeff_suffix_lsb.positions,
"coeff_suffix_lsb positions must match"
);
assert_eq!(
decoder_cover.coeff_suffix_lsb.bits,
encoder_cover.coeff_suffix_lsb.bits,
"coeff_suffix_lsb bits must match"
);
assert_eq!(decoder_cover.mvd_sign_bypass.positions,
encoder_cover.mvd_sign_bypass.positions);
assert_eq!(decoder_cover.mvd_sign_bypass.bits,
encoder_cover.mvd_sign_bypass.bits);
assert_eq!(decoder_cover.mvd_suffix_lsb.positions,
encoder_cover.mvd_suffix_lsb.positions);
assert_eq!(decoder_cover.mvd_suffix_lsb.bits,
encoder_cover.mvd_suffix_lsb.bits);
}
#[test]
fn walks_i_then_p_frame_without_error() {
use crate::codec::h264::encoder::encoder::{Encoder, EntropyMode};
let frame_size = 16 * 16 * 3 / 2;
let yuv = vec![128u8; frame_size];
let mut enc = Encoder::new(16, 16, Some(26)).expect("encoder");
enc.entropy_mode = EntropyMode::Cabac;
enc.enable_transform_8x8 = false;
let mut bytes = Vec::new();
bytes.extend_from_slice(&enc.encode_i_frame(&yuv).expect("I-frame"));
bytes.extend_from_slice(&enc.encode_p_frame(&yuv).expect("P-frame"));
match walk_annex_b_for_cover(&bytes) {
Ok(walk) => {
assert_eq!(walk.n_slices, 2, "expected I + P slices");
assert!(walk.n_mb >= 2);
}
Err(WalkError::H264(H264Error::Unsupported(s)))
if s.contains("P_partition") || s.contains("I_NxN")
|| s.contains("I_PCM") =>
{
}
Err(e) => panic!("unexpected walk error: {e}"),
}
}
#[test]
fn walks_i_then_p_frame_with_motion() {
use crate::codec::h264::encoder::encoder::{Encoder, EntropyMode};
let frame_size = 16 * 16 * 3 / 2;
let mut frame0 = vec![0u8; frame_size];
let mut frame1 = vec![0u8; frame_size];
let mut s: u32 = 0x4242_DEAD;
for i in 0..frame_size {
s = s.wrapping_mul(1103515245).wrapping_add(12345);
frame0[i] = (128 + ((s >> 24) & 0x07) as u8) as u8;
frame1[i] = (128 + ((s >> 22) & 0x07) as u8) as u8;
}
let mut enc = Encoder::new(16, 16, Some(26)).expect("encoder");
enc.entropy_mode = EntropyMode::Cabac;
enc.enable_transform_8x8 = false;
let mut bytes = Vec::new();
bytes.extend_from_slice(&enc.encode_i_frame(&frame0).expect("I"));
bytes.extend_from_slice(&enc.encode_p_frame(&frame1).expect("P"));
match walk_annex_b_for_cover(&bytes) {
Ok(walk) => {
assert_eq!(walk.n_slices, 2);
assert!(walk.n_mb >= 2);
}
Err(WalkError::H264(H264Error::Unsupported(s)))
if s.contains("P_partition mb_type_p=") || s.contains("I_NxN")
|| s.contains("I_PCM") || s.contains("transform_size") =>
{
}
Err(e) => panic!("unexpected walk error: {e}"),
}
}
#[test]
fn p_slice_parity_walker_matches_encoder_cover() {
use crate::codec::h264::encoder::encoder::{Encoder, EntropyMode};
use crate::codec::h264::stego::encoder_hook::{
PositionLoggerHook, StegoMbHook,
};
let frame_size = 16 * 16 * 3 / 2;
let mut frame0 = vec![0u8; frame_size];
let mut frame1 = vec![0u8; frame_size];
let mut s: u32 = 0xBEEF_F00D;
for i in 0..frame_size {
s = s.wrapping_mul(1103515245).wrapping_add(12345);
frame0[i] = (128 + ((s >> 24) & 0x07) as u8) as u8;
frame1[i] = (128 + ((s >> 22) & 0x07) as u8) as u8;
}
let mut enc = Encoder::new(16, 16, Some(26)).expect("encoder");
enc.entropy_mode = EntropyMode::Cabac;
enc.enable_transform_8x8 = false;
enc.set_stego_hook(Some(Box::new(PositionLoggerHook::new())));
let mut bytes = Vec::new();
bytes.extend_from_slice(&enc.encode_i_frame(&frame0).expect("I"));
bytes.extend_from_slice(&enc.encode_p_frame(&frame1).expect("P"));
let mut hook = enc.take_stego_hook().expect("hook present");
let encoder_gop = hook.take_cover_if_logger().expect("logger");
let walk = match walk_annex_b_for_cover(&bytes) {
Ok(w) => w,
Err(WalkError::H264(H264Error::Unsupported(s))) => {
eprintln!("Skipping §30B parity (encoder picked {s}); §30A4+ pending");
return;
}
Err(e) => panic!("unexpected walk error: {e}"),
};
assert_eq!(
walk.cover.coeff_sign_bypass.positions,
encoder_gop.cover.coeff_sign_bypass.positions,
"P-slice coeff_sign_bypass positions must match"
);
assert_eq!(
walk.cover.coeff_sign_bypass.bits,
encoder_gop.cover.coeff_sign_bypass.bits,
"P-slice coeff_sign_bypass bits must match"
);
assert_eq!(
walk.cover.coeff_suffix_lsb.positions,
encoder_gop.cover.coeff_suffix_lsb.positions,
);
assert_eq!(
walk.cover.coeff_suffix_lsb.bits,
encoder_gop.cover.coeff_suffix_lsb.bits,
);
assert_eq!(walk.cover.mvd_sign_bypass.len(),
encoder_gop.cover.mvd_sign_bypass.len());
assert_eq!(walk.cover.mvd_suffix_lsb.len(),
encoder_gop.cover.mvd_suffix_lsb.len());
}
#[test]
fn p_slice_parity_high_entropy_yuv_32x32() {
use crate::codec::h264::encoder::encoder::{Encoder, EntropyMode};
use crate::codec::h264::stego::encoder_hook::{
PositionLoggerHook, StegoMbHook,
};
let frame_size = 32 * 32 * 3 / 2;
let mut frame0 = Vec::with_capacity(frame_size);
let mut frame1 = Vec::with_capacity(frame_size);
let mut s: u32 = 0xABCD_1234;
for _ in 0..frame_size {
s = s.wrapping_mul(1664525).wrapping_add(1013904223);
frame0.push((s >> 16) as u8);
}
for _ in 0..frame_size {
s = s.wrapping_mul(1664525).wrapping_add(1013904223);
frame1.push((s >> 16) as u8);
}
let mut enc = Encoder::new(32, 32, Some(26)).expect("encoder");
enc.entropy_mode = EntropyMode::Cabac;
enc.enable_transform_8x8 = false;
enc.set_stego_hook(Some(Box::new(PositionLoggerHook::new())));
let mut bytes = Vec::new();
bytes.extend_from_slice(&enc.encode_i_frame(&frame0).expect("I"));
bytes.extend_from_slice(&enc.encode_p_frame(&frame1).expect("P"));
let mut hook = enc.take_stego_hook().expect("hook");
let encoder_gop = hook.take_cover_if_logger().expect("logger");
let walk = walk_annex_b_for_cover(&bytes).expect("walk");
assert_eq!(
walk.cover.coeff_sign_bypass.positions,
encoder_gop.cover.coeff_sign_bypass.positions,
);
assert_eq!(
walk.cover.coeff_sign_bypass.bits,
encoder_gop.cover.coeff_sign_bypass.bits,
);
assert_eq!(
walk.cover.coeff_suffix_lsb.positions,
encoder_gop.cover.coeff_suffix_lsb.positions,
);
assert_eq!(
walk.cover.coeff_suffix_lsb.bits,
encoder_gop.cover.coeff_suffix_lsb.bits,
);
}
#[test]
fn s30d_a_encoder_mvd_hook_fires_when_flag_on() {
use crate::codec::h264::encoder::encoder::{Encoder, EntropyMode};
use crate::codec::h264::stego::encoder_hook::{
PositionLoggerHook, StegoMbHook,
};
let frame_size = 16 * 16 * 3 / 2;
let mut frame0 = vec![0u8; frame_size];
let mut frame1 = vec![0u8; frame_size];
let mut s: u32 = 0xFEED_BEEF;
for i in 0..frame_size {
s = s.wrapping_mul(1103515245).wrapping_add(12345);
frame0[i] = (128 + ((s >> 24) & 0x07) as u8) as u8;
frame1[i] = (128 + ((s >> 22) & 0x07) as u8) as u8;
}
let mut enc_on = Encoder::new(16, 16, Some(26)).expect("encoder");
enc_on.entropy_mode = EntropyMode::Cabac;
enc_on.enable_transform_8x8 = false;
enc_on.enable_mvd_stego_hook = true;
enc_on.set_stego_hook(Some(Box::new(PositionLoggerHook::new())));
let _ = enc_on.encode_i_frame(&frame0).expect("I");
let _ = enc_on.encode_p_frame(&frame1).expect("P");
let mut hook_on = enc_on.take_stego_hook().expect("hook");
let cover_on = hook_on.take_cover_if_logger().expect("logger");
let mut enc_off = Encoder::new(16, 16, Some(26)).expect("encoder");
enc_off.entropy_mode = EntropyMode::Cabac;
enc_off.enable_transform_8x8 = false;
enc_off.set_stego_hook(Some(Box::new(PositionLoggerHook::new())));
let _ = enc_off.encode_i_frame(&frame0).expect("I");
let _ = enc_off.encode_p_frame(&frame1).expect("P");
let mut hook_off = enc_off.take_stego_hook().expect("hook");
let cover_off = hook_off.take_cover_if_logger().expect("logger");
assert_eq!(cover_off.cover.mvd_sign_bypass.len(), 0,
"flag OFF: encoder must not log MVD sign positions");
assert_eq!(cover_off.cover.mvd_suffix_lsb.len(), 0);
let _ = cover_on; }
#[test]
fn s30d_b_parity_walker_records_mvd_when_flag_on() {
use crate::codec::h264::encoder::encoder::{Encoder, EntropyMode};
use crate::codec::h264::stego::encoder_hook::{
PositionLoggerHook, StegoMbHook,
};
let frame_size = 16 * 16 * 3 / 2;
let mut frame0 = vec![0u8; frame_size];
let mut frame1 = vec![0u8; frame_size];
let mut s: u32 = 0xC0FF_EE42;
for i in 0..frame_size {
s = s.wrapping_mul(1103515245).wrapping_add(12345);
frame0[i] = (128 + ((s >> 24) & 0x07) as u8) as u8;
frame1[i] = (128 + ((s >> 22) & 0x07) as u8) as u8;
}
let mut enc = Encoder::new(16, 16, Some(26)).expect("encoder");
enc.entropy_mode = EntropyMode::Cabac;
enc.enable_transform_8x8 = false;
enc.enable_mvd_stego_hook = true;
enc.set_stego_hook(Some(Box::new(PositionLoggerHook::new())));
let mut bytes = Vec::new();
bytes.extend_from_slice(&enc.encode_i_frame(&frame0).expect("I"));
bytes.extend_from_slice(&enc.encode_p_frame(&frame1).expect("P"));
let mut hook = enc.take_stego_hook().expect("hook");
let encoder_gop = hook.take_cover_if_logger().expect("logger");
let opts = WalkOptions { record_mvd: true };
let walk = walk_annex_b_for_cover_with_options(&bytes, opts)
.expect("walk");
assert_eq!(
walk.cover.coeff_sign_bypass.positions,
encoder_gop.cover.coeff_sign_bypass.positions,
);
assert_eq!(
walk.cover.coeff_sign_bypass.bits,
encoder_gop.cover.coeff_sign_bypass.bits,
);
assert_eq!(
walk.cover.coeff_suffix_lsb.positions,
encoder_gop.cover.coeff_suffix_lsb.positions,
);
assert_eq!(
walk.cover.coeff_suffix_lsb.bits,
encoder_gop.cover.coeff_suffix_lsb.bits,
);
assert_eq!(
walk.cover.mvd_sign_bypass.positions,
encoder_gop.cover.mvd_sign_bypass.positions,
"§30D-B: mvd_sign_bypass positions must match"
);
assert_eq!(
walk.cover.mvd_sign_bypass.bits,
encoder_gop.cover.mvd_sign_bypass.bits,
"§30D-B: mvd_sign_bypass bits must match"
);
assert_eq!(
walk.cover.mvd_suffix_lsb.positions,
encoder_gop.cover.mvd_suffix_lsb.positions,
);
assert_eq!(
walk.cover.mvd_suffix_lsb.bits,
encoder_gop.cover.mvd_suffix_lsb.bits,
);
}
#[test]
fn s30d_b_parity_high_entropy_yuv_32x32() {
use crate::codec::h264::encoder::encoder::{Encoder, EntropyMode};
use crate::codec::h264::stego::encoder_hook::{
PositionLoggerHook, StegoMbHook,
};
let frame_size = 32 * 32 * 3 / 2;
let mut frame0 = Vec::with_capacity(frame_size);
let mut frame1 = Vec::with_capacity(frame_size);
let mut s: u32 = 0xABCD_1234;
for _ in 0..frame_size {
s = s.wrapping_mul(1664525).wrapping_add(1013904223);
frame0.push((s >> 16) as u8);
}
for _ in 0..frame_size {
s = s.wrapping_mul(1664525).wrapping_add(1013904223);
frame1.push((s >> 16) as u8);
}
let mut enc = Encoder::new(32, 32, Some(26)).expect("encoder");
enc.entropy_mode = EntropyMode::Cabac;
enc.enable_transform_8x8 = false;
enc.enable_mvd_stego_hook = true;
enc.set_stego_hook(Some(Box::new(PositionLoggerHook::new())));
let mut bytes = Vec::new();
bytes.extend_from_slice(&enc.encode_i_frame(&frame0).expect("I"));
bytes.extend_from_slice(&enc.encode_p_frame(&frame1).expect("P"));
let mut hook = enc.take_stego_hook().expect("hook");
let encoder_gop = hook.take_cover_if_logger().expect("logger");
let opts = WalkOptions { record_mvd: true };
let walk = walk_annex_b_for_cover_with_options(&bytes, opts)
.expect("walk");
assert_eq!(
walk.cover.mvd_sign_bypass.positions,
encoder_gop.cover.mvd_sign_bypass.positions,
);
assert_eq!(
walk.cover.mvd_sign_bypass.bits,
encoder_gop.cover.mvd_sign_bypass.bits,
);
assert_eq!(
walk.cover.mvd_suffix_lsb.positions,
encoder_gop.cover.mvd_suffix_lsb.positions,
);
assert_eq!(
walk.cover.mvd_suffix_lsb.bits,
encoder_gop.cover.mvd_suffix_lsb.bits,
);
assert_eq!(
walk.cover.coeff_sign_bypass.positions,
encoder_gop.cover.coeff_sign_bypass.positions,
);
assert_eq!(
walk.cover.coeff_suffix_lsb.bits,
encoder_gop.cover.coeff_suffix_lsb.bits,
);
}
#[test]
fn pass3_roundtrip_real_world_64x48_5f_diagnostic() {
use crate::codec::h264::stego::encode_pixels::h264_stego_encode_yuv_string_4domain_multigop;
let yuv_path = "test-vectors/video/h264/real-world/img4138_64x48_f5.yuv";
let yuv = match std::fs::read(yuv_path) {
Ok(b) => b,
Err(_) => {
eprintln!("Skipping: {yuv_path} missing (run from core/)");
return;
}
};
let pass = "test-pass-pass3";
let msg = "h";
let stego_bytes = h264_stego_encode_yuv_string_4domain_multigop(
&yuv, 64, 48, 5, 5, msg, pass,
).expect("4-domain encode");
let walk = walk_annex_b_for_cover_with_options(
&stego_bytes, WalkOptions { record_mvd: true },
).expect("walk Pass 3");
eprintln!("Pass 3 walker cover lengths:");
eprintln!(" coeff_sign={} coeff_suffix={} mvd_sign={} mvd_suffix={}",
walk.cover.coeff_sign_bypass.len(),
walk.cover.coeff_suffix_lsb.len(),
walk.cover.mvd_sign_bypass.len(),
walk.cover.mvd_suffix_lsb.len(),
);
use crate::codec::h264::stego::decode_pixels::h264_stego_decode_yuv_string_4domain;
match h264_stego_decode_yuv_string_4domain(&stego_bytes, pass) {
Ok(s) => {
eprintln!("decode succeeded: {s:?}");
assert_eq!(s, msg, "round trip recovered wrong message");
}
Err(e) => {
eprintln!("decode failed: {e:?}");
panic!("decode error: {e:?}");
}
}
}
#[test]
fn parity_real_world_64x48_5f_diagnostic() {
use crate::codec::h264::encoder::encoder::{Encoder, EntropyMode};
use crate::codec::h264::stego::encoder_hook::{
PositionLoggerHook, StegoMbHook,
};
let yuv_path = "test-vectors/video/h264/real-world/img4138_64x48_f5.yuv";
let yuv = match std::fs::read(yuv_path) {
Ok(b) => b,
Err(_) => {
eprintln!("Skipping: {yuv_path} missing (run from core/)");
return;
}
};
let frame_size = 64 * 48 * 3 / 2;
assert_eq!(yuv.len(), frame_size * 5, "fixture size mismatch");
let mut enc = Encoder::new(64, 48, Some(26)).expect("encoder");
enc.entropy_mode = EntropyMode::Cabac;
enc.enable_transform_8x8 = false;
enc.enable_mvd_stego_hook = true;
enc.set_stego_hook(Some(Box::new(PositionLoggerHook::new())));
let mut bytes = Vec::new();
bytes.extend_from_slice(
&enc.encode_i_frame(&yuv[0..frame_size]).expect("I")
);
for fi in 1..5 {
bytes.extend_from_slice(
&enc.encode_p_frame(&yuv[fi * frame_size..(fi + 1) * frame_size])
.expect("P")
);
}
let mut hook = enc.take_stego_hook().expect("hook");
let enc_gop = hook.take_cover_if_logger().expect("logger");
let walk = walk_annex_b_for_cover_with_options(
&bytes,
WalkOptions { record_mvd: true },
)
.expect("walk");
assert_eq!(
walk.cover.coeff_sign_bypass.positions.len(),
enc_gop.cover.coeff_sign_bypass.positions.len(),
"coeff_sign_bypass length divergence"
);
assert_eq!(
walk.cover.coeff_suffix_lsb.positions.len(),
enc_gop.cover.coeff_suffix_lsb.positions.len(),
"coeff_suffix_lsb length divergence"
);
assert_eq!(
walk.cover.mvd_sign_bypass.positions.len(),
enc_gop.cover.mvd_sign_bypass.positions.len(),
"mvd_sign_bypass length divergence"
);
assert_eq!(
walk.cover.mvd_suffix_lsb.positions.len(),
enc_gop.cover.mvd_suffix_lsb.positions.len(),
"mvd_suffix_lsb length divergence"
);
assert_eq!(
walk.cover.coeff_sign_bypass.bits,
enc_gop.cover.coeff_sign_bypass.bits,
"coeff_sign_bypass BIT-content divergence (round-trip-breaking)"
);
assert_eq!(
walk.cover.coeff_sign_bypass.positions,
enc_gop.cover.coeff_sign_bypass.positions,
"coeff_sign_bypass POSITION-key divergence (shadow-affecting only)"
);
assert_eq!(
walk.cover.coeff_suffix_lsb.positions,
enc_gop.cover.coeff_suffix_lsb.positions,
);
assert_eq!(
walk.cover.coeff_suffix_lsb.bits,
enc_gop.cover.coeff_suffix_lsb.bits,
);
assert_eq!(
walk.cover.mvd_sign_bypass.positions,
enc_gop.cover.mvd_sign_bypass.positions,
);
assert_eq!(
walk.cover.mvd_sign_bypass.bits,
enc_gop.cover.mvd_sign_bypass.bits,
);
assert_eq!(
walk.cover.mvd_suffix_lsb.positions,
enc_gop.cover.mvd_suffix_lsb.positions,
);
assert_eq!(
walk.cover.mvd_suffix_lsb.bits,
enc_gop.cover.mvd_suffix_lsb.bits,
);
}
#[test]
fn measure_criterion_c_safe_count_real_world_64x48() {
use crate::codec::h264::encoder::encoder::{Encoder, EntropyMode};
use crate::codec::h264::stego::encoder_hook::{
PositionLoggerHook, StegoMbHook,
};
use crate::codec::h264::stego::cascade_safety::analyze_safe_mvd_subset;
let yuv_path = "test-vectors/video/h264/real-world/img4138_64x48_f5.yuv";
let yuv = match std::fs::read(yuv_path) {
Ok(b) => b,
Err(_) => return,
};
let frame_size = 64 * 48 * 3 / 2;
let mut enc = Encoder::new(64, 48, Some(26)).expect("encoder");
enc.entropy_mode = EntropyMode::Cabac;
enc.enable_transform_8x8 = false;
enc.enable_mvd_stego_hook = true;
enc.set_stego_hook(Some(Box::new(PositionLoggerHook::new())));
let _ = enc.encode_i_frame(&yuv[0..frame_size]).expect("I");
for fi in 1..5 {
let _ = enc.encode_p_frame(&yuv[fi * frame_size..(fi + 1) * frame_size])
.expect("P");
}
let mut hook = enc.take_stego_hook().expect("hook");
let meta = hook.take_mvd_meta_if_logger();
let safe = analyze_safe_mvd_subset(&meta, 4, 3);
let n = meta.len();
let safe_count = safe.iter().filter(|&&b| b).count();
eprintln!("CRITERION C on img4138_64x48_f5.yuv:");
eprintln!(" total mvd positions: {n}");
eprintln!(" safe count : {safe_count} ({:.1}%)",
100.0 * safe_count as f64 / n.max(1) as f64);
}
#[test]
fn safe_set_parity_real_world_64x48() {
use crate::codec::h264::encoder::encoder::{Encoder, EntropyMode};
use crate::codec::h264::stego::encoder_hook::{
PositionLoggerHook, StegoMbHook,
};
use crate::codec::h264::stego::cascade_safety::analyze_safe_mvd_subset;
let yuv_path = "test-vectors/video/h264/real-world/img4138_64x48_f5.yuv";
let yuv = match std::fs::read(yuv_path) {
Ok(b) => b,
Err(_) => return,
};
let frame_size = 64 * 48 * 3 / 2;
let mut enc = Encoder::new(64, 48, Some(26)).expect("encoder");
enc.entropy_mode = EntropyMode::Cabac;
enc.enable_transform_8x8 = false;
enc.enable_mvd_stego_hook = true;
enc.set_stego_hook(Some(Box::new(PositionLoggerHook::new())));
let mut bytes = Vec::new();
bytes.extend_from_slice(
&enc.encode_i_frame(&yuv[0..frame_size]).expect("I")
);
for fi in 1..5 {
bytes.extend_from_slice(
&enc.encode_p_frame(&yuv[fi * frame_size..(fi + 1) * frame_size])
.expect("P")
);
}
let mut hook = enc.take_stego_hook().expect("hook");
let enc_meta = hook.take_mvd_meta_if_logger();
let walk = walk_annex_b_for_cover_with_options(
&bytes,
WalkOptions { record_mvd: true },
)
.expect("walk");
let enc_safe = analyze_safe_mvd_subset(&enc_meta, 4, 3);
let walk_safe = analyze_safe_mvd_subset(&walk.mvd_meta, 4, 3);
assert_eq!(
enc_safe.len(),
walk_safe.len(),
"safe-set vector lengths differ"
);
for (i, (e, w)) in enc_safe.iter().zip(walk_safe.iter()).enumerate() {
assert_eq!(*e, *w,
"safe-set bit divergence at idx {i}: enc={e} walker={w}");
}
let safe_count = enc_safe.iter().filter(|&&b| b).count();
eprintln!("Safe-set parity: {} entries match; {safe_count} flagged safe",
enc_safe.len());
}
#[test]
fn mvd_meta_parity_real_world_64x48() {
use crate::codec::h264::encoder::encoder::{Encoder, EntropyMode};
use crate::codec::h264::stego::encoder_hook::{
PositionLoggerHook, StegoMbHook,
};
let yuv_path = "test-vectors/video/h264/real-world/img4138_64x48_f5.yuv";
let yuv = match std::fs::read(yuv_path) {
Ok(b) => b,
Err(_) => {
eprintln!("Skipping: {yuv_path} missing (run from core/)");
return;
}
};
let frame_size = 64 * 48 * 3 / 2;
let mut enc = Encoder::new(64, 48, Some(26)).expect("encoder");
enc.entropy_mode = EntropyMode::Cabac;
enc.enable_transform_8x8 = false;
enc.enable_mvd_stego_hook = true;
enc.set_stego_hook(Some(Box::new(PositionLoggerHook::new())));
let mut bytes = Vec::new();
bytes.extend_from_slice(
&enc.encode_i_frame(&yuv[0..frame_size]).expect("I")
);
for fi in 1..5 {
bytes.extend_from_slice(
&enc.encode_p_frame(&yuv[fi * frame_size..(fi + 1) * frame_size])
.expect("P")
);
}
let mut hook = enc.take_stego_hook().expect("hook");
let enc_meta = hook.take_mvd_meta_if_logger();
let walk = walk_annex_b_for_cover_with_options(
&bytes,
WalkOptions { record_mvd: true },
)
.expect("walk");
assert_eq!(
enc_meta.len(),
walk.mvd_meta.len(),
"encoder mvd_meta count != walker mvd_meta count"
);
for (i, (e, w)) in enc_meta.iter().zip(walk.mvd_meta.iter()).enumerate() {
assert_eq!(e.frame_idx, w.frame_idx, "mismatch frame_idx at idx {i}");
assert_eq!(e.mb_addr, w.mb_addr, "mismatch mb_addr at idx {i}");
assert_eq!(e.partition, w.partition, "mismatch partition at idx {i}");
assert_eq!(e.axis, w.axis, "mismatch axis at idx {i}");
assert_eq!(e.magnitude, w.magnitude,
"mismatch magnitude at idx {i}: enc={} walker={}",
e.magnitude, w.magnitude);
}
eprintln!("MVD-meta parity: {} entries match byte-identical", enc_meta.len());
}
#[test]
#[ignore = "Phase 6F.2(i) measurement prototype — informs future MVD re-enablement decision; print-only, run manually with --include-ignored --nocapture."]
fn measure_safe_mvd_subset_real_world_64x48() {
use crate::codec::h264::encoder::encoder::{Encoder, EntropyMode};
use crate::codec::h264::stego::encoder_hook::{
PositionLoggerHook, StegoMbHook,
};
use crate::codec::h264::stego::inject::MvdSlot;
use crate::codec::h264::stego::Axis;
use crate::codec::h264::stego::hook::PositionKey;
let yuv_path = "test-vectors/video/h264/real-world/img4138_64x48_f5.yuv";
let yuv = match std::fs::read(yuv_path) {
Ok(b) => b,
Err(_) => {
eprintln!("Skipping: {yuv_path} missing (run from core/)");
return;
}
};
let frame_size = 64 * 48 * 3 / 2;
#[derive(Debug, Default)]
struct MagLogger {
inner: PositionLoggerHook,
mvd: Vec<(PositionKey, u8, u32, u32, u8, u8, u32)>,
}
impl StegoMbHook for MagLogger {
fn on_residual_block(
&mut self, fi: u32, mba: u32,
sc: &mut [i32], si: usize, ei: usize,
pk: crate::codec::h264::stego::orchestrate::ResidualPathKind,
) {
self.inner.on_residual_block(fi, mba, sc, si, ei, pk);
}
fn on_mvd_slot(&mut self, fi: u32, mba: u32, slot: &mut MvdSlot) {
if slot.value != 0 {
use crate::codec::h264::stego::inject::enumerate_mvd_sign_positions;
let single = [*slot];
let positions = enumerate_mvd_sign_positions(&single, fi, mba);
if let Some(pos) = positions.first() {
let bit = if slot.value < 0 { 1u8 } else { 0u8 };
let mag = slot.value.unsigned_abs();
let axis = match slot.axis { Axis::X => 0u8, Axis::Y => 1u8 };
self.mvd.push((*pos, bit, mag, mba, slot.partition, axis, fi));
}
}
self.inner.on_mvd_slot(fi, mba, slot);
}
fn begin_mvd_for_mb(&mut self) { self.inner.begin_mvd_for_mb(); }
fn commit_mvd_for_mb(&mut self) { self.inner.commit_mvd_for_mb(); }
fn rollback_mvd_for_mb(&mut self) { self.inner.rollback_mvd_for_mb(); }
fn take_cover_if_logger(&mut self)
-> Option<crate::codec::h264::stego::orchestrate::GopCover>
{ Some(self.inner.take_cover()) }
}
let mut enc = Encoder::new(64, 48, Some(26)).expect("encoder");
enc.entropy_mode = EntropyMode::Cabac;
enc.enable_transform_8x8 = false;
enc.enable_mvd_stego_hook = true;
enc.set_stego_hook(Some(Box::<MagLogger>::default()));
let _ = enc.encode_i_frame(&yuv[0..frame_size]).expect("I");
for fi in 1..5 {
let _ = enc.encode_p_frame(&yuv[fi * frame_size..(fi + 1) * frame_size])
.expect("P");
}
let mut hook = enc.take_stego_hook().expect("hook");
let mvd_data: Vec<(PositionKey, u8, u32, u32, u8, u8, u32)> = {
let raw = Box::into_raw(hook);
let mag_logger: Box<MagLogger> = unsafe { Box::from_raw(raw as *mut MagLogger) };
mag_logger.mvd
};
const MB_W: u32 = 64 / 16; const MB_H: u32 = 48 / 16; const MB_TOTAL: u32 = MB_W * MB_H;
let mut frame_groups: std::collections::BTreeMap<u32, Vec<usize>> =
std::collections::BTreeMap::new();
for (i, t) in mvd_data.iter().enumerate() {
frame_groups.entry(t.6).or_default().push(i);
}
let mb_propagates = |mx_p: u32, my_p: u32, mx_q: u32, my_q: u32| -> bool {
if my_q < my_p { return false; }
if my_q == my_p && mx_q <= mx_p { return false; }
(mx_q == mx_p + 1 && my_q == my_p) || (mx_q == mx_p && my_q == my_p + 1) || (my_q == my_p + 1 && (
mx_q == mx_p
|| mx_q + 1 == mx_p || mx_q == mx_p + 1 ))
};
let total = mvd_data.len();
let mut count_a_sink = 0usize;
let mut count_b_loose = 0usize;
let mut count_c_independent = 0usize;
for (_fi, idxs) in &frame_groups {
let mut sorted = idxs.clone();
sorted.sort_by_key(|&i| {
let t = &mvd_data[i];
(t.3, t.4, t.5) });
let mut selected_mbs: Vec<(u32, u32, u32)> = Vec::new();
for &i in &sorted {
let (_pk, _bit, mag_p, mba_p, _part, _axis, _fi_p) = mvd_data[i];
let mx_p = mba_p % MB_W;
let my_p = mba_p / MB_W;
let is_sink = (mx_p == MB_W - 1) && (my_p == MB_H - 1);
if is_sink {
count_a_sink += 1;
}
let two_m_p = 2 * mag_p;
let mut loose_safe = true;
for &j in &sorted {
if j == i { continue; }
let (_pkj, _bj, mag_q, mba_q, _, _, _) = mvd_data[j];
if mba_q <= mba_p { continue; } let mx_q = mba_q % MB_W;
let my_q = mba_q / MB_W;
if mb_propagates(mx_p, my_p, mx_q, my_q) {
if two_m_p >= mag_q {
loose_safe = false;
break;
}
}
}
if loose_safe {
count_b_loose += 1;
}
let pred_safe = !selected_mbs.iter().any(|&(sx, sy, _)| {
mb_propagates(sx, sy, mx_p, my_p)
});
if pred_safe && loose_safe {
count_c_independent += 1;
selected_mbs.push((mx_p, my_p, two_m_p));
}
}
}
eprintln!("CASCADE-SAFE PROTOTYPE on img4138_64x48_f5.yuv:");
eprintln!(" Total cover_p1.mvd_sign positions: {total}");
eprintln!(" (A) Sink-safe : {count_a_sink} ({:.1}%)",
100.0 * count_a_sink as f64 / total.max(1) as f64);
eprintln!(" (B) Loose-flip-safe : {count_b_loose} ({:.1}%)",
100.0 * count_b_loose as f64 / total.max(1) as f64);
eprintln!(" (C) Mutually-indep+safe: {count_c_independent} ({:.1}%)",
100.0 * count_c_independent as f64 / total.max(1) as f64);
eprintln!();
eprintln!(" Decision rule:");
eprintln!(" - C>=50% → productionize safe-subset injection.");
eprintln!(" - C 20-50% → tier-2 analysis (level-2 chains, etc.).");
eprintln!(" - C<20% → residual-only stays. Stealth tradeoff documented.");
}
#[test]
fn cabac_data_byte_offset_handles_aligned_and_unaligned() {
assert_eq!(cabac_data_byte_offset(0), 0);
assert_eq!(cabac_data_byte_offset(8), 1);
assert_eq!(cabac_data_byte_offset(7), 1);
assert_eq!(cabac_data_byte_offset(9), 2);
assert_eq!(cabac_data_byte_offset(16), 2);
assert_eq!(cabac_data_byte_offset(17), 3);
}
fn encode_one_gop_annex_b(seed: u32) -> Vec<u8> {
use crate::codec::h264::stego::encode_pixels::h264_stego_encode_i_frames_only;
let frame_size = 32 * 32 * 3 / 2;
let mut yuv = Vec::with_capacity(frame_size);
let mut s: u32 = seed;
for _ in 0..frame_size {
s = s.wrapping_mul(1664525).wrapping_add(1013904223);
yuv.push((s >> 16) as u8);
}
h264_stego_encode_i_frames_only(
&yuv, 32, 32, 1, &[], "test", 4, Some(26),
).expect("encode")
}
#[test]
fn streaming_walker_single_gop_fires_callback_once() {
let bytes = encode_one_gop_annex_b(0xCAFE_F00D);
let mut gop_count = 0;
let mut last_gop_idx = u32::MAX;
let mut gop_n_mb_seen = 0;
let mut gop_cover_total_len = 0;
let out = walk_annex_b_streaming(
&bytes, WalkOptions::default(), |gop_ctx| {
gop_count += 1;
last_gop_idx = gop_ctx.gop_idx;
gop_n_mb_seen += gop_ctx.n_mb;
gop_cover_total_len += gop_ctx.cover.total_len();
Ok(WalkAction::Continue)
},
).expect("streaming walk");
assert_eq!(gop_count, 1, "expected one GOP callback");
assert_eq!(last_gop_idx, 0, "first GOP idx is 0");
assert_eq!(out.n_gops, 1);
assert_eq!(out.n_mb, gop_n_mb_seen);
let wrapper = walk_annex_b_for_cover(&bytes).expect("wrapper walk");
assert_eq!(wrapper.n_mb, out.n_mb);
assert_eq!(wrapper.n_slices, out.n_slices);
assert_eq!(wrapper.cover.total_len(), gop_cover_total_len);
}
#[test]
fn streaming_walker_two_gops_fires_callback_twice_in_order() {
let g0 = encode_one_gop_annex_b(0xCAFE_F00D);
let g1 = encode_one_gop_annex_b(0xDEAD_BEEF);
let mut bytes = g0.clone();
bytes.extend_from_slice(&g1);
let mut gop_idxs: Vec<u32> = Vec::new();
let mut total_cover_total_len = 0;
let out = walk_annex_b_streaming(
&bytes, WalkOptions::default(), |gop_ctx| {
gop_idxs.push(gop_ctx.gop_idx);
total_cover_total_len += gop_ctx.cover.total_len();
Ok(WalkAction::Continue)
},
).expect("streaming walk");
assert_eq!(out.n_gops, 2, "expected two GOPs");
assert_eq!(gop_idxs, vec![0, 1], "GOP indices monotonic from 0");
let wrapper = walk_annex_b_for_cover(&bytes).expect("wrapper walk");
assert_eq!(wrapper.cover.total_len(), total_cover_total_len);
assert_eq!(wrapper.n_slices, out.n_slices);
assert_eq!(wrapper.n_mb, out.n_mb);
}
#[test]
fn streaming_walker_stop_walk_terminates_early() {
let g0 = encode_one_gop_annex_b(0xCAFE_F00D);
let g1 = encode_one_gop_annex_b(0xDEAD_BEEF);
let mut bytes = g0.clone();
bytes.extend_from_slice(&g1);
let mut gop_count = 0;
let out = walk_annex_b_streaming(
&bytes, WalkOptions::default(), |_gop_ctx| {
gop_count += 1;
Ok(WalkAction::StopWalk)
},
).expect("streaming walk");
assert_eq!(gop_count, 1, "callback fired exactly once before StopWalk");
assert_eq!(out.n_gops, 1, "streaming output reports only 1 GOP");
}
}