mod support;
use mediadecode::demuxer::{DemuxedPacket, Demuxer, TrackKind};
use mediadecode_ffmpeg::{
DemuxError, DemuxLimits, FfmpegDemuxer, FfmpegOwnedDemuxer, PacketBufferError,
};
use support::Corpus;
fn drain_owned(path: &std::path::Path) -> Vec<(usize, TrackKind, Vec<u8>)> {
let mut demuxer = FfmpegOwnedDemuxer::open(path).expect("open");
let kinds: Vec<TrackKind> = demuxer.tracks().iter().map(|t| t.kind()).collect();
let mut out = Vec::new();
while let Some(packet) = demuxer.next_packet().expect("read") {
let (track, bytes) = match packet {
DemuxedPacket::Video(p) => (p.track(), p.packet().data().as_ref().to_vec()),
DemuxedPacket::Audio(p) => (p.track(), p.packet().data().as_ref().to_vec()),
DemuxedPacket::Subtitle(p) => (p.track(), p.packet().data().as_ref().to_vec()),
DemuxedPacket::Data(p) => (p.track(), p.packet().data().as_ref().to_vec()),
DemuxedPacket::Attachment(p) => (p.track(), p.packet().data().as_ref().to_vec()),
};
let index = track.get();
out.push((index, kinds[index], bytes));
}
out
}
fn drain_view(path: &std::path::Path) -> Vec<(usize, TrackKind, Vec<u8>)> {
let mut demuxer = FfmpegDemuxer::open(path).expect("open");
let kinds: Vec<TrackKind> = demuxer.tracks().iter().map(|t| t.kind()).collect();
let mut out = Vec::new();
while let Some(packet) = demuxer.next_packet().expect("read") {
let (track, bytes) = match packet {
DemuxedPacket::Video(p) => (p.track(), p.packet().data().as_ref().to_vec()),
DemuxedPacket::Audio(p) => (p.track(), p.packet().data().as_ref().to_vec()),
DemuxedPacket::Subtitle(p) => (p.track(), p.packet().data().as_ref().to_vec()),
DemuxedPacket::Data(p) => (p.track(), p.packet().data().as_ref().to_vec()),
DemuxedPacket::Attachment(p) => (p.track(), p.packet().data().as_ref().to_vec()),
};
let index = track.get();
out.push((index, kinds[index], bytes));
}
out
}
#[test]
fn a_demuxed_view_packet_windows_libavformats_own_allocation() {
let Some(corpus) = Corpus::new() else {
return;
};
support::init_ffmpeg();
let path = corpus.multi_track_mkv();
let mut demuxer = FfmpegDemuxer::open(&path).expect("open");
let mut windows = 0usize;
while let Some(packet) = demuxer.next_packet().expect("read") {
let DemuxedPacket::Video(p) = packet else {
continue;
};
let carrier = p.packet().data();
if carrier.is_empty() {
continue;
}
let (base, size) = unsafe {
let buf = carrier.as_av_buffer_ref();
((*buf).data as usize, (*buf).size)
};
let exported = carrier.as_ref().as_ptr() as usize;
assert!(
exported >= base && exported + carrier.len() <= base + size,
"the view escaped its buffer",
);
assert!(
size > carrier.len(),
"a {}-byte payload in a {}-byte buffer is a copy, not a window",
carrier.len(),
size,
);
assert_eq!(
exported,
base + carrier.offset(),
"the view's offset must be where it says it is",
);
let twin = carrier.clone();
assert!(
twin.ptr_eq(carrier),
"clone must bump the refcount, not copy"
);
windows += 1;
}
assert!(
windows > 0,
"the fixture delivered no video packets to prove anything with"
);
}
#[test]
fn both_lanes_carry_the_same_bytes() {
let Some(corpus) = Corpus::new() else {
return;
};
support::init_ffmpeg();
for path in [corpus.multi_track_mkv(), corpus.cover_art_mp3()] {
let owned = drain_owned(&path);
let view = drain_view(&path);
assert_eq!(
owned.len(),
view.len(),
"{}: the two lanes delivered different packet counts",
path.display(),
);
for (i, (o, v)) in owned.iter().zip(view.iter()).enumerate() {
assert_eq!(
o.0,
v.0,
"{}: packet {i} came from a different track",
path.display()
);
assert_eq!(o.1, v.1, "{}: packet {i} changed kind", path.display());
assert_eq!(
o.2,
v.2,
"{}: packet {i} differs between lanes ({} vs {} bytes)",
path.display(),
o.2.len(),
v.2.len(),
);
}
assert!(
!owned.is_empty(),
"{}: nothing was delivered",
path.display()
);
}
}
#[test]
fn the_budgets_fire_identically_on_both_lanes() {
let Some(corpus) = Corpus::new() else {
return;
};
support::init_ffmpeg();
let path = corpus.cover_art_mp3();
let starved = DemuxLimits::new().with_max_attachment_bytes(16);
let owned = FfmpegOwnedDemuxer::open_with(&path, starved);
let view = FfmpegDemuxer::open_with(&path, starved);
match (owned, view) {
(Err(DemuxError::AttachmentTooLarge(a)), Err(DemuxError::AttachmentTooLarge(b))) => {
assert_eq!(
a.limit(),
b.limit(),
"the two lanes reported different ceilings"
);
assert_eq!(
a.bytes(),
b.bytes(),
"the two lanes measured different payloads"
);
}
(o, v) => panic!(
"the lanes disagreed: owned={:?} view={:?}",
o.err().map(|e| e.to_string()),
v.err().map(|e| e.to_string()),
),
}
assert!(FfmpegOwnedDemuxer::open_with(&path, DemuxLimits::new()).is_ok());
assert!(FfmpegDemuxer::open_with(&path, DemuxLimits::new()).is_ok());
}
#[test]
fn a_trusted_payload_is_uncarriable_on_both_lanes() {
use ffmpeg_next::packet::Mut;
use mediadecode_ffmpeg::{
PacketLimits,
boundary::{owned_video_packet_from_ffmpeg_in, video_packet_from_ffmpeg_in},
};
support::init_ffmpeg();
let mut packet = ffmpeg_next::Packet::copy(&[1u8, 2, 3, 4]);
unsafe {
(*packet.as_mut_ptr()).flags =
ffmpeg_next::ffi::AV_PKT_FLAG_KEY | ffmpeg_next::ffi::AV_PKT_FLAG_TRUSTED;
}
let tb = mediadecode::Timebase::default();
assert!(matches!(
owned_video_packet_from_ffmpeg_in(&packet, tb, PacketLimits::default()),
Err(PacketBufferError::TrustedPayload(_)),
));
assert!(matches!(
video_packet_from_ffmpeg_in(packet.clone(), tb, PacketLimits::default()),
Err(PacketBufferError::TrustedPayload(_)),
));
assert!(matches!(
video_packet_from_ffmpeg_in(packet, tb, PacketLimits::default()),
Err(PacketBufferError::TrustedPayload(_)),
));
}
#[test]
fn a_packet_handed_to_a_caller_never_shares_its_carrier() {
let Some(corpus) = Corpus::new() else {
return;
};
support::init_ffmpeg();
use ffmpeg_next::packet::Ref;
use mediadecode_ffmpeg::{PacketLimits, boundary::ffmpeg_packet_from_video_packet};
let path = corpus.multi_track_mkv();
let mut demuxer = FfmpegDemuxer::open(&path).expect("open");
let mut checked = 0usize;
while let Some(packet) = demuxer.next_packet().expect("read") {
let DemuxedPacket::Video(p) = packet else {
continue;
};
let carrier = p.packet().data().clone();
if carrier.is_empty() {
continue;
}
let rebuilt =
ffmpeg_packet_from_video_packet(p.packet(), PacketLimits::default()).expect("rebuilt");
let (rebuilt_data, carrier_data) = unsafe {
(
(*rebuilt.as_ptr()).data as usize,
carrier.as_ref().as_ptr() as usize,
)
};
assert_ne!(
rebuilt_data, carrier_data,
"a packet a caller holds must own its bytes — sharing them makes \
`data_mut` an aliasing `&mut` over a live carrier",
);
assert_eq!(rebuilt.data().unwrap_or(&[]), carrier.as_ref());
checked += 1;
if checked >= 3 {
break;
}
}
assert!(
checked > 0,
"no video packet was available to prove the send leg with"
);
}
unsafe fn view_video_frame(
frame: &ffmpeg_next::frame::Video,
time_base: mediadecode::Timebase,
limits: mediadecode_ffmpeg::FrameLimits,
) -> Result<mediadecode_ffmpeg::VideoFrame, mediadecode_ffmpeg::convert::ConvertError> {
unsafe { mediadecode_ffmpeg::convert::av_frame_to_video_frame(frame.as_ptr(), time_base, limits) }
}
unsafe fn view_audio_frame(
frame: &ffmpeg_next::frame::Audio,
time_base: mediadecode::Timebase,
limits: mediadecode_ffmpeg::FrameLimits,
) -> Result<mediadecode_ffmpeg::AudioFrame, mediadecode_ffmpeg::convert::ConvertError> {
unsafe { mediadecode_ffmpeg::convert::av_frame_to_audio_frame(frame.as_ptr(), time_base, limits) }
}
fn gray_frame(width: u32, height: u32) -> ffmpeg_next::frame::Video {
support::init_ffmpeg();
let mut frame = ffmpeg_next::frame::Video::new(ffmpeg_next::format::Pixel::GRAY8, width, height);
let linesize = frame.stride(0);
let data = frame.data_mut(0);
for y in 0..height as usize {
for x in 0..width as usize {
data[y * linesize + x] = (y as u8).wrapping_mul(31).wrapping_add(x as u8);
}
}
frame
}
fn backing_range(frame: &ffmpeg_next::frame::Video) -> (usize, usize) {
unsafe {
let raw = frame.as_ptr();
let buf = (*raw).buf[0];
assert!(!buf.is_null(), "a built frame has a refcounted buffer");
let start = (*buf).data as usize;
(start, start + (*buf).size)
}
}
fn rows_of<B: AsRef<[u8]>>(
plane: &mediadecode::frame::Plane<B>,
width: usize,
rows: usize,
) -> Vec<u8> {
let stride = plane.stride() as usize;
let bytes = plane.data_ref().as_ref();
let mut out = Vec::with_capacity(width * rows);
for y in 0..rows {
out.extend_from_slice(&bytes[y * stride..y * stride + width]);
}
out
}
#[test]
fn a_tight_video_plane_is_a_window_into_the_decoders_own_buffer() {
use mediadecode_ffmpeg::FrameLimits;
let frame = gray_frame(128, 8);
assert_eq!(frame.stride(0), 128, "the premise: a tight plane");
let (start, end) = backing_range(&frame);
let out = unsafe {
view_video_frame(
&frame,
mediadecode::Timebase::default(),
FrameLimits::default(),
)
}
.expect("a GRAY8 frame converts");
let plane = &out.planes()[0];
let ptr = plane.data_ref().as_ref().as_ptr() as usize;
assert!(
ptr >= start && ptr + plane.data_ref().as_ref().len() <= end,
"a tight view plane must point inside the AVFrame's own buffer \
(plane {ptr:#x}..{:#x}, buffer {start:#x}..{end:#x})",
ptr + plane.data_ref().as_ref().len(),
);
assert_eq!(
plane.stride(),
128,
"a shared plane carries the decoder's own stride",
);
assert_eq!(plane.data_ref().as_ref().len(), 128 * 8);
}
#[test]
fn a_padded_video_plane_is_compacted_on_both_lanes() {
use mediadecode_ffmpeg::{FrameLimits, convert};
let frame = gray_frame(100, 6);
let linesize = frame.stride(0);
assert!(linesize > 100, "the premise: a padded plane");
let (start, end) = backing_range(&frame);
let time_base = mediadecode::Timebase::default();
let viewed = unsafe { view_video_frame(&frame, time_base, FrameLimits::default()) }
.expect("converts on the view lane");
let owned = convert::video_frame_from(&frame, time_base, FrameLimits::default())
.expect("converts on the owned lane");
let view_plane = &viewed.planes()[0];
let ptr = view_plane.data_ref().as_ref().as_ptr() as usize;
assert!(
ptr < start || ptr >= end,
"a padded plane must be copied, not shared: the gaps between rows \
are allocator memory nothing initialised",
);
assert_eq!(
view_plane.stride(),
100,
"a compacted plane arrives with row_bytes as its stride",
);
assert_eq!(view_plane.data_ref().as_ref().len(), 100 * 6);
assert_eq!(
view_plane.data_ref().as_ref(),
owned.planes()[0].data_ref().as_ref(),
"the lanes disagree about a padded plane's contents",
);
assert_eq!(view_plane.stride(), owned.planes()[0].stride());
}
#[test]
fn the_two_lanes_carry_the_same_video_content() {
use mediadecode_ffmpeg::{FrameLimits, convert};
for (width, height) in [(128u32, 8u32), (100, 6)] {
let frame = gray_frame(width, height);
let time_base = mediadecode::Timebase::default();
let viewed =
unsafe { view_video_frame(&frame, time_base, FrameLimits::default()) }.expect("view lane");
let owned =
convert::video_frame_from(&frame, time_base, FrameLimits::default()).expect("owned lane");
assert_eq!(
rows_of(&viewed.planes()[0], width as usize, height as usize),
rows_of(&owned.planes()[0], width as usize, height as usize),
"the lanes disagree about the pixels of a {width}x{height} frame",
);
assert_eq!(viewed.width(), owned.width());
assert_eq!(viewed.height(), owned.height());
assert_eq!(viewed.pixel_format(), owned.pixel_format());
}
}
#[test]
fn the_frame_budget_fires_identically_on_both_lanes() {
use mediadecode_ffmpeg::{FrameLimits, convert};
let frame = gray_frame(128, 8);
let time_base = mediadecode::Timebase::default();
let tight = FrameLimits::new().with_max_frame_bytes(16);
let viewed = unsafe { view_video_frame(&frame, time_base, tight) };
let owned = convert::video_frame_from(&frame, time_base, tight);
match (viewed, owned) {
(Err(v), Err(o)) => assert_eq!(
core::mem::discriminant(&v),
core::mem::discriminant(&o),
"the lanes refused for different reasons: {v:?} vs {o:?}",
),
(v, o) => panic!(
"a budget must fire on both lanes: view={:?} owned={:?}",
v.map(|_| ()),
o.map(|_| ()),
),
}
let roomy = FrameLimits::new().with_max_frame_bytes(1 << 20);
assert!(unsafe { view_video_frame(&frame, time_base, roomy) }.is_ok());
assert!(convert::video_frame_from(&frame, time_base, roomy).is_ok());
}
#[test]
fn an_audio_view_plane_stops_at_exactly_the_valid_bytes() {
use mediadecode_ffmpeg::FrameLimits;
support::init_ffmpeg();
const SAMPLES: usize = 100;
let frame = ffmpeg_next::frame::Audio::new(
ffmpeg_next::format::Sample::I16(ffmpeg_next::format::sample::Type::Planar),
SAMPLES,
ffmpeg_next::ChannelLayout::STEREO,
);
let (linesize, ranges) = unsafe {
let raw = frame.as_ptr();
let ranges: Vec<(usize, usize)> = (0..2)
.map(|index| {
let buf = (*raw).buf[index];
assert!(!buf.is_null(), "a built audio frame has refcounted planes");
let start = (*buf).data as usize;
(start, start + (*buf).size)
})
.collect();
((*raw).linesize[0] as usize, ranges)
};
let valid = SAMPLES * 2; assert!(
linesize > valid,
"the premise: `av_samples_get_buffer_size` pads the plane past its \
samples (linesize {linesize}, valid {valid})",
);
let out = unsafe {
view_audio_frame(
&frame,
mediadecode::Timebase::default(),
FrameLimits::default(),
)
}
.expect("a planar s16 frame converts");
assert_eq!(
out.plane_count(),
2,
"planar declares one plane per channel"
);
for (index, (start, end)) in ranges.into_iter().enumerate() {
let plane = &out.planes()[index];
assert_eq!(
plane.data_ref().as_ref().len(),
valid,
"plane {index} carries the padding the decoder never wrote",
);
assert_eq!(plane.stride() as usize, valid);
let ptr = plane.data_ref().as_ref().as_ptr() as usize;
assert!(
ptr >= start && ptr + valid <= end,
"plane {index} is a copy, not a window into the frame's buffer",
);
let capacity = unsafe { (*plane.data_ref().as_av_buffer_ref()).size };
assert!(
capacity > valid,
"plane {index} should look into a padded allocation ({capacity} bytes) and expose only its {valid} valid ones",
);
}
assert!(
!out.planes()[0]
.data_ref()
.ptr_eq(out.planes()[1].data_ref()),
"planar audio planes are separate allocations",
);
}
#[test]
fn the_two_lanes_carry_the_same_audio_samples() {
use mediadecode_ffmpeg::{FrameLimits, convert};
support::init_ffmpeg();
const SAMPLES: usize = 100;
let mut frame = ffmpeg_next::frame::Audio::new(
ffmpeg_next::format::Sample::I16(ffmpeg_next::format::sample::Type::Planar),
SAMPLES,
ffmpeg_next::ChannelLayout::STEREO,
);
for plane in 0..2usize {
let data = frame.data_mut(plane);
for (index, byte) in data.iter_mut().take(SAMPLES * 2).enumerate() {
*byte = (index as u8).wrapping_mul(7).wrapping_add(plane as u8);
}
}
let time_base = mediadecode::Timebase::default();
let viewed =
unsafe { view_audio_frame(&frame, time_base, FrameLimits::default()) }.expect("view lane");
let owned =
convert::audio_frame_from(&frame, time_base, FrameLimits::default()).expect("owned lane");
assert_eq!(viewed.nb_samples(), owned.nb_samples());
assert_eq!(viewed.plane_count(), owned.plane_count());
for index in 0..viewed.plane_count() as usize {
assert_eq!(
viewed.planes()[index].data_ref().as_ref(),
owned.planes()[index].data_ref().as_ref(),
"the lanes disagree about plane {index}",
);
}
}
#[test]
#[cfg(feature = "resample")]
fn a_resampled_view_frame_shares_the_resamplers_output_buffer() {
use mediadecode::resampler::AudioResampler;
use mediadecode_ffmpeg::{FfmpegResampler, FrameLimits, ResampleSpec};
support::init_ffmpeg();
const SAMPLES: u32 = 1_024;
let source = ResampleSpec::new(
48_000,
ffmpeg_next::format::Sample::I16(ffmpeg_next::format::sample::Type::Packed),
ffmpeg_next::ChannelLayout::STEREO,
);
let target = ResampleSpec::new(
48_000,
ffmpeg_next::format::Sample::F32(ffmpeg_next::format::sample::Type::Planar),
ffmpeg_next::ChannelLayout::STEREO,
);
let mut resampler =
FfmpegResampler::new(source, target, FrameLimits::default()).expect("open resampler");
let plane =
mediadecode_ffmpeg::FfmpegBuffer::copy_from_slice(&vec![0u8; SAMPLES as usize * 2 * 2])
.expect("a plane to feed");
let planes = std::array::from_fn(|index| {
mediadecode::frame::Plane::new(
if index == 0 {
plane.clone()
} else {
mediadecode_ffmpeg::FfmpegBuffer::empty()
},
0,
)
});
let input = mediadecode_ffmpeg::AudioFrame::new(
48_000,
SAMPLES,
2,
mediadecode_ffmpeg::SampleFormat::S16,
mediadecode_ffmpeg::channel_layout_description_from_ffmpeg(&ffmpeg_next::ChannelLayout::STEREO),
planes,
1,
Default::default(),
);
resampler.send_frame(&input).expect("send");
resampler.send_eof().expect("eof");
let mut dst = mediadecode_ffmpeg::empty_audio_frame();
let mut proved = 0usize;
while resampler.receive_frame(&mut dst).is_ok() {
if dst.nb_samples() == 0 || dst.plane_count() < 2 {
continue;
}
let first = &dst.planes()[0];
assert_eq!(
first.data_ref().as_ref().len(),
dst.nb_samples() as usize * 4,
"f32 planar: a plane is exactly the samples produced, never the \
capacity that was allocated for them",
);
let capacity = unsafe { (*first.data_ref().as_av_buffer_ref()).size };
assert!(
capacity > first.data_ref().as_ref().len(),
"a resampled view plane must look into the output frame's own \
allocation (capacity {capacity}, committed {})",
first.data_ref().as_ref().len(),
);
proved += 1;
}
assert!(proved > 0, "the resampler produced nothing to prove with");
}
#[test]
fn the_frame_families_carry_the_auto_traits_their_lanes_promise() {
use mediadecode_ffmpeg::{
AudioFrame, ImageFrame, OwnedAudioFrame, OwnedImageFrame, OwnedSubtitleFrame, OwnedVideoFrame,
SubtitleFrame, VideoFrame,
};
const fn assert_send<T: Send>() {}
const fn assert_sync<T: Sync>() {}
assert_send::<VideoFrame>();
assert_send::<AudioFrame>();
assert_send::<SubtitleFrame>();
assert_send::<ImageFrame>();
assert_send::<OwnedVideoFrame>();
assert_send::<OwnedAudioFrame>();
assert_send::<OwnedSubtitleFrame>();
assert_send::<OwnedImageFrame>();
assert_sync::<OwnedVideoFrame>();
assert_sync::<OwnedAudioFrame>();
assert_sync::<OwnedSubtitleFrame>();
assert_sync::<OwnedImageFrame>();
struct Probe<T>(core::marker::PhantomData<T>);
trait IsSync {
fn sync_status() -> bool;
}
impl<T: Sync> IsSync for Probe<T> {
fn sync_status() -> bool {
true
}
}
impl Probe<VideoFrame> {
#[allow(dead_code)]
fn sync_status() -> bool {
false
}
}
impl Probe<AudioFrame> {
#[allow(dead_code)]
fn sync_status() -> bool {
false
}
}
assert!(
!Probe::<VideoFrame>::sync_status(),
"a view video frame must not be Sync",
);
assert!(
!Probe::<AudioFrame>::sync_status(),
"a view audio frame must not be Sync",
);
assert!(
Probe::<OwnedVideoFrame>::sync_status(),
"an owned video frame must stay Sync",
);
}
#[test]
fn a_side_data_only_packet_reaches_a_view_decoder_without_a_buffer_behind_it() {
use mediadecode::decoder::AudioStreamDecoder;
use mediadecode_ffmpeg::{
AudioPacket, DecoderLimits, FfmpegAudioStreamDecoder, FfmpegBuffer, empty_audio_frame,
extras::AudioPacketExtra,
};
let Some(corpus) = Corpus::new() else {
return;
};
support::init_ffmpeg();
let path = corpus.multi_track_mkv();
let mut demuxer = FfmpegDemuxer::open(&path).expect("open");
let track = demuxer
.tracks()
.iter()
.position(|t| t.kind() == TrackKind::Audio)
.expect("an audio track");
let info = &demuxer.tracks()[track];
let mut decoder = FfmpegAudioStreamDecoder::open(
info.extra().clone_parameters().expect("parameters"),
info.timebase(),
DecoderLimits::default(),
)
.expect("open decoder");
let payload_less = AudioPacket::new(FfmpegBuffer::empty(), AudioPacketExtra::new(track as i32));
let _ = decoder.send_packet(&payload_less);
let mut frame = empty_audio_frame();
let mut samples = 0u64;
while let Some(packet) = demuxer.next_packet().expect("pull") {
let DemuxedPacket::Audio(p) = packet else {
continue;
};
decoder.send_packet(p.packet()).expect("send a real packet");
while decoder.receive_frame(&mut frame).is_ok() {
samples += u64::from(frame.nb_samples());
}
if samples > 0 {
break;
}
}
assert!(
samples > 0,
"the decoder must still decode after a payload-less packet",
);
}
#[test]
fn the_borrowed_doors_copy_and_the_consuming_ones_share() {
use ffmpeg_next::packet::Ref;
use mediadecode_ffmpeg::{
PacketLimits,
boundary::{owned_video_packet_from_ffmpeg_in, video_packet_from_ffmpeg_in},
};
let Some(corpus) = Corpus::new() else {
return;
};
support::init_ffmpeg();
let path = corpus.multi_track_mkv();
let mut input = ffmpeg_next::format::input(&path).expect("open");
let mut proved = 0usize;
let packets: Vec<ffmpeg_next::Packet> = input
.packets()
.map(|(_, packet)| packet)
.filter(|packet| packet.data().is_some_and(|d| !d.is_empty()))
.take(3)
.collect();
for packet in packets {
let source = unsafe { (*packet.as_ptr()).data as usize };
let tb = mediadecode::Timebase::default();
let owned = owned_video_packet_from_ffmpeg_in(&packet, tb, PacketLimits::default())
.expect("carried")
.expect("a payload");
assert_ne!(
owned.data().as_ref().as_ptr() as usize,
source,
"the borrowing door must copy — the caller still holds the packet",
);
let expected = owned.data().as_ref().to_vec();
let viewed = video_packet_from_ffmpeg_in(packet, tb, PacketLimits::default())
.expect("carried")
.expect("a payload");
assert_eq!(
viewed.data().as_ref().as_ptr() as usize,
source,
"the consuming door must share the buffer it was handed",
);
assert_eq!(viewed.data().as_ref(), expected.as_slice());
proved += 1;
}
assert!(
proved > 0,
"no packet was available to prove the split with"
);
}
#[test]
fn a_tight_plane_copies_on_the_borrowed_road_and_shares_on_the_consuming_one() {
use mediadecode_ffmpeg::{FrameLimits, convert};
let frame = gray_frame(128, 8);
assert_eq!(frame.stride(0), 128, "the premise: a tight plane");
let (start, end) = backing_range(&frame);
let time_base = mediadecode::Timebase::default();
let borrowed = convert::video_frame_from(&frame, time_base, FrameLimits::default())
.expect("the owned lane converts");
let ptr = borrowed.planes()[0].data_ref().as_ref().as_ptr() as usize;
assert!(
ptr < start || ptr >= end,
"a borrowed conversion must copy, even where the geometry would \
have allowed a window",
);
let viewed = unsafe { view_video_frame(&frame, time_base, FrameLimits::default()) }
.expect("the view lane converts");
let shared = viewed.planes()[0].data_ref().as_ref().as_ptr() as usize;
assert!(
shared >= start && shared + viewed.planes()[0].data_ref().as_ref().len() <= end,
"the consuming road still shares",
);
assert_eq!(
borrowed.planes()[0].data_ref().as_ref(),
viewed.planes()[0].data_ref().as_ref(),
);
}
#[test]
fn a_container_held_cover_picture_is_still_carried() {
let Some(corpus) = Corpus::new() else {
return;
};
support::init_ffmpeg();
let path = corpus.cover_art_mp3();
let mut demuxer = FfmpegDemuxer::open(&path).expect("a cover-art container opens");
let mut covers = 0usize;
while let Some(packet) = demuxer.next_packet().expect("read") {
if let DemuxedPacket::Attachment(attachment) = packet {
assert!(
!attachment.packet().data().as_ref().is_empty(),
"a cover picture must arrive with its bytes",
);
covers += 1;
}
}
assert!(covers > 0, "the fixture must carry a cover picture");
}
fn payload_references(packet: &ffmpeg_next::Packet) -> i32 {
use ffmpeg_next::packet::Ref;
unsafe {
let buf = (*packet.as_ptr()).buf;
if buf.is_null() {
return -1;
}
ffmpeg_next::ffi::av_buffer_get_ref_count(buf)
}
}
#[test]
fn a_queue_backed_demuxer_delivers_on_both_lanes() {
use mediadecode_ffmpeg::FfmpegOwnedDemuxer;
let Some(corpus) = Corpus::new() else {
return;
};
support::init_ffmpeg();
let path = corpus.subrip();
{
let mut raw = ffmpeg_next::format::input(&path).expect("open");
let mut seen = 0usize;
for (_stream, packet) in raw.packets() {
if packet.size() == 0 {
continue;
}
assert!(
payload_references(&packet) > 1,
"the premise this test exists for: a queue-backed demuxer \
delivers a second reference to a packet it keeps",
);
seen += 1;
if seen >= 2 {
break;
}
}
assert!(seen > 0, "the fixture must deliver cues");
}
let mut viewed = FfmpegDemuxer::open(&path).expect("open on the view lane");
let mut view_cues: Vec<Vec<u8>> = Vec::new();
while let Some(packet) = viewed.next_packet().expect("read") {
if let DemuxedPacket::Subtitle(p) = packet {
view_cues.push(p.packet().data().as_ref().to_vec());
}
}
let mut owned = FfmpegOwnedDemuxer::open(&path).expect("open on the owned lane");
let mut owned_cues: Vec<Vec<u8>> = Vec::new();
while let Some(packet) = owned.next_packet().expect("read") {
if let DemuxedPacket::Subtitle(p) = packet {
owned_cues.push(p.packet().data().as_ref().to_vec());
}
}
assert!(!view_cues.is_empty(), "the view lane must deliver the cues");
assert_eq!(view_cues, owned_cues, "the lanes must agree on the cues");
assert!(
view_cues.iter().any(|c| c == b"first cue"),
"the cues must be the ones the fixture wrote, got {:?}",
view_cues
.iter()
.map(|c| String::from_utf8_lossy(c).into_owned())
.collect::<Vec<_>>(),
);
let mut again = FfmpegDemuxer::open(&path).expect("reopen");
let seek = again.seek(mediadecode::Timestamp::new(
0,
mediadecode::Timebase::new(1, std::num::NonZeroI32::new(1_000_000).expect("nonzero")),
));
assert!(
seek.is_err(),
"if this container ever becomes seekable, this lane should assert \
the cues after the seek instead of the refusal before it",
);
let mut after = 0usize;
while let Some(packet) = again
.next_packet()
.expect("a refused seek must not poison the session")
{
if let DemuxedPacket::Subtitle(p) = packet {
assert!(!p.packet().data().as_ref().is_empty());
after += 1;
}
}
assert_eq!(
after,
view_cues.len(),
"a refused seek must leave the session delivering every cue",
);
}
fn in_subprocess(test_name: &str, body: impl FnOnce()) {
const CHILD: &str = "MEDIADECODE_FFMPEG_VIEW_FAULT_CHILD";
if std::env::var(CHILD).as_deref() == Ok(test_name) {
body();
return;
}
let exe = std::env::current_exe().expect("the test binary");
let output = std::process::Command::new(exe)
.args(["--exact", test_name, "--nocapture", "--test-threads=1"])
.env(CHILD, test_name)
.output()
.expect("spawning the child");
let stdout = String::from_utf8_lossy(&output.stdout);
assert_eq!(
output.status.code(),
Some(0),
"the child running `{test_name}` did not exit cleanly ({:?})\n{stdout}\n{}",
output.status,
String::from_utf8_lossy(&output.stderr),
);
assert!(
stdout.contains("1 passed"),
"the child ran no test — is `{test_name}` still the name?\n{stdout}",
);
}
fn cap_allocations(max: usize) {
unsafe { ffmpeg_next::ffi::av_max_alloc(max) };
}
fn drain_cues(demuxer: &mut FfmpegDemuxer) -> Vec<Vec<u8>> {
let mut out = Vec::new();
while let Some(packet) = demuxer.next_packet().expect("read") {
if let DemuxedPacket::Subtitle(p) = packet {
out.push(p.packet().data().as_ref().to_vec());
}
}
out
}
#[test]
fn a_failed_copy_parks_the_packet_instead_of_losing_it() {
let Some(corpus) = Corpus::new() else {
return;
};
support::init_ffmpeg();
let path = corpus.subrip_bulky();
in_subprocess(
"a_failed_copy_parks_the_packet_instead_of_losing_it",
move || {
let expected = drain_cues(&mut FfmpegDemuxer::open(&path).expect("open"));
assert!(expected.len() >= 2, "the fixture must have cues to lose");
let mut demuxer = FfmpegDemuxer::open(&path).expect("open");
cap_allocations(1024);
let first = demuxer.next_packet().map(|p| p.map(|_| ()));
assert!(
matches!(
first,
Err(mediadecode_ffmpeg::DemuxError::PacketBuffer(ref e))
if matches!(e.source(), PacketBufferError::CaptureFailed(_))
),
"expected a transient capture refusal, got {first:?}",
);
let again = demuxer.next_packet().map(|p| p.map(|_| ()));
assert!(
matches!(
again,
Err(mediadecode_ffmpeg::DemuxError::PacketBuffer(ref e))
if matches!(e.source(), PacketBufferError::CaptureFailed(_))
),
"a parked packet must be retried, got {again:?}",
);
cap_allocations(i32::MAX as usize);
let recovered = drain_cues(&mut demuxer);
assert_eq!(
recovered, expected,
"a transient refusal must cost no packet at all",
);
},
);
}
fn payload_of(
packet: DemuxedPacket<mediadecode_ffmpeg::Ffmpeg, mediadecode_ffmpeg::FfmpegBuffer>,
) -> (usize, Vec<u8>) {
match packet {
DemuxedPacket::Video(p) => (p.track().get(), p.packet().data().as_ref().to_vec()),
DemuxedPacket::Audio(p) => (p.track().get(), p.packet().data().as_ref().to_vec()),
DemuxedPacket::Subtitle(p) => (p.track().get(), p.packet().data().as_ref().to_vec()),
DemuxedPacket::Data(p) => (p.track().get(), p.packet().data().as_ref().to_vec()),
DemuxedPacket::Attachment(p) => (p.track().get(), p.packet().data().as_ref().to_vec()),
}
}
fn drain_payloads(demuxer: &mut FfmpegDemuxer) -> Vec<(usize, Vec<u8>)> {
let mut out = Vec::new();
while let Some(packet) = demuxer.next_packet().expect("read") {
out.push(payload_of(packet));
}
out
}
#[test]
fn a_failed_view_capture_parks_the_packet_instead_of_losing_it() {
let Some(corpus) = Corpus::new() else {
return;
};
support::init_ffmpeg();
let path = corpus.multi_track_mkv();
in_subprocess(
"a_failed_view_capture_parks_the_packet_instead_of_losing_it",
move || {
let expected = drain_payloads(&mut FfmpegDemuxer::open(&path).expect("open"));
assert!(expected.len() >= 2, "the fixture must have packets to lose");
let mut demuxer = FfmpegDemuxer::open(&path).expect("open");
let mut got: Vec<(usize, Vec<u8>)> = Vec::new();
cap_allocations(16);
let refused = loop {
match demuxer.next_packet() {
Ok(Some(packet)) => got.push(payload_of(packet)),
Ok(None) => break None,
Err(e) => break Some(e),
}
};
cap_allocations(i32::MAX as usize);
let refused = refused.expect("the ceiling must refuse a capture");
assert!(
matches!(
refused,
mediadecode_ffmpeg::DemuxError::PacketBuffer(ref e)
if matches!(e.source(), PacketBufferError::CaptureFailed(_))
),
"expected a transient capture refusal, got {refused:?}",
);
cap_allocations(16);
let again = demuxer.next_packet().map(|p| p.map(|_| ()));
cap_allocations(i32::MAX as usize);
assert!(
matches!(
again,
Err(mediadecode_ffmpeg::DemuxError::PacketBuffer(ref e))
if matches!(e.source(), PacketBufferError::CaptureFailed(_))
),
"a parked packet must be retried, got {again:?}",
);
got.extend(drain_payloads(&mut demuxer));
assert_eq!(
got, expected,
"a transient refusal must cost no packet at all",
);
},
);
}
#[test]
fn a_refused_seek_keeps_the_parked_packet() {
let Some(corpus) = Corpus::new() else {
return;
};
support::init_ffmpeg();
let path = corpus.subrip_bulky();
in_subprocess("a_refused_seek_keeps_the_parked_packet", move || {
let expected = drain_cues(&mut FfmpegDemuxer::open(&path).expect("open"));
assert!(!expected.is_empty(), "the fixture must have cues");
let mut demuxer = FfmpegDemuxer::open(&path).expect("open");
cap_allocations(1024);
let refused = demuxer.next_packet().map(|p| p.map(|_| ()));
cap_allocations(i32::MAX as usize);
assert!(refused.is_err(), "the ceiling must park a cue");
let seek = demuxer.seek(mediadecode::Timestamp::new(
0,
mediadecode::Timebase::new(1, std::num::NonZeroI32::new(1_000_000).expect("nonzero")),
));
assert!(seek.is_err(), "the fixture's refusal is the premise here");
let recovered = drain_cues(&mut demuxer);
assert_eq!(
recovered, expected,
"a refused seek must cost no packet either",
);
});
}
#[test]
fn a_failed_frame_conversion_parks_the_frame_instead_of_losing_it() {
use mediadecode::decoder::AudioStreamDecoder;
use mediadecode_ffmpeg::{DecoderLimits, FfmpegAudioStreamDecoder, empty_audio_frame};
let Some(corpus) = Corpus::new() else {
return;
};
support::init_ffmpeg();
let path = corpus.multi_track_mkv();
in_subprocess(
"a_failed_frame_conversion_parks_the_frame_instead_of_losing_it",
move || {
let open = |path: &std::path::Path| {
let demuxer = FfmpegDemuxer::open(path).expect("open");
let track = demuxer
.tracks()
.iter()
.position(|t| t.kind() == TrackKind::Audio)
.expect("an audio track");
let info = &demuxer.tracks()[track];
let decoder = FfmpegAudioStreamDecoder::open(
info.extra().clone_parameters().expect("parameters"),
info.timebase(),
DecoderLimits::default(),
)
.expect("open decoder");
(demuxer, decoder, track)
};
let (mut demuxer, mut decoder, track) = open(&path);
let mut frame = empty_audio_frame();
let mut expected: Option<(u32, Vec<u8>)> = None;
'reference: while let Some(packet) = demuxer.next_packet().expect("pull") {
let DemuxedPacket::Audio(p) = packet else {
continue;
};
if p.track().get() != track {
continue;
}
decoder.send_packet(p.packet()).expect("send");
if decoder.receive_frame(&mut frame).is_ok() {
expected = Some((
frame.nb_samples(),
frame.planes()[0].data_ref().as_ref().to_vec(),
));
break 'reference;
}
}
let expected = expected.expect("the fixture must decode a frame");
let (mut demuxer, mut decoder, track) = open(&path);
let mut frame = empty_audio_frame();
let mut refused = None;
'capped: while let Some(packet) = demuxer.next_packet().expect("pull") {
let DemuxedPacket::Audio(p) = packet else {
continue;
};
if p.track().get() != track {
continue;
}
decoder.send_packet(p.packet()).expect("send");
cap_allocations(16);
let got = decoder.receive_frame(&mut frame);
cap_allocations(i32::MAX as usize);
match got {
Ok(()) => panic!("the ceiling should have refused the carrier"),
Err(e) if format!("{e:?}").contains("CarrierAllocFailed") => {
refused = Some(e);
break 'capped;
}
Err(_) => {}
}
}
assert!(
refused.is_some(),
"the ceiling must refuse a frame carrier to test the seat",
);
decoder.receive_frame(&mut frame).expect("the parked frame");
assert_eq!(
(
frame.nb_samples(),
frame.planes()[0].data_ref().as_ref().to_vec()
),
expected,
"a transient refusal must cost no frame at all",
);
},
);
}
#[test]
fn a_failed_video_frame_conversion_parks_the_frame_instead_of_losing_it() {
use mediadecode::decoder::VideoStreamDecoder;
use mediadecode_ffmpeg::{DecoderLimits, FfmpegVideoStreamDecoder, empty_video_frame};
let Some(corpus) = Corpus::new() else {
return;
};
support::init_ffmpeg();
let path = corpus.software_only_video();
in_subprocess(
"a_failed_video_frame_conversion_parks_the_frame_instead_of_losing_it",
move || {
let open = |path: &std::path::Path| {
let demuxer = FfmpegDemuxer::open(path).expect("open");
let track = demuxer
.tracks()
.iter()
.position(|t| t.kind() == TrackKind::Video)
.expect("a video track");
let info = &demuxer.tracks()[track];
let decoder = FfmpegVideoStreamDecoder::open(
info.extra().clone_parameters().expect("parameters"),
info.timebase(),
DecoderLimits::default(),
)
.expect("open decoder");
(demuxer, decoder, track)
};
let (mut demuxer, mut decoder, track) = open(&path);
let mut frame = empty_video_frame();
let mut expected: Option<Vec<u8>> = None;
'reference: while let Some(packet) = demuxer.next_packet().expect("pull") {
let DemuxedPacket::Video(p) = packet else {
continue;
};
if p.track().get() != track {
continue;
}
decoder.send_packet(p.packet()).expect("send");
if decoder.receive_frame(&mut frame).is_ok() {
expected = Some(frame.planes()[0].data_ref().as_ref().to_vec());
break 'reference;
}
}
let expected = expected.expect("the fixture must decode a frame");
let (mut demuxer, mut decoder, track) = open(&path);
let mut frame = empty_video_frame();
let mut refused = false;
'capped: while let Some(packet) = demuxer.next_packet().expect("pull") {
let DemuxedPacket::Video(p) = packet else {
continue;
};
if p.track().get() != track {
continue;
}
decoder.send_packet(p.packet()).expect("send");
cap_allocations(16);
let got = decoder.receive_frame(&mut frame);
cap_allocations(i32::MAX as usize);
match got {
Ok(()) => panic!("the ceiling should have refused the carrier"),
Err(e) if format!("{e:?}").contains("CarrierAllocFailed") => {
refused = true;
break 'capped;
}
Err(_) => {}
}
}
assert!(
refused,
"the ceiling must refuse a frame carrier to test the seat",
);
decoder.receive_frame(&mut frame).expect("the parked frame");
assert_eq!(
frame.planes()[0].data_ref().as_ref().to_vec(),
expected,
"a transient refusal must cost no frame at all",
);
},
);
}
#[test]
fn a_failed_cue_conversion_parks_the_cue_instead_of_losing_it() {
use mediadecode::decoder::SubtitleDecoder;
use mediadecode_ffmpeg::{DecoderLimits, FfmpegSubtitleStreamDecoder, empty_subtitle_frame};
let Some(corpus) = Corpus::new() else {
return;
};
support::init_ffmpeg();
let path = corpus.subrip_bulky();
in_subprocess(
"a_failed_cue_conversion_parks_the_cue_instead_of_losing_it",
move || {
let open = |path: &std::path::Path| {
let demuxer = FfmpegDemuxer::open(path).expect("open");
let track = demuxer
.tracks()
.iter()
.position(|t| t.kind() == TrackKind::Subtitle)
.expect("a subtitle track");
let info = &demuxer.tracks()[track];
let decoder = FfmpegSubtitleStreamDecoder::open(
info.extra().clone_parameters().expect("parameters"),
info.timebase(),
DecoderLimits::default(),
)
.expect("open decoder");
(demuxer, decoder, track)
};
let first_cue = |demuxer: &mut FfmpegDemuxer, track: usize| loop {
let packet = demuxer.next_packet().expect("pull").expect("a cue");
if let DemuxedPacket::Subtitle(p) = packet
&& p.track().get() == track
{
return p.into_packet();
}
};
let (mut demuxer, mut decoder, track) = open(&path);
let packet = first_cue(&mut demuxer, track);
decoder.send_packet(&packet).expect("send");
let mut frame = empty_subtitle_frame();
decoder.receive_frame(&mut frame).expect("the cue");
let expected = format!("{:?}", frame.payload());
assert!(expected.contains("Text"), "the fixture must decode to text");
let (mut demuxer, mut decoder, track) = open(&path);
let packet = first_cue(&mut demuxer, track);
decoder.send_packet(&packet).expect("send");
let mut frame = empty_subtitle_frame();
cap_allocations(1024);
let refused = decoder.receive_frame(&mut frame);
cap_allocations(i32::MAX as usize);
assert!(
matches!(&refused, Err(e) if format!("{e:?}").contains("CarrierAllocFailed")),
"the ceiling must refuse the cue's carrier, got {refused:?}",
);
decoder.receive_frame(&mut frame).expect("the parked cue");
assert_eq!(
format!("{:?}", frame.payload()),
expected,
"a transient refusal must cost no cue at all",
);
},
);
}