use oxideav_core::{CodecId, CodecParameters, CodecTag, Error, MediaType, Result, SampleFormat};
use crate::stream_format::{
write_bitmap_info_header, write_bitmap_info_header_oriented, write_waveformatex,
write_waveformatextensible, Guid, WAVE_FORMAT_EXTENSIBLE,
};
#[derive(Debug)]
pub(crate) struct StrfEntry {
pub chunk_suffix: [u8; 2],
pub handler_fourcc: [u8; 4],
pub strf: Vec<u8>,
pub strh_type: [u8; 4],
pub sample_size: u32,
pub scale: u32,
pub rate: u32,
}
pub(crate) fn build_strf(
params: &CodecParameters,
top_down: bool,
extensible: Option<(u32, u16, Guid)>,
) -> Result<StrfEntry> {
match params.media_type {
MediaType::Video => build_video_strf(params, top_down),
MediaType::Audio => build_audio_strf(params, extensible),
_ => Err(Error::unsupported(format!(
"avi muxer: media type {:?} not supported",
params.media_type
))),
}
}
fn video_fourcc(params: &CodecParameters) -> Result<[u8; 4]> {
if let Some(CodecTag::Fourcc(bytes)) = ¶ms.tag {
return Ok(*bytes);
}
if let Some(hint) = extradata_fourcc_hint(¶ms.extradata) {
return Ok(hint);
}
if params.codec_id.as_str() == "rgb24" {
return Ok([0, 0, 0, 0]);
}
Err(Error::unsupported(format!(
"avi muxer: codec `{}` has no FourCC; \
set `params.tag = Some(CodecTag::fourcc(...))` (preferred), \
or pre-fill `extradata`'s first 4 bytes with the desired FourCC",
params.codec_id
)))
}
fn audio_format_tag(params: &CodecParameters) -> Result<u16> {
if let Some(CodecTag::WaveFormat(t)) = ¶ms.tag {
return Ok(*t);
}
if let Some(synth) = pcm_synth_format_tag(¶ms.codec_id) {
return Ok(synth);
}
Err(Error::unsupported(format!(
"avi muxer: codec `{}` has no WAVEFORMATEX wFormatTag; \
set `params.tag = Some(CodecTag::wave_format(...))`",
params.codec_id
)))
}
fn pcm_synth_format_tag(codec_id: &CodecId) -> Option<u16> {
match codec_id.as_str() {
"pcm_u8" | "pcm_s16le" | "pcm_s24le" | "pcm_s32le" => Some(0x0001),
"pcm_f32le" | "pcm_f64le" => Some(0x0003),
_ => None,
}
}
fn extradata_fourcc_hint(extradata: &[u8]) -> Option<[u8; 4]> {
if extradata.len() < 4 {
return None;
}
let mut hint = [0u8; 4];
hint.copy_from_slice(&extradata[..4]);
if !hint.iter().all(|&b| b.is_ascii_alphanumeric() || b == b' ') {
return None;
}
for b in hint.iter_mut() {
*b = b.to_ascii_uppercase();
}
Some(hint)
}
fn build_video_strf(params: &CodecParameters, top_down: bool) -> Result<StrfEntry> {
let width = params
.width
.ok_or_else(|| Error::invalid("avi muxer: video stream missing width"))?;
let height = params
.height
.ok_or_else(|| Error::invalid("avi muxer: video stream missing height"))?;
let fourcc = video_fourcc(params)?;
let bit_count: u16 = 24;
let allow_top_down = fourcc == [0, 0, 0, 0];
let strf = if top_down && allow_top_down {
write_bitmap_info_header_oriented(width, height, fourcc, bit_count, ¶ms.extradata, true)
} else {
write_bitmap_info_header(width, height, fourcc, bit_count, ¶ms.extradata)
};
let (scale, rate) = video_scale_rate(params);
let chunk_suffix = if fourcc == [0, 0, 0, 0] {
*b"db"
} else {
*b"dc"
};
Ok(StrfEntry {
chunk_suffix,
handler_fourcc: fourcc,
strf,
strh_type: *b"vids",
sample_size: 0,
scale,
rate,
})
}
fn build_audio_strf(
params: &CodecParameters,
extensible: Option<(u32, u16, Guid)>,
) -> Result<StrfEntry> {
let channels = params
.channels
.ok_or_else(|| Error::invalid("avi muxer: audio stream missing channels"))?;
let sample_rate = params
.sample_rate
.ok_or_else(|| Error::invalid("avi muxer: audio stream missing sample_rate"))?;
if let Some((channel_mask, valid_bps, subformat)) = extensible {
let id = params.codec_id.as_str();
let container_bits = pcm_bits_per_sample(id, params.sample_format).unwrap_or_else(|| {
valid_bps.max(8).next_multiple_of(8)
});
let block_align = channels * (container_bits / 8).max(1);
let avg_bytes_per_sec = sample_rate * block_align as u32;
let strf = write_waveformatextensible(
channels,
sample_rate,
avg_bytes_per_sec,
block_align,
container_bits,
valid_bps,
channel_mask,
&subformat,
);
let sample_size = if pcm_bits_per_sample(id, params.sample_format).is_some() {
block_align as u32
} else {
0
};
return Ok(StrfEntry {
chunk_suffix: *b"wb",
handler_fourcc: *b"\0\0\0\0",
strf,
strh_type: *b"auds",
sample_size,
scale: 1,
rate: sample_rate,
});
}
let format_tag = audio_format_tag(params)?;
if format_tag == WAVE_FORMAT_EXTENSIBLE {
return Err(Error::invalid(
"avi muxer: WAVE_FORMAT_EXTENSIBLE (0xFFFE) requires \
AviMuxOptions::with_extensible_audio(channel_mask, valid_bps, subformat_guid)",
));
}
let id = params.codec_id.as_str();
if let Some(bits) = pcm_bits_per_sample(id, params.sample_format) {
let block_align = channels * (bits / 8).max(1);
let avg_bytes_per_sec = sample_rate * block_align as u32;
let strf = write_waveformatex(
format_tag,
channels,
sample_rate,
avg_bytes_per_sec,
block_align,
bits,
&[],
);
return Ok(StrfEntry {
chunk_suffix: *b"wb",
handler_fourcc: *b"\0\0\0\0",
strf,
strh_type: *b"auds",
sample_size: block_align as u32,
scale: 1,
rate: sample_rate,
});
}
if matches!(id, "pcm_alaw" | "pcm_mulaw") {
let block_align = channels;
let avg_bytes_per_sec = sample_rate * block_align as u32;
let strf = write_waveformatex(
format_tag,
channels,
sample_rate,
avg_bytes_per_sec,
block_align,
8,
&[],
);
return Ok(StrfEntry {
chunk_suffix: *b"wb",
handler_fourcc: *b"\0\0\0\0",
strf,
strh_type: *b"auds",
sample_size: block_align as u32,
scale: 1,
rate: sample_rate,
});
}
let avg_bytes_per_sec = params.bit_rate.map(|b| (b / 8) as u32).unwrap_or(0);
let block_align: u16 = 1;
let strf = write_waveformatex(
format_tag,
channels,
sample_rate,
avg_bytes_per_sec,
block_align,
0,
¶ms.extradata,
);
Ok(StrfEntry {
chunk_suffix: *b"wb",
handler_fourcc: *b"\0\0\0\0",
strf,
strh_type: *b"auds",
sample_size: 0,
scale: 1,
rate: sample_rate,
})
}
fn pcm_bits_per_sample(codec_id: &str, sample_format: Option<SampleFormat>) -> Option<u16> {
match codec_id {
"pcm_u8" => Some(8),
"pcm_s16le" | "pcm_s16be" => Some(16),
"pcm_s24le" => Some(24),
"pcm_s32le" => Some(32),
"pcm_f32le" => Some(32),
"pcm_f64le" => Some(64),
_ => sample_format.map(|f| (f.bytes_per_sample() as u16) * 8),
}
}
fn video_scale_rate(params: &CodecParameters) -> (u32, u32) {
if let Some(fr) = params.frame_rate {
let num = fr.num.max(1) as u32;
let den = fr.den.max(1) as u32;
return (den, num);
}
(1, 25)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn extradata_hint_picks_uppercase_printable() {
assert_eq!(extradata_fourcc_hint(b"M8RGtail"), Some(*b"M8RG"));
assert_eq!(extradata_fourcc_hint(b"m8rgtail"), Some(*b"M8RG"));
assert!(extradata_fourcc_hint(&[0, 1, 2, 3]).is_none());
assert!(extradata_fourcc_hint(b"abc").is_none());
}
#[test]
fn video_fourcc_reads_params_tag() {
let mut p =
CodecParameters::video(CodecId::new("magicyuv")).with_tag(CodecTag::fourcc(b"M8RG"));
p.width = Some(64);
p.height = Some(64);
let fc = video_fourcc(&p).unwrap();
assert_eq!(&fc, b"M8RG");
}
#[test]
fn video_fourcc_params_tag_wins_over_extradata_hint() {
let mut p =
CodecParameters::video(CodecId::new("magicyuv")).with_tag(CodecTag::fourcc(b"M8RG"));
p.width = Some(64);
p.height = Some(64);
p.extradata = b"M8YAtail".to_vec();
let fc = video_fourcc(&p).unwrap();
assert_eq!(&fc, b"M8RG");
}
#[test]
fn video_fourcc_falls_back_to_extradata_hint() {
let mut p = CodecParameters::video(CodecId::new("magicyuv"));
p.width = Some(64);
p.height = Some(64);
p.extradata = b"M8YA-extra".to_vec();
let fc = video_fourcc(&p).unwrap();
assert_eq!(&fc, b"M8YA");
}
#[test]
fn video_fourcc_unknown_codec_errors() {
let mut p = CodecParameters::video(CodecId::new("noexist"));
p.width = Some(64);
p.height = Some(64);
match video_fourcc(&p) {
Err(Error::Unsupported(_)) => {}
other => panic!("expected Unsupported, got {other:?}"),
}
}
#[test]
fn rgb24_uses_bi_rgb_sentinel() {
let mut p = CodecParameters::video(CodecId::new("rgb24"));
p.width = Some(64);
p.height = Some(64);
let fc = video_fourcc(&p).unwrap();
assert_eq!(&fc, &[0, 0, 0, 0]);
}
#[test]
fn pcm_format_tag_is_synthesised() {
let mut p = CodecParameters::audio(CodecId::new("pcm_s16le"));
p.channels = Some(2);
p.sample_rate = Some(48_000);
let entry = build_strf(&p, false, None).unwrap();
assert_eq!(&entry.strh_type, b"auds");
assert_eq!(entry.sample_size, 4); }
#[test]
fn compressed_audio_uses_params_tag() {
let mut p =
CodecParameters::audio(CodecId::new("mp3")).with_tag(CodecTag::wave_format(0x0055));
p.channels = Some(2);
p.sample_rate = Some(48_000);
let entry = build_strf(&p, false, None).unwrap();
assert_eq!(&entry.strh_type, b"auds");
assert_eq!(&entry.strf[0..2], &0x0055u16.to_le_bytes());
}
#[test]
fn unknown_audio_codec_errors() {
let mut p = CodecParameters::audio(CodecId::new("noexist"));
p.channels = Some(2);
p.sample_rate = Some(48_000);
match build_strf(&p, false, None) {
Err(Error::Unsupported(_)) => {}
other => panic!("expected Unsupported, got {other:?}"),
}
}
#[test]
fn top_down_only_honoured_for_bi_rgb() {
let mut p = CodecParameters::video(CodecId::new("mjpeg"))
.with_tag(oxideav_core::CodecTag::fourcc(b"MJPG"));
p.width = Some(320);
p.height = Some(240);
let entry = build_strf(&p, true, None).unwrap();
let h = i32::from_le_bytes([entry.strf[8], entry.strf[9], entry.strf[10], entry.strf[11]]);
assert_eq!(h, 240, "compressed FourCCs MUST use positive biHeight");
let mut p = CodecParameters::video(CodecId::new("rgb24"));
p.width = Some(320);
p.height = Some(240);
let entry = build_strf(&p, true, None).unwrap();
let h = i32::from_le_bytes([entry.strf[8], entry.strf[9], entry.strf[10], entry.strf[11]]);
assert_eq!(h, -240, "BI_RGB + top_down ⇒ negative biHeight");
}
}