use crate::codec::h264::cabac::context::CabacInitSlot;
use crate::stego::error::StegoError;
use super::encoder_hook::{InjectAndLogHook, InjectionHook, PositionLoggerHook};
use super::keys::CabacStegoMasterKeys;
use super::orchestrate::{
pass2_stc_plan_with_keys, split_message_per_domain, DomainPlan,
};
use super::PositionKey;
use super::{BitInjector, GopCapacity};
use crate::codec::h264::cabac::bin_decoder::{
walk_annex_b_for_cover_with_options, WalkOptions,
};
pub fn h264_stego_encode_i_frames_only(
yuv: &[u8],
width: u32,
height: u32,
n_frames: usize,
message: &[u8],
passphrase: &str,
h: usize,
quality: Option<u8>,
) -> Result<Vec<u8>, StegoError> {
if !width.is_multiple_of(16) || !height.is_multiple_of(16) {
return Err(StegoError::InvalidVideo(format!(
"dimensions must be 16-aligned, got {width}x{height}"
)));
}
let frame_size = (width * height * 3 / 2) as usize;
if yuv.len() != frame_size * n_frames {
return Err(StegoError::InvalidVideo(format!(
"yuv len {} != expected {} ({}×{}×{})",
yuv.len(), frame_size * n_frames, frame_size, n_frames, "3/2"
)));
}
if !(1..=7).contains(&h) {
return Err(StegoError::InvalidVideo(format!("STC h must be 1..=7, got {h}")));
}
let keys = CabacStegoMasterKeys::derive(passphrase)?;
let cover = pass1_count(yuv, width, height, n_frames, frame_size, quality)?;
let capacity = cover.cover.capacity();
let messages = split_message_per_domain(message, &capacity)
.ok_or(StegoError::MessageTooLarge)?;
let plan = pass2_stc_plan_with_keys(&cover, &messages, h, &keys, 0)
.ok_or_else(|| StegoError::InvalidVideo(
"STC plan failed (per-domain cover smaller than message slice)".into()
))?;
pass3_inject(yuv, width, height, n_frames, frame_size, quality, &cover.cover, &plan)
}
pub(crate) fn pass1_count(
yuv: &[u8],
width: u32,
height: u32,
n_frames: usize,
frame_size: usize,
quality: Option<u8>,
) -> Result<super::orchestrate::GopCover, StegoError> {
pass1_count_with_mode(yuv, width, height, n_frames, frame_size, quality,
true)
}
fn pass1_count_with_mode(
yuv: &[u8],
width: u32,
height: u32,
n_frames: usize,
frame_size: usize,
quality: Option<u8>,
all_idr: bool,
) -> Result<super::orchestrate::GopCover, StegoError> {
let mut enc = build_encoder(width, height, quality)?;
enc.set_stego_hook(Some(Box::new(PositionLoggerHook::new())));
for fi in 0..n_frames {
let frame = &yuv[fi * frame_size..(fi + 1) * frame_size];
let ft = if all_idr || fi == 0 {
super::gop_pattern::FrameType::Idr
} else {
super::gop_pattern::FrameType::P
};
encode_one_frame(&mut enc, frame, ft)
.map_err(|e| StegoError::InvalidVideo(format!("Pass 1 frame {fi}: {e}")))?;
}
let mut hook = enc.take_stego_hook().ok_or_else(|| StegoError::InvalidVideo(
"Pass 1 stego hook missing".into()
))?;
let cover = drain_position_logger(&mut hook)?;
Ok(cover)
}
fn pass3_inject(
yuv: &[u8],
width: u32,
height: u32,
n_frames: usize,
frame_size: usize,
quality: Option<u8>,
cover: &super::DomainCover,
plan: &DomainPlan,
) -> Result<Vec<u8>, StegoError> {
pass3_inject_with_mode(yuv, width, height, n_frames, frame_size, quality,
cover, plan, true)
}
#[allow(clippy::too_many_arguments)]
fn pass3_inject_with_mode(
yuv: &[u8],
width: u32,
height: u32,
n_frames: usize,
frame_size: usize,
quality: Option<u8>,
cover: &super::DomainCover,
plan: &DomainPlan,
all_idr: bool,
) -> Result<Vec<u8>, StegoError> {
let injector = PlanInjector::from_plan(cover, plan);
let hook = InjectionHook::new(injector);
let mut enc = build_encoder(width, height, quality)?;
enc.set_stego_hook(Some(Box::new(hook)));
let mut out = Vec::new();
for fi in 0..n_frames {
let frame = &yuv[fi * frame_size..(fi + 1) * frame_size];
let ft = if all_idr || fi == 0 {
super::gop_pattern::FrameType::Idr
} else {
super::gop_pattern::FrameType::P
};
let bytes = encode_one_frame(&mut enc, frame, ft)
.map_err(|e| StegoError::InvalidVideo(format!("Pass 3 frame {fi}: {e}")))?;
out.extend_from_slice(&bytes);
}
Ok(out)
}
fn encode_one_frame(
enc: &mut super::super::encoder::encoder::Encoder,
pixels: &[u8],
frame_type: super::gop_pattern::FrameType,
) -> Result<Vec<u8>, super::super::encoder::EncoderError> {
use super::gop_pattern::FrameType;
match frame_type {
FrameType::Idr => enc.encode_i_frame(pixels),
FrameType::P => enc.encode_p_frame(pixels),
FrameType::B => enc.encode_b_frame(pixels),
}
}
fn build_encoder(
width: u32,
height: u32,
quality: Option<u8>,
) -> Result<super::super::encoder::encoder::Encoder, StegoError> {
use super::super::encoder::encoder::{Encoder, EntropyMode};
let mut enc = Encoder::new(width, height, quality)
.map_err(|e| StegoError::InvalidVideo(format!("encoder new: {e}")))?;
enc.entropy_mode = EntropyMode::Cabac;
enc.enable_transform_8x8 = true;
let _ = CabacInitSlot::ISI;
Ok(enc)
}
fn drain_position_logger(
hook: &mut Box<dyn super::encoder_hook::StegoMbHook>,
) -> Result<super::orchestrate::GopCover, StegoError> {
let cover = hook.take_cover_if_logger()
.ok_or_else(|| StegoError::InvalidVideo(
"Pass 1 hook was not a PositionLoggerHook".into()
))?;
Ok(cover)
}
pub struct PlanInjector {
plan: std::collections::HashMap<PositionKey, u8>,
}
impl PlanInjector {
pub fn from_plan(cover: &super::DomainCover, plan: &DomainPlan) -> Self {
let mut map = std::collections::HashMap::new();
Self::extend(&mut map, &cover.coeff_sign_bypass.positions, &plan.coeff_sign_bypass);
Self::extend(&mut map, &cover.coeff_suffix_lsb.positions, &plan.coeff_suffix_lsb);
Self::extend(&mut map, &cover.mvd_sign_bypass.positions, &plan.mvd_sign_bypass);
Self::extend(&mut map, &cover.mvd_suffix_lsb.positions, &plan.mvd_suffix_lsb);
Self { plan: map }
}
fn extend(
map: &mut std::collections::HashMap<PositionKey, u8>,
positions: &[PositionKey],
bits: &[u8],
) {
let n = positions.len().min(bits.len());
for i in 0..n {
map.insert(positions[i], bits[i]);
}
}
}
impl BitInjector for PlanInjector {
fn override_bit(&mut self, key: PositionKey) -> Option<u8> {
self.plan.get(&key).copied()
}
}
#[allow(dead_code)]
fn _docs_only() -> GopCapacity {
GopCapacity::default()
}
pub fn h264_stego_encode_yuv_string(
yuv: &[u8],
width: u32,
height: u32,
n_frames: usize,
message: &str,
passphrase: &str,
) -> Result<Vec<u8>, StegoError> {
use crate::stego::{crypto, frame, payload};
let payload_bytes = payload::encode_payload(message, &[])?;
let (ciphertext, nonce, salt) = crypto::encrypt(&payload_bytes, passphrase)?;
let frame_bytes = frame::build_frame(payload_bytes.len(), &salt, &nonce, &ciphertext);
let frame_bits: Vec<u8> = frame_bytes
.iter()
.flat_map(|&byte| (0..8).rev().map(move |i| (byte >> i) & 1))
.collect();
h264_stego_encode_i_frames_only(
yuv, width, height, n_frames,
&frame_bits, passphrase,
4, Some(26),
)
}
pub fn h264_stego_encode_yuv_string_i_then_p(
yuv: &[u8],
width: u32,
height: u32,
n_frames: usize,
message: &str,
passphrase: &str,
) -> Result<Vec<u8>, StegoError> {
use crate::stego::{crypto, frame, payload};
let payload_bytes = payload::encode_payload(message, &[])?;
let (ciphertext, nonce, salt) = crypto::encrypt(&payload_bytes, passphrase)?;
let frame_bytes = frame::build_frame(payload_bytes.len(), &salt, &nonce, &ciphertext);
let frame_bits: Vec<u8> = frame_bytes
.iter()
.flat_map(|&byte| (0..8).rev().map(move |i| (byte >> i) & 1))
.collect();
h264_stego_encode_i_then_p_frames(
yuv, width, height, n_frames,
&frame_bits, passphrase,
4, Some(26),
)
}
pub fn h264_stego_encode_i_then_p_frames(
yuv: &[u8],
width: u32,
height: u32,
n_frames: usize,
message: &[u8],
passphrase: &str,
h: usize,
quality: Option<u8>,
) -> Result<Vec<u8>, StegoError> {
if !width.is_multiple_of(16) || !height.is_multiple_of(16) {
return Err(StegoError::InvalidVideo(format!(
"dimensions must be 16-aligned, got {width}x{height}"
)));
}
let frame_size = (width * height * 3 / 2) as usize;
if yuv.len() != frame_size * n_frames {
return Err(StegoError::InvalidVideo(format!(
"yuv len {} != expected {}", yuv.len(), frame_size * n_frames
)));
}
if !(1..=7).contains(&h) {
return Err(StegoError::InvalidVideo(format!("STC h must be 1..=7, got {h}")));
}
let keys = CabacStegoMasterKeys::derive(passphrase)?;
let cover = pass1_count_with_mode(
yuv, width, height, n_frames, frame_size, quality,
false,
)?;
let capacity = cover.cover.capacity();
let messages = split_message_per_domain(message, &capacity)
.ok_or(StegoError::MessageTooLarge)?;
let plan = pass2_stc_plan_with_keys(&cover, &messages, h, &keys, 0)
.ok_or_else(|| StegoError::InvalidVideo(
"STC plan failed (per-domain cover smaller than message slice)".into()
))?;
pass3_inject_with_mode(
yuv, width, height, n_frames, frame_size, quality,
&cover.cover, &plan, false,
)
}
pub fn h264_stego_encode_yuv_string_4domain(
yuv: &[u8],
width: u32,
height: u32,
n_frames: usize,
message: &str,
passphrase: &str,
) -> Result<Vec<u8>, StegoError> {
h264_stego_encode_yuv_string_4domain_multigop(
yuv, width, height, n_frames,
n_frames,
message, passphrase,
)
}
pub fn h264_stego_encode_yuv_string_4domain_multigop(
yuv: &[u8],
width: u32,
height: u32,
n_frames: usize,
gop_size: usize,
message: &str,
passphrase: &str,
) -> Result<Vec<u8>, StegoError> {
use crate::stego::{crypto, frame, payload};
if gop_size == 0 {
return Err(StegoError::InvalidVideo("gop_size must be > 0".into()));
}
if gop_size > n_frames {
return Err(StegoError::InvalidVideo(format!(
"gop_size ({gop_size}) > n_frames ({n_frames})"
)));
}
let payload_bytes = payload::encode_payload(message, &[])?;
let (ciphertext, nonce, salt) = crypto::encrypt(&payload_bytes, passphrase)?;
let frame_bytes = frame::build_frame(payload_bytes.len(), &salt, &nonce, &ciphertext);
let frame_bits: Vec<u8> = frame_bytes
.iter()
.flat_map(|&byte| (0..8).rev().map(move |i| (byte >> i) & 1))
.collect();
h264_stego_encode_i_then_p_frames_4domain_multigop(
yuv, width, height, n_frames, gop_size,
1, &frame_bits, passphrase,
4, Some(26),
)
}
pub fn h264_stego_encode_yuv_string_4domain_multigop_with_pattern(
yuv: &[u8],
width: u32,
height: u32,
n_frames: usize,
pattern: super::gop_pattern::GopPattern,
message: &str,
passphrase: &str,
) -> Result<Vec<u8>, StegoError> {
use crate::stego::{crypto, frame, payload};
use super::gop_pattern::GopPattern;
let gop_size = pattern.gop_size();
let b_count = match pattern {
GopPattern::Ipppp { .. } => 0,
GopPattern::Ibpbp { b_count, .. } => b_count,
};
if gop_size == 0 {
return Err(StegoError::InvalidVideo("gop_size must be > 0".into()));
}
if gop_size > n_frames {
return Err(StegoError::InvalidVideo(format!(
"gop_size ({gop_size}) > n_frames ({n_frames})"
)));
}
let payload_bytes = payload::encode_payload(message, &[])?;
let (ciphertext, nonce, salt) = crypto::encrypt(&payload_bytes, passphrase)?;
let frame_bytes = frame::build_frame(payload_bytes.len(), &salt, &nonce, &ciphertext);
let frame_bits: Vec<u8> = frame_bytes
.iter()
.flat_map(|&byte| (0..8).rev().map(move |i| (byte >> i) & 1))
.collect();
h264_stego_encode_i_then_p_frames_4domain_multigop(
yuv, width, height, n_frames, gop_size, b_count,
&frame_bits, passphrase,
4, Some(26),
)
}
pub fn h264_stego_encode_i_then_p_frames_4domain(
yuv: &[u8],
width: u32,
height: u32,
n_frames: usize,
message: &[u8],
passphrase: &str,
h: usize,
quality: Option<u8>,
) -> Result<Vec<u8>, StegoError> {
h264_stego_encode_i_then_p_frames_4domain_multigop(
yuv, width, height, n_frames,
n_frames,
1,
message, passphrase, h, quality,
)
}
pub fn h264_stego_encode_yuv_string_4domain_multigop_streaming(
yuv: &[u8],
width: u32,
height: u32,
n_frames: usize,
gop_size: usize,
message: &str,
passphrase: &str,
) -> Result<Vec<u8>, StegoError> {
use crate::stego::{crypto, frame, payload};
use crate::stego::stc::streaming_segmented::{
stc_embed_streaming_segmented,
};
use super::cover_replay::H264GopReplayCover;
use super::hook::EmbedDomain;
use super::inject::DomainCover;
use super::orchestrate::{
stealth_weighted_allocation, split_message_per_domain, DomainPlan,
PlanInjector, StealthAllocator,
};
use super::encoder_hook::InjectionHook;
use super::keys::CabacStegoMasterKeys;
use super::gop_pattern::GopPattern;
if !width.is_multiple_of(16) || !height.is_multiple_of(16) {
return Err(StegoError::InvalidVideo(format!(
"dimensions must be 16-aligned, got {width}x{height}"
)));
}
let frame_size = (width * height * 3 / 2) as usize;
if yuv.len() != frame_size * n_frames {
return Err(StegoError::InvalidVideo(format!(
"yuv len {} != expected {}",
yuv.len(),
frame_size * n_frames,
)));
}
if gop_size == 0 || gop_size > n_frames {
return Err(StegoError::InvalidVideo(format!(
"gop_size {gop_size} must be in 1..={n_frames}"
)));
}
let b_count = 1usize; let h = 4usize;
let quality = Some(26u8);
let payload_bytes = payload::encode_payload(message, &[])?;
let (ct, nonce, salt) = crypto::encrypt(&payload_bytes, passphrase)?;
let frame_bytes = frame::build_frame(payload_bytes.len(), &salt, &nonce, &ct);
let frame_bits: Vec<u8> = frame_bytes
.iter()
.flat_map(|&byte| (0..8).rev().map(move |i| (byte >> i) & 1))
.collect();
let m_total = frame_bits.len();
let keys = CabacStegoMasterKeys::derive(passphrase)?;
let per_gop_counts = pass1_count_per_gop_4domain(
yuv, width, height, n_frames, gop_size, b_count, quality,
)?;
let mut totals = [0usize; 4];
for row in per_gop_counts.iter() {
for d in 0..4 {
totals[d] += row[d];
}
}
let cap_p1 = GopCapacity {
coeff_sign_bypass: totals[0],
coeff_suffix_lsb: totals[1],
mvd_sign_bypass: totals[2],
mvd_suffix_lsb: totals[3],
};
let allocator = StealthAllocator::v1_default();
let cap_for_alloc = GopCapacity {
coeff_sign_bypass: cap_p1.coeff_sign_bypass,
coeff_suffix_lsb: cap_p1.coeff_suffix_lsb,
mvd_sign_bypass: cap_p1.mvd_sign_bypass,
mvd_suffix_lsb: 0,
};
let (m_cs, m_cl, m_ms, _m_ml) =
stealth_weighted_allocation(m_total, &cap_for_alloc, &allocator)
.ok_or(StegoError::MessageTooLarge)?;
let m_mvd = m_ms;
let m_residual = m_cs + m_cl;
let mut plan_a = DomainPlan::default();
let mut plan_b = DomainPlan::default();
if m_mvd > 0 {
let cap_mvd_only = GopCapacity {
coeff_sign_bypass: 0,
coeff_suffix_lsb: 0,
mvd_sign_bypass: cap_p1.mvd_sign_bypass,
mvd_suffix_lsb: 0,
};
let messages_a = split_message_per_domain(&frame_bits[..m_mvd], &cap_mvd_only)
.ok_or_else(|| StegoError::InvalidVideo(
"Pass 2A streaming: MVD split failed".into()
))?;
let m_dom = messages_a.mvd_sign_bypass.len();
let n_dom = cap_p1.mvd_sign_bypass;
if m_dom > 0 && n_dom > 0 {
let w = n_dom / m_dom.max(1);
if w > 0 {
let k = ((m_dom as f64).sqrt().ceil() as usize).max(1);
let mut cover = H264GopReplayCover::from_counts(
yuv, width, height, n_frames, gop_size, b_count, quality,
EmbedDomain::MvdSignBypass,
&per_gop_counts, m_dom, w, k,
)?;
let seed = keys.per_gop_seeds(EmbedDomain::MvdSignBypass, 0).hhat_seed;
let hhat = crate::stego::stc::hhat::generate_hhat(h, w, &seed);
let result = stc_embed_streaming_segmented(
&mut cover, &messages_a.mvd_sign_bypass, &hhat, h, w,
).map_err(|e| StegoError::InvalidVideo(format!(
"Pass 2A streaming-Viterbi: {e}"
)))?;
plan_a.mvd_sign_bypass = result.stego_bits;
plan_a.total_modifications += result.num_modifications;
plan_a.total_cost += result.total_cost;
}
}
}
if m_residual > 0 {
let cap_coeff_only = GopCapacity {
coeff_sign_bypass: cap_p1.coeff_sign_bypass,
coeff_suffix_lsb: cap_p1.coeff_suffix_lsb,
mvd_sign_bypass: 0,
mvd_suffix_lsb: 0,
};
let messages_b = split_message_per_domain(&frame_bits[m_mvd..], &cap_coeff_only)
.ok_or_else(|| StegoError::InvalidVideo(
"Pass 2B streaming: residual split failed".into()
))?;
for (domain, msg, n_dom, plan_slot) in [
(EmbedDomain::CoeffSignBypass, &messages_b.coeff_sign_bypass,
cap_p1.coeff_sign_bypass, &mut plan_b.coeff_sign_bypass),
(EmbedDomain::CoeffSuffixLsb, &messages_b.coeff_suffix_lsb,
cap_p1.coeff_suffix_lsb, &mut plan_b.coeff_suffix_lsb),
] {
let m_dom = msg.len();
if m_dom == 0 || n_dom == 0 {
continue;
}
let w = n_dom / m_dom.max(1);
if w == 0 {
continue;
}
let k = ((m_dom as f64).sqrt().ceil() as usize).max(1);
let mut cover = H264GopReplayCover::from_counts(
yuv, width, height, n_frames, gop_size, b_count, quality,
domain, &per_gop_counts, m_dom, w, k,
)?;
let seed = keys.per_gop_seeds(domain, 0).hhat_seed;
let hhat = crate::stego::stc::hhat::generate_hhat(h, w, &seed);
let result = stc_embed_streaming_segmented(
&mut cover, msg, &hhat, h, w,
).map_err(|e| StegoError::InvalidVideo(format!(
"Pass 2B streaming-Viterbi ({domain:?}): {e}"
)))?;
*plan_slot = result.stego_bits;
plan_b.total_modifications += result.num_modifications;
plan_b.total_cost += result.total_cost;
}
}
let combined_plan = DomainPlan {
coeff_sign_bypass: plan_b.coeff_sign_bypass.clone(),
coeff_suffix_lsb: plan_b.coeff_suffix_lsb.clone(),
mvd_sign_bypass: plan_a.mvd_sign_bypass.clone(),
mvd_suffix_lsb: Vec::new(),
total_modifications: plan_a.total_modifications + plan_b.total_modifications,
total_cost: plan_a.total_cost + plan_b.total_cost,
};
let cum: [Vec<usize>; 4] = std::array::from_fn(|d| {
let mut v = Vec::with_capacity(per_gop_counts.len() + 1);
v.push(0);
for row in per_gop_counts.iter() {
v.push(*v.last().unwrap() + row[d]);
}
v
});
let pattern = GopPattern::Ibpbp { gop: gop_size, b_count };
let num_gops = per_gop_counts.len();
let mut out = Vec::new();
for g in 0..num_gops {
let gop_cover = pass1_capture_4domain_for_gop_range(
yuv, width, height, n_frames, gop_size, b_count, quality, g, g + 1,
)?;
let mut gop_plan = DomainPlan::default();
for d in 0..4 {
let lo = cum[d][g];
let hi = cum[d][g + 1];
let src = match d {
0 => &combined_plan.coeff_sign_bypass,
1 => &combined_plan.coeff_suffix_lsb,
2 => &combined_plan.mvd_sign_bypass,
_ => &combined_plan.mvd_suffix_lsb,
};
let dst = match d {
0 => &mut gop_plan.coeff_sign_bypass,
1 => &mut gop_plan.coeff_suffix_lsb,
2 => &mut gop_plan.mvd_sign_bypass,
_ => &mut gop_plan.mvd_suffix_lsb,
};
if src.is_empty() {
continue;
}
let lo = lo.min(src.len());
let hi = hi.min(src.len());
*dst = src[lo..hi].to_vec();
}
let mut gop_cover_only = DomainCover::default();
gop_cover_only.coeff_sign_bypass = gop_cover.cover.coeff_sign_bypass.clone();
gop_cover_only.coeff_suffix_lsb = gop_cover.cover.coeff_suffix_lsb.clone();
gop_cover_only.mvd_sign_bypass = gop_cover.cover.mvd_sign_bypass.clone();
gop_cover_only.mvd_suffix_lsb = gop_cover.cover.mvd_suffix_lsb.clone();
let injector = PlanInjector::from_plan(&gop_cover_only, &gop_plan);
let mut enc = build_encoder(width, height, quality)?;
enc.enable_mvd_stego_hook = true;
enc.enable_b_frames = pattern.has_b_frames();
enc.set_stego_hook(Some(Box::new(InjectionHook::new(injector))));
let mut primed = false;
for (meta, frame) in iter_frames_in_encode_order(
yuv, frame_size, n_frames, pattern,
) {
if (meta.gop_idx as usize) != g {
if (meta.gop_idx as usize) > g {
break;
}
continue;
}
if !primed {
enc.stego_frame_idx = meta.encode_idx;
primed = true;
}
let bytes = encode_one_frame(&mut enc, frame, meta.frame_type)
.map_err(|e| StegoError::InvalidVideo(format!(
"Pass 3 streaming GOP {g} frame {}: {e}",
meta.encode_idx,
)))?;
out.extend_from_slice(&bytes);
}
}
Ok(out)
}
pub fn h264_stego_encode_yuv_string_4domain_multigop_streaming_v2(
yuv: &[u8],
width: u32,
height: u32,
n_frames: usize,
gop_size: usize,
message: &str,
passphrase: &str,
) -> Result<Vec<u8>, StegoError> {
h264_stego_encode_yuv_string_4domain_multigop_streaming_v2_with_files(
yuv, width, height, n_frames, gop_size, message, &[], passphrase,
)
}
#[allow(clippy::too_many_arguments)]
pub fn h264_stego_encode_yuv_string_4domain_multigop_streaming_v2_with_files(
yuv: &[u8],
width: u32,
height: u32,
n_frames: usize,
gop_size: usize,
message: &str,
files: &[crate::stego::payload::FileEntry],
passphrase: &str,
) -> Result<Vec<u8>, StegoError> {
use crate::stego::{crypto, frame, payload};
use crate::stego::stc::streaming_segmented::StreamingViterbiPhaseB;
use super::cover_replay::H264GopReplayCover;
use super::hook::EmbedDomain;
use super::inject::DomainCover;
use super::orchestrate::{
stealth_weighted_allocation, split_message_per_domain, DomainPlan,
PlanInjector, StealthAllocator,
};
use super::encoder_hook::InjectionHook;
use super::keys::CabacStegoMasterKeys;
use super::gop_pattern::GopPattern;
use super::per_gop_plan::PerGopPlanBuilder;
if !width.is_multiple_of(16) || !height.is_multiple_of(16) {
return Err(StegoError::InvalidVideo(format!(
"dimensions must be 16-aligned, got {width}x{height}"
)));
}
let frame_size = (width * height * 3 / 2) as usize;
if yuv.len() != frame_size * n_frames {
return Err(StegoError::InvalidVideo(format!(
"yuv len {} != expected {}",
yuv.len(),
frame_size * n_frames,
)));
}
if gop_size == 0 || gop_size > n_frames {
return Err(StegoError::InvalidVideo(format!(
"gop_size {gop_size} must be in 1..={n_frames}"
)));
}
let b_count = 1usize;
let h = 4usize;
let quality = Some(26u8);
let payload_bytes = payload::encode_payload(message, files)?;
let (ct, nonce, salt) = crypto::encrypt(&payload_bytes, passphrase)?;
let frame_bytes = frame::build_frame(payload_bytes.len(), &salt, &nonce, &ct);
let frame_bits: Vec<u8> = frame_bytes
.iter()
.flat_map(|&byte| (0..8).rev().map(move |i| (byte >> i) & 1))
.collect();
let m_total = frame_bits.len();
let keys = CabacStegoMasterKeys::derive(passphrase)?;
let per_gop_counts = pass1_count_per_gop_4domain(
yuv, width, height, n_frames, gop_size, b_count, quality,
)?;
let mut totals = [0usize; 4];
for row in per_gop_counts.iter() {
for d in 0..4 {
totals[d] += row[d];
}
}
let cap_p1 = GopCapacity {
coeff_sign_bypass: totals[0],
coeff_suffix_lsb: totals[1],
mvd_sign_bypass: totals[2],
mvd_suffix_lsb: totals[3],
};
let allocator = StealthAllocator::v1_default();
let cap_for_alloc = GopCapacity {
coeff_sign_bypass: cap_p1.coeff_sign_bypass,
coeff_suffix_lsb: cap_p1.coeff_suffix_lsb,
mvd_sign_bypass: cap_p1.mvd_sign_bypass,
mvd_suffix_lsb: 0,
};
let (m_cs, m_cl, m_ms, _m_ml) =
stealth_weighted_allocation(m_total, &cap_for_alloc, &allocator)
.ok_or(StegoError::MessageTooLarge)?;
let m_mvd = m_ms;
let m_residual = m_cs + m_cl;
let messages_a = if m_mvd > 0 {
let cap_mvd_only = GopCapacity {
coeff_sign_bypass: 0,
coeff_suffix_lsb: 0,
mvd_sign_bypass: cap_p1.mvd_sign_bypass,
mvd_suffix_lsb: 0,
};
Some(
split_message_per_domain(&frame_bits[..m_mvd], &cap_mvd_only)
.ok_or_else(|| {
StegoError::InvalidVideo("v2: MVD split failed".into())
})?,
)
} else {
None
};
let messages_b = if m_residual > 0 {
let cap_coeff_only = GopCapacity {
coeff_sign_bypass: cap_p1.coeff_sign_bypass,
coeff_suffix_lsb: cap_p1.coeff_suffix_lsb,
mvd_sign_bypass: 0,
mvd_suffix_lsb: 0,
};
Some(
split_message_per_domain(&frame_bits[m_mvd..], &cap_coeff_only)
.ok_or_else(|| {
StegoError::InvalidVideo("v2: residual split failed".into())
})?,
)
} else {
None
};
struct DomainOwned {
domain: EmbedDomain,
message: Vec<u8>,
w: usize,
hhat: Vec<Vec<u32>>,
}
let mut owned: [Option<DomainOwned>; 4] = [None, None, None, None];
let mut k_per_domain: [usize; 4] = [0; 4];
let domain_table: [(EmbedDomain, usize, Option<&Vec<u8>>); 4] = [
(
EmbedDomain::CoeffSignBypass,
cap_p1.coeff_sign_bypass,
messages_b.as_ref().map(|mb| &mb.coeff_sign_bypass),
),
(
EmbedDomain::CoeffSuffixLsb,
cap_p1.coeff_suffix_lsb,
messages_b.as_ref().map(|mb| &mb.coeff_suffix_lsb),
),
(
EmbedDomain::MvdSignBypass,
cap_p1.mvd_sign_bypass,
messages_a.as_ref().map(|ma| &ma.mvd_sign_bypass),
),
(EmbedDomain::MvdSuffixLsb, 0, None),
];
for (d, (domain, n_dom, msg_opt)) in domain_table.iter().enumerate() {
let Some(msg) = msg_opt else {
continue;
};
let m_dom = msg.len();
if m_dom == 0 || *n_dom == 0 {
continue;
}
let w = *n_dom / m_dom.max(1);
if w == 0 {
continue;
}
let k = ((m_dom as f64).sqrt().ceil() as usize).max(1);
let seed = keys.per_gop_seeds(*domain, 0).hhat_seed;
let hhat = crate::stego::stc::hhat::generate_hhat(h, w, &seed);
owned[d] = Some(DomainOwned {
domain: *domain,
message: (*msg).clone(),
w,
hhat,
});
k_per_domain[d] = k;
}
let active_domains: [bool; 4] = [
owned[0].is_some(),
owned[1].is_some(),
owned[2].is_some(),
owned[3].is_some(),
];
let mut cover0: Option<H264GopReplayCover> = None;
let mut cover1: Option<H264GopReplayCover> = None;
let mut cover2: Option<H264GopReplayCover> = None;
if let Some(o) = &owned[0] {
cover0 = Some(H264GopReplayCover::from_counts(
yuv, width, height, n_frames, gop_size, b_count, quality,
o.domain, &per_gop_counts, o.message.len(), o.w, k_per_domain[0],
)?);
}
if let Some(o) = &owned[1] {
cover1 = Some(H264GopReplayCover::from_counts(
yuv, width, height, n_frames, gop_size, b_count, quality,
o.domain, &per_gop_counts, o.message.len(), o.w, k_per_domain[1],
)?);
}
if let Some(o) = &owned[2] {
cover2 = Some(H264GopReplayCover::from_counts(
yuv, width, height, n_frames, gop_size, b_count, quality,
o.domain, &per_gop_counts, o.message.len(), o.w, k_per_domain[2],
)?);
}
let mut driver0: Option<StreamingViterbiPhaseB> = None;
let mut driver1: Option<StreamingViterbiPhaseB> = None;
let mut driver2: Option<StreamingViterbiPhaseB> = None;
if let (Some(o), Some(c)) = (&owned[0], cover0.as_mut()) {
driver0 = Some(
StreamingViterbiPhaseB::new(c, &o.message, &o.hhat, h, o.w)
.map_err(|e| StegoError::InvalidVideo(format!(
"v2 Phase A driver[0]: {e}"
)))?,
);
}
if let (Some(o), Some(c)) = (&owned[1], cover1.as_mut()) {
driver1 = Some(
StreamingViterbiPhaseB::new(c, &o.message, &o.hhat, h, o.w)
.map_err(|e| StegoError::InvalidVideo(format!(
"v2 Phase A driver[1]: {e}"
)))?,
);
}
if let (Some(o), Some(c)) = (&owned[2], cover2.as_mut()) {
driver2 = Some(
StreamingViterbiPhaseB::new(c, &o.message, &o.hhat, h, o.w)
.map_err(|e| StegoError::InvalidVideo(format!(
"v2 Phase A driver[2]: {e}"
)))?,
);
}
let num_gops = per_gop_counts.len();
let cum_raw: [Vec<usize>; 4] = std::array::from_fn(|d| {
let mut v = Vec::with_capacity(num_gops + 1);
v.push(0);
for row in per_gop_counts.iter() {
v.push(*v.last().unwrap() + row[d]);
}
v
});
let m_w_per_domain: [usize; 4] = [
owned[0].as_ref().map(|o| o.message.len() * o.w).unwrap_or(0),
owned[1].as_ref().map(|o| o.message.len() * o.w).unwrap_or(0),
owned[2].as_ref().map(|o| o.message.len() * o.w).unwrap_or(0),
0,
];
let effective_counts: Vec<[usize; 4]> = (0..num_gops)
.map(|g| {
std::array::from_fn(|d| {
if !active_domains[d] {
return 0;
}
let lo = cum_raw[d][g].min(m_w_per_domain[d]);
let hi = cum_raw[d][g + 1].min(m_w_per_domain[d]);
hi - lo
})
})
.collect();
let mut builder = PerGopPlanBuilder::new(&effective_counts, active_domains);
debug_assert_eq!(builder.num_gops(), num_gops);
let pattern = GopPattern::Ibpbp { gop: gop_size, b_count };
let mut gop_bytes: Vec<Option<Vec<u8>>> = vec![None; num_gops];
loop {
let alive = [
driver0.is_some(),
driver1.is_some(),
driver2.is_some(),
false,
];
if !alive.iter().any(|&a| a) {
break;
}
let mut best_d: Option<usize> = None;
let mut best_g: Option<usize> = None;
let mut fallback_d: Option<usize> = None;
for d in 0..4 {
if !alive[d] {
continue;
}
fallback_d.get_or_insert(d);
if let Some(g) = builder.highest_pending_gop(d) {
match best_g {
None => {
best_g = Some(g);
best_d = Some(d);
}
Some(cur) if g > cur => {
best_g = Some(g);
best_d = Some(d);
}
_ => {}
}
}
}
let step_d = best_d.or(fallback_d).expect("alive checked above");
let emission = match step_d {
0 => driver0
.as_mut()
.unwrap()
.step()
.map_err(|e| StegoError::InvalidVideo(format!(
"v2 Phase B driver[0]: {e}"
)))?,
1 => driver1
.as_mut()
.unwrap()
.step()
.map_err(|e| StegoError::InvalidVideo(format!(
"v2 Phase B driver[1]: {e}"
)))?,
2 => driver2
.as_mut()
.unwrap()
.step()
.map_err(|e| StegoError::InvalidVideo(format!(
"v2 Phase B driver[2]: {e}"
)))?,
_ => unreachable!("step_d in 0..3 (domain 3 always inactive)"),
};
match emission {
Some(em) => {
builder
.accept_emission(step_d, em.j_start, &em.stego_bits)
.map_err(|e| StegoError::InvalidVideo(format!(
"v2 accept_emission[{step_d}]: {e}"
)))?;
builder.add_modifications(em.num_modifications);
}
None => {
match step_d {
0 => driver0 = None,
1 => driver1 = None,
2 => driver2 = None,
_ => unreachable!(),
}
}
}
for ready in builder.take_ready_gops() {
let g = ready.gop_idx;
let gop_plan = DomainPlan {
coeff_sign_bypass: ready.plans[0].clone(),
coeff_suffix_lsb: ready.plans[1].clone(),
mvd_sign_bypass: ready.plans[2].clone(),
mvd_suffix_lsb: ready.plans[3].clone(),
total_modifications: 0,
total_cost: 0.0,
};
gop_bytes[g] = Some(encode_one_gop_with_plan_and_capture(
yuv, width, height, n_frames, frame_size, quality, pattern,
g, &gop_plan, None,
)?);
}
}
if !builder.all_fired() {
return Err(StegoError::InvalidVideo(format!(
"v2: {} GOPs not fired after lockstep loop",
num_gops - gop_bytes.iter().filter(|b| b.is_some()).count(),
)));
}
let mut out = Vec::new();
for g in 0..num_gops {
match gop_bytes[g].take() {
Some(b) => out.extend_from_slice(&b),
None => {
return Err(StegoError::InvalidVideo(format!(
"v2: GOP {g} did not fire"
)))
}
}
}
Ok(out)
}
#[allow(clippy::too_many_arguments)]
pub fn h264_stego_encode_i_then_p_frames_4domain_multigop(
yuv: &[u8],
width: u32,
height: u32,
n_frames: usize,
gop_size: usize,
b_count: usize,
message: &[u8],
passphrase: &str,
h: usize,
quality: Option<u8>,
) -> Result<Vec<u8>, StegoError> {
if !width.is_multiple_of(16) || !height.is_multiple_of(16) {
return Err(StegoError::InvalidVideo(format!(
"dimensions must be 16-aligned, got {width}x{height}"
)));
}
let frame_size = (width * height * 3 / 2) as usize;
if yuv.len() != frame_size * n_frames {
return Err(StegoError::InvalidVideo(format!(
"yuv len {} != expected {}", yuv.len(), frame_size * n_frames
)));
}
if !(1..=7).contains(&h) {
return Err(StegoError::InvalidVideo(format!("STC h must be 1..=7, got {h}")));
}
if gop_size == 0 {
return Err(StegoError::InvalidVideo("gop_size must be > 0".into()));
}
let keys = CabacStegoMasterKeys::derive(passphrase)?;
let m_total = message.len();
let cover_p1 = pass1_count_4domain(
yuv, width, height, n_frames, frame_size, quality, gop_size, b_count,
)?;
let cap_p1 = cover_p1.cover.capacity();
let allocator = super::orchestrate::StealthAllocator::v1_default();
let cap_for_alloc = GopCapacity {
coeff_sign_bypass: cap_p1.coeff_sign_bypass,
coeff_suffix_lsb: cap_p1.coeff_suffix_lsb,
mvd_sign_bypass: cap_p1.mvd_sign_bypass,
mvd_suffix_lsb: 0, };
let (m_cs, m_cl, m_ms, _m_ml) =
super::orchestrate::stealth_weighted_allocation(
m_total, &cap_for_alloc, &allocator,
).ok_or(StegoError::MessageTooLarge)?;
let m_mvd = m_ms; let m_residual = m_cs + m_cl;
debug_assert_eq!(m_total, m_mvd + m_residual,
"stealth-weighted allocation must conserve m_total");
let plan_a = if m_mvd > 0 {
let cap_mvd_only = GopCapacity {
coeff_sign_bypass: 0,
coeff_suffix_lsb: 0,
mvd_sign_bypass: cap_p1.mvd_sign_bypass,
mvd_suffix_lsb: 0,
};
let messages_a = split_message_per_domain(&message[..m_mvd], &cap_mvd_only)
.ok_or_else(|| StegoError::InvalidVideo(
"Pass 2A: MVD split failed".into()
))?;
let mut cover_for_a = super::orchestrate::GopCover::default();
cover_for_a.cover.mvd_sign_bypass = cover_p1.cover.mvd_sign_bypass.clone();
cover_for_a.costs.mvd_sign_bypass = cover_p1.costs.mvd_sign_bypass.clone();
pass2_stc_plan_with_keys(&cover_for_a, &messages_a, h, &keys, 0)
.ok_or_else(|| StegoError::InvalidVideo(
"Pass 2A: STC plan failed".into()
))?
} else {
DomainPlan::default()
};
let cover_p1b_residual = pass1b_inject_mvd_log_residual(
yuv, width, height, n_frames, frame_size, quality,
&cover_p1.cover, &plan_a, gop_size, b_count,
)?;
let cap_p1b = cover_p1b_residual.cover.capacity();
let residual_capacity = cap_p1b.coeff_sign_bypass + cap_p1b.coeff_suffix_lsb;
if m_residual > residual_capacity {
return Err(StegoError::MessageTooLarge);
}
let plan_b = if m_residual > 0 {
let cap_coeff_only = GopCapacity {
coeff_sign_bypass: cap_p1b.coeff_sign_bypass,
coeff_suffix_lsb: cap_p1b.coeff_suffix_lsb,
mvd_sign_bypass: 0,
mvd_suffix_lsb: 0,
};
let messages_b = split_message_per_domain(&message[m_mvd..], &cap_coeff_only)
.ok_or_else(|| StegoError::InvalidVideo(
"Pass 2B: residual split failed".into()
))?;
let mut cover_for_b = super::orchestrate::GopCover::default();
cover_for_b.cover.coeff_sign_bypass = cover_p1b_residual.cover.coeff_sign_bypass.clone();
cover_for_b.cover.coeff_suffix_lsb = cover_p1b_residual.cover.coeff_suffix_lsb.clone();
cover_for_b.costs.coeff_sign_bypass = cover_p1b_residual.costs.coeff_sign_bypass.clone();
cover_for_b.costs.coeff_suffix_lsb = cover_p1b_residual.costs.coeff_suffix_lsb.clone();
pass2_stc_plan_with_keys(&cover_for_b, &messages_b, h, &keys, 0)
.ok_or_else(|| StegoError::InvalidVideo(
"Pass 2B: STC plan failed".into()
))?
} else {
DomainPlan::default()
};
let mut combined_cover = super::DomainCover::default();
combined_cover.coeff_sign_bypass = cover_p1b_residual.cover.coeff_sign_bypass.clone();
combined_cover.coeff_suffix_lsb = cover_p1b_residual.cover.coeff_suffix_lsb.clone();
combined_cover.mvd_sign_bypass = cover_p1.cover.mvd_sign_bypass.clone();
combined_cover.mvd_suffix_lsb = cover_p1.cover.mvd_suffix_lsb.clone();
let mut combined_plan = DomainPlan::default();
combined_plan.coeff_sign_bypass = plan_b.coeff_sign_bypass.clone();
combined_plan.coeff_suffix_lsb = plan_b.coeff_suffix_lsb.clone();
combined_plan.mvd_sign_bypass = plan_a.mvd_sign_bypass.clone();
combined_plan.mvd_suffix_lsb = plan_a.mvd_suffix_lsb.clone();
pass3_inject_4domain(
yuv, width, height, n_frames, frame_size, quality,
&combined_cover, &combined_plan, gop_size, b_count,
)
}
pub struct H264ShadowCapacityInfo {
pub cover_size_bits: usize,
pub max_message_bytes: usize,
pub n_shadows: usize,
}
#[allow(clippy::too_many_arguments)]
pub fn h264_stego_shadow_capacity(
yuv: &[u8],
width: u32,
height: u32,
n_frames: usize,
gop_size: usize,
n_shadows: usize,
) -> Result<H264ShadowCapacityInfo, StegoError> {
use crate::stego::shadow_layer::SHADOW_FRAME_OVERHEAD_WIDE
as SHADOW_FRAME_OVERHEAD;
if !width.is_multiple_of(16) || !height.is_multiple_of(16) {
return Err(StegoError::InvalidVideo(format!(
"dimensions must be 16-aligned, got {width}x{height}"
)));
}
let frame_size = (width * height * 3 / 2) as usize;
if yuv.len() != frame_size * n_frames {
return Err(StegoError::InvalidVideo(format!(
"yuv len {} != expected {}", yuv.len(), frame_size * n_frames
)));
}
if gop_size == 0 || gop_size > n_frames {
return Err(StegoError::InvalidVideo(format!(
"gop_size {gop_size} must be in 1..={n_frames}"
)));
}
let cover_p1 = pass1_count_4domain(
yuv, width, height, n_frames, frame_size, Some(26), gop_size,
1,
)?;
let cap_p1 = cover_p1.cover.capacity();
let cover_size_bits =
cap_p1.coeff_sign_bypass + cap_p1.coeff_suffix_lsb + cap_p1.mvd_sign_bypass;
let max_message_bytes = if n_shadows == 0 {
0
} else {
let denom = n_shadows.saturating_sub(1).max(1);
let m_max_bits_squared = 1024usize.saturating_mul(cover_size_bits) / denom;
let m_max_bits = (m_max_bits_squared as f64).sqrt() as usize;
let m_max_bits = m_max_bits.min(cover_size_bits);
let m_max_bytes = m_max_bits / 8;
m_max_bytes
.saturating_sub(128)
.saturating_sub(SHADOW_FRAME_OVERHEAD)
};
Ok(H264ShadowCapacityInfo {
cover_size_bits,
max_message_bytes,
n_shadows,
})
}
#[derive(Debug, Clone, Copy)]
pub struct H264StegoCapacityInfo {
pub cover_size_bits: usize,
pub primary_max_message_bytes: usize,
pub shadow_max_message_bytes: usize,
}
#[allow(clippy::too_many_arguments)]
pub fn h264_stego_capacity_4domain(
yuv: &[u8],
width: u32,
height: u32,
n_frames: usize,
gop_size: usize,
) -> Result<H264StegoCapacityInfo, StegoError> {
use crate::stego::frame::FRAME_OVERHEAD;
if !width.is_multiple_of(16) || !height.is_multiple_of(16) {
return Err(StegoError::InvalidVideo(format!(
"dimensions must be 16-aligned, got {width}x{height}"
)));
}
let frame_size = (width * height * 3 / 2) as usize;
if yuv.len() != frame_size * n_frames {
return Err(StegoError::InvalidVideo(format!(
"yuv len {} != expected {}", yuv.len(), frame_size * n_frames
)));
}
if gop_size == 0 || gop_size > n_frames {
return Err(StegoError::InvalidVideo(format!(
"gop_size {gop_size} must be in 1..={n_frames}"
)));
}
let cover_p1 = pass1_count_4domain(
yuv, width, height, n_frames, frame_size, Some(26),
gop_size, 1,
)?;
let cap_p1 = cover_p1.cover.capacity();
let cover_size_bits =
cap_p1.coeff_sign_bypass + cap_p1.coeff_suffix_lsb + cap_p1.mvd_sign_bypass;
let primary_max_message_bytes =
(cover_size_bits / 8).saturating_sub(FRAME_OVERHEAD);
let shadow_info = h264_stego_shadow_capacity(
yuv, width, height, n_frames, gop_size, 1,
)?;
Ok(H264StegoCapacityInfo {
cover_size_bits,
primary_max_message_bytes,
shadow_max_message_bytes: shadow_info.max_message_bytes,
})
}
#[allow(clippy::too_many_arguments)]
pub fn h264_stego_encode_yuv_string_with_shadow(
yuv: &[u8],
width: u32,
height: u32,
n_frames: usize,
gop_size: usize,
primary_message: &str,
primary_passphrase: &str,
shadow_message: &str,
shadow_passphrase: &str,
) -> Result<Vec<u8>, StegoError> {
use crate::stego::shadow_layer::ShadowLayer;
let shadow = ShadowLayer {
message: shadow_message,
passphrase: shadow_passphrase,
files: &[],
};
h264_stego_encode_yuv_string_with_n_shadows(
yuv, width, height, n_frames, gop_size,
primary_message, primary_passphrase,
std::slice::from_ref(&shadow),
)
}
#[allow(clippy::too_many_arguments)]
pub fn h264_stego_encode_yuv_string_with_shadow_with_pattern(
yuv: &[u8],
width: u32,
height: u32,
n_frames: usize,
pattern: super::gop_pattern::GopPattern,
primary_message: &str,
primary_passphrase: &str,
shadow_message: &str,
shadow_passphrase: &str,
) -> Result<Vec<u8>, StegoError> {
use crate::stego::shadow_layer::ShadowLayer;
let shadow = ShadowLayer {
message: shadow_message,
passphrase: shadow_passphrase,
files: &[],
};
h264_stego_encode_yuv_string_with_n_shadows_with_pattern(
yuv, width, height, n_frames, pattern,
primary_message, primary_passphrase,
std::slice::from_ref(&shadow),
)
}
#[allow(clippy::too_many_arguments)]
pub fn h264_stego_encode_yuv_string_with_n_shadows<'a>(
yuv: &[u8],
width: u32,
height: u32,
n_frames: usize,
gop_size: usize,
primary_message: &str,
primary_passphrase: &str,
shadows: &'a [crate::stego::shadow_layer::ShadowLayer<'a>],
) -> Result<Vec<u8>, StegoError> {
h264_stego_encode_yuv_string_with_n_shadows_with_pattern(
yuv, width, height, n_frames,
super::gop_pattern::GopPattern::Ibpbp { gop: gop_size, b_count: 1 },
primary_message, primary_passphrase, shadows,
)
}
#[allow(clippy::too_many_arguments)]
pub fn h264_stego_encode_yuv_string_with_n_shadows_with_pattern<'a>(
yuv: &[u8],
width: u32,
height: u32,
n_frames: usize,
pattern: super::gop_pattern::GopPattern,
primary_message: &str,
primary_passphrase: &str,
shadows: &'a [crate::stego::shadow_layer::ShadowLayer<'a>],
) -> Result<Vec<u8>, StegoError> {
h264_stego_encode_yuv_string_with_n_shadows_with_pattern_and_files(
yuv, width, height, n_frames, pattern,
primary_message, &[], primary_passphrase, shadows,
)
}
#[allow(clippy::too_many_arguments)]
pub fn h264_stego_encode_yuv_string_with_n_shadows_with_pattern_and_files<'a>(
yuv: &[u8],
width: u32,
height: u32,
n_frames: usize,
pattern: super::gop_pattern::GopPattern,
primary_message: &str,
primary_files: &[crate::stego::payload::FileEntry],
primary_passphrase: &str,
shadows: &'a [crate::stego::shadow_layer::ShadowLayer<'a>],
) -> Result<Vec<u8>, StegoError> {
use crate::stego::{crypto, frame, payload};
use crate::stego::shadow_layer::SHADOW_PARITY_TIERS;
let gop_size = pattern.gop_size();
let b_count = pattern.legacy_b_count();
if !width.is_multiple_of(16) || !height.is_multiple_of(16) {
return Err(StegoError::InvalidVideo(format!(
"dimensions must be 16-aligned, got {width}x{height}"
)));
}
let frame_size = (width * height * 3 / 2) as usize;
if yuv.len() != frame_size * n_frames {
return Err(StegoError::InvalidVideo(format!(
"yuv len {} != expected {}", yuv.len(), frame_size * n_frames
)));
}
if gop_size == 0 || gop_size > n_frames {
return Err(StegoError::InvalidVideo(format!(
"gop_size {gop_size} must be in 1..={n_frames}"
)));
}
let mut all_passphrases: Vec<&str> = Vec::with_capacity(shadows.len() + 1);
all_passphrases.push(primary_passphrase);
for s in shadows {
all_passphrases.push(s.passphrase);
}
for i in 0..all_passphrases.len() {
for j in (i + 1)..all_passphrases.len() {
if all_passphrases[i] == all_passphrases[j] {
return Err(StegoError::DuplicatePassphrase);
}
}
}
if !shadows.is_empty() {
let cap_info = h264_stego_shadow_capacity(
yuv, width, height, n_frames, gop_size, shadows.len(),
)?;
for s in shadows {
let msg_bytes = s.message.len()
+ s.files.iter().map(|f| f.content.len()).sum::<usize>();
if msg_bytes > cap_info.max_message_bytes {
return Err(StegoError::MessageTooLarge);
}
}
}
let primary_bytes = payload::encode_payload(primary_message, primary_files)?;
let (ct, nonce, salt) = crypto::encrypt(&primary_bytes, primary_passphrase)?;
let frame_bytes = frame::build_frame(primary_bytes.len(), &salt, &nonce, &ct);
let frame_bits: Vec<u8> = frame_bytes
.iter()
.flat_map(|&byte| (0..8).rev().map(move |i| (byte >> i) & 1))
.collect();
let h: usize = 4;
let quality = Some(26);
let keys = CabacStegoMasterKeys::derive(primary_passphrase)?;
let m_total = frame_bits.len();
let cover_p1 = pass1_count_4domain(
yuv, width, height, n_frames, frame_size, quality, gop_size,
b_count,
)?;
let cap_p1 = cover_p1.cover.capacity();
let mvd_capacity = cap_p1.mvd_sign_bypass;
let m_mvd = m_total.min(mvd_capacity);
let m_residual = m_total - m_mvd;
let per_gop_counts = derive_per_gop_counts_from_cover(
yuv, frame_size, n_frames, pattern, &cover_p1.cover,
);
let (primary_emit_cover, cover_p1b_residual_prov, safe_msl_prov) = if shadows.is_empty() {
(
super::DomainCover::default(),
super::orchestrate::GopCover::default(),
Vec::<bool>::new(),
)
} else {
let plan_a_prov = if m_mvd > 0 {
let cap_mvd_only = GopCapacity {
coeff_sign_bypass: 0,
coeff_suffix_lsb: 0,
mvd_sign_bypass: cap_p1.mvd_sign_bypass,
mvd_suffix_lsb: 0,
};
let messages_a = split_message_per_domain(&frame_bits[..m_mvd], &cap_mvd_only)
.ok_or_else(|| StegoError::InvalidVideo(
"Pass 2A_prov: MVD split failed".into(),
))?;
let mut cover_for_a = super::orchestrate::GopCover::default();
cover_for_a.cover.mvd_sign_bypass = cover_p1.cover.mvd_sign_bypass.clone();
cover_for_a.cover.mvd_suffix_lsb = cover_p1.cover.mvd_suffix_lsb.clone();
cover_for_a.costs.mvd_sign_bypass = cover_p1.costs.mvd_sign_bypass.clone();
cover_for_a.costs.mvd_suffix_lsb = cover_p1.costs.mvd_suffix_lsb.clone();
pass2_stc_plan_with_keys(&cover_for_a, &messages_a, h, &keys, 0)
.ok_or_else(|| StegoError::InvalidVideo(
"Pass 2A_prov: STC plan failed".into(),
))?
} else {
DomainPlan::default()
};
let cover_p1b_prov = pass1b_inject_mvd_log_residual_streaming(
yuv, width, height, n_frames, frame_size, quality,
pattern, &cover_p1.cover, &plan_a_prov, &per_gop_counts,
)?;
let cap_p1b_prov = cover_p1b_prov.cover.capacity();
let residual_capacity_prov =
cap_p1b_prov.coeff_sign_bypass + cap_p1b_prov.coeff_suffix_lsb;
if m_residual > residual_capacity_prov {
return Err(StegoError::MessageTooLarge);
}
let plan_b_prov = if m_residual > 0 {
let cap_coeff_only = GopCapacity {
coeff_sign_bypass: cap_p1b_prov.coeff_sign_bypass,
coeff_suffix_lsb: cap_p1b_prov.coeff_suffix_lsb,
mvd_sign_bypass: 0,
mvd_suffix_lsb: 0,
};
let messages_b = split_message_per_domain(&frame_bits[m_mvd..], &cap_coeff_only)
.ok_or_else(|| StegoError::InvalidVideo(
"Pass 2B_prov: residual split failed".into(),
))?;
let mut cover_for_b = super::orchestrate::GopCover::default();
cover_for_b.cover.coeff_sign_bypass = cover_p1b_prov.cover.coeff_sign_bypass.clone();
cover_for_b.cover.coeff_suffix_lsb = cover_p1b_prov.cover.coeff_suffix_lsb.clone();
cover_for_b.costs.coeff_sign_bypass = cover_p1b_prov.costs.coeff_sign_bypass.clone();
cover_for_b.costs.coeff_suffix_lsb = cover_p1b_prov.costs.coeff_suffix_lsb.clone();
pass2_stc_plan_with_keys(&cover_for_b, &messages_b, h, &keys, 0)
.ok_or_else(|| StegoError::InvalidVideo(
"Pass 2B_prov: STC plan failed".into(),
))?
} else {
DomainPlan::default()
};
let mut combined_plan_prov = DomainPlan {
coeff_sign_bypass: plan_b_prov.coeff_sign_bypass.clone(),
coeff_suffix_lsb: plan_b_prov.coeff_suffix_lsb.clone(),
mvd_sign_bypass: plan_a_prov.mvd_sign_bypass.clone(),
mvd_suffix_lsb: plan_a_prov.mvd_suffix_lsb.clone(),
total_modifications: plan_a_prov.total_modifications + plan_b_prov.total_modifications,
total_cost: plan_a_prov.total_cost + plan_b_prov.total_cost,
};
if combined_plan_prov.coeff_sign_bypass.is_empty() {
combined_plan_prov.coeff_sign_bypass =
cover_p1b_prov.cover.coeff_sign_bypass.bits.clone();
}
if combined_plan_prov.coeff_suffix_lsb.is_empty() {
combined_plan_prov.coeff_suffix_lsb =
cover_p1b_prov.cover.coeff_suffix_lsb.bits.clone();
}
if combined_plan_prov.mvd_sign_bypass.is_empty() {
combined_plan_prov.mvd_sign_bypass = cover_p1.cover.mvd_sign_bypass.bits.clone();
}
if combined_plan_prov.mvd_suffix_lsb.is_empty() {
combined_plan_prov.mvd_suffix_lsb = cover_p1.cover.mvd_suffix_lsb.bits.clone();
}
let bytes_prov = pass3_inject_4domain_streaming(
yuv, width, height, n_frames, frame_size, quality, pattern,
&combined_plan_prov, &per_gop_counts,
)?;
let walk_opts = WalkOptions { record_mvd: true };
let walk_prov = walk_annex_b_for_cover_with_options(&bytes_prov, walk_opts)
.map_err(|e| StegoError::InvalidVideo(format!("provisional walk: {e}")))?;
let safe_msb_prov = super::cascade_safety::analyze_safe_mvd_subset(
&walk_prov.mvd_meta, walk_prov.mb_w, walk_prov.mb_h,
);
let safe_msl = super::cascade_safety::derive_msl_safe_from_msb(
&walk_prov.cover.mvd_sign_bypass.positions,
&safe_msb_prov,
&walk_prov.cover.mvd_suffix_lsb.positions,
);
(walk_prov.cover, cover_p1b_prov, safe_msl)
};
let safe_msl_for_select: Option<&[bool]> =
if shadows.is_empty() { None } else { Some(safe_msl_prov.as_slice()) };
for &parity_len in &SHADOW_PARITY_TIERS {
let shadow_states_emit: Vec<super::shadow::ShadowState> = shadows
.iter()
.map(|s| super::shadow::prepare_shadow_over_emit_cover_safe(
&primary_emit_cover, s.passphrase, s.message, s.files, parity_len,
None, safe_msl_for_select,
))
.collect::<Result<_, _>>()?;
let shadow_states_phase1: Vec<super::shadow::ShadowState> = shadow_states_emit
.iter()
.map(|s| translate_shadow_state(
s, &primary_emit_cover, &cover_p1.cover, &cover_p1b_residual_prov.cover,
))
.collect();
let mut cover_for_a = super::orchestrate::GopCover::default();
cover_for_a.cover.mvd_sign_bypass = cover_p1.cover.mvd_sign_bypass.clone();
cover_for_a.cover.mvd_suffix_lsb = cover_p1.cover.mvd_suffix_lsb.clone();
cover_for_a.costs.mvd_sign_bypass = cover_p1.costs.mvd_sign_bypass.clone();
cover_for_a.costs.mvd_suffix_lsb = cover_p1.costs.mvd_suffix_lsb.clone();
let mut dummy_csb_bits = vec![0u8; cover_p1.cover.coeff_sign_bypass.bits.len()];
let mut dummy_csl_bits = vec![0u8; cover_p1.cover.coeff_suffix_lsb.bits.len()];
let mut dummy_csb_cost = vec![0.0f32; cover_p1.costs.coeff_sign_bypass.len()];
let mut dummy_csl_cost = vec![0.0f32; cover_p1.costs.coeff_suffix_lsb.len()];
for state in &shadow_states_phase1 {
super::shadow::embed_shadow_lsb_all4(
&mut dummy_csb_bits,
&mut dummy_csl_bits,
&mut cover_for_a.cover.mvd_sign_bypass.bits,
&mut cover_for_a.cover.mvd_suffix_lsb.bits,
state,
);
super::shadow::overlay_infinity_costs_all4(
&mut dummy_csb_cost,
&mut dummy_csl_cost,
&mut cover_for_a.costs.mvd_sign_bypass,
&mut cover_for_a.costs.mvd_suffix_lsb,
state,
);
}
let plan_a = if m_mvd > 0 {
let cap_mvd_only = GopCapacity {
coeff_sign_bypass: 0,
coeff_suffix_lsb: 0,
mvd_sign_bypass: cap_p1.mvd_sign_bypass,
mvd_suffix_lsb: 0,
};
let messages_a = split_message_per_domain(&frame_bits[..m_mvd], &cap_mvd_only)
.ok_or_else(|| StegoError::InvalidVideo("Pass 2A: MVD split failed".into()))?;
pass2_stc_plan_with_keys(&cover_for_a, &messages_a, h, &keys, 0)
.ok_or_else(|| StegoError::InvalidVideo("Pass 2A: STC plan failed".into()))?
} else {
DomainPlan::default()
};
let cover_p1b_residual = pass1b_inject_mvd_log_residual_streaming(
yuv, width, height, n_frames, frame_size, quality,
pattern, &cover_p1.cover, &plan_a, &per_gop_counts,
)?;
let cap_p1b = cover_p1b_residual.cover.capacity();
let residual_capacity = cap_p1b.coeff_sign_bypass + cap_p1b.coeff_suffix_lsb;
if m_residual > residual_capacity {
return Err(StegoError::MessageTooLarge);
}
let shadow_states: Vec<super::shadow::ShadowState> = shadow_states_emit
.iter()
.map(|s| translate_shadow_state(
s, &primary_emit_cover, &cover_p1.cover, &cover_p1b_residual.cover,
))
.collect();
let mut cover_for_b = super::orchestrate::GopCover::default();
cover_for_b.cover.coeff_sign_bypass = cover_p1b_residual.cover.coeff_sign_bypass.clone();
cover_for_b.cover.coeff_suffix_lsb = cover_p1b_residual.cover.coeff_suffix_lsb.clone();
cover_for_b.costs.coeff_sign_bypass = cover_p1b_residual.costs.coeff_sign_bypass.clone();
cover_for_b.costs.coeff_suffix_lsb = cover_p1b_residual.costs.coeff_suffix_lsb.clone();
let mut dummy_msb_bits = vec![0u8; cover_p1.cover.mvd_sign_bypass.bits.len()];
let mut dummy_msl_bits = vec![0u8; cover_p1.cover.mvd_suffix_lsb.bits.len()];
let mut dummy_msb_cost = vec![0.0f32; cover_p1.costs.mvd_sign_bypass.len()];
let mut dummy_msl_cost = vec![0.0f32; cover_p1.costs.mvd_suffix_lsb.len()];
for state in &shadow_states {
super::shadow::embed_shadow_lsb_all4(
&mut cover_for_b.cover.coeff_sign_bypass.bits,
&mut cover_for_b.cover.coeff_suffix_lsb.bits,
&mut dummy_msb_bits,
&mut dummy_msl_bits,
state,
);
super::shadow::overlay_infinity_costs_all4(
&mut cover_for_b.costs.coeff_sign_bypass,
&mut cover_for_b.costs.coeff_suffix_lsb,
&mut dummy_msb_cost,
&mut dummy_msl_cost,
state,
);
}
let plan_b = if m_residual > 0 {
let cap_coeff_only = GopCapacity {
coeff_sign_bypass: cap_p1b.coeff_sign_bypass,
coeff_suffix_lsb: cap_p1b.coeff_suffix_lsb,
mvd_sign_bypass: 0,
mvd_suffix_lsb: 0,
};
let messages_b = split_message_per_domain(&frame_bits[m_mvd..], &cap_coeff_only)
.ok_or_else(|| StegoError::InvalidVideo("Pass 2B: residual split failed".into()))?;
pass2_stc_plan_with_keys(&cover_for_b, &messages_b, h, &keys, 0)
.ok_or_else(|| StegoError::InvalidVideo("Pass 2B: STC plan failed".into()))?
} else {
DomainPlan {
coeff_sign_bypass: cover_for_b.cover.coeff_sign_bypass.bits.clone(),
coeff_suffix_lsb: cover_for_b.cover.coeff_suffix_lsb.bits.clone(),
mvd_sign_bypass: Vec::new(),
mvd_suffix_lsb: Vec::new(),
total_modifications: 0,
total_cost: 0.0,
}
};
let mut combined_plan = DomainPlan {
coeff_sign_bypass: plan_b.coeff_sign_bypass.clone(),
coeff_suffix_lsb: plan_b.coeff_suffix_lsb.clone(),
mvd_sign_bypass: plan_a.mvd_sign_bypass.clone(),
mvd_suffix_lsb: plan_a.mvd_suffix_lsb.clone(),
total_modifications: plan_a.total_modifications + plan_b.total_modifications,
total_cost: plan_a.total_cost + plan_b.total_cost,
};
if combined_plan.mvd_sign_bypass.is_empty() {
combined_plan.mvd_sign_bypass = cover_p1.cover.mvd_sign_bypass.bits.clone();
}
if combined_plan.mvd_suffix_lsb.is_empty() {
combined_plan.mvd_suffix_lsb = cover_p1.cover.mvd_suffix_lsb.bits.clone();
}
for state in &shadow_states {
super::shadow::apply_shadow_to_plan_all4(
&mut combined_plan.coeff_sign_bypass,
&mut combined_plan.coeff_suffix_lsb,
&mut combined_plan.mvd_sign_bypass,
&mut combined_plan.mvd_suffix_lsb,
state,
);
}
let mvd_msl_gate: std::collections::HashSet<PositionKey> = shadow_states
.iter()
.flat_map(|state| state.positions.iter())
.filter(|s| s.domain == super::EmbedDomain::MvdSuffixLsb)
.filter_map(|s| cover_p1
.cover
.mvd_suffix_lsb
.positions
.get(s.intra_index)
.copied())
.collect();
let bytes = pass3_inject_4domain_streaming_with_gate(
yuv, width, height, n_frames, frame_size, quality, pattern,
&combined_plan, &per_gop_counts,
if mvd_msl_gate.is_empty() { None } else { Some(&mvd_msl_gate) },
)?;
let opts = WalkOptions { record_mvd: true };
let walk = walk_annex_b_for_cover_with_options(&bytes, opts)
.map_err(|e| StegoError::InvalidVideo(format!("verify walk: {e}")))?;
let safe_msb_walk = super::cascade_safety::analyze_safe_mvd_subset(
&walk.mvd_meta, walk.mb_w, walk.mb_h,
);
let safe_msl_walk = super::cascade_safety::derive_msl_safe_from_msb(
&walk.cover.mvd_sign_bypass.positions,
&safe_msb_walk,
&walk.cover.mvd_suffix_lsb.positions,
);
let mut all_ok = true;
for s in shadows {
match super::shadow::shadow_extract_all4_safe(
&walk.cover, s.passphrase, None, Some(&safe_msl_walk),
) {
Ok(payload_data) if payload_data.text == s.message => continue,
_ => {
all_ok = false;
break;
}
}
}
if all_ok {
return Ok(bytes);
}
}
Err(StegoError::ShadowEmbedFailed)
}
fn translate_shadow_state(
state: &super::shadow::ShadowState,
source_cover: &super::DomainCover,
mvd_target: &super::DomainCover,
coeff_target: &super::DomainCover,
) -> super::shadow::ShadowState {
use std::collections::HashMap;
let build_map = |positions: &[PositionKey]| -> HashMap<PositionKey, usize> {
positions.iter().enumerate().map(|(i, &k)| (k, i)).collect()
};
let target_csb = build_map(&coeff_target.coeff_sign_bypass.positions);
let target_csl = build_map(&coeff_target.coeff_suffix_lsb.positions);
let target_msb = build_map(&mvd_target.mvd_sign_bypass.positions);
let target_msl = build_map(&mvd_target.mvd_suffix_lsb.positions);
let mut out_positions = Vec::with_capacity(state.positions.len());
let mut out_bits = Vec::with_capacity(state.bits.len());
for (i, slot) in state.positions.iter().enumerate().take(state.n_total) {
if i >= state.bits.len() {
break;
}
let pk_opt = match slot.domain {
super::EmbedDomain::CoeffSignBypass =>
source_cover.coeff_sign_bypass.positions.get(slot.intra_index),
super::EmbedDomain::CoeffSuffixLsb =>
source_cover.coeff_suffix_lsb.positions.get(slot.intra_index),
super::EmbedDomain::MvdSignBypass =>
source_cover.mvd_sign_bypass.positions.get(slot.intra_index),
super::EmbedDomain::MvdSuffixLsb =>
source_cover.mvd_suffix_lsb.positions.get(slot.intra_index),
};
let pk = match pk_opt {
Some(&k) => k,
None => continue,
};
let target_idx = match slot.domain {
super::EmbedDomain::CoeffSignBypass => target_csb.get(&pk).copied(),
super::EmbedDomain::CoeffSuffixLsb => target_csl.get(&pk).copied(),
super::EmbedDomain::MvdSignBypass => target_msb.get(&pk).copied(),
super::EmbedDomain::MvdSuffixLsb => target_msl.get(&pk).copied(),
};
if let Some(target_idx) = target_idx {
out_positions.push(super::shadow::ShadowSlot {
domain: slot.domain,
intra_index: target_idx,
priority: slot.priority,
});
out_bits.push(state.bits[i]);
}
}
let n_total = out_bits.len();
super::shadow::ShadowState {
positions: out_positions,
bits: out_bits,
n_total,
parity_len: state.parity_len,
frame_data_len: state.frame_data_len,
}
}
#[inline]
fn is_idr_frame(fi: usize, gop_size: usize) -> bool {
debug_assert!(gop_size > 0, "gop_size must be > 0");
fi.is_multiple_of(gop_size)
}
fn iter_frames_in_encode_order<'a>(
yuv: &'a [u8],
frame_size: usize,
n_frames: usize,
pattern: super::gop_pattern::GopPattern,
) -> impl Iterator<Item = (super::gop_pattern::EncodeOrderFrame, &'a [u8])> {
super::gop_pattern::iter_encode_order(n_frames, pattern).map(move |meta| {
let d = meta.display_idx as usize;
let frame = &yuv[d * frame_size..(d + 1) * frame_size];
(meta, frame)
})
}
pub fn pass1_count_per_gop_4domain(
yuv: &[u8],
width: u32,
height: u32,
n_frames: usize,
gop_size: usize,
b_count: usize,
quality: Option<u8>,
) -> Result<Vec<[usize; 4]>, StegoError> {
use super::encoder_hook::PositionCountingHook;
let frame_size = (width * height * 3 / 2) as usize;
let mut enc = build_encoder(width, height, quality)?;
enc.enable_mvd_stego_hook = true;
let pattern = pattern_from_legacy_args(gop_size, b_count);
enc.enable_b_frames = pattern.has_b_frames();
let mut per_gop_counts: Vec<[usize; 4]> = Vec::new();
let mut current_gop_idx: u32 = u32::MAX;
let mut current_gop_running: [usize; 4] = [0; 4];
let mut frame_iter = iter_frames_in_encode_order(
yuv, frame_size, n_frames, pattern,
)
.peekable();
enc.set_stego_hook(Some(Box::new(PositionCountingHook::new())));
while let Some((meta, frame)) = frame_iter.next() {
if meta.gop_idx != current_gop_idx {
if current_gop_idx != u32::MAX {
let mut hook_box = enc
.take_stego_hook()
.ok_or_else(|| StegoError::InvalidVideo(
"missing PositionCountingHook on GOP boundary".into(),
))?;
let row = hook_box.take_counts_if_counter().ok_or_else(|| {
StegoError::InvalidVideo(
"hook is not PositionCountingHook".into(),
)
})?;
per_gop_counts.push(row);
let _ = current_gop_running; current_gop_running = [0; 4];
}
current_gop_idx = meta.gop_idx;
enc.set_stego_hook(Some(Box::new(PositionCountingHook::new())));
}
let ft = meta.frame_type;
encode_one_frame(&mut enc, frame, ft).map_err(|e| {
StegoError::InvalidVideo(format!("Pass 1 (count): {e}"))
})?;
}
let mut final_hook = enc.take_stego_hook().ok_or_else(|| {
StegoError::InvalidVideo("missing final PositionCountingHook".into())
})?;
let row = final_hook.take_counts_if_counter().ok_or_else(|| {
StegoError::InvalidVideo(
"final hook is not PositionCountingHook".into(),
)
})?;
per_gop_counts.push(row);
Ok(per_gop_counts)
}
pub fn pass1_capture_4domain_for_gop_range(
yuv: &[u8],
width: u32,
height: u32,
n_frames: usize,
gop_size: usize,
b_count: usize,
quality: Option<u8>,
gop_start: usize,
gop_end: usize,
) -> Result<super::orchestrate::GopCover, StegoError> {
if gop_start >= gop_end {
return Err(StegoError::InvalidVideo(format!(
"gop_start {gop_start} >= gop_end {gop_end}"
)));
}
let frame_size = (width * height * 3 / 2) as usize;
let frame_start = gop_start * gop_size;
let frame_end = (gop_end * gop_size).min(n_frames);
if frame_start >= n_frames {
return Err(StegoError::InvalidVideo(format!(
"gop_start {gop_start} maps to frame {frame_start} >= n_frames {n_frames}"
)));
}
let mut enc = build_encoder(width, height, quality)?;
enc.enable_mvd_stego_hook = true;
let pattern = pattern_from_legacy_args(gop_size, b_count);
enc.enable_b_frames = pattern.has_b_frames();
enc.set_stego_hook(Some(Box::new(PositionLoggerHook::new())));
let mut primed = false;
for (meta, frame) in iter_frames_in_encode_order(
yuv, frame_size, n_frames, pattern,
) {
let in_range = (meta.gop_idx as usize) >= gop_start
&& (meta.gop_idx as usize) < gop_end;
if !in_range {
if (meta.gop_idx as usize) >= gop_end {
break;
}
continue;
}
if !primed {
debug_assert_eq!(
meta.frame_type,
super::gop_pattern::FrameType::Idr,
"first in-range frame must be IDR (gop_start={gop_start})",
);
enc.stego_frame_idx = meta.encode_idx;
primed = true;
}
let ft = meta.frame_type;
encode_one_frame(&mut enc, frame, ft).map_err(|e| {
StegoError::InvalidVideo(format!(
"Pass 1 (gop_range): frame_idx={} {e}",
meta.encode_idx,
))
})?;
if meta.encode_idx as usize + 1 >= frame_end {
break;
}
}
let mut hook = enc.take_stego_hook().ok_or_else(|| {
StegoError::InvalidVideo("Pass 1 (gop_range): hook missing".into())
})?;
let cover = drain_position_logger(&mut hook)?;
Ok(cover)
}
fn pass1_count_4domain(
yuv: &[u8],
width: u32,
height: u32,
n_frames: usize,
frame_size: usize,
quality: Option<u8>,
gop_size: usize,
b_count: usize,
) -> Result<super::orchestrate::GopCover, StegoError> {
let (cover, _meta) = pass1_count_4domain_with_meta(
yuv, width, height, n_frames, frame_size, quality, gop_size, b_count,
)?;
Ok(cover)
}
fn pattern_from_legacy_args(gop_size: usize, b_count: usize) -> super::gop_pattern::GopPattern {
if b_count == 0 {
super::gop_pattern::GopPattern::Ipppp { gop: gop_size }
} else {
super::gop_pattern::GopPattern::Ibpbp { gop: gop_size, b_count }
}
}
fn pass1_count_4domain_with_meta(
yuv: &[u8],
width: u32,
height: u32,
n_frames: usize,
frame_size: usize,
quality: Option<u8>,
gop_size: usize,
b_count: usize,
) -> Result<(super::orchestrate::GopCover, Vec<super::encoder_hook::MvdPositionMeta>), StegoError> {
let mut enc = build_encoder(width, height, quality)?;
enc.enable_mvd_stego_hook = true;
enc.set_stego_hook(Some(Box::new(PositionLoggerHook::new())));
let pattern = pattern_from_legacy_args(gop_size, b_count);
enc.enable_b_frames = pattern.has_b_frames();
for (_meta, frame) in iter_frames_in_encode_order(yuv, frame_size, n_frames, pattern) {
let ft = _meta.frame_type;
encode_one_frame(&mut enc, frame, ft)
.map_err(|e| StegoError::InvalidVideo(format!("Pass 1: {e}")))?;
}
let mut hook = enc.take_stego_hook().ok_or_else(|| StegoError::InvalidVideo(
"Pass 1 hook missing".into()
))?;
let meta = hook.take_mvd_meta_if_logger();
let cover = drain_position_logger(&mut hook)?;
Ok((cover, meta))
}
#[allow(clippy::too_many_arguments)]
#[allow(dead_code)] fn pass1_count_4domain_with_safe_masks(
yuv: &[u8],
width: u32,
height: u32,
n_frames: usize,
frame_size: usize,
quality: Option<u8>,
gop_size: usize,
b_count: usize,
) -> Result<(super::orchestrate::GopCover, Vec<bool>, Vec<bool>), StegoError> {
let (cover, meta) = pass1_count_4domain_with_meta(
yuv, width, height, n_frames, frame_size, quality, gop_size, b_count,
)?;
let mb_w = width / 16;
let mb_h = height / 16;
let safe_msb = super::cascade_safety::analyze_safe_mvd_subset(&meta, mb_w, mb_h);
let safe_msl = super::cascade_safety::derive_msl_safe_from_msb(
&cover.cover.mvd_sign_bypass.positions,
&safe_msb,
&cover.cover.mvd_suffix_lsb.positions,
);
Ok((cover, safe_msb, safe_msl))
}
#[allow(clippy::too_many_arguments)]
fn pass1b_inject_mvd_log_residual(
yuv: &[u8],
width: u32,
height: u32,
n_frames: usize,
frame_size: usize,
quality: Option<u8>,
cover_p1: &super::DomainCover,
plan_a: &DomainPlan,
gop_size: usize,
b_count: usize,
) -> Result<super::orchestrate::GopCover, StegoError> {
let (cover, _bytes) = pass1b_inject_mvd_log_residual_with_bytes(
yuv, width, height, n_frames, frame_size, quality,
cover_p1, plan_a, gop_size, b_count,
)?;
Ok(cover)
}
#[allow(clippy::too_many_arguments)]
fn pass1b_inject_mvd_log_residual_with_bytes(
yuv: &[u8],
width: u32,
height: u32,
n_frames: usize,
frame_size: usize,
quality: Option<u8>,
cover_p1: &super::DomainCover,
plan_a: &DomainPlan,
gop_size: usize,
b_count: usize,
) -> Result<(super::orchestrate::GopCover, Vec<u8>), StegoError> {
let injector = PlanInjector::from_plan(cover_p1, plan_a);
let hook = InjectAndLogHook::new(injector);
let mut enc = build_encoder(width, height, quality)?;
enc.enable_mvd_stego_hook = true;
enc.set_stego_hook(Some(Box::new(hook)));
let mut bytes = Vec::new();
let pattern = pattern_from_legacy_args(gop_size, b_count);
enc.enable_b_frames = pattern.has_b_frames();
for (_meta, frame) in iter_frames_in_encode_order(yuv, frame_size, n_frames, pattern) {
let ft = _meta.frame_type;
let frame_bytes = encode_one_frame(&mut enc, frame, ft)
.map_err(|e| StegoError::InvalidVideo(format!("Pass 1B: {e}")))?;
bytes.extend_from_slice(&frame_bytes);
}
let mut hook = enc.take_stego_hook().ok_or_else(|| StegoError::InvalidVideo(
"Pass 1B hook missing".into()
))?;
let cover = drain_position_logger(&mut hook)?;
Ok((cover, bytes))
}
#[allow(clippy::too_many_arguments)]
fn pass3_inject_4domain(
yuv: &[u8],
width: u32,
height: u32,
n_frames: usize,
frame_size: usize,
quality: Option<u8>,
combined_cover: &super::DomainCover,
combined_plan: &DomainPlan,
gop_size: usize,
b_count: usize,
) -> Result<Vec<u8>, StegoError> {
let injector = PlanInjector::from_plan(combined_cover, combined_plan);
let hook = InjectionHook::new(injector);
let mut enc = build_encoder(width, height, quality)?;
enc.enable_mvd_stego_hook = true;
enc.set_stego_hook(Some(Box::new(hook)));
let mut out = Vec::new();
let pattern = pattern_from_legacy_args(gop_size, b_count);
enc.enable_b_frames = pattern.has_b_frames();
for (_meta, frame) in iter_frames_in_encode_order(yuv, frame_size, n_frames, pattern) {
let ft = _meta.frame_type;
let bytes = encode_one_frame(&mut enc, frame, ft)
.map_err(|e| StegoError::InvalidVideo(format!("Pass 3: {e}")))?;
out.extend_from_slice(&bytes);
}
Ok(out)
}
fn derive_per_gop_counts_from_cover(
yuv: &[u8],
frame_size: usize,
n_frames: usize,
pattern: super::gop_pattern::GopPattern,
cover: &super::DomainCover,
) -> Vec<[usize; 4]> {
let mut encode_to_gop: Vec<usize> = vec![0; n_frames];
let mut max_gop: usize = 0;
for (meta, _frame) in iter_frames_in_encode_order(yuv, frame_size, n_frames, pattern) {
let g = meta.gop_idx as usize;
encode_to_gop[meta.encode_idx as usize] = g;
if g > max_gop {
max_gop = g;
}
}
let num_gops = max_gop + 1;
let mut per_gop_counts: Vec<[usize; 4]> = vec![[0; 4]; num_gops];
for p in &cover.coeff_sign_bypass.positions {
let g = encode_to_gop[p.frame_idx() as usize];
per_gop_counts[g][0] += 1;
}
for p in &cover.coeff_suffix_lsb.positions {
let g = encode_to_gop[p.frame_idx() as usize];
per_gop_counts[g][1] += 1;
}
for p in &cover.mvd_sign_bypass.positions {
let g = encode_to_gop[p.frame_idx() as usize];
per_gop_counts[g][2] += 1;
}
for p in &cover.mvd_suffix_lsb.positions {
let g = encode_to_gop[p.frame_idx() as usize];
per_gop_counts[g][3] += 1;
}
per_gop_counts
}
#[allow(clippy::too_many_arguments)]
fn encode_one_gop_pass1b_inject_mvd_log_residual(
yuv: &[u8],
width: u32,
height: u32,
n_frames: usize,
frame_size: usize,
quality: Option<u8>,
pattern: super::gop_pattern::GopPattern,
g: usize,
mvd_cover: &super::DomainCover,
mvd_plan: &DomainPlan,
) -> Result<super::orchestrate::GopCover, StegoError> {
let injector = PlanInjector::from_plan(mvd_cover, mvd_plan);
let hook = InjectAndLogHook::new(injector);
let mut enc = build_encoder(width, height, quality)?;
enc.enable_mvd_stego_hook = true;
enc.enable_b_frames = pattern.has_b_frames();
enc.set_stego_hook(Some(Box::new(hook)));
let mut primed = false;
for (meta, frame) in iter_frames_in_encode_order(
yuv, frame_size, n_frames, pattern,
) {
if (meta.gop_idx as usize) != g {
if (meta.gop_idx as usize) > g {
break;
}
continue;
}
if !primed {
enc.stego_frame_idx = meta.encode_idx;
primed = true;
}
encode_one_frame(&mut enc, frame, meta.frame_type)
.map_err(|e| StegoError::InvalidVideo(format!(
"Pass 1B GOP {g} frame {}: {e}",
meta.encode_idx,
)))?;
}
let mut hook = enc.take_stego_hook().ok_or_else(|| {
StegoError::InvalidVideo("Pass 1B GOP hook missing".into())
})?;
drain_position_logger(&mut hook)
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn pass1b_inject_mvd_log_residual_streaming(
yuv: &[u8],
width: u32,
height: u32,
n_frames: usize,
frame_size: usize,
quality: Option<u8>,
pattern: super::gop_pattern::GopPattern,
mvd_cover: &super::DomainCover,
mvd_plan: &DomainPlan,
per_gop_counts: &[[usize; 4]],
) -> Result<super::orchestrate::GopCover, StegoError> {
let num_gops = per_gop_counts.len();
let mut full_cover = super::orchestrate::GopCover::default();
for g in 0..num_gops {
let gop_cover = encode_one_gop_pass1b_inject_mvd_log_residual(
yuv, width, height, n_frames, frame_size, quality, pattern,
g, mvd_cover, mvd_plan,
)?;
for b in &gop_cover.cover.coeff_sign_bypass.bits {
full_cover.cover.coeff_sign_bypass.bits.push(*b);
}
for p in &gop_cover.cover.coeff_sign_bypass.positions {
full_cover.cover.coeff_sign_bypass.positions.push(*p);
}
for c in &gop_cover.costs.coeff_sign_bypass {
full_cover.costs.coeff_sign_bypass.push(*c);
}
for b in &gop_cover.cover.coeff_suffix_lsb.bits {
full_cover.cover.coeff_suffix_lsb.bits.push(*b);
}
for p in &gop_cover.cover.coeff_suffix_lsb.positions {
full_cover.cover.coeff_suffix_lsb.positions.push(*p);
}
for c in &gop_cover.costs.coeff_suffix_lsb {
full_cover.costs.coeff_suffix_lsb.push(*c);
}
for b in &gop_cover.cover.mvd_sign_bypass.bits {
full_cover.cover.mvd_sign_bypass.bits.push(*b);
}
for p in &gop_cover.cover.mvd_sign_bypass.positions {
full_cover.cover.mvd_sign_bypass.positions.push(*p);
}
for c in &gop_cover.costs.mvd_sign_bypass {
full_cover.costs.mvd_sign_bypass.push(*c);
}
for b in &gop_cover.cover.mvd_suffix_lsb.bits {
full_cover.cover.mvd_suffix_lsb.bits.push(*b);
}
for p in &gop_cover.cover.mvd_suffix_lsb.positions {
full_cover.cover.mvd_suffix_lsb.positions.push(*p);
}
for c in &gop_cover.costs.mvd_suffix_lsb {
full_cover.costs.mvd_suffix_lsb.push(*c);
}
}
let _ = per_gop_counts; Ok(full_cover)
}
#[allow(clippy::too_many_arguments)]
fn encode_one_gop_with_plan_and_capture(
yuv: &[u8],
width: u32,
height: u32,
n_frames: usize,
frame_size: usize,
quality: Option<u8>,
pattern: super::gop_pattern::GopPattern,
g: usize,
gop_plan: &DomainPlan,
mvd_msl_safe_keys: Option<&std::collections::HashSet<PositionKey>>,
) -> Result<Vec<u8>, StegoError> {
let gop_size = pattern.gop_size();
let b_count = pattern.legacy_b_count();
let gop_cover = pass1_capture_4domain_for_gop_range(
yuv, width, height, n_frames, gop_size, b_count, quality, g, g + 1,
)?;
let mut gop_cover_only = super::DomainCover::default();
gop_cover_only.coeff_sign_bypass =
gop_cover.cover.coeff_sign_bypass.clone();
gop_cover_only.coeff_suffix_lsb =
gop_cover.cover.coeff_suffix_lsb.clone();
gop_cover_only.mvd_sign_bypass =
gop_cover.cover.mvd_sign_bypass.clone();
gop_cover_only.mvd_suffix_lsb =
gop_cover.cover.mvd_suffix_lsb.clone();
let injector = PlanInjector::from_plan(&gop_cover_only, gop_plan);
let mut enc = build_encoder(width, height, quality)?;
enc.enable_mvd_stego_hook = true;
enc.enable_b_frames = pattern.has_b_frames();
let mut hook = InjectionHook::new(injector);
if let Some(keys) = mvd_msl_safe_keys {
hook.set_mvd_msl_safe_gate(keys.clone());
}
enc.set_stego_hook(Some(Box::new(hook)));
let mut primed = false;
let mut bytes = Vec::new();
for (meta, frame) in iter_frames_in_encode_order(
yuv, frame_size, n_frames, pattern,
) {
if (meta.gop_idx as usize) != g {
if (meta.gop_idx as usize) > g {
break;
}
continue;
}
if !primed {
enc.stego_frame_idx = meta.encode_idx;
primed = true;
}
let frame_bytes = encode_one_frame(&mut enc, frame, meta.frame_type)
.map_err(|e| StegoError::InvalidVideo(format!(
"Pass 3 GOP {g} frame {}: {e}",
meta.encode_idx,
)))?;
bytes.extend_from_slice(&frame_bytes);
}
Ok(bytes)
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn pass3_inject_4domain_streaming(
yuv: &[u8],
width: u32,
height: u32,
n_frames: usize,
frame_size: usize,
quality: Option<u8>,
pattern: super::gop_pattern::GopPattern,
combined_plan: &DomainPlan,
per_gop_counts: &[[usize; 4]],
) -> Result<Vec<u8>, StegoError> {
pass3_inject_4domain_streaming_with_gate(
yuv, width, height, n_frames, frame_size, quality, pattern,
combined_plan, per_gop_counts, None,
)
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn pass3_inject_4domain_streaming_with_gate(
yuv: &[u8],
width: u32,
height: u32,
n_frames: usize,
frame_size: usize,
quality: Option<u8>,
pattern: super::gop_pattern::GopPattern,
combined_plan: &DomainPlan,
per_gop_counts: &[[usize; 4]],
mvd_msl_safe_keys: Option<&std::collections::HashSet<PositionKey>>,
) -> Result<Vec<u8>, StegoError> {
let num_gops = per_gop_counts.len();
let cum: [Vec<usize>; 4] = std::array::from_fn(|d| {
let mut v = Vec::with_capacity(num_gops + 1);
v.push(0);
for row in per_gop_counts.iter() {
v.push(*v.last().unwrap() + row[d]);
}
v
});
let mut out = Vec::new();
for g in 0..num_gops {
let mut gop_plan = DomainPlan::default();
for d in 0..4 {
let lo = cum[d][g];
let hi = cum[d][g + 1];
let src = match d {
0 => &combined_plan.coeff_sign_bypass,
1 => &combined_plan.coeff_suffix_lsb,
2 => &combined_plan.mvd_sign_bypass,
_ => &combined_plan.mvd_suffix_lsb,
};
let dst = match d {
0 => &mut gop_plan.coeff_sign_bypass,
1 => &mut gop_plan.coeff_suffix_lsb,
2 => &mut gop_plan.mvd_sign_bypass,
_ => &mut gop_plan.mvd_suffix_lsb,
};
if src.is_empty() {
continue;
}
let lo = lo.min(src.len());
let hi = hi.min(src.len());
*dst = src[lo..hi].to_vec();
}
let gop_bytes = encode_one_gop_with_plan_and_capture(
yuv, width, height, n_frames, frame_size, quality, pattern,
g, &gop_plan, mvd_msl_safe_keys,
)?;
out.extend_from_slice(&gop_bytes);
}
Ok(out)
}
#[cfg(test)]
mod pass1b_streaming_diag {
use super::*;
use crate::codec::h264::stego::PositionKey;
use crate::codec::h264::stego::orchestrate::DomainPlan;
use crate::codec::h264::stego::gop_pattern::GopPattern;
#[test]
fn h264_stego_encode_with_files_roundtrip_128x80() {
use crate::stego::payload::FileEntry;
let yuv = match std::fs::read(
"test-vectors/video/h264/real-world/img4138_128x80_f10.yuv",
) {
Ok(y) => y,
Err(_) => return,
};
let files = [FileEntry {
filename: "note.txt".into(),
content: b"phasm files demo: hello video!".to_vec(),
}];
let bytes = match super::h264_stego_encode_yuv_string_4domain_multigop_streaming_v2_with_files(
&yuv, 128, 80, 10, 5, "msg", &files, "files-pass-128",
) {
Ok(b) => b,
Err(e) => {
eprintln!("encode_with_files flake (#94): {e:?}");
return; }
};
let payload =
super::super::decode_pixels::h264_stego_smart_decode_video_with_payload(
&bytes, "files-pass-128",
).expect("decode_with_payload");
assert_eq!(payload.text, "msg");
assert_eq!(payload.files.len(), 1);
assert_eq!(payload.files[0].filename, "note.txt");
assert_eq!(payload.files[0].content, b"phasm files demo: hello video!");
}
#[test]
fn h264_stego_encode_with_shadow_and_files_roundtrip_128x80() {
use crate::stego::payload::FileEntry;
use crate::stego::shadow_layer::ShadowLayer;
let yuv = match std::fs::read(
"test-vectors/video/h264/real-world/img4138_128x80_f10.yuv",
) {
Ok(y) => y,
Err(_) => return,
};
let primary_files = [FileEntry {
filename: "primary.txt".into(),
content: b"primary file".to_vec(),
}];
let shadow_files = [FileEntry {
filename: "shadow.txt".into(),
content: b"shadow file".to_vec(),
}];
let shadows = [ShadowLayer {
message: "s",
passphrase: "shadow-pass-128",
files: &shadow_files,
}];
let pattern = GopPattern::Ibpbp { gop: 5, b_count: 1 };
let bytes = match super::h264_stego_encode_yuv_string_with_n_shadows_with_pattern_and_files(
&yuv, 128, 80, 10, pattern,
"p", &primary_files, "primary-pass-128",
&shadows,
) {
Ok(b) => b,
Err(e) => {
eprintln!("encode_with_shadow_and_files flake (#94): {e:?}");
return; }
};
let prim_payload =
super::super::decode_pixels::h264_stego_smart_decode_video_with_payload(
&bytes, "primary-pass-128",
).expect("primary decode");
assert_eq!(prim_payload.text, "p");
assert_eq!(prim_payload.files.len(), 1);
assert_eq!(prim_payload.files[0].filename, "primary.txt");
assert_eq!(prim_payload.files[0].content, b"primary file");
let shadow_payload =
super::super::decode_pixels::h264_stego_smart_decode_video_with_payload(
&bytes, "shadow-pass-128",
).expect("shadow decode");
assert_eq!(shadow_payload.text, "s");
assert_eq!(shadow_payload.files.len(), 1);
assert_eq!(shadow_payload.files[0].filename, "shadow.txt");
assert_eq!(shadow_payload.files[0].content, b"shadow file");
}
#[test]
fn h264_stego_capacity_4domain_smoke() {
let yuv = match std::fs::read(
"test-vectors/video/h264/real-world/img4138_128x80_f10.yuv",
) {
Ok(y) => y,
Err(_) => return,
};
let info = super::h264_stego_capacity_4domain(&yuv, 128, 80, 10, 5)
.expect("capacity_4domain");
assert!(info.cover_size_bits > 0, "cover bits must be positive");
assert!(
info.primary_max_message_bytes > 0,
"primary capacity must be > 0",
);
assert!(
info.primary_max_message_bytes >= info.shadow_max_message_bytes,
"primary {} should be >= shadow (n=1) {}",
info.primary_max_message_bytes,
info.shadow_max_message_bytes,
);
eprintln!(
"128x80 capacity: cover_bits={} primary={} shadow_n1={}",
info.cover_size_bits,
info.primary_max_message_bytes,
info.shadow_max_message_bytes,
);
}
#[test]
fn pass1b_streaming_matches_inmemory_128x80_2gop() {
let yuv = match std::fs::read(
"test-vectors/video/h264/real-world/img4138_128x80_f10.yuv",
) {
Ok(y) => y,
Err(_) => return,
};
compare(&yuv, 128, 80, 10, 5, 1);
}
#[test]
#[ignore = "needs /tmp/img4138_1080p_f10.yuv + ~10 min release"]
fn pass1b_streaming_matches_inmemory_1080p_2gop() {
let yuv = match std::fs::read("/tmp/img4138_1080p_f10.yuv") {
Ok(y) => y,
Err(_) => return,
};
compare(&yuv, 1920, 1072, 10, 5, 1);
}
fn compare(yuv: &[u8], w: u32, h: u32, n: usize, gop: usize, b: usize) {
let frame_size = (w * h * 3 / 2) as usize;
let pattern = GopPattern::Ibpbp { gop, b_count: b };
let cover_p1 = pass1_count_4domain(
yuv, w, h, n, frame_size, Some(26), gop, b,
)
.expect("pass1");
let plan = DomainPlan::default();
let in_mem = pass1b_inject_mvd_log_residual(
yuv, w, h, n, frame_size, Some(26),
&cover_p1.cover, &plan, gop, b,
)
.expect("in-memory pass1b");
let per_gop = derive_per_gop_counts_from_cover(
yuv, frame_size, n, pattern, &cover_p1.cover,
);
let stream = pass1b_inject_mvd_log_residual_streaming(
yuv, w, h, n, frame_size, Some(26),
pattern, &cover_p1.cover, &plan, &per_gop,
)
.expect("streaming pass1b");
let cmp_domain = |label: &str,
a: &Vec<u8>, b: &Vec<u8>,
ap: &Vec<PositionKey>, bp: &Vec<PositionKey>,
ac: &Vec<f32>, bc: &Vec<f32>| {
eprintln!(
"{label}: in_mem(bits={} positions={} costs={}) stream(bits={} positions={} costs={})",
a.len(), ap.len(), ac.len(),
b.len(), bp.len(), bc.len(),
);
assert_eq!(a.len(), b.len(), "{label}: bits len differs");
let mut ap_sorted: Vec<u64> =
ap.iter().map(|p| p.raw()).collect();
let mut bp_sorted: Vec<u64> =
bp.iter().map(|p| p.raw()).collect();
ap_sorted.sort();
bp_sorted.sort();
assert_eq!(
ap_sorted, bp_sorted,
"{label}: position SETS differ",
);
};
cmp_domain(
"coeff_sign",
&in_mem.cover.coeff_sign_bypass.bits,
&stream.cover.coeff_sign_bypass.bits,
&in_mem.cover.coeff_sign_bypass.positions,
&stream.cover.coeff_sign_bypass.positions,
&in_mem.costs.coeff_sign_bypass,
&stream.costs.coeff_sign_bypass,
);
cmp_domain(
"coeff_suffix",
&in_mem.cover.coeff_suffix_lsb.bits,
&stream.cover.coeff_suffix_lsb.bits,
&in_mem.cover.coeff_suffix_lsb.positions,
&stream.cover.coeff_suffix_lsb.positions,
&in_mem.costs.coeff_suffix_lsb,
&stream.costs.coeff_suffix_lsb,
);
cmp_domain(
"mvd_sign",
&in_mem.cover.mvd_sign_bypass.bits,
&stream.cover.mvd_sign_bypass.bits,
&in_mem.cover.mvd_sign_bypass.positions,
&stream.cover.mvd_sign_bypass.positions,
&in_mem.costs.mvd_sign_bypass,
&stream.costs.mvd_sign_bypass,
);
cmp_domain(
"mvd_suffix",
&in_mem.cover.mvd_suffix_lsb.bits,
&stream.cover.mvd_suffix_lsb.bits,
&in_mem.cover.mvd_suffix_lsb.positions,
&stream.cover.mvd_suffix_lsb.positions,
&in_mem.costs.mvd_suffix_lsb,
&stream.costs.mvd_suffix_lsb,
);
}
#[test]
fn pass3_walked_cover_matches_inmemory_128x80_2gop() {
use crate::codec::h264::cabac::bin_decoder::slice::{
walk_annex_b_for_cover_with_options, WalkOptions,
};
let yuv = match std::fs::read(
"test-vectors/video/h264/real-world/img4138_128x80_f10.yuv",
) {
Ok(y) => y,
Err(_) => return,
};
let (w, h, n, gop, b) = (128u32, 80u32, 10usize, 5usize, 1usize);
let pattern = GopPattern::Ibpbp { gop, b_count: b };
let frame_size = (w * h * 3 / 2) as usize;
let cover_p1 = pass1_count_4domain(
&yuv, w, h, n, frame_size, Some(26), gop, b,
)
.expect("pass1");
let combined_plan = DomainPlan {
coeff_sign_bypass: cover_p1.cover.coeff_sign_bypass.bits.clone(),
coeff_suffix_lsb: cover_p1.cover.coeff_suffix_lsb.bits.clone(),
mvd_sign_bypass: cover_p1.cover.mvd_sign_bypass.bits.clone(),
mvd_suffix_lsb: cover_p1.cover.mvd_suffix_lsb.bits.clone(),
total_modifications: 0,
total_cost: 0.0,
};
let in_mem_bytes = pass3_inject_4domain(
&yuv, w, h, n, frame_size, Some(26),
&cover_p1.cover, &combined_plan, gop, b,
)
.expect("in-memory pass3");
let per_gop = derive_per_gop_counts_from_cover(
&yuv, frame_size, n, pattern, &cover_p1.cover,
);
let stream_bytes = pass3_inject_4domain_streaming(
&yuv, w, h, n, frame_size, Some(26), pattern,
&combined_plan, &per_gop,
)
.expect("streaming pass3");
eprintln!(
"in-mem bytes: {}, stream bytes: {}",
in_mem_bytes.len(),
stream_bytes.len(),
);
let walk_opts = WalkOptions { record_mvd: true };
let in_mem_walk =
walk_annex_b_for_cover_with_options(&in_mem_bytes, walk_opts)
.expect("walk in-mem");
let stream_walk =
walk_annex_b_for_cover_with_options(&stream_bytes, walk_opts)
.expect("walk streaming");
eprintln!(
"in-mem walk: cs={} cl={} ms={} ml={}",
in_mem_walk.cover.coeff_sign_bypass.bits.len(),
in_mem_walk.cover.coeff_suffix_lsb.bits.len(),
in_mem_walk.cover.mvd_sign_bypass.bits.len(),
in_mem_walk.cover.mvd_suffix_lsb.bits.len(),
);
eprintln!(
"stream walk: cs={} cl={} ms={} ml={}",
stream_walk.cover.coeff_sign_bypass.bits.len(),
stream_walk.cover.coeff_suffix_lsb.bits.len(),
stream_walk.cover.mvd_sign_bypass.bits.len(),
stream_walk.cover.mvd_suffix_lsb.bits.len(),
);
let mut im_pos: Vec<u64> = in_mem_walk
.cover
.coeff_sign_bypass
.positions
.iter()
.map(|p| p.raw())
.collect();
let mut st_pos: Vec<u64> = stream_walk
.cover
.coeff_sign_bypass
.positions
.iter()
.map(|p| p.raw())
.collect();
im_pos.sort();
st_pos.sort();
assert_eq!(
im_pos, st_pos,
"walker-recovered coeff_sign positions differ between in-memory and streaming Pass-3",
);
}
}
#[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 out = Vec::with_capacity(frame_size * n_frames);
let mut s: u32 = 0x1234_5678;
for _ in 0..n_frames {
for _ in 0..frame_size {
s = s.wrapping_mul(1664525).wrapping_add(1013904223);
out.push((s >> 16) as u8);
}
}
out
}
#[test]
fn empty_message_produces_byte_identical_output_to_no_stego() {
let yuv = deterministic_yuv(32, 32, 1);
use super::super::super::encoder::encoder::{Encoder, EntropyMode};
let mut baseline_enc = Encoder::new(32, 32, Some(26)).unwrap();
baseline_enc.entropy_mode = EntropyMode::Cabac;
baseline_enc.enable_transform_8x8 = true;
let baseline = baseline_enc.encode_i_frame(&yuv).unwrap();
let stego = h264_stego_encode_i_frames_only(
&yuv, 32, 32, 1,
&[],
"test-pass", 4, Some(26),
).unwrap();
assert_eq!(stego, baseline,
"empty message → stego MUST equal baseline byte-identical");
}
#[test]
fn rejects_non_aligned_dimensions() {
let yuv = vec![0u8; 33 * 32 * 3 / 2];
let r = h264_stego_encode_i_frames_only(
&yuv, 33, 32, 1, &[0, 1, 0], "pass", 4, Some(26),
);
assert!(r.is_err(), "non-16-aligned dimensions must error");
}
#[test]
fn rejects_wrong_yuv_length() {
let yuv = vec![0u8; 100];
let r = h264_stego_encode_i_frames_only(
&yuv, 32, 32, 1, &[0, 1, 0], "pass", 4, Some(26),
);
assert!(r.is_err(), "wrong YUV length must error");
}
#[test]
fn rejects_invalid_h_param() {
let yuv = deterministic_yuv(32, 32, 1);
let r = h264_stego_encode_i_frames_only(
&yuv, 32, 32, 1, &[0, 1, 0], "pass", 8, Some(26),
);
assert!(r.is_err(), "h>7 must error");
}
#[test]
fn small_message_encodes_without_error() {
let yuv = deterministic_yuv(32, 32, 1);
let msg = vec![1u8, 0, 1, 1, 0]; let bytes = h264_stego_encode_i_frames_only(
&yuv, 32, 32, 1, &msg, "test-pass", 4, Some(26),
).unwrap();
assert!(!bytes.is_empty(), "stego output must be non-empty");
assert!(bytes.len() > 50, "stego output suspiciously small: {} bytes", bytes.len());
}
#[test]
fn message_too_large_returns_error() {
let yuv = deterministic_yuv(32, 32, 1);
let msg = vec![0u8; 100_000]; let r = h264_stego_encode_i_frames_only(
&yuv, 32, 32, 1, &msg, "test-pass", 4, Some(26),
);
assert!(r.is_err(), "oversize message must return error");
}
#[test]
fn string_wrapper_returns_non_empty_annex_b() {
let yuv = deterministic_yuv(32, 32, 1);
let bytes = h264_stego_encode_yuv_string(
&yuv, 32, 32, 1, "hi", "pass-1",
).unwrap();
assert!(bytes.len() > 50, "stego output too small: {} bytes", bytes.len());
}
#[test]
fn string_wrapper_rejects_non_aligned_dimensions() {
let yuv = vec![0u8; 33 * 32 * 3 / 2];
let r = h264_stego_encode_yuv_string(
&yuv, 33, 32, 1, "msg", "pass",
);
assert!(r.is_err(), "non-16-aligned dims must error");
}
#[test]
fn string_wrapper_distinct_passphrases_produce_distinct_output() {
let yuv = deterministic_yuv(32, 32, 1);
let a = h264_stego_encode_yuv_string(
&yuv, 32, 32, 1, "hi", "pass-a",
).unwrap();
let b = h264_stego_encode_yuv_string(
&yuv, 32, 32, 1, "hi", "pass-b",
).unwrap();
assert_ne!(a, b, "distinct passphrases must produce distinct output");
}
fn correlated_yuv(w: u32, h: u32, n_frames: usize) -> Vec<u8> {
let frame_size = (w * h * 3 / 2) as usize;
let mut out = Vec::with_capacity(frame_size * n_frames);
let mut base = Vec::with_capacity(frame_size);
let mut s: u32 = 0x1234_5678;
for _ in 0..frame_size {
s = s.wrapping_mul(1103515245).wrapping_add(12345);
let v = 64i32 + ((s >> 24) & 0x7F) as i32; base.push(v.clamp(0, 255) as u8);
}
for fi in 0..n_frames {
let mut p: u32 = 0x4242_DEAD ^ (fi as u32 * 17);
for &b in &base {
p = p.wrapping_mul(1103515245).wrapping_add(12345);
let delta = ((p >> 28) & 0x07) as i32 - 4; let v = b as i32 + delta;
out.push(v.clamp(0, 255) as u8);
}
}
out
}
#[test]
fn multigop_emits_expected_number_of_idrs() {
use crate::codec::h264::bitstream::parse_nal_units_annexb;
let n_frames = 6;
let gop_size = 2;
let yuv = correlated_yuv(32, 32, n_frames);
let bytes = h264_stego_encode_yuv_string_4domain_multigop(
&yuv, 32, 32, n_frames, gop_size, "hi", "pass",
).expect("multigop encode");
let nalus = parse_nal_units_annexb(&bytes).expect("nal parse");
let mut idr_count = 0;
let mut non_idr_count = 0;
for n in &nalus {
if n.nal_type.is_idr() { idr_count += 1; }
else if n.nal_type.is_vcl() { non_idr_count += 1; }
}
let expected_idr = n_frames.div_ceil(gop_size);
let expected_non_idr = n_frames - expected_idr;
assert_eq!(
(idr_count, non_idr_count),
(expected_idr, expected_non_idr),
"expected {expected_idr} IDRs + {expected_non_idr} non-IDR slices \
(n_frames={n_frames}, gop_size={gop_size}), got {idr_count} IDRs + {non_idr_count} non-IDR slices"
);
}
#[test]
fn multigop_roundtrip_recovers_message() {
use super::super::decode_pixels::h264_stego_decode_yuv_string_4domain;
let n_frames = 6;
let gop_size = 2;
let yuv = correlated_yuv(32, 32, n_frames);
let msg = "hi multi-IDR";
let pass = "test-pass";
let bytes = h264_stego_encode_yuv_string_4domain_multigop(
&yuv, 32, 32, n_frames, gop_size, msg, pass,
).expect("multigop encode");
let recovered = h264_stego_decode_yuv_string_4domain(&bytes, pass)
.expect("multigop decode");
assert_eq!(recovered, msg, "message must round-trip through multi-IDR encode/decode");
}
#[test]
fn multigop_gop_size_equals_n_frames_matches_legacy() {
let yuv = deterministic_yuv(32, 32, 3);
use super::super::decode_pixels::h264_stego_decode_yuv_string_4domain;
let legacy = h264_stego_encode_yuv_string_4domain(
&yuv, 32, 32, 3, "msg", "pass",
).expect("legacy encode");
let multigop = h264_stego_encode_yuv_string_4domain_multigop(
&yuv, 32, 32, 3, 3, "msg", "pass",
).expect("multigop encode");
let legacy_dec = h264_stego_decode_yuv_string_4domain(&legacy, "pass")
.expect("legacy decode");
let multigop_dec = h264_stego_decode_yuv_string_4domain(&multigop, "pass")
.expect("multigop decode");
assert_eq!(legacy_dec, "msg");
assert_eq!(multigop_dec, "msg");
}
#[test]
fn multigop_rejects_gop_size_zero() {
let yuv = deterministic_yuv(32, 32, 2);
let r = h264_stego_encode_yuv_string_4domain_multigop(
&yuv, 32, 32, 2, 0, "x", "p",
);
assert!(r.is_err());
}
#[test]
fn multigop_rejects_gop_size_larger_than_n_frames() {
let yuv = deterministic_yuv(32, 32, 2);
let r = h264_stego_encode_yuv_string_4domain_multigop(
&yuv, 32, 32, 2, 5, "x", "p",
);
assert!(r.is_err());
}
#[test]
fn shadow_roundtrip_recovers_both_messages() {
use super::super::decode_pixels::{
h264_stego_decode_yuv_string_4domain,
h264_stego_shadow_decode,
};
let yuv = correlated_yuv(64, 64, 4);
let primary = "primary";
let shadow = "shadow";
let primary_pass = "alice";
let shadow_pass = "bob";
let bytes = h264_stego_encode_yuv_string_with_shadow(
&yuv, 64, 64, 4, 4,
primary, primary_pass, shadow, shadow_pass,
).expect("shadow encode");
let recovered_primary = h264_stego_decode_yuv_string_4domain(
&bytes, primary_pass,
).expect("primary decode");
assert_eq!(recovered_primary, primary);
let recovered_shadow = h264_stego_shadow_decode(&bytes, shadow_pass)
.expect("shadow decode");
assert_eq!(recovered_shadow, shadow);
}
#[test]
fn shadow_roundtrip_handles_longer_primary_via_cascade() {
use super::super::decode_pixels::{
h264_stego_decode_yuv_string_4domain,
h264_stego_shadow_decode,
};
let yuv = correlated_yuv(64, 64, 4);
let primary = "primary message — a sentence of moderate length to drive primary STC's residual flips up";
let shadow = "hi";
let primary_pass = "alice";
let shadow_pass = "bob";
let bytes = h264_stego_encode_yuv_string_with_shadow(
&yuv, 64, 64, 4, 4,
primary, primary_pass, shadow, shadow_pass,
).expect("shadow encode (cascade should succeed within 6 tiers)");
let recovered_primary = h264_stego_decode_yuv_string_4domain(
&bytes, primary_pass,
).expect("primary decode");
assert_eq!(recovered_primary, primary);
let recovered_shadow = h264_stego_shadow_decode(&bytes, shadow_pass)
.expect("shadow decode");
assert_eq!(recovered_shadow, shadow);
}
#[test]
fn shadow_smart_decode_chooses_by_passphrase() {
use super::super::decode_pixels::h264_stego_smart_decode_video;
let yuv = correlated_yuv(64, 64, 4);
let primary = "primary";
let shadow = "shadow";
let bytes = h264_stego_encode_yuv_string_with_shadow(
&yuv, 64, 64, 4, 4,
primary, "alice", shadow, "bob",
).expect("shadow encode");
let with_alice = h264_stego_smart_decode_video(&bytes, "alice")
.expect("smart decode (primary pass)");
assert_eq!(with_alice, primary);
let with_bob = h264_stego_smart_decode_video(&bytes, "bob")
.expect("smart decode (shadow pass)");
assert_eq!(with_bob, shadow);
}
#[test]
fn shadow_wrong_passphrase_fails() {
use super::super::decode_pixels::h264_stego_shadow_decode;
let yuv = correlated_yuv(64, 64, 4);
let bytes = h264_stego_encode_yuv_string_with_shadow(
&yuv, 64, 64, 4, 4,
"primary", "alice", "shadow", "bob",
).expect("shadow encode");
let r = h264_stego_shadow_decode(&bytes, "wrong-passphrase");
assert!(r.is_err(), "wrong passphrase must fail shadow decode");
}
#[test]
fn n_shadows_roundtrip_n_equals_2() {
use super::super::decode_pixels::{
h264_stego_decode_yuv_string_4domain,
h264_stego_shadow_decode,
};
use crate::stego::shadow_layer::ShadowLayer;
let yuv = correlated_yuv(128, 128, 4);
let primary = "p";
let shadow_a = "a";
let shadow_b = "b";
let layers = [
ShadowLayer { message: shadow_a, passphrase: "alice", files: &[] },
ShadowLayer { message: shadow_b, passphrase: "bob", files: &[] },
];
let bytes = h264_stego_encode_yuv_string_with_n_shadows(
&yuv, 128, 128, 4, 4,
primary, "primary-pass",
&layers,
).expect("N=2 shadow encode");
let recovered_primary = h264_stego_decode_yuv_string_4domain(
&bytes, "primary-pass",
).expect("primary decode");
assert_eq!(recovered_primary, primary);
let recovered_a = h264_stego_shadow_decode(&bytes, "alice")
.expect("shadow alice decode");
assert_eq!(recovered_a, shadow_a);
let recovered_b = h264_stego_shadow_decode(&bytes, "bob")
.expect("shadow bob decode");
assert_eq!(recovered_b, shadow_b);
}
#[test]
fn n_shadows_rejects_duplicate_passphrases() {
use crate::stego::shadow_layer::ShadowLayer;
let yuv = correlated_yuv(128, 64, 4);
let layers = [
ShadowLayer { message: "a", passphrase: "shared", files: &[] },
ShadowLayer { message: "b", passphrase: "shared", files: &[] },
];
let r = h264_stego_encode_yuv_string_with_n_shadows(
&yuv, 128, 64, 4, 4,
"primary", "primary-pass",
&layers,
);
assert!(matches!(r, Err(StegoError::DuplicatePassphrase)));
let layers2 = [
ShadowLayer { message: "a", passphrase: "primary-pass", files: &[] },
];
let r2 = h264_stego_encode_yuv_string_with_n_shadows(
&yuv, 128, 64, 4, 4,
"primary", "primary-pass",
&layers2,
);
assert!(matches!(r2, Err(StegoError::DuplicatePassphrase)));
}
#[test]
fn shadow_capacity_returns_sensible_values() {
let yuv = correlated_yuv(128, 128, 4);
let cap_n0 = h264_stego_shadow_capacity(
&yuv, 128, 128, 4, 4, 0,
).expect("capacity n=0");
assert!(cap_n0.cover_size_bits > 0);
assert_eq!(cap_n0.max_message_bytes, 0);
let cap_n1 = h264_stego_shadow_capacity(
&yuv, 128, 128, 4, 4, 1,
).expect("capacity n=1");
assert_eq!(cap_n1.cover_size_bits, cap_n0.cover_size_bits);
assert!(cap_n1.max_message_bytes > 0);
let cap_n5 = h264_stego_shadow_capacity(
&yuv, 128, 128, 4, 4, 5,
).expect("capacity n=5");
assert!(
cap_n5.max_message_bytes <= cap_n1.max_message_bytes,
"max_message_bytes should not increase with shadow count"
);
}
#[test]
fn encoder_rejects_oversize_shadow_payload() {
use crate::stego::shadow_layer::ShadowLayer;
let yuv = correlated_yuv(64, 64, 4);
let cap = h264_stego_shadow_capacity(&yuv, 64, 64, 4, 4, 1)
.expect("capacity");
let oversize = "X".repeat(cap.max_message_bytes + 1024);
let layers = [ShadowLayer {
message: &oversize,
passphrase: "shadow-pass",
files: &[],
}];
let r = h264_stego_encode_yuv_string_with_n_shadows(
&yuv, 64, 64, 4, 4, "p", "primary-pass", &layers,
);
assert!(matches!(r, Err(StegoError::MessageTooLarge)));
}
#[test]
fn n_shadows_n_equals_0_matches_primary_only() {
use super::super::decode_pixels::h264_stego_decode_yuv_string_4domain;
let yuv = correlated_yuv(64, 64, 4);
let bytes = h264_stego_encode_yuv_string_with_n_shadows(
&yuv, 64, 64, 4, 4,
"primary", "primary-pass",
&[],
).expect("N=0 encode");
let recovered = h264_stego_decode_yuv_string_4domain(
&bytes, "primary-pass",
).expect("primary decode");
assert_eq!(recovered, "primary");
}
}