use crate::codec::h264::bitstream::{self};
use crate::codec::h264::cavlc::{EmbedDomain, EmbeddablePosition};
use crate::codec::h264::macroblock::{self, Macroblock, NeighborContext};
use crate::codec::h264::slice::{self, SliceType};
use crate::codec::h264::sps::{self, Pps, Sps};
use crate::codec::h264::NalType;
use crate::codec::mp4;
use crate::det_math::det_powi_f64;
use crate::stego::cost::{h264_cost, h264_uniward};
use crate::stego::crypto;
use crate::stego::error::StegoError;
use crate::stego::frame;
use crate::stego::payload;
use crate::stego::permute::{self, CoeffPos};
use crate::stego::stc::{embed as stc_embed_mod, extract as stc_extract_mod, hhat};
#[cfg(feature = "parallel")]
use rayon::prelude::*;
const STC_H: usize = 7;
const MAX_PARALLEL_GOPS_DEFAULT: usize = 4;
fn max_parallel_gops() -> usize {
#[cfg(not(feature = "parallel"))]
{
return 1;
}
#[cfg(feature = "parallel")]
{
std::env::var("PHASM_H264_PARALLEL_GOPS")
.ok()
.and_then(|s| s.parse::<usize>().ok())
.filter(|&n| n > 0)
.unwrap_or(MAX_PARALLEL_GOPS_DEFAULT)
}
}
#[derive(Debug, Clone, Copy)]
struct FlipOp {
abs_offset: usize,
mask: u8,
}
struct DomainStcBuild {
cover_bits: Vec<u8>,
stc_costs: Vec<f32>,
permuted_to_orig: Vec<usize>,
}
fn build_domain_stc(
usable: &[(usize, &EmbeddablePosition)],
all_positions: &[EmbeddablePosition],
costs: &[f32],
perm_seed: &[u8; 32],
track: &mp4::Track,
mp4_bytes: &[u8],
) -> DomainStcBuild {
let mut coeff_positions: Vec<(usize, CoeffPos)> = Vec::with_capacity(usable.len());
for &(orig_idx, _pos) in usable {
let cost = costs[orig_idx];
if cost.is_finite() {
coeff_positions.push((
orig_idx,
CoeffPos {
flat_idx: coeff_positions.len() as u32,
cost,
},
));
}
}
let mut stc_positions: Vec<CoeffPos> =
coeff_positions.iter().map(|(_, cp)| cp.clone()).collect();
permute::permute_positions(&mut stc_positions, perm_seed);
let mut cover_bits = Vec::with_capacity(stc_positions.len());
let mut stc_costs = Vec::with_capacity(stc_positions.len());
let mut permuted_to_orig: Vec<usize> = Vec::with_capacity(stc_positions.len());
for spos in &stc_positions {
let (orig_idx, _) = coeff_positions[spos.flat_idx as usize];
let epos = &all_positions[orig_idx];
let sample = &track.samples[epos.frame_idx as usize];
let abs_offset = sample.offset as usize + epos.raw_byte_offset;
if abs_offset < mp4_bytes.len() {
let current_bit = (mp4_bytes[abs_offset] >> (7 - epos.bit_offset)) & 1;
cover_bits.push(current_bit);
} else {
cover_bits.push(0);
}
stc_costs.push(spos.cost);
permuted_to_orig.push(orig_idx);
}
DomainStcBuild {
cover_bits,
stc_costs,
permuted_to_orig,
}
}
pub fn h264_ghost_encode(
mp4_bytes: &[u8],
message: &str,
passphrase: &str,
) -> Result<Vec<u8>, StegoError> {
let mut output = mp4_bytes.to_vec();
h264_ghost_encode_inplace(&mut output, message, passphrase)?;
Ok(output)
}
pub fn h264_ghost_encode_inplace(
mp4_bytes: &mut [u8],
message: &str,
passphrase: &str,
) -> Result<(), StegoError> {
let mp4_file = mp4::demux::demux(&*mp4_bytes)?;
let (sps, pps, length_size) = extract_h264_params(&mp4_file)?;
if pps.entropy_coding_mode_flag {
return Err(StegoError::InvalidVideo(
"H.264 CABAC not supported; input must be Baseline CAVLC".into(),
));
}
let track_idx = mp4_file
.video_track_idx
.ok_or(StegoError::InvalidVideo("no video track".into()))?;
let track = &mp4_file.tracks[track_idx];
let capacities = scan_gop_capacities(track, &sps, &pps, length_size, &*mp4_bytes)?;
if capacities.is_empty() {
return Err(StegoError::InvalidVideo(
"no GOPs found in video track".into(),
));
}
let total_n_coeff: usize = capacities.iter().map(|c| c.n_coeff).sum();
let total_n_mvd: usize = capacities.iter().map(|c| c.n_mvd).sum();
if total_n_coeff == 0 && total_n_mvd == 0 {
return Err(StegoError::InvalidVideo(
"no embeddable positions found in video".into(),
));
}
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();
let m_total = frame_bits.len();
if m_total > total_n_coeff + total_n_mvd {
return Err(StegoError::MessageTooLarge);
}
let (gop_m_coeff, gop_m_mvd) =
compute_per_gop_message_split(&capacities, m_total, total_n_coeff, total_n_mvd);
let mut w_chosen: Option<usize> = None;
for w_try in (1..=10usize).rev() {
let mut ok = true;
for (i, cap) in capacities.iter().enumerate() {
if gop_m_coeff[i] * w_try > cap.n_coeff || gop_m_mvd[i] * w_try > cap.n_mvd {
ok = false;
break;
}
}
if ok {
w_chosen = Some(w_try);
break;
}
}
let w = w_chosen.ok_or(StegoError::MessageTooLarge)?;
let coeff_master = crypto::derive_structural_key(passphrase)?;
let coeff_master_perm: [u8; 32] = coeff_master[..32].try_into().unwrap();
let coeff_master_hhat: [u8; 32] = coeff_master[32..].try_into().unwrap();
let mvd_master = crypto::derive_h264_mvd_structural_key(passphrase)?;
let mvd_master_perm: [u8; 32] = mvd_master[..32].try_into().unwrap();
let mvd_master_hhat: [u8; 32] = mvd_master[32..].try_into().unwrap();
let mut gop_frame_offsets: Vec<usize> = Vec::with_capacity(capacities.len());
{
let mut acc = 0usize;
for i in 0..capacities.len() {
gop_frame_offsets.push(acc);
acc += gop_m_coeff[i] + gop_m_mvd[i];
}
}
let max_parallel = max_parallel_gops();
let mut num_flips = 0usize;
let mut num_ep_skipped = 0usize;
for chunk in capacities.chunks(max_parallel) {
let mp4_immut: &[u8] = &*mp4_bytes;
let chunk_offset = chunk.as_ptr() as usize - capacities.as_ptr() as usize;
let chunk_start_idx = chunk_offset / std::mem::size_of::<GopCapacity>();
let work = |idx_in_chunk: usize, cap: &GopCapacity| -> Result<Vec<FlipOp>, StegoError> {
let i = chunk_start_idx + idx_in_chunk;
compute_gop_flips(
track,
&sps,
&pps,
length_size,
mp4_immut,
cap,
gop_frame_offsets[i],
gop_m_coeff[i],
gop_m_mvd[i],
w,
&frame_bits,
&coeff_master_perm,
&coeff_master_hhat,
&mvd_master_perm,
&mvd_master_hhat,
)
};
#[cfg(feature = "parallel")]
let chunk_flips: Result<Vec<Vec<FlipOp>>, StegoError> = chunk
.par_iter()
.enumerate()
.map(|(idx_in_chunk, cap)| work(idx_in_chunk, cap))
.collect();
#[cfg(not(feature = "parallel"))]
let chunk_flips: Result<Vec<Vec<FlipOp>>, StegoError> = chunk
.iter()
.enumerate()
.map(|(idx_in_chunk, cap)| work(idx_in_chunk, cap))
.collect();
let chunk_flips = chunk_flips?;
for flips in chunk_flips {
for op in flips {
if op.abs_offset >= mp4_bytes.len() {
continue;
}
if would_change_ep_pattern(mp4_bytes, op.abs_offset, op.mask) {
num_ep_skipped += 1;
continue;
}
mp4_bytes[op.abs_offset] ^= op.mask;
num_flips += 1;
}
}
}
if num_ep_skipped > 0 {
return Err(StegoError::InvalidVideo(format!(
"H.264 encode aborted: {num_ep_skipped} flip(s) would have created \
or destroyed an emulation-prevention byte sequence. This indicates \
coefficient data landed next to 0x00 0x00 bytes in the bitstream; \
re-encoding the cover at a different QP usually fixes it."
)));
}
let _ = num_flips;
Ok(())
}
pub fn h264_ghost_encode_path(
input_path: &std::path::Path,
output_path: &std::path::Path,
message: &str,
passphrase: &str,
) -> Result<(), StegoError> {
if input_path != output_path {
std::fs::copy(input_path, output_path)
.map_err(|e| StegoError::InvalidVideo(format!("copy failed: {e}")))?;
}
let file = std::fs::OpenOptions::new()
.read(true)
.write(true)
.open(output_path)
.map_err(|e| StegoError::InvalidVideo(format!("open output failed: {e}")))?;
let mut mmap = unsafe { memmap2::MmapMut::map_mut(&file) }
.map_err(|e| StegoError::InvalidVideo(format!("mmap failed: {e}")))?;
h264_ghost_encode_inplace(&mut mmap, message, passphrase)?;
mmap.flush()
.map_err(|e| StegoError::InvalidVideo(format!("mmap flush failed: {e}")))?;
Ok(())
}
pub fn h264_ghost_decode_path(
path: &std::path::Path,
passphrase: &str,
) -> Result<crate::stego::payload::PayloadData, StegoError> {
let file = std::fs::File::open(path)
.map_err(|e| StegoError::InvalidVideo(format!("open failed: {e}")))?;
let mmap = unsafe { memmap2::Mmap::map(&file) }
.map_err(|e| StegoError::InvalidVideo(format!("mmap failed: {e}")))?;
h264_ghost_decode(&mmap, passphrase)
}
pub fn h264_ghost_capacity_path(path: &std::path::Path) -> Result<usize, StegoError> {
let file = std::fs::File::open(path)
.map_err(|e| StegoError::InvalidVideo(format!("open failed: {e}")))?;
let mmap = unsafe { memmap2::Mmap::map(&file) }
.map_err(|e| StegoError::InvalidVideo(format!("mmap failed: {e}")))?;
h264_ghost_capacity(&mmap)
}
fn read_u16_be_from_bits(bits: &[u8]) -> u16 {
debug_assert!(bits.len() >= 16);
let mut v = 0u16;
for i in 0..16 {
v = (v << 1) | (bits[i] & 1) as u16;
}
v
}
fn read_u32_be_from_bits(bits: &[u8]) -> u32 {
debug_assert!(bits.len() >= 32);
let mut v = 0u32;
for i in 0..32 {
v = (v << 1) | (bits[i] & 1) as u32;
}
v
}
fn apply_domain_flips(
output: &mut [u8],
stego_bits: &[u8],
build: &DomainStcBuild,
all_positions: &[EmbeddablePosition],
track: &mp4::Track,
num_flips: &mut usize,
num_ep_skipped: &mut usize,
) {
for (perm_idx, &stego_bit) in stego_bits.iter().enumerate() {
if stego_bit == build.cover_bits[perm_idx] {
continue;
}
let orig_idx = build.permuted_to_orig[perm_idx];
let epos = &all_positions[orig_idx];
let sample = &track.samples[epos.frame_idx as usize];
let abs_offset = sample.offset as usize + epos.raw_byte_offset;
if abs_offset >= output.len() {
continue;
}
let mask = 1u8 << (7 - epos.bit_offset);
if would_change_ep_pattern(output, abs_offset, mask) {
*num_ep_skipped += 1;
continue;
}
output[abs_offset] ^= mask;
*num_flips += 1;
}
}
#[inline]
fn would_change_ep_pattern(output: &[u8], byte_offset: usize, mask: u8) -> bool {
let new_byte = output[byte_offset] ^ mask;
for offset in 0..3usize {
let Some(start) = byte_offset.checked_sub(offset) else { continue };
if start + 3 > output.len() {
continue;
}
let b0 = if start == byte_offset { new_byte } else { output[start] };
let b1 = if start + 1 == byte_offset { new_byte } else { output[start + 1] };
let b2 = if start + 2 == byte_offset { new_byte } else { output[start + 2] };
let orig_is_ep = output[start] == 0 && output[start + 1] == 0 && output[start + 2] <= 3;
let new_is_ep = b0 == 0 && b1 == 0 && b2 <= 3;
if orig_is_ep != new_is_ep {
return true;
}
}
false
}
pub fn h264_ghost_decode(
mp4_bytes: &[u8],
passphrase: &str,
) -> Result<crate::stego::payload::PayloadData, StegoError> {
let mp4_file = mp4::demux::demux(mp4_bytes)?;
let (sps, pps, length_size) = extract_h264_params(&mp4_file)?;
if pps.entropy_coding_mode_flag {
#[cfg(feature = "cabac-stego")]
{
return decode_cabac_via_chunk_6g(&mp4_file, length_size, passphrase);
}
#[cfg(not(feature = "cabac-stego"))]
return Err(StegoError::InvalidVideo(
"H.264 CABAC not supported for decode \
(build with --features cabac-stego to enable)".into(),
));
}
let track_idx = mp4_file
.video_track_idx
.ok_or(StegoError::InvalidVideo("no video track".into()))?;
let track = &mp4_file.tracks[track_idx];
let capacities = scan_gop_capacities(track, &sps, &pps, length_size, mp4_bytes)?;
if capacities.is_empty() {
return Err(StegoError::InvalidVideo("no GOPs found".into()));
}
let total_n_coeff: usize = capacities.iter().map(|c| c.n_coeff).sum();
let total_n_mvd: usize = capacities.iter().map(|c| c.n_mvd).sum();
let total_n = total_n_coeff + total_n_mvd;
if total_n == 0 {
return Err(StegoError::InvalidVideo("no embeddable positions".into()));
}
let coeff_master = crypto::derive_structural_key(passphrase)?;
let coeff_master_perm: [u8; 32] = coeff_master[..32].try_into().unwrap();
let coeff_master_hhat: [u8; 32] = coeff_master[32..].try_into().unwrap();
let mvd_master = crypto::derive_h264_mvd_structural_key(passphrase)?;
let mvd_master_perm: [u8; 32] = mvd_master[..32].try_into().unwrap();
let mvd_master_hhat: [u8; 32] = mvd_master[32..].try_into().unwrap();
let gop0_builds = scan_and_build_gop_stc(
track,
&sps,
&pps,
length_size,
mp4_bytes,
&capacities[0],
&coeff_master_perm,
&coeff_master_hhat,
&mvd_master_perm,
&mvd_master_hhat,
)?;
'w_loop: for w in (1..=10usize).rev() {
let n_c0 = gop0_builds.coeff.cover_bits.len();
let n_m0 = gop0_builds.mvd.cover_bits.len();
let m_c0_max = n_c0 / w;
let m_m0_max = n_m0 / w;
if m_c0_max == 0 && m_m0_max == 0 {
continue 'w_loop;
}
let coeff_extract_0 = if m_c0_max > 0 {
let hhat_c = hhat::generate_hhat(STC_H, w, &gop0_builds.coeff_hhat_seed);
stc_extract_mod::stc_extract(
&gop0_builds.coeff.cover_bits[..m_c0_max * w],
&hhat_c,
w,
)
} else {
Vec::new()
};
let mvd_extract_0 = if m_m0_max > 0 {
let hhat_m = hhat::generate_hhat(STC_H, w, &gop0_builds.mvd_hhat_seed);
stc_extract_mod::stc_extract(
&gop0_builds.mvd.cover_bits[..m_m0_max * w],
&hhat_m,
w,
)
} else {
Vec::new()
};
if coeff_extract_0.len() < 16 {
continue 'w_loop;
}
let length_prefix = read_u16_be_from_bits(&coeff_extract_0[0..16]);
let (total_payload_bytes, frame_overhead) = if length_prefix != 0 {
(length_prefix as usize, frame::FRAME_OVERHEAD)
} else {
if coeff_extract_0.len() < 48 {
continue 'w_loop;
}
let ext_len = read_u32_be_from_bits(&coeff_extract_0[16..48]);
(ext_len as usize, frame::FRAME_OVERHEAD_EXT)
};
let m_total = (frame_overhead + total_payload_bytes) * 8;
if m_total == 0 || m_total > total_n {
continue 'w_loop;
}
let (gop_m_c, gop_m_m) =
compute_per_gop_message_split(&capacities, m_total, total_n_coeff, total_n_mvd);
let mut fits = true;
for (i, cap) in capacities.iter().enumerate() {
if gop_m_c[i] * w > cap.n_coeff || gop_m_m[i] * w > cap.n_mvd {
fits = false;
break;
}
}
if !fits {
continue 'w_loop;
}
if gop_m_c[0] > coeff_extract_0.len() || gop_m_m[0] > mvd_extract_0.len() {
continue 'w_loop;
}
let mut message_bits = Vec::with_capacity(m_total);
message_bits.extend_from_slice(&coeff_extract_0[..gop_m_c[0]]);
message_bits.extend_from_slice(&mvd_extract_0[..gop_m_m[0]]);
let mut gop_failed = false;
for (i, cap) in capacities.iter().enumerate().skip(1) {
if gop_m_c[i] == 0 && gop_m_m[i] == 0 {
continue;
}
let builds = match scan_and_build_gop_stc(
track,
&sps,
&pps,
length_size,
mp4_bytes,
cap,
&coeff_master_perm,
&coeff_master_hhat,
&mvd_master_perm,
&mvd_master_hhat,
) {
Ok(b) => b,
Err(_) => {
gop_failed = true;
break;
}
};
let n_c = builds.coeff.cover_bits.len();
let n_m = builds.mvd.cover_bits.len();
if gop_m_c[i] * w > n_c || gop_m_m[i] * w > n_m {
gop_failed = true;
break;
}
let m_c_max = n_c / w;
let m_m_max = n_m / w;
let coeff_extract = if m_c_max > 0 {
let hhat_c = hhat::generate_hhat(STC_H, w, &builds.coeff_hhat_seed);
stc_extract_mod::stc_extract(&builds.coeff.cover_bits[..m_c_max * w], &hhat_c, w)
} else {
Vec::new()
};
let mvd_extract = if m_m_max > 0 {
let hhat_m = hhat::generate_hhat(STC_H, w, &builds.mvd_hhat_seed);
stc_extract_mod::stc_extract(&builds.mvd.cover_bits[..m_m_max * w], &hhat_m, w)
} else {
Vec::new()
};
if gop_m_c[i] > coeff_extract.len() || gop_m_m[i] > mvd_extract.len() {
gop_failed = true;
break;
}
message_bits.extend_from_slice(&coeff_extract[..gop_m_c[i]]);
message_bits.extend_from_slice(&mvd_extract[..gop_m_m[i]]);
}
if gop_failed {
continue 'w_loop;
}
let message_bytes: Vec<u8> = message_bits
.chunks(8)
.map(|chunk| {
let mut byte = 0u8;
for (i, &bit) in chunk.iter().enumerate() {
byte |= bit << (7 - i);
}
byte
})
.collect();
if let Ok(parsed) = frame::parse_frame(&message_bytes)
&& let Ok(plaintext) =
crypto::decrypt(&parsed.ciphertext, passphrase, &parsed.salt, &parsed.nonce)
&& let Ok(payload_data) = payload::decode_payload(&plaintext)
{
return Ok(payload_data);
}
}
Err(StegoError::DecryptionFailed)
}
#[allow(clippy::too_many_arguments)]
fn compute_gop_flips(
track: &mp4::Track,
sps: &Sps,
pps: &Pps,
length_size: u8,
mp4_bytes: &[u8],
cap: &GopCapacity,
frame_offset: usize,
m_g_c: usize,
m_g_m: usize,
w: usize,
frame_bits: &[u8],
coeff_master_perm: &[u8; 32],
coeff_master_hhat: &[u8; 32],
mvd_master_perm: &[u8; 32],
mvd_master_hhat: &[u8; 32],
) -> Result<Vec<FlipOp>, StegoError> {
if m_g_c == 0 && m_g_m == 0 {
return Ok(Vec::new());
}
let (positions, ac_energies, uniward_costs) = scan_frame_range(
track,
sps,
pps,
length_size,
mp4_bytes,
cap.range.first,
cap.range.last,
)?;
let costs = compute_all_costs(&positions, &ac_energies, &uniward_costs, track);
let usable: Vec<_> = positions
.iter()
.enumerate()
.filter(|(_, p)| {
let is_coeff = matches!(
p.domain,
EmbedDomain::T1Sign | EmbedDomain::LevelSuffixMag | EmbedDomain::LevelSuffixSign
);
(is_coeff && p.scan_pos > 0) || p.domain == EmbedDomain::MvdLsb
})
.collect();
let (coeff_usable, mvd_usable): (Vec<_>, Vec<_>) = usable
.iter()
.partition(|(_, p)| p.domain != EmbedDomain::MvdLsb);
let gop_id = cap.range.gop_idx;
let coeff_perm_seed =
crypto::derive_per_gop_seed_from_master(coeff_master_perm, gop_id, b"coeff-perm");
let coeff_hhat_seed =
crypto::derive_per_gop_seed_from_master(coeff_master_hhat, gop_id, b"coeff-hhat");
let mvd_perm_seed =
crypto::derive_per_gop_seed_from_master(mvd_master_perm, gop_id, b"mvd-perm");
let mvd_hhat_seed =
crypto::derive_per_gop_seed_from_master(mvd_master_hhat, gop_id, b"mvd-hhat");
let coeff_build = build_domain_stc(
&coeff_usable,
&positions,
&costs,
&coeff_perm_seed,
track,
mp4_bytes,
);
let mvd_build = build_domain_stc(
&mvd_usable,
&positions,
&costs,
&mvd_perm_seed,
track,
mp4_bytes,
);
let mut flips = Vec::new();
if m_g_c > 0 {
let n_used_c = m_g_c * w;
if coeff_build.cover_bits.len() < n_used_c {
return Err(StegoError::MessageTooLarge);
}
let hhat_c = hhat::generate_hhat(STC_H, w, &coeff_hhat_seed);
let embed_c = stc_embed_mod::stc_embed(
&coeff_build.cover_bits[..n_used_c],
&coeff_build.stc_costs[..n_used_c],
&frame_bits[frame_offset..frame_offset + m_g_c],
&hhat_c,
STC_H,
w,
)
.ok_or(StegoError::MessageTooLarge)?;
collect_domain_flips(&embed_c.stego_bits, &coeff_build, &positions, track, &mut flips);
}
if m_g_m > 0 {
let n_used_m = m_g_m * w;
if mvd_build.cover_bits.len() < n_used_m {
return Err(StegoError::MessageTooLarge);
}
let hhat_m = hhat::generate_hhat(STC_H, w, &mvd_hhat_seed);
let embed_m = stc_embed_mod::stc_embed(
&mvd_build.cover_bits[..n_used_m],
&mvd_build.stc_costs[..n_used_m],
&frame_bits[frame_offset + m_g_c..frame_offset + m_g_c + m_g_m],
&hhat_m,
STC_H,
w,
)
.ok_or(StegoError::MessageTooLarge)?;
collect_domain_flips(&embed_m.stego_bits, &mvd_build, &positions, track, &mut flips);
}
Ok(flips)
}
fn collect_domain_flips(
stego_bits: &[u8],
build: &DomainStcBuild,
all_positions: &[EmbeddablePosition],
track: &mp4::Track,
flips: &mut Vec<FlipOp>,
) {
for (perm_idx, &stego_bit) in stego_bits.iter().enumerate() {
if stego_bit == build.cover_bits[perm_idx] {
continue;
}
let orig_idx = build.permuted_to_orig[perm_idx];
let epos = &all_positions[orig_idx];
let sample = &track.samples[epos.frame_idx as usize];
let abs_offset = sample.offset as usize + epos.raw_byte_offset;
let mask = 1u8 << (7 - epos.bit_offset);
flips.push(FlipOp { abs_offset, mask });
}
}
struct GopStcBuilds {
coeff: DomainStcBuild,
mvd: DomainStcBuild,
coeff_hhat_seed: [u8; 32],
mvd_hhat_seed: [u8; 32],
}
fn scan_and_build_gop_stc(
track: &mp4::Track,
sps: &Sps,
pps: &Pps,
length_size: u8,
mp4_bytes: &[u8],
cap: &GopCapacity,
coeff_master_perm: &[u8; 32],
coeff_master_hhat: &[u8; 32],
mvd_master_perm: &[u8; 32],
mvd_master_hhat: &[u8; 32],
) -> Result<GopStcBuilds, StegoError> {
let (positions, ac_energies, uniward_costs) = scan_frame_range(
track,
sps,
pps,
length_size,
mp4_bytes,
cap.range.first,
cap.range.last,
)?;
let costs = compute_all_costs(&positions, &ac_energies, &uniward_costs, track);
let usable: Vec<_> = positions
.iter()
.enumerate()
.filter(|(_, p)| {
let is_coeff = matches!(
p.domain,
EmbedDomain::T1Sign | EmbedDomain::LevelSuffixMag | EmbedDomain::LevelSuffixSign
);
(is_coeff && p.scan_pos > 0) || p.domain == EmbedDomain::MvdLsb
})
.collect();
let (coeff_usable, mvd_usable): (Vec<_>, Vec<_>) = usable
.iter()
.partition(|(_, p)| p.domain != EmbedDomain::MvdLsb);
let gop_id = cap.range.gop_idx;
let coeff_perm_seed =
crypto::derive_per_gop_seed_from_master(coeff_master_perm, gop_id, b"coeff-perm");
let coeff_hhat_seed =
crypto::derive_per_gop_seed_from_master(coeff_master_hhat, gop_id, b"coeff-hhat");
let mvd_perm_seed =
crypto::derive_per_gop_seed_from_master(mvd_master_perm, gop_id, b"mvd-perm");
let mvd_hhat_seed =
crypto::derive_per_gop_seed_from_master(mvd_master_hhat, gop_id, b"mvd-hhat");
let coeff = build_domain_stc(
&coeff_usable,
&positions,
&costs,
&coeff_perm_seed,
track,
mp4_bytes,
);
let mvd = build_domain_stc(
&mvd_usable,
&positions,
&costs,
&mvd_perm_seed,
track,
mp4_bytes,
);
Ok(GopStcBuilds {
coeff,
mvd,
coeff_hhat_seed,
mvd_hhat_seed,
})
}
pub fn h264_ghost_capacity(mp4_bytes: &[u8]) -> Result<usize, StegoError> {
let mp4_file = mp4::demux::demux(mp4_bytes)?;
let (sps, pps, length_size) = extract_h264_params(&mp4_file)?;
if pps.entropy_coding_mode_flag {
return Err(StegoError::InvalidVideo("CABAC not supported".into()));
}
let track_idx = mp4_file
.video_track_idx
.ok_or(StegoError::InvalidVideo("no video track".into()))?;
let track = &mp4_file.tracks[track_idx];
let capacities = scan_gop_capacities(track, &sps, &pps, length_size, mp4_bytes)?;
let usable_count: usize = capacities
.iter()
.map(|c| c.n_coeff + c.n_mvd)
.sum();
let message_bits = usable_count / 5;
let payload_bytes = message_bits.saturating_sub(frame::FRAME_OVERHEAD * 8) / 8;
Ok(payload_bytes)
}
pub fn h264_ghost_capacity_max(mp4_bytes: &[u8]) -> Result<usize, StegoError> {
let mp4_file = mp4::demux::demux(mp4_bytes)?;
let (sps, pps, length_size) = extract_h264_params(&mp4_file)?;
if pps.entropy_coding_mode_flag {
return Err(StegoError::InvalidVideo("CABAC not supported".into()));
}
let track_idx = mp4_file
.video_track_idx
.ok_or(StegoError::InvalidVideo("no video track".into()))?;
let track = &mp4_file.tracks[track_idx];
let capacities = scan_gop_capacities(track, &sps, &pps, length_size, mp4_bytes)?;
let usable_count: usize = capacities
.iter()
.map(|c| c.n_coeff + c.n_mvd)
.sum();
let message_bits = usable_count;
let payload_bytes = message_bits.saturating_sub(frame::FRAME_OVERHEAD * 8) / 8;
Ok(payload_bytes)
}
pub fn h264_ghost_capacity_max_path(path: &std::path::Path) -> Result<usize, StegoError> {
let file = std::fs::File::open(path)
.map_err(|e| StegoError::InvalidVideo(format!("open failed: {e}")))?;
let mmap = unsafe { memmap2::Mmap::map(&file) }
.map_err(|e| StegoError::InvalidVideo(format!("mmap failed: {e}")))?;
h264_ghost_capacity_max(&mmap)
}
#[cfg(feature = "cabac-stego")]
fn decode_cabac_via_chunk_6g(
mp4_file: &mp4::Mp4File,
length_size: u8,
passphrase: &str,
) -> Result<crate::stego::payload::PayloadData, StegoError> {
use crate::codec::h264::bitstream::{parse_nal_unit, parse_nal_units_mp4};
use crate::codec::h264::stego::decode_pixels::
h264_stego_decode_nalus_string;
let track_idx = mp4_file
.video_track_idx
.ok_or(StegoError::InvalidVideo("no video track".into()))?;
let track = &mp4_file.tracks[track_idx];
let avcc = track.avcc_data.as_ref().ok_or(
StegoError::InvalidVideo("no avcC configuration".into())
)?;
let mut nalus: Vec<crate::codec::h264::NalUnit> = Vec::new();
for sps_bytes in &avcc.sps_nalus {
if sps_bytes.is_empty() {
continue;
}
let nu = parse_nal_unit(sps_bytes)
.map_err(|e| StegoError::InvalidVideo(format!("avcC SPS: {e}")))?;
nalus.push(nu);
}
for pps_bytes in &avcc.pps_nalus {
if pps_bytes.is_empty() {
continue;
}
let nu = parse_nal_unit(pps_bytes)
.map_err(|e| StegoError::InvalidVideo(format!("avcC PPS: {e}")))?;
nalus.push(nu);
}
for sample in &track.samples {
if sample.data.is_empty() {
continue;
}
let sample_nalus = parse_nal_units_mp4(&sample.data, length_size)
.map_err(|e| StegoError::InvalidVideo(
format!("sample NAL parse: {e}")
))?;
nalus.extend(sample_nalus);
}
let text = h264_stego_decode_nalus_string(&nalus, passphrase)?;
Ok(crate::stego::payload::PayloadData {
text,
files: Vec::new(),
})
}
fn extract_h264_params(mp4_file: &mp4::Mp4File) -> Result<(Sps, Pps, u8), StegoError> {
let track_idx = mp4_file
.video_track_idx
.ok_or(StegoError::InvalidVideo("no video track".into()))?;
let track = &mp4_file.tracks[track_idx];
if !track.is_h264() {
return Err(StegoError::InvalidVideo(format!(
"video track codec {:?} is not H.264",
std::str::from_utf8(&track.codec).unwrap_or("????")
)));
}
let avcc = track
.avcc_data
.as_ref()
.ok_or(StegoError::InvalidVideo("no avcC configuration".into()))?;
let length_size = avcc.length_size_minus1 + 1;
if avcc.sps_nalus.is_empty() {
return Err(StegoError::InvalidVideo("no SPS in avcC".into()));
}
let sps_nalu = &avcc.sps_nalus[0];
let sps_rbsp = if !sps_nalu.is_empty() && (sps_nalu[0] & 0x1F) == 7 {
bitstream::remove_emulation_prevention(&sps_nalu[1..])
} else {
bitstream::remove_emulation_prevention(sps_nalu)
};
let sps_parsed = sps::parse_sps(&sps_rbsp)?;
if avcc.pps_nalus.is_empty() {
return Err(StegoError::InvalidVideo("no PPS in avcC".into()));
}
let pps_nalu = &avcc.pps_nalus[0];
let pps_rbsp = if !pps_nalu.is_empty() && (pps_nalu[0] & 0x1F) == 8 {
bitstream::remove_emulation_prevention(&pps_nalu[1..])
} else {
bitstream::remove_emulation_prevention(pps_nalu)
};
let pps_parsed = sps::parse_pps(&pps_rbsp)?;
Ok((sps_parsed, pps_parsed, length_size))
}
struct PendingIFrameDrift {
ref_map: crate::stego::cost::h264_ddca::InterFrameRefMap,
i_frame_idx: usize,
entries: Vec<(usize, usize, usize, f32)>,
}
pub fn scan_frames_for_stealth_analysis(
mp4_bytes: &[u8],
) -> Result<(Vec<EmbeddablePosition>, Vec<f32>, Vec<Option<f32>>), StegoError> {
let mp4_file = mp4::demux::demux(mp4_bytes)?;
let (sps, pps, length_size) = extract_h264_params(&mp4_file)?;
if pps.entropy_coding_mode_flag {
return Err(StegoError::InvalidVideo(
"H.264 CABAC not supported; input must be Baseline CAVLC".into(),
));
}
let track_idx = mp4_file
.video_track_idx
.ok_or(StegoError::InvalidVideo("no video track".into()))?;
let track = &mp4_file.tracks[track_idx];
let last_frame = track.samples.len().saturating_sub(1);
scan_frame_range(track, &sps, &pps, length_size, mp4_bytes, 0, last_frame)
}
#[derive(Debug, Clone, Copy)]
struct GopRange {
gop_idx: u32,
first: usize,
last: usize, }
fn discover_gops(track: &mp4::Track) -> Vec<GopRange> {
let n = track.samples.len();
if n == 0 {
return Vec::new();
}
let mut gops = Vec::new();
let mut current_first = 0usize;
let mut next_idx = 0u32;
for i in 1..n {
if track.samples[i].is_sync {
gops.push(GopRange {
gop_idx: next_idx,
first: current_first,
last: i - 1,
});
next_idx += 1;
current_first = i;
}
}
gops.push(GopRange {
gop_idx: next_idx,
first: current_first,
last: n - 1,
});
gops
}
#[derive(Debug, Clone, Copy)]
struct GopCapacity {
range: GopRange,
n_coeff: usize,
n_mvd: usize,
}
fn scan_gop_capacities(
track: &mp4::Track,
sps: &Sps,
pps: &Pps,
length_size: u8,
mp4_bytes: &[u8],
) -> Result<Vec<GopCapacity>, StegoError> {
let gops = discover_gops(track);
let mut caps = Vec::with_capacity(gops.len());
for range in gops {
let (positions, ac_energies, uniward_costs) = scan_frame_range(
track,
sps,
pps,
length_size,
mp4_bytes,
range.first,
range.last,
)?;
let costs = compute_all_costs(&positions, &ac_energies, &uniward_costs, track);
let mut n_coeff = 0usize;
let mut n_mvd = 0usize;
for (i, p) in positions.iter().enumerate() {
if !costs[i].is_finite() {
continue;
}
let is_coeff = matches!(
p.domain,
EmbedDomain::T1Sign | EmbedDomain::LevelSuffixMag | EmbedDomain::LevelSuffixSign
);
if is_coeff && p.scan_pos > 0 {
n_coeff += 1;
} else if p.domain == EmbedDomain::MvdLsb {
n_mvd += 1;
}
}
caps.push(GopCapacity {
range,
n_coeff,
n_mvd,
});
}
Ok(caps)
}
fn compute_per_gop_message_split(
capacities: &[GopCapacity],
m_total: usize,
total_n_coeff: usize,
total_n_mvd: usize,
) -> (Vec<usize>, Vec<usize>) {
let total_n = total_n_coeff + total_n_mvd;
let m_c_total = if total_n > 0 {
m_total * total_n_coeff / total_n
} else {
0
};
let m_m_total = m_total.saturating_sub(m_c_total);
let mut gop_m_c = Vec::with_capacity(capacities.len());
let mut gop_m_m = Vec::with_capacity(capacities.len());
let mut allocated_m_c = 0usize;
let mut allocated_m_m = 0usize;
for cap in capacities {
let m_g_c = if total_n_coeff > 0 {
m_c_total * cap.n_coeff / total_n_coeff
} else {
0
};
let m_g_m = if total_n_mvd > 0 {
m_m_total * cap.n_mvd / total_n_mvd
} else {
0
};
gop_m_c.push(m_g_c);
gop_m_m.push(m_g_m);
allocated_m_c += m_g_c;
allocated_m_m += m_g_m;
}
if let Some(idx) = capacities.iter().rposition(|c| c.n_coeff > 0) {
gop_m_c[idx] += m_c_total.saturating_sub(allocated_m_c);
}
if let Some(idx) = capacities.iter().rposition(|c| c.n_mvd > 0) {
gop_m_m[idx] += m_m_total.saturating_sub(allocated_m_m);
}
(gop_m_c, gop_m_m)
}
fn scan_frame_range(
track: &mp4::Track,
sps: &Sps,
pps: &Pps,
length_size: u8,
_mp4_bytes: &[u8],
first_frame_inclusive: usize,
last_frame_inclusive: usize,
) -> Result<(Vec<EmbeddablePosition>, Vec<f32>, Vec<Option<f32>>), StegoError> {
let mut all_positions = Vec::new();
let mut all_ac_energies = Vec::new();
let mut uniward_costs: Vec<Option<f32>> = Vec::new();
let mut gop_position = 0u32;
let mut global_block_idx = 0u32;
let mut pending_drift: Option<PendingIFrameDrift> = None;
let ddca_params = crate::stego::cost::h264_ddca::DdcaParams::default();
let height_in_mbs = if sps.frame_mbs_only_flag {
sps.pic_height_in_map_units
} else {
sps.pic_height_in_map_units * 2
};
let n_frames = last_frame_inclusive
.saturating_add(1)
.saturating_sub(first_frame_inclusive);
for (frame_idx, sample) in track
.samples
.iter()
.enumerate()
.skip(first_frame_inclusive)
.take(n_frames)
{
if sample.data.is_empty() {
continue;
}
if sample.is_sync {
gop_position = 0;
}
let ls = length_size as usize;
let mut nal_pos = 0usize;
while nal_pos + ls <= sample.data.len() {
let mut nal_len = 0usize;
for i in 0..ls {
nal_len = (nal_len << 8) | sample.data[nal_pos + i] as usize;
}
let nal_data_start = nal_pos + ls;
let nal_data_end = (nal_data_start + nal_len).min(sample.data.len());
nal_pos = nal_data_start + nal_len;
if nal_len == 0 || nal_data_start >= sample.data.len() {
continue;
}
let nal_data = &sample.data[nal_data_start..nal_data_end];
if nal_data.is_empty() {
continue;
}
let nal_type = NalType(nal_data[0] & 0x1F);
let nal_ref_idc = (nal_data[0] >> 5) & 0x03;
if !nal_type.is_slice() {
continue;
}
let raw_payload = &nal_data[1..];
let (rbsp, ep_map) = bitstream::remove_emulation_prevention_with_map(raw_payload);
let slice_hdr =
slice::parse_slice_header(&rbsp, sps, pps, nal_type, nal_ref_idc)
.map_err(|e| {
StegoError::InvalidVideo(format!("frame {frame_idx} slice header: {e}"))
})?;
let _data_byte = slice_hdr.data_bit_offset / 8;
let _data_bit = (slice_hdr.data_bit_offset % 8) as u8;
let mut reader = bitstream::RbspReader::new(&rbsp);
reader.skip_bits(slice_hdr.data_bit_offset as u32)
.map_err(|e| StegoError::InvalidVideo(format!("skip to data: {e}")))?;
let mut neighbor_ctx = NeighborContext::new(sps.pic_width_in_mbs, height_in_mbs);
let mut mv_ctx = crate::codec::h264::mv::MvPredictorContext::new(
sps.pic_width_in_mbs,
height_in_mbs,
);
let mut current_qp = slice_hdr.slice_qp;
let total_mbs = sps.pic_size_in_mbs;
let mut mb_idx = slice_hdr.first_mb_in_slice;
let _slice_start_mb = mb_idx;
let mut _parse_ok_count = 0usize;
let mut _parse_skip_count = 0usize;
let mut _parse_fail_info: Option<(u32, String)> = None;
let is_intra_slice = matches!(slice_hdr.slice_type, SliceType::I | SliceType::SI);
let capture_recon = is_intra_slice;
let mut frame_mbs: Vec<Macroblock> = if is_intra_slice {
(0..total_mbs as usize)
.map(|_| default_macroblock())
.collect()
} else {
Vec::new()
};
let mut frame_qps: Vec<i32> = if is_intra_slice {
vec![slice_hdr.slice_qp; total_mbs as usize]
} else {
Vec::new()
};
let mut slice_parsed_mbs: Vec<(usize, usize, usize)> = Vec::new();
while mb_idx < total_mbs {
if slice_hdr.slice_type == SliceType::P || slice_hdr.slice_type == SliceType::SP {
if reader.bits_remaining() < 2 {
break;
}
let skip_run = reader.read_ue()
.map_err(|e| StegoError::InvalidVideo(format!("mb_skip_run: {e}")))?;
for _ in 0..skip_run {
if mb_idx >= total_mbs {
break;
}
let sx = mb_idx % sps.pic_width_in_mbs;
let sy = mb_idx / sps.pic_width_in_mbs;
for blk in 0..16 {
let (bpx, bpy) = crate::codec::h264::macroblock::BLOCK_INDEX_TO_POS[blk];
let bx = (sx * 4 + bpx as u32) as usize;
let by = (sy * 4 + bpy as u32) as usize;
neighbor_ctx.set_luma_tc(bx, by, 0);
}
all_ac_energies.extend([0.0; 26]);
global_block_idx += 26;
mb_idx += 1;
_parse_skip_count += 1;
}
if mb_idx >= total_mbs || skip_run > 0 && reader.bits_remaining() < 2 {
break;
}
if skip_run > 0 {
}
}
if mb_idx >= total_mbs || reader.bits_remaining() < 2 {
break;
}
let mb_x = mb_idx % sps.pic_width_in_mbs;
let mb_y = mb_idx / sps.pic_width_in_mbs;
let _mb_bit_pos_before = reader.bits_read();
let mb_result = macroblock::parse_macroblock_with_recon(
&mut reader,
slice_hdr.slice_type,
mb_x,
mb_y,
sps,
pps,
&mut neighbor_ctx,
&ep_map,
raw_payload,
&mut current_qp,
slice_hdr.num_ref_idx_l0_active,
capture_recon,
Some(&mut mv_ctx),
);
match mb_result {
Ok(mb) => {
_parse_ok_count += 1;
for blk_idx in 0..16 {
let tc = mb.luma_total_coeffs[blk_idx];
all_ac_energies.push(tc as f32 * 2.0);
}
all_ac_energies.push(0.0);
all_ac_energies.push(0.0);
for blk_idx in 0..4 {
let tc = mb.chroma_total_coeffs[blk_idx];
all_ac_energies.push(tc as f32 * 2.0);
}
for blk_idx in 4..8 {
let tc = mb.chroma_total_coeffs[blk_idx];
all_ac_energies.push(tc as f32 * 2.0);
}
let pos_start = all_positions.len();
let positions_moved: Vec<_> = mb.positions.to_vec();
let mb_idx_val = global_block_idx / 26;
for mut pos in positions_moved {
pos.frame_idx = frame_idx as u16;
pos.mb_idx = mb_idx_val;
if pos.block_idx != u32::MAX
&& pos.block_idx != crate::codec::h264::mv::MVD_BLOCK_IDX_SENTINEL
{
pos.block_idx += global_block_idx;
}
pos.raw_byte_offset += nal_data_start + 1;
all_positions.push(pos);
uniward_costs.push(None);
}
let pos_end = all_positions.len();
if is_intra_slice {
frame_qps[mb_idx as usize] = current_qp;
frame_mbs[mb_idx as usize] = mb;
slice_parsed_mbs.push((mb_idx as usize, pos_start, pos_end));
} else if let Some(pending) = pending_drift.as_mut() {
if let Some(mv_field) = mb.mv_field.as_ref() {
let hops = frame_idx.saturating_sub(pending.i_frame_idx + 1) as i32;
let decay =
det_powi_f64(ddca_params.inter_frame_decay as f64, hops) as f32;
let mb_x_usize =
(mb_idx % sps.pic_width_in_mbs) as usize;
let mb_y_usize =
(mb_idx / sps.pic_width_in_mbs) as usize;
pending.ref_map.accumulate_mv_field(
mv_field,
mb_x_usize,
mb_y_usize,
decay,
);
}
}
global_block_idx += 26;
}
Err(_e) => {
if _parse_fail_info.is_none() {
_parse_fail_info = Some((mb_idx, format!("{:?} (at bit {})", _e, _mb_bit_pos_before)));
}
break;
}
}
mb_idx += 1;
}
if std::env::var("PHASM_H264_DEBUG").is_ok() {
eprintln!("[h264-parse] frame={} slice_type={:?} qp={} parsed={}+skip={}/{} (start={}) bits_left={} num_ref_l0={}",
frame_idx, slice_hdr.slice_type, slice_hdr.slice_qp, _parse_ok_count, _parse_skip_count, total_mbs - _slice_start_mb, _slice_start_mb, reader.bits_remaining(), slice_hdr.num_ref_idx_l0_active);
if let Some((failed_mb, err)) = _parse_fail_info {
eprintln!(" FIRST FAIL at MB {}: {}", failed_mb, err);
}
}
if is_intra_slice && !slice_parsed_mbs.is_empty() {
let width = sps.pic_width_in_mbs as usize * 16;
let height = height_in_mbs as usize * 16;
let planes =
crate::codec::h264::reconstruct::reconstruct_i_frame_planes(
&frame_mbs,
sps.pic_width_in_mbs as usize,
height_in_mbs as usize,
);
let mut frame_pos: Vec<h264_uniward::FramePosition> = Vec::new();
let mut cost_target_idx: Vec<usize> = Vec::new();
for &(mb_idx_in_frame, pos_start, pos_end) in &slice_parsed_mbs {
let (qp_cb, qp_cr) = frame_mbs
.get(mb_idx_in_frame)
.and_then(|mb| mb.recon.as_ref())
.map_or((26, 26), |r| (r.qp_cb, r.qp_cr));
for global_pos_idx in pos_start..pos_end {
let pos = &all_positions[global_pos_idx];
if pos.block_idx == u32::MAX
|| pos.block_idx == crate::codec::h264::mv::MVD_BLOCK_IDX_SENTINEL
{
continue;
}
let within_mb = (pos.block_idx % 26) as usize;
frame_pos.push(h264_uniward::FramePosition {
pos,
mb_idx: mb_idx_in_frame,
within_mb_block_idx: within_mb,
qp_cb,
qp_cr,
});
cost_target_idx.push(global_pos_idx);
}
}
let frame_planes = h264_uniward::FramePlanes {
y: &planes.y,
cb: &planes.cb,
cr: &planes.cr,
width,
height,
};
let scores = h264_uniward::compute_frame_uniward_costs(
&frame_planes,
&frame_pos,
&frame_qps,
);
let mode_map = crate::stego::cost::h264_ddca::IntraModeMap::build(
&frame_mbs,
sps.pic_width_in_mbs as usize,
height_in_mbs as usize,
);
let adjusted = crate::stego::cost::h264_ddca::apply_drift_multipliers(
&frame_pos,
&scores,
&mode_map,
sps.pic_width_in_mbs as usize,
&ddca_params,
);
finalize_pending_drift(&mut pending_drift, &mut uniward_costs, &ddca_params);
let width_in_4x4 = sps.pic_width_in_mbs as usize * 4;
let height_in_4x4 = height_in_mbs as usize * 4;
let mut entries: Vec<(usize, usize, usize, f32)> =
Vec::with_capacity(cost_target_idx.len());
for (i, &cost) in adjusted.iter().enumerate() {
let fp = &frame_pos[i];
let (bx, by) = if fp.within_mb_block_idx < 16 {
let (dx, dy) = crate::codec::h264::macroblock::BLOCK_INDEX_TO_POS
[fp.within_mb_block_idx];
let mb_x = fp.mb_idx % sps.pic_width_in_mbs as usize;
let mb_y = fp.mb_idx / sps.pic_width_in_mbs as usize;
(mb_x * 4 + dx as usize, mb_y * 4 + dy as usize)
} else {
(usize::MAX, usize::MAX)
};
entries.push((cost_target_idx[i], bx, by, cost));
}
pending_drift = Some(PendingIFrameDrift {
ref_map: crate::stego::cost::h264_ddca::InterFrameRefMap::new(
width_in_4x4,
height_in_4x4,
),
i_frame_idx: frame_idx,
entries,
});
}
}
gop_position += 1;
}
finalize_pending_drift(&mut pending_drift, &mut uniward_costs, &ddca_params);
Ok((all_positions, all_ac_energies, uniward_costs))
}
fn finalize_pending_drift(
pending_drift: &mut Option<PendingIFrameDrift>,
uniward_costs: &mut [Option<f32>],
params: &crate::stego::cost::h264_ddca::DdcaParams,
) {
let Some(pending) = pending_drift.take() else {
return;
};
for (cost_idx, bx, by, base_cost) in pending.entries {
if bx == usize::MAX || by == usize::MAX {
uniward_costs[cost_idx] = Some(base_cost);
continue;
}
let refs = pending.ref_map.ref_count(bx, by);
let adjusted = base_cost * (1.0 + params.w_inter_drift * refs);
uniward_costs[cost_idx] = Some(adjusted);
}
}
fn default_macroblock() -> Macroblock {
Macroblock {
mb_type: crate::codec::h264::macroblock::MbType::P16x16,
mb_qp_delta: 0,
coded_block_pattern: 0,
luma_total_coeffs: [0; 16],
chroma_total_coeffs: [0; 8],
positions: Vec::new(),
recon: None,
mv_field: None,
}
}
fn compute_all_costs(
positions: &[EmbeddablePosition],
ac_energies: &[f32],
uniward_costs: &[Option<f32>],
track: &mp4::Track,
) -> Vec<f32> {
let gop_length = estimate_gop_length(track);
positions
.iter()
.enumerate()
.map(|(i, pos)| {
if let Some(uw) = uniward_costs.get(i).copied().flatten()
&& uw.is_finite() && uw > 0.0 {
return uw;
}
let frame_idx = pos.frame_idx as usize;
let is_sync = frame_idx < track.samples.len() && track.samples[frame_idx].is_sync;
let slice_type = if is_sync { SliceType::I } else { SliceType::P };
let mut gop_pos = 0u32;
for i in (0..=frame_idx).rev() {
if i < track.samples.len() && track.samples[i].is_sync {
gop_pos = (frame_idx - i) as u32;
break;
}
}
h264_cost::compute_h264_costs(
std::slice::from_ref(pos),
ac_energies,
slice_type,
gop_pos,
gop_length,
)[0]
})
.collect()
}
fn estimate_gop_length(track: &mp4::Track) -> u32 {
let sync_count = track.samples.iter().filter(|s| s.is_sync).count();
if sync_count <= 1 {
track.samples.len() as u32
} else {
(track.samples.len() as u32) / (sync_count as u32)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn estimate_gop_length_single_iframe() {
let track = mp4::Track {
track_id: 1,
handler_type: *b"vide",
codec: *b"avc1",
width: 320,
height: 240,
timescale: 30000,
duration: 60000,
samples: vec![
mp4::Sample { offset: 0, size: 100, is_sync: true, data: vec![] },
mp4::Sample { offset: 100, size: 50, is_sync: false, data: vec![] },
mp4::Sample { offset: 150, size: 50, is_sync: false, data: vec![] },
],
hvcc_data: None,
avcc_data: None,
stsd_raw: vec![],
trak_raw: vec![],
};
assert_eq!(estimate_gop_length(&track), 3);
}
#[test]
fn estimate_gop_length_two_iframes() {
let track = mp4::Track {
track_id: 1,
handler_type: *b"vide",
codec: *b"avc1",
width: 320,
height: 240,
timescale: 30000,
duration: 120000,
samples: vec![
mp4::Sample { offset: 0, size: 100, is_sync: true, data: vec![] },
mp4::Sample { offset: 100, size: 50, is_sync: false, data: vec![] },
mp4::Sample { offset: 150, size: 50, is_sync: false, data: vec![] },
mp4::Sample { offset: 200, size: 100, is_sync: true, data: vec![] },
mp4::Sample { offset: 300, size: 50, is_sync: false, data: vec![] },
mp4::Sample { offset: 350, size: 50, is_sync: false, data: vec![] },
],
hvcc_data: None,
avcc_data: None,
stsd_raw: vec![],
trak_raw: vec![],
};
assert_eq!(estimate_gop_length(&track), 3);
}
}