mod support;
use ffmpeg_next::{
codec::Parameters,
ffi::{
AV_INPUT_BUFFER_PADDING_SIZE, AVChannelCustom, AVChannelOrder, AVCodecParameters,
AVPacketSideData, av_mallocz,
},
};
use mediadecode::{
Received, Sent, Timebase,
decoder::{AudioStreamDecoder, VideoStreamDecoder},
demuxer::{Demuxer, TrackKind},
};
use mediadecode_ffmpeg::{
CodecTicket, DecoderLimits, DemuxError, DemuxLimits, FfmpegOwnedAudioStreamDecoder,
FfmpegOwnedDemuxer, FfmpegOwnedVideoStreamDecoder, OwnedAudioFrame, OwnedVideoFrame,
PacketLimits, empty_owned_audio_frame, empty_owned_video_frame,
owned_audio_packet_from_ffmpeg_in, owned_video_packet_from_ffmpeg_in,
};
unsafe fn compare(src: *const AVCodecParameters, dst: *const AVCodecParameters) -> Vec<String> {
use core::ptr::{addr_of, read_unaligned};
let mut bad = Vec::new();
macro_rules! scalar {
($name:ident) => {
let (a, b) = unsafe { ((*src).$name, (*dst).$name) };
if a != b {
bad.push(format!("{}: {:?} != {:?}", stringify!($name), a, b));
}
};
}
macro_rules! raw_enum {
($name:ident) => {
let (a, b) = unsafe {
(
read_unaligned(addr_of!((*src).$name).cast::<i32>()),
read_unaligned(addr_of!((*dst).$name).cast::<i32>()),
)
};
if a != b {
bad.push(format!("{}: {} != {}", stringify!($name), a, b));
}
};
}
macro_rules! rational {
($name:ident) => {
let (a, b) = unsafe { ((*src).$name, (*dst).$name) };
if (a.num, a.den) != (b.num, b.den) {
bad.push(format!(
"{}: {}/{} != {}/{}",
stringify!($name),
a.num,
a.den,
b.num,
b.den,
));
}
};
}
raw_enum!(codec_type);
raw_enum!(codec_id);
scalar!(codec_tag);
let (src_ex, src_len, dst_ex, dst_len) = unsafe {
(
(*src).extradata,
(*src).extradata_size,
(*dst).extradata,
(*dst).extradata_size,
)
};
if src_len != dst_len {
bad.push(format!("extradata_size: {src_len} != {dst_len}"));
} else if src_ex.is_null() != dst_ex.is_null() {
bad.push(format!(
"extradata: presence differs ({} vs {})",
src_ex.is_null(),
dst_ex.is_null(),
));
} else if !src_ex.is_null() && src_len > 0 {
if src_ex == dst_ex {
bad.push("extradata: the rebuild aliased the original".to_owned());
}
let len = src_len as usize;
let (a, b) = unsafe {
(
core::slice::from_raw_parts(src_ex, len + AV_INPUT_BUFFER_PADDING_SIZE as usize),
core::slice::from_raw_parts(dst_ex, len + AV_INPUT_BUFFER_PADDING_SIZE as usize),
)
};
if a[..len] != b[..len] {
bad.push(format!("extradata: {len} payload bytes differ"));
}
if b[len..].iter().any(|&byte| byte != 0) {
bad.push("extradata: the rebuild's trailing padding is not zeroed".to_owned());
}
}
let (src_sd, src_n, dst_sd, dst_n) = unsafe {
(
(*src).coded_side_data,
(*src).nb_coded_side_data,
(*dst).coded_side_data,
(*dst).nb_coded_side_data,
)
};
if src_n != dst_n {
bad.push(format!("nb_coded_side_data: {src_n} != {dst_n}"));
} else if src_n > 0 {
if src_sd == dst_sd {
bad.push("coded_side_data: the rebuild aliased the original".to_owned());
}
for index in 0..src_n as usize {
let (a_kind, a_size, a_data, b_kind, b_size, b_data) = unsafe {
let a = src_sd.add(index);
let b = dst_sd.add(index);
(
read_unaligned(addr_of!((*a).type_).cast::<i32>()),
read_unaligned(addr_of!((*a).size)),
read_unaligned(addr_of!((*a).data)),
read_unaligned(addr_of!((*b).type_).cast::<i32>()),
read_unaligned(addr_of!((*b).size)),
read_unaligned(addr_of!((*b).data)),
)
};
if a_kind != b_kind {
bad.push(format!(
"coded_side_data[{index}].type: {a_kind} != {b_kind}"
));
}
if a_size != b_size {
bad.push(format!(
"coded_side_data[{index}].size: {a_size} != {b_size}"
));
} else if a_size > 0 && !a_data.is_null() && !b_data.is_null() {
if a_data == b_data {
bad.push(format!("coded_side_data[{index}]: aliased the original"));
}
let (a, b) = unsafe {
(
core::slice::from_raw_parts(a_data, a_size),
core::slice::from_raw_parts(b_data, b_size),
)
};
if a != b {
bad.push(format!(
"coded_side_data[{index}]: {a_size} payload bytes differ"
));
}
}
}
}
scalar!(format);
scalar!(bit_rate);
scalar!(bits_per_coded_sample);
scalar!(bits_per_raw_sample);
scalar!(profile);
scalar!(level);
scalar!(width);
scalar!(height);
rational!(sample_aspect_ratio);
rational!(framerate);
raw_enum!(field_order);
raw_enum!(color_range);
raw_enum!(color_primaries);
raw_enum!(color_trc);
raw_enum!(color_space);
raw_enum!(chroma_location);
scalar!(video_delay);
let (a_order, a_ch, a_opaque, b_order, b_ch, b_opaque) = unsafe {
(
read_unaligned(addr_of!((*src).ch_layout.order).cast::<i32>()),
(*src).ch_layout.nb_channels,
(*src).ch_layout.opaque,
read_unaligned(addr_of!((*dst).ch_layout.order).cast::<i32>()),
(*dst).ch_layout.nb_channels,
(*dst).ch_layout.opaque,
)
};
if a_order != b_order {
bad.push(format!("ch_layout.order: {a_order} != {b_order}"));
}
if a_ch != b_ch {
bad.push(format!("ch_layout.nb_channels: {a_ch} != {b_ch}"));
}
if a_opaque != b_opaque {
bad.push("ch_layout.opaque: differs".to_owned());
}
if a_order == b_order {
if a_order == AVChannelOrder::AV_CHANNEL_ORDER_CUSTOM as i32 {
let (a_map, b_map) = unsafe { ((*src).ch_layout.u.map, (*dst).ch_layout.u.map) };
if a_map.is_null() != b_map.is_null() {
bad.push("ch_layout.u.map: presence differs".to_owned());
} else if !a_map.is_null() {
if a_map == b_map {
bad.push("ch_layout.u.map: the rebuild aliased the original".to_owned());
}
for index in 0..a_ch.max(0) as usize {
let (a_id, a_name, a_op, b_id, b_name, b_op) = unsafe {
let a = a_map.add(index);
let b = b_map.add(index);
(
read_unaligned(addr_of!((*a).id).cast::<i32>()),
read_unaligned(addr_of!((*a).name).cast::<[u8; 16]>()),
read_unaligned(addr_of!((*a).opaque)),
read_unaligned(addr_of!((*b).id).cast::<i32>()),
read_unaligned(addr_of!((*b).name).cast::<[u8; 16]>()),
read_unaligned(addr_of!((*b).opaque)),
)
};
if (a_id, a_name, a_op) != (b_id, b_name, b_op) {
bad.push(format!("ch_layout.u.map[{index}]: differs"));
}
}
}
} else {
let (a_mask, b_mask) = unsafe { ((*src).ch_layout.u.mask, (*dst).ch_layout.u.mask) };
if a_mask != b_mask {
bad.push(format!("ch_layout.u.mask: {a_mask:#x} != {b_mask:#x}"));
}
}
}
scalar!(sample_rate);
scalar!(block_align);
scalar!(frame_size);
scalar!(initial_padding);
scalar!(trailing_padding);
scalar!(seek_preroll);
raw_enum!(alpha_mode);
bad
}
fn assert_round_trip(original: &Parameters, what: &str) -> CodecTicket {
let ticket = CodecTicket::mirror(original, 0, usize::MAX)
.unwrap_or_else(|e| panic!("{what}: the mirror refused: {e}"));
let rebuilt = ticket
.rebuild()
.unwrap_or_else(|e| panic!("{what}: the rebuild failed: {e}"));
let bad = unsafe { compare(original.as_ptr(), rebuilt.as_ptr()) };
assert!(
bad.is_empty(),
"{what}: the rebuild is not the original:\n {}",
bad.join("\n ")
);
let rebuilt_again = ticket.rebuild().expect("a second rebuild");
let bad = unsafe { compare(original.as_ptr(), rebuilt_again.as_ptr()) };
assert!(bad.is_empty(), "{what}: the second rebuild diverged");
ticket
}
fn fixtures(corpus: &support::Corpus) -> Vec<(&'static str, std::path::PathBuf)> {
let minted = corpus.dir().join("icc.mp4");
std::fs::write(
&minted,
mint_mp4_with_icc_profile(&icc_profile_bytes(3_072)),
)
.expect("write the minted container");
vec![
("icc.mp4", minted),
("multi.mkv", corpus.multi_track_mkv()),
("cover.mp3", corpus.cover_art_mp3()),
("timecode.mov", corpus.timecode_mov()),
("cropped.mp4", corpus.cropped_h264()),
("swonly.webm", corpus.software_only_video()),
("surround.flac", corpus.surround_flac()),
("cues.srt", corpus.subrip()),
("mono.png", corpus.monochrome_png()),
("indexed.png", corpus.indexed_png()),
]
}
#[test]
fn every_stream_of_every_fixture_rebuilds_into_itself() {
let Some(corpus) = support::Corpus::new() else {
return;
};
let mut streams = 0usize;
let mut with_extradata = 0usize;
let mut with_side_data = 0usize;
for (name, path) in fixtures(&corpus) {
let input = ffmpeg_next::format::input(&path)
.unwrap_or_else(|e| panic!("{name}: libavformat could not open the fixture: {e}"));
for stream in input.streams() {
let index = stream.index();
let parameters = stream.parameters();
let ticket = assert_round_trip(¶meters, &format!("{name} stream {index}"));
streams += 1;
if !ticket.extradata().is_empty() {
with_extradata += 1;
}
if !ticket.coded_side_data().is_empty() {
with_side_data += 1;
}
}
}
assert!(streams >= 16, "only {streams} streams swept");
assert!(
with_extradata >= 4,
"only {with_extradata} streams carried extradata; the sweep proves nothing about it",
);
assert!(
with_side_data >= 1,
"no stream carried coded side data; the minted fixture is not reaching the sweep",
);
eprintln!(
"codec-ticket parity: {streams} streams, {with_extradata} with extradata, \
{with_side_data} with coded side data",
);
}
#[test]
fn every_track_row_hands_back_the_parameters_the_file_declared() {
let Some(corpus) = support::Corpus::new() else {
return;
};
for (name, path) in fixtures(&corpus) {
let input = ffmpeg_next::format::input(&path).expect("open for reference");
let reference: Vec<Parameters> = input.streams().map(|s| s.parameters()).collect();
let demuxer = FfmpegOwnedDemuxer::open(&path).unwrap_or_else(|e| panic!("{name}: {e}"));
for (index, track) in demuxer.tracks().iter().enumerate() {
let extra = track.extra();
let rebuilt = extra
.clone_parameters()
.unwrap_or_else(|e| panic!("{name} track {index}: {e}"));
let attachment = track.kind() == TrackKind::Attachment;
let bad = unsafe { compare(reference[index].as_ptr(), rebuilt.as_ptr()) };
if attachment {
assert!(
bad.iter().all(|m| m.starts_with("extradata")),
"{name} track {index}: an attachment diverged beyond its extradata:\n {}",
bad.join("\n "),
);
assert!(
extra.ticket().extradata().is_empty(),
"{name} track {index}: the attachment road kept its extradata",
);
} else {
assert!(
bad.is_empty(),
"{name} track {index}: the row is not the file:\n {}",
bad.join("\n "),
);
}
assert_eq!(extra.parameter_bytes(), extra.ticket().footprint_bytes());
}
}
}
#[test]
fn a_decoder_opened_from_a_rebuilt_ticket_produces_frames() {
let Some(corpus) = support::Corpus::new() else {
return;
};
let path = corpus.multi_track_mkv();
let mut input = ffmpeg_next::format::input(&path).expect("open multi.mkv");
let video = input
.streams()
.best(ffmpeg_next::media::Type::Video)
.expect("multi.mkv has video");
let audio = input
.streams()
.best(ffmpeg_next::media::Type::Audio)
.expect("multi.mkv has audio");
let (video_index, audio_index) = (video.index(), audio.index());
let video_tb = timebase_of(&video);
let audio_tb = timebase_of(&audio);
let video_ticket = CodecTicket::mirror(&video.parameters(), video_index, usize::MAX)
.expect("mirror the video parameters");
let audio_ticket = CodecTicket::mirror(&audio.parameters(), audio_index, usize::MAX)
.expect("mirror the audio parameters");
assert!(
!video_ticket.extradata().is_empty(),
"libx264 wrote no extradata — this lane would prove nothing",
);
assert!(
!audio_ticket.extradata().is_empty(),
"the AAC encoder wrote no AudioSpecificConfig — this lane would prove nothing",
);
let mut video_decoder = FfmpegOwnedVideoStreamDecoder::open(
video_ticket
.rebuild()
.expect("rebuild the video parameters"),
video_tb,
DecoderLimits::default(),
)
.expect("open a video decoder from the rebuilt ticket");
let mut audio_decoder = FfmpegOwnedAudioStreamDecoder::open(
audio_ticket
.rebuild()
.expect("rebuild the audio parameters"),
audio_tb,
DecoderLimits::default(),
)
.expect("open an audio decoder from the rebuilt ticket");
let mut video_frame: OwnedVideoFrame = empty_owned_video_frame();
let mut audio_frame: OwnedAudioFrame = empty_owned_audio_frame();
let (mut video_frames, mut audio_frames) = (0usize, 0usize);
for (stream, av_packet) in input.packets() {
if video_frames > 0 && audio_frames > 0 {
break;
}
let index = stream.index();
if index == video_index && video_frames == 0 {
let Some(packet) =
owned_video_packet_from_ffmpeg_in(&av_packet, video_tb, PacketLimits::default())
.expect("a wrappable video payload")
else {
continue;
};
if video_decoder.send_packet(&packet).expect("send video") != Sent::Accepted {
continue;
}
while let Received::Frame = video_decoder
.receive_frame(&mut video_frame)
.expect("video")
{
assert!(video_frame.width() > 0 && video_frame.height() > 0);
video_frames += 1;
}
} else if index == audio_index && audio_frames == 0 {
let Some(packet) =
owned_audio_packet_from_ffmpeg_in(&av_packet, audio_tb, PacketLimits::default())
.expect("a wrappable audio payload")
else {
continue;
};
if audio_decoder.send_packet(&packet).expect("send audio") != Sent::Accepted {
continue;
}
while let Received::Frame = audio_decoder
.receive_frame(&mut audio_frame)
.expect("audio")
{
assert!(audio_frame.sample_rate() > 0 && audio_frame.nb_samples() > 0);
audio_frames += 1;
}
}
}
assert!(
video_frames > 0,
"H.264 decoded nothing through the rebuilt ticket",
);
assert!(
audio_frames > 0,
"AAC decoded nothing through the rebuilt ticket",
);
}
fn timebase_of(stream: &ffmpeg_next::format::stream::Stream<'_>) -> Timebase {
let tb = stream.time_base();
Timebase::new(
tb.numerator(),
core::num::NonZeroI32::new(tb.denominator().max(1)).expect("a non-zero denominator"),
)
}
#[test]
fn a_custom_channel_map_survives_the_round_trip() {
support::init_ffmpeg();
let mut parameters = Parameters::new();
unsafe {
let par = parameters.as_mut_ptr();
let count = 3usize;
let map = av_mallocz(count * core::mem::size_of::<AVChannelCustom>()).cast::<AVChannelCustom>();
assert!(!map.is_null(), "av_mallocz the channel map");
for (index, (id, name)) in [(0i32, b"FL\0"), (1, b"FR\0"), (2, b"FC\0")]
.into_iter()
.enumerate()
{
let entry = map.add(index);
core::ptr::write_unaligned(core::ptr::addr_of_mut!((*entry).id).cast::<i32>(), id);
let mut bytes = [0u8; 16];
bytes[..name.len()].copy_from_slice(name);
core::ptr::write_unaligned(
core::ptr::addr_of_mut!((*entry).name).cast::<[u8; 16]>(),
bytes,
);
}
core::ptr::write_unaligned(
core::ptr::addr_of_mut!((*par).ch_layout.order).cast::<i32>(),
AVChannelOrder::AV_CHANNEL_ORDER_CUSTOM as i32,
);
(*par).ch_layout.nb_channels = count as i32;
(*par).ch_layout.u.map = map;
}
let ticket = assert_round_trip(¶meters, "a custom channel map");
let layout = ticket.ch_layout();
assert_eq!(
layout.order(),
AVChannelOrder::AV_CHANNEL_ORDER_CUSTOM as i32
);
assert_eq!(layout.channels(), 3);
assert_eq!(layout.map().len(), 3);
assert_eq!(layout.map()[2].id(), 2);
assert_eq!(&layout.map()[2].name_bytes()[..3], b"FC\0");
assert_eq!(layout.mask(), 0, "a pointer leaked into the mask seat");
}
#[test]
fn a_custom_layout_without_a_map_is_refused() {
support::init_ffmpeg();
let mut parameters = Parameters::new();
unsafe {
let par = parameters.as_mut_ptr();
core::ptr::write_unaligned(
core::ptr::addr_of_mut!((*par).ch_layout.order).cast::<i32>(),
AVChannelOrder::AV_CHANNEL_ORDER_CUSTOM as i32,
);
(*par).ch_layout.nb_channels = 3;
}
match CodecTicket::mirror(¶meters, 4, usize::MAX) {
Err(DemuxError::ParametersChannelMap(ref p)) => {
assert_eq!(p.stream_index(), 4);
assert_eq!(p.channels(), 3);
}
Err(other) => panic!("expected ParametersChannelMap, got {other:?}"),
Ok(_) => panic!("a custom layout with a null map was mirrored"),
}
let mut empty = Parameters::new();
unsafe {
core::ptr::write_unaligned(
core::ptr::addr_of_mut!((*empty.as_mut_ptr()).ch_layout.order).cast::<i32>(),
AVChannelOrder::AV_CHANNEL_ORDER_CUSTOM as i32,
);
}
assert!(matches!(
CodecTicket::mirror(&empty, 0, usize::MAX),
Err(DemuxError::ParametersChannelMap(_)),
));
let mut negative = Parameters::new();
unsafe {
let par = negative.as_mut_ptr();
core::ptr::write_unaligned(
core::ptr::addr_of_mut!((*par).ch_layout.order).cast::<i32>(),
AVChannelOrder::AV_CHANNEL_ORDER_CUSTOM as i32,
);
(*par).ch_layout.nb_channels = -1;
}
match CodecTicket::mirror(&negative, 7, usize::MAX) {
Err(DemuxError::ParametersTooLarge(ref p)) => assert_eq!(p.stream_index(), 7),
Err(other) => panic!("expected ParametersTooLarge, got {other:?}"),
Ok(_) => panic!("a custom layout declaring -1 channels was mirrored"),
}
}
#[test]
fn a_rebuilt_custom_layout_declares_exactly_its_map() {
support::init_ffmpeg();
let mut parameters = Parameters::new();
unsafe {
let par = parameters.as_mut_ptr();
let count = 5usize;
let map = av_mallocz(count * core::mem::size_of::<AVChannelCustom>()).cast::<AVChannelCustom>();
assert!(!map.is_null(), "av_mallocz the channel map");
for index in 0..count {
core::ptr::write_unaligned(
core::ptr::addr_of_mut!((*map.add(index)).id).cast::<i32>(),
index as i32,
);
}
core::ptr::write_unaligned(
core::ptr::addr_of_mut!((*par).ch_layout.order).cast::<i32>(),
AVChannelOrder::AV_CHANNEL_ORDER_CUSTOM as i32,
);
(*par).ch_layout.nb_channels = count as i32;
(*par).ch_layout.u.map = map;
}
let ticket = assert_round_trip(¶meters, "a five-channel custom layout");
assert_eq!(ticket.ch_layout().map().len(), 5);
let rebuilt = ticket.rebuild().expect("rebuild");
unsafe {
let par = rebuilt.as_ptr();
assert_eq!((*par).ch_layout.nb_channels, 5);
assert!(!(*par).ch_layout.u.map.is_null());
}
}
#[test]
fn a_side_data_kind_the_bindings_cannot_name_survives() {
support::init_ffmpeg();
const UNNAMED: i32 = 0x7f00_0001;
let mut parameters = Parameters::new();
unsafe {
let par = parameters.as_mut_ptr();
let array = av_mallocz(2 * core::mem::size_of::<AVPacketSideData>()).cast::<AVPacketSideData>();
assert!(!array.is_null(), "av_mallocz the descriptor array");
for (index, (kind, len, fill)) in [(UNNAMED, 37usize, 0x5Au8), (0x1234_5678, 0, 0)]
.into_iter()
.enumerate()
{
let entry = array.add(index);
core::ptr::write_unaligned(core::ptr::addr_of_mut!((*entry).type_).cast::<i32>(), kind);
if len > 0 {
let payload = av_mallocz(len).cast::<u8>();
assert!(!payload.is_null(), "av_mallocz the payload");
core::ptr::write_bytes(payload, fill, len);
core::ptr::write_unaligned(core::ptr::addr_of_mut!((*entry).data), payload);
core::ptr::write_unaligned(core::ptr::addr_of_mut!((*entry).size), len);
}
}
(*par).coded_side_data = array;
(*par).nb_coded_side_data = 2;
}
let ticket = assert_round_trip(¶meters, "an unnamed side-data kind");
assert_eq!(ticket.coded_side_data().len(), 2);
assert_eq!(ticket.coded_side_data()[0].kind(), UNNAMED);
assert_eq!(ticket.coded_side_data()[0].data().len(), 37);
assert!(
ticket.coded_side_data()[0]
.data()
.iter()
.all(|&b| b == 0x5A)
);
assert_eq!(ticket.coded_side_data()[1].kind(), 0x1234_5678);
assert!(ticket.coded_side_data()[1].data().is_empty());
}
#[test]
fn a_layout_carrying_user_private_data_is_refused() {
support::init_ffmpeg();
let mut parameters = Parameters::new();
let mut anything = 0u8;
unsafe {
let par = parameters.as_mut_ptr();
(*par).ch_layout.nb_channels = 2;
(*par).ch_layout.u.mask = 3;
(*par).ch_layout.opaque = core::ptr::addr_of_mut!(anything).cast();
}
let refused = CodecTicket::mirror(¶meters, 5, usize::MAX);
match refused {
Err(DemuxError::ParametersOpaque(ref p)) => {
assert_eq!(p.stream_index(), 5);
assert_eq!(p.channel(), None, "the pointer was on the layout itself");
}
Err(other) => panic!("expected ParametersOpaque, got {other:?}"),
Ok(_) => panic!("a layout carrying a user pointer was mirrored"),
}
unsafe {
(*parameters.as_mut_ptr()).ch_layout.opaque = core::ptr::null_mut();
}
assert_round_trip(¶meters, "a mask layout");
}
#[test]
fn a_custom_channel_carrying_user_private_data_is_refused() {
support::init_ffmpeg();
let mut parameters = Parameters::new();
let mut anything = 0u8;
unsafe {
let par = parameters.as_mut_ptr();
let map = av_mallocz(2 * core::mem::size_of::<AVChannelCustom>()).cast::<AVChannelCustom>();
assert!(!map.is_null(), "av_mallocz the channel map");
core::ptr::write_unaligned(
core::ptr::addr_of_mut!((*map.add(1)).opaque),
core::ptr::addr_of_mut!(anything).cast(),
);
core::ptr::write_unaligned(
core::ptr::addr_of_mut!((*par).ch_layout.order).cast::<i32>(),
AVChannelOrder::AV_CHANNEL_ORDER_CUSTOM as i32,
);
(*par).ch_layout.nb_channels = 2;
(*par).ch_layout.u.map = map;
}
match CodecTicket::mirror(¶meters, 2, usize::MAX) {
Err(DemuxError::ParametersOpaque(ref p)) => {
assert_eq!(p.stream_index(), 2);
assert_eq!(p.channel(), Some(1), "the entry the pointer was on");
}
Err(other) => panic!("expected ParametersOpaque, got {other:?}"),
Ok(_) => panic!("a custom channel carrying a user pointer was mirrored"),
}
unsafe {
core::ptr::write_unaligned(
core::ptr::addr_of_mut!((*(*parameters.as_mut_ptr()).ch_layout.u.map.add(1)).opaque),
core::ptr::null_mut(),
);
}
}
fn iso_box(kind: &[u8; 4], body: &[u8]) -> Vec<u8> {
let mut out = Vec::with_capacity(8 + body.len());
out.extend_from_slice(
&u32::try_from(8 + body.len())
.expect("a small box")
.to_be_bytes(),
);
out.extend_from_slice(kind);
out.extend_from_slice(body);
out
}
fn matrix_bytes() -> Vec<u8> {
[0x0001_0000u32, 0, 0, 0, 0x0001_0000, 0, 0, 0, 0x4000_0000]
.iter()
.flat_map(|w| w.to_be_bytes())
.collect()
}
fn mint_mp4_with_icc_profile(icc: &[u8]) -> Vec<u8> {
let be32 = |v: u32| v.to_be_bytes().to_vec();
let be16 = |v: u16| v.to_be_bytes().to_vec();
let colr = iso_box(b"colr", &[b"prof".as_slice(), icc].concat());
let mut visual = Vec::new();
visual.extend_from_slice(&[0u8; 6]); visual.extend_from_slice(&be16(1)); visual.extend_from_slice(&[0u8; 16]); visual.extend_from_slice(&be16(16)); visual.extend_from_slice(&be16(16)); visual.extend_from_slice(&be32(0x0048_0000)); visual.extend_from_slice(&be32(0x0048_0000)); visual.extend_from_slice(&be32(0)); visual.extend_from_slice(&be16(1)); visual.push(4); visual.extend_from_slice(b"mint");
visual.extend_from_slice(&[0u8; 27]);
visual.extend_from_slice(&be16(24)); visual.extend_from_slice(&[0xFF, 0xFF]); assert_eq!(visual.len(), 78, "the VisualSampleEntry body is fixed-size");
visual.extend_from_slice(&colr);
let entry = iso_box(b"raw ", &visual);
let stbl = iso_box(
b"stbl",
&[
iso_box(b"stsd", &[vec![0; 4], be32(1), entry].concat()),
iso_box(b"stts", &[vec![0; 4], be32(0)].concat()),
iso_box(b"stsc", &[vec![0; 4], be32(0)].concat()),
iso_box(b"stsz", &[vec![0; 4], be32(0), be32(0)].concat()),
iso_box(b"stco", &[vec![0; 4], be32(0)].concat()),
]
.concat(),
);
let dinf = iso_box(
b"dinf",
&iso_box(
b"dref",
&[vec![0; 4], be32(1), iso_box(b"url ", &[0, 0, 0, 1])].concat(),
),
);
let minf = iso_box(
b"minf",
&[
iso_box(b"vmhd", &[vec![0, 0, 0, 1], vec![0; 8]].concat()),
dinf,
stbl,
]
.concat(),
);
let mdhd = iso_box(
b"mdhd",
&[
vec![0; 4],
be32(0),
be32(0),
be32(600),
be32(0),
be16(0x55c4), be16(0),
]
.concat(),
);
let hdlr = iso_box(
b"hdlr",
&[
vec![0; 8],
b"vide".to_vec(),
vec![0; 12],
b"mint\0".to_vec(),
]
.concat(),
);
let mdia = iso_box(b"mdia", &[mdhd, hdlr, minf].concat());
let tkhd = iso_box(
b"tkhd",
&[
vec![0, 0, 0, 3], be32(0),
be32(0),
be32(1), be32(0),
be32(0), vec![0; 8],
be16(0), be16(0), be16(0), be16(0), matrix_bytes(),
be32(16 << 16), be32(16 << 16), ]
.concat(),
);
let trak = iso_box(b"trak", &[tkhd, mdia].concat());
let mvhd = iso_box(
b"mvhd",
&[
vec![0; 4],
be32(0),
be32(0),
be32(600), be32(0), be32(0x0001_0000), be16(0x0100), vec![0; 2],
vec![0; 8],
matrix_bytes(),
vec![0; 24],
be32(2), ]
.concat(),
);
let moov = iso_box(b"moov", &[mvhd, trak].concat());
let ftyp = iso_box(
b"ftyp",
&[b"isom".as_slice(), &be32(512), b"isomiso2mp41"].concat(),
);
[ftyp, moov].concat()
}
fn icc_profile_bytes(len: usize) -> Vec<u8> {
assert!(len >= 128, "an ICC header is 128 bytes");
let mut icc: Vec<u8> = (0..len).map(|i| ((i * 7 + 3) & 0xFF) as u8).collect();
icc[..4].copy_from_slice(&u32::try_from(len).expect("a small profile").to_be_bytes());
icc[36..40].copy_from_slice(b"acsp");
icc
}
#[test]
fn a_minted_container_carries_its_icc_profile_through_the_ticket() {
support::init_ffmpeg();
let icc = icc_profile_bytes(3_072);
let dir = tempfile::tempdir().expect("temp dir");
let path = dir.path().join("icc.mp4");
std::fs::write(&path, mint_mp4_with_icc_profile(&icc)).expect("write the minted container");
let input = ffmpeg_next::format::input(&path).expect("libavformat opens the minted container");
let stream = input.streams().next().expect("the minted track");
let parameters = stream.parameters();
let populated = unsafe { (*parameters.as_ptr()).nb_coded_side_data };
assert_eq!(
populated, 1,
"the MOV demuxer did not route the minted `colr`/`prof` atom into `coded_side_data` — \
the fixture is wrong, or FFmpeg changed that road",
);
let demuxer = FfmpegOwnedDemuxer::open(&path).expect("open the minted container");
let track = demuxer.tracks().first().expect("one track");
let ticket = track.extra().ticket();
let entries = ticket.coded_side_data();
assert_eq!(entries.len(), 1, "the ticket lost the profile");
assert_eq!(
entries[0].kind(),
ffmpeg_next::ffi::AVPacketSideDataType::AV_PKT_DATA_ICC_PROFILE as i32,
"the type id did not survive",
);
assert_eq!(
entries[0].data(),
icc.as_slice(),
"the profile bytes differ"
);
assert!(
ticket.footprint_bytes() >= icc.len(),
"a {}-byte profile was charged {} bytes",
icc.len(),
ticket.footprint_bytes(),
);
let rebuilt = track.extra().clone_parameters().expect("rebuild");
let bad = unsafe { compare(parameters.as_ptr(), rebuilt.as_ptr()) };
assert!(
bad.is_empty(),
"the minted container's row is not the file:\n {}",
bad.join("\n "),
);
}
#[test]
fn an_oversized_minted_profile_is_refused_at_open() {
support::init_ffmpeg();
let icc = icc_profile_bytes(512 * 1024);
let dir = tempfile::tempdir().expect("temp dir");
let path = dir.path().join("fat-icc.mp4");
std::fs::write(&path, mint_mp4_with_icc_profile(&icc)).expect("write the minted container");
let limits = DemuxLimits::default().with_max_codec_parameter_bytes(64 * 1024);
match FfmpegOwnedDemuxer::open_with(&path, limits) {
Err(DemuxError::ParametersTooLarge(ref p)) => {
assert_eq!(p.stream_index(), 0);
assert!(p.bytes() >= icc.len(), "charged {} bytes", p.bytes());
assert_eq!(p.limit(), 64 * 1024);
}
Err(other) => panic!("expected ParametersTooLarge, got {other:?}"),
Ok(_) => panic!("a 512 KiB ICC profile passed a 64 KiB ceiling"),
}
let demuxer = FfmpegOwnedDemuxer::open(&path).expect("the default ceiling admits a real profile");
assert_eq!(
demuxer.tracks()[0].extra().ticket().coded_side_data()[0].data(),
icc.as_slice(),
);
}
#[test]
fn every_scalar_seat_is_written_back() {
support::init_ffmpeg();
let mut parameters = Parameters::new();
unsafe {
let par = parameters.as_mut_ptr();
core::ptr::write_unaligned(core::ptr::addr_of_mut!((*par).codec_type).cast::<i32>(), 0);
core::ptr::write_unaligned(core::ptr::addr_of_mut!((*par).codec_id).cast::<i32>(), 27);
(*par).codec_tag = 0x3462_6461;
(*par).format = 12;
(*par).bit_rate = 5_000_000;
(*par).bits_per_coded_sample = 8;
(*par).bits_per_raw_sample = 10;
(*par).profile = 100;
(*par).level = 41;
(*par).width = 1920;
(*par).height = 1080;
(*par).sample_aspect_ratio.num = 4;
(*par).sample_aspect_ratio.den = 3;
(*par).framerate.num = 24_000;
(*par).framerate.den = 1_001;
core::ptr::write_unaligned(core::ptr::addr_of_mut!((*par).field_order).cast::<i32>(), 2);
core::ptr::write_unaligned(core::ptr::addr_of_mut!((*par).color_range).cast::<i32>(), 2);
core::ptr::write_unaligned(
core::ptr::addr_of_mut!((*par).color_primaries).cast::<i32>(),
9,
);
core::ptr::write_unaligned(core::ptr::addr_of_mut!((*par).color_trc).cast::<i32>(), 16);
core::ptr::write_unaligned(core::ptr::addr_of_mut!((*par).color_space).cast::<i32>(), 9);
core::ptr::write_unaligned(
core::ptr::addr_of_mut!((*par).chroma_location).cast::<i32>(),
2,
);
(*par).video_delay = 3;
core::ptr::write_unaligned(core::ptr::addr_of_mut!((*par).alpha_mode).cast::<i32>(), 1);
(*par).ch_layout.nb_channels = 6;
(*par).ch_layout.u.mask = 0x3f;
(*par).sample_rate = 48_000;
(*par).block_align = 4;
(*par).frame_size = 1_024;
(*par).initial_padding = 1_024;
(*par).trailing_padding = 512;
(*par).seek_preroll = 30;
}
let ticket = assert_round_trip(¶meters, "every scalar seat");
assert_eq!(ticket.codec_id(), 27);
assert_eq!(ticket.codec_tag(), 0x3462_6461);
assert_eq!(ticket.format(), 12);
assert_eq!(ticket.bit_rate(), 5_000_000);
assert_eq!(ticket.bits_per_coded_sample(), 8);
assert_eq!(ticket.bits_per_raw_sample(), 10);
assert_eq!(ticket.profile(), 100);
assert_eq!(ticket.level(), 41);
assert_eq!((ticket.width(), ticket.height()), (1920, 1080));
assert_eq!(
(
ticket.sample_aspect_ratio().num(),
ticket.sample_aspect_ratio().den()
),
(4, 3),
);
assert_eq!(
(ticket.framerate().num(), ticket.framerate().den()),
(24_000, 1_001)
);
assert_eq!(ticket.field_order(), 2);
assert_eq!(ticket.color_range(), 2);
assert_eq!(ticket.color_primaries(), 9);
assert_eq!(ticket.color_trc(), 16);
assert_eq!(ticket.color_space(), 9);
assert_eq!(ticket.chroma_location(), 2);
assert_eq!(ticket.video_delay(), 3);
assert_eq!(ticket.alpha_mode(), 1);
assert_eq!(ticket.ch_layout().channels(), 6);
assert_eq!(ticket.ch_layout().mask(), 0x3f);
assert_eq!(ticket.sample_rate(), 48_000);
assert_eq!(ticket.block_align(), 4);
assert_eq!(ticket.frame_size(), 1_024);
assert_eq!(ticket.initial_padding(), 1_024);
assert_eq!(ticket.trailing_padding(), 512);
assert_eq!(ticket.seek_preroll(), 30);
assert_eq!(ticket.codec_type(), 0);
}