use anyhow::{Context, Result, bail};
use codec::frame::{ColorSpace, PixelFormat, StreamInfo};
use crate::demux::{AudioTrack, DemuxResult};
use crate::streaming::{DemuxHeader, Sample, StreamingDemuxer};
pub(crate) fn demux_avi(data: &[u8]) -> Result<DemuxResult> {
if data.len() < 12 || &data[..4] != b"RIFF" || &data[8..12] != b"AVI " {
bail!("not a RIFF/AVI file");
}
let mut hdrl: Option<(usize, usize)> = None;
let mut movi_lists: Vec<(usize, usize)> = Vec::new();
scan_top_level_records(data, &mut hdrl, &mut movi_lists);
let (hdrl_start, hdrl_end) = hdrl.context("AVI: missing hdrl LIST")?;
if movi_lists.is_empty() {
bail!("AVI: missing movi LIST");
}
let video = find_video_stream(&data[hdrl_start..hdrl_end])
.context("AVI: no video stream found in hdrl")?;
let codec = fourcc_to_codec(&video.handler)
.or_else(|| fourcc_to_codec(&video.compression))
.with_context(|| {
format!(
"AVI: unsupported video fourcc {:?}/{:?}",
ascii(&video.handler),
ascii(&video.compression)
)
})?;
let stream_idx = video.stream_index;
let prefix = format!("{:02}", stream_idx);
let mut samples: Vec<Vec<u8>> = Vec::new();
for &(movi_start, movi_end) in &movi_lists {
collect_movi_samples(&data[movi_start..movi_end], &prefix, &mut samples)?;
}
if samples.is_empty() {
bail!(
"AVI: movi LIST contained no video samples for stream {:02}",
stream_idx
);
}
let total_frames =
read_dmlh_total_frames(&data[hdrl_start..hdrl_end]).unwrap_or(samples.len() as u64);
let duration = if video.frame_rate > 0.0 {
total_frames as f64 / video.frame_rate
} else {
0.0
};
let info = StreamInfo {
codec: codec.clone(),
width: video.width,
height: video.height,
frame_rate: video.frame_rate,
duration,
pixel_format: PixelFormat::Yuv420p,
color_space: ColorSpace::Bt709,
color_metadata: Default::default(),
total_frames,
bitrate: 0,
};
let detected_pf = codec::pixel_format::detect(&codec, &samples);
let info = StreamInfo {
pixel_format: detected_pf,
..info
};
Ok(DemuxResult {
codec,
info,
samples,
audio: None,
})
}
#[derive(Debug)]
struct VideoStream {
stream_index: u32,
handler: [u8; 4],
compression: [u8; 4],
width: u32,
height: u32,
frame_rate: f64,
}
fn find_video_stream(hdrl: &[u8]) -> Option<VideoStream> {
let mut pos = 0;
let mut stream_idx: u32 = 0;
while pos + 8 <= hdrl.len() {
let size = u32::from_le_bytes([hdrl[pos + 4], hdrl[pos + 5], hdrl[pos + 6], hdrl[pos + 7]])
as usize;
let fourcc = &hdrl[pos..pos + 4];
let payload_start = pos + 8;
let payload_end = payload_start + size;
if payload_end > hdrl.len() {
break;
}
if fourcc == b"LIST" && payload_start + 4 <= payload_end {
let list_type = &hdrl[payload_start..payload_start + 4];
if list_type == b"strl" {
let strl = &hdrl[payload_start + 4..payload_end];
if let Some(v) = parse_strl(strl, stream_idx) {
return Some(v);
}
stream_idx += 1;
}
}
pos = payload_end + (payload_end & 1);
}
None
}
fn parse_strl(strl: &[u8], stream_index: u32) -> Option<VideoStream> {
let mut strh: Option<&[u8]> = None;
let mut strf: Option<&[u8]> = None;
let mut pos = 0;
while pos + 8 <= strl.len() {
let fourcc = &strl[pos..pos + 4];
let size = u32::from_le_bytes([strl[pos + 4], strl[pos + 5], strl[pos + 6], strl[pos + 7]])
as usize;
let end = pos + 8 + size;
if end > strl.len() {
break;
}
let body = &strl[pos + 8..end];
if fourcc == b"strh" {
strh = Some(body);
} else if fourcc == b"strf" {
strf = Some(body);
}
pos = end + (end & 1);
}
let strh = strh?;
let strf = strf?;
if strh.len() < 32 {
return None;
}
let fcc_type: [u8; 4] = strh[0..4].try_into().ok()?;
if &fcc_type != b"vids" {
return None;
}
let handler: [u8; 4] = strh[4..8].try_into().ok()?;
let scale = u32::from_le_bytes([strh[20], strh[21], strh[22], strh[23]]);
let rate = u32::from_le_bytes([strh[24], strh[25], strh[26], strh[27]]);
let frame_rate = if scale > 0 {
rate as f64 / scale as f64
} else {
30.0
};
if strf.len() < 20 {
return None;
}
let width = i32::from_le_bytes([strf[4], strf[5], strf[6], strf[7]]).unsigned_abs();
let height = i32::from_le_bytes([strf[8], strf[9], strf[10], strf[11]]).unsigned_abs();
let compression: [u8; 4] = strf[16..20].try_into().ok()?;
Some(VideoStream {
stream_index,
handler,
compression,
width,
height,
frame_rate,
})
}
fn fourcc_to_codec(fcc: &[u8; 4]) -> Option<String> {
let mut norm = [0u8; 4];
for (i, b) in fcc.iter().enumerate() {
norm[i] = if (b'a'..=b'z').contains(b) {
b - 32
} else {
*b
};
}
match &norm {
b"DIVX" | b"DX50" | b"XVID" | b"DIV3" | b"DIV4" | b"DIV5" | b"DIV6" | b"MP4V" | b"MP4S"
| b"M4S2" | b"FMP4" | b"DM4V" | b"3IVX" | b"3IV2" | b"XVIX" => Some("mpeg4".into()),
b"H264" | b"X264" | b"AVC1" | b"DAVC" => Some("h264".into()),
b"MPG2" | b"MPEG" => Some("mpeg2".into()),
_ => None,
}
}
fn collect_movi_samples(movi: &[u8], stream_prefix: &str, out: &mut Vec<Vec<u8>>) -> Result<()> {
let prefix = stream_prefix.as_bytes();
if prefix.len() != 2 {
bail!("stream prefix must be 2 chars, got {:?}", stream_prefix);
}
let mut pos = 0;
while pos + 8 <= movi.len() {
let fcc = &movi[pos..pos + 4];
let size = u32::from_le_bytes([movi[pos + 4], movi[pos + 5], movi[pos + 6], movi[pos + 7]])
as usize;
let payload_start = pos + 8;
let payload_end = payload_start + size;
if payload_end > movi.len() {
break;
}
if fcc == b"LIST" && payload_start + 4 <= payload_end {
let list_type = &movi[payload_start..payload_start + 4];
if list_type == b"rec " {
collect_movi_samples(&movi[payload_start + 4..payload_end], stream_prefix, out)?;
}
} else if fcc.len() == 4 && fcc[0] == prefix[0] && fcc[1] == prefix[1] {
let kind = fcc[3];
if kind == b'c' || kind == b'b' {
out.push(movi[payload_start..payload_end].to_vec());
}
}
pos = payload_end + (payload_end & 1);
}
Ok(())
}
fn ascii(b: &[u8; 4]) -> String {
b.iter()
.map(|c| {
if c.is_ascii_graphic() {
*c as char
} else {
'.'
}
})
.collect()
}
pub struct AviStreamingDemuxer {
data: Vec<u8>,
header: DemuxHeader,
backend: Backend,
prefix: [u8; 2],
next_idx: u64,
pixel_format_detected: bool,
}
enum Backend {
Cursor(Vec<(usize, usize)>),
OpenDml {
samples: Vec<(usize, usize)>,
cursor: usize,
},
}
pub(crate) fn demux_avi_streaming_init(data: &[u8]) -> Result<AviStreamingDemuxer> {
if data.len() < 12 || &data[..4] != b"RIFF" || &data[8..12] != b"AVI " {
bail!("not a RIFF/AVI file");
}
let owned = data.to_vec();
let mut hdrl: Option<(usize, usize)> = None;
let mut movi_lists: Vec<(usize, usize)> = Vec::new();
scan_top_level_records(&owned, &mut hdrl, &mut movi_lists);
let (hdrl_start, hdrl_end) = hdrl.context("AVI: missing hdrl LIST")?;
if movi_lists.is_empty() {
bail!("AVI: missing movi LIST");
}
let video = find_video_stream(&owned[hdrl_start..hdrl_end])
.context("AVI: no video stream found in hdrl")?;
let codec = fourcc_to_codec(&video.handler)
.or_else(|| fourcc_to_codec(&video.compression))
.with_context(|| {
format!(
"AVI: unsupported video fourcc {:?}/{:?}",
ascii(&video.handler),
ascii(&video.compression)
)
})?;
let stream_idx = video.stream_index;
let prefix_str = format!("{:02}", stream_idx);
let prefix_bytes = prefix_str.as_bytes();
if prefix_bytes.len() != 2 {
bail!("AVI: stream index out of range");
}
let prefix = [prefix_bytes[0], prefix_bytes[1]];
let backend =
if let Some(ix_refs) = locate_stream_indx(&owned[hdrl_start..hdrl_end], stream_idx) {
let mut samples: Vec<(usize, usize)> = Vec::new();
for (ix_off, ix_size) in ix_refs {
parse_ix_chunk(&owned, ix_off, ix_size, &prefix, &mut samples);
}
Backend::OpenDml { samples, cursor: 0 }
} else {
Backend::Cursor(movi_lists)
};
let total_frames = read_dmlh_total_frames(&owned[hdrl_start..hdrl_end])
.or_else(|| read_avih_total_frames(&owned[hdrl_start..hdrl_end]))
.unwrap_or(0);
let duration = if total_frames > 0 && video.frame_rate > 0.0 {
total_frames as f64 / video.frame_rate
} else {
0.0
};
let info = StreamInfo {
codec: codec.clone(),
width: video.width,
height: video.height,
frame_rate: video.frame_rate,
duration,
pixel_format: PixelFormat::Yuv420p,
color_space: ColorSpace::Bt709,
color_metadata: Default::default(),
total_frames,
bitrate: 0,
};
Ok(AviStreamingDemuxer {
data: owned,
header: DemuxHeader { codec, info },
backend,
prefix,
next_idx: 0,
pixel_format_detected: false,
})
}
fn read_avih_total_frames(hdrl: &[u8]) -> Option<u64> {
let mut pos = 0;
while pos + 8 <= hdrl.len() {
let fcc = &hdrl[pos..pos + 4];
let size = u32::from_le_bytes([hdrl[pos + 4], hdrl[pos + 5], hdrl[pos + 6], hdrl[pos + 7]])
as usize;
let body_start = pos + 8;
let body_end = body_start + size;
if body_end > hdrl.len() {
return None;
}
if fcc == b"avih" {
if size < 20 {
return None;
}
let body = &hdrl[body_start..body_end];
let total = u32::from_le_bytes([body[16], body[17], body[18], body[19]]);
return if total > 0 { Some(total as u64) } else { None };
}
pos = body_end + (body_end & 1);
}
None
}
fn read_dmlh_total_frames(hdrl: &[u8]) -> Option<u64> {
let mut pos = 0;
while pos + 8 <= hdrl.len() {
let fcc = &hdrl[pos..pos + 4];
let size = u32::from_le_bytes([hdrl[pos + 4], hdrl[pos + 5], hdrl[pos + 6], hdrl[pos + 7]])
as usize;
let body_start = pos + 8;
let body_end = body_start + size;
if body_end > hdrl.len() {
return None;
}
if fcc == b"LIST" && size >= 4 && &hdrl[body_start..body_start + 4] == b"odml" {
let mut p = body_start + 4;
while p + 8 <= body_end {
let f = &hdrl[p..p + 4];
let s = u32::from_le_bytes([hdrl[p + 4], hdrl[p + 5], hdrl[p + 6], hdrl[p + 7]])
as usize;
let bs = p + 8;
let be = bs + s;
if be > body_end {
return None;
}
if f == b"dmlh" && s >= 4 {
let total =
u32::from_le_bytes([hdrl[bs], hdrl[bs + 1], hdrl[bs + 2], hdrl[bs + 3]]);
return if total > 0 { Some(total as u64) } else { None };
}
p = be + (be & 1);
}
return None;
}
pos = body_end + (body_end & 1);
}
None
}
fn scan_top_level_records(
data: &[u8],
hdrl: &mut Option<(usize, usize)>,
movi_lists: &mut Vec<(usize, usize)>,
) {
let mut pos = 0;
while pos + 8 <= data.len() {
let fcc = &data[pos..pos + 4];
let size = u32::from_le_bytes([data[pos + 4], data[pos + 5], data[pos + 6], data[pos + 7]])
as usize;
let payload_start = pos + 8;
let claimed_end = payload_start.saturating_add(size);
let payload_end = claimed_end.min(data.len());
if fcc == b"RIFF" && payload_start + 4 <= payload_end {
let form: [u8; 4] = data[payload_start..payload_start + 4].try_into().unwrap();
if &form == b"AVI " || &form == b"AVIX" {
scan_riff_segment(data, payload_start + 4, payload_end, hdrl, movi_lists);
}
} else if fcc == b"LIST" && payload_start + 4 <= payload_end {
classify_list(data, payload_start, payload_end, hdrl, movi_lists);
}
if claimed_end > data.len() {
break;
}
pos = payload_end + (payload_end & 1);
}
}
fn scan_riff_segment(
data: &[u8],
body_start: usize,
body_end: usize,
hdrl: &mut Option<(usize, usize)>,
movi_lists: &mut Vec<(usize, usize)>,
) {
let mut p = body_start;
while p + 8 <= body_end {
let fcc = &data[p..p + 4];
let size =
u32::from_le_bytes([data[p + 4], data[p + 5], data[p + 6], data[p + 7]]) as usize;
let payload_start = p + 8;
let claimed_end = payload_start.saturating_add(size);
let payload_end = claimed_end.min(body_end);
if fcc == b"LIST" && payload_start + 4 <= payload_end {
classify_list(data, payload_start, payload_end, hdrl, movi_lists);
}
if claimed_end > body_end {
break;
}
p = payload_end + (payload_end & 1);
}
}
fn classify_list(
data: &[u8],
payload_start: usize,
payload_end: usize,
hdrl: &mut Option<(usize, usize)>,
movi_lists: &mut Vec<(usize, usize)>,
) {
let list_type: [u8; 4] = data[payload_start..payload_start + 4].try_into().unwrap();
match &list_type {
b"hdrl" => {
if hdrl.is_none() {
*hdrl = Some((payload_start + 4, payload_end));
}
}
b"movi" => movi_lists.push((payload_start + 4, payload_end)),
_ => {}
}
}
fn locate_stream_indx(hdrl: &[u8], target_stream_idx: u32) -> Option<Vec<(usize, usize)>> {
let mut stream_idx: u32 = 0;
let mut pos = 0;
while pos + 8 <= hdrl.len() {
let fcc = &hdrl[pos..pos + 4];
let size = u32::from_le_bytes([hdrl[pos + 4], hdrl[pos + 5], hdrl[pos + 6], hdrl[pos + 7]])
as usize;
let body_start = pos + 8;
let body_end = body_start + size;
if body_end > hdrl.len() {
return None;
}
if fcc == b"LIST" && size >= 4 && &hdrl[body_start..body_start + 4] == b"strl" {
if stream_idx == target_stream_idx {
return parse_indx_in_strl(&hdrl[body_start + 4..body_end]);
}
stream_idx += 1;
}
pos = body_end + (body_end & 1);
}
None
}
fn parse_indx_in_strl(strl: &[u8]) -> Option<Vec<(usize, usize)>> {
let mut pos = 0;
while pos + 8 <= strl.len() {
let fcc = &strl[pos..pos + 4];
let size = u32::from_le_bytes([strl[pos + 4], strl[pos + 5], strl[pos + 6], strl[pos + 7]])
as usize;
let body_start = pos + 8;
let body_end = body_start + size;
if body_end > strl.len() {
return None;
}
if fcc == b"indx" {
return parse_indx_body(&strl[body_start..body_end]);
}
pos = body_end + (body_end & 1);
}
None
}
fn parse_indx_body(body: &[u8]) -> Option<Vec<(usize, usize)>> {
if body.len() < 24 {
return None;
}
let longs_per_entry = u16::from_le_bytes([body[0], body[1]]);
let _index_sub_type = body[2];
let index_type = body[3];
let n_entries = u32::from_le_bytes([body[4], body[5], body[6], body[7]]) as usize;
if index_type != 0x00 {
return None;
}
if longs_per_entry != 4 {
return None;
} let entries_start = 24;
let mut refs = Vec::with_capacity(n_entries);
for i in 0..n_entries {
let off = entries_start + i * 16;
if off + 16 > body.len() {
break;
}
let qw_offset = u64::from_le_bytes([
body[off],
body[off + 1],
body[off + 2],
body[off + 3],
body[off + 4],
body[off + 5],
body[off + 6],
body[off + 7],
]);
let dw_size =
u32::from_le_bytes([body[off + 8], body[off + 9], body[off + 10], body[off + 11]]);
let off_us = qw_offset as usize;
refs.push((off_us, dw_size as usize));
}
Some(refs)
}
fn parse_ix_chunk(
data: &[u8],
ix_header_off: usize,
_ix_size: usize,
prefix: &[u8; 2],
out: &mut Vec<(usize, usize)>,
) {
if ix_header_off + 8 > data.len() {
return;
}
let body_start = ix_header_off + 8;
let body_size = u32::from_le_bytes([
data[ix_header_off + 4],
data[ix_header_off + 5],
data[ix_header_off + 6],
data[ix_header_off + 7],
]) as usize;
let body_end = body_start.saturating_add(body_size).min(data.len());
if body_end < body_start + 24 {
return;
}
let body = &data[body_start..body_end];
let longs_per_entry = u16::from_le_bytes([body[0], body[1]]);
let _index_sub_type = body[2];
let index_type = body[3];
let n_entries = u32::from_le_bytes([body[4], body[5], body[6], body[7]]) as usize;
let chunk_id: [u8; 4] = body[8..12].try_into().unwrap();
let qw_base_offset = u64::from_le_bytes([
body[12], body[13], body[14], body[15], body[16], body[17], body[18], body[19],
]) as usize;
if index_type != 0x01 {
return;
} if longs_per_entry != 2 {
return;
}
if chunk_id[0] != prefix[0] || chunk_id[1] != prefix[1] {
return;
}
let kind = chunk_id[3];
if kind != b'c' && kind != b'b' {
return;
}
let entries_start = 24;
for i in 0..n_entries {
let off = entries_start + i * 8;
if off + 8 > body.len() {
break;
}
let dw_offset =
u32::from_le_bytes([body[off], body[off + 1], body[off + 2], body[off + 3]]) as usize;
let dw_size_raw =
u32::from_le_bytes([body[off + 4], body[off + 5], body[off + 6], body[off + 7]]);
let dw_size = (dw_size_raw & 0x7FFFFFFF) as usize;
let abs_off = qw_base_offset.saturating_add(dw_offset);
out.push((abs_off, dw_size));
}
}
impl StreamingDemuxer for AviStreamingDemuxer {
fn header(&self) -> &DemuxHeader {
&self.header
}
fn next_video_sample(&mut self) -> Result<Option<Sample>> {
let payload_range = match &mut self.backend {
Backend::OpenDml { samples, cursor } => {
loop {
if *cursor >= samples.len() {
return Ok(None);
}
let (off, size) = samples[*cursor];
*cursor += 1;
let end = off
.checked_add(size)
.ok_or_else(|| anyhow::anyhow!("AVI: ix## entry overflows usize"))?;
if end > self.data.len() {
continue;
}
break Some((off, end));
}
}
Backend::Cursor(walk) => {
loop {
while let Some(&(pos, end)) = walk.last() {
if pos + 8 <= end {
break;
}
walk.pop();
}
let Some(&mut (ref mut pos, end)) = walk.last_mut() else {
return Ok(None);
};
let fcc: [u8; 4] = self.data[*pos..*pos + 4].try_into()?;
let size = u32::from_le_bytes([
self.data[*pos + 4],
self.data[*pos + 5],
self.data[*pos + 6],
self.data[*pos + 7],
]) as usize;
let payload_start = *pos + 8;
let payload_end = payload_start + size;
if payload_end > end || payload_end > self.data.len() {
walk.pop();
continue;
}
*pos = payload_end + (payload_end & 1);
if &fcc == b"LIST" && payload_start + 4 <= payload_end {
let list_type: [u8; 4] =
self.data[payload_start..payload_start + 4].try_into()?;
if &list_type == b"rec " {
walk.push((payload_start + 4, payload_end));
continue;
}
continue; }
if fcc[0] != self.prefix[0] || fcc[1] != self.prefix[1] {
continue; }
let kind = fcc[3];
if kind != b'c' && kind != b'b' {
continue; }
break Some((payload_start, payload_end));
}
}
};
let Some((start, end)) = payload_range else {
return Ok(None);
};
let pts_ticks = self.next_idx as i64;
self.next_idx += 1;
let data = self.data[start..end].to_vec();
if !self.pixel_format_detected {
let detected =
codec::pixel_format::detect(&self.header.codec, std::slice::from_ref(&data));
self.header.info.pixel_format = detected;
self.pixel_format_detected = true;
}
Ok(Some(Sample {
data,
pts_ticks,
duration_ticks: 0,
}))
}
fn audio(&self) -> Option<&AudioTrack> {
None
}
}
#[cfg(test)]
mod tests {
use super::*;
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);
}
}