use std::io::{Seek, SeekFrom, Write};
use oxideav_core::{Error, Packet, Result, StreamInfo};
use oxideav_core::{Muxer, WriteSeek};
use crate::codec_map::{build_strf, StrfEntry};
use crate::riff::{begin_list, finish_chunk, write_chunk, AVI_FORM, LIST, RIFF};
#[derive(Clone, Copy, Debug)]
struct IndexEntry {
ckid: [u8; 4],
flags: u32,
offset: u32,
size: u32,
}
struct TrackState {
stream: StreamInfo,
entry: StrfEntry,
packet_fourcc: [u8; 4],
packet_count: u32,
sample_count: u64,
max_chunk_size: u32,
total_bytes: u64,
}
pub fn open(output: Box<dyn WriteSeek>, streams: &[StreamInfo]) -> Result<Box<dyn Muxer>> {
if streams.is_empty() {
return Err(Error::invalid("avi muxer: need at least one stream"));
}
if streams.len() > 99 {
return Err(Error::unsupported(
"avi muxer: > 99 streams not supported in legacy index",
));
}
let mut tracks = Vec::with_capacity(streams.len());
for (i, s) in streams.iter().enumerate() {
let entry = build_strf(&s.params)?;
let packet_fourcc = packet_fourcc_for(i as u32, entry.chunk_suffix);
tracks.push(TrackState {
stream: s.clone(),
entry,
packet_fourcc,
packet_count: 0,
sample_count: 0,
max_chunk_size: 0,
total_bytes: 0,
});
}
Ok(Box::new(AviMuxer {
output,
tracks,
riff_size_off: 0,
movi_size_off: 0,
movi_start_off: 0,
index: Vec::new(),
header_written: false,
trailer_written: false,
}))
}
fn packet_fourcc_for(index: u32, suffix: [u8; 2]) -> [u8; 4] {
let tens = (index / 10) as u8 + b'0';
let ones = (index % 10) as u8 + b'0';
[tens, ones, suffix[0], suffix[1]]
}
struct AviMuxer {
output: Box<dyn WriteSeek>,
tracks: Vec<TrackState>,
riff_size_off: u64,
movi_size_off: u64,
movi_start_off: u64,
index: Vec<IndexEntry>,
header_written: bool,
trailer_written: bool,
}
impl Muxer for AviMuxer {
fn format_name(&self) -> &str {
"avi"
}
fn write_header(&mut self) -> Result<()> {
if self.header_written {
return Err(Error::other("avi muxer: write_header called twice"));
}
self.riff_size_off = begin_list(self.output.as_mut(), &RIFF, &AVI_FORM)?;
let hdrl_size_off = begin_list(self.output.as_mut(), &LIST, b"hdrl")?;
let avih = build_avih(&self.tracks);
write_chunk(self.output.as_mut(), b"avih", &avih)?;
for (i, t) in self.tracks.iter().enumerate() {
write_strl(self.output.as_mut(), i as u32, t)?;
}
finish_chunk(self.output.as_mut(), hdrl_size_off)?;
self.movi_size_off = begin_list(self.output.as_mut(), &LIST, b"movi")?;
self.movi_start_off = self.movi_size_off + 4; self.header_written = true;
Ok(())
}
fn write_packet(&mut self, packet: &Packet) -> Result<()> {
if !self.header_written {
return Err(Error::other("avi muxer: write_header not called"));
}
let idx = packet.stream_index as usize;
if idx >= self.tracks.len() {
return Err(Error::invalid(format!(
"avi muxer: unknown stream index {idx}"
)));
}
if packet.data.len() > u32::MAX as usize {
return Err(Error::invalid("avi muxer: packet larger than 4 GiB"));
}
let fourcc = self.tracks[idx].packet_fourcc;
let chunk_off = self.output.stream_position()?;
let rel_off = chunk_off
.checked_sub(self.movi_start_off)
.ok_or_else(|| Error::other("avi muxer: movi offset underflow"))?;
if rel_off > u32::MAX as u64 {
return Err(Error::unsupported(
"avi muxer: movi > 4 GiB, use OpenDML (not supported)",
));
}
let size = packet.data.len() as u32;
let flags = if packet.flags.keyframe {
0x10 } else {
0
};
write_chunk(self.output.as_mut(), &fourcc, &packet.data)?;
let t = &mut self.tracks[idx];
t.packet_count += 1;
if size > t.max_chunk_size {
t.max_chunk_size = size;
}
t.total_bytes += size as u64;
t.sample_count += sample_count_of_packet(&t.stream, &t.entry, size);
self.index.push(IndexEntry {
ckid: fourcc,
flags,
offset: rel_off as u32,
size,
});
let cur = self.output.stream_position()?;
if cur > (2 * 1024 * 1024 * 1024) - 1024 {
return Err(Error::unsupported(
"avi muxer: file would exceed 2 GiB (OpenDML not supported)",
));
}
Ok(())
}
fn write_trailer(&mut self) -> Result<()> {
if self.trailer_written {
return Ok(());
}
if !self.header_written {
return Err(Error::other("avi muxer: write_trailer before write_header"));
}
finish_chunk(self.output.as_mut(), self.movi_size_off)?;
let mut idx_body = Vec::with_capacity(self.index.len() * 16);
for e in &self.index {
idx_body.extend_from_slice(&e.ckid);
idx_body.extend_from_slice(&e.flags.to_le_bytes());
idx_body.extend_from_slice(&e.offset.to_le_bytes());
idx_body.extend_from_slice(&e.size.to_le_bytes());
}
write_chunk(self.output.as_mut(), b"idx1", &idx_body)?;
finish_chunk(self.output.as_mut(), self.riff_size_off)?;
self.patch_post_counts()?;
self.output.flush()?;
self.trailer_written = true;
Ok(())
}
}
impl AviMuxer {
fn patch_post_counts(&mut self) -> Result<()> {
let total_video_frames = self
.tracks
.iter()
.find(|t| &t.entry.strh_type == b"vids")
.map(|t| t.packet_count)
.unwrap_or_else(|| self.tracks.first().map(|t| t.packet_count).unwrap_or(0));
let end_pos = self.output.stream_position()?;
self.output.seek(SeekFrom::Start(44))?;
self.output.write_all(&total_video_frames.to_le_bytes())?;
let mut strl_off: u64 = 88;
for t in &self.tracks {
let strh_body_off = strl_off + 20;
let length = if &t.entry.strh_type == b"auds" {
t.sample_count as u32
} else {
t.packet_count
};
self.output.seek(SeekFrom::Start(strh_body_off + 32))?;
self.output.write_all(&length.to_le_bytes())?;
self.output.seek(SeekFrom::Start(strh_body_off + 36))?;
self.output.write_all(&t.max_chunk_size.to_le_bytes())?;
let strf_padded = t.entry.strf.len() + (t.entry.strf.len() & 1);
let strl_body = 4 + 64 + 8 + strf_padded;
strl_off += 8 + strl_body as u64;
}
self.output.seek(SeekFrom::Start(end_pos))?;
Ok(())
}
}
fn build_avih(tracks: &[TrackState]) -> Vec<u8> {
let (video_micro_per_frame, width, height) = tracks
.iter()
.find(|t| &t.entry.strh_type == b"vids")
.map(|t| {
let scale = t.entry.scale.max(1) as u64;
let rate = t.entry.rate.max(1) as u64;
let upf = (1_000_000u64 * scale / rate) as u32;
let w = t.stream.params.width.unwrap_or(0);
let h = t.stream.params.height.unwrap_or(0);
(upf, w, h)
})
.unwrap_or((0, 0, 0));
let flags: u32 = 0x00000810; let total_frames: u32 = 0; let streams = tracks.len() as u32;
let mut body = Vec::with_capacity(56);
body.extend_from_slice(&video_micro_per_frame.to_le_bytes());
body.extend_from_slice(&0u32.to_le_bytes()); body.extend_from_slice(&0u32.to_le_bytes()); body.extend_from_slice(&flags.to_le_bytes());
body.extend_from_slice(&total_frames.to_le_bytes());
body.extend_from_slice(&0u32.to_le_bytes()); body.extend_from_slice(&streams.to_le_bytes());
body.extend_from_slice(&0u32.to_le_bytes()); body.extend_from_slice(&width.to_le_bytes());
body.extend_from_slice(&height.to_le_bytes());
body.extend_from_slice(&[0u8; 16]); body
}
fn write_strl<W: Write + Seek + ?Sized>(w: &mut W, _index: u32, t: &TrackState) -> Result<()> {
let strl_off = begin_list(w, &LIST, b"strl")?;
let mut strh = Vec::with_capacity(56);
strh.extend_from_slice(&t.entry.strh_type); strh.extend_from_slice(&t.entry.handler_fourcc); strh.extend_from_slice(&0u32.to_le_bytes()); strh.extend_from_slice(&0u16.to_le_bytes()); strh.extend_from_slice(&0u16.to_le_bytes()); strh.extend_from_slice(&0u32.to_le_bytes()); strh.extend_from_slice(&t.entry.scale.to_le_bytes());
strh.extend_from_slice(&t.entry.rate.to_le_bytes());
strh.extend_from_slice(&0u32.to_le_bytes()); strh.extend_from_slice(&0u32.to_le_bytes()); strh.extend_from_slice(&0u32.to_le_bytes()); strh.extend_from_slice(&0xFFFF_FFFFu32.to_le_bytes()); strh.extend_from_slice(&t.entry.sample_size.to_le_bytes());
if &t.entry.strh_type == b"vids" {
let w_val = t.stream.params.width.unwrap_or(0) as i16;
let h_val = t.stream.params.height.unwrap_or(0) as i16;
strh.extend_from_slice(&0i16.to_le_bytes());
strh.extend_from_slice(&0i16.to_le_bytes());
strh.extend_from_slice(&w_val.to_le_bytes());
strh.extend_from_slice(&h_val.to_le_bytes());
} else {
strh.extend_from_slice(&[0u8; 8]);
}
write_chunk(w, b"strh", &strh)?;
write_chunk(w, b"strf", &t.entry.strf)?;
finish_chunk(w, strl_off)?;
Ok(())
}
fn sample_count_of_packet(stream: &StreamInfo, entry: &StrfEntry, size: u32) -> u64 {
if &entry.strh_type == b"auds" && entry.sample_size > 0 {
(size as u64) / (entry.sample_size as u64)
} else {
let _ = stream;
1
}
}
#[cfg(test)]
mod tests {
use super::*;
use oxideav_core::{CodecId, CodecParameters};
#[test]
fn packet_fourcc_layout() {
assert_eq!(packet_fourcc_for(0, *b"dc"), *b"00dc");
assert_eq!(packet_fourcc_for(1, *b"wb"), *b"01wb");
assert_eq!(packet_fourcc_for(12, *b"db"), *b"12db");
}
#[test]
fn unsupported_codec_errors_at_open() {
use oxideav_core::WriteSeek;
use std::io::Cursor;
let mut params = CodecParameters::audio(CodecId::new("opus"));
params.channels = Some(2);
params.sample_rate = Some(48_000);
let stream = StreamInfo {
index: 0,
time_base: oxideav_core::TimeBase::new(1, 48_000),
duration: None,
start_time: Some(0),
params,
};
let cursor: Box<dyn WriteSeek> = Box::new(Cursor::new(Vec::new()));
match open(cursor, &[stream]) {
Err(Error::Unsupported(_)) => {}
Err(other) => panic!("expected Unsupported, got {other:?}"),
Ok(_) => panic!("expected Unsupported"),
}
}
}