use std::io::{Seek, SeekFrom};
use oxideav_core::{
CodecId, CodecParameters, CodecResolver, CodecTag, Error, MediaType, Packet, ProbeContext,
Rational, Result, SampleFormat, StreamInfo, TimeBase,
};
use oxideav_core::{Demuxer, ReadSeek};
use crate::riff::{read_chunk_header, read_form_type, skip_chunk, skip_pad, AVI_FORM, LIST, RIFF};
use crate::stream_format::{
parse_bitmap_info_header, parse_waveformatex, parse_waveformatextensible, subformat_codec_hint,
ChannelLayout, ChannelMask, Guid, WAVE_FORMAT_EXTENSIBLE,
};
const AVI_INDEX_OF_INDEXES: u8 = 0x00;
const AVI_INDEX_OF_CHUNKS: u8 = 0x01;
const AVI_INDEX_SUB_2FIELD: u8 = 0x01;
const AVISTDINDEX_DELTA_BIT: u32 = 0x8000_0000;
const OPENDML_SUPER_INDEX_SOFT_CAP: usize = 256;
pub fn open(input: Box<dyn ReadSeek>, codecs: &dyn CodecResolver) -> Result<Box<dyn Demuxer>> {
Ok(Box::new(open_avi(input, codecs)?))
}
pub fn open_avi(input: Box<dyn ReadSeek>, codecs: &dyn CodecResolver) -> Result<AviDemuxer> {
open_avi_inner(
input, codecs, false, false,
)
}
pub fn open_avi_lenient(
input: Box<dyn ReadSeek>,
codecs: &dyn CodecResolver,
) -> Result<AviDemuxer> {
open_avi_inner(
input, codecs, true, false,
)
}
pub fn open_avi_strict(input: Box<dyn ReadSeek>, codecs: &dyn CodecResolver) -> Result<AviDemuxer> {
open_avi_inner(
input, codecs, false, true,
)
}
fn open_avi_inner(
mut input: Box<dyn ReadSeek>,
codecs: &dyn CodecResolver,
lenient: bool,
strict_cross_validate: bool,
) -> Result<AviDemuxer> {
let file_len = probe_file_len(&mut *input)?;
let top = match read_chunk_header(&mut *input)? {
Some(h) => h,
None => return Err(Error::invalid("AVI: empty file")),
};
if top.id != RIFF {
return Err(Error::invalid("AVI: not a RIFF file"));
}
let form = read_form_type(&mut *input)?;
if form != AVI_FORM {
return Err(Error::invalid("AVI: RIFF form type is not AVI"));
}
let declared_riff_total = 8u64.saturating_add(top.size as u64);
let truncated_head = declared_riff_total > file_len;
let riff_end = declared_riff_total.min(file_len);
let mut streams: Vec<StreamInfo> = Vec::new();
let mut packet_chunk_suffix: Vec<[u8; 2]> = Vec::new();
let mut movi_segments: Vec<(u64, u64)> = Vec::new();
let mut avih: Option<AviMainHeader> = None;
let mut metadata: Vec<(String, String)> = Vec::new();
let mut idx1_raw: Option<Vec<u8>> = None;
let mut super_indexes: Vec<SuperIndex> = Vec::new();
let mut vprps: Vec<VprpHeader> = Vec::new();
let mut dmlh_total_frames: Option<u32> = None;
let mut audio_infos: Vec<Option<AudioStrhInfo>> = Vec::new();
let mut video_strfs: Vec<Option<VideoStrfInfo>> = Vec::new();
let mut audio_strfs: Vec<Option<AudioStrfInfo>> = Vec::new();
let mut stream_names: Vec<Option<String>> = Vec::new();
let mut stream_header_data: Vec<Option<Vec<u8>>> = Vec::new();
let mut stream_frame_rects: Vec<Option<[i16; 4]>> = Vec::new();
let mut stream_languages: Vec<Option<u16>> = Vec::new();
let mut stream_initial_frames: Vec<Option<u32>> = Vec::new();
let mut stream_qualities: Vec<Option<u32>> = Vec::new();
let mut stream_priorities: Vec<Option<u16>> = Vec::new();
let mut stream_starts: Vec<Option<u32>> = Vec::new();
let mut stream_handlers: Vec<Option<[u8; 4]>> = Vec::new();
let mut stream_suggested_buffer_sizes: Vec<Option<u32>> = Vec::new();
let mut stream_lengths: Vec<Option<u32>> = Vec::new();
let mut stream_sample_sizes: Vec<Option<u32>> = Vec::new();
let mut stream_flags: Vec<Option<u32>> = Vec::new();
let mut stream_rates: Vec<Option<(u32, u32)>> = Vec::new();
let mut stream_fcc_types: Vec<Option<[u8; 4]>> = Vec::new();
let mut digitization_date: Option<String> = None;
let mut smpte_timecode: Option<String> = None;
walk_riff_body(
&mut *input,
riff_end,
file_len,
&mut streams,
&mut packet_chunk_suffix,
&mut movi_segments,
&mut avih,
&mut metadata,
&mut idx1_raw,
&mut super_indexes,
&mut vprps,
&mut dmlh_total_frames,
&mut audio_infos,
&mut video_strfs,
&mut audio_strfs,
&mut stream_names,
&mut stream_header_data,
&mut stream_frame_rects,
&mut stream_languages,
&mut stream_initial_frames,
&mut stream_qualities,
&mut stream_priorities,
&mut stream_starts,
&mut stream_handlers,
&mut stream_suggested_buffer_sizes,
&mut stream_sample_sizes,
&mut stream_lengths,
&mut stream_flags,
&mut stream_rates,
&mut stream_fcc_types,
&mut digitization_date,
&mut smpte_timecode,
codecs,
true,
)?;
input.seek(SeekFrom::Start(riff_end))?;
while let Some(hdr) = read_chunk_header_lenient(&mut *input)? {
if hdr.id == RIFF {
let form = read_form_type(&mut *input)?;
let ext_end =
(input.stream_position()? + hdr.size.saturating_sub(4) as u64).min(file_len);
if &form == b"AVIX" {
walk_riff_body(
&mut *input,
ext_end,
file_len,
&mut streams,
&mut packet_chunk_suffix,
&mut movi_segments,
&mut avih,
&mut metadata,
&mut idx1_raw,
&mut super_indexes,
&mut vprps,
&mut dmlh_total_frames,
&mut audio_infos,
&mut video_strfs,
&mut audio_strfs,
&mut stream_names,
&mut stream_header_data,
&mut stream_frame_rects,
&mut stream_languages,
&mut stream_initial_frames,
&mut stream_qualities,
&mut stream_priorities,
&mut stream_starts,
&mut stream_handlers,
&mut stream_suggested_buffer_sizes,
&mut stream_sample_sizes,
&mut stream_lengths,
&mut stream_flags,
&mut stream_rates,
&mut stream_fcc_types,
&mut digitization_date,
&mut smpte_timecode,
codecs,
false,
)?;
}
input.seek(SeekFrom::Start(ext_end))?;
skip_pad(&mut *input, hdr.size)?;
} else {
skip_chunk(&mut *input, &hdr)?;
}
}
if !lenient {
for (i, ai) in audio_infos.iter().enumerate() {
let info = match ai {
Some(v) => v,
None => continue,
};
if let Some(violation) = audio_strh_violation(info) {
return Err(Error::invalid(format!(
"AVI: audio stream {i} (wFormatTag=0x{:04X}): {violation}",
info.format_tag
)));
}
}
}
let audio_cbr_block_aligns: Vec<Option<u16>> = audio_infos
.iter()
.map(|ai| {
ai.and_then(|info| match classify_audio_sample_size(info.format_tag) {
Some(false) if info.block_align > 1 => Some(info.block_align),
_ => None,
})
})
.collect();
if movi_segments.is_empty() {
return Err(Error::invalid("AVI: missing movi list"));
}
let movi_start = movi_segments[0].0;
if streams.is_empty() {
return Err(Error::invalid("AVI: no streams"));
}
let duration_micros: i64 = match avih {
Some(h) if h.micro_sec_per_frame > 0 && h.total_frames > 0 => {
(h.total_frames as i64) * (h.micro_sec_per_frame as i64)
}
_ => 0,
};
if let Some(h) = &avih {
if h.width > 0 {
metadata.push(("avi:width".into(), h.width.to_string()));
}
if h.height > 0 {
metadata.push(("avi:height".into(), h.height.to_string()));
}
if h.streams > 0 {
metadata.push(("avi:streams".into(), h.streams.to_string()));
}
if h.flags != 0 {
metadata.push(("avi:flags".into(), format!("0x{:08X}", h.flags)));
}
if h.suggested_buffer_size > 0 {
metadata.push((
"avi:suggested_buffer_size".into(),
h.suggested_buffer_size.to_string(),
));
}
if h.max_bytes_per_sec > 0 {
metadata.push((
"avi:max_bytes_per_sec".into(),
h.max_bytes_per_sec.to_string(),
));
}
if h.padding_granularity > 0 {
metadata.push((
"avi:padding_granularity".into(),
h.padding_granularity.to_string(),
));
}
if h.initial_frames > 0 {
metadata.push(("avi:initial_frames".into(), h.initial_frames.to_string()));
}
if h.micro_sec_per_frame > 0 {
metadata.push((
"avi:micro_sec_per_frame".into(),
h.micro_sec_per_frame.to_string(),
));
}
if h.total_frames > 0 {
metadata.push(("avi:total_frames".into(), h.total_frames.to_string()));
}
}
if truncated_head {
metadata.push(("avi:truncated".into(), "true".into()));
}
if let Some(total) = dmlh_total_frames {
metadata.push(("avi:total_frames_all_segments".into(), total.to_string()));
}
if let Some(ref idit) = digitization_date {
metadata.push(("avi:idit".into(), idit.clone()));
}
if let Some(ref ismp) = smpte_timecode {
metadata.push(("avi:ismp".into(), ismp.clone()));
}
for (i, vp) in vprps.iter().enumerate() {
if vp.nb_field_per_frame == 0 {
continue;
}
let prefix = format!("avi:vprp.{i}");
metadata.push((
format!("{prefix}.video_format_token"),
vp.video_format_token.to_string(),
));
metadata.push((
format!("{prefix}.video_standard"),
vp.video_standard.to_string(),
));
if vp.vertical_refresh_rate > 0 {
metadata.push((
format!("{prefix}.vertical_refresh_rate"),
vp.vertical_refresh_rate.to_string(),
));
}
if vp.h_total_in_t > 0 {
metadata.push((
format!("{prefix}.h_total_in_t"),
vp.h_total_in_t.to_string(),
));
}
if vp.v_total_in_lines > 0 {
metadata.push((
format!("{prefix}.v_total_in_lines"),
vp.v_total_in_lines.to_string(),
));
}
if vp.frame_aspect_ratio > 0 {
let x = (vp.frame_aspect_ratio >> 16) & 0xFFFF;
let y = vp.frame_aspect_ratio & 0xFFFF;
metadata.push((format!("{prefix}.frame_aspect_ratio"), format!("{x}:{y}")));
}
if vp.frame_width_in_pixels > 0 {
metadata.push((
format!("{prefix}.frame_width_in_pixels"),
vp.frame_width_in_pixels.to_string(),
));
}
if vp.frame_height_in_lines > 0 {
metadata.push((
format!("{prefix}.frame_height_in_lines"),
vp.frame_height_in_lines.to_string(),
));
}
metadata.push((
format!("{prefix}.nb_field_per_frame"),
vp.nb_field_per_frame.to_string(),
));
for (j, fd) in vp.field_descs.iter().enumerate() {
let all_zero = fd.compressed_bm_height == 0
&& fd.compressed_bm_width == 0
&& fd.valid_bm_height == 0
&& fd.valid_bm_width == 0
&& fd.valid_bm_x_offset == 0
&& fd.valid_bm_y_offset == 0
&& fd.video_x_offset_in_t == 0
&& fd.video_y_valid_start_line == 0;
if all_zero {
continue;
}
let fp = format!("{prefix}.field{j}");
if fd.compressed_bm_height > 0 {
metadata.push((
format!("{fp}.compressed_bm_height"),
fd.compressed_bm_height.to_string(),
));
}
if fd.compressed_bm_width > 0 {
metadata.push((
format!("{fp}.compressed_bm_width"),
fd.compressed_bm_width.to_string(),
));
}
if fd.valid_bm_height > 0 {
metadata.push((
format!("{fp}.valid_bm_height"),
fd.valid_bm_height.to_string(),
));
}
if fd.valid_bm_width > 0 {
metadata.push((
format!("{fp}.valid_bm_width"),
fd.valid_bm_width.to_string(),
));
}
if fd.valid_bm_x_offset > 0 {
metadata.push((
format!("{fp}.valid_bm_x_offset"),
fd.valid_bm_x_offset.to_string(),
));
}
if fd.valid_bm_y_offset > 0 {
metadata.push((
format!("{fp}.valid_bm_y_offset"),
fd.valid_bm_y_offset.to_string(),
));
}
if fd.video_x_offset_in_t > 0 {
metadata.push((
format!("{fp}.video_x_offset_in_t"),
fd.video_x_offset_in_t.to_string(),
));
}
if fd.video_y_valid_start_line > 0 {
metadata.push((
format!("{fp}.video_y_valid_start_line"),
fd.video_y_valid_start_line.to_string(),
));
}
}
}
for (i, vs_opt) in video_strfs.iter().enumerate() {
let vs = match vs_opt {
Some(v) => v,
None => continue,
};
if vs.top_down {
metadata.push((format!("avi:vids.{i}.top_down"), "true".into()));
}
if let Some((r, g, b)) = vs.bitfields_masks {
metadata.push((
format!("avi:vids.{i}.bitfields"),
format!("r=0x{r:08X},g=0x{g:08X},b=0x{b:08X}"),
));
}
}
for (i, as_opt) in audio_strfs.iter().enumerate() {
let asi = match as_opt {
Some(a) => a,
None => continue,
};
if asi.format_tag != WAVE_FORMAT_EXTENSIBLE {
continue;
}
if let Some(valid) = asi.valid_bits_per_sample {
metadata.push((
format!("avi:auds.{i}.valid_bits_per_sample"),
valid.to_string(),
));
}
if let Some(mask) = asi.channel_mask {
metadata.push((
format!("avi:auds.{i}.channel_mask"),
format!("0x{mask:08X}"),
));
let cm = ChannelMask::from_raw(mask);
if !cm.is_empty() {
let speakers: Vec<&'static str> = cm.iter_speakers().map(|s| s.abbrev()).collect();
metadata.push((format!("avi:auds.{i}.channel_speakers"), speakers.join(",")));
}
if let Some(layout) = cm.layout() {
metadata.push((
format!("avi:auds.{i}.channel_layout"),
layout.label().to_string(),
));
}
}
if let Some(guid) = asi.subformat {
metadata.push((format!("avi:auds.{i}.subformat"), guid.display()));
if let Some(tag) = guid.ksdataformat_tag() {
metadata.push((
format!("avi:auds.{i}.subformat_wformat_tag"),
format!("0x{tag:04X}"),
));
}
}
}
for (i, name_opt) in stream_names.iter().enumerate() {
if let Some(name) = name_opt {
if !name.is_empty() {
metadata.push((format!("avi:strn.{i}"), name.clone()));
}
}
}
for (i, sh_opt) in stream_header_data.iter().enumerate() {
if let Some(bytes) = sh_opt {
metadata.push((format!("avi:strd.{i}.len"), bytes.len().to_string()));
}
}
for (i, rc_opt) in stream_frame_rects.iter().enumerate() {
if let Some([l, t, r, b]) = rc_opt {
metadata.push((
format!("avi:strh.{i}.frame_rect"),
format!("{l},{t},{r},{b}"),
));
}
}
for (i, lang_opt) in stream_languages.iter().enumerate() {
if let Some(lang) = lang_opt {
metadata.push((format!("avi:strh.{i}.language"), lang.to_string()));
}
}
for (i, init_opt) in stream_initial_frames.iter().enumerate() {
if let Some(init) = init_opt {
metadata.push((format!("avi:strh.{i}.initial_frames"), init.to_string()));
}
}
for (i, quality_opt) in stream_qualities.iter().enumerate() {
if let Some(q) = quality_opt {
metadata.push((format!("avi:strh.{i}.quality"), q.to_string()));
}
}
for (i, priority_opt) in stream_priorities.iter().enumerate() {
if let Some(p) = priority_opt {
metadata.push((format!("avi:strh.{i}.priority"), p.to_string()));
}
}
for (i, start_opt) in stream_starts.iter().enumerate() {
if let Some(s) = start_opt {
metadata.push((format!("avi:strh.{i}.start"), s.to_string()));
}
}
for (i, fcc_opt) in stream_fcc_types.iter().enumerate() {
if let Some(f) = fcc_opt {
metadata.push((format!("avi:strh.{i}.fcc_type"), format_fourcc_or_hex(f)));
}
}
for (i, handler_opt) in stream_handlers.iter().enumerate() {
if let Some(h) = handler_opt {
metadata.push((format!("avi:strh.{i}.handler"), format_fourcc_or_hex(h)));
}
}
for (i, sbs_opt) in stream_suggested_buffer_sizes.iter().enumerate() {
if let Some(n) = sbs_opt {
metadata.push((format!("avi:strh.{i}.suggested_buffer_size"), n.to_string()));
}
}
for (i, ss_opt) in stream_sample_sizes.iter().enumerate() {
if let Some(n) = ss_opt {
metadata.push((format!("avi:strh.{i}.sample_size"), n.to_string()));
}
}
for (i, len_opt) in stream_lengths.iter().enumerate() {
if let Some(n) = len_opt {
metadata.push((format!("avi:strh.{i}.length"), n.to_string()));
}
}
for (i, flags_opt) in stream_flags.iter().enumerate() {
if let Some(bits) = flags_opt {
metadata.push((format!("avi:strh.{i}.flags"), format!("0x{bits:08X}")));
}
}
for (i, rate_opt) in stream_rates.iter().enumerate() {
if let Some((scale, rate)) = rate_opt {
metadata.push((format!("avi:strh.{i}.scale"), scale.to_string()));
metadata.push((format!("avi:strh.{i}.rate"), rate.to_string()));
}
}
let mut palette_change_counts: Vec<u32> = vec![0u32; streams.len()];
let mut text_chunk_counts: Vec<u32> = vec![0u32; streams.len()];
let mut palette_change_data: Vec<Vec<Vec<u8>>> = vec![Vec::new(); streams.len()];
let mut text_chunk_data: Vec<Vec<Vec<u8>>> = vec![Vec::new(); streams.len()];
let mut sideband_data_loaded = false;
let mut idx1_rec_entries: Vec<Idx1RecEntry> = Vec::new();
let idx_table = if let Some(raw) = idx1_raw {
scan_idx1_for_suffix(&raw, &streams, *b"pc", &mut palette_change_counts);
scan_idx1_for_suffix(&raw, &streams, *b"tx", &mut text_chunk_counts);
read_sideband_data_from_idx1(
&mut *input,
&raw,
movi_start,
&streams,
*b"pc",
&mut palette_change_data,
);
read_sideband_data_from_idx1(
&mut *input,
&raw,
movi_start,
&streams,
*b"tx",
&mut text_chunk_data,
);
sideband_data_loaded = true;
let (table, recs) = build_idx_table(&mut *input, &raw, movi_start, &streams)?;
idx1_rec_entries = recs;
table
} else {
Vec::new()
};
for (s, &count) in palette_change_counts.iter().enumerate() {
if count > 0 {
metadata.push((format!("avi:palette_change.{s}"), count.to_string()));
}
}
for (s, &count) in text_chunk_counts.iter().enumerate() {
if count > 0 {
metadata.push((format!("avi:text_chunk.{s}"), count.to_string()));
}
}
if !idx1_rec_entries.is_empty() {
metadata.push((
"avi:idx1.rec_lists".into(),
idx1_rec_entries.len().to_string(),
));
}
let want_ix_scan = super_indexes.iter().any(|s| !s.entries.is_empty())
|| movi_segments.len() > 1
|| super_indexes
.iter()
.any(|s| s.b_index_sub_type == AVI_INDEX_SUB_2FIELD);
let std_indexes = if want_ix_scan {
scan_ix_in_movi(&mut *input, &movi_segments).unwrap_or_default()
} else {
Vec::new()
};
let mut field2_streams_seen: std::collections::BTreeSet<u32> =
std::collections::BTreeSet::new();
{
use std::collections::BTreeMap;
let mut per_stream_offsets: BTreeMap<u32, Vec<u32>> = BTreeMap::new();
let mut per_stream_2field: BTreeMap<u32, bool> = BTreeMap::new();
for ix in &std_indexes {
if let Some(stream) = parse_stream_index(&ix.chunk_id) {
if ix.b_index_sub_type == AVI_INDEX_SUB_2FIELD {
per_stream_2field.insert(stream, true);
let v = per_stream_offsets.entry(stream).or_default();
for e in &ix.entries {
v.push(e.dw_offset_field2);
}
}
}
}
for (stream, _) in per_stream_2field.iter() {
metadata.push((format!("avi:ix.{stream}.is_2field"), "true".into()));
field2_streams_seen.insert(*stream);
if let Some(offsets) = per_stream_offsets.get(stream) {
let joined = offsets
.iter()
.map(|o| o.to_string())
.collect::<Vec<_>>()
.join(",");
metadata.push((format!("avi:ix.{stream}.field2_offsets"), joined));
}
}
}
for (i, sx) in super_indexes.iter().enumerate() {
if sx.entries.len() > OPENDML_SUPER_INDEX_SOFT_CAP {
metadata.push((
format!("avi:indx.{i}.overflow_entries"),
sx.entries.len().to_string(),
));
}
}
for (i, sx) in super_indexes.iter().enumerate() {
if sx.entries.is_empty() {
continue;
}
if sx.b_index_sub_type == AVI_INDEX_SUB_2FIELD {
metadata.push((format!("avi:indx.{i}.sub_type_2field"), "true".into()));
}
}
for (i, sx) in super_indexes.iter().enumerate() {
if sx.entries.is_empty() {
continue;
}
if sx.w_longs_per_entry != 4 {
metadata.push((
format!("avi:indx.{i}.longs_per_entry"),
sx.w_longs_per_entry.to_string(),
));
}
}
for (i, sx) in super_indexes.iter().enumerate() {
if sx.entries.is_empty() {
continue;
}
let declares_own_slot = parse_stream_index(&sx.chunk_id) == Some(i as u32);
if !declares_own_slot {
metadata.push((
format!("avi:indx.{i}.chunk_id"),
format_fourcc_or_hex(&sx.chunk_id),
));
}
}
if !idx_table.is_empty() {
for s in &field2_streams_seen {
let any = idx_table.iter().any(|e| e.stream == *s);
if any {
metadata.push((format!("avi:idx1.{s}.is_2field"), "true".into()));
}
}
}
if !idx_table.is_empty() {
const PART_BOTH: u32 = 0x0020 | 0x0040; for s in 0..(streams.len() as u32) {
if field2_streams_seen.contains(&s) {
continue;
}
let mut entries = 0usize;
let mut all_part_both = true;
for e in idx_table.iter().filter(|e| e.stream == s) {
entries += 1;
if (e.flags & PART_BOTH) != PART_BOTH {
all_part_both = false;
break;
}
}
if entries > 0 && all_part_both {
metadata.push((format!("avi:idx1.{s}.is_2field"), "true".into()));
}
}
}
while super_indexes.len() < streams.len() {
super_indexes.push(SuperIndex::default());
}
let mut idx1_flags_per_stream: Vec<Vec<u32>> = vec![Vec::new(); streams.len()];
for e in &idx_table {
let s = e.stream as usize;
if s < idx1_flags_per_stream.len() {
idx1_flags_per_stream[s].push(e.flags);
}
}
if !idx_table.is_empty() && !std_indexes.is_empty() {
let primary_range = movi_segments.first().copied();
for (s_idx, _) in streams.iter().enumerate() {
let stream_id = s_idx as u32;
let idx1_for_stream: Vec<(u64, u32)> = idx_table
.iter()
.filter(|e| e.stream == stream_id)
.map(|e| (e.offset, e.size))
.collect();
if idx1_for_stream.is_empty() {
continue;
}
let mut ix_for_stream: Vec<(u64, u32)> = Vec::new();
for ix in &std_indexes {
let ix_stream = match parse_stream_index(&ix.chunk_id) {
Some(s) => s,
None => continue,
};
if ix_stream != stream_id {
continue;
}
if let Some((p_start, p_end)) = primary_range {
if ix.qw_base_offset < p_start || ix.qw_base_offset >= p_end {
continue;
}
}
for entry in &ix.entries {
let header_off = ix
.qw_base_offset
.saturating_add(entry.dw_offset as u64)
.saturating_sub(8);
ix_for_stream.push((header_off, entry.dw_size));
}
}
if ix_for_stream.is_empty() {
continue;
}
let common = idx1_for_stream.len().min(ix_for_stream.len());
let mut divergent_at: Option<usize> = None;
for i in 0..common {
if idx1_for_stream[i] != ix_for_stream[i] {
divergent_at = Some(i);
break;
}
}
if divergent_at.is_none() && idx1_for_stream.len() != ix_for_stream.len() {
divergent_at = Some(common);
}
if let Some(seq) = divergent_at {
let (a_off, a_size) = idx1_for_stream
.get(seq)
.copied()
.unwrap_or((u64::MAX, u32::MAX));
let (b_off, b_size) = ix_for_stream
.get(seq)
.copied()
.unwrap_or((u64::MAX, u32::MAX));
if strict_cross_validate {
return Err(Error::invalid(format!(
"AVI: idx1↔ix## offset divergence at seq={seq} \
on stream {stream_id}: \
idx1=offset_{a_off}_size_{a_size} \
ix##=offset_{b_off}_size_{b_size}"
)));
}
metadata.push((
format!("avi:idx1.{stream_id}.divergent_offsets"),
format!(
"seq={seq} idx1=offset_{a_off}_size_{a_size} \
ix##=offset_{b_off}_size_{b_size}"
),
));
}
}
}
if let Some(h) = &avih {
if h.max_bytes_per_sec > 0 && duration_micros > 0 && !idx_table.is_empty() {
let audio_sum: u64 = streams
.iter()
.filter(|s| matches!(s.params.media_type, MediaType::Audio))
.filter_map(|s| s.params.bit_rate)
.map(|br| br / 8)
.sum();
let mut video_bytes: u64 = 0;
for e in &idx_table {
let s = e.stream as usize;
if let Some(stream) = streams.get(s) {
if matches!(stream.params.media_type, MediaType::Video) {
video_bytes = video_bytes.saturating_add(e.size as u64);
}
}
}
let video_bps = if duration_micros > 0 {
let big = (video_bytes as u128) * 1_000_000u128;
let bps = big / (duration_micros as u128);
bps.min(u32::MAX as u128) as u64
} else {
0
};
let expected = audio_sum.saturating_add(video_bps);
if expected > h.max_bytes_per_sec as u64 {
metadata.push((
"avi:over_budget".into(),
format!(
"expected_max={} stamped={}",
expected.min(u32::MAX as u64),
h.max_bytes_per_sec
),
));
}
}
}
input.seek(SeekFrom::Start(movi_start))?;
Ok(AviDemuxer {
input,
streams,
packet_chunk_suffix,
movi_start,
movi_segments,
current_segment: 0,
per_stream_counter: Vec::new(),
metadata,
duration_micros,
idx_table,
idx1_rec_entries,
super_indexes,
std_indexes,
audio_cbr_block_aligns,
idx1_flags_per_stream,
palette_change_counts,
text_chunk_counts,
avih_flags: avih.as_ref().map(|h| h.flags).unwrap_or(0),
avih_suggested_buffer_size: avih.as_ref().map(|h| h.suggested_buffer_size).unwrap_or(0),
avih_padding_granularity: avih.as_ref().map(|h| h.padding_granularity).unwrap_or(0),
avih_initial_frames: avih.as_ref().map(|h| h.initial_frames).unwrap_or(0),
avih_micro_sec_per_frame: avih.as_ref().map(|h| h.micro_sec_per_frame).unwrap_or(0),
avih_max_bytes_per_sec: avih.as_ref().map(|h| h.max_bytes_per_sec).unwrap_or(0),
avih_total_frames: avih.as_ref().map(|h| h.total_frames).unwrap_or(0),
avih_streams: avih.as_ref().map(|h| h.streams).unwrap_or(0),
avih_width: avih.as_ref().map(|h| h.width).unwrap_or(0),
avih_height: avih.as_ref().map(|h| h.height).unwrap_or(0),
vprps,
dmlh_total_frames,
palette_change_data,
text_chunk_data,
sideband_data_loaded,
video_strf: video_strfs,
audio_strf: audio_strfs,
stream_names,
stream_header_data,
stream_frame_rects,
stream_languages,
stream_initial_frames,
stream_qualities,
stream_priorities,
stream_starts,
stream_handlers,
stream_suggested_buffer_sizes,
stream_sample_sizes,
stream_lengths,
stream_flags,
stream_rates,
stream_fcc_types,
digitization_date,
smpte_timecode,
})
}
#[allow(clippy::too_many_arguments)]
fn walk_riff_body(
input: &mut dyn ReadSeek,
end: u64,
file_len: u64,
streams: &mut Vec<StreamInfo>,
packet_chunk_suffix: &mut Vec<[u8; 2]>,
movi_segments: &mut Vec<(u64, u64)>,
avih: &mut Option<AviMainHeader>,
metadata: &mut Vec<(String, String)>,
idx1_raw: &mut Option<Vec<u8>>,
super_indexes: &mut Vec<SuperIndex>,
vprps: &mut Vec<VprpHeader>,
dmlh_total_frames: &mut Option<u32>,
audio_infos: &mut Vec<Option<AudioStrhInfo>>,
video_strfs: &mut Vec<Option<VideoStrfInfo>>,
audio_strfs: &mut Vec<Option<AudioStrfInfo>>,
stream_names: &mut Vec<Option<String>>,
stream_header_data: &mut Vec<Option<Vec<u8>>>,
stream_frame_rects: &mut Vec<Option<[i16; 4]>>,
stream_languages: &mut Vec<Option<u16>>,
stream_initial_frames: &mut Vec<Option<u32>>,
stream_qualities: &mut Vec<Option<u32>>,
stream_priorities: &mut Vec<Option<u16>>,
stream_starts: &mut Vec<Option<u32>>,
stream_handlers: &mut Vec<Option<[u8; 4]>>,
stream_suggested_buffer_sizes: &mut Vec<Option<u32>>,
stream_sample_sizes: &mut Vec<Option<u32>>,
stream_lengths: &mut Vec<Option<u32>>,
stream_flags: &mut Vec<Option<u32>>,
stream_rates: &mut Vec<Option<(u32, u32)>>,
stream_fcc_types: &mut Vec<Option<[u8; 4]>>,
digitization_date: &mut Option<String>,
smpte_timecode: &mut Option<String>,
codecs: &dyn CodecResolver,
is_primary: bool,
) -> Result<()> {
while input.stream_position()? < end {
let hdr = match read_chunk_header_lenient(input)? {
Some(h) => h,
None => break,
};
if hdr.id == LIST {
let list_type = read_form_type(input)?;
let body_len = hdr.size.saturating_sub(4);
let body_start = input.stream_position()?;
let body_end = (body_start + body_len as u64).min(end).min(file_len);
match &list_type {
b"hdrl" if is_primary => {
let (
main,
stream_infos,
suffixes,
sxs,
vps,
dmlh,
info_md,
ais,
vss,
asfs,
names,
strds,
rcframes,
langs,
initial_frames_vec,
qualities_vec,
priorities_vec,
starts_vec,
handlers_vec,
suggested_buffer_sizes_vec,
sample_sizes_vec,
lengths_vec,
flags_vec,
rates_vec,
fcc_types_vec,
idit,
ismp,
) = parse_hdrl(input, body_end, codecs)?;
*avih = Some(main);
*streams = stream_infos;
*packet_chunk_suffix = suffixes;
*super_indexes = sxs;
*vprps = vps;
*dmlh_total_frames = dmlh;
metadata.extend(info_md);
*audio_infos = ais;
*video_strfs = vss;
*audio_strfs = asfs;
*stream_names = names;
*stream_header_data = strds;
*stream_frame_rects = rcframes;
*stream_languages = langs;
*stream_initial_frames = initial_frames_vec;
*stream_qualities = qualities_vec;
*stream_priorities = priorities_vec;
*stream_starts = starts_vec;
*stream_handlers = handlers_vec;
*stream_suggested_buffer_sizes = suggested_buffer_sizes_vec;
*stream_sample_sizes = sample_sizes_vec;
*stream_lengths = lengths_vec;
*stream_flags = flags_vec;
*stream_rates = rates_vec;
*stream_fcc_types = fcc_types_vec;
*digitization_date = idit;
*smpte_timecode = ismp;
}
b"movi" => {
movi_segments.push((body_start, body_end));
}
b"INFO" if is_primary => {
let avail = body_end.saturating_sub(body_start) as usize;
let mut buf = vec![0u8; avail];
let _ = read_up_to(input, &mut buf)?;
parse_info_list(&buf, metadata);
}
_ => {}
}
input.seek(SeekFrom::Start(body_end))?;
skip_pad(input, hdr.size)?;
} else if &hdr.id == b"idx1" && is_primary {
let pos = input.stream_position()?;
let avail = file_len.saturating_sub(pos);
let take = (hdr.size as u64).min(avail) as usize;
let mut buf = vec![0u8; take];
let read = read_up_to(input, &mut buf)?;
buf.truncate(read);
let remaining = hdr.size as u64 - read as u64;
if remaining > 0 {
let _ = input.seek(SeekFrom::Current(remaining as i64));
}
skip_pad(input, hdr.size)?;
*idx1_raw = Some(buf);
} else {
skip_chunk(input, &hdr)?;
}
}
Ok(())
}
fn read_chunk_header_lenient<R: std::io::Read + ?Sized>(
r: &mut R,
) -> Result<Option<crate::riff::ChunkHeader>> {
let mut buf = [0u8; 8];
let mut got = 0;
while got < 8 {
match r.read(&mut buf[got..]) {
Ok(0) => return Ok(None),
Ok(n) => got += n,
Err(e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
Err(e) => return Err(e.into()),
}
}
let id = [buf[0], buf[1], buf[2], buf[3]];
let size = u32::from_le_bytes([buf[4], buf[5], buf[6], buf[7]]);
Ok(Some(crate::riff::ChunkHeader { id, size }))
}
fn read_up_to<R: std::io::Read + ?Sized>(r: &mut R, buf: &mut [u8]) -> Result<usize> {
let mut got = 0;
while got < buf.len() {
match r.read(&mut buf[got..]) {
Ok(0) => break,
Ok(n) => got += n,
Err(e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
Err(e) => return Err(e.into()),
}
}
Ok(got)
}
fn parse_info_list(buf: &[u8], out: &mut Vec<(String, String)>) {
let mut i = 0usize;
while i + 8 <= buf.len() {
let id: [u8; 4] = [buf[i], buf[i + 1], buf[i + 2], buf[i + 3]];
let size = u32::from_le_bytes([buf[i + 4], buf[i + 5], buf[i + 6], buf[i + 7]]) as usize;
i += 8;
if i + size > buf.len() {
break;
}
let raw = &buf[i..i + size];
let end = raw.iter().position(|&b| b == 0).unwrap_or(raw.len());
let value = String::from_utf8_lossy(&raw[..end]).trim().to_string();
if !value.is_empty() {
match info_id_to_key(&id) {
Some(k) => out.push((k.to_string(), value)),
None => {
let key = if id.iter().all(|b| b.is_ascii_graphic()) {
format!("avi:info.{}", std::str::from_utf8(&id).unwrap_or("____"))
} else {
format!(
"avi:info.tag_{:02x}{:02x}{:02x}{:02x}",
id[0], id[1], id[2], id[3]
)
};
out.push((key, value));
}
}
}
i += size;
if size % 2 == 1 {
i += 1;
}
}
}
fn info_id_to_key(id: &[u8; 4]) -> Option<&'static str> {
match id {
b"INAM" => Some("title"),
b"IART" => Some("artist"),
b"IPRD" => Some("album"),
b"ICMT" => Some("comment"),
b"ICRD" => Some("date"),
b"IGNR" => Some("genre"),
b"ICOP" => Some("copyright"),
b"IENG" => Some("engineer"),
b"ITCH" => Some("technician"),
b"ISFT" => Some("encoder"),
b"ISBJ" => Some("subject"),
b"ITRK" => Some("track"),
_ => None,
}
}
#[derive(Clone, Copy, Debug, Default)]
struct AviMainHeader {
micro_sec_per_frame: u32,
max_bytes_per_sec: u32,
flags: u32,
total_frames: u32,
initial_frames: u32,
streams: u32,
suggested_buffer_size: u32,
width: u32,
height: u32,
padding_granularity: u32,
}
fn parse_avih(buf: &[u8]) -> Result<AviMainHeader> {
if buf.len() < 40 {
return Err(Error::invalid("AVI: avih too short"));
}
Ok(AviMainHeader {
micro_sec_per_frame: u32::from_le_bytes([buf[0], buf[1], buf[2], buf[3]]),
max_bytes_per_sec: u32::from_le_bytes([buf[4], buf[5], buf[6], buf[7]]),
padding_granularity: u32::from_le_bytes([buf[8], buf[9], buf[10], buf[11]]),
flags: u32::from_le_bytes([buf[12], buf[13], buf[14], buf[15]]),
total_frames: u32::from_le_bytes([buf[16], buf[17], buf[18], buf[19]]),
initial_frames: u32::from_le_bytes([buf[20], buf[21], buf[22], buf[23]]),
streams: u32::from_le_bytes([buf[24], buf[25], buf[26], buf[27]]),
suggested_buffer_size: u32::from_le_bytes([buf[28], buf[29], buf[30], buf[31]]),
width: u32::from_le_bytes([buf[32], buf[33], buf[34], buf[35]]),
height: u32::from_le_bytes([buf[36], buf[37], buf[38], buf[39]]),
})
}
type HdrlOutput = (
AviMainHeader,
Vec<StreamInfo>,
Vec<[u8; 2]>,
Vec<SuperIndex>,
Vec<VprpHeader>,
Option<u32>,
Vec<(String, String)>,
Vec<Option<AudioStrhInfo>>,
Vec<Option<VideoStrfInfo>>,
Vec<Option<AudioStrfInfo>>,
Vec<Option<String>>,
Vec<Option<Vec<u8>>>,
Vec<Option<[i16; 4]>>,
Vec<Option<u16>>,
Vec<Option<u32>>,
Vec<Option<u32>>,
Vec<Option<u16>>,
Vec<Option<u32>>,
Vec<Option<[u8; 4]>>,
Vec<Option<u32>>,
Vec<Option<u32>>,
Vec<Option<u32>>,
Vec<Option<u32>>,
Vec<Option<(u32, u32)>>,
Vec<Option<[u8; 4]>>,
Option<String>,
Option<String>,
);
fn parse_hdrl<R: ReadSeek + ?Sized>(
r: &mut R,
end_pos: u64,
codecs: &dyn CodecResolver,
) -> Result<HdrlOutput> {
let mut main = AviMainHeader::default();
let mut streams: Vec<StreamInfo> = Vec::new();
let mut suffixes: Vec<[u8; 2]> = Vec::new();
let mut super_indexes: Vec<SuperIndex> = Vec::new();
let mut vprps: Vec<VprpHeader> = Vec::new();
let mut audio_infos: Vec<Option<AudioStrhInfo>> = Vec::new();
let mut video_strfs: Vec<Option<VideoStrfInfo>> = Vec::new();
let mut audio_strfs: Vec<Option<AudioStrfInfo>> = Vec::new();
let mut stream_names: Vec<Option<String>> = Vec::new();
let mut stream_header_data: Vec<Option<Vec<u8>>> = Vec::new();
let mut stream_frame_rects: Vec<Option<[i16; 4]>> = Vec::new();
let mut stream_languages: Vec<Option<u16>> = Vec::new();
let mut stream_initial_frames: Vec<Option<u32>> = Vec::new();
let mut stream_qualities: Vec<Option<u32>> = Vec::new();
let mut stream_priorities: Vec<Option<u16>> = Vec::new();
let mut stream_starts: Vec<Option<u32>> = Vec::new();
let mut stream_handlers: Vec<Option<[u8; 4]>> = Vec::new();
let mut stream_suggested_buffer_sizes: Vec<Option<u32>> = Vec::new();
let mut stream_sample_sizes: Vec<Option<u32>> = Vec::new();
let mut stream_lengths: Vec<Option<u32>> = Vec::new();
let mut stream_flags: Vec<Option<u32>> = Vec::new();
let mut stream_rates: Vec<Option<(u32, u32)>> = Vec::new();
let mut stream_fcc_types: Vec<Option<[u8; 4]>> = Vec::new();
let mut dmlh_total_frames: Option<u32> = None;
let mut info_metadata: Vec<(String, String)> = Vec::new();
let mut digitization_date: Option<String> = None;
let mut smpte_timecode: Option<String> = None;
while r.stream_position()? < end_pos {
let hdr = match read_chunk_header(r)? {
Some(h) => h,
None => break,
};
match &hdr.id {
b"IDIT" => {
let body = read_body_bounded(r, hdr.size)?;
digitization_date = parse_idit_body(&body);
skip_pad(r, hdr.size)?;
}
b"ISMP" => {
let body = read_body_bounded(r, hdr.size)?;
smpte_timecode = parse_ismp_body(&body);
skip_pad(r, hdr.size)?;
}
b"avih" => {
let body = read_body_bounded(r, hdr.size)?;
main = parse_avih(&body)?;
skip_pad(r, hdr.size)?;
}
b"LIST" => {
let list_type = read_form_type(r)?;
let body_len = hdr.size.saturating_sub(4);
let body_start = r.stream_position()?;
let body_end = body_start + body_len as u64;
if &list_type == b"strl" {
let (
si,
suf,
sx,
vp,
ai,
vs,
asi,
name,
strd_bytes,
rc_frame,
lang,
initial_frames,
quality,
priority,
start,
handler,
suggested_buffer_size,
sample_size,
length,
flags,
rate_scale,
fcc_type,
) = parse_strl(r, body_end, streams.len() as u32, codecs)?;
if let Some(si) = si {
streams.push(si);
suffixes.push(suf.unwrap_or(*b"xx"));
super_indexes.push(sx);
vprps.push(vp);
audio_infos.push(ai);
video_strfs.push(vs);
audio_strfs.push(asi);
stream_names.push(name);
stream_header_data.push(strd_bytes);
stream_frame_rects.push(rc_frame);
stream_languages.push(lang);
stream_initial_frames.push(initial_frames);
stream_qualities.push(quality);
stream_priorities.push(priority);
stream_starts.push(start);
stream_handlers.push(handler);
stream_suggested_buffer_sizes.push(suggested_buffer_size);
stream_sample_sizes.push(sample_size);
stream_lengths.push(length);
stream_flags.push(flags);
stream_rates.push(rate_scale);
stream_fcc_types.push(fcc_type);
}
} else if &list_type == b"odml" {
dmlh_total_frames = parse_odml_list(r, body_end)?;
} else if &list_type == b"INFO" {
let avail = body_end.saturating_sub(body_start) as usize;
let mut buf = vec![0u8; avail];
let _ = read_up_to(r, &mut buf)?;
parse_info_list(&buf, &mut info_metadata);
}
r.seek(SeekFrom::Start(body_end))?;
skip_pad(r, hdr.size)?;
}
_ => {
skip_chunk(r, &hdr)?;
}
}
}
Ok((
main,
streams,
suffixes,
super_indexes,
vprps,
dmlh_total_frames,
info_metadata,
audio_infos,
video_strfs,
audio_strfs,
stream_names,
stream_header_data,
stream_frame_rects,
stream_languages,
stream_initial_frames,
stream_qualities,
stream_priorities,
stream_starts,
stream_handlers,
stream_suggested_buffer_sizes,
stream_sample_sizes,
stream_lengths,
stream_flags,
stream_rates,
stream_fcc_types,
digitization_date,
smpte_timecode,
))
}
fn parse_idit_body(body: &[u8]) -> Option<String> {
let end = body
.iter()
.rposition(|&b| b != 0 && !b.is_ascii_whitespace())
.map(|p| p + 1)
.unwrap_or(0);
if end == 0 {
return None;
}
Some(String::from_utf8_lossy(&body[..end]).into_owned())
}
fn parse_ismp_body(body: &[u8]) -> Option<String> {
let end = body
.iter()
.rposition(|&b| b != 0 && !b.is_ascii_whitespace())
.map(|p| p + 1)
.unwrap_or(0);
if end == 0 {
return None;
}
Some(String::from_utf8_lossy(&body[..end]).into_owned())
}
fn parse_odml_list<R: ReadSeek + ?Sized>(r: &mut R, end_pos: u64) -> Result<Option<u32>> {
while r.stream_position()? < end_pos {
let hdr = match read_chunk_header(r)? {
Some(h) => h,
None => break,
};
if &hdr.id == b"dmlh" {
let take = (hdr.size as u64).min(4096) as u32;
let body = read_body_bounded(r, take)?;
let remaining = (hdr.size as u64).saturating_sub(take as u64);
if remaining > 0 {
r.seek(SeekFrom::Current(remaining as i64))?;
}
skip_pad(r, hdr.size)?;
if body.len() >= 4 {
let total = u32::from_le_bytes([body[0], body[1], body[2], body[3]]);
return Ok(Some(total));
}
return Ok(None);
}
skip_chunk(r, &hdr)?;
}
Ok(None)
}
type StrlOutput = (
Option<StreamInfo>,
Option<[u8; 2]>,
SuperIndex,
VprpHeader,
Option<AudioStrhInfo>,
Option<VideoStrfInfo>,
Option<AudioStrfInfo>,
Option<String>,
Option<Vec<u8>>,
Option<[i16; 4]>,
Option<u16>,
Option<u32>,
Option<u32>,
Option<u16>,
Option<u32>,
Option<[u8; 4]>,
Option<u32>,
Option<u32>,
Option<u32>,
Option<u32>,
Option<(u32, u32)>,
Option<[u8; 4]>,
);
fn parse_strl<R: ReadSeek + ?Sized>(
r: &mut R,
end_pos: u64,
index: u32,
codecs: &dyn CodecResolver,
) -> Result<StrlOutput> {
let mut strh_buf: Option<Vec<u8>> = None;
let mut strf_buf: Option<Vec<u8>> = None;
let mut super_index = SuperIndex::default();
let mut vprp = VprpHeader::default();
let mut strn_name: Option<String> = None;
let mut strd_bytes: Option<Vec<u8>> = None;
while r.stream_position()? < end_pos {
let hdr = match read_chunk_header(r)? {
Some(h) => h,
None => break,
};
match &hdr.id {
b"strh" => {
strh_buf = Some(read_body_bounded(r, hdr.size)?);
skip_pad(r, hdr.size)?;
}
b"strf" => {
strf_buf = Some(read_body_bounded(r, hdr.size)?);
skip_pad(r, hdr.size)?;
}
b"strn" => {
let body = read_body_bounded(r, hdr.size)?;
skip_pad(r, hdr.size)?;
strn_name = parse_strn_body(&body);
}
b"strd" => {
let body = read_body_bounded(r, hdr.size)?;
skip_pad(r, hdr.size)?;
strd_bytes = Some(body);
}
b"indx" => {
let body = read_body_bounded(r, hdr.size)?;
skip_pad(r, hdr.size)?;
super_index = parse_indx(&body)?;
}
b"vprp" => {
let body = read_body_bounded(r, hdr.size)?;
skip_pad(r, hdr.size)?;
if let Some(parsed) = parse_vprp(&body) {
vprp = parsed;
}
}
_ => {
skip_chunk(r, &hdr)?;
}
}
}
let strh = match strh_buf {
Some(b) => b,
None => {
return Ok((
None,
None,
super_index,
vprp,
None,
None,
None,
strn_name,
strd_bytes,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
));
}
};
let strf = strf_buf.unwrap_or_default();
let parsed = build_stream(index, &strh, &strf, codecs)?;
Ok((
Some(parsed.0),
Some(parsed.1),
super_index,
vprp,
parsed.2,
parsed.3,
parsed.4,
strn_name,
strd_bytes,
parsed.5,
parsed.6,
parsed.7,
parsed.8,
parsed.9,
parsed.10,
parsed.11,
parsed.12,
parsed.13,
parsed.14,
parsed.15,
parsed.16,
parsed.17,
))
}
fn parse_strn_body(body: &[u8]) -> Option<String> {
let end = body
.iter()
.rposition(|&b| b != 0)
.map(|p| p + 1)
.unwrap_or(0);
if end == 0 {
return None;
}
Some(String::from_utf8_lossy(&body[..end]).into_owned())
}
fn parse_vprp(body: &[u8]) -> Option<VprpHeader> {
if body.len() < 36 {
return None;
}
let read_dword = |off: usize| -> u32 {
u32::from_le_bytes([body[off], body[off + 1], body[off + 2], body[off + 3]])
};
let nb_field_per_frame = read_dword(32);
let max_descs_by_body = (body.len().saturating_sub(36)) / 32;
let n = (nb_field_per_frame as usize).min(max_descs_by_body).min(8);
let mut field_descs = Vec::with_capacity(n);
for i in 0..n {
let base = 36 + i * 32;
field_descs.push(VprpFieldDesc {
compressed_bm_height: read_dword(base),
compressed_bm_width: read_dword(base + 4),
valid_bm_height: read_dword(base + 8),
valid_bm_width: read_dword(base + 12),
valid_bm_x_offset: read_dword(base + 16),
valid_bm_y_offset: read_dword(base + 20),
video_x_offset_in_t: read_dword(base + 24),
video_y_valid_start_line: read_dword(base + 28),
});
}
Some(VprpHeader {
video_format_token: read_dword(0),
video_standard: read_dword(4),
vertical_refresh_rate: read_dword(8),
h_total_in_t: read_dword(12),
v_total_in_lines: read_dword(16),
frame_aspect_ratio: read_dword(20),
frame_width_in_pixels: read_dword(24),
frame_height_in_lines: read_dword(28),
nb_field_per_frame,
field_descs,
})
}
fn parse_indx(body: &[u8]) -> Result<SuperIndex> {
if body.len() < 24 {
return Err(Error::invalid("AVI: indx super-index header truncated"));
}
let w_longs_per_entry = u16::from_le_bytes([body[0], body[1]]);
let b_index_sub_type = body[2];
let b_index_type = body[3];
let n_entries_in_use = u32::from_le_bytes([body[4], body[5], body[6], body[7]]) as usize;
let mut chunk_id = [0u8; 4];
chunk_id.copy_from_slice(&body[8..12]);
let entries_byte_len = n_entries_in_use.saturating_mul(16);
let need = 24usize.saturating_add(entries_byte_len);
if body.len() < need {
return Err(Error::invalid(
"AVI: indx super-index entry table truncated",
));
}
if b_index_type != AVI_INDEX_OF_INDEXES {
return Ok(SuperIndex::default());
}
let mut entries = Vec::with_capacity(n_entries_in_use);
for i in 0..n_entries_in_use {
let base = 24 + i * 16;
let qw_offset = u64::from_le_bytes([
body[base],
body[base + 1],
body[base + 2],
body[base + 3],
body[base + 4],
body[base + 5],
body[base + 6],
body[base + 7],
]);
let dw_size = u32::from_le_bytes([
body[base + 8],
body[base + 9],
body[base + 10],
body[base + 11],
]);
let dw_duration = u32::from_le_bytes([
body[base + 12],
body[base + 13],
body[base + 14],
body[base + 15],
]);
entries.push(SuperIndexEntry {
qw_offset,
dw_size,
dw_duration,
});
}
Ok(SuperIndex {
w_longs_per_entry,
b_index_sub_type,
chunk_id,
entries,
})
}
fn parse_ix_chunk(body: &[u8]) -> Option<StdIndex> {
if body.len() < 24 {
return None;
}
let w_longs_per_entry = u16::from_le_bytes([body[0], body[1]]);
let b_index_sub_type = body[2];
let b_index_type = body[3];
if b_index_type != AVI_INDEX_OF_CHUNKS {
return None;
}
let entry_size = match w_longs_per_entry {
2 => 8usize,
3 if b_index_sub_type == AVI_INDEX_SUB_2FIELD => 12usize,
_ => return None,
};
let n_entries_in_use = u32::from_le_bytes([body[4], body[5], body[6], body[7]]) as usize;
let mut chunk_id = [0u8; 4];
chunk_id.copy_from_slice(&body[8..12]);
let qw_base_offset = u64::from_le_bytes([
body[12], body[13], body[14], body[15], body[16], body[17], body[18], body[19],
]);
let entries_byte_len = n_entries_in_use.saturating_mul(entry_size);
let need = 24usize.saturating_add(entries_byte_len);
if body.len() < need {
return None;
}
let mut entries = Vec::with_capacity(n_entries_in_use);
for i in 0..n_entries_in_use {
let base = 24 + i * entry_size;
let dw_offset =
u32::from_le_bytes([body[base], body[base + 1], body[base + 2], body[base + 3]]);
let dw_size_raw = u32::from_le_bytes([
body[base + 4],
body[base + 5],
body[base + 6],
body[base + 7],
]);
let is_keyframe = (dw_size_raw & AVISTDINDEX_DELTA_BIT) == 0;
let dw_size = dw_size_raw & !AVISTDINDEX_DELTA_BIT;
let dw_offset_field2 = if entry_size == 12 {
u32::from_le_bytes([
body[base + 8],
body[base + 9],
body[base + 10],
body[base + 11],
])
} else {
0
};
entries.push(StdIndexEntry {
dw_offset,
dw_size,
is_keyframe,
dw_offset_field2,
});
}
Some(StdIndex {
chunk_id,
qw_base_offset,
b_index_sub_type,
entries,
})
}
fn scan_ix_in_movi<R: ReadSeek + ?Sized>(
r: &mut R,
movi_segments: &[(u64, u64)],
) -> Result<Vec<StdIndex>> {
let mut out: Vec<StdIndex> = Vec::new();
for &(start, end) in movi_segments {
r.seek(SeekFrom::Start(start))?;
while r.stream_position()? + 8 <= end {
let hdr = match read_chunk_header_lenient(r)? {
Some(h) => h,
None => break,
};
let body_end = (r.stream_position()? + hdr.size as u64).min(end);
if hdr.id[0] == b'i' && hdr.id[1] == b'x' {
let body = read_body_bounded(r, hdr.size).ok();
if let Some(b) = body {
if let Some(idx) = parse_ix_chunk(&b) {
out.push(idx);
}
}
skip_pad(r, hdr.size)?;
} else if hdr.id == LIST {
let _ = read_form_type(r)?;
continue;
} else {
let _ = body_end; skip_chunk(r, &hdr)?;
}
}
}
Ok(out)
}
#[derive(Clone, Copy, Debug)]
pub(crate) struct AudioStrhInfo {
pub format_tag: u16,
pub sample_size: u32,
pub block_align: u16,
}
#[derive(Clone, Copy, Debug, Default)]
pub struct AudioStrfInfo {
pub format_tag: u16,
pub valid_bits_per_sample: Option<u16>,
pub channel_mask: Option<u32>,
pub subformat: Option<Guid>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct BlockAlignViolation {
pub stream_index: u32,
pub entry_index: usize,
pub dw_size: u32,
pub block_align: u16,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct SuperIndexDurationViolation {
pub stream_index: u32,
pub super_index_duration_total: u64,
pub dmlh_total_frames: u64,
}
type BuildStreamOutput = (
StreamInfo,
[u8; 2],
Option<AudioStrhInfo>,
Option<VideoStrfInfo>,
Option<AudioStrfInfo>,
Option<[i16; 4]>,
Option<u16>,
Option<u32>,
Option<u32>,
Option<u16>,
Option<u32>,
Option<[u8; 4]>,
Option<u32>,
Option<u32>,
Option<u32>,
Option<u32>,
Option<(u32, u32)>,
Option<[u8; 4]>,
);
fn build_stream(
index: u32,
strh: &[u8],
strf: &[u8],
codecs: &dyn CodecResolver,
) -> Result<BuildStreamOutput> {
if strh.len() < 48 {
return Err(Error::invalid("AVI: strh too short"));
}
let mut fcc_type = [0u8; 4];
fcc_type.copy_from_slice(&strh[0..4]);
let mut fcc_handler = [0u8; 4];
fcc_handler.copy_from_slice(&strh[4..8]);
let scale_raw = u32::from_le_bytes([strh[20], strh[21], strh[22], strh[23]]);
let rate_raw = u32::from_le_bytes([strh[24], strh[25], strh[26], strh[27]]);
let scale = scale_raw.max(1);
let rate = rate_raw.max(1);
let length = u32::from_le_bytes([strh[32], strh[33], strh[34], strh[35]]);
let sample_size = u32::from_le_bytes([strh[44], strh[45], strh[46], strh[47]]);
let rate_scale: Option<(u32, u32)> = if scale_raw == 0 || rate_raw == 0 {
None
} else {
Some((scale_raw, rate_raw))
};
let fcc_type_opt: Option<[u8; 4]> = if fcc_type == [0, 0, 0, 0] {
None
} else {
Some(fcc_type)
};
let language_raw = u16::from_le_bytes([strh[14], strh[15]]);
let language: Option<u16> = if language_raw == 0 {
None
} else {
Some(language_raw)
};
let priority_raw = u16::from_le_bytes([strh[12], strh[13]]);
let priority: Option<u16> = if priority_raw == 0 {
None
} else {
Some(priority_raw)
};
let start_raw = u32::from_le_bytes([strh[28], strh[29], strh[30], strh[31]]);
let start: Option<u32> = if start_raw == 0 {
None
} else {
Some(start_raw)
};
let initial_frames_raw = u32::from_le_bytes([strh[16], strh[17], strh[18], strh[19]]);
let initial_frames: Option<u32> = if initial_frames_raw == 0 {
None
} else {
Some(initial_frames_raw)
};
let quality_raw = u32::from_le_bytes([strh[40], strh[41], strh[42], strh[43]]);
let quality: Option<u32> = if quality_raw == 0xFFFF_FFFF {
None
} else {
Some(quality_raw)
};
let rc_frame: Option<[i16; 4]> = if strh.len() >= 56 {
let left = i16::from_le_bytes([strh[48], strh[49]]);
let top = i16::from_le_bytes([strh[50], strh[51]]);
let right = i16::from_le_bytes([strh[52], strh[53]]);
let bottom = i16::from_le_bytes([strh[54], strh[55]]);
if left == 0 && top == 0 && right == 0 && bottom == 0 {
None
} else {
Some([left, top, right, bottom])
}
} else {
None
};
let flags_raw = u32::from_le_bytes([strh[8], strh[9], strh[10], strh[11]]);
let flags: Option<u32> = if flags_raw == 0 {
None
} else {
Some(flags_raw)
};
let handler: Option<[u8; 4]> = if fcc_handler == [0, 0, 0, 0] {
None
} else {
Some(fcc_handler)
};
let suggested_buffer_size_raw = u32::from_le_bytes([strh[36], strh[37], strh[38], strh[39]]);
let suggested_buffer_size: Option<u32> = if suggested_buffer_size_raw == 0 {
None
} else {
Some(suggested_buffer_size_raw)
};
let sample_size_opt: Option<u32> = if sample_size == 0 {
None
} else {
Some(sample_size)
};
let length_opt: Option<u32> = if length == 0 { None } else { Some(length) };
let mut audio_info: Option<AudioStrhInfo> = None;
let mut video_strf_info: Option<VideoStrfInfo> = None;
let mut audio_strf_info: Option<AudioStrfInfo> = None;
let (media_type, codec_id, params, suffix) = match &fcc_type {
b"vids" => {
let bmih = if !strf.is_empty() {
Some(parse_bitmap_info_header(strf)?)
} else {
None
};
let compression = bmih.as_ref().map(|b| b.compression).unwrap_or(fcc_handler);
let tag = CodecTag::fourcc(&compression);
let mut ctx = ProbeContext::new(&tag).header(strf);
if let Some(b) = &bmih {
ctx = ctx.width(b.width).height(b.height);
}
let codec_id = codecs
.resolve_tag(&ctx)
.unwrap_or_else(|| video_codec_id_fallback(&compression));
let mut p = CodecParameters::video(codec_id.clone());
p.tag = Some(CodecTag::fourcc(&compression));
if let Some(b) = &bmih {
p.width = Some(b.width);
p.height = Some(b.height);
p.extradata = b.extradata.clone();
let bitfields_masks = if b.compression == crate::stream_format::BI_BITFIELDS {
crate::stream_format::parse_bitfields_masks(&b.extradata)
} else {
None
};
video_strf_info = Some(VideoStrfInfo {
top_down: b.top_down,
bitfields_masks,
});
}
p.frame_rate = Some(Rational::new(rate as i64, scale as i64));
let suffix = if codec_id.as_str() == "rgb24" {
*b"db"
} else {
*b"dc"
};
(MediaType::Video, codec_id, p, suffix)
}
b"auds" => {
let wfx = if !strf.is_empty() {
Some(parse_waveformatex(strf)?)
} else {
None
};
let format_tag = wfx.as_ref().map(|w| w.format_tag).unwrap_or(0);
let bits = wfx.as_ref().map(|w| w.bits_per_sample).unwrap_or(0);
let wfex = if format_tag == WAVE_FORMAT_EXTENSIBLE && !strf.is_empty() {
Some(parse_waveformatextensible(strf)?)
} else {
None
};
let tag = CodecTag::wave_format(format_tag);
let mut ctx = ProbeContext::new(&tag).header(strf);
if let Some(w) = &wfx {
ctx = ctx
.bits(w.bits_per_sample)
.channels(w.channels)
.sample_rate(w.samples_per_sec);
}
let codec_id = codecs.resolve_tag(&ctx).unwrap_or_else(|| {
if let Some(wfe) = &wfex {
let depth = if wfe.valid_bits_per_sample > 0 {
wfe.valid_bits_per_sample
} else {
bits
};
if let Some(hint) = subformat_codec_hint(&wfe.subformat, depth) {
return CodecId::new(hint);
}
return CodecId::new(format!("avi:guid_{}", wfe.subformat.display()));
}
audio_codec_id_fallback(format_tag, bits)
});
let mut p = CodecParameters::audio(codec_id.clone());
p.tag = Some(CodecTag::wave_format(format_tag));
if let Some(w) = &wfx {
p.channels = Some(w.channels);
p.sample_rate = Some(w.samples_per_sec);
p.extradata = w.extradata.clone();
let depth_for_format = wfex
.as_ref()
.map(|wfe| {
if wfe.valid_bits_per_sample > 0 {
wfe.valid_bits_per_sample
} else {
w.bits_per_sample
}
})
.unwrap_or(w.bits_per_sample);
p.sample_format = sample_format_for(codec_id.as_str(), depth_for_format);
p.bit_rate = if w.avg_bytes_per_sec > 0 {
Some(w.avg_bytes_per_sec as u64 * 8)
} else {
None
};
}
audio_info = Some(AudioStrhInfo {
format_tag,
sample_size,
block_align: wfx.as_ref().map(|w| w.block_align).unwrap_or(0),
});
audio_strf_info = Some(AudioStrfInfo {
format_tag,
valid_bits_per_sample: wfex.as_ref().map(|wfe| wfe.valid_bits_per_sample),
channel_mask: wfex.as_ref().map(|wfe| wfe.channel_mask),
subformat: wfex.as_ref().map(|wfe| wfe.subformat),
});
(MediaType::Audio, codec_id, p, *b"wb")
}
_ => {
let codec_id = CodecId::new(format!(
"avi:{}",
std::str::from_utf8(&fcc_type).unwrap_or("????")
));
let mut p = CodecParameters::audio(codec_id.clone());
p.media_type = MediaType::Data;
(MediaType::Data, codec_id, p, *b"xx")
}
};
let _ = codec_id;
let time_base = match media_type {
MediaType::Video => TimeBase::new(scale as i64, rate as i64),
MediaType::Audio => {
TimeBase::new(scale as i64, rate as i64)
}
_ => TimeBase::new(scale as i64, rate as i64),
};
let duration = if length > 0 {
Some(length as i64)
} else {
None
};
let stream = StreamInfo {
index,
time_base,
duration,
start_time: Some(0),
params,
};
Ok((
stream,
suffix,
audio_info,
video_strf_info,
audio_strf_info,
rc_frame,
language,
initial_frames,
quality,
priority,
start,
handler,
suggested_buffer_size,
sample_size_opt,
length_opt,
flags,
rate_scale,
fcc_type_opt,
))
}
fn format_fourcc_or_hex(fourcc: &[u8; 4]) -> String {
let printable = fourcc.iter().all(|&b| (0x20..=0x7e).contains(&b));
if printable {
std::str::from_utf8(fourcc).unwrap().to_string()
} else {
format!(
"0x{:02x}{:02x}{:02x}{:02x}",
fourcc[0], fourcc[1], fourcc[2], fourcc[3]
)
}
}
fn video_codec_id_fallback(fourcc: &[u8; 4]) -> CodecId {
if fourcc == &[0, 0, 0, 0] {
return CodecId::new("rgb24");
}
let printable = fourcc.iter().all(|b| b.is_ascii_graphic() || *b == b' ');
if printable {
let s = std::str::from_utf8(fourcc).unwrap_or("????");
CodecId::new(format!("avi:{s}"))
} else {
CodecId::new(format!(
"avi:0x{:02X}{:02X}{:02X}{:02X}",
fourcc[0], fourcc[1], fourcc[2], fourcc[3]
))
}
}
fn audio_codec_id_fallback(format_tag: u16, bits: u16) -> CodecId {
let name = match format_tag {
0x0001 => match bits {
8 => "pcm_u8",
24 => "pcm_s24le",
32 => "pcm_s32le",
_ => "pcm_s16le",
},
0x0003 => match bits {
64 => "pcm_f64le",
_ => "pcm_f32le",
},
_ => return CodecId::new(format!("avi:tag_{format_tag:04x}")),
};
CodecId::new(name)
}
fn sample_format_for(codec: &str, bits: u16) -> Option<SampleFormat> {
match codec {
"pcm_u8" => Some(SampleFormat::U8),
"pcm_s16le" | "pcm_s16be" => Some(SampleFormat::S16),
"pcm_s24le" => Some(SampleFormat::S24),
"pcm_s32le" => Some(SampleFormat::S32),
"pcm_f32le" => Some(SampleFormat::F32),
"pcm_f64le" => Some(SampleFormat::F64),
"pcm_mulaw" | "pcm_alaw" => Some(SampleFormat::S16),
_ => match bits {
8 => Some(SampleFormat::U8),
16 => Some(SampleFormat::S16),
24 => Some(SampleFormat::S24),
32 => Some(SampleFormat::S32),
_ => None,
},
}
}
fn read_body_bounded<R: std::io::Read + ?Sized>(r: &mut R, size: u32) -> Result<Vec<u8>> {
let mut buf = vec![0u8; size as usize];
r.read_exact(&mut buf)?;
Ok(buf)
}
fn probe_file_len<R: ReadSeek + ?Sized>(r: &mut R) -> Result<u64> {
let cur = r.stream_position()?;
let end = r.seek(SeekFrom::End(0))?;
r.seek(SeekFrom::Start(cur))?;
Ok(end)
}
fn is_unexpected_eof(e: &Error) -> bool {
matches!(e, Error::Io(io) if io.kind() == std::io::ErrorKind::UnexpectedEof)
}
fn scan_idx1_for_suffix(raw: &[u8], streams: &[StreamInfo], suffix: [u8; 2], counts: &mut [u32]) {
if raw.len() < 16 || counts.len() < streams.len() {
return;
}
let n = raw.len() / 16;
for i in 0..n {
let base = i * 16;
let ckid = [raw[base], raw[base + 1], raw[base + 2], raw[base + 3]];
if ckid[2] != suffix[0] || ckid[3] != suffix[1] {
continue;
}
if let Some(stream) = parse_stream_index(&ckid) {
let s = stream as usize;
if s < counts.len() && s < streams.len() {
counts[s] = counts[s].saturating_add(1);
}
}
}
}
fn read_sideband_data_from_idx1<R: ReadSeek + ?Sized>(
r: &mut R,
raw: &[u8],
movi_start: u64,
streams: &[StreamInfo],
suffix: [u8; 2],
out: &mut [Vec<Vec<u8>>],
) {
if raw.len() < 16 || out.len() < streams.len() {
return;
}
let n = raw.len() / 16;
let movi_fourcc_pos = movi_start.saturating_sub(4);
let mut probe_raw_offset: Option<u32> = None;
let mut probe_ckid: Option<[u8; 4]> = None;
for i in 0..n {
let base = i * 16;
let mut ckid = [0u8; 4];
ckid.copy_from_slice(&raw[base..base + 4]);
if parse_stream_index(&ckid).is_none() {
continue;
}
let off =
u32::from_le_bytes([raw[base + 8], raw[base + 9], raw[base + 10], raw[base + 11]]);
if off != 0 {
probe_raw_offset = Some(off);
probe_ckid = Some(ckid);
break;
}
}
let mut movi_relative = true;
if let (Some(raw_off), Some(ckid)) = (probe_raw_offset, probe_ckid) {
let try_movi = movi_fourcc_pos.checked_add(raw_off as u64);
let movi_ok = match try_movi {
Some(p) => probe_offset_has_ckid(r, p, &ckid).unwrap_or(false),
None => false,
};
let abs_ok = probe_offset_has_ckid(r, raw_off as u64, &ckid).unwrap_or(false);
movi_relative = match (movi_ok, abs_ok) {
(true, false) => true,
(false, true) => false,
_ => true,
};
}
let base_off = if movi_relative { movi_fourcc_pos } else { 0 };
for i in 0..n {
let base = i * 16;
let ckid = [raw[base], raw[base + 1], raw[base + 2], raw[base + 3]];
if ckid[2] != suffix[0] || ckid[3] != suffix[1] {
continue;
}
let stream = match parse_stream_index(&ckid) {
Some(s) => s,
None => continue,
};
let s = stream as usize;
if s >= out.len() || s >= streams.len() {
continue;
}
let raw_off =
u32::from_le_bytes([raw[base + 8], raw[base + 9], raw[base + 10], raw[base + 11]]);
let size = u32::from_le_bytes([
raw[base + 12],
raw[base + 13],
raw[base + 14],
raw[base + 15],
]);
let chunk_off = base_off.saturating_add(raw_off as u64);
if r.seek(SeekFrom::Start(chunk_off + 8)).is_err() {
continue;
}
match read_body_bounded(r, size) {
Ok(body) => out[s].push(body),
Err(_) => continue,
}
}
}
fn build_idx_table<R: ReadSeek + ?Sized>(
r: &mut R,
raw: &[u8],
movi_start: u64,
streams: &[StreamInfo],
) -> Result<(Vec<IdxEntry>, Vec<Idx1RecEntry>)> {
if raw.len() < 16 {
return Ok((Vec::new(), Vec::new()));
}
let n = raw.len() / 16;
let mut probe_raw_offset: Option<u32> = None;
let mut probe_ckid: Option<[u8; 4]> = None;
for i in 0..n {
let base = i * 16;
let mut ckid = [0u8; 4];
ckid.copy_from_slice(&raw[base..base + 4]);
if parse_stream_index(&ckid).is_none() {
continue;
}
let off =
u32::from_le_bytes([raw[base + 8], raw[base + 9], raw[base + 10], raw[base + 11]]);
if off != 0 {
probe_raw_offset = Some(off);
probe_ckid = Some(ckid);
break;
}
}
let movi_fourcc_pos = movi_start.saturating_sub(4);
let mut movi_relative = true; if let (Some(raw_off), Some(ckid)) = (probe_raw_offset, probe_ckid) {
let try_movi = movi_fourcc_pos.checked_add(raw_off as u64);
let try_abs = Some(raw_off as u64);
let movi_ok = match try_movi {
Some(p) => probe_offset_has_ckid(r, p, &ckid).unwrap_or(false),
None => false,
};
let abs_ok = match try_abs {
Some(p) => probe_offset_has_ckid(r, p, &ckid).unwrap_or(false),
None => false,
};
movi_relative = match (movi_ok, abs_ok) {
(true, false) => true,
(false, true) => false,
_ => true,
};
}
let base_off = if movi_relative { movi_fourcc_pos } else { 0 };
let mut entries: Vec<IdxEntry> = Vec::with_capacity(n);
let mut rec_entries: Vec<Idx1RecEntry> = Vec::new();
for i in 0..n {
let base = i * 16;
let mut ckid = [0u8; 4];
ckid.copy_from_slice(&raw[base..base + 4]);
let flags =
u32::from_le_bytes([raw[base + 4], raw[base + 5], raw[base + 6], raw[base + 7]]);
let raw_off =
u32::from_le_bytes([raw[base + 8], raw[base + 9], raw[base + 10], raw[base + 11]]);
let size = u32::from_le_bytes([
raw[base + 12],
raw[base + 13],
raw[base + 14],
raw[base + 15],
]);
let stream = match parse_stream_index(&ckid) {
Some(s) => s,
None => {
if ckid == *b"rec " {
rec_entries.push(Idx1RecEntry {
flags,
offset: base_off.saturating_add(raw_off as u64),
size,
});
}
continue;
}
};
if (stream as usize) >= streams.len() {
continue;
}
let abs = base_off.saturating_add(raw_off as u64);
entries.push(IdxEntry {
stream,
flags,
offset: abs,
size,
pts: 0,
});
}
let mut per_stream_pts: Vec<i64> = vec![0; streams.len()];
for e in entries.iter_mut() {
let s = e.stream as usize;
e.pts = per_stream_pts[s];
let bump = packet_time_delta(&streams[s], e.size as usize) as i64;
per_stream_pts[s] = per_stream_pts[s].saturating_add(bump);
}
Ok((entries, rec_entries))
}
fn probe_offset_has_ckid<R: ReadSeek + ?Sized>(
r: &mut R,
offset: u64,
expected: &[u8; 4],
) -> Result<bool> {
r.seek(SeekFrom::Start(offset))?;
let mut buf = [0u8; 4];
let mut got = 0;
while got < 4 {
match r.read(&mut buf[got..]) {
Ok(0) => return Ok(false),
Ok(n) => got += n,
Err(e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
Err(_) => return Ok(false),
}
}
Ok(&buf == expected)
}
#[derive(Clone, Debug, Default)]
pub struct VideoStrfInfo {
pub top_down: bool,
pub bitfields_masks: Option<(u32, u32, u32)>,
}
pub struct AviDemuxer {
input: Box<dyn ReadSeek>,
streams: Vec<StreamInfo>,
packet_chunk_suffix: Vec<[u8; 2]>,
movi_start: u64,
movi_segments: Vec<(u64, u64)>,
current_segment: usize,
per_stream_counter: Vec<u64>,
metadata: Vec<(String, String)>,
duration_micros: i64,
idx_table: Vec<IdxEntry>,
idx1_rec_entries: Vec<Idx1RecEntry>,
#[allow(dead_code)]
super_indexes: Vec<SuperIndex>,
std_indexes: Vec<StdIndex>,
audio_cbr_block_aligns: Vec<Option<u16>>,
idx1_flags_per_stream: Vec<Vec<u32>>,
palette_change_counts: Vec<u32>,
text_chunk_counts: Vec<u32>,
avih_flags: u32,
avih_suggested_buffer_size: u32,
avih_padding_granularity: u32,
avih_initial_frames: u32,
avih_micro_sec_per_frame: u32,
avih_max_bytes_per_sec: u32,
avih_total_frames: u32,
avih_streams: u32,
avih_width: u32,
avih_height: u32,
vprps: Vec<VprpHeader>,
dmlh_total_frames: Option<u32>,
palette_change_data: Vec<Vec<Vec<u8>>>,
text_chunk_data: Vec<Vec<Vec<u8>>>,
sideband_data_loaded: bool,
video_strf: Vec<Option<VideoStrfInfo>>,
audio_strf: Vec<Option<AudioStrfInfo>>,
stream_names: Vec<Option<String>>,
stream_header_data: Vec<Option<Vec<u8>>>,
stream_frame_rects: Vec<Option<[i16; 4]>>,
stream_languages: Vec<Option<u16>>,
stream_initial_frames: Vec<Option<u32>>,
stream_qualities: Vec<Option<u32>>,
stream_priorities: Vec<Option<u16>>,
stream_starts: Vec<Option<u32>>,
stream_handlers: Vec<Option<[u8; 4]>>,
stream_suggested_buffer_sizes: Vec<Option<u32>>,
stream_sample_sizes: Vec<Option<u32>>,
stream_lengths: Vec<Option<u32>>,
stream_flags: Vec<Option<u32>>,
stream_rates: Vec<Option<(u32, u32)>>,
stream_fcc_types: Vec<Option<[u8; 4]>>,
digitization_date: Option<String>,
smpte_timecode: Option<String>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct KeyframeSeekResult {
pub target_pts: i64,
pub landed_pts: i64,
pub gop_distance: i64,
}
pub const AVIF_HASINDEX: u32 = 0x0000_0010;
pub const AVIF_MUSTUSEINDEX: u32 = 0x0000_0020;
pub const AVIF_ISINTERLEAVED: u32 = 0x0000_0100;
pub const AVIF_TRUSTCKTYPE: u32 = 0x0000_0800;
pub const AVIF_WASCAPTUREFILE: u32 = 0x0001_0000;
pub const AVIF_COPYRIGHTED: u32 = 0x0002_0000;
pub const AVISF_DISABLED: u32 = 0x0000_0001;
pub const AVISF_VIDEO_PALCHANGES: u32 = 0x0001_0000;
pub const WAVE_FORMAT_PCM: u16 = 0x0001;
pub const WAVE_FORMAT_ALAW: u16 = 0x0006;
pub const WAVE_FORMAT_MULAW: u16 = 0x0007;
pub const WAVE_FORMAT_DVI_ADPCM: u16 = 0x0011;
pub const WAVE_FORMAT_MPEG: u16 = 0x0050;
pub const WAVE_FORMAT_MPEGLAYER3: u16 = 0x0055;
pub const WAVE_FORMAT_AAC: u16 = 0x00FF;
pub const WAVE_FORMAT_AAC_ADTS: u16 = 0x1601;
pub const WAVE_FORMAT_AC3: u16 = 0x2000;
pub const WAVE_FORMAT_DTS: u16 = 0x2001;
pub const WAVE_FORMAT_WMA1: u16 = 0x0160;
pub const WAVE_FORMAT_WMA2: u16 = 0x0161;
pub const WAVE_FORMAT_WMA_PRO: u16 = 0x0162;
pub const WAVE_FORMAT_WMA_LOSSLESS: u16 = 0x0163;
pub const WAVE_FORMAT_OPUS: u16 = 0x704F;
fn classify_audio_sample_size(format_tag: u16) -> Option<bool> {
match format_tag {
WAVE_FORMAT_MPEG
| WAVE_FORMAT_MPEGLAYER3
| WAVE_FORMAT_AAC
| WAVE_FORMAT_AAC_ADTS
| WAVE_FORMAT_AC3
| WAVE_FORMAT_DTS
| WAVE_FORMAT_WMA1
| WAVE_FORMAT_WMA2
| WAVE_FORMAT_WMA_PRO
| WAVE_FORMAT_WMA_LOSSLESS
| WAVE_FORMAT_OPUS => Some(true),
WAVE_FORMAT_PCM | WAVE_FORMAT_ALAW | WAVE_FORMAT_MULAW | WAVE_FORMAT_DVI_ADPCM => {
Some(false)
}
_ => None,
}
}
fn audio_strh_violation(info: &AudioStrhInfo) -> Option<String> {
let vbr = classify_audio_sample_size(info.format_tag)?;
if vbr {
if info.sample_size != 0 {
return Some(format!(
"VBR codec requires strh.dwSampleSize == 0, got {}",
info.sample_size
));
}
} else if info.sample_size == 0 {
return Some("CBR codec requires strh.dwSampleSize > 0, got 0".to_string());
}
None
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct AvihFlags {
pub has_index: bool,
pub must_use_index: bool,
pub is_interleaved: bool,
pub trust_ck_type: bool,
pub was_capture_file: bool,
pub copyrighted: bool,
pub bits: u32,
}
impl AvihFlags {
pub fn from_bits(bits: u32) -> Self {
Self {
has_index: bits & AVIF_HASINDEX != 0,
must_use_index: bits & AVIF_MUSTUSEINDEX != 0,
is_interleaved: bits & AVIF_ISINTERLEAVED != 0,
trust_ck_type: bits & AVIF_TRUSTCKTYPE != 0,
was_capture_file: bits & AVIF_WASCAPTUREFILE != 0,
copyrighted: bits & AVIF_COPYRIGHTED != 0,
bits,
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct StrhFlags {
pub disabled: bool,
pub video_palchanges: bool,
pub bits: u32,
}
impl StrhFlags {
pub fn from_bits(bits: u32) -> Self {
Self {
disabled: bits & AVISF_DISABLED != 0,
video_palchanges: bits & AVISF_VIDEO_PALCHANGES != 0,
bits,
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct Idx1Flags {
pub is_list: bool,
pub is_keyframe: bool,
pub is_first_part: bool,
pub is_last_part: bool,
pub is_no_time: bool,
pub bits: u32,
}
impl Idx1Flags {
pub fn from_bits(bits: u32) -> Self {
Self {
is_list: bits & AVIIF_LIST != 0,
is_keyframe: bits & AVIIF_KEYFRAME != 0,
is_first_part: bits & AVIIF_FIRSTPART != 0,
is_last_part: bits & AVIIF_LASTPART != 0,
is_no_time: bits & AVIIF_NO_TIME != 0,
bits,
}
}
pub fn compressor_bits(self) -> u32 {
self.bits & AVIIF_COMPRESSOR
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct Idx1RecEntry {
pub flags: u32,
pub offset: u64,
pub size: u32,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct PaletteEntry {
pub red: u8,
pub green: u8,
pub blue: u8,
pub flags: u8,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct PaletteChange {
pub first_entry: u8,
pub num_entries: u8,
pub flags: u16,
pub entries: Vec<PaletteEntry>,
}
impl PaletteChange {
pub fn parse(body: &[u8]) -> Option<Self> {
if body.len() < 4 {
return None;
}
let first_entry = body[0];
let num_entries = body[1];
let flags = u16::from_le_bytes([body[2], body[3]]);
let tail = &body[4..];
if tail.len() % 4 != 0 {
return None;
}
let mut entries = Vec::with_capacity(tail.len() / 4);
for chunk in tail.chunks_exact(4) {
entries.push(PaletteEntry {
red: chunk[0],
green: chunk[1],
blue: chunk[2],
flags: chunk[3],
});
}
Some(Self {
first_entry,
num_entries,
flags,
entries,
})
}
pub fn to_bytes(&self) -> Vec<u8> {
let mut out = Vec::with_capacity(4 + self.entries.len() * 4);
out.push(self.first_entry);
out.push(self.num_entries);
out.extend_from_slice(&self.flags.to_le_bytes());
for e in &self.entries {
out.push(e.red);
out.push(e.green);
out.push(e.blue);
out.push(e.flags);
}
out
}
}
pub struct PaletteChangeTypedIter<'a> {
bodies: &'a [Vec<u8>],
next: usize,
}
impl<'a> Iterator for PaletteChangeTypedIter<'a> {
type Item = Result<PaletteChange>;
fn next(&mut self) -> Option<Self::Item> {
let body = self.bodies.get(self.next)?;
self.next += 1;
match PaletteChange::parse(body) {
Some(pc) => Some(Ok(pc)),
None => Some(Err(Error::invalid(format!(
"AVI: xxpc body #{} ({} bytes) failed to decode as PaletteChange",
self.next - 1,
body.len()
)))),
}
}
fn size_hint(&self) -> (usize, Option<usize>) {
let remaining = self.bodies.len().saturating_sub(self.next);
(remaining, Some(remaining))
}
}
impl<'a> ExactSizeIterator for PaletteChangeTypedIter<'a> {}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct TextChunk {
pub codepage: u16,
pub language: u16,
pub dialect: u16,
pub body: String,
}
impl TextChunk {
pub fn parse(body: &[u8]) -> Option<Self> {
if body.len() < 6 {
return None;
}
let codepage = u16::from_le_bytes([body[0], body[1]]);
let language = u16::from_le_bytes([body[2], body[3]]);
let dialect = u16::from_le_bytes([body[4], body[5]]);
let tail = &body[6..];
let text = if codepage == 0 || codepage == 65001 {
String::from_utf8_lossy(tail).into_owned()
} else {
tail.iter().map(|&b| b as char).collect()
};
Some(Self {
codepage,
language,
dialect,
body: text,
})
}
pub fn to_bytes(&self) -> Vec<u8> {
let mut out = Vec::with_capacity(6 + self.body.len());
out.extend_from_slice(&self.codepage.to_le_bytes());
out.extend_from_slice(&self.language.to_le_bytes());
out.extend_from_slice(&self.dialect.to_le_bytes());
if self.codepage == 0 || self.codepage == 65001 {
out.extend_from_slice(self.body.as_bytes());
} else {
for c in self.body.chars() {
out.push((c as u32) as u8);
}
}
out
}
}
pub struct TextChunkTypedIter<'a> {
bodies: &'a [Vec<u8>],
next: usize,
}
impl<'a> Iterator for TextChunkTypedIter<'a> {
type Item = Result<TextChunk>;
fn next(&mut self) -> Option<Self::Item> {
let body = self.bodies.get(self.next)?;
self.next += 1;
match TextChunk::parse(body) {
Some(tc) => Some(Ok(tc)),
None => Some(Err(Error::invalid(format!(
"AVI: xxtx body #{} ({} bytes) failed to decode as TextChunk",
self.next - 1,
body.len()
)))),
}
}
fn size_hint(&self) -> (usize, Option<usize>) {
let remaining = self.bodies.len().saturating_sub(self.next);
(remaining, Some(remaining))
}
}
impl<'a> ExactSizeIterator for TextChunkTypedIter<'a> {}
#[derive(Clone, Debug, Default)]
struct VprpHeader {
video_format_token: u32,
video_standard: u32,
vertical_refresh_rate: u32,
h_total_in_t: u32,
v_total_in_lines: u32,
frame_aspect_ratio: u32,
frame_width_in_pixels: u32,
frame_height_in_lines: u32,
nb_field_per_frame: u32,
field_descs: Vec<VprpFieldDesc>,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct VprpFieldDesc {
pub compressed_bm_height: u32,
pub compressed_bm_width: u32,
pub valid_bm_height: u32,
pub valid_bm_width: u32,
pub valid_bm_x_offset: u32,
pub valid_bm_y_offset: u32,
pub video_x_offset_in_t: u32,
pub video_y_valid_start_line: u32,
}
#[allow(dead_code)]
#[derive(Clone, Copy, Debug)]
struct SuperIndexEntry {
qw_offset: u64,
dw_size: u32,
dw_duration: u32,
}
#[allow(dead_code)]
#[derive(Clone, Debug, Default)]
struct SuperIndex {
w_longs_per_entry: u16,
b_index_sub_type: u8,
chunk_id: [u8; 4],
entries: Vec<SuperIndexEntry>,
}
#[derive(Clone, Copy, Debug)]
struct StdIndexEntry {
dw_offset: u32,
dw_size: u32,
is_keyframe: bool,
#[allow(dead_code)]
dw_offset_field2: u32,
}
#[derive(Clone, Debug)]
struct StdIndex {
chunk_id: [u8; 4],
qw_base_offset: u64,
#[allow(dead_code)]
b_index_sub_type: u8,
entries: Vec<StdIndexEntry>,
}
#[derive(Clone, Copy, Debug)]
struct IdxEntry {
stream: u32,
flags: u32,
offset: u64,
#[allow(dead_code)]
size: u32,
pts: i64,
}
pub const AVIIF_LIST: u32 = 0x0000_0001;
pub const AVIIF_KEYFRAME: u32 = 0x0000_0010;
pub const AVIIF_FIRSTPART: u32 = 0x0000_0020;
pub const AVIIF_LASTPART: u32 = 0x0000_0040;
pub const AVIIF_NO_TIME: u32 = 0x0000_0100;
pub const AVIIF_COMPRESSOR: u32 = 0x0FFF_0000;
impl Demuxer for AviDemuxer {
fn format_name(&self) -> &str {
"avi"
}
fn streams(&self) -> &[StreamInfo] {
&self.streams
}
fn next_packet(&mut self) -> Result<Packet> {
if self.per_stream_counter.len() != self.streams.len() {
self.per_stream_counter = vec![0u64; self.streams.len()];
}
loop {
let current_end = self
.movi_segments
.get(self.current_segment)
.map(|s| s.1)
.ok_or(Error::Eof)?;
if self.input.stream_position()? >= current_end {
self.current_segment += 1;
if let Some(&(next_start, _)) = self.movi_segments.get(self.current_segment) {
self.input.seek(SeekFrom::Start(next_start))?;
continue;
}
return Err(Error::Eof);
}
let hdr = match read_chunk_header_lenient(&mut *self.input)? {
Some(h) => h,
None => return Err(Error::Eof),
};
if hdr.id == LIST {
let _form = read_form_type(&mut *self.input)?; continue;
}
let body_end = self.input.stream_position()? + hdr.size as u64;
if body_end > current_end {
return Err(Error::Eof);
}
if hdr.id == *b"JUNK" || hdr.id == *b"junk" {
skip_chunk(&mut *self.input, &hdr)?;
continue;
}
if let Some(idx) = parse_stream_index(&hdr.id) {
if (idx as usize) < self.streams.len() {
let expected = self.packet_chunk_suffix[idx as usize];
let suffix = [hdr.id[2], hdr.id[3]];
if suffix == *b"pc" {
let s = idx as usize;
if self.palette_change_counts.len() <= s {
self.palette_change_counts.resize(s + 1, 0);
}
self.palette_change_counts[s] =
self.palette_change_counts[s].saturating_add(1);
if self.sideband_data_loaded {
skip_chunk(&mut *self.input, &hdr)?;
} else {
if self.palette_change_data.len() <= s {
self.palette_change_data.resize(s + 1, Vec::new());
}
match read_body_bounded(&mut *self.input, hdr.size) {
Ok(body) => {
skip_pad(&mut *self.input, hdr.size)?;
self.palette_change_data[s].push(body);
}
Err(_) => {
skip_chunk(&mut *self.input, &hdr)?;
}
}
}
continue;
}
if suffix == *b"tx" {
let s = idx as usize;
if self.text_chunk_counts.len() <= s {
self.text_chunk_counts.resize(s + 1, 0);
}
self.text_chunk_counts[s] = self.text_chunk_counts[s].saturating_add(1);
if self.sideband_data_loaded {
skip_chunk(&mut *self.input, &hdr)?;
} else {
if self.text_chunk_data.len() <= s {
self.text_chunk_data.resize(s + 1, Vec::new());
}
match read_body_bounded(&mut *self.input, hdr.size) {
Ok(body) => {
skip_pad(&mut *self.input, hdr.size)?;
self.text_chunk_data[s].push(body);
}
Err(_) => {
skip_chunk(&mut *self.input, &hdr)?;
}
}
}
continue;
}
let accept = suffix == expected
|| suffix == *b"dc"
|| suffix == *b"db"
|| suffix == *b"wb";
if accept {
let data = match read_body_bounded(&mut *self.input, hdr.size) {
Ok(d) => d,
Err(e) if is_unexpected_eof(&e) => {
return Err(Error::Eof);
}
Err(e) => return Err(e),
};
skip_pad(&mut *self.input, hdr.size)?;
let stream = &self.streams[idx as usize];
let counter = self.per_stream_counter[idx as usize];
let pts = counter as i64;
let mut pkt = Packet::new(idx, stream.time_base, data);
pkt.pts = Some(pts);
pkt.dts = Some(pts);
pkt.flags.keyframe = true;
let bump = packet_time_delta(stream, pkt.data.len());
self.per_stream_counter[idx as usize] = counter + bump;
return Ok(pkt);
} else {
skip_chunk(&mut *self.input, &hdr)?;
continue;
}
} else {
skip_chunk(&mut *self.input, &hdr)?;
continue;
}
}
skip_chunk(&mut *self.input, &hdr)?;
}
}
fn seek_to(&mut self, stream_index: u32, pts: i64) -> Result<i64> {
if (stream_index as usize) >= self.streams.len() {
return Err(Error::invalid(format!(
"AVI: stream index {stream_index} out of range"
)));
}
if self.idx_table.is_empty() {
if !self.std_indexes.is_empty() {
return self.seek_via_std_indexes(stream_index, pts);
}
return Err(Error::unsupported(
"AVI: seek requires idx1 or OpenDML ix## standard indexes",
));
}
let mut best: Option<&IdxEntry> = None;
for e in &self.idx_table {
if e.stream != stream_index || (e.flags & AVIIF_KEYFRAME) == 0 {
continue;
}
if e.pts <= pts {
best = match best {
Some(b) if b.pts >= e.pts => Some(b),
_ => Some(e),
};
}
}
if best.is_none() {
for e in &self.idx_table {
if e.stream == stream_index && (e.flags & AVIIF_KEYFRAME) != 0 {
best = Some(e);
break;
}
}
}
let landed = best.ok_or_else(|| {
Error::unsupported(format!(
"AVI: no keyframes in idx1 for stream {stream_index}"
))
})?;
let mut target_off = landed.offset;
if target_off < self.movi_start {
target_off = self.movi_start;
}
let seg = self
.movi_segments
.iter()
.position(|&(s, e)| target_off >= s && target_off < e)
.ok_or_else(|| Error::invalid("AVI: idx1 entry points past end of movi segments"))?;
self.current_segment = seg;
self.input.seek(SeekFrom::Start(target_off))?;
if self.per_stream_counter.len() != self.streams.len() {
self.per_stream_counter = vec![0u64; self.streams.len()];
} else {
for c in self.per_stream_counter.iter_mut() {
*c = 0;
}
}
for e in &self.idx_table {
if e.offset > target_off {
break;
}
let s = e.stream as usize;
if s < self.per_stream_counter.len() {
self.per_stream_counter[s] = e.pts.max(0) as u64;
}
}
Ok(landed.pts)
}
fn metadata(&self) -> &[(String, String)] {
&self.metadata
}
fn duration_micros(&self) -> Option<i64> {
if self.duration_micros > 0 {
Some(self.duration_micros)
} else {
None
}
}
}
impl AviDemuxer {
pub fn info_for(&self, id: [u8; 4]) -> Option<&str> {
let canonical = info_id_to_key(&id);
let avi_namespaced = if id.iter().all(|b| b.is_ascii_graphic()) {
std::str::from_utf8(&id)
.ok()
.map(|s| format!("avi:info.{s}"))
} else {
Some(format!(
"avi:info.tag_{:02x}{:02x}{:02x}{:02x}",
id[0], id[1], id[2], id[3]
))
};
for (k, v) in &self.metadata {
if let Some(canon) = canonical {
if k == canon {
return Some(v.as_str());
}
}
if let Some(ns) = avi_namespaced.as_deref() {
if k == ns {
return Some(v.as_str());
}
}
}
None
}
pub fn all_info_for(&self, fourcc: &str) -> Vec<&str> {
let bytes = fourcc.as_bytes();
if bytes.len() != 4 {
return Vec::new();
}
let id = [bytes[0], bytes[1], bytes[2], bytes[3]];
self.info_all_for(id)
}
pub fn info_all_for(&self, id: [u8; 4]) -> Vec<&str> {
let canonical = info_id_to_key(&id);
let avi_namespaced = if id.iter().all(|b| b.is_ascii_graphic()) {
std::str::from_utf8(&id)
.ok()
.map(|s| format!("avi:info.{s}"))
} else {
Some(format!(
"avi:info.tag_{:02x}{:02x}{:02x}{:02x}",
id[0], id[1], id[2], id[3]
))
};
let mut out: Vec<&str> = Vec::new();
for (k, v) in &self.metadata {
let matches_canonical = canonical.is_some_and(|c| k == c);
let matches_ns = avi_namespaced.as_deref().is_some_and(|ns| k == ns);
if matches_canonical || matches_ns {
out.push(v.as_str());
}
}
out
}
pub fn palette_change_count(&self, stream_index: u32) -> u32 {
self.palette_change_counts
.get(stream_index as usize)
.copied()
.unwrap_or(0)
}
pub fn text_chunk_count(&self, stream_index: u32) -> u32 {
self.text_chunk_counts
.get(stream_index as usize)
.copied()
.unwrap_or(0)
}
pub fn palette_change_data(&self, stream_index: u32) -> &[Vec<u8>] {
self.palette_change_data
.get(stream_index as usize)
.map(|v| v.as_slice())
.unwrap_or(&[])
}
pub fn palette_change_typed(&self, stream_index: u32) -> Vec<PaletteChange> {
self.palette_change_data
.get(stream_index as usize)
.map(|v| v.iter().filter_map(|b| PaletteChange::parse(b)).collect())
.unwrap_or_default()
}
pub fn palette_change_typed_iter(&self, stream_index: u32) -> PaletteChangeTypedIter<'_> {
let bodies = self
.palette_change_data
.get(stream_index as usize)
.map(|v| v.as_slice())
.unwrap_or(&[]);
PaletteChangeTypedIter { bodies, next: 0 }
}
pub fn avih_suggested_buffer_size(&self) -> u32 {
self.avih_suggested_buffer_size
}
pub fn avih_suggested_buffer_size_typed(&self) -> Option<u32> {
if self.avih_suggested_buffer_size == 0 {
None
} else {
Some(self.avih_suggested_buffer_size)
}
}
pub fn text_chunk_data(&self, stream_index: u32) -> &[Vec<u8>] {
self.text_chunk_data
.get(stream_index as usize)
.map(|v| v.as_slice())
.unwrap_or(&[])
}
pub fn text_chunk_typed(&self, stream_index: u32) -> Vec<TextChunk> {
self.text_chunk_data
.get(stream_index as usize)
.map(|v| v.iter().filter_map(|b| TextChunk::parse(b)).collect())
.unwrap_or_default()
}
pub fn text_chunk_typed_iter(&self, stream_index: u32) -> TextChunkTypedIter<'_> {
let bodies = self
.text_chunk_data
.get(stream_index as usize)
.map(|v| v.as_slice())
.unwrap_or(&[]);
TextChunkTypedIter { bodies, next: 0 }
}
pub fn avih_flags(&self) -> AvihFlags {
AvihFlags::from_bits(self.avih_flags)
}
pub fn padding_granularity(&self) -> u32 {
self.avih_padding_granularity
}
pub fn initial_frames(&self) -> Option<u32> {
if self.avih_initial_frames == 0 {
None
} else {
Some(self.avih_initial_frames)
}
}
pub fn micro_sec_per_frame(&self) -> Option<u32> {
if self.avih_micro_sec_per_frame == 0 {
None
} else {
Some(self.avih_micro_sec_per_frame)
}
}
pub fn max_bytes_per_sec(&self) -> Option<u32> {
if self.avih_max_bytes_per_sec == 0 {
None
} else {
Some(self.avih_max_bytes_per_sec)
}
}
pub fn avih_total_frames(&self) -> Option<u32> {
if self.avih_total_frames == 0 {
None
} else {
Some(self.avih_total_frames)
}
}
pub fn avih_declared_stream_count(&self) -> Option<u32> {
if self.avih_streams == 0 {
None
} else {
Some(self.avih_streams)
}
}
pub fn declared_vs_actual_stream_count_mismatch(&self) -> Option<(u32, u32)> {
if self.avih_streams == 0 {
return None;
}
let actual = self.streams.len() as u32;
if self.avih_streams != actual {
Some((self.avih_streams, actual))
} else {
None
}
}
pub fn avih_movie_rect(&self) -> Option<(u32, u32)> {
if self.avih_width == 0 || self.avih_height == 0 {
None
} else {
Some((self.avih_width, self.avih_height))
}
}
pub fn idx1_flags_for_packet(&self, stream_index: u32, packet_seq: usize) -> Option<u32> {
self.idx1_flags_per_stream
.get(stream_index as usize)?
.get(packet_seq)
.copied()
}
pub fn idx1_typed_flags_for_packet(
&self,
stream_index: u32,
packet_seq: usize,
) -> Option<Idx1Flags> {
self.idx1_flags_for_packet(stream_index, packet_seq)
.map(Idx1Flags::from_bits)
}
pub fn idx1_rec_list_entries(&self) -> &[Idx1RecEntry] {
&self.idx1_rec_entries
}
pub fn idx1_rec_list_count(&self) -> u32 {
self.idx1_rec_entries.len().min(u32::MAX as usize) as u32
}
pub fn field2_offset_for_packet(&self, stream_index: u32, packet_seq: usize) -> Option<u32> {
let mut seen = 0usize;
for ix in &self.std_indexes {
let stream = parse_stream_index(&ix.chunk_id)?;
if stream != stream_index {
continue;
}
if ix.b_index_sub_type != AVI_INDEX_SUB_2FIELD {
seen = seen.saturating_add(ix.entries.len());
continue;
}
let local = packet_seq.checked_sub(seen)?;
if local < ix.entries.len() {
let v = ix.entries[local].dw_offset_field2;
return if v == 0 { None } else { Some(v) };
}
seen = seen.saturating_add(ix.entries.len());
}
None
}
pub fn dmlh_total_frames(&self) -> Option<u64> {
self.dmlh_total_frames.map(|v| v as u64)
}
pub fn cbr_audio_block_alignment_violations(&self) -> Vec<BlockAlignViolation> {
let mut out = Vec::new();
let mut seen_per_stream: Vec<usize> = vec![0; self.audio_cbr_block_aligns.len()];
for ix in &self.std_indexes {
let stream = match parse_stream_index(&ix.chunk_id) {
Some(s) => s,
None => continue,
};
let block_align = match self
.audio_cbr_block_aligns
.get(stream as usize)
.copied()
.flatten()
{
Some(ba) => ba,
None => continue,
};
let base = seen_per_stream
.get(stream as usize)
.copied()
.unwrap_or_default();
for (local, entry) in ix.entries.iter().enumerate() {
if entry.dw_size % block_align as u32 != 0 {
out.push(BlockAlignViolation {
stream_index: stream,
entry_index: base + local,
dw_size: entry.dw_size,
block_align,
});
}
}
if let Some(slot) = seen_per_stream.get_mut(stream as usize) {
*slot = base.saturating_add(ix.entries.len());
}
}
out
}
pub fn super_index_segment_durations(&self, stream_index: u32) -> Vec<u32> {
match self.super_indexes.get(stream_index as usize) {
Some(sx) => sx.entries.iter().map(|e| e.dw_duration).collect(),
None => Vec::new(),
}
}
pub fn super_index_duration_violations(&self) -> Vec<SuperIndexDurationViolation> {
let mut out = Vec::new();
let Some(dmlh) = self.dmlh_total_frames else {
return out;
};
let dmlh_total = dmlh as u64;
for (i, sx) in self.super_indexes.iter().enumerate() {
if sx.entries.is_empty() {
continue;
}
let is_video = self
.streams
.get(i)
.map(|s| matches!(s.params.media_type, MediaType::Video))
.unwrap_or(false);
if !is_video {
continue;
}
let total: u64 = sx
.entries
.iter()
.fold(0u64, |acc, e| acc.saturating_add(e.dw_duration as u64));
if total != dmlh_total {
out.push(SuperIndexDurationViolation {
stream_index: i as u32,
super_index_duration_total: total,
dmlh_total_frames: dmlh_total,
});
}
}
out
}
pub fn super_index_sub_type(&self, stream_index: u32) -> Option<u8> {
let sx = self.super_indexes.get(stream_index as usize)?;
if sx.entries.is_empty() {
return None;
}
Some(sx.b_index_sub_type)
}
pub fn super_index_is_2field(&self, stream_index: u32) -> bool {
self.super_index_sub_type(stream_index) == Some(AVI_INDEX_SUB_2FIELD)
}
pub fn super_index_longs_per_entry(&self, stream_index: u32) -> Option<u16> {
let sx = self.super_indexes.get(stream_index as usize)?;
if sx.entries.is_empty() {
return None;
}
Some(sx.w_longs_per_entry)
}
pub fn super_index_chunk_id(&self, stream_index: u32) -> Option<[u8; 4]> {
let sx = self.super_indexes.get(stream_index as usize)?;
if sx.entries.is_empty() {
return None;
}
Some(sx.chunk_id)
}
pub fn vprp_field_descs(&self, stream_index: u32) -> &[VprpFieldDesc] {
match self.vprps.get(stream_index as usize) {
Some(vp) => &vp.field_descs,
None => &[],
}
}
pub fn vprp_frame_aspect_ratio(&self, stream_index: u32) -> Option<(u16, u16)> {
let vp = self.vprps.get(stream_index as usize)?;
if vp.nb_field_per_frame == 0 || vp.frame_aspect_ratio == 0 {
return None;
}
let x = (vp.frame_aspect_ratio >> 16) as u16;
let y = (vp.frame_aspect_ratio & 0xFFFF) as u16;
Some((x, y))
}
pub fn stream_top_down(&self, stream_index: u32) -> Option<bool> {
self.video_strf
.get(stream_index as usize)
.and_then(|v| v.as_ref())
.map(|vs| vs.top_down)
}
pub fn stream_bitfields_masks(&self, stream_index: u32) -> Option<(u32, u32, u32)> {
self.video_strf
.get(stream_index as usize)
.and_then(|v| v.as_ref())
.and_then(|vs| vs.bitfields_masks)
}
pub fn stream_audio_strf(&self, stream_index: u32) -> Option<AudioStrfInfo> {
self.audio_strf.get(stream_index as usize).and_then(|v| *v)
}
pub fn stream_channel_mask(&self, stream_index: u32) -> Option<u32> {
self.stream_audio_strf(stream_index)
.and_then(|asi| asi.channel_mask)
}
pub fn stream_valid_bits_per_sample(&self, stream_index: u32) -> Option<u16> {
self.stream_audio_strf(stream_index)
.and_then(|asi| asi.valid_bits_per_sample)
}
pub fn stream_subformat(&self, stream_index: u32) -> Option<Guid> {
self.stream_audio_strf(stream_index)
.and_then(|asi| asi.subformat)
}
pub fn stream_channel_mask_typed(&self, stream_index: u32) -> Option<ChannelMask> {
self.stream_channel_mask(stream_index)
.map(ChannelMask::from_raw)
}
pub fn stream_channel_layout(&self, stream_index: u32) -> Option<ChannelLayout> {
self.stream_channel_mask_typed(stream_index)
.and_then(|cm| cm.layout())
}
pub fn stream_name(&self, stream_index: u32) -> Option<&str> {
self.stream_names
.get(stream_index as usize)
.and_then(|n| n.as_deref())
}
pub fn stream_header_data(&self, stream_index: u32) -> Option<&[u8]> {
self.stream_header_data
.get(stream_index as usize)
.and_then(|h| h.as_deref())
}
pub fn stream_frame_rect(&self, stream_index: u32) -> Option<(i16, i16, i16, i16)> {
self.stream_frame_rects
.get(stream_index as usize)
.and_then(|r| r.as_ref())
.map(|&[l, t, r, b]| (l, t, r, b))
}
pub fn stream_language(&self, stream_index: u32) -> Option<u16> {
self.stream_languages
.get(stream_index as usize)
.and_then(|l| *l)
}
pub fn stream_initial_frames(&self, stream_index: u32) -> Option<u32> {
self.stream_initial_frames
.get(stream_index as usize)
.and_then(|f| *f)
}
pub fn stream_quality(&self, stream_index: u32) -> Option<u32> {
self.stream_qualities
.get(stream_index as usize)
.and_then(|q| *q)
}
pub fn stream_priority(&self, stream_index: u32) -> Option<u16> {
self.stream_priorities
.get(stream_index as usize)
.and_then(|p| *p)
}
pub fn stream_start(&self, stream_index: u32) -> Option<u32> {
self.stream_starts
.get(stream_index as usize)
.and_then(|s| *s)
}
pub fn stream_handler(&self, stream_index: u32) -> Option<[u8; 4]> {
self.stream_handlers
.get(stream_index as usize)
.and_then(|h| *h)
}
pub fn stream_suggested_buffer_size(&self, stream_index: u32) -> Option<u32> {
self.stream_suggested_buffer_sizes
.get(stream_index as usize)
.and_then(|s| *s)
}
pub fn stream_sample_size(&self, stream_index: u32) -> Option<u32> {
self.stream_sample_sizes
.get(stream_index as usize)
.and_then(|s| *s)
}
pub fn stream_length(&self, stream_index: u32) -> Option<u32> {
self.stream_lengths
.get(stream_index as usize)
.and_then(|s| *s)
}
pub fn stream_flags(&self, stream_index: u32) -> Option<u32> {
self.stream_flags
.get(stream_index as usize)
.and_then(|s| *s)
}
pub fn stream_flags_typed(&self, stream_index: u32) -> Option<StrhFlags> {
self.stream_flags(stream_index).map(StrhFlags::from_bits)
}
pub fn stream_timebase(&self, stream_index: u32) -> Option<(u32, u32)> {
self.stream_rates
.get(stream_index as usize)
.and_then(|s| *s)
}
pub fn stream_fcc_type(&self, stream_index: u32) -> Option<[u8; 4]> {
self.stream_fcc_types
.get(stream_index as usize)
.and_then(|f| *f)
}
pub fn digitization_date(&self) -> Option<&str> {
self.digitization_date.as_deref()
}
pub fn smpte_timecode(&self) -> Option<&str> {
self.smpte_timecode.as_deref()
}
pub fn seek_to_keyframe_strict(
&mut self,
stream_index: u32,
target_pts: i64,
) -> Result<KeyframeSeekResult> {
let landed_pts = <Self as Demuxer>::seek_to(self, stream_index, target_pts)?;
let gop_distance = target_pts.saturating_sub(landed_pts).max(0);
Ok(KeyframeSeekResult {
target_pts,
landed_pts,
gop_distance,
})
}
pub fn seek_to_first_video_keyframe_after(
&mut self,
stream_index: u32,
target_pts: i64,
) -> Result<KeyframeSeekResult> {
if (stream_index as usize) >= self.streams.len() {
return Err(Error::invalid(format!(
"AVI: stream index {stream_index} out of range"
)));
}
if self.idx_table.is_empty() {
return Err(Error::unsupported(
"AVI: seek_to_first_video_keyframe_after requires idx1 \
(OpenDML ix## not yet supported in this helper)",
));
}
let mut best: Option<&IdxEntry> = None;
for e in &self.idx_table {
if e.stream != stream_index {
continue;
}
if (e.flags & AVIIF_KEYFRAME) == 0 {
continue;
}
if (e.flags & AVIIF_NO_TIME) != 0 {
continue;
}
if e.pts >= target_pts {
best = match best {
Some(b) if b.pts <= e.pts => Some(b),
_ => Some(e),
};
}
}
if best.is_none() {
for e in &self.idx_table {
if e.stream != stream_index
|| (e.flags & AVIIF_KEYFRAME) == 0
|| (e.flags & AVIIF_NO_TIME) != 0
{
continue;
}
best = match best {
Some(b) if b.pts >= e.pts => Some(b),
_ => Some(e),
};
}
}
let landed = best.ok_or_else(|| {
Error::unsupported(format!(
"AVI: no non-NO_TIME keyframes in idx1 for stream {stream_index}"
))
})?;
let mut target_off = landed.offset;
if target_off < self.movi_start {
target_off = self.movi_start;
}
let seg = self
.movi_segments
.iter()
.position(|&(s, e)| target_off >= s && target_off < e)
.ok_or_else(|| Error::invalid("AVI: idx1 entry points past end of movi segments"))?;
self.current_segment = seg;
self.input.seek(SeekFrom::Start(target_off))?;
if self.per_stream_counter.len() != self.streams.len() {
self.per_stream_counter = vec![0u64; self.streams.len()];
} else {
for c in self.per_stream_counter.iter_mut() {
*c = 0;
}
}
for e in &self.idx_table {
if e.offset > target_off {
break;
}
let s = e.stream as usize;
if s < self.per_stream_counter.len() {
self.per_stream_counter[s] = e.pts.max(0) as u64;
}
}
let landed_pts = landed.pts;
let gop_distance = landed_pts.saturating_sub(target_pts).max(0);
Ok(KeyframeSeekResult {
target_pts,
landed_pts,
gop_distance,
})
}
pub fn seek_to_keyframe_strict_via_std_index(
&mut self,
stream_index: u32,
target_pts: i64,
) -> Result<KeyframeSeekResult> {
if (stream_index as usize) >= self.streams.len() {
return Err(Error::invalid(format!(
"AVI: stream index {stream_index} out of range"
)));
}
if self.std_indexes.is_empty() {
return Err(Error::unsupported(
"AVI: seek_to_keyframe_strict_via_std_index requires OpenDML ix## standard indexes",
));
}
let landed_pts = self.seek_via_std_indexes(stream_index, target_pts)?;
let gop_distance = target_pts.saturating_sub(landed_pts).max(0);
Ok(KeyframeSeekResult {
target_pts,
landed_pts,
gop_distance,
})
}
fn seek_via_std_indexes(&mut self, stream_index: u32, target_pts: i64) -> Result<i64> {
let mut per_stream_entries: Vec<(u64, i64, bool)> = Vec::new();
let mut running_pts: i64 = 0;
for ix in &self.std_indexes {
let stream = match parse_stream_index(&ix.chunk_id) {
Some(s) => s,
None => continue,
};
if stream != stream_index {
continue;
}
let s = stream as usize;
for e in &ix.entries {
let abs_off = ix.qw_base_offset.saturating_add(e.dw_offset as u64);
let header_off = abs_off.saturating_sub(8);
per_stream_entries.push((header_off, running_pts, e.is_keyframe));
let bump = packet_time_delta(&self.streams[s], e.dw_size as usize) as i64;
running_pts = running_pts.saturating_add(bump);
}
}
if per_stream_entries.is_empty() {
return Err(Error::unsupported(format!(
"AVI: no OpenDML std-index entries for stream {stream_index}"
)));
}
let mut best: Option<(u64, i64)> = None;
for &(off, pts, kf) in &per_stream_entries {
if !kf {
continue;
}
if pts <= target_pts {
best = Some(match best {
Some(b) if b.1 >= pts => b,
_ => (off, pts),
});
}
}
if best.is_none() {
for &(off, pts, kf) in &per_stream_entries {
if kf {
best = Some((off, pts));
break;
}
}
}
let (target_off, landed_pts) = best.ok_or_else(|| {
Error::unsupported(format!(
"AVI: no keyframes in std-index for stream {stream_index}"
))
})?;
let seg = self
.movi_segments
.iter()
.position(|&(s, e)| target_off >= s && target_off < e)
.ok_or_else(|| Error::invalid("AVI: ix## entry points outside of any movi segment"))?;
self.current_segment = seg;
self.input.seek(SeekFrom::Start(target_off))?;
if self.per_stream_counter.len() != self.streams.len() {
self.per_stream_counter = vec![0u64; self.streams.len()];
} else {
for c in self.per_stream_counter.iter_mut() {
*c = 0;
}
}
let mut running_pts: Vec<u64> = vec![0u64; self.streams.len()];
for ix in &self.std_indexes {
let stream = match parse_stream_index(&ix.chunk_id) {
Some(s) => s,
None => continue,
};
let s = stream as usize;
if s >= self.per_stream_counter.len() {
continue;
}
for e in &ix.entries {
let abs_off = ix.qw_base_offset.saturating_add(e.dw_offset as u64);
let header_off = abs_off.saturating_sub(8);
if header_off <= target_off {
self.per_stream_counter[s] = running_pts[s];
}
let bump = packet_time_delta(&self.streams[s], e.dw_size as usize);
running_pts[s] = running_pts[s].saturating_add(bump);
}
}
Ok(landed_pts)
}
}
fn parse_stream_index(name: &[u8; 4]) -> Option<u32> {
let h = ascii_hex(name[0])?;
let l = ascii_hex(name[1])?;
Some((h as u32) * 16 + l as u32)
}
fn ascii_hex(b: u8) -> Option<u8> {
match b {
b'0'..=b'9' => Some(b - b'0'),
b'a'..=b'f' => Some(b - b'a' + 10),
b'A'..=b'F' => Some(b - b'A' + 10),
_ => None,
}
}
fn packet_time_delta(stream: &StreamInfo, payload_len: usize) -> u64 {
match stream.params.media_type {
MediaType::Video => 1,
MediaType::Audio => {
let block_align = stream
.params
.channels
.zip(stream.params.sample_format)
.map(|(c, f)| (c as usize) * f.bytes_per_sample())
.filter(|&v| v > 0)
.unwrap_or(0);
payload_len.checked_div(block_align).unwrap_or(1) as u64
}
_ => 1,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn stream_index_parses() {
assert_eq!(parse_stream_index(b"00dc"), Some(0));
assert_eq!(parse_stream_index(b"01wb"), Some(1));
assert_eq!(parse_stream_index(b"0adb"), Some(10));
assert_eq!(parse_stream_index(b"XXXX"), None);
}
#[test]
fn parse_ix_chunk_default_subtype_8b_entries() {
let mut body = Vec::new();
body.extend_from_slice(&2u16.to_le_bytes()); body.push(0); body.push(0x01); body.extend_from_slice(&2u32.to_le_bytes()); body.extend_from_slice(b"00dc"); body.extend_from_slice(&0x1000u64.to_le_bytes()); body.extend_from_slice(&0u32.to_le_bytes()); body.extend_from_slice(&0x100u32.to_le_bytes()); body.extend_from_slice(&512u32.to_le_bytes()); body.extend_from_slice(&0x300u32.to_le_bytes());
body.extend_from_slice(&((512u32) | 0x8000_0000).to_le_bytes());
let parsed = parse_ix_chunk(&body).unwrap();
assert_eq!(&parsed.chunk_id, b"00dc");
assert_eq!(parsed.qw_base_offset, 0x1000);
assert_eq!(parsed.b_index_sub_type, 0);
assert_eq!(parsed.entries.len(), 2);
assert_eq!(parsed.entries[0].dw_offset, 0x100);
assert_eq!(parsed.entries[0].dw_size, 512);
assert!(parsed.entries[0].is_keyframe);
assert_eq!(parsed.entries[0].dw_offset_field2, 0);
assert_eq!(parsed.entries[1].dw_offset, 0x300);
assert_eq!(parsed.entries[1].dw_size, 512);
assert!(!parsed.entries[1].is_keyframe);
}
#[test]
fn parse_ix_chunk_2field_subtype_12b_entries() {
let mut body = Vec::new();
body.extend_from_slice(&3u16.to_le_bytes()); body.push(AVI_INDEX_SUB_2FIELD); body.push(0x01); body.extend_from_slice(&1u32.to_le_bytes()); body.extend_from_slice(b"00dc");
body.extend_from_slice(&0x2000u64.to_le_bytes()); body.extend_from_slice(&0u32.to_le_bytes()); body.extend_from_slice(&0x40u32.to_le_bytes()); body.extend_from_slice(&1024u32.to_le_bytes()); body.extend_from_slice(&0x80u32.to_le_bytes());
let parsed = parse_ix_chunk(&body).expect("2-field index must parse");
assert_eq!(parsed.b_index_sub_type, AVI_INDEX_SUB_2FIELD);
assert_eq!(parsed.entries.len(), 1);
assert_eq!(parsed.entries[0].dw_offset, 0x40);
assert_eq!(parsed.entries[0].dw_size, 1024);
assert_eq!(
parsed.entries[0].dw_offset_field2, 0x80,
"field-2 offset must round-trip from the 12-byte entry layout"
);
assert!(parsed.entries[0].is_keyframe);
}
fn build_indx_body(w_longs_per_entry: u16, entries: &[(u64, u32, u32)]) -> Vec<u8> {
let mut body = Vec::new();
body.extend_from_slice(&w_longs_per_entry.to_le_bytes()); body.push(0); body.push(AVI_INDEX_OF_INDEXES); 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_offset, dw_size, dw_duration) in entries {
body.extend_from_slice(&qw_offset.to_le_bytes());
body.extend_from_slice(&dw_size.to_le_bytes());
body.extend_from_slice(&dw_duration.to_le_bytes());
}
body
}
#[test]
fn parse_indx_surfaces_default_longs_per_entry() {
let body = build_indx_body(4, &[(0x1000, 0x200, 30), (0x4000, 0x200, 30)]);
let sx = parse_indx(&body).unwrap();
assert_eq!(
sx.w_longs_per_entry, 4,
"spec-default AVISUPERINDEX stride must round-trip verbatim"
);
assert_eq!(sx.entries.len(), 2);
assert_eq!(sx.entries[0].qw_offset, 0x1000);
assert_eq!(sx.entries[0].dw_duration, 30);
}
#[test]
fn parse_indx_surfaces_nondefault_longs_per_entry() {
let body = build_indx_body(8, &[(0x2000, 0x100, 15)]);
let sx = parse_indx(&body).unwrap();
assert_eq!(
sx.w_longs_per_entry, 8,
"non-default AVISUPERINDEX stride must be surfaced verbatim, not normalised to 4"
);
assert_eq!(sx.entries.len(), 1);
assert_eq!(sx.entries[0].qw_offset, 0x2000);
}
#[test]
fn parse_vprp_extracts_fixed_dwords() {
let mut body = Vec::new();
body.extend_from_slice(&3u32.to_le_bytes()); body.extend_from_slice(&2u32.to_le_bytes()); body.extend_from_slice(&60u32.to_le_bytes()); body.extend_from_slice(&780u32.to_le_bytes()); body.extend_from_slice(&525u32.to_le_bytes()); body.extend_from_slice(&((4u32 << 16) | 3u32).to_le_bytes()); body.extend_from_slice(&640u32.to_le_bytes()); body.extend_from_slice(&480u32.to_le_bytes()); body.extend_from_slice(&2u32.to_le_bytes());
let v = parse_vprp(&body).expect("vprp must parse");
assert_eq!(v.video_format_token, 3);
assert_eq!(v.video_standard, 2);
assert_eq!(v.vertical_refresh_rate, 60);
assert_eq!(v.h_total_in_t, 780);
assert_eq!(v.v_total_in_lines, 525);
assert_eq!(v.frame_aspect_ratio, (4u32 << 16) | 3);
assert_eq!(v.frame_width_in_pixels, 640);
assert_eq!(v.frame_height_in_lines, 480);
assert_eq!(v.nb_field_per_frame, 2);
assert!(
v.field_descs.is_empty(),
"no rect tail in the body → no field_descs"
);
}
#[test]
fn parse_vprp_extracts_two_field_rects() {
let mut body = Vec::new();
body.extend_from_slice(&2u32.to_le_bytes()); body.extend_from_slice(&1u32.to_le_bytes()); body.extend_from_slice(&50u32.to_le_bytes()); body.extend_from_slice(&864u32.to_le_bytes()); body.extend_from_slice(&625u32.to_le_bytes()); body.extend_from_slice(&((4u32 << 16) | 3u32).to_le_bytes()); body.extend_from_slice(&720u32.to_le_bytes()); body.extend_from_slice(&576u32.to_le_bytes()); body.extend_from_slice(&2u32.to_le_bytes()); body.extend_from_slice(&288u32.to_le_bytes()); body.extend_from_slice(&720u32.to_le_bytes()); body.extend_from_slice(&288u32.to_le_bytes()); body.extend_from_slice(&720u32.to_le_bytes()); body.extend_from_slice(&0u32.to_le_bytes()); body.extend_from_slice(&0u32.to_le_bytes()); body.extend_from_slice(&0u32.to_le_bytes()); body.extend_from_slice(&23u32.to_le_bytes()); body.extend_from_slice(&288u32.to_le_bytes());
body.extend_from_slice(&720u32.to_le_bytes());
body.extend_from_slice(&288u32.to_le_bytes());
body.extend_from_slice(&720u32.to_le_bytes());
body.extend_from_slice(&0u32.to_le_bytes());
body.extend_from_slice(&0u32.to_le_bytes());
body.extend_from_slice(&0u32.to_le_bytes());
body.extend_from_slice(&335u32.to_le_bytes());
let v = parse_vprp(&body).expect("vprp must parse");
assert_eq!(v.field_descs.len(), 2);
assert_eq!(v.field_descs[0].compressed_bm_height, 288);
assert_eq!(v.field_descs[0].compressed_bm_width, 720);
assert_eq!(v.field_descs[0].valid_bm_height, 288);
assert_eq!(v.field_descs[0].valid_bm_width, 720);
assert_eq!(v.field_descs[0].video_y_valid_start_line, 23);
assert_eq!(v.field_descs[1].video_y_valid_start_line, 335);
}
#[test]
fn parse_vprp_truncated_tail_clamps_field_descs() {
let mut body = Vec::new();
body.extend_from_slice(&0u32.to_le_bytes()); body.extend_from_slice(&0u32.to_le_bytes()); body.extend_from_slice(&50u32.to_le_bytes()); body.extend_from_slice(&864u32.to_le_bytes());
body.extend_from_slice(&625u32.to_le_bytes());
body.extend_from_slice(&((4u32 << 16) | 3u32).to_le_bytes());
body.extend_from_slice(&720u32.to_le_bytes());
body.extend_from_slice(&576u32.to_le_bytes());
body.extend_from_slice(&2u32.to_le_bytes()); body.extend_from_slice(&288u32.to_le_bytes());
body.extend_from_slice(&720u32.to_le_bytes());
body.extend_from_slice(&288u32.to_le_bytes());
body.extend_from_slice(&720u32.to_le_bytes());
body.extend_from_slice(&0u32.to_le_bytes());
body.extend_from_slice(&0u32.to_le_bytes());
body.extend_from_slice(&0u32.to_le_bytes());
body.extend_from_slice(&23u32.to_le_bytes());
let v = parse_vprp(&body).expect("vprp must parse");
assert_eq!(
v.field_descs.len(),
1,
"truncated tail → only the descs that fit"
);
}
#[test]
fn parse_vprp_short_returns_none() {
let body = vec![0u8; 16];
assert!(parse_vprp(&body).is_none());
}
}