use std::io::{Seek, SeekFrom, Write};
use oxideav_core::{Error, Packet, Result, StreamInfo};
use oxideav_core::{Muxer, WriteSeek};
use crate::packaging::{build_strf, StrfEntry};
use crate::riff::{
begin_list, finish_chunk, write_chunk, write_chunk_header, AVI_FORM, LIST, RIFF,
};
pub const AVIIF_KEYFRAME: u32 = 0x0000_0010;
pub const AVIIF_FIRSTPART: u32 = 0x0000_0020;
pub const AVIIF_LASTPART: u32 = 0x0000_0040;
#[derive(Clone, Copy, Debug)]
pub enum RiffSegmentLimit {
OneGiB,
Bytes(u64),
}
impl RiffSegmentLimit {
pub fn bytes(self) -> u64 {
match self {
RiffSegmentLimit::OneGiB => 1024 * 1024 * 1024,
RiffSegmentLimit::Bytes(n) => n.max(4096),
}
}
}
#[derive(Clone, Copy, Debug, Default)]
pub enum AviKind {
#[default]
Avi10,
OpenDml(RiffSegmentLimit),
}
#[derive(Clone, Debug, Default)]
pub struct AviMuxOptions {
pub rec_cluster_packets: Option<u32>,
pub rec_cluster_bytes: Option<u32>,
pub vprp_overrides: Vec<(u32, VprpConfig)>,
pub field2_streams: Vec<u32>,
pub super_index_capacity: Option<usize>,
pub info_entries: Vec<([u8; 4], String)>,
pub mid_movi_index_streams: Vec<(u32, u32)>,
pub info_top_level: bool,
pub avih_flags_override: Option<u32>,
pub suggested_buffer_size_override: Option<u32>,
pub max_bytes_per_sec_override: Option<u32>,
pub synthesise_idx1_from_ix: bool,
pub per_stream_max_bytes_per_sec: Vec<(u32, u32)>,
pub strict_per_stream_budget: bool,
pub top_down_video_streams: Vec<u32>,
pub extensible_audio_streams: Vec<(u32, u32, u16, crate::stream_format::Guid)>,
pub stream_names: Vec<(u32, String)>,
pub stream_header_data: Vec<(u32, Vec<u8>)>,
pub stream_frame_rects: Vec<(u32, [i16; 4])>,
pub padding_granularity: Option<u32>,
pub digitization_date: Option<String>,
pub smpte_timecode: Option<String>,
}
#[derive(Clone, Debug, Default)]
pub struct VprpConfig {
pub video_format_token: u32,
pub video_standard: u32,
pub vertical_refresh_rate: u32,
pub frame_aspect_ratio: u32,
pub nb_field_per_frame: u32,
pub field_descs: Vec<VprpFieldDescOverride>,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct VprpFieldDescOverride {
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,
}
pub const VIDEO_FORMAT_UNKNOWN: u32 = 0;
pub const VIDEO_FORMAT_PAL_SQUARE: u32 = 1;
pub const VIDEO_FORMAT_PAL_CCIR_601: u32 = 2;
pub const VIDEO_FORMAT_NTSC_SQUARE: u32 = 3;
pub const VIDEO_FORMAT_NTSC_CCIR_601: u32 = 4;
pub const VIDEO_STANDARD_UNKNOWN: u32 = 0;
pub const VIDEO_STANDARD_PAL: u32 = 1;
pub const VIDEO_STANDARD_NTSC: u32 = 2;
pub const VIDEO_STANDARD_SECAM: u32 = 3;
impl VprpConfig {
pub fn ntsc() -> Self {
Self {
video_format_token: VIDEO_FORMAT_NTSC_CCIR_601,
video_standard: VIDEO_STANDARD_NTSC,
vertical_refresh_rate: 60,
frame_aspect_ratio: (4u32 << 16) | 3,
nb_field_per_frame: 2,
field_descs: Vec::new(),
}
}
pub fn pal() -> Self {
Self {
video_format_token: VIDEO_FORMAT_PAL_CCIR_601,
video_standard: VIDEO_STANDARD_PAL,
vertical_refresh_rate: 50,
frame_aspect_ratio: (4u32 << 16) | 3,
nb_field_per_frame: 2,
field_descs: Vec::new(),
}
}
pub fn secam() -> Self {
Self {
video_format_token: VIDEO_FORMAT_UNKNOWN,
video_standard: VIDEO_STANDARD_SECAM,
vertical_refresh_rate: 50,
frame_aspect_ratio: (4u32 << 16) | 3,
nb_field_per_frame: 2,
field_descs: Vec::new(),
}
}
pub fn with_nb_field_per_frame(mut self, n: u32) -> Self {
self.nb_field_per_frame = n;
self
}
pub fn with_frame_aspect_ratio(mut self, packed: u32) -> Self {
self.frame_aspect_ratio = packed;
self
}
pub fn with_aspect(mut self, x: u32, y: u32) -> Self {
self.frame_aspect_ratio = ((x & 0xFFFF) << 16) | (y & 0xFFFF);
self
}
pub fn with_field_descs(mut self, descs: Vec<VprpFieldDescOverride>) -> Self {
self.field_descs = descs;
self
}
}
impl AviMuxOptions {
pub fn new() -> Self {
Self::default()
}
pub fn with_rec_cluster_packets(mut self, n: u32) -> Self {
self.rec_cluster_packets = if n >= 2 { Some(n) } else { None };
self
}
pub fn with_rec_cluster_bytes(mut self, n: u32) -> Self {
self.rec_cluster_bytes = if n >= 256 { Some(n) } else { None };
self
}
pub fn with_vprp(mut self, stream_index: u32, config: VprpConfig) -> Self {
self.vprp_overrides.retain(|(i, _)| *i != stream_index);
self.vprp_overrides.push((stream_index, config));
self
}
pub fn with_field2_stream(mut self, stream_index: u32) -> Self {
if !self.field2_streams.contains(&stream_index) {
self.field2_streams.push(stream_index);
}
self
}
pub fn with_super_index_capacity(mut self, n: usize) -> Self {
self.super_index_capacity = if n >= OPENDML_SUPER_INDEX_MIN_CAPACITY {
Some(n)
} else {
None
};
self
}
pub fn with_info(mut self, id: [u8; 4], value: impl Into<String>) -> Self {
let v = value.into();
if !v.is_empty() {
self.info_entries.push((id, v));
}
self
}
pub fn with_top_level_info(mut self, on: bool) -> Self {
self.info_top_level = on;
self
}
pub fn with_avih_flags(mut self, bits: u32) -> Self {
self.avih_flags_override = Some(bits);
self
}
pub fn with_avih_flag_bit(mut self, bit: u32) -> Self {
let base = self.avih_flags_override.unwrap_or(DEFAULT_AVIH_FLAGS);
self.avih_flags_override = Some(base | bit);
self
}
pub fn with_has_index(mut self, on: bool) -> Self {
let base = self.avih_flags_override.unwrap_or(DEFAULT_AVIH_FLAGS);
self.avih_flags_override = Some(if on {
base | crate::demuxer::AVIF_HASINDEX
} else {
base & !crate::demuxer::AVIF_HASINDEX
});
self
}
pub fn with_must_use_index(mut self, on: bool) -> Self {
let base = self.avih_flags_override.unwrap_or(DEFAULT_AVIH_FLAGS);
self.avih_flags_override = Some(if on {
base | crate::demuxer::AVIF_MUSTUSEINDEX
} else {
base & !crate::demuxer::AVIF_MUSTUSEINDEX
});
self
}
pub fn with_is_interleaved(mut self, on: bool) -> Self {
let base = self.avih_flags_override.unwrap_or(DEFAULT_AVIH_FLAGS);
self.avih_flags_override = Some(if on {
base | crate::demuxer::AVIF_ISINTERLEAVED
} else {
base & !crate::demuxer::AVIF_ISINTERLEAVED
});
self
}
pub fn with_trust_ck_type(mut self, on: bool) -> Self {
let base = self.avih_flags_override.unwrap_or(DEFAULT_AVIH_FLAGS);
self.avih_flags_override = Some(if on {
base | crate::demuxer::AVIF_TRUSTCKTYPE
} else {
base & !crate::demuxer::AVIF_TRUSTCKTYPE
});
self
}
pub fn with_was_capture_file(mut self, on: bool) -> Self {
let base = self.avih_flags_override.unwrap_or(DEFAULT_AVIH_FLAGS);
self.avih_flags_override = Some(if on {
base | crate::demuxer::AVIF_WASCAPTUREFILE
} else {
base & !crate::demuxer::AVIF_WASCAPTUREFILE
});
self
}
pub fn with_copyrighted(mut self, on: bool) -> Self {
let base = self.avih_flags_override.unwrap_or(DEFAULT_AVIH_FLAGS);
self.avih_flags_override = Some(if on {
base | crate::demuxer::AVIF_COPYRIGHTED
} else {
base & !crate::demuxer::AVIF_COPYRIGHTED
});
self
}
pub fn with_suggested_buffer_size(mut self, n: u32) -> Self {
self.suggested_buffer_size_override = Some(n);
self
}
pub fn with_max_bytes_per_sec(mut self, n: u32) -> Self {
self.max_bytes_per_sec_override = Some(n);
self
}
pub fn with_per_stream_max_bytes_per_sec(
mut self,
stream_index: u32,
bytes_per_sec: u32,
) -> Self {
self.per_stream_max_bytes_per_sec
.retain(|(idx, _)| *idx != stream_index);
if bytes_per_sec > 0 {
self.per_stream_max_bytes_per_sec
.push((stream_index, bytes_per_sec));
}
self
}
pub fn with_strict_per_stream_budget(mut self, on: bool) -> Self {
self.strict_per_stream_budget = on;
self
}
pub fn synthesise_idx1_from_ix(mut self, on: bool) -> Self {
self.synthesise_idx1_from_ix = on;
self
}
pub fn with_mid_movi_index(mut self, stream_index: u32, packets_per_flush: u32) -> Self {
self.mid_movi_index_streams
.retain(|(i, _)| *i != stream_index);
if packets_per_flush > 0 {
self.mid_movi_index_streams
.push((stream_index, packets_per_flush));
}
self
}
pub fn with_top_down_video(mut self, stream_index: u32) -> Self {
if !self.top_down_video_streams.contains(&stream_index) {
self.top_down_video_streams.push(stream_index);
}
self
}
pub fn with_extensible_audio(
mut self,
stream_index: u32,
channel_mask: u32,
valid_bps: u16,
subformat: crate::stream_format::Guid,
) -> Self {
self.extensible_audio_streams
.retain(|(idx, _, _, _)| *idx != stream_index);
self.extensible_audio_streams
.push((stream_index, channel_mask, valid_bps, subformat));
self
}
pub fn with_stream_name(mut self, stream_index: u32, name: impl Into<String>) -> Self {
let name = name.into();
self.stream_names.retain(|(idx, _)| *idx != stream_index);
self.stream_names.push((stream_index, name));
self
}
pub fn with_stream_header_data(mut self, stream_index: u32, bytes: impl Into<Vec<u8>>) -> Self {
let bytes = bytes.into();
self.stream_header_data
.retain(|(idx, _)| *idx != stream_index);
self.stream_header_data.push((stream_index, bytes));
self
}
pub fn with_stream_frame_rect(
mut self,
stream_index: u32,
left: i16,
top: i16,
right: i16,
bottom: i16,
) -> Self {
self.stream_frame_rects
.retain(|(idx, _)| *idx != stream_index);
self.stream_frame_rects
.push((stream_index, [left, top, right, bottom]));
self
}
pub fn with_padding_granularity(mut self, n: u32) -> Self {
self.padding_granularity = if (2..=65536).contains(&n) && n.is_power_of_two() {
Some(n)
} else {
None
};
self
}
pub fn with_digitization_date(mut self, date: impl Into<String>) -> Self {
self.digitization_date = Some(date.into());
self
}
pub fn with_smpte_timecode(mut self, timecode: impl Into<String>) -> Self {
self.smpte_timecode = Some(timecode.into());
self
}
}
#[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,
ix_entries: Vec<IxStdEntry>,
}
#[derive(Clone, Copy, Debug)]
struct PrimaryIxSnapshot {
ckid: [u8; 4],
flags: u32,
offset: u32,
size: u32,
}
#[derive(Clone, Copy, Debug)]
struct IxStdEntry {
dw_offset: u32,
dw_size_with_flag: u32,
dw_offset_field2: u32,
}
pub fn open(output: Box<dyn WriteSeek>, streams: &[StreamInfo]) -> Result<Box<dyn Muxer>> {
open_with_kind(output, streams, AviKind::Avi10)
}
pub fn open_with_kind(
output: Box<dyn WriteSeek>,
streams: &[StreamInfo],
kind: AviKind,
) -> Result<Box<dyn Muxer>> {
open_with_options(output, streams, kind, AviMuxOptions::default())
}
pub fn open_with_options(
output: Box<dyn WriteSeek>,
streams: &[StreamInfo],
kind: AviKind,
options: AviMuxOptions,
) -> Result<Box<dyn Muxer>> {
let m = open_avi(output, streams, kind, options)?;
Ok(Box::new(m))
}
pub fn open_avi(
output: Box<dyn WriteSeek>,
streams: &[StreamInfo],
kind: AviKind,
options: AviMuxOptions,
) -> Result<AviMuxer> {
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 top_down = options
.top_down_video_streams
.iter()
.any(|&idx| idx as usize == i);
let extensible = options
.extensible_audio_streams
.iter()
.find(|(idx, _, _, _)| (*idx as usize) == i)
.map(|(_, mask, valid, guid)| (*mask, *valid, *guid));
let entry = build_strf(&s.params, top_down, extensible)?;
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,
ix_entries: Vec::new(),
});
}
Ok(AviMuxer {
output,
tracks,
kind,
options,
riff_size_off: 0,
movi_size_off: 0,
movi_start_off: 0,
index: Vec::new(),
indx_entries_count_off: None,
indx_entries_start_off: None,
indx_entries_capacity: 0,
indx_for_2field: false,
dmlh_total_frames_off: None,
segments: Vec::new(),
current_segment_packets: 0,
current_segment_indexed_packets: 0,
rec_open_size_off: None,
rec_packets_in_cluster: 0,
rec_bytes_in_cluster: 0,
pending_field2_offset: None,
primary_ix_snapshot: Vec::new(),
over_budget_streams: 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]]
}
#[derive(Clone, Copy, Debug)]
struct SegmentRecord {
riff_offset: u64,
total_size: u64,
#[allow(dead_code)]
packet_count: u32,
indexed_packet_count: u32,
}
pub struct AviMuxer {
output: Box<dyn WriteSeek>,
tracks: Vec<TrackState>,
kind: AviKind,
options: AviMuxOptions,
riff_size_off: u64,
movi_size_off: u64,
movi_start_off: u64,
index: Vec<IndexEntry>,
indx_entries_count_off: Option<u64>,
indx_entries_start_off: Option<u64>,
indx_entries_capacity: usize,
indx_for_2field: bool,
dmlh_total_frames_off: Option<u64>,
segments: Vec<SegmentRecord>,
current_segment_packets: u32,
current_segment_indexed_packets: u32,
rec_open_size_off: Option<u64>,
rec_packets_in_cluster: u32,
rec_bytes_in_cluster: u64,
pending_field2_offset: Option<u32>,
primary_ix_snapshot: Vec<PrimaryIxSnapshot>,
over_budget_streams: Vec<(u32, u64, u32)>,
header_written: bool,
trailer_written: bool,
}
pub const OPENDML_SUPER_INDEX_DEFAULT_CAPACITY: usize = 256;
pub const OPENDML_SUPER_INDEX_MIN_CAPACITY: usize = 16;
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,
self.options.avih_flags_override,
self.options.padding_granularity,
);
write_chunk(self.output.as_mut(), b"avih", &avih)?;
let want_indx = matches!(self.kind, AviKind::OpenDml(_));
let want_vprp = want_indx; let indx_is_2field = want_indx && self.options.field2_streams.contains(&0);
self.indx_for_2field = indx_is_2field;
let indx_capacity = self
.options
.super_index_capacity
.filter(|n| *n >= OPENDML_SUPER_INDEX_MIN_CAPACITY)
.unwrap_or(OPENDML_SUPER_INDEX_DEFAULT_CAPACITY);
for (i, t) in self.tracks.iter().enumerate() {
let with_indx = want_indx && i == 0;
let with_vprp = want_vprp && &t.entry.strh_type == b"vids";
let vprp_override = self
.options
.vprp_overrides
.iter()
.find(|(idx, _)| *idx == i as u32)
.map(|(_, c)| c.clone());
let indx_2field_here = indx_is_2field && with_indx;
let stream_name = self
.options
.stream_names
.iter()
.find(|(idx, _)| *idx == i as u32)
.map(|(_, n)| n.as_str());
let stream_header_data = self
.options
.stream_header_data
.iter()
.find(|(idx, _)| *idx == i as u32)
.map(|(_, b)| b.as_slice());
let frame_rect_override = self
.options
.stream_frame_rects
.iter()
.find(|(idx, _)| *idx == i as u32)
.map(|(_, r)| *r);
let (indx_count_off, indx_entries_off) = write_strl(
self.output.as_mut(),
i as u32,
t,
with_indx,
with_vprp,
vprp_override,
indx_2field_here,
indx_capacity,
stream_name,
stream_header_data,
frame_rect_override,
)?;
if with_indx {
self.indx_entries_count_off = indx_count_off;
self.indx_entries_start_off = indx_entries_off;
self.indx_entries_capacity = indx_capacity;
}
}
if want_indx {
let odml_size_off = begin_list(self.output.as_mut(), &LIST, b"odml")?;
crate::riff::write_chunk_header(self.output.as_mut(), b"dmlh", 4)?;
let dmlh_off = self.output.stream_position()?;
self.output.write_all(&0u32.to_le_bytes())?;
self.dmlh_total_frames_off = Some(dmlh_off);
finish_chunk(self.output.as_mut(), odml_size_off)?;
}
let want_info = !self.options.info_entries.is_empty();
if want_info && !self.options.info_top_level {
write_info_list(self.output.as_mut(), &self.options.info_entries)?;
}
if let Some(ref date) = self.options.digitization_date {
let mut body = date.as_bytes().to_vec();
body.push(0);
write_chunk(self.output.as_mut(), b"IDIT", &body)?;
}
if let Some(ref timecode) = self.options.smpte_timecode {
let mut body = timecode.as_bytes().to_vec();
body.push(0);
write_chunk(self.output.as_mut(), b"ISMP", &body)?;
}
finish_chunk(self.output.as_mut(), hdrl_size_off)?;
if want_info && self.options.info_top_level {
write_info_list(self.output.as_mut(), &self.options.info_entries)?;
}
self.movi_size_off = begin_list(self.output.as_mut(), &LIST, b"movi")?;
self.movi_start_off = self.movi_size_off + 4; self.current_segment_packets = 0;
self.current_segment_indexed_packets = 0;
self.rec_open_size_off = None;
self.rec_packets_in_cluster = 0;
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"));
}
if let AviKind::OpenDml(limit) = self.kind {
let projected = self.output.stream_position()?
+ 8 + packet.data.len() as u64
+ (packet.data.len() & 1) as u64
+ 16 ;
let segment_start = self.riff_size_off - 4;
let segment_used = projected.saturating_sub(segment_start);
if self.current_segment_packets > 0 && segment_used > limit.bytes() {
self.close_current_segment()?;
self.open_avix_segment()?;
}
}
let want_clustering =
self.options.rec_cluster_packets.is_some() || self.options.rec_cluster_bytes.is_some();
if want_clustering {
let projected_packet_bytes =
8u64 + packet.data.len() as u64 + (packet.data.len() & 1) as u64;
let needs_close_for_packets = self
.options
.rec_cluster_packets
.map(|n| self.rec_packets_in_cluster >= n)
.unwrap_or(false);
let needs_close_for_bytes = self
.options
.rec_cluster_bytes
.map(|n| {
self.rec_packets_in_cluster > 0
&& self.rec_bytes_in_cluster + projected_packet_bytes > n as u64
})
.unwrap_or(false);
if self.rec_open_size_off.is_none() {
self.open_rec_cluster()?;
} else if needs_close_for_packets || needs_close_for_bytes {
self.close_rec_cluster()?;
self.open_rec_cluster()?;
}
}
if let Some(n) = self.options.padding_granularity {
let before = self.output.stream_position()?;
self.emit_padding_junk_for(packet.data.len(), n)?;
let after = self.output.stream_position()?;
if self.rec_open_size_off.is_some() {
self.rec_bytes_in_cluster += after.saturating_sub(before);
}
}
let fourcc = self.tracks[idx].packet_fourcc;
let chunk_off = self.output.stream_position()?;
let rel_off_opt = chunk_off.checked_sub(self.movi_start_off);
let size = packet.data.len() as u32;
let pending_field2 = self.pending_field2_offset.take();
let stream_is_2field = self.options.field2_streams.contains(&(idx as u32));
let mut flags = if packet.flags.keyframe {
AVIIF_KEYFRAME
} else {
0
};
if stream_is_2field {
flags |= AVIIF_FIRSTPART | AVIIF_LASTPART;
}
if matches!(self.kind, AviKind::OpenDml(_)) {
let qw_base = self.movi_start_off + 4;
let data_off = chunk_off + 8;
if let Some(d) = data_off.checked_sub(qw_base) {
if d <= u32::MAX as u64 {
let dw_size_with_flag = if packet.flags.keyframe {
size
} else {
size | 0x8000_0000
};
let dw_offset_field2 = if stream_is_2field {
match pending_field2 {
Some(payload_off) => {
let abs = d + payload_off as u64;
if abs <= u32::MAX as u64 {
abs as u32
} else {
0
}
}
None => 0,
}
} else {
0
};
let ix_entry = IxStdEntry {
dw_offset: d as u32,
dw_size_with_flag,
dw_offset_field2,
};
self.tracks[idx].ix_entries.push(ix_entry);
if self.options.synthesise_idx1_from_ix && self.segments.is_empty() {
let idx1_offset = (d as u32).saturating_sub(4);
self.primary_ix_snapshot.push(PrimaryIxSnapshot {
ckid: fourcc,
flags,
offset: idx1_offset,
size,
});
}
}
}
}
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, packet.duration);
self.current_segment_packets += 1;
if idx == 0 {
self.current_segment_indexed_packets += 1;
}
if self.options.rec_cluster_packets.is_some() || self.options.rec_cluster_bytes.is_some() {
self.rec_packets_in_cluster += 1;
self.rec_bytes_in_cluster +=
8u64 + packet.data.len() as u64 + (packet.data.len() & 1) as u64;
}
let in_primary_segment = self.segments.is_empty();
if in_primary_segment {
if let Some(rel_off) = rel_off_opt {
if rel_off <= u32::MAX as u64 {
self.index.push(IndexEntry {
ckid: fourcc,
flags,
offset: rel_off as u32,
size,
});
}
}
}
if matches!(self.kind, AviKind::OpenDml(_)) {
let cadence = self
.options
.mid_movi_index_streams
.iter()
.find(|(i, _)| *i == idx as u32)
.map(|(_, n)| *n);
if let Some(n) = cadence {
if n > 0 && self.tracks[idx].ix_entries.len() as u32 >= n {
if self.rec_open_size_off.is_some() {
self.close_rec_cluster()?;
}
self.flush_ix_for_track(idx)?;
}
}
}
if matches!(self.kind, AviKind::Avi10) {
let cur = self.output.stream_position()?;
if cur > (2 * 1024 * 1024 * 1024) - 1024 {
return Err(Error::unsupported(
"avi muxer: file would exceed 2 GiB; use AviKind::OpenDml",
));
}
}
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"));
}
let in_primary_segment = self.segments.is_empty();
if self.rec_open_size_off.is_some() {
self.close_rec_cluster()?;
}
if matches!(self.kind, AviKind::OpenDml(_)) {
self.flush_ix_chunks()?;
}
finish_chunk(self.output.as_mut(), self.movi_size_off)?;
if in_primary_segment {
let idx_body = self.serialize_idx1();
write_chunk(self.output.as_mut(), b"idx1", &idx_body)?;
finish_chunk(self.output.as_mut(), self.riff_size_off)?;
if matches!(self.kind, AviKind::OpenDml(_)) {
let total_size = self.output.stream_position()?;
let riff_start = self.riff_size_off - 4;
self.segments.push(SegmentRecord {
riff_offset: riff_start,
total_size: total_size - riff_start,
packet_count: self.current_segment_packets,
indexed_packet_count: self.current_segment_indexed_packets,
});
}
} else {
finish_chunk(self.output.as_mut(), self.riff_size_off)?;
let total_size = self.output.stream_position()?;
let riff_start = self.riff_size_off - 4;
self.segments.push(SegmentRecord {
riff_offset: riff_start,
total_size: total_size - riff_start,
packet_count: self.current_segment_packets,
indexed_packet_count: self.current_segment_indexed_packets,
});
}
self.patch_post_counts()?;
if matches!(self.kind, AviKind::OpenDml(_)) {
self.patch_super_index()?;
}
if !self.options.per_stream_max_bytes_per_sec.is_empty() {
self.compute_per_stream_budget_breaches();
if self.options.strict_per_stream_budget {
if let Some(&(idx, observed, cap)) = self.over_budget_streams.first() {
return Err(Error::invalid(format!(
"AVI: stream {idx} exceeded per-stream dwMaxBytesPerSec cap: \
observed={observed} cap={cap}"
)));
}
}
}
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(48))?;
self.output.write_all(&total_video_frames.to_le_bytes())?;
let max_bytes_per_sec = self.options.max_bytes_per_sec_override.unwrap_or_else(|| {
let micro_per_frame: u64 = self
.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;
1_000_000u64 * scale / rate
})
.unwrap_or(0);
let total_bytes: u64 = self.tracks.iter().map(|t| t.total_bytes).sum();
let micros: u64 = (total_video_frames as u64).saturating_mul(micro_per_frame);
if micros == 0 || total_bytes == 0 {
self.tracks
.iter()
.filter(|t| &t.entry.strh_type == b"auds")
.filter_map(|t| audio_strf_avg_bytes_per_sec(&t.entry.strf))
.fold(0u32, |acc, v| acc.saturating_add(v))
} else {
let big = (total_bytes as u128) * 1_000_000u128;
let bps = big / (micros as u128);
bps.min(u32::MAX as u128) as u32
}
});
self.output.seek(SeekFrom::Start(36))?;
self.output.write_all(&max_bytes_per_sec.to_le_bytes())?;
let suggested_buffer_size =
self.options
.suggested_buffer_size_override
.unwrap_or_else(|| {
let max_track = self
.tracks
.iter()
.map(|t| t.max_chunk_size)
.max()
.unwrap_or(0);
max_track.saturating_add(3) & !3u32
});
self.output.seek(SeekFrom::Start(60))?;
self.output
.write_all(&suggested_buffer_size.to_le_bytes())?;
let mut strl_off: u64 = 88;
let opendml = matches!(self.kind, AviKind::OpenDml(_));
for (i, t) in self.tracks.iter().enumerate() {
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 mut strl_body = 4 + 64 + 8 + strf_padded;
if opendml && i == 0 {
let indx_payload = 24 + 16 * self.indx_entries_capacity;
let indx_padded = indx_payload + (indx_payload & 1);
strl_body += 8 + indx_padded;
}
if opendml && &t.entry.strh_type == b"vids" {
strl_body += 8 + 68;
}
strl_off += 8 + strl_body as u64;
}
self.output.seek(SeekFrom::Start(end_pos))?;
if let Some(off) = self.dmlh_total_frames_off {
self.output.seek(SeekFrom::Start(off))?;
self.output.write_all(&total_video_frames.to_le_bytes())?;
self.output.seek(SeekFrom::Start(end_pos))?;
}
Ok(())
}
fn serialize_idx1(&self) -> Vec<u8> {
if self.options.synthesise_idx1_from_ix
&& matches!(self.kind, AviKind::OpenDml(_))
&& !self.primary_ix_snapshot.is_empty()
{
return self.serialize_idx1_from_ix();
}
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());
}
idx_body
}
fn serialize_idx1_from_ix(&self) -> Vec<u8> {
let mut idx_body = Vec::with_capacity(self.primary_ix_snapshot.len() * 16);
for s in &self.primary_ix_snapshot {
idx_body.extend_from_slice(&s.ckid);
idx_body.extend_from_slice(&s.flags.to_le_bytes());
idx_body.extend_from_slice(&s.offset.to_le_bytes());
idx_body.extend_from_slice(&s.size.to_le_bytes());
}
idx_body
}
fn close_current_segment(&mut self) -> Result<()> {
let in_primary = self.segments.is_empty();
if self.rec_open_size_off.is_some() {
self.close_rec_cluster()?;
}
self.flush_ix_chunks()?;
finish_chunk(self.output.as_mut(), self.movi_size_off)?;
if in_primary {
let idx_body = self.serialize_idx1();
write_chunk(self.output.as_mut(), b"idx1", &idx_body)?;
}
finish_chunk(self.output.as_mut(), self.riff_size_off)?;
let after_riff = self.output.stream_position()?;
let riff_start = self.riff_size_off - 4;
self.segments.push(SegmentRecord {
riff_offset: riff_start,
total_size: after_riff - riff_start,
packet_count: self.current_segment_packets,
indexed_packet_count: self.current_segment_indexed_packets,
});
Ok(())
}
fn flush_ix_chunks(&mut self) -> Result<()> {
for track_idx in 0..self.tracks.len() {
self.flush_ix_for_track(track_idx)?;
}
Ok(())
}
fn flush_ix_for_track(&mut self, track_idx: usize) -> Result<()> {
if self.tracks[track_idx].ix_entries.is_empty() {
return Ok(());
}
let qw_base = self.movi_start_off + 4;
let entries = std::mem::take(&mut self.tracks[track_idx].ix_entries);
let stream_is_2field = self.options.field2_streams.contains(&(track_idx as u32));
let stream_digits = packet_fourcc_for(track_idx as u32, *b"xx");
let ix_id = [b'i', b'x', stream_digits[0], stream_digits[1]];
let (w_longs, sub_type, entry_size) = if stream_is_2field {
(3u16, 0x01u8, 12usize)
} else {
(2u16, 0u8, 8usize)
};
let mut payload = Vec::with_capacity(32 + entries.len() * entry_size);
payload.extend_from_slice(&w_longs.to_le_bytes());
payload.push(sub_type);
payload.push(0x01);
payload.extend_from_slice(&(entries.len() as u32).to_le_bytes());
payload.extend_from_slice(&self.tracks[track_idx].packet_fourcc);
payload.extend_from_slice(&qw_base.to_le_bytes());
payload.extend_from_slice(&0u32.to_le_bytes());
for e in entries.iter() {
payload.extend_from_slice(&e.dw_offset.to_le_bytes());
payload.extend_from_slice(&e.dw_size_with_flag.to_le_bytes());
if stream_is_2field {
payload.extend_from_slice(&e.dw_offset_field2.to_le_bytes());
}
}
write_chunk(self.output.as_mut(), &ix_id, &payload)?;
Ok(())
}
fn open_avix_segment(&mut self) -> Result<()> {
self.riff_size_off = begin_list(self.output.as_mut(), &RIFF, b"AVIX")?;
self.movi_size_off = begin_list(self.output.as_mut(), &LIST, b"movi")?;
self.movi_start_off = self.movi_size_off + 4;
self.current_segment_packets = 0;
self.current_segment_indexed_packets = 0;
self.rec_open_size_off = None;
self.rec_packets_in_cluster = 0;
self.rec_bytes_in_cluster = 0;
Ok(())
}
fn open_rec_cluster(&mut self) -> Result<()> {
let off = begin_list(self.output.as_mut(), &LIST, b"rec ")?;
self.rec_open_size_off = Some(off);
self.rec_packets_in_cluster = 0;
self.rec_bytes_in_cluster = 0;
Ok(())
}
fn close_rec_cluster(&mut self) -> Result<()> {
if let Some(off) = self.rec_open_size_off.take() {
finish_chunk(self.output.as_mut(), off)?;
self.rec_packets_in_cluster = 0;
self.rec_bytes_in_cluster = 0;
}
Ok(())
}
fn emit_padding_junk_for(&mut self, _payload_len: usize, granularity: u32) -> Result<()> {
let n = granularity as u64;
let cur = self.output.stream_position()?;
let aligned_now = (cur % n) == 0;
if aligned_now {
return Ok(());
}
let needed = cur + 8;
let target = needed.div_ceil(n) * n;
let body_len = (target - cur - 8) as u32;
let body_size_field = body_len & !1u32; crate::riff::write_chunk_header(self.output.as_mut(), b"JUNK", body_size_field)?;
let total_to_write = body_len as u64; let zeros = [0u8; 256];
let mut remaining = total_to_write;
while remaining > 0 {
let chunk = remaining.min(zeros.len() as u64) as usize;
self.output.write_all(&zeros[..chunk])?;
remaining -= chunk as u64;
}
Ok(())
}
pub fn set_field2_offset(&mut self, payload_offset: u32) {
self.pending_field2_offset = Some(payload_offset);
}
pub fn write_text_chunk(&mut self, stream_index: u32, data: &[u8]) -> Result<()> {
self.write_sideband_chunk(stream_index, *b"tx", data)
}
pub fn write_palette_change(&mut self, stream_index: u32, data: &[u8]) -> Result<()> {
self.write_sideband_chunk(stream_index, *b"pc", data)
}
pub fn with_palette_change_typed(
&mut self,
stream_index: u32,
change: &crate::demuxer::PaletteChange,
) -> Result<()> {
let body = change.to_bytes();
self.write_palette_change(stream_index, &body)
}
pub fn with_text_chunk_typed(
&mut self,
stream_index: u32,
text: &crate::demuxer::TextChunk,
) -> Result<()> {
let body = text.to_bytes();
self.write_text_chunk(stream_index, &body)
}
fn write_sideband_chunk(
&mut self,
stream_index: u32,
suffix: [u8; 2],
data: &[u8],
) -> Result<()> {
if !self.header_written {
return Err(Error::other(
"avi muxer: write_sideband_chunk before write_header",
));
}
if self.trailer_written {
return Err(Error::other(
"avi muxer: write_sideband_chunk after write_trailer",
));
}
let idx = stream_index as usize;
if idx >= self.tracks.len() {
return Err(Error::invalid(format!(
"avi muxer: unknown stream index {idx}"
)));
}
if data.len() > u32::MAX as usize {
return Err(Error::invalid(
"avi muxer: side-band chunk larger than 4 GiB",
));
}
if let AviKind::OpenDml(limit) = self.kind {
let projected = self.output.stream_position()?
+ 8 + data.len() as u64
+ (data.len() & 1) as u64
+ 16 ;
let segment_start = self.riff_size_off - 4;
let segment_used = projected.saturating_sub(segment_start);
if self.current_segment_packets > 0 && segment_used > limit.bytes() {
self.close_current_segment()?;
self.open_avix_segment()?;
}
}
let want_clustering =
self.options.rec_cluster_packets.is_some() || self.options.rec_cluster_bytes.is_some();
if want_clustering {
let projected_chunk_bytes = 8u64 + data.len() as u64 + (data.len() & 1) as u64;
let needs_close_for_packets = self
.options
.rec_cluster_packets
.map(|n| self.rec_packets_in_cluster >= n)
.unwrap_or(false);
let needs_close_for_bytes = self
.options
.rec_cluster_bytes
.map(|n| {
self.rec_packets_in_cluster > 0
&& self.rec_bytes_in_cluster + projected_chunk_bytes > n as u64
})
.unwrap_or(false);
if self.rec_open_size_off.is_none() {
self.open_rec_cluster()?;
} else if needs_close_for_packets || needs_close_for_bytes {
self.close_rec_cluster()?;
self.open_rec_cluster()?;
}
}
let fourcc = packet_fourcc_for(stream_index, suffix);
let chunk_off = self.output.stream_position()?;
let rel_off_opt = chunk_off.checked_sub(self.movi_start_off);
let size = data.len() as u32;
if matches!(self.kind, AviKind::OpenDml(_)) {
let qw_base = self.movi_start_off + 4;
let data_off = chunk_off + 8;
if let Some(d) = data_off.checked_sub(qw_base) {
if d <= u32::MAX as u64 {
let dw_size_with_flag = size | 0x8000_0000;
let ix_entry = IxStdEntry {
dw_offset: d as u32,
dw_size_with_flag,
dw_offset_field2: 0,
};
self.tracks[idx].ix_entries.push(ix_entry);
if self.options.synthesise_idx1_from_ix && self.segments.is_empty() {
let idx1_offset = (d as u32).saturating_sub(4);
self.primary_ix_snapshot.push(PrimaryIxSnapshot {
ckid: fourcc,
flags: 0,
offset: idx1_offset,
size,
});
}
}
}
}
write_chunk(self.output.as_mut(), &fourcc, data)?;
if want_clustering {
self.rec_packets_in_cluster += 1;
self.rec_bytes_in_cluster += 8u64 + data.len() as u64 + (data.len() & 1) as u64;
}
let in_primary_segment = self.segments.is_empty();
if in_primary_segment {
if let Some(rel_off) = rel_off_opt {
if rel_off <= u32::MAX as u64 {
self.index.push(IndexEntry {
ckid: fourcc,
flags: 0,
offset: rel_off as u32,
size,
});
}
}
}
Ok(())
}
pub fn truncated_super_index_segments(&self) -> usize {
if !matches!(self.kind, AviKind::OpenDml(_)) {
return 0;
}
if self.indx_entries_capacity == 0 {
return 0;
}
self.segments
.len()
.saturating_sub(self.indx_entries_capacity)
}
pub fn over_budget_streams(&self) -> &[(u32, u64, u32)] {
&self.over_budget_streams
}
fn compute_per_stream_budget_breaches(&mut self) {
self.over_budget_streams.clear();
let micro_per_frame: u64 = self
.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;
1_000_000u64 * scale / rate
})
.unwrap_or(0);
let video_packet_count: u64 = self
.tracks
.iter()
.find(|t| &t.entry.strh_type == b"vids")
.map(|t| t.packet_count as u64)
.unwrap_or(0);
let micros: u64 = video_packet_count.saturating_mul(micro_per_frame);
if micros == 0 {
return;
}
for &(idx, cap) in &self.options.per_stream_max_bytes_per_sec {
let s = idx as usize;
let track = match self.tracks.get(s) {
Some(t) => t,
None => continue,
};
let big = (track.total_bytes as u128) * 1_000_000u128;
let observed = (big / (micros as u128)).min(u64::MAX as u128) as u64;
if observed > cap as u64 {
self.over_budget_streams.push((idx, observed, cap));
}
}
}
fn patch_super_index(&mut self) -> Result<()> {
let (Some(n_off), Some(start_off)) =
(self.indx_entries_count_off, self.indx_entries_start_off)
else {
return Ok(());
};
let end_pos = self.output.stream_position()?;
let n_to_write = self.segments.len().min(self.indx_entries_capacity);
self.output.seek(SeekFrom::Start(n_off))?;
self.output.write_all(&(n_to_write as u32).to_le_bytes())?;
for (i, seg) in self.segments.iter().take(n_to_write).enumerate() {
let slot = start_off + (i as u64) * 16;
self.output.seek(SeekFrom::Start(slot))?;
self.output.write_all(&seg.riff_offset.to_le_bytes())?;
let dw_size = seg.total_size.min(u32::MAX as u64) as u32;
self.output.write_all(&dw_size.to_le_bytes())?;
self.output
.write_all(&seg.indexed_packet_count.to_le_bytes())?;
}
self.output.seek(SeekFrom::Start(end_pos))?;
Ok(())
}
}
fn write_info_list<W: Write + Seek + ?Sized>(
w: &mut W,
entries: &[([u8; 4], String)],
) -> Result<()> {
let info_size_off = begin_list(w, &LIST, b"INFO")?;
for (id, value) in entries {
let mut body = value.as_bytes().to_vec();
body.push(0);
write_chunk(w, id, &body)?;
}
finish_chunk(w, info_size_off)?;
Ok(())
}
pub const DEFAULT_AVIH_FLAGS: u32 = 0x0000_0810;
fn build_avih(
tracks: &[TrackState],
flags_override: Option<u32>,
padding_granularity: Option<u32>,
) -> 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 = flags_override.unwrap_or(DEFAULT_AVIH_FLAGS);
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(&padding_granularity.unwrap_or(0).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
}
#[allow(clippy::too_many_arguments)]
fn write_strl<W: Write + Seek + ?Sized>(
w: &mut W,
_index: u32,
t: &TrackState,
with_indx: bool,
with_vprp: bool,
vprp_override: Option<VprpConfig>,
indx_2field: bool,
indx_capacity: usize,
stream_name: Option<&str>,
stream_header_data: Option<&[u8]>,
frame_rect_override: Option<[i16; 4]>,
) -> Result<(Option<u64>, Option<u64>)> {
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 let Some([l, t_, r, b]) = frame_rect_override {
strh.extend_from_slice(&l.to_le_bytes());
strh.extend_from_slice(&t_.to_le_bytes());
strh.extend_from_slice(&r.to_le_bytes());
strh.extend_from_slice(&b.to_le_bytes());
} else 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)?;
let mut indx_n_entries_off: Option<u64> = None;
let mut indx_entries_start_off: Option<u64> = None;
if with_indx {
let chunk_id = packet_fourcc_for(0, t.entry.chunk_suffix);
let entries_bytes = indx_capacity * 16;
let payload_len = 24 + entries_bytes;
write_chunk_header(w, b"indx", payload_len as u32)?;
let payload_off = w.stream_position()?;
let mut buf = vec![0u8; payload_len];
buf[0..2].copy_from_slice(&4u16.to_le_bytes());
if indx_2field {
buf[2] = 0x01; }
buf[8..12].copy_from_slice(&chunk_id);
w.write_all(&buf)?;
if payload_len & 1 == 1 {
w.write_all(&[0])?;
}
indx_n_entries_off = Some(payload_off + 4);
indx_entries_start_off = Some(payload_off + 24);
}
if with_vprp {
let body = build_vprp_body(t, vprp_override);
write_chunk(w, b"vprp", &body)?;
}
if let Some(bytes) = stream_header_data {
write_chunk(w, b"strd", bytes)?;
}
if let Some(name) = stream_name {
let mut body = Vec::with_capacity(name.len() + 1);
body.extend_from_slice(name.as_bytes());
body.push(0); write_chunk(w, b"strn", &body)?;
}
finish_chunk(w, strl_off)?;
Ok((indx_n_entries_off, indx_entries_start_off))
}
fn build_vprp_body(t: &TrackState, override_cfg: Option<VprpConfig>) -> Vec<u8> {
let width = t.stream.params.width.unwrap_or(0);
let height = t.stream.params.height.unwrap_or(0);
let stream_refresh_rate = if t.entry.scale > 0 {
((t.entry.rate as u64 + (t.entry.scale as u64 / 2)) / t.entry.scale as u64) as u32
} else {
0
};
let cfg = override_cfg.unwrap_or_default();
let video_format_token = cfg.video_format_token; let video_standard = cfg.video_standard; let refresh_rate = if cfg.vertical_refresh_rate > 0 {
cfg.vertical_refresh_rate
} else {
stream_refresh_rate
};
let frame_aspect_ratio = if cfg.frame_aspect_ratio > 0 {
cfg.frame_aspect_ratio
} else {
(4u32 << 16) | 3u32
};
let nb_field_per_frame = if cfg.nb_field_per_frame > 0 {
cfg.nb_field_per_frame
} else {
1
};
let mut body = Vec::with_capacity(36 + 32 * nb_field_per_frame as usize);
body.extend_from_slice(&video_format_token.to_le_bytes());
body.extend_from_slice(&video_standard.to_le_bytes());
body.extend_from_slice(&refresh_rate.to_le_bytes()); body.extend_from_slice(&width.to_le_bytes()); body.extend_from_slice(&height.to_le_bytes()); body.extend_from_slice(&frame_aspect_ratio.to_le_bytes()); body.extend_from_slice(&width.to_le_bytes()); body.extend_from_slice(&height.to_le_bytes()); body.extend_from_slice(&nb_field_per_frame.to_le_bytes());
let active_fields = nb_field_per_frame.max(1) as usize;
let use_override = !cfg.field_descs.is_empty() && cfg.field_descs.len() >= active_fields;
if use_override {
for d in cfg.field_descs.iter().take(active_fields) {
body.extend_from_slice(&d.compressed_bm_height.to_le_bytes());
body.extend_from_slice(&d.compressed_bm_width.to_le_bytes());
body.extend_from_slice(&d.valid_bm_height.to_le_bytes());
body.extend_from_slice(&d.valid_bm_width.to_le_bytes());
body.extend_from_slice(&d.valid_bm_x_offset.to_le_bytes());
body.extend_from_slice(&d.valid_bm_y_offset.to_le_bytes());
body.extend_from_slice(&d.video_x_offset_in_t.to_le_bytes());
body.extend_from_slice(&d.video_y_valid_start_line.to_le_bytes());
}
} else if nb_field_per_frame >= 2 {
let half_height = height / 2;
for field_index in 0..nb_field_per_frame.min(2) {
let video_y_valid_start_line = if field_index == 0 {
23u32
} else {
half_height + 23
};
body.extend_from_slice(&half_height.to_le_bytes()); body.extend_from_slice(&width.to_le_bytes()); body.extend_from_slice(&half_height.to_le_bytes()); body.extend_from_slice(&width.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(&video_y_valid_start_line.to_le_bytes());
}
} else {
body.extend_from_slice(&height.to_le_bytes()); body.extend_from_slice(&width.to_le_bytes()); body.extend_from_slice(&height.to_le_bytes()); body.extend_from_slice(&width.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(&0u32.to_le_bytes()); }
body
}
fn audio_strf_avg_bytes_per_sec(strf: &[u8]) -> Option<u32> {
if strf.len() < 12 {
return None;
}
let bps = u32::from_le_bytes([strf[8], strf[9], strf[10], strf[11]]);
if bps == 0 {
None
} else {
Some(bps)
}
}
fn sample_count_of_packet(
stream: &StreamInfo,
entry: &StrfEntry,
size: u32,
duration: Option<i64>,
) -> u64 {
if &entry.strh_type == b"auds" {
if entry.sample_size > 0 {
return (size as u64) / (entry.sample_size as u64);
}
if let Some(d) = duration {
if d > 0 {
return d as u64;
}
}
}
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"),
}
}
}