use super::encoder::{Encoder, EntropyMode};
use super::EncoderError;
#[derive(Debug, Clone, Copy)]
pub struct BaselineTranscodeConfig {
pub width: u32,
pub height: u32,
pub quality: Option<u8>,
pub gop_length: u32,
pub n_frames: usize,
}
impl BaselineTranscodeConfig {
pub fn defaults(width: u32, height: u32, n_frames: usize) -> Self {
Self {
width,
height,
quality: Some(26),
gop_length: 30,
n_frames,
}
}
}
pub fn transcode_yuv_to_baseline_cavlc_h264(
pixels: &[u8],
config: BaselineTranscodeConfig,
) -> Result<Vec<u8>, EncoderError> {
let frame_size = (config.width * config.height * 3 / 2) as usize;
if pixels.len() != config.n_frames * frame_size {
return Err(EncoderError::InvalidInput(format!(
"pixel buffer size mismatch: got {} bytes, expected {} ({} frames × {})",
pixels.len(),
config.n_frames * frame_size,
config.n_frames,
frame_size,
)));
}
let mut enc = Encoder::new(config.width, config.height, config.quality)?;
enc.entropy_mode = EntropyMode::Cavlc;
enc.set_gop_length(config.gop_length);
let estimated_bytes = config
.n_frames
.saturating_mul((config.width * config.height) as usize / 8);
let mut out: Vec<u8> = Vec::with_capacity(estimated_bytes);
for fi in 0..config.n_frames {
let frame = &pixels[fi * frame_size..(fi + 1) * frame_size];
let is_idr = fi % (config.gop_length as usize) == 0;
let nal = if is_idr {
enc.encode_i_frame(frame)?
} else {
enc.encode_p_frame(frame)?
};
out.extend_from_slice(&nal);
}
Ok(out)
}
pub struct StreamingEncoder {
enc: Encoder,
gop_length: u32,
frame_index: u32,
}
impl StreamingEncoder {
pub fn new(
width: u32,
height: u32,
quality: Option<u8>,
gop_length: u32,
) -> Result<Self, EncoderError> {
let mut enc = Encoder::new(width, height, quality)?;
enc.entropy_mode = EntropyMode::Cavlc;
let gop = if gop_length == 0 { 30 } else { gop_length };
enc.set_gop_length(gop);
Ok(Self { enc, gop_length: gop, frame_index: 0 })
}
pub fn push_frame(&mut self, pixels: &[u8]) -> Result<Vec<u8>, EncoderError> {
let frame_size = (self.enc.width * self.enc.height * 3 / 2) as usize;
if pixels.len() != frame_size {
return Err(EncoderError::InvalidInput(format!(
"frame buffer size mismatch: got {} bytes, expected {}",
pixels.len(),
frame_size,
)));
}
let is_idr = self.frame_index.is_multiple_of(self.gop_length);
let nal = if is_idr {
self.enc.encode_i_frame(pixels)?
} else {
self.enc.encode_p_frame(pixels)?
};
self.frame_index += 1;
Ok(nal)
}
pub fn frames_emitted(&self) -> u32 {
self.frame_index
}
}
#[cfg(test)]
mod tests {
use super::*;
fn deterministic_yuv(w: u32, h: u32, n_frames: usize) -> Vec<u8> {
let frame_size = (w * h * 3 / 2) as usize;
let mut buf = Vec::with_capacity(n_frames * frame_size);
for fi in 0..n_frames {
for y in 0..h {
for x in 0..w {
buf.push(((x + y + fi as u32 * 3) & 0xFF) as u8);
}
}
for _ in 0..(w * h / 4) {
buf.push(128);
}
for _ in 0..(w * h / 4) {
buf.push(128);
}
}
buf
}
#[test]
fn transcode_emits_nonempty_annex_b_for_single_idr() {
let yuv = deterministic_yuv(32, 32, 1);
let cfg = BaselineTranscodeConfig::defaults(32, 32, 1);
let h264 = transcode_yuv_to_baseline_cavlc_h264(&yuv, cfg).unwrap();
assert!(
h264.starts_with(&[0, 0, 0, 1]) || h264.starts_with(&[0, 0, 1]),
"expected Annex-B start code, got {:02x?}",
&h264[..h264.len().min(8)]
);
let mut starts = 0;
for w in h264.windows(4) {
if w == [0, 0, 0, 1] {
starts += 1;
}
}
assert!(starts >= 3, "expected ≥3 NALs (SPS+PPS+slice), got {starts}");
}
#[test]
fn transcode_idr_then_p_runs_clean() {
let yuv = deterministic_yuv(32, 32, 5);
let cfg = BaselineTranscodeConfig::defaults(32, 32, 5);
let h264 = transcode_yuv_to_baseline_cavlc_h264(&yuv, cfg).unwrap();
let mut starts = 0;
for w in h264.windows(4) {
if w == [0, 0, 0, 1] {
starts += 1;
}
}
assert!(starts >= 7, "expected ≥7 NALs for IDR+4P, got {starts}");
}
#[test]
fn transcode_rejects_size_mismatch() {
let yuv = vec![0u8; 100]; let cfg = BaselineTranscodeConfig::defaults(32, 32, 1);
assert!(transcode_yuv_to_baseline_cavlc_h264(&yuv, cfg).is_err());
}
#[test]
fn transcode_rejects_non_mb_aligned() {
let yuv = vec![0u8; (33 * 32 * 3 / 2) as usize];
let cfg = BaselineTranscodeConfig::defaults(33, 32, 1);
assert!(transcode_yuv_to_baseline_cavlc_h264(&yuv, cfg).is_err());
}
#[test]
fn streaming_encoder_per_frame_matches_one_shot() {
let n_frames = 5;
let yuv = deterministic_yuv(32, 32, n_frames);
let frame_size = (32 * 32 * 3 / 2) as usize;
let cfg = BaselineTranscodeConfig::defaults(32, 32, n_frames);
let one_shot = transcode_yuv_to_baseline_cavlc_h264(&yuv, cfg).unwrap();
let mut streaming = StreamingEncoder::new(32, 32, Some(26), 30).unwrap();
let mut concat: Vec<u8> = Vec::new();
for fi in 0..n_frames {
let frame = &yuv[fi * frame_size..(fi + 1) * frame_size];
concat.extend_from_slice(&streaming.push_frame(frame).unwrap());
}
assert_eq!(streaming.frames_emitted(), n_frames as u32);
assert_eq!(concat, one_shot, "streaming != one-shot byte-for-byte");
}
#[test]
fn streaming_encoder_rejects_wrong_frame_size() {
let mut streaming = StreamingEncoder::new(32, 32, Some(26), 30).unwrap();
let bad = vec![0u8; 100];
assert!(streaming.push_frame(&bad).is_err());
}
#[test]
fn encoder_invokes_stego_hook_during_cabac_encode() {
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use crate::codec::h264::stego::encoder_hook::StegoMbHook;
use crate::codec::h264::stego::orchestrate::ResidualPathKind;
use crate::codec::h264::stego::inject::MvdSlot;
#[derive(Debug)]
struct CountHook {
residual_calls: Arc<AtomicUsize>,
mvd_calls: Arc<AtomicUsize>,
}
impl StegoMbHook for CountHook {
fn on_residual_block(
&mut self, _: u32, _: u32, _: &mut [i32], _: usize, _: usize,
_: ResidualPathKind,
) {
self.residual_calls.fetch_add(1, Ordering::Relaxed);
}
fn on_mvd_slot(&mut self, _: u32, _: u32, _: &mut MvdSlot) {
self.mvd_calls.fetch_add(1, Ordering::Relaxed);
}
}
let residual_calls = Arc::new(AtomicUsize::new(0));
let mvd_calls = Arc::new(AtomicUsize::new(0));
let hook = Box::new(CountHook {
residual_calls: residual_calls.clone(),
mvd_calls: mvd_calls.clone(),
});
let yuv = deterministic_yuv(32, 32, 1);
let mut enc = Encoder::new(32, 32, Some(26)).unwrap();
enc.entropy_mode = EntropyMode::Cabac;
enc.enable_transform_8x8 = false;
enc.set_stego_hook(Some(hook));
let _bytes = enc.encode_i_frame(&yuv).unwrap();
let r = residual_calls.load(Ordering::Relaxed);
assert!(
r >= 4,
"stego hook MUST fire ≥4× on a 32×32 4-MB I-frame CABAC encode (got {r})",
);
}
#[test]
fn encoder_with_none_stego_hook_byte_identical() {
let yuv = deterministic_yuv(32, 32, 5);
let cfg = BaselineTranscodeConfig::defaults(32, 32, 5);
let baseline = transcode_yuv_to_baseline_cavlc_h264(&yuv, cfg).unwrap();
let mut enc2 = Encoder::new(32, 32, Some(26)).unwrap();
enc2.entropy_mode = EntropyMode::Cavlc;
enc2.set_gop_length(30);
enc2.set_stego_hook(None);
let frame_size = (32 * 32 * 3 / 2) as usize;
let mut concat = Vec::new();
for f in 0..5 {
let frame = &yuv[f * frame_size..(f + 1) * frame_size];
let bytes = if f == 0 {
enc2.encode_i_frame(frame).unwrap()
} else {
enc2.encode_p_frame(frame).unwrap()
};
concat.extend_from_slice(&bytes);
}
assert_eq!(
concat, baseline,
"encoder with set_stego_hook(None) MUST be byte-identical",
);
assert!(enc2.take_stego_hook().is_none());
}
#[test]
fn streaming_encoder_idr_period_respected() {
let mut streaming = StreamingEncoder::new(32, 32, Some(26), 2).unwrap();
let frame_size = (32 * 32 * 3 / 2) as usize;
let yuv = deterministic_yuv(32, 32, 5);
for fi in 0..5 {
let frame = &yuv[fi * frame_size..(fi + 1) * frame_size];
let _ = streaming.push_frame(frame).unwrap();
}
assert_eq!(streaming.frames_emitted(), 5);
}
}