use oxideav_core::{
CodecId, CodecParameters, CodecRegistry, CodecTag, Demuxer, MediaType, Muxer, Packet, Rational,
ReadSeek, SampleFormat, StreamInfo, TimeBase, WriteSeek,
};
use oxideav_avi::demuxer::open_avi as demuxer_open_avi;
use oxideav_avi::muxer::{open_avi, AviKind, AviMuxOptions};
fn video_stream(index: u32) -> StreamInfo {
let mut params =
CodecParameters::video(CodecId::new("mjpeg")).with_tag(CodecTag::fourcc(b"MJPG"));
params.media_type = MediaType::Video;
params.width = Some(64);
params.height = Some(48);
params.frame_rate = Some(Rational::new(25, 1));
StreamInfo {
index,
time_base: TimeBase::new(1, 25),
duration: None,
start_time: Some(0),
params,
}
}
fn audio_stream(index: u32) -> StreamInfo {
let mut params = CodecParameters::audio(CodecId::new("pcm_s16le"));
params.media_type = MediaType::Audio;
params.channels = Some(2);
params.sample_rate = Some(48_000);
params.sample_format = Some(SampleFormat::S16);
StreamInfo {
index,
time_base: TimeBase::new(1, 48_000),
duration: None,
start_time: Some(0),
params,
}
}
fn write_minimal(path: &std::path::Path, options: AviMuxOptions) {
let streams = [video_stream(0), audio_stream(1)];
let f = std::fs::File::create(path).unwrap();
let ws: Box<dyn WriteSeek> = Box::new(f);
let mut mux = open_avi(ws, &streams, AviKind::Avi10, options).unwrap();
mux.write_header().unwrap();
let mut v = Packet::new(0, streams[0].time_base, vec![0x55u8; 64]);
v.pts = Some(0);
v.flags.keyframe = true;
mux.write_packet(&v).unwrap();
let mut a = Packet::new(1, streams[1].time_base, vec![0u8; 8]);
a.pts = Some(0);
a.flags.keyframe = true;
mux.write_packet(&a).unwrap();
mux.write_trailer().unwrap();
}
#[test]
fn initial_frames_audio_override_roundtrip_accessor_and_metadata() {
let tmp = std::env::temp_dir().join("oxideav-avi-r153-init-aud.avi");
let opts = AviMuxOptions::new().with_stream_initial_frames(1, 18);
write_minimal(&tmp, opts);
let reg = CodecRegistry::new();
let rs: Box<dyn ReadSeek> = Box::new(std::fs::File::open(&tmp).unwrap());
let dmx = demuxer_open_avi(rs, ®).unwrap();
assert_eq!(dmx.stream_initial_frames(1), Some(18));
assert_eq!(dmx.stream_initial_frames(0), None);
let md = dmx.metadata();
assert!(
md.iter()
.any(|(k, v)| k == "avi:strh.1.initial_frames" && v == "18"),
"missing avi:strh.1.initial_frames metadata key; got {:?}",
md.iter()
.filter(|(k, _)| k.contains("initial_frames"))
.collect::<Vec<_>>()
);
assert!(!md.iter().any(|(k, _)| k == "avi:strh.0.initial_frames"));
}
#[test]
fn default_no_override_reads_as_none() {
let tmp = std::env::temp_dir().join("oxideav-avi-r153-default-init.avi");
write_minimal(&tmp, AviMuxOptions::new());
let reg = CodecRegistry::new();
let rs: Box<dyn ReadSeek> = Box::new(std::fs::File::open(&tmp).unwrap());
let dmx = demuxer_open_avi(rs, ®).unwrap();
assert_eq!(dmx.stream_initial_frames(0), None);
assert_eq!(dmx.stream_initial_frames(1), None);
assert!(!dmx
.metadata()
.iter()
.any(|(k, _)| k.starts_with("avi:strh.") && k.ends_with(".initial_frames")));
}
#[test]
fn initial_frames_video_override_roundtrip() {
let tmp = std::env::temp_dir().join("oxideav-avi-r153-init-vid.avi");
let opts = AviMuxOptions::new().with_stream_initial_frames(0, 5);
write_minimal(&tmp, opts);
let reg = CodecRegistry::new();
let rs: Box<dyn ReadSeek> = Box::new(std::fs::File::open(&tmp).unwrap());
let dmx = demuxer_open_avi(rs, ®).unwrap();
assert_eq!(dmx.stream_initial_frames(0), Some(5));
assert_eq!(dmx.stream_initial_frames(1), None);
assert!(dmx
.metadata()
.iter()
.any(|(k, v)| k == "avi:strh.0.initial_frames" && v == "5"));
assert!(!dmx
.metadata()
.iter()
.any(|(k, _)| k == "avi:strh.1.initial_frames"));
}
#[test]
fn with_stream_initial_frames_dedups() {
let opts = AviMuxOptions::new()
.with_stream_initial_frames(0, 5)
.with_stream_initial_frames(0, 18);
let entries: Vec<_> = opts
.stream_initial_frames
.iter()
.filter(|(idx, _)| *idx == 0)
.collect();
assert_eq!(
entries.len(),
1,
"duplicate index must collapse to one entry"
);
assert_eq!(entries[0].1, 18);
}
#[test]
fn explicit_zero_override_reads_as_none() {
let tmp = std::env::temp_dir().join("oxideav-avi-r153-zero-override.avi");
let opts = AviMuxOptions::new().with_stream_initial_frames(0, 0);
write_minimal(&tmp, opts);
let reg = CodecRegistry::new();
let rs: Box<dyn ReadSeek> = Box::new(std::fs::File::open(&tmp).unwrap());
let dmx = demuxer_open_avi(rs, ®).unwrap();
assert_eq!(
dmx.stream_initial_frames(0),
None,
"an all-zero dwInitialFrames must read back as None"
);
assert!(!dmx
.metadata()
.iter()
.any(|(k, _)| k == "avi:strh.0.initial_frames"));
}
#[test]
fn distinct_per_stream_skews_roundtrip_independently() {
let tmp = std::env::temp_dir().join("oxideav-avi-r153-distinct-init.avi");
let opts = AviMuxOptions::new()
.with_stream_initial_frames(0, 3) .with_stream_initial_frames(1, 21); write_minimal(&tmp, opts);
let reg = CodecRegistry::new();
let rs: Box<dyn ReadSeek> = Box::new(std::fs::File::open(&tmp).unwrap());
let dmx = demuxer_open_avi(rs, ®).unwrap();
assert_eq!(dmx.stream_initial_frames(0), Some(3));
assert_eq!(dmx.stream_initial_frames(1), Some(21));
let md = dmx.metadata();
assert!(md
.iter()
.any(|(k, v)| k == "avi:strh.0.initial_frames" && v == "3"));
assert!(md
.iter()
.any(|(k, v)| k == "avi:strh.1.initial_frames" && v == "21"));
}
#[test]
fn out_of_range_index_is_none() {
let tmp = std::env::temp_dir().join("oxideav-avi-r153-oor.avi");
write_minimal(&tmp, AviMuxOptions::new());
let reg = CodecRegistry::new();
let rs: Box<dyn ReadSeek> = Box::new(std::fs::File::open(&tmp).unwrap());
let dmx = demuxer_open_avi(rs, ®).unwrap();
assert_eq!(dmx.stream_initial_frames(99), None);
}
#[test]
fn all_bits_set_initial_frames_roundtrip() {
let tmp = std::env::temp_dir().join("oxideav-avi-r153-all-bits.avi");
let opts = AviMuxOptions::new().with_stream_initial_frames(0, 0xFFFF_FFFF);
write_minimal(&tmp, opts);
let reg = CodecRegistry::new();
let rs: Box<dyn ReadSeek> = Box::new(std::fs::File::open(&tmp).unwrap());
let dmx = demuxer_open_avi(rs, ®).unwrap();
assert_eq!(dmx.stream_initial_frames(0), Some(0xFFFF_FFFF));
assert!(dmx
.metadata()
.iter()
.any(|(k, v)| k == "avi:strh.0.initial_frames" && v == "4294967295"));
}
fn push_chunk(out: &mut Vec<u8>, id: &[u8; 4], body: &[u8]) {
out.extend_from_slice(id);
out.extend_from_slice(&(body.len() as u32).to_le_bytes());
out.extend_from_slice(body);
if body.len() & 1 == 1 {
out.push(0);
}
}
fn list(form: &[u8; 4], body: &[u8]) -> Vec<u8> {
let mut v = Vec::new();
v.extend_from_slice(b"LIST");
v.extend_from_slice(&((4 + body.len()) as u32).to_le_bytes());
v.extend_from_slice(form);
v.extend_from_slice(body);
if (4 + body.len()) & 1 == 1 {
v.push(0);
}
v
}
fn strh_video_with_initial_frames(initial_frames: u32) -> Vec<u8> {
let mut strh = Vec::with_capacity(56);
strh.extend_from_slice(b"vids"); strh.extend_from_slice(b"MJPG"); strh.extend_from_slice(&0u32.to_le_bytes()); strh.extend_from_slice(&0u16.to_le_bytes()); strh.extend_from_slice(&0u16.to_le_bytes()); strh.extend_from_slice(&initial_frames.to_le_bytes()); strh.extend_from_slice(&1u32.to_le_bytes()); strh.extend_from_slice(&25u32.to_le_bytes()); strh.extend_from_slice(&0u32.to_le_bytes()); strh.extend_from_slice(&1u32.to_le_bytes()); strh.extend_from_slice(&0u32.to_le_bytes()); strh.extend_from_slice(&0xFFFF_FFFFu32.to_le_bytes()); strh.extend_from_slice(&0u32.to_le_bytes()); strh.extend_from_slice(&0i16.to_le_bytes()); strh.extend_from_slice(&0i16.to_le_bytes()); strh.extend_from_slice(&64i16.to_le_bytes()); strh.extend_from_slice(&48i16.to_le_bytes()); assert_eq!(strh.len(), 56);
strh
}
fn strf_video_mjpg() -> Vec<u8> {
let mut strf = Vec::with_capacity(40);
strf.extend_from_slice(&40u32.to_le_bytes()); strf.extend_from_slice(&64u32.to_le_bytes()); strf.extend_from_slice(&48u32.to_le_bytes()); strf.extend_from_slice(&1u16.to_le_bytes()); strf.extend_from_slice(&24u16.to_le_bytes()); strf.extend_from_slice(b"MJPG"); strf.extend_from_slice(&(64u32 * 48 * 3).to_le_bytes()); strf.extend_from_slice(&0u32.to_le_bytes()); strf.extend_from_slice(&0u32.to_le_bytes()); strf.extend_from_slice(&0u32.to_le_bytes()); strf.extend_from_slice(&0u32.to_le_bytes()); strf
}
fn build_avi_with_initial_frames(initial_frames: u32) -> Vec<u8> {
let mut avih = Vec::with_capacity(56);
avih.extend_from_slice(&40000u32.to_le_bytes()); avih.extend_from_slice(&0u32.to_le_bytes()); avih.extend_from_slice(&0u32.to_le_bytes()); avih.extend_from_slice(&0x0010u32.to_le_bytes()); avih.extend_from_slice(&1u32.to_le_bytes()); avih.extend_from_slice(&0u32.to_le_bytes()); avih.extend_from_slice(&1u32.to_le_bytes()); avih.extend_from_slice(&0u32.to_le_bytes()); avih.extend_from_slice(&64u32.to_le_bytes()); avih.extend_from_slice(&48u32.to_le_bytes()); avih.extend_from_slice(&[0u8; 16]); assert_eq!(avih.len(), 56);
let strh_body = strh_video_with_initial_frames(initial_frames);
let strf_body = strf_video_mjpg();
let mut strl_body = Vec::new();
push_chunk(&mut strl_body, b"strh", &strh_body);
push_chunk(&mut strl_body, b"strf", &strf_body);
let strl = list(b"strl", &strl_body);
let mut hdrl_body = Vec::new();
push_chunk(&mut hdrl_body, b"avih", &avih);
hdrl_body.extend_from_slice(&strl);
let hdrl = list(b"hdrl", &hdrl_body);
let mut movi_body = Vec::new();
push_chunk(&mut movi_body, b"00dc", &[0x55u8; 64]);
let movi = list(b"movi", &movi_body);
let mut riff_body = Vec::new();
riff_body.extend_from_slice(b"AVI ");
riff_body.extend_from_slice(&hdrl);
riff_body.extend_from_slice(&movi);
let mut out = Vec::new();
out.extend_from_slice(b"RIFF");
out.extend_from_slice(&(riff_body.len() as u32).to_le_bytes());
out.extend_from_slice(&riff_body);
out
}
#[test]
fn handrolled_explicit_nonzero_initial_frames_decodes() {
let buf = build_avi_with_initial_frames(0xCAFE_BABE);
let reg = CodecRegistry::new();
let rs: Box<dyn ReadSeek> = Box::new(std::io::Cursor::new(buf));
let dmx = demuxer_open_avi(rs, ®).unwrap();
assert_eq!(dmx.stream_initial_frames(0), Some(0xCAFE_BABE));
assert!(dmx
.metadata()
.iter()
.any(|(k, v)| k == "avi:strh.0.initial_frames" && v == "3405691582"));
}
#[test]
fn handrolled_zero_initial_frames_parses_as_none() {
let buf = build_avi_with_initial_frames(0);
let reg = CodecRegistry::new();
let rs: Box<dyn ReadSeek> = Box::new(std::io::Cursor::new(buf));
let dmx = demuxer_open_avi(rs, ®).unwrap();
assert_eq!(dmx.stream_initial_frames(0), None);
assert!(!dmx
.metadata()
.iter()
.any(|(k, _)| k == "avi:strh.0.initial_frames"));
}