use super::*;
use super::opendml::{parse_indx_body, read_dmlh_total_frames};
use super::streaming::Backend;
use crate::streaming::StreamingDemuxer;
fn chunk(fourcc: &[u8; 4], payload: &[u8]) -> Vec<u8> {
let mut out = Vec::with_capacity(8 + payload.len());
out.extend_from_slice(fourcc);
out.extend_from_slice(&(payload.len() as u32).to_le_bytes());
out.extend_from_slice(payload);
if out.len() & 1 == 1 {
out.push(0);
} out
}
fn list(list_type: &[u8; 4], payload: &[u8]) -> Vec<u8> {
let mut body = Vec::with_capacity(4 + payload.len());
body.extend_from_slice(list_type);
body.extend_from_slice(payload);
chunk(b"LIST", &body)
}
fn video_strl(
handler: &[u8; 4],
compression: &[u8; 4],
w: u32,
h: u32,
rate: u32,
scale: u32,
) -> Vec<u8> {
let mut strh = Vec::with_capacity(56);
strh.extend_from_slice(b"vids");
strh.extend_from_slice(handler);
strh.extend_from_slice(&[0u8; 12]); strh.extend_from_slice(&scale.to_le_bytes());
strh.extend_from_slice(&rate.to_le_bytes());
strh.extend_from_slice(&[0u8; 24]); let strh_chunk = chunk(b"strh", &strh);
let mut strf = Vec::with_capacity(40);
strf.extend_from_slice(&40u32.to_le_bytes()); strf.extend_from_slice(&(w as i32).to_le_bytes()); strf.extend_from_slice(&(h as i32).to_le_bytes()); strf.extend_from_slice(&1u16.to_le_bytes()); strf.extend_from_slice(&24u16.to_le_bytes()); strf.extend_from_slice(compression); strf.extend_from_slice(&[0u8; 20]); let strf_chunk = chunk(b"strf", &strf);
let mut strl_body = Vec::new();
strl_body.extend_from_slice(&strh_chunk);
strl_body.extend_from_slice(&strf_chunk);
list(b"strl", &strl_body)
}
#[test]
fn demux_minimal_xvid_avi_emits_samples() {
let mut hdrl_body = Vec::new();
hdrl_body.extend_from_slice(&chunk(b"avih", &[0u8; 56])); hdrl_body.extend_from_slice(&video_strl(b"XVID", b"XVID", 320, 240, 30, 1));
let hdrl = list(b"hdrl", &hdrl_body);
let mut movi_body = Vec::new();
movi_body.extend_from_slice(&chunk(b"00dc", b"frame-1-bytes"));
movi_body.extend_from_slice(&chunk(b"01wb", b"audio-ignored"));
movi_body.extend_from_slice(&chunk(b"00dc", b"frame-2"));
movi_body.extend_from_slice(&chunk(b"00dc", b"frame-3-payload"));
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 file = Vec::with_capacity(8 + riff_body.len());
file.extend_from_slice(b"RIFF");
file.extend_from_slice(&(riff_body.len() as u32).to_le_bytes());
file.extend_from_slice(&riff_body);
let d = demux_avi(&file).expect("demux");
assert_eq!(d.codec, "mpeg4");
assert_eq!(d.info.width, 320);
assert_eq!(d.info.height, 240);
assert_eq!(d.samples.len(), 3);
assert_eq!(d.samples[0], b"frame-1-bytes");
assert_eq!(d.samples[1], b"frame-2");
assert_eq!(d.samples[2], b"frame-3-payload");
}
#[test]
fn demux_rejects_unknown_fourcc() {
let mut hdrl_body = Vec::new();
hdrl_body.extend_from_slice(&chunk(b"avih", &[0u8; 56]));
hdrl_body.extend_from_slice(&video_strl(b"ZZZZ", b"ZZZZ", 100, 100, 30, 1));
let hdrl = list(b"hdrl", &hdrl_body);
let movi = list(b"movi", &chunk(b"00dc", b"x"));
let mut body = Vec::new();
body.extend_from_slice(b"AVI ");
body.extend_from_slice(&hdrl);
body.extend_from_slice(&movi);
let mut file = Vec::new();
file.extend_from_slice(b"RIFF");
file.extend_from_slice(&(body.len() as u32).to_le_bytes());
file.extend_from_slice(&body);
assert!(demux_avi(&file).is_err());
}
#[test]
fn demux_handles_divx_variants() {
for fcc in [b"DIVX", b"DX50", b"DIV3", b"XviD"] {
let mut hdrl_body = Vec::new();
hdrl_body.extend_from_slice(&chunk(b"avih", &[0u8; 56]));
hdrl_body.extend_from_slice(&video_strl(fcc, fcc, 640, 480, 25, 1));
let hdrl = list(b"hdrl", &hdrl_body);
let movi = list(b"movi", &chunk(b"00dc", b"sample"));
let mut body = Vec::new();
body.extend_from_slice(b"AVI ");
body.extend_from_slice(&hdrl);
body.extend_from_slice(&movi);
let mut file = Vec::new();
file.extend_from_slice(b"RIFF");
file.extend_from_slice(&(body.len() as u32).to_le_bytes());
file.extend_from_slice(&body);
let d = demux_avi(&file).expect("should demux");
assert_eq!(d.codec, "mpeg4", "fourcc {:?} did not map to mpeg4", fcc);
}
}
fn build_opendml_two_movi_six_samples() -> (Vec<u8>, Vec<Vec<u8>>) {
let payloads: Vec<Vec<u8>> = (0..6)
.map(|i| format!("opendml-frame-{i}").into_bytes())
.collect();
let mut movi1_body = Vec::new();
let mut chunk_data_offsets_in_movi1 = Vec::new();
for i in 0..3 {
let cur_off = movi1_body.len();
let c = chunk(b"00dc", &payloads[i]);
movi1_body.extend_from_slice(&c);
chunk_data_offsets_in_movi1.push((cur_off + 8, payloads[i].len()));
}
let mut movi2_body = Vec::new();
let mut chunk_data_offsets_in_movi2 = Vec::new();
for i in 3..6 {
let cur_off = movi2_body.len();
let c = chunk(b"00dc", &payloads[i]);
movi2_body.extend_from_slice(&c);
chunk_data_offsets_in_movi2.push((cur_off + 8, payloads[i].len()));
}
let movi1_chunk = list(b"movi", &movi1_body);
let movi2_chunk = list(b"movi", &movi2_body);
let build_ix00 = |entries: &[(usize, usize)], qw_base_offset: u64| -> Vec<u8> {
let mut body = Vec::new();
body.extend_from_slice(&2u16.to_le_bytes()); body.push(0); body.push(0x01); body.extend_from_slice(&(entries.len() as u32).to_le_bytes()); body.extend_from_slice(b"00dc"); body.extend_from_slice(&qw_base_offset.to_le_bytes()); body.extend_from_slice(&0u32.to_le_bytes()); for (data_off, data_size) in entries {
body.extend_from_slice(&(*data_off as u32).to_le_bytes()); body.extend_from_slice(&(*data_size as u32).to_le_bytes()); }
chunk(b"ix00", &body)
};
let placeholder_indx = build_indx_placeholder();
let hdrl_with_placeholder = build_hdrl(
&placeholder_indx,
6,
3,
);
let avi_body_start = 12usize;
let hdrl_offset = avi_body_start; let hdrl_end = hdrl_offset + hdrl_with_placeholder.len();
let movi1_offset = hdrl_end; let movi1_body_offset = movi1_offset + 12;
let movi1_end = movi1_offset + movi1_chunk.len();
let ix1_offset = movi1_end; let ix1_chunk_real = build_ix00(&chunk_data_offsets_in_movi1, movi1_body_offset as u64);
let ix1_end = ix1_offset + ix1_chunk_real.len();
let avix_outer_start = ix1_end;
let avix_body_start = avix_outer_start + 12;
let movi2_offset = avix_body_start;
let movi2_body_offset = movi2_offset + 12;
let movi2_end = movi2_offset + movi2_chunk.len();
let ix2_offset = movi2_end;
let ix2_chunk_real = build_ix00(&chunk_data_offsets_in_movi2, movi2_body_offset as u64);
let real_indx = build_indx_real(&[
(
ix1_offset as u64,
(ix1_chunk_real.len() - 8) as u32,
3,
),
(
ix2_offset as u64,
(ix2_chunk_real.len() - 8) as u32,
3,
),
]);
assert_eq!(
real_indx.len(),
placeholder_indx.len(),
"indx size sanity — placeholder and real must match for offsets to stay valid"
);
let hdrl_real = build_hdrl(&real_indx, 6, 3);
assert_eq!(
hdrl_real.len(),
hdrl_with_placeholder.len(),
"hdrl size sanity — must not depend on indx values, only sizes"
);
let mut avi_seg_body = Vec::new();
avi_seg_body.extend_from_slice(b"AVI ");
avi_seg_body.extend_from_slice(&hdrl_real);
avi_seg_body.extend_from_slice(&movi1_chunk);
avi_seg_body.extend_from_slice(&ix1_chunk_real);
let mut file = Vec::new();
file.extend_from_slice(b"RIFF");
file.extend_from_slice(&(avi_seg_body.len() as u32).to_le_bytes());
file.extend_from_slice(&avi_seg_body);
let mut avix_seg_body = Vec::new();
avix_seg_body.extend_from_slice(b"AVIX");
avix_seg_body.extend_from_slice(&movi2_chunk);
avix_seg_body.extend_from_slice(&ix2_chunk_real);
file.extend_from_slice(b"RIFF");
file.extend_from_slice(&(avix_seg_body.len() as u32).to_le_bytes());
file.extend_from_slice(&avix_seg_body);
assert_eq!(
&file[movi1_offset..movi1_offset + 4],
b"LIST",
"movi#1 should start with LIST at the planned offset"
);
assert_eq!(
&file[movi1_body_offset - 4..movi1_body_offset],
b"movi",
"movi#1 type fourcc should sit just before the body"
);
assert_eq!(&file[ix1_offset..ix1_offset + 4], b"ix00");
assert_eq!(&file[movi2_offset..movi2_offset + 4], b"LIST");
assert_eq!(&file[movi2_body_offset - 4..movi2_body_offset], b"movi");
assert_eq!(&file[ix2_offset..ix2_offset + 4], b"ix00");
(file, payloads)
}
fn build_indx_placeholder() -> Vec<u8> {
build_indx_real(&[(0, 0, 0), (0, 0, 0)])
}
fn build_indx_real(entries: &[(u64, u32, u32)]) -> Vec<u8> {
let mut body = Vec::new();
body.extend_from_slice(&4u16.to_le_bytes()); body.push(0); body.push(0x00); body.extend_from_slice(&(entries.len() as u32).to_le_bytes()); body.extend_from_slice(b"00dc"); body.extend_from_slice(&[0u8; 12]); for (qw_off, dw_size, dw_duration) in entries {
body.extend_from_slice(&qw_off.to_le_bytes());
body.extend_from_slice(&dw_size.to_le_bytes());
body.extend_from_slice(&dw_duration.to_le_bytes());
}
chunk(b"indx", &body)
}
fn build_hdrl(indx_chunk: &[u8], dmlh_total: u32, avih_total: u32) -> Vec<u8> {
let mut avih_body = Vec::with_capacity(56);
avih_body.extend_from_slice(&33333u32.to_le_bytes()); avih_body.extend_from_slice(&[0u8; 12]); avih_body.extend_from_slice(&avih_total.to_le_bytes());
avih_body.extend_from_slice(&[0u8; 32]); let avih_chunk = chunk(b"avih", &avih_body);
let strh_chunk = {
let mut strh = Vec::with_capacity(56);
strh.extend_from_slice(b"vids");
strh.extend_from_slice(b"XVID");
strh.extend_from_slice(&[0u8; 12]);
strh.extend_from_slice(&1u32.to_le_bytes()); strh.extend_from_slice(&30u32.to_le_bytes()); strh.extend_from_slice(&[0u8; 24]);
chunk(b"strh", &strh)
};
let strf_chunk = {
let mut strf = Vec::with_capacity(40);
strf.extend_from_slice(&40u32.to_le_bytes());
strf.extend_from_slice(&320i32.to_le_bytes());
strf.extend_from_slice(&240i32.to_le_bytes());
strf.extend_from_slice(&1u16.to_le_bytes());
strf.extend_from_slice(&24u16.to_le_bytes());
strf.extend_from_slice(b"XVID");
strf.extend_from_slice(&[0u8; 20]);
chunk(b"strf", &strf)
};
let mut strl_body = Vec::new();
strl_body.extend_from_slice(&strh_chunk);
strl_body.extend_from_slice(&strf_chunk);
strl_body.extend_from_slice(indx_chunk);
let strl_chunk = list(b"strl", &strl_body);
let dmlh_chunk = {
let mut body = Vec::new();
body.extend_from_slice(&dmlh_total.to_le_bytes());
chunk(b"dmlh", &body)
};
let odml_chunk = list(b"odml", &dmlh_chunk);
let mut hdrl_body = Vec::new();
hdrl_body.extend_from_slice(&avih_chunk);
hdrl_body.extend_from_slice(&strl_chunk);
hdrl_body.extend_from_slice(&odml_chunk);
list(b"hdrl", &hdrl_body)
}
#[test]
fn opendml_streaming_walks_both_movi_lists_in_order() {
let (file, expected) = build_opendml_two_movi_six_samples();
let mut d = demux_avi_streaming_init(&file).expect("OpenDML init");
assert_eq!(d.header.info.total_frames, 6);
let mut got = Vec::new();
while let Some(s) = d.next_video_sample().expect("next") {
got.push(s.data);
}
assert_eq!(
got.len(),
6,
"should pull all six samples across both movi LISTs"
);
for (i, (g, e)) in got.iter().zip(expected.iter()).enumerate() {
assert_eq!(
g, e,
"sample {i} mismatch — OpenDML walk lost ordering or content"
);
}
}
#[test]
fn opendml_legacy_demux_also_walks_both_movi_lists() {
let (file, expected) = build_opendml_two_movi_six_samples();
let d = demux_avi(&file).expect("legacy demux");
assert_eq!(d.samples.len(), 6);
for (i, (g, e)) in d.samples.iter().zip(expected.iter()).enumerate() {
assert_eq!(g, e, "legacy sample {i} mismatch");
}
assert_eq!(
d.info.total_frames, 6,
"legacy total_frames should honor dmlh"
);
}
#[test]
fn opendml_total_frames_prefers_dmlh_over_avih() {
let (file, _) = build_opendml_two_movi_six_samples();
let d = demux_avi_streaming_init(&file).expect("init");
assert_eq!(
d.header.info.total_frames, 6,
"dmlh.dwTotalFrames (6) must win over avih.dwTotalFrames (3)"
);
assert!(
(d.header.info.duration - 0.2).abs() < 1e-6,
"duration = total_frames / frame_rate, got {}",
d.header.info.duration
);
}
#[test]
fn opendml_picks_indx_path_not_cursor_walk() {
let (file, _) = build_opendml_two_movi_six_samples();
let d = demux_avi_streaming_init(&file).expect("init");
assert!(
matches!(d.backend, Backend::OpenDml { .. }),
"fixture has indx — backend must be OpenDml"
);
}
#[test]
fn legacy_single_movi_without_indx_uses_cursor_backend() {
let mut hdrl_body = Vec::new();
hdrl_body.extend_from_slice(&chunk(b"avih", &[0u8; 56]));
hdrl_body.extend_from_slice(&video_strl(b"XVID", b"XVID", 320, 240, 30, 1));
let hdrl = list(b"hdrl", &hdrl_body);
let mut movi_body = Vec::new();
movi_body.extend_from_slice(&chunk(b"00dc", b"f0"));
movi_body.extend_from_slice(&chunk(b"00dc", b"f1"));
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 file = Vec::new();
file.extend_from_slice(b"RIFF");
file.extend_from_slice(&(riff_body.len() as u32).to_le_bytes());
file.extend_from_slice(&riff_body);
let mut d = demux_avi_streaming_init(&file).expect("init");
assert!(
matches!(d.backend, Backend::Cursor(_)),
"no indx → must take cursor backend (legacy path)"
);
let s0 = d.next_video_sample().unwrap().unwrap();
let s1 = d.next_video_sample().unwrap().unwrap();
assert_eq!(s0.data, b"f0");
assert_eq!(s1.data, b"f1");
assert!(d.next_video_sample().unwrap().is_none());
}
#[test]
fn parse_indx_body_decodes_two_index_of_indexes_entries() {
let entries = [
(0xDEAD_BEEFu64, 0x1234u32, 100u32),
(0xCAFE_F00Du64, 0x5678u32, 200u32),
];
let chunk_bytes = build_indx_real(&entries);
let body = &chunk_bytes[8..8 + (chunk_bytes.len() - 8 - (chunk_bytes.len() & 1))];
let parsed = parse_indx_body(body).expect("parse");
assert_eq!(parsed.len(), 2);
assert_eq!(parsed[0], (0xDEAD_BEEFusize, 0x1234usize));
assert_eq!(parsed[1], (0xCAFE_F00Dusize, 0x5678usize));
}
#[test]
fn read_dmlh_total_frames_finds_value_inside_odml_list() {
let dmlh_chunk = {
let mut body = Vec::new();
body.extend_from_slice(&42u32.to_le_bytes());
body.extend_from_slice(&[0u8; 244]); chunk(b"dmlh", &body)
};
let odml = list(b"odml", &dmlh_chunk);
let mut hdrl_body = Vec::new();
hdrl_body.extend_from_slice(&chunk(b"avih", &[0u8; 56]));
hdrl_body.extend_from_slice(&odml);
assert_eq!(read_dmlh_total_frames(&hdrl_body), Some(42));
}
#[test]
fn read_dmlh_total_frames_returns_none_when_odml_absent() {
let mut hdrl_body = Vec::new();
hdrl_body.extend_from_slice(&chunk(b"avih", &[0u8; 56]));
assert_eq!(read_dmlh_total_frames(&hdrl_body), None);
}