use std::io::{Seek, SeekFrom};
use oxideav_container::{Demuxer, ReadSeek};
use oxideav_core::{
CodecId,
CodecParameters,
CodecResolver,
Error,
MediaType,
Packet,
Rational,
Result,
SampleFormat,
StreamInfo,
TimeBase,
};
use crate::codec_map::{audio_codec_id_full, video_codec_id};
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};
pub fn open(mut input: Box<dyn ReadSeek>, _codecs: &dyn CodecResolver) -> Result<Box<dyn Demuxer>> {
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 mut streams: Vec<StreamInfo> = Vec::new();
let mut packet_chunk_suffix: Vec<[u8; 2]> = Vec::new();
let mut movi_start: Option<u64> = None;
let mut movi_end: Option<u64> = None;
let mut avih: Option<AviMainHeader> = None;
let mut metadata: Vec<(String, String)> = Vec::new();
let mut idx1_raw: Option<Vec<u8>> = None;
while let Some(hdr) = read_chunk_header(&mut *input)? {
if hdr.id == LIST {
let list_type = read_form_type(&mut *input)?;
let body_len = hdr.size.saturating_sub(4);
let body_start = input.stream_position()?;
let body_end = body_start + body_len as u64;
match &list_type {
b"hdrl" => {
let (main, stream_infos, suffixes) = parse_hdrl(&mut *input, body_end)?;
avih = Some(main);
streams = stream_infos;
packet_chunk_suffix = suffixes;
}
b"movi" => {
movi_start = Some(body_start);
movi_end = Some(body_end);
}
b"INFO" => {
let mut buf = vec![0u8; body_len as usize];
input.read_exact(&mut buf)?;
parse_info_list(&buf, &mut metadata);
}
_ => {}
}
input.seek(SeekFrom::Start(body_end))?;
skip_pad(&mut *input, hdr.size)?;
} else if &hdr.id == b"idx1" {
let mut buf = vec![0u8; hdr.size as usize];
input.read_exact(&mut buf)?;
skip_pad(&mut *input, hdr.size)?;
idx1_raw = Some(buf);
} else {
skip_chunk(&mut *input, &hdr)?;
}
}
let movi_start = movi_start.ok_or_else(|| Error::invalid("AVI: missing movi list"))?;
let movi_end = movi_end.ok_or_else(|| Error::invalid("AVI: missing movi list"))?;
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,
};
let idx_table = if let Some(raw) = idx1_raw {
build_idx_table(&mut *input, &raw, movi_start, &streams)?
} else {
Vec::new()
};
input.seek(SeekFrom::Start(movi_start))?;
Ok(Box::new(AviDemuxer {
input,
streams,
packet_chunk_suffix,
movi_start,
movi_end,
per_stream_counter: Vec::new(),
metadata,
duration_micros,
idx_table,
}))
}
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();
let key = info_id_to_key(&id);
if !value.is_empty() {
if let Some(k) = key {
out.push((k.to_string(), 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,
}
}
#[allow(dead_code)]
#[derive(Clone, Copy, Debug, Default)]
struct AviMainHeader {
micro_sec_per_frame: u32,
#[allow(dead_code)]
max_bytes_per_sec: u32,
#[allow(dead_code)]
flags: u32,
total_frames: u32,
#[allow(dead_code)]
initial_frames: u32,
streams: u32,
#[allow(dead_code)]
suggested_buffer_size: u32,
width: u32,
height: 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]]),
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]]),
})
}
fn parse_hdrl<R: ReadSeek + ?Sized>(
r: &mut R,
end_pos: u64,
) -> Result<(AviMainHeader, Vec<StreamInfo>, Vec<[u8; 2]>)> {
let mut main = AviMainHeader::default();
let mut streams: Vec<StreamInfo> = Vec::new();
let mut suffixes: Vec<[u8; 2]> = Vec::new();
while r.stream_position()? < end_pos {
let hdr = match read_chunk_header(r)? {
Some(h) => h,
None => break,
};
match &hdr.id {
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) = parse_strl(r, body_end, streams.len() as u32)?;
if let Some(si) = si {
streams.push(si);
suffixes.push(suf.unwrap_or(*b"xx"));
}
}
r.seek(SeekFrom::Start(body_end))?;
skip_pad(r, hdr.size)?;
}
_ => {
skip_chunk(r, &hdr)?;
}
}
}
Ok((main, streams, suffixes))
}
fn parse_strl<R: ReadSeek + ?Sized>(
r: &mut R,
end_pos: u64,
index: u32,
) -> Result<(Option<StreamInfo>, Option<[u8; 2]>)> {
let mut strh_buf: Option<Vec<u8>> = None;
let mut strf_buf: 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)?;
}
_ => {
skip_chunk(r, &hdr)?;
}
}
}
let strh = match strh_buf {
Some(b) => b,
None => return Ok((None, None)),
};
let strf = strf_buf.unwrap_or_default();
let parsed = build_stream(index, &strh, &strf)?;
Ok((Some(parsed.0), Some(parsed.1)))
}
fn build_stream(index: u32, strh: &[u8], strf: &[u8]) -> Result<(StreamInfo, [u8; 2])> {
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 = u32::from_le_bytes([strh[20], strh[21], strh[22], strh[23]]).max(1);
let rate = u32::from_le_bytes([strh[24], strh[25], strh[26], strh[27]]).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 (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 codec_id = video_codec_id(&compression);
let mut p = CodecParameters::video(codec_id.clone());
if let Some(b) = &bmih {
p.width = Some(b.width);
p.height = Some(b.height);
p.extradata = b.extradata.clone();
}
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 codec_id = audio_codec_id_full(format_tag, bits);
let mut p = CodecParameters::audio(codec_id.clone());
if let Some(w) = &wfx {
p.channels = Some(w.channels);
p.sample_rate = Some(w.samples_per_sec);
p.extradata = w.extradata.clone();
p.sample_format = sample_format_for(codec_id.as_str(), w.bits_per_sample);
p.bit_rate = if w.avg_bytes_per_sec > 0 {
Some(w.avg_bytes_per_sec as u64 * 8)
} else {
None
};
}
(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,
};
let _ = sample_size;
Ok((stream, suffix))
}
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 build_idx_table<R: ReadSeek + ?Sized>(
r: &mut R,
raw: &[u8],
movi_start: u64,
streams: &[StreamInfo],
) -> Result<Vec<IdxEntry>> {
if raw.len() < 16 {
return Ok(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 off =
u32::from_le_bytes([raw[base + 8], raw[base + 9], raw[base + 10], raw[base + 11]]);
if off != 0 {
let mut ckid = [0u8; 4];
ckid.copy_from_slice(&raw[base..base + 4]);
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);
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 => 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)
}
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)
}
struct AviDemuxer {
input: Box<dyn ReadSeek>,
streams: Vec<StreamInfo>,
packet_chunk_suffix: Vec<[u8; 2]>,
movi_start: u64,
movi_end: u64,
per_stream_counter: Vec<u64>,
metadata: Vec<(String, String)>,
duration_micros: i64,
idx_table: Vec<IdxEntry>,
}
#[derive(Clone, Copy, Debug)]
struct IdxEntry {
stream: u32,
flags: u32,
offset: u64,
#[allow(dead_code)]
size: u32,
pts: i64,
}
const AVIIF_KEYFRAME: u32 = 0x0000_0010;
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 pos = self.input.stream_position()?;
if pos >= self.movi_end {
return Err(Error::Eof);
}
let hdr = match read_chunk_header(&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 > self.movi_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]];
let accept = suffix == expected
|| suffix == *b"dc"
|| suffix == *b"db"
|| suffix == *b"wb";
if accept {
let data = read_body_bounded(&mut *self.input, hdr.size)?;
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() {
return Err(Error::unsupported(
"AVI: seek requires idx1; OpenDML indx/ix## not implemented",
));
}
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;
}
if target_off >= self.movi_end {
return Err(Error::invalid(
"AVI: idx1 entry points past end of movi list",
));
}
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
}
}
}
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);
}
}