use oxideav_core::{CodecId, CodecParameters, CodecTag, Error, MediaType, Result, SampleFormat};
use crate::stream_format::{write_bitmap_info_header, write_waveformatex};
#[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) -> Result<StrfEntry> {
match params.media_type {
MediaType::Video => build_video_strf(params),
MediaType::Audio => build_audio_strf(params),
_ => 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) -> 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 strf = 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) -> 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"))?;
let format_tag = audio_format_tag(params)?;
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).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).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) {
Err(Error::Unsupported(_)) => {}
other => panic!("expected Unsupported, got {other:?}"),
}
}
}