use super::{write_u32, write_u64, AvccData, Mp4Error};
use crate::codec::h264::NalType;
#[cfg(feature = "h264-encoder")]
use crate::codec::h264::stego::gop_pattern::{iter_encode_order, FrameType, GopPattern};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MuxerProfile {
HandbrakeX264,
}
#[derive(Debug, Clone, Copy)]
pub struct FrameTiming {
pub fps_num: u32,
pub fps_den: u32,
}
impl FrameTiming {
pub const FPS_30: Self = Self { fps_num: 30, fps_den: 1 };
}
pub fn build_mp4(
profile: MuxerProfile,
annex_b: &[u8],
width: u32,
height: u32,
timing: FrameTiming,
) -> Result<Vec<u8>, Mp4Error> {
if width == 0 || height == 0 {
return Err(Mp4Error::InvalidBox(format!(
"invalid dimensions {width}x{height}"
)));
}
if timing.fps_num == 0 || timing.fps_den == 0 {
return Err(Mp4Error::InvalidBox(format!(
"invalid frame rate {}/{}",
timing.fps_num, timing.fps_den
)));
}
let nals = parse_annexb_nals(annex_b);
if nals.is_empty() {
return Err(Mp4Error::InvalidBox("no NAL units in input".into()));
}
let avcc = AvccData::from_annexb(annex_b)
.ok_or_else(|| Mp4Error::InvalidBox("missing SPS or PPS in input".into()))?;
let access_units = group_access_units(&nals);
if access_units.is_empty() {
return Err(Mp4Error::InvalidBox("no access units in input".into()));
}
match profile {
MuxerProfile::HandbrakeX264 => build_handbrake_x264(
annex_b,
&access_units,
&avcc,
width,
height,
timing,
None,
None,
),
}
}
#[cfg(feature = "h264-encoder")]
pub fn build_mp4_with_pattern(
profile: MuxerProfile,
annex_b: &[u8],
width: u32,
height: u32,
timing: FrameTiming,
pattern: GopPattern,
n_display_frames: usize,
) -> Result<Vec<u8>, Mp4Error> {
if width == 0 || height == 0 {
return Err(Mp4Error::InvalidBox(format!(
"invalid dimensions {width}x{height}"
)));
}
if timing.fps_num == 0 || timing.fps_den == 0 {
return Err(Mp4Error::InvalidBox(format!(
"invalid frame rate {}/{}",
timing.fps_num, timing.fps_den
)));
}
let nals = parse_annexb_nals(annex_b);
if nals.is_empty() {
return Err(Mp4Error::InvalidBox("no NAL units in input".into()));
}
let avcc = AvccData::from_annexb(annex_b)
.ok_or_else(|| Mp4Error::InvalidBox("missing SPS or PPS in input".into()))?;
let access_units = group_access_units(&nals);
if access_units.is_empty() {
return Err(Mp4Error::InvalidBox("no access units in input".into()));
}
let encode_order: Vec<_> = iter_encode_order(n_display_frames, pattern).collect();
if encode_order.len() != access_units.len() {
return Err(Mp4Error::InvalidBox(format!(
"pattern emits {} frames but input has {} access units",
encode_order.len(),
access_units.len(),
)));
}
let any_b_frame = encode_order.iter().any(|f| f.frame_type == FrameType::B);
let composition_offsets: Option<Vec<i32>> = if any_b_frame {
Some(
encode_order
.iter()
.map(|f| {
let display = f.display_idx as i32;
let encode = f.encode_idx as i32;
(display - encode) * (handbrake::STTS_DELTA_PER_FRAME as i32)
})
.collect(),
)
} else {
None
};
match profile {
MuxerProfile::HandbrakeX264 => build_handbrake_x264(
annex_b,
&access_units,
&avcc,
width,
height,
timing,
composition_offsets.as_deref(),
None,
),
}
}
#[cfg(feature = "h264-encoder")]
pub fn build_mp4_with_pattern_audio(
profile: MuxerProfile,
annex_b: &[u8],
width: u32,
height: u32,
timing: FrameTiming,
pattern: GopPattern,
n_display_frames: usize,
source_mp4: &[u8],
) -> Result<Vec<u8>, Mp4Error> {
if width == 0 || height == 0 {
return Err(Mp4Error::InvalidBox(format!(
"invalid dimensions {width}x{height}"
)));
}
if timing.fps_num == 0 || timing.fps_den == 0 {
return Err(Mp4Error::InvalidBox(format!(
"invalid frame rate {}/{}",
timing.fps_num, timing.fps_den
)));
}
let nals = parse_annexb_nals(annex_b);
if nals.is_empty() {
return Err(Mp4Error::InvalidBox("no NAL units in input".into()));
}
let avcc = AvccData::from_annexb(annex_b)
.ok_or_else(|| Mp4Error::InvalidBox("missing SPS or PPS in input".into()))?;
let access_units = group_access_units(&nals);
if access_units.is_empty() {
return Err(Mp4Error::InvalidBox("no access units in input".into()));
}
let encode_order: Vec<_> = iter_encode_order(n_display_frames, pattern).collect();
if encode_order.len() != access_units.len() {
return Err(Mp4Error::InvalidBox(format!(
"pattern emits {} frames but input has {} access units",
encode_order.len(),
access_units.len(),
)));
}
let any_b_frame = encode_order.iter().any(|f| f.frame_type == FrameType::B);
let composition_offsets: Option<Vec<i32>> = if any_b_frame {
Some(
encode_order
.iter()
.map(|f| {
let display = f.display_idx as i32;
let encode = f.encode_idx as i32;
(display - encode) * (handbrake::STTS_DELTA_PER_FRAME as i32)
})
.collect(),
)
} else {
None
};
let audio = AudioPassthrough::extract_first(source_mp4)?;
match profile {
MuxerProfile::HandbrakeX264 => build_handbrake_x264(
annex_b,
&access_units,
&avcc,
width,
height,
timing,
composition_offsets.as_deref(),
audio.as_ref(),
),
}
}
pub struct AudioPassthrough<'a> {
samples: Vec<&'a [u8]>,
trak_raw: Vec<u8>,
duration_media: u64,
chunks: Vec<(usize, usize)>,
}
impl<'a> AudioPassthrough<'a> {
fn extract_first(source_mp4: &'a [u8]) -> Result<Option<Self>, Mp4Error> {
let parsed = super::demux::demux(source_mp4)?;
let audio_idx = parsed
.tracks
.iter()
.position(|t| &t.handler_type == b"soun");
let audio_idx = match audio_idx {
Some(i) => i,
None => return Ok(None),
};
let track = &parsed.tracks[audio_idx];
if track.samples.is_empty() {
return Ok(None);
}
let mut samples: Vec<&[u8]> = Vec::with_capacity(track.samples.len());
for s in &track.samples {
let start = s.offset as usize;
let end = start + s.size as usize;
if end > source_mp4.len() {
return Err(Mp4Error::UnexpectedEof);
}
samples.push(&source_mp4[start..end]);
}
let chunks = reconstruct_chunk_layout(&track.trak_raw, track.samples.len())?;
Ok(Some(AudioPassthrough {
samples,
trak_raw: track.trak_raw.clone(),
duration_media: track.duration,
chunks,
}))
}
}
mod handbrake {
pub const FTYP_MAJOR: &[u8; 4] = b"isom";
pub const FTYP_MINOR: u32 = 0x0000_0200;
pub const FTYP_COMPATIBLE: &[&[u8; 4]] =
&[b"isom", b"iso2", b"avc1", b"mp41"];
pub const MVHD_TIMESCALE: u32 = 1000;
pub fn mdhd_timescale(fps_num: u32, fps_den: u32) -> u32 {
let num = (fps_num as u64) * 512;
let den = fps_den as u64;
((num + den / 2) / den) as u32
}
pub const STTS_DELTA_PER_FRAME: u32 = 512;
pub const UDTA_TOO: &str = "HandBrake 1.7.3 2024010100";
pub const HDLR_VIDE_NAME: &str = "VideoHandler";
pub const X264_SEI_UUID: [u8; 16] = [
0xDC, 0x45, 0xE9, 0xBD, 0xE6, 0xD9, 0x48, 0xB7,
0x96, 0x2C, 0xD8, 0x20, 0xD9, 0x23, 0xEE, 0xEF,
];
pub const X264_SEI_PLAINTEXT: &str = concat!(
"x264 - core 164 r3107 a8b68eb",
" - H.264/MPEG-4 AVC codec",
" - Copyleft 2003-2024",
" - http://www.videolan.org/x264.html",
" - options: cabac=1 ref=3 deblock=1:0:0 analyse=0x3:0x113",
" me=hex subme=7 psy=1 psy_rd=1.00:0.00 mixed_ref=1",
" me_range=16 chroma_me=1 trellis=1 8x8dct=1 cqm=0",
" deadzone=21,11 fast_pskip=1 chroma_qp_offset=-2",
" threads=6 lookahead_threads=1 sliced_threads=0 nr=0",
" decimate=1 interlaced=0 bluray_compat=0",
" constrained_intra=0 bframes=3 b_pyramid=2",
" b_adapt=1 b_bias=0 direct=1 weightb=1 open_gop=0",
" weightp=2 keyint=250 keyint_min=25 scenecut=40",
" intra_refresh=0 rc_lookahead=40 rc=crf mbtree=1",
" crf=23.0 qcomp=0.60 qpmin=0 qpmax=69 qpstep=4",
" ip_ratio=1.40 aq=1:1.00",
"\0",
);
}
#[allow(clippy::too_many_arguments)]
fn build_handbrake_x264(
annex_b: &[u8],
access_units: &[AccessUnit],
avcc: &AvccData,
width: u32,
height: u32,
timing: FrameTiming,
composition_offsets: Option<&[i32]>,
audio: Option<&AudioPassthrough<'_>>,
) -> Result<Vec<u8>, Mp4Error> {
let mdhd_timescale = handbrake::mdhd_timescale(timing.fps_num, timing.fps_den);
let frame_count = access_units.len() as u32;
let track_duration_media: u64 =
(frame_count as u64) * (handbrake::STTS_DELTA_PER_FRAME as u64);
let movie_duration_ms: u64 = (frame_count as u64) * 1000 * (timing.fps_den as u64)
/ (timing.fps_num as u64);
let x264_sei_nal = build_x264_sei_user_data_unregistered();
let mut sei_consumed = false;
let mut sample_data: Vec<Vec<u8>> = Vec::with_capacity(access_units.len());
for au in access_units {
let mut buf = Vec::new();
if au.is_sync && !sei_consumed {
let len = x264_sei_nal.len() as u32;
buf.extend_from_slice(&len.to_be_bytes());
buf.extend_from_slice(&x264_sei_nal);
sei_consumed = true;
}
for &(nal_start, nal_end, nal_type) in &au.nals {
if nal_type == NalType::AUD {
continue; }
if nal_type == NalType::SPS || nal_type == NalType::PPS {
continue;
}
let nal = &annex_b[nal_start..nal_end];
let len = nal.len() as u32;
buf.extend_from_slice(&len.to_be_bytes());
buf.extend_from_slice(nal);
}
sample_data.push(buf);
}
let sample_sizes: Vec<u32> = sample_data.iter().map(|s| s.len() as u32).collect();
let video_payload: u64 = sample_sizes.iter().map(|&s| s as u64).sum();
let audio_payload: u64 = audio
.map(|a| a.samples.iter().map(|s| s.len() as u64).sum::<u64>())
.unwrap_or(0);
let total_mdat_payload: u64 = video_payload + audio_payload;
let ftyp = build_ftyp_handbrake();
let ftyp_len = ftyp.len() as u64;
let mdat_header_len: u64 = if total_mdat_payload + 8 > u32::MAX as u64 { 16 } else { 8 };
let mdat_total_size = mdat_header_len + total_mdat_payload;
let first_sample_offset = ftyp_len + mdat_header_len;
let mut sample_offsets: Vec<u64> = Vec::with_capacity(sample_data.len());
let mut cursor = first_sample_offset;
for &size in &sample_sizes {
sample_offsets.push(cursor);
cursor += size as u64;
}
let mut audio_sample_offsets: Vec<u64> = Vec::new();
if let Some(a) = audio {
for s in &a.samples {
audio_sample_offsets.push(cursor);
cursor += s.len() as u64;
}
}
let sync_samples: Vec<u32> = access_units
.iter()
.enumerate()
.filter_map(|(i, au)| if au.is_sync { Some(i as u32 + 1) } else { None })
.collect();
let audio_trak_patched: Option<Vec<u8>> = audio.map(|a| {
let mut trak = a.trak_raw.clone();
let chunk_offsets: Vec<u64> = a
.chunks
.iter()
.map(|&(first_idx, _spc)| {
audio_sample_offsets
.get(first_idx)
.copied()
.unwrap_or(0)
})
.collect();
patch_trak_stco(&mut trak, &chunk_offsets)?;
patch_trak_track_id(&mut trak, 2)?;
Ok::<Vec<u8>, Mp4Error>(trak)
}).transpose()?;
let audio_duration_movie: Option<u64> = audio.map(|a| {
let mdhd_ts = read_audio_mdhd_timescale(&a.trak_raw).unwrap_or(48000) as u64;
a.duration_media * (handbrake::MVHD_TIMESCALE as u64) / mdhd_ts
});
let movie_duration_final = audio_duration_movie
.map(|ad| ad.max(movie_duration_ms))
.unwrap_or(movie_duration_ms);
let moov = build_moov_handbrake(MoovParams {
width,
height,
movie_duration_ms: movie_duration_final,
track_duration_media,
mdhd_timescale,
sample_sizes: &sample_sizes,
sample_offsets: &sample_offsets,
sync_samples: &sync_samples,
avcc,
composition_offsets,
audio_trak: audio_trak_patched.as_deref(),
});
let mut out = Vec::with_capacity(ftyp.len() + mdat_total_size as usize + moov.len());
out.extend_from_slice(&ftyp);
if mdat_header_len == 16 {
write_u32(&mut out, 1); out.extend_from_slice(b"mdat");
write_u64(&mut out, mdat_total_size);
} else {
write_u32(&mut out, mdat_total_size as u32);
out.extend_from_slice(b"mdat");
}
for sample in &sample_data {
out.extend_from_slice(sample);
}
if let Some(a) = audio {
for s in &a.samples {
out.extend_from_slice(s);
}
}
out.extend_from_slice(&moov);
Ok(out)
}
fn build_ftyp_handbrake() -> Vec<u8> {
let mut content = Vec::new();
content.extend_from_slice(handbrake::FTYP_MAJOR);
content.extend_from_slice(&handbrake::FTYP_MINOR.to_be_bytes());
for &brand in handbrake::FTYP_COMPATIBLE {
content.extend_from_slice(brand);
}
wrap_box(b"ftyp", &content)
}
struct MoovParams<'a> {
width: u32,
height: u32,
movie_duration_ms: u64,
track_duration_media: u64,
mdhd_timescale: u32,
sample_sizes: &'a [u32],
sample_offsets: &'a [u64],
sync_samples: &'a [u32],
avcc: &'a AvccData,
composition_offsets: Option<&'a [i32]>,
audio_trak: Option<&'a [u8]>,
}
fn build_moov_handbrake(p: MoovParams<'_>) -> Vec<u8> {
let next_track_id: u32 = if p.audio_trak.is_some() { 3 } else { 2 };
let mvhd = build_mvhd(p.movie_duration_ms, next_track_id);
let trak = build_video_trak_handbrake(&p);
let udta = build_udta_handbrake();
let mut moov = Vec::new();
moov.extend_from_slice(&mvhd);
moov.extend_from_slice(&trak);
if let Some(audio_trak) = p.audio_trak {
moov.extend_from_slice(audio_trak);
}
moov.extend_from_slice(&udta);
wrap_box(b"moov", &moov)
}
fn build_mvhd(duration_ms: u64, next_track_id: u32) -> Vec<u8> {
let mut content = Vec::new();
content.extend_from_slice(&[0, 0, 0, 0]);
content.extend_from_slice(&[0; 4]);
content.extend_from_slice(&[0; 4]);
content.extend_from_slice(&handbrake::MVHD_TIMESCALE.to_be_bytes());
content.extend_from_slice(&(duration_ms as u32).to_be_bytes());
content.extend_from_slice(&0x0001_0000u32.to_be_bytes());
content.extend_from_slice(&0x0100u16.to_be_bytes());
content.extend_from_slice(&[0; 10]);
content.extend_from_slice(&UNITY_MATRIX);
content.extend_from_slice(&[0; 24]);
content.extend_from_slice(&next_track_id.to_be_bytes());
wrap_box(b"mvhd", &content)
}
const UNITY_MATRIX: [u8; 36] = [
0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00,
];
fn build_video_trak_handbrake(p: &MoovParams<'_>) -> Vec<u8> {
let tkhd = build_tkhd_video(p.movie_duration_ms, p.width, p.height);
let edts = build_edts_handbrake(p);
let mdia = build_mdia_video_handbrake(p);
let mut trak = Vec::new();
trak.extend_from_slice(&tkhd);
if let Some(edts) = edts {
trak.extend_from_slice(&edts);
}
trak.extend_from_slice(&mdia);
wrap_box(b"trak", &trak)
}
fn build_edts_handbrake(p: &MoovParams<'_>) -> Option<Vec<u8>> {
let offsets = p.composition_offsets?;
if offsets.is_empty() {
return None;
}
let min_ctts = offsets.iter().copied().min().unwrap_or(0);
if min_ctts >= 0 {
return None; }
let pre_roll_media: u64 = (-min_ctts) as u64;
let pre_roll_movie: u64 = pre_roll_media * (handbrake::MVHD_TIMESCALE as u64) /
(p.mdhd_timescale as u64);
let track_duration_movie: u64 = p.movie_duration_ms;
let mut content = Vec::new();
content.extend_from_slice(&[0, 0, 0, 0]); content.extend_from_slice(&1u32.to_be_bytes()); content.extend_from_slice(&((track_duration_movie + pre_roll_movie) as u32).to_be_bytes());
content.extend_from_slice(&(pre_roll_media as u32).to_be_bytes()); content.extend_from_slice(&0x0001_0000u32.to_be_bytes()); let elst = wrap_box(b"elst", &content);
Some(wrap_box(b"edts", &elst))
}
fn build_tkhd_video(duration_ms: u64, width: u32, height: u32) -> Vec<u8> {
let mut content = Vec::new();
content.extend_from_slice(&[0, 0x00, 0x00, 0x07]);
content.extend_from_slice(&[0; 4]); content.extend_from_slice(&[0; 4]); content.extend_from_slice(&1u32.to_be_bytes()); content.extend_from_slice(&[0; 4]); content.extend_from_slice(&(duration_ms as u32).to_be_bytes());
content.extend_from_slice(&[0; 8]); content.extend_from_slice(&[0; 2]); content.extend_from_slice(&[0; 2]); content.extend_from_slice(&[0; 2]); content.extend_from_slice(&[0; 2]); content.extend_from_slice(&UNITY_MATRIX);
content.extend_from_slice(&(width << 16).to_be_bytes());
content.extend_from_slice(&(height << 16).to_be_bytes());
wrap_box(b"tkhd", &content)
}
fn build_mdia_video_handbrake(p: &MoovParams<'_>) -> Vec<u8> {
let mdhd = build_mdhd(p.track_duration_media, p.mdhd_timescale);
let hdlr = build_hdlr_vide();
let minf = build_minf_video_handbrake(p);
let mut mdia = Vec::new();
mdia.extend_from_slice(&mdhd);
mdia.extend_from_slice(&hdlr);
mdia.extend_from_slice(&minf);
wrap_box(b"mdia", &mdia)
}
fn build_mdhd(duration: u64, timescale: u32) -> Vec<u8> {
let mut content = Vec::new();
content.extend_from_slice(&[0, 0, 0, 0]); content.extend_from_slice(&[0; 4]); content.extend_from_slice(&[0; 4]); content.extend_from_slice(×cale.to_be_bytes());
content.extend_from_slice(&(duration as u32).to_be_bytes());
content.extend_from_slice(&[0x55, 0xC4]);
content.extend_from_slice(&[0; 2]); wrap_box(b"mdhd", &content)
}
fn build_hdlr_vide() -> Vec<u8> {
let mut content = Vec::new();
content.extend_from_slice(&[0, 0, 0, 0]); content.extend_from_slice(&[0; 4]); content.extend_from_slice(b"vide");
content.extend_from_slice(&[0; 12]); content.extend_from_slice(handbrake::HDLR_VIDE_NAME.as_bytes());
content.push(0); wrap_box(b"hdlr", &content)
}
fn build_minf_video_handbrake(p: &MoovParams<'_>) -> Vec<u8> {
let vmhd = build_vmhd();
let dinf = build_dinf();
let stbl = build_stbl_video_handbrake(p);
let mut minf = Vec::new();
minf.extend_from_slice(&vmhd);
minf.extend_from_slice(&dinf);
minf.extend_from_slice(&stbl);
wrap_box(b"minf", &minf)
}
fn build_vmhd() -> Vec<u8> {
let mut content = Vec::new();
content.extend_from_slice(&[0, 0, 0, 1]);
content.extend_from_slice(&[0; 8]);
wrap_box(b"vmhd", &content)
}
fn build_dinf() -> Vec<u8> {
let mut url = Vec::new();
url.extend_from_slice(&[0, 0, 0, 1]); let url_box = wrap_box(b"url ", &url);
let mut dref_content = Vec::new();
dref_content.extend_from_slice(&[0, 0, 0, 0]); dref_content.extend_from_slice(&1u32.to_be_bytes()); dref_content.extend_from_slice(&url_box);
let dref = wrap_box(b"dref", &dref_content);
wrap_box(b"dinf", &dref)
}
fn build_stbl_video_handbrake(p: &MoovParams<'_>) -> Vec<u8> {
let chunks = chunk_layout_per_gop(p.sample_sizes.len() as u32, p.sync_samples);
let chunk_offsets = chunk_offsets_for_layout(&chunks, p.sample_offsets);
let stsd = build_stsd_avc1(p.width, p.height, p.avcc);
let stts = build_stts_uniform(p.sample_sizes.len() as u32);
let ctts = p.composition_offsets.map(build_ctts);
let stsc = build_stsc(&chunks);
let stsz = build_stsz(p.sample_sizes);
let stco = build_stco(&chunk_offsets);
let stss = build_stss(p.sync_samples);
let mut stbl = Vec::new();
stbl.extend_from_slice(&stsd);
stbl.extend_from_slice(&stts);
if let Some(ctts) = ctts {
stbl.extend_from_slice(&ctts);
}
stbl.extend_from_slice(&stss);
stbl.extend_from_slice(&stsc);
stbl.extend_from_slice(&stsz);
stbl.extend_from_slice(&stco);
wrap_box(b"stbl", &stbl)
}
fn build_ctts(offsets: &[i32]) -> Vec<u8> {
let mut content = Vec::new();
content.extend_from_slice(&[1, 0, 0, 0]); content.extend_from_slice(&(offsets.len() as u32).to_be_bytes());
for &off in offsets {
content.extend_from_slice(&1u32.to_be_bytes()); content.extend_from_slice(&off.to_be_bytes()); }
wrap_box(b"ctts", &content)
}
fn chunk_layout_per_gop(sample_count: u32, sync_samples: &[u32]) -> Vec<u32> {
if sync_samples.is_empty() || sample_count == 0 {
return vec![sample_count];
}
let mut chunks = Vec::with_capacity(sync_samples.len());
for w in sync_samples.windows(2) {
chunks.push(w[1] - w[0]);
}
chunks.push(sample_count + 1 - sync_samples[sync_samples.len() - 1]);
chunks
}
fn chunk_offsets_for_layout(chunks: &[u32], sample_offsets: &[u64]) -> Vec<u64> {
let mut out = Vec::with_capacity(chunks.len());
let mut idx = 0usize;
for &spc in chunks {
if idx < sample_offsets.len() {
out.push(sample_offsets[idx]);
} else {
out.push(0);
}
idx += spc as usize;
}
out
}
fn build_stsd_avc1(width: u32, height: u32, avcc: &AvccData) -> Vec<u8> {
let avc1 = build_avc1(width, height, avcc);
let mut content = Vec::new();
content.extend_from_slice(&[0, 0, 0, 0]); content.extend_from_slice(&1u32.to_be_bytes()); content.extend_from_slice(&avc1);
wrap_box(b"stsd", &content)
}
fn build_avc1(width: u32, height: u32, avcc: &AvccData) -> Vec<u8> {
let mut content = Vec::new();
content.extend_from_slice(&[0; 6]); content.extend_from_slice(&1u16.to_be_bytes()); content.extend_from_slice(&[0; 2]); content.extend_from_slice(&[0; 2]); content.extend_from_slice(&[0; 12]); content.extend_from_slice(&(width as u16).to_be_bytes());
content.extend_from_slice(&(height as u16).to_be_bytes());
content.extend_from_slice(&0x0048_0000u32.to_be_bytes()); content.extend_from_slice(&0x0048_0000u32.to_be_bytes()); content.extend_from_slice(&[0; 4]); content.extend_from_slice(&1u16.to_be_bytes()); content.extend_from_slice(&[0; 32]); content.extend_from_slice(&0x0018u16.to_be_bytes()); content.extend_from_slice(&[0xFF, 0xFF]);
let avcc_box = wrap_box(b"avcC", &avcc.to_bytes());
content.extend_from_slice(&avcc_box);
wrap_box(b"avc1", &content)
}
fn build_stts_uniform(sample_count: u32) -> Vec<u8> {
let mut content = Vec::new();
content.extend_from_slice(&[0, 0, 0, 0]); content.extend_from_slice(&1u32.to_be_bytes()); content.extend_from_slice(&sample_count.to_be_bytes());
content.extend_from_slice(&handbrake::STTS_DELTA_PER_FRAME.to_be_bytes());
wrap_box(b"stts", &content)
}
fn build_stsc(chunk_sizes: &[u32]) -> Vec<u8> {
let mut entries: Vec<(u32, u32)> = Vec::new(); for (i, &spc) in chunk_sizes.iter().enumerate() {
if entries.last().is_none_or(|&(_, prev_spc)| prev_spc != spc) {
entries.push((i as u32 + 1, spc));
}
}
let mut content = Vec::new();
content.extend_from_slice(&[0, 0, 0, 0]); content.extend_from_slice(&(entries.len() as u32).to_be_bytes());
for (first_chunk, spc) in entries {
content.extend_from_slice(&first_chunk.to_be_bytes());
content.extend_from_slice(&spc.to_be_bytes());
content.extend_from_slice(&1u32.to_be_bytes()); }
wrap_box(b"stsc", &content)
}
fn build_stsz(sample_sizes: &[u32]) -> Vec<u8> {
let mut content = Vec::new();
content.extend_from_slice(&[0, 0, 0, 0]); content.extend_from_slice(&0u32.to_be_bytes()); content.extend_from_slice(&(sample_sizes.len() as u32).to_be_bytes());
for &s in sample_sizes {
content.extend_from_slice(&s.to_be_bytes());
}
wrap_box(b"stsz", &content)
}
fn build_stco(chunk_offsets: &[u64]) -> Vec<u8> {
let needs_co64 = chunk_offsets.iter().any(|&o| o > u32::MAX as u64);
let mut content = Vec::new();
content.extend_from_slice(&[0, 0, 0, 0]); content.extend_from_slice(&(chunk_offsets.len() as u32).to_be_bytes());
if needs_co64 {
for &o in chunk_offsets {
content.extend_from_slice(&o.to_be_bytes());
}
wrap_box(b"co64", &content)
} else {
for &o in chunk_offsets {
content.extend_from_slice(&(o as u32).to_be_bytes());
}
wrap_box(b"stco", &content)
}
}
fn build_stss(sync_samples: &[u32]) -> Vec<u8> {
let mut content = Vec::new();
content.extend_from_slice(&[0, 0, 0, 0]); content.extend_from_slice(&(sync_samples.len() as u32).to_be_bytes());
for &s in sync_samples {
content.extend_from_slice(&s.to_be_bytes());
}
wrap_box(b"stss", &content)
}
fn build_udta_handbrake() -> Vec<u8> {
let mut too_content = Vec::new();
let s = handbrake::UDTA_TOO.as_bytes();
too_content.extend_from_slice(&(s.len() as u16).to_be_bytes());
too_content.extend_from_slice(&0u16.to_be_bytes()); too_content.extend_from_slice(s);
let too_box = wrap_box(&[0xA9, b't', b'o', b'o'], &too_content);
wrap_box(b"udta", &too_box)
}
fn build_x264_sei_user_data_unregistered() -> Vec<u8> {
let mut payload = Vec::new();
payload.extend_from_slice(&handbrake::X264_SEI_UUID);
payload.extend_from_slice(handbrake::X264_SEI_PLAINTEXT.as_bytes());
let mut rbsp = Vec::new();
rbsp.push(0x06);
rbsp.push(5);
let mut remaining = payload.len();
while remaining >= 0xFF {
rbsp.push(0xFF);
remaining -= 0xFF;
}
rbsp.push(remaining as u8);
rbsp.extend_from_slice(&payload);
rbsp.push(0x80);
add_emulation_prevention(&rbsp)
}
fn add_emulation_prevention(rbsp: &[u8]) -> Vec<u8> {
let mut out = Vec::with_capacity(rbsp.len());
if !rbsp.is_empty() {
out.push(rbsp[0]);
}
let mut zero_run = 0usize;
for &b in &rbsp[1..] {
if zero_run >= 2 && b <= 0x03 {
out.push(0x03);
zero_run = 0;
}
out.push(b);
if b == 0 {
zero_run += 1;
} else {
zero_run = 0;
}
}
out
}
#[derive(Debug, Clone)]
struct ParsedNal {
start: usize,
end: usize,
nal_type: NalType,
}
fn parse_annexb_nals(bytes: &[u8]) -> Vec<ParsedNal> {
let starts = find_start_codes(bytes);
let mut out = Vec::with_capacity(starts.len());
for (k, &(payload_start, _sc_len)) in starts.iter().enumerate() {
let payload_end = if k + 1 < starts.len() {
let next = starts[k + 1];
next.0 - next.1
} else {
bytes.len()
};
if payload_start >= payload_end {
continue;
}
let nal_type = NalType(bytes[payload_start] & 0x1F);
out.push(ParsedNal { start: payload_start, end: payload_end, nal_type });
}
out
}
fn find_start_codes(bytes: &[u8]) -> Vec<(usize, usize)> {
let mut out = Vec::new();
let mut i = 0;
while i + 3 <= bytes.len() {
if bytes[i] == 0 && bytes[i + 1] == 0 {
if i + 4 <= bytes.len() && bytes[i + 2] == 0 && bytes[i + 3] == 1 {
out.push((i + 4, 4));
i += 4;
continue;
} else if bytes[i + 2] == 1 {
out.push((i + 3, 3));
i += 3;
continue;
}
}
i += 1;
}
out
}
#[derive(Debug, Clone)]
struct AccessUnit {
nals: Vec<(usize, usize, NalType)>,
is_sync: bool,
}
fn group_access_units(nals: &[ParsedNal]) -> Vec<AccessUnit> {
let mut out: Vec<AccessUnit> = Vec::new();
let mut current = AccessUnit { nals: Vec::new(), is_sync: false };
let mut current_has_vcl = false;
for n in nals {
let is_vcl = n.nal_type.is_vcl();
let is_aud = n.nal_type == NalType::AUD;
let starts_new = is_aud
|| (is_vcl && current_has_vcl);
if starts_new && !current.nals.is_empty() {
out.push(std::mem::replace(
&mut current,
AccessUnit { nals: Vec::new(), is_sync: false },
));
current_has_vcl = false;
}
current.nals.push((n.start, n.end, n.nal_type));
if is_vcl {
current_has_vcl = true;
if n.nal_type.is_idr() {
current.is_sync = true;
}
}
}
if !current.nals.is_empty() {
out.push(current);
}
out.retain(|au| au.nals.iter().any(|&(_, _, t)| t.is_vcl()));
out
}
fn wrap_box(box_type: &[u8; 4], content: &[u8]) -> Vec<u8> {
let total_size = (8 + content.len()) as u32;
let mut out = Vec::with_capacity(total_size as usize);
out.extend_from_slice(&total_size.to_be_bytes());
out.extend_from_slice(box_type);
out.extend_from_slice(content);
out
}
fn find_subbox_offset(buf: &[u8], target: &[u8; 4]) -> Option<usize> {
fn recurse(buf: &[u8], start: usize, end: usize, target: &[u8; 4]) -> Option<usize> {
let mut found = None;
let _ = super::iterate_boxes(buf, start, end, |h, content_start, _| {
if found.is_some() {
return Ok(());
}
let box_start = content_start - h.header_len as usize;
if h.box_type == *target {
found = Some(box_start);
} else if matches!(
&h.box_type,
b"trak" | b"mdia" | b"minf" | b"stbl" | b"edts"
) {
let inner_end = box_start + h.size as usize;
if let Some(inner) = recurse(buf, content_start, inner_end, target) {
found = Some(inner);
}
}
Ok(())
});
found
}
recurse(buf, 0, buf.len(), target)
}
fn reconstruct_chunk_layout(
trak: &[u8],
n_samples: usize,
) -> Result<Vec<(usize, usize)>, Mp4Error> {
if n_samples == 0 {
return Ok(Vec::new());
}
let (num_chunks, is_co64) = if let Some(off) = find_subbox_offset(trak, b"stco") {
let h = super::parse_box_header(trak, off)?;
let cs = off + h.header_len as usize;
(super::read_u32(trak, cs + 4)? as usize, false)
} else if let Some(off) = find_subbox_offset(trak, b"co64") {
let h = super::parse_box_header(trak, off)?;
let cs = off + h.header_len as usize;
(super::read_u32(trak, cs + 4)? as usize, true)
} else {
return Ok(Vec::new());
};
let _ = is_co64;
let stsc_entries = if let Some(off) = find_subbox_offset(trak, b"stsc") {
let h = super::parse_box_header(trak, off)?;
let cs = off + h.header_len as usize;
super::demux::parse_stsc(trak, cs)?
} else {
Vec::new()
};
let mut samples_per_chunk = vec![0u32; num_chunks];
if stsc_entries.is_empty() {
samples_per_chunk.fill(1);
} else {
for (i, entry) in stsc_entries.iter().enumerate() {
let first_chunk = entry.0 as usize;
let spc = entry.1;
let next_first = if i + 1 < stsc_entries.len() {
stsc_entries[i + 1].0 as usize
} else {
num_chunks + 1
};
for chunk_idx in first_chunk..next_first {
if (1..=num_chunks).contains(&chunk_idx) {
samples_per_chunk[chunk_idx - 1] = spc;
}
}
}
}
let mut chunks = Vec::with_capacity(num_chunks);
let mut sample_idx = 0usize;
for &spc in &samples_per_chunk {
let count = spc as usize;
chunks.push((sample_idx, count));
sample_idx += count;
}
Ok(chunks)
}
fn patch_trak_stco(trak: &mut [u8], new_chunk_offsets: &[u64]) -> Result<(), Mp4Error> {
if let Some(off) = find_subbox_offset(trak, b"stco") {
let h = super::parse_box_header(trak, off)?;
let cs = off + h.header_len as usize;
let count = super::read_u32(trak, cs + 4)? as usize;
if count != new_chunk_offsets.len() {
return Err(Mp4Error::InvalidBox(format!(
"stco entry count mismatch in audio trak: {} vs {}",
count,
new_chunk_offsets.len()
)));
}
if new_chunk_offsets.iter().any(|&o| o > u32::MAX as u64) {
return Err(Mp4Error::InvalidBox(
"audio trak stco offsets exceed 32-bit; co64 upgrade not supported".into(),
));
}
for (i, &offset) in new_chunk_offsets.iter().enumerate() {
let pos = cs + 8 + i * 4;
trak[pos..pos + 4].copy_from_slice(&(offset as u32).to_be_bytes());
}
return Ok(());
}
if let Some(off) = find_subbox_offset(trak, b"co64") {
let h = super::parse_box_header(trak, off)?;
let cs = off + h.header_len as usize;
let count = super::read_u32(trak, cs + 4)? as usize;
if count != new_chunk_offsets.len() {
return Err(Mp4Error::InvalidBox(format!(
"co64 entry count mismatch in audio trak: {} vs {}",
count,
new_chunk_offsets.len()
)));
}
for (i, &offset) in new_chunk_offsets.iter().enumerate() {
let pos = cs + 8 + i * 8;
trak[pos..pos + 8].copy_from_slice(&offset.to_be_bytes());
}
return Ok(());
}
Err(Mp4Error::InvalidBox("audio trak missing stco/co64".into()))
}
fn patch_trak_track_id(trak: &mut [u8], new_track_id: u32) -> Result<(), Mp4Error> {
let off = find_subbox_offset(trak, b"tkhd")
.ok_or_else(|| Mp4Error::InvalidBox("audio trak missing tkhd".into()))?;
let h = super::parse_box_header(trak, off)?;
let cs = off + h.header_len as usize;
let version = trak[cs];
let track_id_pos = if version == 1 {
cs + 4 + 16 } else {
cs + 4 + 8 };
trak[track_id_pos..track_id_pos + 4].copy_from_slice(&new_track_id.to_be_bytes());
Ok(())
}
fn read_audio_mdhd_timescale(trak: &[u8]) -> Option<u32> {
let off = find_subbox_offset(trak, b"mdhd")?;
let h = super::parse_box_header(trak, off).ok()?;
let cs = off + h.header_len as usize;
let version = trak[cs];
let ts_pos = if version == 1 {
cs + 4 + 16 } else {
cs + 4 + 8 };
super::read_u32(trak, ts_pos).ok()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::codec::mp4::{is_mp4, parse_box_header};
fn build_minimal_annexb() -> Vec<u8> {
let sps_payload = vec![
0x67, 0x64, 0x00, 0x1E, 0xAC, 0xD9, 0x40, 0x40, 0x3C, 0x80,
];
let pps_payload = vec![0x68, 0xEB, 0xE3, 0xCB, 0x22, 0xC0];
let aud_payload = vec![0x09, 0x10]; let idr_payload = vec![
0x65, 0x88, 0x84, 0x00, 0x33, 0xFF, 0xFE, ];
let mut bytes = Vec::new();
bytes.extend_from_slice(&[0, 0, 0, 1]);
bytes.extend(&sps_payload);
bytes.extend_from_slice(&[0, 0, 0, 1]);
bytes.extend(&pps_payload);
bytes.extend_from_slice(&[0, 0, 0, 1]);
bytes.extend(&aud_payload);
bytes.extend_from_slice(&[0, 0, 0, 1]);
bytes.extend(&idr_payload);
bytes
}
#[test]
fn ftyp_handbrake_is_byte_exact() {
let ftyp = build_ftyp_handbrake();
assert_eq!(ftyp.len(), 8 + 8 + 16);
assert_eq!(&ftyp[0..4], &(ftyp.len() as u32).to_be_bytes());
assert_eq!(&ftyp[4..8], b"ftyp");
assert_eq!(&ftyp[8..12], b"isom");
assert_eq!(&ftyp[12..16], &[0x00, 0x00, 0x02, 0x00]);
assert_eq!(&ftyp[16..20], b"isom");
assert_eq!(&ftyp[20..24], b"iso2");
assert_eq!(&ftyp[24..28], b"avc1");
assert_eq!(&ftyp[28..32], b"mp41");
}
#[test]
fn build_mp4_round_trips_minimal_annexb() {
let annex_b = build_minimal_annexb();
let mp4 = build_mp4(
MuxerProfile::HandbrakeX264,
&annex_b,
1920,
1080,
FrameTiming::FPS_30,
)
.expect("build_mp4 succeeds");
assert!(is_mp4(&mp4), "output is recognized as MP4");
let h0 = parse_box_header(&mp4, 0).unwrap();
assert_eq!(h0.box_type, *b"ftyp");
let h1 = parse_box_header(&mp4, h0.size as usize).unwrap();
assert_eq!(h1.box_type, *b"mdat");
let moov_off = h0.size as usize + h1.size as usize;
let h2 = parse_box_header(&mp4, moov_off).unwrap();
assert_eq!(h2.box_type, *b"moov");
assert_eq!(moov_off + h2.size as usize, mp4.len());
}
#[test]
fn build_mp4_demuxes_back_to_one_video_track() {
use crate::codec::mp4::demux::demux;
let annex_b = build_minimal_annexb();
let mp4 = build_mp4(
MuxerProfile::HandbrakeX264,
&annex_b,
1920,
1080,
FrameTiming::FPS_30,
)
.unwrap();
let parsed = demux(&mp4).expect("demux must succeed");
assert_eq!(parsed.tracks.len(), 1);
let video_idx = parsed.video_track_idx.expect("video track");
let track = &parsed.tracks[video_idx];
assert!(track.is_h264(), "track is recognised as H.264");
assert_eq!(track.width, 1920);
assert_eq!(track.height, 1080);
assert_eq!(track.samples.len(), 1);
assert!(track.samples[0].is_sync, "IDR sample is sync");
assert_eq!(track.timescale, 15360);
}
#[test]
fn handbrake_mdhd_timescale_30fps() {
assert_eq!(handbrake::mdhd_timescale(30, 1), 15360);
}
#[test]
fn stbl_child_order_matches_handbrake() {
let annex_b = build_minimal_annexb();
let mp4 = build_mp4(
MuxerProfile::HandbrakeX264,
&annex_b,
1920,
1080,
FrameTiming::FPS_30,
)
.unwrap();
let order = collect_stbl_child_order(&mp4);
let expected: &[&[u8; 4]] = &[b"stsd", b"stts", b"stss", b"stsc", b"stsz", b"stco"];
assert_eq!(
order.iter().map(|s| *s).collect::<Vec<_>>(),
expected.iter().map(|s| **s).collect::<Vec<_>>(),
);
}
#[test]
fn stbl_child_order_with_ctts_inserts_after_stts() {
use crate::codec::h264::stego::gop_pattern::GopPattern;
let mut bytes = Vec::new();
bytes.extend_from_slice(&[0, 0, 0, 1, 0x09, 0x10]);
bytes.extend_from_slice(&[0, 0, 0, 1, 0x67, 0x64, 0x00, 0x1E, 0xAC, 0xD9, 0x40, 0x40, 0x3C, 0x80]);
bytes.extend_from_slice(&[0, 0, 0, 1, 0x68, 0xEB, 0xE3, 0xCB]);
bytes.extend_from_slice(&[0, 0, 0, 1, 0x65, 0x88, 0x84, 0x00]);
for _ in 0..4 {
bytes.extend_from_slice(&[0, 0, 0, 1, 0x09, 0x30]);
bytes.extend_from_slice(&[0, 0, 0, 1, 0x41, 0x9A, 0x00]);
}
let mp4 = build_mp4_with_pattern(
MuxerProfile::HandbrakeX264,
&bytes,
1920,
1080,
FrameTiming::FPS_30,
GopPattern::Ibpbp { gop: 5, b_count: 1 },
5,
)
.unwrap();
let order = collect_stbl_child_order(&mp4);
let expected: &[&[u8; 4]] =
&[b"stsd", b"stts", b"ctts", b"stss", b"stsc", b"stsz", b"stco"];
assert_eq!(
order.iter().map(|s| *s).collect::<Vec<_>>(),
expected.iter().map(|s| **s).collect::<Vec<_>>(),
);
}
fn collect_stbl_child_order(mp4: &[u8]) -> Vec<[u8; 4]> {
let mut order = Vec::new();
let _ = crate::codec::mp4::iterate_boxes(mp4, 0, mp4.len(), |h, content_start, _| {
if h.box_type == *b"moov" {
let _ = crate::codec::mp4::iterate_boxes(
mp4,
content_start,
content_start + h.size as usize - h.header_len as usize,
|h2, cs2, _| {
if h2.box_type == *b"trak" {
walk_trak_to_stbl(mp4, cs2, h2.size as usize, &mut order);
}
Ok(())
},
);
}
Ok(())
});
order
}
fn walk_trak_to_stbl(mp4: &[u8], content_start: usize, size: usize, out: &mut Vec<[u8; 4]>) {
let end = content_start + size - 8;
let _ = crate::codec::mp4::iterate_boxes(mp4, content_start, end, |h, cs, _| {
if h.box_type == *b"mdia" {
let _ = crate::codec::mp4::iterate_boxes(
mp4,
cs,
cs + h.size as usize - h.header_len as usize,
|h2, cs2, _| {
if h2.box_type == *b"minf" {
let _ = crate::codec::mp4::iterate_boxes(
mp4,
cs2,
cs2 + h2.size as usize - h2.header_len as usize,
|h3, cs3, _| {
if h3.box_type == *b"stbl" {
let _ = crate::codec::mp4::iterate_boxes(
mp4,
cs3,
cs3 + h3.size as usize - h3.header_len as usize,
|h4, _, _| {
out.push(h4.box_type);
Ok(())
},
);
}
Ok(())
},
);
}
Ok(())
},
);
}
Ok(())
});
}
#[test]
fn udta_too_length_field_is_string_length_only() {
let udta = build_udta_handbrake();
let s = handbrake::UDTA_TOO.as_bytes();
let too_content_len = 2 + 2 + s.len();
assert_eq!(udta.len(), 8 + 8 + too_content_len);
let len_field = u16::from_be_bytes([udta[16], udta[17]]);
assert_eq!(len_field as usize, s.len(), "length field = string length");
}
#[test]
fn handbrake_mdhd_timescale_25fps() {
assert_eq!(handbrake::mdhd_timescale(25, 1), 12800);
}
#[test]
fn handbrake_mdhd_timescale_29_97fps() {
let t = handbrake::mdhd_timescale(30000, 1001);
assert!((t as i64 - 15345).abs() <= 1, "got {t}");
}
#[test]
fn group_access_units_splits_on_aud() {
let mut bytes = Vec::new();
bytes.extend_from_slice(&[0, 0, 0, 1, 0x09, 0x10]);
bytes.extend_from_slice(&[0, 0, 0, 1, 0x67, 0x64, 0x00, 0x1E, 0xAC, 0xD9, 0x40, 0x40, 0x3C, 0x80]);
bytes.extend_from_slice(&[0, 0, 0, 1, 0x68, 0xEB, 0xE3, 0xCB]);
bytes.extend_from_slice(&[0, 0, 0, 1, 0x65, 0x88, 0x84, 0x00]);
bytes.extend_from_slice(&[0, 0, 0, 1, 0x09, 0x30]);
bytes.extend_from_slice(&[0, 0, 0, 1, 0x41, 0x9A, 0x00]);
bytes.extend_from_slice(&[0, 0, 0, 1, 0x09, 0x30]);
bytes.extend_from_slice(&[0, 0, 0, 1, 0x41, 0x9A, 0x00]);
let nals = parse_annexb_nals(&bytes);
let aus = group_access_units(&nals);
assert_eq!(aus.len(), 3);
assert!(aus[0].is_sync);
assert!(!aus[1].is_sync);
assert!(!aus[2].is_sync);
}
#[test]
fn build_mp4_strips_aud_and_parameter_sets_from_mdat() {
let annex_b = build_minimal_annexb();
let mp4 = build_mp4(
MuxerProfile::HandbrakeX264,
&annex_b,
1920,
1080,
FrameTiming::FPS_30,
)
.unwrap();
let h0 = parse_box_header(&mp4, 0).unwrap();
let h1 = parse_box_header(&mp4, h0.size as usize).unwrap();
assert_eq!(h1.box_type, *b"mdat");
let mdat_payload =
&mp4[h0.size as usize + 8..h0.size as usize + h1.size as usize];
let mut pos = 0;
let mut saw_sei = false;
let mut saw_idr = false;
while pos + 4 <= mdat_payload.len() {
let len = u32::from_be_bytes([
mdat_payload[pos],
mdat_payload[pos + 1],
mdat_payload[pos + 2],
mdat_payload[pos + 3],
]) as usize;
pos += 4;
assert!(pos + len <= mdat_payload.len());
let nal_type = mdat_payload[pos] & 0x1F;
assert_ne!(nal_type, 7, "SPS must NOT appear in mdat");
assert_ne!(nal_type, 8, "PPS must NOT appear in mdat");
assert_ne!(nal_type, 9, "AUD must NOT appear in mdat");
if nal_type == 6 {
saw_sei = true;
}
if nal_type == 5 {
saw_idr = true;
}
pos += len;
}
assert_eq!(pos, mdat_payload.len(), "mdat parses exactly to end");
assert!(saw_sei, "x264 SEI NAL is present in mdat");
assert!(saw_idr, "IDR slice NAL is present in mdat");
}
#[test]
fn build_mp4_rejects_missing_parameter_sets() {
let bytes = vec![0, 0, 0, 1, 0x65, 0x88, 0x84, 0x00];
let r = build_mp4(
MuxerProfile::HandbrakeX264,
&bytes,
1920,
1080,
FrameTiming::FPS_30,
);
assert!(r.is_err());
}
#[test]
fn x264_sei_nal_starts_with_sei_header_and_carries_uuid() {
let nal = build_x264_sei_user_data_unregistered();
assert_eq!(nal[0], 0x06, "SEI NAL header byte");
assert_eq!(nal[1], 5, "payload_type = user_data_unregistered (5)");
let mut pos = 2;
while nal[pos] == 0xFF {
pos += 1;
}
pos += 1;
assert_eq!(
&nal[pos..pos + 16],
&handbrake::X264_SEI_UUID,
"x264 SEI UUID present at expected offset"
);
}
#[test]
fn x264_sei_plaintext_present_in_nal() {
let nal = build_x264_sei_user_data_unregistered();
let banner = b"x264 - core";
let found = nal.windows(banner.len()).any(|w| w == banner);
assert!(found, "x264 banner is plaintext-readable inside SEI NAL");
}
#[test]
fn build_mp4_injects_exactly_one_sei_at_first_idr() {
let mut bytes = Vec::new();
bytes.extend_from_slice(&[0, 0, 0, 1, 0x09, 0x10]);
bytes.extend_from_slice(&[0, 0, 0, 1, 0x67, 0x64, 0x00, 0x1E, 0xAC, 0xD9, 0x40, 0x40, 0x3C, 0x80]);
bytes.extend_from_slice(&[0, 0, 0, 1, 0x68, 0xEB, 0xE3, 0xCB]);
bytes.extend_from_slice(&[0, 0, 0, 1, 0x65, 0x88, 0x84, 0x00]);
bytes.extend_from_slice(&[0, 0, 0, 1, 0x09, 0x10]);
bytes.extend_from_slice(&[0, 0, 0, 1, 0x65, 0x88, 0x84, 0x01]);
let mp4 = build_mp4(
MuxerProfile::HandbrakeX264,
&bytes,
1920,
1080,
FrameTiming::FPS_30,
)
.unwrap();
let mut count = 0;
for w in mp4.windows(handbrake::X264_SEI_UUID.len()) {
if w == handbrake::X264_SEI_UUID {
count += 1;
}
}
assert_eq!(count, 1, "x264 SEI UUID appears exactly once in mp4 file");
use crate::codec::mp4::demux::demux;
let parsed = demux(&mp4).unwrap();
let track = &parsed.tracks[parsed.video_track_idx.unwrap()];
let s0 = &track.samples[0].data;
let s1 = &track.samples[1].data;
let in_s0 = s0
.windows(handbrake::X264_SEI_UUID.len())
.any(|w| w == handbrake::X264_SEI_UUID);
let in_s1 = s1
.windows(handbrake::X264_SEI_UUID.len())
.any(|w| w == handbrake::X264_SEI_UUID);
assert!(in_s0, "SEI UUID is in sample 0");
assert!(!in_s1, "SEI UUID is NOT in sample 1");
}
#[test]
fn emulation_prevention_inserts_03_after_two_zeros() {
let rbsp = vec![0x42, 0x00, 0x00, 0x01, 0xAA];
let out = add_emulation_prevention(&rbsp);
assert_eq!(out, vec![0x42, 0x00, 0x00, 0x03, 0x01, 0xAA]);
}
#[test]
fn emulation_prevention_does_not_touch_header_byte() {
let rbsp = vec![0x06, 0x00, 0x00, 0x00, 0xFF];
let out = add_emulation_prevention(&rbsp);
assert_eq!(out, vec![0x06, 0x00, 0x00, 0x03, 0x00, 0xFF]);
}
#[test]
fn build_mp4_rejects_zero_dimensions() {
let annex_b = build_minimal_annexb();
let r = build_mp4(
MuxerProfile::HandbrakeX264,
&annex_b,
0,
1080,
FrameTiming::FPS_30,
);
assert!(r.is_err());
}
#[test]
fn chunk_layout_per_gop_uniform_size() {
let chunks = chunk_layout_per_gop(6, &[1, 4]);
assert_eq!(chunks, vec![3, 3]);
}
#[test]
fn chunk_layout_per_gop_mixed_size() {
let chunks = chunk_layout_per_gop(10, &[1, 4, 8]);
assert_eq!(chunks, vec![3, 4, 3]);
}
#[test]
fn chunk_layout_per_gop_no_sync_falls_back() {
let chunks = chunk_layout_per_gop(5, &[]);
assert_eq!(chunks, vec![5]);
}
#[test]
fn build_stsc_collapses_uniform_runs() {
let stsc = build_stsc(&[30, 30, 30]);
assert_eq!(stsc.len(), 8 + 4 + 4 + 12);
assert_eq!(&stsc[12..16], &1u32.to_be_bytes());
assert_eq!(&stsc[16..20], &1u32.to_be_bytes()); assert_eq!(&stsc[20..24], &30u32.to_be_bytes()); assert_eq!(&stsc[24..28], &1u32.to_be_bytes()); }
#[test]
fn build_stsc_splits_mixed_runs() {
let stsc = build_stsc(&[30, 30, 25, 30]);
assert_eq!(stsc.len(), 8 + 4 + 4 + 3 * 12);
assert_eq!(&stsc[12..16], &3u32.to_be_bytes());
assert_eq!(&stsc[16..20], &1u32.to_be_bytes()); assert_eq!(&stsc[20..24], &30u32.to_be_bytes()); assert_eq!(&stsc[28..32], &3u32.to_be_bytes()); assert_eq!(&stsc[32..36], &25u32.to_be_bytes()); assert_eq!(&stsc[40..44], &4u32.to_be_bytes()); assert_eq!(&stsc[44..48], &30u32.to_be_bytes()); }
#[test]
fn build_ctts_writes_per_sample_signed_offsets() {
let offsets: Vec<i32> = vec![0, 512, -512, 512, -512];
let ctts = build_ctts(&offsets);
assert_eq!(ctts.len(), 8 + 4 + 4 + 5 * 8);
assert_eq!(ctts[8], 1);
assert_eq!(&ctts[12..16], &5u32.to_be_bytes());
assert_eq!(&ctts[16..20], &1u32.to_be_bytes());
assert_eq!(&ctts[20..24], &0i32.to_be_bytes());
assert_eq!(&ctts[32..36], &1u32.to_be_bytes());
assert_eq!(&ctts[36..40], &(-512i32).to_be_bytes());
}
#[test]
fn build_mp4_with_ipppp_pattern_emits_no_ctts_no_edts() {
use crate::codec::h264::stego::gop_pattern::GopPattern;
let annex_b = build_minimal_annexb();
let mp4 = build_mp4_with_pattern(
MuxerProfile::HandbrakeX264,
&annex_b,
1920,
1080,
FrameTiming::FPS_30,
GopPattern::Ipppp { gop: 1 },
1,
)
.unwrap();
assert!(!mp4.windows(4).any(|w| w == b"ctts"), "no ctts box");
assert!(!mp4.windows(4).any(|w| w == b"edts"), "no edts box");
}
#[test]
fn build_mp4_with_pattern_rejects_au_count_mismatch() {
use crate::codec::h264::stego::gop_pattern::GopPattern;
let annex_b = build_minimal_annexb();
let r = build_mp4_with_pattern(
MuxerProfile::HandbrakeX264,
&annex_b,
1920,
1080,
FrameTiming::FPS_30,
GopPattern::Ibpbp { gop: 5, b_count: 1 },
5,
);
assert!(r.is_err());
}
#[test]
fn build_mp4_rejects_zero_fps() {
let annex_b = build_minimal_annexb();
let r = build_mp4(
MuxerProfile::HandbrakeX264,
&annex_b,
1920,
1080,
FrameTiming { fps_num: 0, fps_den: 1 },
);
assert!(r.is_err());
}
}