use super::*;
use ffmpeg_next::codec::packet::Mut as PacketMut;
use ffmpeg_next::ffi::AVCodecID;
#[test]
fn no_codec_for_unknown_id() {
let err = Error::NoCodec(0);
assert!(format!("{err}").contains("no decoder"));
}
#[test]
fn videodecoder_is_send() {
_assert_send();
}
#[test]
fn open_rejects_null_parameters() {
let null_params = unsafe { codec::Parameters::wrap(std::ptr::null_mut(), None) };
match VideoDecoder::open(null_params) {
Ok(_) => panic!("open should fail on null parameters"),
Err(Error::Ffmpeg(ffmpeg_next::Error::Other { errno })) => {
assert_eq!(errno, libc::ENOMEM, "expected ENOMEM, got {errno}");
}
Err(other) => panic!("expected Ffmpeg(Other {{ ENOMEM }}), got {other:?}"),
}
}
#[test]
fn open_with_rejects_null_parameters() {
let null_params = unsafe { codec::Parameters::wrap(std::ptr::null_mut(), None) };
match VideoDecoder::open_with(null_params, Backend::VideoToolbox) {
Ok(_) => panic!("open_with should fail on null parameters"),
Err(Error::Ffmpeg(ffmpeg_next::Error::Other { errno })) => {
assert_eq!(errno, libc::ENOMEM, "expected ENOMEM, got {errno}");
}
Err(other) => panic!("expected Ffmpeg(Other {{ ENOMEM }}), got {other:?}"),
}
}
#[test]
fn packet_side_data_counts_against_probe_budget() {
use ffmpeg_next::ffi::{AVPacketSideDataType, av_packet_new_side_data};
const PAYLOAD_SIZE: usize = 16;
const SIDE_DATA_SIZE: usize = 1024 * 1024;
let mut packet = Packet::new(PAYLOAD_SIZE);
let p = unsafe {
av_packet_new_side_data(
packet.as_mut_ptr(),
AVPacketSideDataType::AV_PKT_DATA_NEW_EXTRADATA,
SIDE_DATA_SIZE,
)
};
assert!(!p.is_null(), "av_packet_new_side_data returned NULL");
assert_eq!(packet.size(), PAYLOAD_SIZE);
let side = packet_side_data_bytes(&packet, MAX_PROBE_PACKET_SIDE_DATA_ENTRIES);
assert!(
side >= SIDE_DATA_SIZE,
"side-data accounting must include the attached buffer; got {side}"
);
let total = packet.size().saturating_add(side);
assert!(
total >= PAYLOAD_SIZE + SIDE_DATA_SIZE,
"probe budget must charge payload + side data; got {total}"
);
}
#[test]
fn packet_side_data_is_zero_when_no_side_data() {
let packet = Packet::new(64);
assert_eq!(
packet_side_data_bytes(&packet, MAX_PROBE_PACKET_SIDE_DATA_ENTRIES),
0
);
assert_eq!(packet_side_data_count(&packet), 0);
}
#[test]
fn packet_side_data_bytes_charges_descriptor_overhead_for_zero_size_entries() {
use ffmpeg_next::ffi::{AVPacketSideDataType, av_packet_new_side_data};
let mut packet = Packet::new(0);
let p1 = unsafe {
av_packet_new_side_data(
packet.as_mut_ptr(),
AVPacketSideDataType::AV_PKT_DATA_NEW_EXTRADATA,
0,
)
};
let p2 = unsafe {
av_packet_new_side_data(
packet.as_mut_ptr(),
AVPacketSideDataType::AV_PKT_DATA_PALETTE,
0,
)
};
assert!(
!p1.is_null() && !p2.is_null(),
"av_packet_new_side_data NULL"
);
assert_eq!(packet_side_data_count(&packet), 2);
let bytes = packet_side_data_bytes(&packet, MAX_PROBE_PACKET_SIDE_DATA_ENTRIES);
assert!(
bytes >= 2 * SIDE_DATA_ENTRY_OVERHEAD,
"must charge descriptor overhead per entry even at zero payload; got {bytes}"
);
}
#[test]
fn packet_side_data_bytes_respects_max_entries_cap() {
use ffmpeg_next::ffi::{AVPacketSideDataType, av_packet_new_side_data};
let mut packet = Packet::new(0);
let types_and_sizes: [(AVPacketSideDataType, usize); 5] = [
(AVPacketSideDataType::AV_PKT_DATA_NEW_EXTRADATA, 100),
(AVPacketSideDataType::AV_PKT_DATA_PALETTE, 200),
(AVPacketSideDataType::AV_PKT_DATA_REPLAYGAIN, 300),
(AVPacketSideDataType::AV_PKT_DATA_DISPLAYMATRIX, 400),
(AVPacketSideDataType::AV_PKT_DATA_STEREO3D, 500),
];
for (ty, size) in types_and_sizes {
let p = unsafe { av_packet_new_side_data(packet.as_mut_ptr(), ty, size) };
assert!(!p.is_null(), "av_packet_new_side_data returned NULL");
}
assert_eq!(packet_side_data_count(&packet), 5);
let walked_2 = packet_side_data_bytes(&packet, 2);
let walked_5 = packet_side_data_bytes(&packet, 5);
assert_eq!(
walked_2,
2 * SIDE_DATA_ENTRY_OVERHEAD + 100 + 200,
"max_entries=2 must walk exactly the first two entries"
);
assert_eq!(
walked_5,
5 * SIDE_DATA_ENTRY_OVERHEAD + 100 + 200 + 300 + 400 + 500,
"max_entries=5 must walk all five entries"
);
assert_eq!(packet_side_data_bytes(&packet, 0), 0);
let walked_huge = packet_side_data_bytes(&packet, 1_000_000);
assert_eq!(walked_huge, walked_5);
}
#[test]
fn packet_side_data_count_reports_attached_entries() {
use ffmpeg_next::ffi::{AVPacketSideDataType, av_packet_new_side_data};
let mut packet = Packet::new(0);
let _p1 = unsafe {
av_packet_new_side_data(
packet.as_mut_ptr(),
AVPacketSideDataType::AV_PKT_DATA_NEW_EXTRADATA,
4,
)
};
let _p2 = unsafe {
av_packet_new_side_data(
packet.as_mut_ptr(),
AVPacketSideDataType::AV_PKT_DATA_PALETTE,
4,
)
};
assert_eq!(packet_side_data_count(&packet), 2);
}
#[test]
fn partial_build_state_drop_is_no_op_on_null_pointers() {
let _g = PartialBuildState {
hw_device_ref: ptr::null_mut(),
callback_state: ptr::null_mut(),
};
}
#[test]
fn partial_build_state_into_owned_disarms_and_returns_originals() {
use ffmpeg_next::ffi::{av_buffer_alloc, av_buffer_unref};
let hw_ptr = unsafe { av_buffer_alloc(64) };
assert!(!hw_ptr.is_null(), "av_buffer_alloc(64) returned NULL");
let cb_ptr = Box::into_raw(Box::new(CallbackState::new(u64::MAX)));
let g = PartialBuildState {
hw_device_ref: hw_ptr,
callback_state: cb_ptr,
};
let (hw_back, cb_back) = g.into_owned();
assert_eq!(
hw_back, hw_ptr,
"into_owned must return the original device ref"
);
assert_eq!(
cb_back, cb_ptr,
"into_owned must return the original callback box"
);
unsafe {
let mut hw = hw_back;
av_buffer_unref(&mut hw);
drop(Box::from_raw(cb_back));
}
}
#[test]
#[ignore = "requires HWDECODE_SAMPLE_VIDEO and a working hardware backend"]
fn a_packet_past_the_byte_ceiling_commits_the_candidate_that_takes_it() {
use ffmpeg_next::{format, media};
let path = std::env::var_os("HWDECODE_SAMPLE_VIDEO")
.expect("HWDECODE_SAMPLE_VIDEO must be set for this test");
ffmpeg_next::init().expect("ffmpeg init");
let mut input = format::input(&path).expect("open input");
let stream_index = input
.streams()
.best(media::Type::Video)
.expect("video stream")
.index();
let stream_params = input
.streams()
.best(media::Type::Video)
.expect("video stream")
.parameters();
let mut decoder = VideoDecoder::open(stream_params).expect("open decoder");
assert_eq!(
decoder.session_kind(),
SessionKind::Trial,
"probe must be active immediately after open"
);
{
let probe = decoder.probe_mut().expect("probe present");
probe.buffered_packets.push(Packet::new(8));
probe.fed = probe.buffered_packets.len();
probe.buffered_bytes = MAX_PROBE_PACKET_BYTES;
}
let mut taken = false;
for (s, packet) in input.packets() {
if s.index() != stream_index {
continue;
}
match unsafe { decoder.send_av_packet(&packet) } {
Ok(Sent::Accepted) => {
taken = true;
break;
}
other => panic!("the candidate takes the packet past the ceiling, got {other:?}"),
}
}
assert!(taken, "expected a video packet to send past the ceiling");
assert_eq!(
decoder.session_kind(),
SessionKind::Committed,
"the candidate that took the packet past the ceiling is committed"
);
assert!(
decoder.backlog().is_empty(),
"the candidate had been fed the whole history, so nothing is owed it"
);
}
#[test]
#[ignore = "requires HWDECODE_SAMPLE_VIDEO and a working hardware backend"]
fn probe_exhausted_returns_buffered_packets_to_caller() {
use ffmpeg_next::{format, media};
let path = std::env::var_os("HWDECODE_SAMPLE_VIDEO")
.expect("HWDECODE_SAMPLE_VIDEO must be set for this test");
ffmpeg_next::init().expect("ffmpeg init");
let input = format::input(&path).expect("open input");
let stream_params = input
.streams()
.best(media::Type::Video)
.expect("video stream")
.parameters();
let mut decoder = VideoDecoder::open(stream_params).expect("open decoder");
assert_eq!(
decoder.session_kind(),
SessionKind::Trial,
"probe must be active immediately after open"
);
let p1 = Packet::new(16);
let p2 = Packet::new(32);
{
let probe = decoder.probe_mut().expect("probe");
probe.buffered_packets.push(p1);
probe.buffered_packets.push(p2);
probe.remaining_backends.clear();
}
let result = decoder.advance_probe(BackendAnswer::of_call(Error::Ffmpeg(
ffmpeg_next::Error::InvalidData,
)));
match result {
Err(Error::ProbeExhausted(p)) => {
let attempts = p.attempts();
let unconsumed_packets = p.unconsumed_packets();
assert_eq!(
unconsumed_packets.len(),
2,
"buffered probe packets must be returned to the caller for SW fallback"
);
assert_eq!(unconsumed_packets[0].size(), 16);
assert_eq!(unconsumed_packets[1].size(), 32);
assert!(
!attempts.is_empty(),
"the active backend's failure should be in attempts"
);
}
other => panic!("expected ProbeExhausted, got {other:?}"),
}
assert_spent(
&mut decoder,
crate::Spent::ProbeExhausted,
SessionKind::Exhausted,
);
}
#[test]
#[ignore = "requires HWDECODE_SAMPLE_VIDEO and a working hardware backend"]
fn probe_exhausted_preserves_earlier_open_failures() {
use ffmpeg_next::{format, media};
let path = std::env::var_os("HWDECODE_SAMPLE_VIDEO")
.expect("HWDECODE_SAMPLE_VIDEO must be set for this test");
ffmpeg_next::init().expect("ffmpeg init");
let input = format::input(&path).expect("open input");
let stream_params = input
.streams()
.best(media::Type::Video)
.expect("video stream")
.parameters();
let mut decoder = VideoDecoder::open(stream_params).expect("open decoder");
let active_backend = decoder.backend();
let earlier_backend = match active_backend {
Backend::VideoToolbox => Backend::Vaapi,
Backend::Vaapi => Backend::Cuda,
Backend::Cuda => Backend::Vaapi,
Backend::D3d11va => Backend::Cuda,
};
let synthetic_earlier = Error::BackendUnsupportedByCodec(earlier_backend);
{
let probe = decoder.probe_mut().expect("probe present");
probe.attempts.push((
earlier_backend,
BackendAnswer::of_open(synthetic_earlier).into(),
));
probe.remaining_backends.clear(); }
let result = decoder.advance_probe(BackendAnswer::of_call(Error::Ffmpeg(
ffmpeg_next::Error::InvalidData,
)));
match result {
Err(Error::ProbeExhausted(p)) => {
let attempts = p.attempts();
assert_eq!(
attempts.len(),
2,
"ProbeExhausted must surface BOTH the seeded earlier failure \
and the active backend's runtime failure"
);
assert_eq!(
attempts[0].0, earlier_backend,
"earlier open failure must come first in probe order"
);
assert!(
matches!(*attempts[0].1, Error::BackendUnsupportedByCodec(_)),
"earlier failure must preserve its original error variant"
);
assert_eq!(
attempts[1].0, active_backend,
"active backend's runtime failure must come second"
);
assert!(
matches!(
*attempts[1].1,
Error::Ffmpeg(ffmpeg_next::Error::InvalidData)
),
"active backend's failure must preserve the synthetic InvalidData"
);
}
other => panic!("expected ProbeExhausted, got {other:?}"),
}
}
#[test]
fn the_transfer_judge_prices_the_pool_not_the_display() {
use super::judge_hw_transfer;
use crate::FrameLimits;
let frame = hw_frame_with_pool(100, 100, 8192, 8192);
let refusal = unsafe {
judge_hw_transfer(
frame.as_ptr(),
DownloadFormat::Picked(NV12),
FrameLimits::new().with_max_frame_bytes(16 * 1024 * 1024),
)
};
let err = refusal.expect_err("an 8192x8192 pool must not pass a 16 MiB ceiling");
assert!(
err.bytes() > 16 * 1024 * 1024,
"the refusal reports the pool's cost, got {}",
err.bytes(),
);
assert!(
err.bytes() > 100 * 100 * 16,
"the judge is still reading the display dims",
);
assert!(
unsafe {
judge_hw_transfer(
frame.as_ptr(),
DownloadFormat::Picked(NV12),
FrameLimits::new().with_max_frame_bytes(usize::MAX),
)
}
.is_ok(),
);
}
const NV12: i32 = ffmpeg_next::ffi::AVPixelFormat::AV_PIX_FMT_NV12 as i32;
#[test]
fn a_hardware_frame_with_an_unreadable_pool_fails_closed() {
use super::judge_hw_transfer;
use crate::FrameLimits;
let frame = hw_frame_with_pool(100, 100, 0, 0);
assert!(
unsafe {
judge_hw_transfer(
frame.as_ptr(),
DownloadFormat::Picked(NV12),
FrameLimits::new(),
)
}
.is_err(),
"an unreadable pool extent must fail closed",
);
let plain = ffmpeg_next::frame::Video::empty();
let format = unsafe { download_format_of(&plain) };
assert!(
matches!(format, Ok(DownloadFormat::Unpicked)),
"no format picked for a frame that is no hardware frame"
);
assert!(
unsafe { judge_hw_transfer(plain.as_ptr(), DownloadFormat::Unpicked, FrameLimits::new()) }
.is_ok()
);
}
fn hw_frame_with_pool(dw: i32, dh: i32, pw: i32, ph: i32) -> ffmpeg_next::frame::Video {
use ffmpeg_next::ffi;
let mut frame = ffmpeg_next::frame::Video::empty();
unsafe {
let mut dev: *mut ffi::AVBufferRef = core::ptr::null_mut();
let rc = ffi::av_hwdevice_ctx_create(
&mut dev,
ffi::AVHWDeviceType::AV_HWDEVICE_TYPE_VIDEOTOOLBOX,
core::ptr::null(),
core::ptr::null_mut(),
0,
);
assert_eq!(rc, 0, "videotoolbox device");
let ctx_ref = ffi::av_hwframe_ctx_alloc(dev);
assert!(!ctx_ref.is_null());
let ctx = (*ctx_ref).data as *mut ffi::AVHWFramesContext;
(*ctx).format = ffi::AVPixelFormat::AV_PIX_FMT_VIDEOTOOLBOX;
(*ctx).sw_format = ffi::AVPixelFormat::AV_PIX_FMT_NV12;
(*ctx).width = pw;
(*ctx).height = ph;
let p = frame.as_mut_ptr();
(*p).width = dw;
(*p).height = dh;
(*p).format = ffi::AVPixelFormat::AV_PIX_FMT_VIDEOTOOLBOX as i32;
(*p).hw_frames_ctx = ctx_ref;
ffi::av_buffer_unref(&mut dev);
}
frame
}
#[test]
fn a_side_data_allocation_failure_is_reported_not_absorbed() {
use mediaio_ffmpeg::test_support::{
cap_ffmpeg_allocations, in_subprocess, uncap_ffmpeg_allocations,
};
in_subprocess(
"decoder::tests::a_side_data_allocation_failure_is_reported_not_absorbed",
|| {
ffmpeg_next::init().expect("ffmpeg init");
let mut src = ffmpeg_next::frame::Video::empty();
let mut dst = ffmpeg_next::frame::Video::empty();
unsafe {
let sp = src.as_mut_ptr();
(*sp).width = 16;
(*sp).height = 16;
(*sp).format = ffmpeg_next::ffi::AVPixelFormat::AV_PIX_FMT_NV12 as i32;
let sd = ffmpeg_next::ffi::av_frame_new_side_data(
sp,
ffmpeg_next::ffi::AVFrameSideDataType::AV_FRAME_DATA_DISPLAYMATRIX,
36,
);
assert!(!sd.is_null(), "the source entry must exist to be copied");
}
unsafe { super::copy_frame_props_minimal(dst.as_mut_ptr(), src.as_ptr()) }
.expect("an uncapped copy succeeds");
assert_eq!(unsafe { (*dst.as_ptr()).nb_side_data }, 1);
let mut starved = ffmpeg_next::frame::Video::empty();
unsafe { cap_ffmpeg_allocations(1) };
let refused = unsafe { super::copy_frame_props_minimal(starved.as_mut_ptr(), src.as_ptr()) };
uncap_ffmpeg_allocations();
assert!(
refused.is_err(),
"a side-data allocation failure must not publish a partial frame",
);
},
);
}
#[test]
fn the_declination_reader_reports_then_clears() {
use super::ceiling_declination_of;
use crate::ffi::CallbackState;
let mut quiet = CallbackState::new(u64::MAX);
quiet.set_wanted(ffmpeg_next::ffi::AVPixelFormat::AV_PIX_FMT_VIDEOTOOLBOX);
assert!(unsafe { ceiling_declination_of(&quiet) }.is_none());
let mut declined = CallbackState::new(u64::MAX);
declined.set_wanted(ffmpeg_next::ffi::AVPixelFormat::AV_PIX_FMT_VIDEOTOOLBOX);
unsafe { crate::ffi::declare_ceiling_declined_for_test(&mut declined, 1920 * 1088, 1_048_576) };
match unsafe { ceiling_declination_of(&declined) } {
Some(Error::HwSurfaceTooLarge(p)) => {
assert_eq!(p.bytes(), 1920 * 1088);
assert_eq!(p.limit(), 1_048_576);
}
other => panic!("expected a named coded-surface refusal, got {other:?}"),
}
assert!(
unsafe { ceiling_declination_of(&declined) }.is_none(),
"the refusal must be consumed, not latched",
);
}
#[test]
fn the_judge_prices_the_picked_format_and_an_unpriceable_one_at_the_bound() {
ffmpeg_next::init().expect("ffmpeg init");
use crate::FrameLimits;
use crate::footprint::{video_frame_bytes, video_frame_bytes_upper_bound};
let (w, h) = (1920, 1088);
let cheap = video_frame_bytes(NV12, w, h).expect("NV12 prices");
let bound = video_frame_bytes_upper_bound(w, h).expect("a picture");
assert!(
bound > cheap * 5,
"the bound {bound} should dwarf the cheap format {cheap}",
);
const VT: i32 = ffmpeg_next::ffi::AVPixelFormat::AV_PIX_FMT_VIDEOTOOLBOX as i32;
assert!(
video_frame_bytes(VT, w, h).is_none(),
"the harness needs a genuinely unpriceable format",
);
let frame = hw_frame_with_pool(w, h, w, h);
let between = FrameLimits::new().with_max_frame_bytes(cheap + (bound - cheap) / 2);
unsafe {
assert!(
judge_hw_transfer(frame.as_ptr(), DownloadFormat::Picked(NV12), between).is_ok(),
"NV12, priced as it is"
);
let refused = judge_hw_transfer(frame.as_ptr(), DownloadFormat::Picked(VT), between)
.expect_err("the conservative bound");
assert_eq!(refused.bytes(), bound, "charged the bound");
}
}
#[test]
fn the_pool_judge_fails_closed_and_prices_conservatively() {
ffmpeg_next::init().expect("ffmpeg init");
use crate::footprint::{video_frame_bytes, video_frame_bytes_upper_bound};
assert!(
u64::MAX > u64::from(u32::MAX),
"the refusal must exceed any real budget"
);
for (w, h) in [(65, 65), (129, 129), (1920, 1088), (65_536, 1)] {
let bound = video_frame_bytes_upper_bound(w, h).expect("a picture");
let bare = (w as usize) * (h as usize) * 16;
assert!(
bound >= bare,
"{w}x{h}: bound {bound} below the bare multiply {bare} it replaces",
);
const NV12: i32 = ffmpeg_next::ffi::AVPixelFormat::AV_PIX_FMT_NV12 as i32;
const RGBAF32: i32 = ffmpeg_next::ffi::AVPixelFormat::AV_PIX_FMT_RGBAF32LE as i32;
for fmt in [NV12, RGBAF32] {
if let Some(priced) = video_frame_bytes(fmt, w, h) {
assert!(
bound >= priced,
"{w}x{h}: bound {bound} below {fmt} at {priced}"
);
}
}
}
}
#[test]
fn the_gates_read_every_errno_against_every_phase() {
use SessionPhase::{Auditioning, AuditioningPastEnd, Draining, Streaming};
let eagain = || {
Error::Ffmpeg(ffmpeg_next::Error::Other {
errno: ffmpeg_next::error::EAGAIN,
})
};
let eof = || Error::Ffmpeg(ffmpeg_next::Error::Eof);
for phase in [Streaming, Auditioning] {
assert_eq!(
receive_status(eagain(), phase).expect("back pressure"),
Received::NeedsInput,
"{phase:?}",
);
}
assert_eq!(
receive_status(eagain(), Draining).expect("a settled end"),
Received::Ended,
);
assert!(
matches!(
receive_status(eagain(), AuditioningPastEnd),
Err(Error::Ffmpeg(_))
),
"a candidate past the end that produced nothing must reach the probe road, \
not be answered as a protocol state",
);
for phase in [Streaming, Draining] {
assert_eq!(
receive_status(eof(), phase).expect("the end of a stream"),
Received::Ended,
"{phase:?}",
);
}
for phase in [Auditioning, AuditioningPastEnd] {
assert!(
matches!(receive_status(eof(), phase), Err(Error::Ffmpeg(_))),
"{phase:?}: a candidate draining to EOF is a candidate failing",
);
}
for phase in [Streaming, Auditioning] {
assert_eq!(
send_status(eagain(), phase).expect("back pressure"),
Sent::MustDrain,
"{phase:?}",
);
}
for phase in [Draining, AuditioningPastEnd] {
assert!(
matches!(send_status(eagain(), phase), Err(Error::Ffmpeg(_))),
"{phase:?}: back pressure must not be promised past a recorded end",
);
}
for phase in [Streaming, Draining, Auditioning, AuditioningPastEnd] {
assert!(
matches!(
send_status(eof(), phase),
Err(Error::Ffmpeg(ffmpeg_next::Error::Eof))
),
"{phase:?}: a send after end-of-stream must stay a fault, unlaundered",
);
}
for phase in [Streaming, Draining, Auditioning, AuditioningPastEnd] {
for gate in [
send_status(Error::Ffmpeg(ffmpeg_next::Error::InvalidData), phase).err(),
receive_status(Error::Ffmpeg(ffmpeg_next::Error::InvalidData), phase).err(),
] {
assert!(
matches!(gate, Some(Error::Ffmpeg(ffmpeg_next::Error::InvalidData))),
"{phase:?}",
);
}
}
}
#[test]
fn the_raw_decoder_refuses_a_send_after_end_of_stream() {
ffmpeg_next::init().expect("ffmpeg init");
let mut parameters = ffmpeg_next::codec::Parameters::new();
unsafe {
let raw = parameters.as_mut_ptr();
(*raw).codec_type = ffmpeg_next::ffi::AVMediaType::AVMEDIA_TYPE_VIDEO;
(*raw).codec_id = ffmpeg_next::ffi::AVCodecID::AV_CODEC_ID_MPEG4;
}
let mut dec = VideoDecoder::from_software_for_test(
parameters,
crate::limits::DecoderLimits::default(),
false,
)
.expect("a software-backed raw decoder");
let mut frame = crate::Frame::empty().expect("frame slot");
assert_eq!(
dec
.receive_frame(&mut frame)
.expect("an empty decoder is not a fault"),
Received::NeedsInput,
);
assert_eq!(dec.send_eof().expect("no fault"), Sent::Accepted);
loop {
match dec
.receive_frame(&mut frame)
.expect("no fault while draining")
{
Received::Produced => {}
Received::NeedsInput => panic!("a raw decoder at EOF asked for input"),
Received::Ended => break,
}
}
for _ in 0..2 {
let packet = crate::boundary::try_packet_copy(&[0u8; 16]).expect("a submittable packet");
let sent = unsafe { dec.send_av_packet(&packet) };
assert!(
matches!(sent, Err(Error::Ffmpeg(ffmpeg_next::Error::Eof))),
"a packet after end-of-stream must be libavcodec's refusal, not back \
pressure and not silent acceptance; got {sent:?}",
);
let eof_again = dec.send_eof();
assert!(
matches!(eof_again, Err(Error::Ffmpeg(ffmpeg_next::Error::Eof))),
"a repeated end-of-stream must be the same refusal; got {eof_again:?}",
);
}
assert_eq!(
dec
.receive_frame(&mut frame)
.expect("no fault past the end"),
Received::Ended,
);
}
#[cfg(test)]
fn auditioning_decoder() -> VideoDecoder {
ffmpeg_next::init().expect("ffmpeg init");
let mut parameters = ffmpeg_next::codec::Parameters::new();
unsafe {
let raw = parameters.as_mut_ptr();
(*raw).codec_type = ffmpeg_next::ffi::AVMediaType::AVMEDIA_TYPE_VIDEO;
(*raw).codec_id = ffmpeg_next::ffi::AVCodecID::AV_CODEC_ID_MPEG4;
}
VideoDecoder::from_software_for_test(
parameters,
crate::limits::DecoderLimits::default(),
true,
)
.expect("a software-backed candidate on trial")
}
#[test]
fn a_candidate_that_asks_for_input_past_the_end_fails_the_probe() {
let mut dec = auditioning_decoder();
assert_eq!(dec.phase(), SessionPhase::Auditioning);
dec.set_end(End::Taken);
assert_eq!(
dec.phase(),
SessionPhase::AuditioningPastEnd,
"a recorded end must move a session on trial into the phase that names it",
);
let mut frame = crate::Frame::empty().expect("frame slot");
match dec.receive_frame(&mut frame) {
Ok(Received::NeedsInput) => panic!(
"asked the caller for input on a stream that is already over — and the \
send gates refuse, so nothing can satisfy it",
),
Ok(Received::Ended) => panic!(
"took a candidate that broke the drain for one that finished it, \
stopping the probe from trying the next backend",
),
Ok(Received::Produced) => panic!("a candidate fed only the end produced a frame"),
Err(Error::ProbeExhausted(_)) => {}
Err(other) => panic!("expected the probe road, got {other:?}"),
}
assert_eq!(
dec.phase(),
SessionPhase::Spent,
"an exhausted probe leaves no session to speak for the stream",
);
assert_spent(
&mut dec,
crate::Spent::ProbeExhausted,
SessionKind::Exhausted,
);
}
fn assert_spent(dec: &mut VideoDecoder, spent: crate::Spent, kind: SessionKind) {
let packet = raw_packet(0x99);
let owned =
unsafe { crate::boundary::video_packet_from_ffmpeg(&packet, mediadecode::Timebase::SECONDS) }
.expect("a wrappable payload")
.expect("a buffer");
let empty = Packet::empty();
let mut frame = crate::Frame::empty().expect("frame slot");
for when in ["before a flush", "after a flush"] {
assert_eq!(dec.session_kind(), kind, "{when}: spent");
assert!(dec.live().is_none(), "{when}: no decoder lives");
assert_eq!(
dec.phase(),
SessionPhase::Spent,
"{when}: committed to nothing"
);
refused_by_name(when, "the safe send", dec.send_packet(&owned), spent);
refused_by_name(
when,
"a send",
unsafe { dec.send_av_packet(&packet) },
spent,
);
let sent = unsafe { dec.send_av_packet(&empty) };
refused_by_name(when, "a packet that is the end", sent, spent);
refused_by_name(when, "the end", dec.send_eof(), spent);
refused_by_name(when, "a receive", dec.receive_frame(&mut frame), spent);
dec.flush();
}
}
fn refused_by_name<T: core::fmt::Debug>(
when: &str,
entry: &str,
got: Result<T>,
spent: crate::Spent,
) {
match got {
Err(Error::Spent(named)) if named == spent => {}
other => panic!("{when}: {entry}, refused by name as {spent:?}: {other:?}"),
}
}
#[test]
fn a_latched_refusal_reaches_the_probe_instead_of_dying_unread() {
let mut dec = auditioning_decoder();
dec.set_end(End::Taken);
unsafe {
crate::ffi::declare_ceiling_declined_for_test(
dec.decoder().callback_state,
8_294_400,
2_073_600,
)
};
let mut frame = crate::Frame::empty().expect("frame slot");
let Err(Error::ProbeExhausted(p)) = dec.receive_frame(&mut frame) else {
panic!("the candidate must fail the probe");
};
let reason = format!("{:?}", p.attempts());
assert!(
reason.contains("HwSurfaceTooLarge"),
"the latched refusal must be what the attempt log records, not a bare errno: {reason}",
);
}
#[test]
fn a_latched_refusal_outranks_the_errno_in_every_committed_phase() {
let cells: [(&str, fn(&mut VideoDecoder), SessionPhase); 4] = [
("streaming x EAGAIN", |_| {}, SessionPhase::Streaming),
(
"streaming x EOF",
|dec: &mut VideoDecoder| {
dec
.decoder_mut()
.inner
.send_eof()
.expect("the substrate takes the end");
},
SessionPhase::Streaming,
),
(
"draining x EAGAIN",
|dec: &mut VideoDecoder| {
dec.set_end(End::Taken);
},
SessionPhase::Draining,
),
(
"draining x EOF",
|dec: &mut VideoDecoder| {
assert_eq!(dec.send_eof().expect("no fault"), Sent::Accepted);
},
SessionPhase::Draining,
),
];
for (name, arrange, expected_phase) in cells {
let mut dec = committed_decoder();
arrange(&mut dec);
assert_eq!(dec.phase(), expected_phase, "{name}: the phase under test");
unsafe {
crate::ffi::declare_ceiling_declined_for_test(
dec.decoder().callback_state,
8_294_400,
2_073_600,
)
};
let mut frame = crate::Frame::empty().expect("frame slot");
match dec.receive_frame(&mut frame) {
Ok(Received::Ended) => panic!(
"{name}: reported a clean end over a refusal this crate made — the \
ceiling declined the surface and the caller was told the stream was over",
),
Ok(Received::NeedsInput) => panic!(
"{name}: asked for more input over a refusal this crate made — the \
caller would feed a decoder that already declined the frame",
),
Ok(Received::Produced) => panic!("{name}: a declined surface produced a frame"),
Err(Error::HwSurfaceTooLarge(p)) => {
assert_eq!(p.bytes(), 8_294_400, "{name}: the refusal's own numbers");
}
Err(other) => panic!("{name}: expected the latched refusal, got {other:?}"),
}
let again = dec.receive_frame(&mut frame);
assert!(
matches!(again, Ok(Received::NeedsInput | Received::Ended)),
"{name}: the next call reaches libavcodec, got {again:?}",
);
}
}
#[cfg(test)]
fn committed_decoder() -> VideoDecoder {
ffmpeg_next::init().expect("ffmpeg init");
let mut parameters = ffmpeg_next::codec::Parameters::new();
unsafe {
let raw = parameters.as_mut_ptr();
(*raw).codec_type = ffmpeg_next::ffi::AVMediaType::AVMEDIA_TYPE_VIDEO;
(*raw).codec_id = ffmpeg_next::ffi::AVCodecID::AV_CODEC_ID_MPEG4;
}
VideoDecoder::from_software_for_test(
parameters,
crate::limits::DecoderLimits::default(),
false,
)
.expect("a software-backed committed decoder")
}
#[test]
fn a_latched_refusal_outranks_the_errno_on_the_send_road_too() {
let mut dec = committed_decoder();
assert_eq!(dec.send_eof().expect("no fault"), Sent::Accepted);
unsafe {
crate::ffi::declare_ceiling_declined_for_test(
dec.decoder().callback_state,
8_294_400,
2_073_600,
)
};
match dec.send_eof() {
Ok(Sent::Accepted) => panic!("a repeated end silently accepted over a refusal"),
Ok(Sent::MustDrain) => panic!("back pressure promised over a refusal"),
Err(Error::HwSurfaceTooLarge(p)) => {
assert_eq!(p.bytes(), 8_294_400, "the refusal's own numbers");
}
Err(other) => panic!(
"the latched refusal must outrank libavcodec's report on the send road too, \
got {other:?}",
),
}
}
#[test]
fn the_committed_route_reports_the_verdict_it_was_given() {
let dec = committed_decoder();
unsafe {
crate::ffi::declare_ceiling_declined_for_test(
dec.decoder().callback_state,
8_294_400,
2_073_600,
)
};
let reported_raw = reported(dec.hw_route(
Error::Ffmpeg(ffmpeg_next::Error::External),
BareVerdict::CandidateFailure,
));
assert!(
matches!(reported_raw, Error::Ffmpeg(ffmpeg_next::Error::External)),
"it must report the verdict it was handed, not mint a second one: {reported_raw:?}",
);
let verdict = dec.hw_exit(Error::Ffmpeg(ffmpeg_next::Error::InvalidData));
assert!(
matches!(verdict, Error::HwSurfaceTooLarge(ref q) if q.bytes() == 8_294_400),
"the latch must survive the committed route: {verdict:?}",
);
let threaded = reported(dec.hw_route(verdict, BareVerdict::CandidateFailure));
assert!(
matches!(threaded, Error::HwSurfaceTooLarge(ref p) if p.bytes() == 8_294_400),
"the threaded verdict must reach the caller with its numbers: {threaded:?}",
);
}
#[test]
fn a_funnel_consumes_what_it_collects() {
let dec = committed_decoder();
unsafe {
crate::ffi::declare_ceiling_declined_for_test(dec.decoder().callback_state, 4_096, 1_024)
};
let first = dec.hw_exit(Error::Ffmpeg(ffmpeg_next::Error::InvalidData));
assert!(
matches!(first, Error::HwSurfaceTooLarge(ref p) if p.bytes() == 4_096),
"the first funnel mints the refusal: {first:?}",
);
let second = dec.hw_exit(Error::Ffmpeg(ffmpeg_next::Error::InvalidData));
assert!(
matches!(second, Error::Ffmpeg(ffmpeg_next::Error::InvalidData)),
"the second finds nothing and answers with its fallback — which is why a \
road that re-funnels reports the substrate's errno over its own refusal: \
{second:?}",
);
}
#[test]
fn a_declined_surface_reaches_the_caller_whatever_errno_the_codec_wrapped_it_in() {
let hevc = ffmpeg_next::Error::Other { errno: 1 };
let dec = committed_decoder();
unsafe {
crate::ffi::declare_ceiling_declined_for_test(
dec.decoder().callback_state,
8_294_400,
2_073_600,
)
};
let out = reported(dec.hw_failure(hevc, BareVerdict::CandidateFailure));
assert!(
matches!(out, Error::HwSurfaceTooLarge(ref p) if p.bytes() == 8_294_400),
"the refusal's own numbers, not the codec's errno: {out:?}",
);
let after = dec.hw_exit(Error::Ffmpeg(hevc));
assert!(
matches!(after, Error::Ffmpeg(ffmpeg_next::Error::Other { errno: 1 })),
"the refusal must have been collected, not left standing: {after:?}",
);
}
#[test]
fn a_budget_refusal_travels_unwrapped_whatever_errno_rides_with_it() {
for (name, raw) in [
(
"judge_buffer's own -EINVAL",
ffmpeg_next::Error::Other {
errno: libc::EINVAL,
},
),
("libavcodec's InvalidData", ffmpeg_next::Error::InvalidData),
("a failed restart's EXTERNAL", ffmpeg_next::Error::External),
] {
let dec = committed_decoder();
unsafe {
crate::ffi::declare_frame_budget_declined_for_test(dec.decoder().callback_state, 12_582_912)
};
let out = reported(dec.hw_failure(raw, BareVerdict::CandidateFailure));
assert!(
matches!(out, Error::FrameBudgetExceeded(ref p) if p.bytes() == 12_582_912),
"{name}: a budget refusal must travel unwrapped, naming the action that \
can succeed — raise the ceiling: {out:?}",
);
}
}
#[test]
fn the_committed_receive_route_lets_a_budget_refusal_travel_unwrapped() {
let mut dec = committed_decoder();
unsafe {
crate::ffi::declare_frame_budget_declined_for_test(dec.decoder().callback_state, 12_582_912)
};
let mut frame = crate::Frame::empty().expect("frame slot");
match dec.receive_frame(&mut frame) {
Ok(status) => panic!("a refused frame must not read as a protocol state: {status:?}"),
Err(Error::FrameBudgetExceeded(p)) => {
assert_eq!(p.bytes(), 12_582_912, "the refusal's own numbers");
}
Err(other) => panic!("expected the budget refusal, got {other:?}"),
}
}
#[cfg(test)]
#[track_caller]
fn reported(route: HwRoute) -> Error {
match route {
HwRoute::Report(err) => err,
HwRoute::Advance(err) => {
panic!("a committed decoder has no candidate to advance to, got Advance({err:?})")
}
}
}
#[cfg(test)]
const COMMITTED_FAILURES: [ffmpeg_next::Error; 9] = [
ffmpeg_next::Error::External,
ffmpeg_next::Error::Other {
errno: libc::ENOSYS,
},
ffmpeg_next::Error::InvalidData,
ffmpeg_next::Error::Unknown,
ffmpeg_next::Error::Bug,
ffmpeg_next::Error::Bug2,
ffmpeg_next::Error::Other {
errno: libc::EINVAL,
},
ffmpeg_next::Error::Other {
errno: libc::ENOMEM,
},
ffmpeg_next::Error::Other { errno: 1 },
];
#[test]
fn a_committed_failure_is_its_pictures_own_and_the_next_packet_reaches_libavcodec() {
for raw in COMMITTED_FAILURES {
let mut dec = committed_decoder();
dec
.decoder_mut()
.inner
.send_eof()
.expect("libavcodec takes the end");
let out = reported(dec.hw_failure(raw, BareVerdict::CandidateFailure));
assert!(
matches!(out, Error::Ffmpeg(e) if e == raw),
"{raw:?}: reported as it was minted, got {out:?}",
);
let packet = crate::boundary::try_packet_copy(&[0u8; 16]).expect("a submittable packet");
let sent = unsafe { dec.send_av_packet(&packet) };
assert!(
matches!(sent, Err(Error::Ffmpeg(ffmpeg_next::Error::Eof))),
"{raw:?}: the next packet reaches libavcodec, which answers with the end it \
holds — got {sent:?}",
);
let mut frame = crate::Frame::empty().expect("frame slot");
let received = dec.receive_frame(&mut frame);
assert!(
matches!(received, Ok(Received::Ended)),
"{raw:?}: the next picture asked for reaches libavcodec too — got {received:?}",
);
}
}
#[test]
fn after_a_committed_failure_and_a_flush_the_end_reaches_libavcodec() {
for raw in COMMITTED_FAILURES {
let mut dec = committed_decoder();
let out = reported(dec.hw_failure(raw, BareVerdict::CandidateFailure));
assert!(
matches!(out, Error::Ffmpeg(e) if e == raw),
"{raw:?}: reported as it was minted, got {out:?}",
);
dec.flush();
let eof = dec.send_eof();
assert!(
matches!(eof, Ok(Sent::Accepted)),
"{raw:?}: after a flush the end reaches libavcodec — it is taken, got {eof:?}",
);
assert!(
matches!(
dec.decoder_mut().inner.send_eof(),
Err(ffmpeg_next::Error::Eof)
),
"{raw:?}: the end reached libavcodec, which refuses a second one",
);
let mut frame = crate::Frame::empty().expect("frame slot");
assert_eq!(
dec
.receive_frame(&mut frame)
.expect("a drained decoder answers the end"),
Received::Ended,
"{raw:?}",
);
}
}
#[test]
fn the_probe_advances_on_every_failure_as_before() {
for failure in [
ffmpeg_next::Error::External,
ffmpeg_next::Error::Other {
errno: libc::ENOSYS,
},
ffmpeg_next::Error::InvalidData,
ffmpeg_next::Error::Unknown,
] {
let dec = auditioning_decoder();
match dec.hw_failure(failure, BareVerdict::CandidateFailure) {
HwRoute::Advance(BackendAnswer(Error::Ffmpeg(e))) => assert_eq!(e, failure),
HwRoute::Advance(other) => panic!("{failure:?}: advanced with {other:?}"),
HwRoute::Report(err) => {
panic!("{failure:?}: a candidate's failure must advance the probe, got Report({err:?})")
}
}
}
}
#[test]
fn the_send_roads_route_a_surface_refusal_and_report_a_budget_one() {
#[derive(Clone, Copy)]
enum Latch {
Surface,
Budget,
}
#[derive(Clone, Copy)]
enum Face {
Packet,
Eof,
}
for latch in [Latch::Surface, Latch::Budget] {
for auditioning in [true, false] {
for face in [Face::Packet, Face::Eof] {
let mut dec = if auditioning {
auditioning_decoder()
} else {
committed_decoder()
};
assert_eq!(dec.send_eof().expect("no fault"), Sent::Accepted);
match latch {
Latch::Surface => unsafe {
crate::ffi::declare_ceiling_declined_for_test(
dec.decoder().callback_state,
8_294_400,
2_073_600,
)
},
Latch::Budget => {
unsafe {
crate::ffi::declare_frame_budget_declined_for_test(
dec.decoder().callback_state,
12_582_912,
)
}
}
}
let packet = crate::boundary::try_packet_copy(&[0u8; 16]).expect("packet");
let out = match face {
Face::Packet => unsafe { dec.send_av_packet(&packet) },
Face::Eof => dec.send_eof(),
};
let name = format!(
"{} x {} x {}",
match latch {
Latch::Surface => "surface",
Latch::Budget => "budget",
},
if auditioning {
"auditioning"
} else {
"committed"
},
match face {
Face::Packet => "send_packet",
Face::Eof => "send_eof",
},
);
match (latch, out) {
(Latch::Surface, Err(Error::ProbeExhausted(p))) if auditioning => {
let cause = format!("{:?}", p.attempts());
assert!(
cause.contains("HwSurfaceTooLarge") && cause.contains("8294400"),
"{name}: the refusal must reach the attempt log with its numbers: {cause}",
);
}
(Latch::Surface, Err(Error::HwSurfaceTooLarge(p))) if !auditioning => {
assert_eq!(p.bytes(), 8_294_400, "{name}: the refusal's own numbers");
}
(Latch::Surface, other) => panic!(
"{name}: a declined surface must advance a running probe and otherwise \
reach the caller by name: {other:?}",
),
(Latch::Budget, Err(Error::FrameBudgetExceeded(p))) => {
assert_eq!(p.bytes(), 12_582_912, "{name}: the refusal's own numbers");
}
(Latch::Budget, other) => {
panic!("{name}: a budget refusal must exit direct and unwrapped: {other:?}",)
}
}
}
}
}
}
#[test]
fn the_packet_timebase_reaches_the_codec_context() {
use std::num::NonZeroI32;
ffmpeg_next::init().expect("ffmpeg init");
let parameters = || {
let mut parameters = ffmpeg_next::codec::Parameters::new();
unsafe {
let raw = parameters.as_mut_ptr();
(*raw).codec_type = ffmpeg_next::ffi::AVMediaType::AVMEDIA_TYPE_VIDEO;
(*raw).codec_id = ffmpeg_next::ffi::AVCodecID::AV_CODEC_ID_H264;
}
parameters
};
let limits = crate::limits::DecoderLimits::default();
let read_back = |declared: Option<mediadecode::Timebase>| {
let (ctx, _state) = build_codec_context(
¶meters(),
limits,
crate::DecoderOptions::new(),
declared,
)
.expect("a context is allocated");
let raw = unsafe { (*ctx.as_ptr()).pkt_timebase };
(raw.num, raw.den)
};
let milliseconds = mediadecode::Timebase::new(1, NonZeroI32::new(1_000).expect("non-zero"));
assert_eq!(
read_back(Some(milliseconds)),
(1, 1_000),
"a declared timebase is written verbatim",
);
assert_eq!(
read_back(None),
(0, 1),
"and an undeclared one leaves libavcodec's own default alone",
);
let unset = mediadecode::Timebase::new(0, NonZeroI32::new(1).expect("non-zero"));
assert_eq!(read_back(Some(unset)), (0, 1));
}
#[test]
fn every_discard_value_reaches_the_codec_context() {
ffmpeg_next::init().expect("ffmpeg init");
let mut parameters = ffmpeg_next::codec::Parameters::new();
unsafe {
let raw = parameters.as_mut_ptr();
(*raw).codec_type = ffmpeg_next::ffi::AVMediaType::AVMEDIA_TYPE_VIDEO;
(*raw).codec_id = ffmpeg_next::ffi::AVCodecID::AV_CODEC_ID_H264;
}
let read_back = |options: crate::DecoderOptions| {
let (ctx, state) = build_codec_context(
¶meters,
crate::limits::DecoderLimits::default(),
options,
None,
)
.expect("a context is allocated");
let raw = unsafe {
let ctx = ctx.as_ptr();
(
ptr::addr_of!((*ctx).skip_frame).cast::<i32>().read(),
ptr::addr_of!((*ctx).skip_loop_filter).cast::<i32>().read(),
ptr::addr_of!((*ctx).skip_idct).cast::<i32>().read(),
)
};
drop(ctx);
drop(state);
raw
};
assert_eq!(read_back(crate::DecoderOptions::new()), (0, 0, 0));
for value in crate::Discard::ALL {
let av = value.as_av() as i32;
let one = crate::DecoderOptions::new();
assert_eq!(read_back(one.with_skip_frame(value)), (av, 0, 0), "{value}");
assert_eq!(
read_back(one.with_skip_loop_filter(value)),
(0, av, 0),
"{value}"
);
assert_eq!(read_back(one.with_skip_idct(value)), (0, 0, av), "{value}");
}
}
#[test]
fn the_open_request_starts_at_opens_and_sets_each_optional() {
use std::num::NonZeroI32;
let request = VideoDecoderOpen::new();
assert_eq!(request, VideoDecoderOpen::default());
assert_eq!(
(
request.backend(),
request.limits(),
request.timebase(),
request.options()
),
(
None,
crate::limits::DecoderLimits::new(),
None,
crate::DecoderOptions::new()
)
);
let limits = crate::limits::DecoderLimits::new().with_max_packet_bytes(12_345);
let timebase = mediadecode::Timebase::new(1, NonZeroI32::new(1_000).expect("non-zero"));
let options = crate::DecoderOptions::new().with_skip_frame(crate::Discard::NonReference);
let built = request
.with_backend(Some(Backend::VideoToolbox))
.with_limits(limits)
.with_timebase(Some(timebase))
.with_options(options);
assert_eq!(
(
built.backend(),
built.limits(),
built.timebase(),
built.options()
),
(Some(Backend::VideoToolbox), limits, Some(timebase), options)
);
let mut set = VideoDecoderOpen::new();
set
.set_backend(Some(Backend::VideoToolbox))
.set_limits(limits)
.set_timebase(Some(timebase))
.set_options(options);
assert_eq!(set, built);
set.set_backend(None).set_timebase(None);
assert_eq!(
set,
VideoDecoderOpen::new()
.with_limits(limits)
.with_options(options)
);
}
#[cfg(target_vendor = "apple")]
fn h264_parameters() -> codec::Parameters {
let mut parameters = codec::Parameters::new();
unsafe {
let raw = parameters.as_mut_ptr();
(*raw).codec_type = ffmpeg_next::ffi::AVMediaType::AVMEDIA_TYPE_VIDEO;
(*raw).codec_id = ffmpeg_next::ffi::AVCodecID::AV_CODEC_ID_H264;
}
parameters
}
#[cfg(target_vendor = "apple")]
#[test]
fn the_probe_runs_from_the_open_under_every_skip_level() {
ffmpeg_next::init().expect("ffmpeg init");
let timebase = mediadecode::Timebase::new(1, core::num::NonZeroI32::new(25).expect("non-zero"));
for skip in crate::Discard::ALL {
let options = crate::DecoderOptions::new().with_skip_frame(skip);
let opened = VideoDecoder::open_probing(
h264_parameters(),
crate::limits::DecoderLimits::default(),
options,
None,
backend::probe_order(),
)
.expect("the decoder opens");
assert_eq!(
opened.phase(),
SessionPhase::Auditioning,
"{skip}: the probe runs"
);
assert_eq!(opened.backend(), Backend::VideoToolbox, "{skip}");
let auto = crate::FfmpegOwnedVideoStreamDecoder::open_from(
crate::VideoStreamDecoderOpen::new().with_options(options),
h264_parameters(),
timebase,
crate::limits::DecoderLimits::default(),
)
.expect("the session opens");
assert!(!auto.is_software(), "{skip}: `Auto` probes hardware");
}
}
#[cfg(target_vendor = "apple")]
type Opened = (
bool,
Backend,
crate::limits::DecoderLimits,
(i32, i32),
(i32, i32, i32),
crate::DecoderOptions,
);
#[cfg(target_vendor = "apple")]
fn opened(decoder: Result<VideoDecoder>) -> Opened {
let decoder = decoder.expect("the decoder opens");
let (timebase, skips) = unsafe {
let ctx = decoder.decoder().inner.as_ptr();
(
((*ctx).pkt_timebase.num, (*ctx).pkt_timebase.den),
(
ptr::addr_of!((*ctx).skip_frame).cast::<i32>().read(),
ptr::addr_of!((*ctx).skip_loop_filter).cast::<i32>().read(),
ptr::addr_of!((*ctx).skip_idct).cast::<i32>().read(),
),
)
};
(
decoder.session_kind() == SessionKind::Trial,
decoder.backend(),
decoder.frame_limits,
timebase,
skips,
decoder.options(),
)
}
#[cfg(target_vendor = "apple")]
#[test]
fn the_open_request_reaches_the_decoder_on_both_roads() {
use std::num::NonZeroI32;
ffmpeg_next::init().expect("ffmpeg init");
let options = crate::DecoderOptions::new()
.with_skip_frame(crate::Discard::NonReference)
.with_skip_loop_filter(crate::Discard::All)
.with_skip_idct(crate::Discard::NonKey);
let limits = crate::limits::DecoderLimits::new().with_max_packet_bytes(12_345);
let milliseconds = mediadecode::Timebase::new(1, NonZeroI32::new(1_000).expect("non-zero"));
for pinned in [None, Some(Backend::VideoToolbox)] {
for (timebase, written) in [(None, (0, 1)), (Some(milliseconds), (1, 1_000))] {
let request = VideoDecoderOpen::new()
.with_backend(pinned)
.with_limits(limits)
.with_timebase(timebase)
.with_options(options);
assert_eq!(
opened(VideoDecoder::open_from(request, h264_parameters())),
(
pinned.is_none(),
Backend::VideoToolbox,
limits,
written,
(8, 48, 32),
options
),
"{request:?}"
);
}
}
}
#[cfg(target_vendor = "apple")]
#[test]
fn each_open_constructor_opens_the_request_it_spells() {
use std::num::NonZeroI32;
ffmpeg_next::init().expect("ffmpeg init");
let parameters = h264_parameters;
let default = crate::limits::DecoderLimits::default();
let limits = crate::limits::DecoderLimits::new().with_max_packet_bytes(12_345);
let milliseconds = mediadecode::Timebase::new(1, NonZeroI32::new(1_000).expect("non-zero"));
let (untimed, timed) = ((0, 1), (1, 1_000));
let pinned = Backend::VideoToolbox;
let cases = [
(
"open",
opened(VideoDecoder::open(parameters())),
(true, default, untimed),
),
(
"open_timed",
opened(VideoDecoder::open_timed(parameters(), milliseconds)),
(true, default, timed),
),
(
"open_with_frame_limits",
opened(VideoDecoder::open_with_frame_limits(parameters(), limits)),
(true, limits, untimed),
),
(
"open_with_frame_limits_timed",
opened(VideoDecoder::open_with_frame_limits_timed(
parameters(),
limits,
milliseconds,
)),
(true, limits, timed),
),
(
"open_with",
opened(VideoDecoder::open_with(parameters(), pinned)),
(false, default, untimed),
),
(
"open_with_timed",
opened(VideoDecoder::open_with_timed(
parameters(),
pinned,
milliseconds,
)),
(false, default, timed),
),
(
"open_with_limits",
opened(VideoDecoder::open_with_limits(parameters(), pinned, limits)),
(false, limits, untimed),
),
(
"open_with_limits_timed",
opened(VideoDecoder::open_with_limits_timed(
parameters(),
pinned,
limits,
milliseconds,
)),
(false, limits, timed),
),
];
for (name, opened, (probing, limits, timebase)) in cases {
assert_eq!(
opened,
(
probing,
pinned,
limits,
timebase,
(0, 0, 0),
crate::DecoderOptions::new()
),
"{name}"
);
}
}
#[test]
fn a_context_this_crate_builds_decodes_on_one_thread() {
ffmpeg_next::init().expect("ffmpeg init");
let mut parameters = ffmpeg_next::codec::Parameters::new();
unsafe {
let raw = parameters.as_mut_ptr();
(*raw).codec_type = ffmpeg_next::ffi::AVMediaType::AVMEDIA_TYPE_VIDEO;
(*raw).codec_id = ffmpeg_next::ffi::AVCodecID::AV_CODEC_ID_H264;
}
let (ctx, state) = build_codec_context(
¶meters,
crate::limits::DecoderLimits::default(),
crate::DecoderOptions::new(),
None,
)
.expect("a context is allocated");
let threads = unsafe { (*ctx.as_ptr()).thread_count };
assert_eq!(threads, 1, "one decoding thread, FFmpeg's default");
drop(ctx);
drop(state);
}
#[test]
fn the_public_decoder_has_a_timed_construction_road() {
fn _timed_roads_exist() {
let _: fn(ffmpeg_next::codec::Parameters, mediadecode::Timebase) -> Result<VideoDecoder> =
VideoDecoder::open_timed;
let _: fn(
ffmpeg_next::codec::Parameters,
crate::limits::DecoderLimits,
mediadecode::Timebase,
) -> Result<VideoDecoder> = VideoDecoder::open_with_frame_limits_timed;
let _: fn(
ffmpeg_next::codec::Parameters,
Backend,
mediadecode::Timebase,
) -> Result<VideoDecoder> = VideoDecoder::open_with_timed;
let _: fn(
ffmpeg_next::codec::Parameters,
Backend,
crate::limits::DecoderLimits,
mediadecode::Timebase,
) -> Result<VideoDecoder> = VideoDecoder::open_with_limits_timed;
}
}
const RAW_SIDE: i32 = 16;
const RAW_PICTURE: usize = (RAW_SIDE * RAW_SIDE) as usize;
fn raw_auditioning_decoder() -> VideoDecoder {
raw_auditioning_decoder_with(RAW_SIDE, crate::DecoderOptions::new())
}
fn raw_auditioning_decoder_with(side: i32, options: crate::DecoderOptions) -> VideoDecoder {
ffmpeg_next::init().expect("ffmpeg init");
let parameters = raw_parameters(side);
let codec = find_decoder(¶meters).expect("rawvideo");
VideoDecoder::from_software_for_test_as(
parameters,
codec,
crate::limits::DecoderLimits::default(),
options,
true,
)
.expect("a software-backed rawvideo candidate on trial")
}
fn raw_parameters(side: i32) -> codec::Parameters {
let mut parameters = ffmpeg_next::codec::Parameters::new();
unsafe {
let raw = parameters.as_mut_ptr();
(*raw).codec_type = ffmpeg_next::ffi::AVMediaType::AVMEDIA_TYPE_VIDEO;
(*raw).codec_id = ffmpeg_next::ffi::AVCodecID::AV_CODEC_ID_RAWVIDEO;
(*raw).width = side;
(*raw).height = side;
(*raw).format = ffmpeg_next::ffi::AVPixelFormat::AV_PIX_FMT_GRAY8 as i32;
}
parameters
}
fn with_extradata(mut parameters: codec::Parameters, bytes: usize) -> codec::Parameters {
let padded = bytes + ffmpeg_next::ffi::AV_INPUT_BUFFER_PADDING_SIZE as usize;
unsafe {
let raw = parameters.as_mut_ptr();
assert!(
(*raw).extradata.is_null(),
"parameters with no extradata yet"
);
let extradata = ffmpeg_next::ffi::av_mallocz(padded).cast::<u8>();
assert!(!extradata.is_null(), "the extradata");
(*raw).extradata = extradata;
(*raw).extradata_size = bytes as i32;
}
parameters
}
fn raw_packet(fill: u8) -> Packet {
crate::boundary::try_packet_copy(&[fill; RAW_PICTURE]).expect("a submittable packet")
}
fn payload_of(packet: &Packet) -> (usize, i32) {
unsafe {
let raw = packet.as_ptr();
assert!(!(*raw).buf.is_null(), "the lanes send refcounted packets");
(
(*raw).data as usize,
ffmpeg_next::ffi::av_buffer_get_ref_count((*raw).buf),
)
}
}
fn holds(packet: &Packet, fill: u8) -> bool {
packet
.data()
.is_some_and(|bytes| bytes.iter().all(|&byte| byte == fill))
}
#[test]
fn the_probes_copy_shares_nothing_with_its_source() {
use ffmpeg_next::ffi::{AVPacketSideDataType, av_packet_new_side_data};
ffmpeg_next::init().expect("ffmpeg init");
let mut source = crate::boundary::try_packet_copy(&[0x5A; 1024]).expect("a source packet");
unsafe {
let entry = av_packet_new_side_data(
source.as_mut_ptr(),
AVPacketSideDataType::AV_PKT_DATA_STRINGS_METADATA,
16,
);
assert!(!entry.is_null(), "the entry must exist to be copied");
core::ptr::write_bytes(entry, 0xC3, 16);
}
let copy = unsafe { crate::boundary::try_clone_packet(&source) }.expect("an uncapped copy");
let (source_at, source_refs) = payload_of(&source);
let (copy_at, copy_refs) = payload_of(©);
assert_eq!(copy.data(), source.data(), "the same bytes");
assert_ne!(copy_at, source_at, "in a payload of its own");
assert_eq!(
(source_refs, copy_refs),
(1, 1),
"each buffer referenced by its own packet alone",
);
let side = |packet: &Packet| -> (usize, Vec<u8>) {
unsafe {
let raw = packet.as_ptr();
assert_eq!((*raw).side_data_elems, 1, "one entry");
let entry = (*raw).side_data;
let (data, size) = ((*entry).data, (*entry).size);
(
data as usize,
std::slice::from_raw_parts(data, size).to_vec(),
)
}
};
let (source_side_at, source_side) = side(&source);
let (copy_side_at, copy_side) = side(©);
assert_eq!(copy_side, source_side, "the same side data");
assert_ne!(copy_side_at, source_side_at, "in an entry of its own");
let charge = |packet: &Packet| {
packet.size() + packet_side_data_bytes(packet, MAX_PROBE_PACKET_SIDE_DATA_ENTRIES)
};
assert_eq!(
charge(©),
charge(&source),
"the copy costs what its source was charged",
);
}
#[test]
fn a_rescued_packet_keeps_the_bytes_it_was_sent_with() {
let mut dec = raw_auditioning_decoder();
let fills = [0x11u8, 0x22];
let mut sent: Vec<Packet> = fills.iter().map(|&fill| raw_packet(fill)).collect();
for packet in &sent {
assert_eq!(
unsafe { dec.send_av_packet(packet) }.expect("taken"),
Sent::Accepted
);
}
assert_eq!(
dec.probe().expect("still on trial").buffered_bytes,
fills.len() * RAW_PICTURE,
"each copy is charged its payload",
);
let mut frame = crate::Frame::empty().expect("frame slot");
let Err(Error::ProbeExhausted(p)) = dec.receive_frame(&mut frame) else {
panic!("a CPU frame on trial fails the only backend, and the probe exhausts");
};
let mut rescued = p.into_unconsumed_packets();
drop(dec);
for packet in &mut sent {
packet.data_mut().expect("a payload").fill(0xEE);
}
assert_eq!(
rescued.len(),
fills.len(),
"every packet the probe took comes back"
);
for (packet, &fill) in rescued.iter().zip(&fills) {
assert!(
holds(packet, fill),
"{fill:#04x}: the bytes it was sent with, not the caller's later writes",
);
assert_eq!(
payload_of(packet).1,
1,
"{fill:#04x}: referenced by the rescued packet alone",
);
}
for packet in &mut rescued {
packet.data_mut().expect("a payload").fill(0x55);
}
for packet in &sent {
assert!(
holds(packet, 0xEE),
"a write to a rescued packet reached the caller's"
);
}
}
#[test]
fn a_copy_the_probe_cannot_make_sends_its_packet_and_commits() {
use mediaio_ffmpeg::test_support::{
cap_ffmpeg_allocations, in_subprocess, uncap_ffmpeg_allocations,
};
in_subprocess(
"decoder::tests::a_copy_the_probe_cannot_make_sends_its_packet_and_commits",
|| {
let mut dec = raw_auditioning_decoder();
let first = raw_packet(0x11);
assert_eq!(
unsafe { dec.send_av_packet(&first) }.expect("taken"),
Sent::Accepted
);
let uncopied = crate::boundary::try_packet_copy(&[0x77; 8192]).expect("a packet");
unsafe { cap_ffmpeg_allocations(512) };
let answer = unsafe { dec.send_av_packet(&uncopied) };
uncap_ffmpeg_allocations();
assert!(
matches!(answer, Ok(Sent::Accepted)),
"sent and committed: {answer:?}"
);
assert_eq!(dec.phase(), SessionPhase::Streaming, "committed");
let mut picture = frame::Video::empty();
dec
.decoder_mut()
.inner
.receive_frame(&mut picture)
.expect("the first picture");
dec
.decoder_mut()
.inner
.receive_frame(&mut picture)
.expect("the uncopied packet's picture");
assert_eq!(picture.data(0)[0], 0x77, "decoded from the packet sent");
},
);
}
fn with_side_data_entries(mut packet: Packet, entries: usize) -> Packet {
use ffmpeg_next::ffi::{AVPacketSideData, AVPacketSideDataType, av_malloc, av_mallocz};
unsafe {
let raw = packet.as_mut_ptr();
assert!((*raw).side_data.is_null(), "a packet with no side data yet");
let array =
av_malloc(entries * core::mem::size_of::<AVPacketSideData>()).cast::<AVPacketSideData>();
assert!(!array.is_null(), "the descriptor array");
for index in 0..entries {
let data = av_mallocz(1).cast::<u8>();
assert!(!data.is_null(), "an entry's byte");
array.add(index).write(AVPacketSideData {
data,
size: 1,
type_: AVPacketSideDataType::AV_PKT_DATA_MPEGTS_STREAM_ID,
});
}
(*raw).side_data = array;
(*raw).side_data_elems = entries as i32;
}
packet
}
#[test]
fn a_packet_of_more_side_data_entries_than_the_probe_copies_is_sent_and_commits() {
let crowded = || with_side_data_entries(raw_packet(0x5A), MAX_PROBE_PACKET_SIDE_DATA_ENTRIES + 1);
assert_eq!(
packet_side_data_count(&crowded()),
MAX_PROBE_PACKET_SIDE_DATA_ENTRIES + 1
);
let mut dec = raw_auditioning_decoder();
send_all(&mut dec, &[discarded(0x11)]);
let packet = crowded();
let sent = unsafe { dec.send_av_packet(&packet) };
assert!(
matches!(sent, Ok(Sent::Accepted)),
"sent and committed: {sent:?}"
);
assert_eq!(dec.phase(), SessionPhase::Streaming, "committed");
assert!(
holds_a_picture(&mut dec),
"its picture, the committed candidate's"
);
let mut dec = raw_auditioning_decoder_with(32, crate::DecoderOptions::new());
{
let probe = dec.probe_mut().expect("on trial");
probe.parameters = raw_parameters(RAW_SIDE);
probe.build = VideoDecoder::software_state_for_test;
probe.remaining_backends = vec![Backend::Cuda];
probe.buffered_packets = vec![discarded(0x11)];
probe.fed = 1;
probe.buffered_bytes = RAW_PICTURE;
}
let sent = unsafe { dec.send_av_packet(&packet) };
assert!(
matches!(sent, Ok(Sent::Accepted)),
"the next candidate took it: {sent:?}"
);
assert_eq!(dec.backend(), Backend::Cuda, "the next candidate's");
assert_eq!(dec.phase(), SessionPhase::Streaming, "and committed");
}
#[test]
fn a_candidate_is_replayed_the_history_itself_and_it_is_released_before_it_goes_back() {
let mut dec = raw_auditioning_decoder();
dec
.probe_mut()
.expect("on trial")
.remaining_backends
.push(Backend::Cuda);
let fills = [0x11u8, 0x22];
let sent: Vec<Packet> = fills.iter().map(|&fill| raw_packet(fill)).collect();
send_all(&mut dec, &sent);
dec
.advance_probe_with(
BackendAnswer::of_call(Error::Ffmpeg(ffmpeg_next::Error::InvalidData)),
VideoDecoder::software_state_for_test,
)
.expect("the candidate takes the replay and is installed");
assert_eq!(
dec.backend(),
Backend::Cuda,
"the candidate is the session's"
);
let probe = dec.probe().expect("still on trial: no picture yet");
assert_eq!(probe.fed, fills.len(), "it took the whole history");
for (packet, &fill) in probe.buffered_packets.iter().zip(&fills) {
assert_eq!(
payload_of(packet).1,
2,
"{fill:#04x}: the history's own packet, shared with the candidate — no copy",
);
}
let mut frame = crate::Frame::empty().expect("frame slot");
let Err(Error::ProbeExhausted(p)) = dec.receive_frame(&mut frame) else {
panic!("the candidate fails on a CPU frame, and the probe exhausts");
};
assert_eq!(
p.attempts().len(),
2,
"the first backend's failure and the candidate's: {:?}",
p.attempts(),
);
assert_eq!(
p.unconsumed_packets().len(),
fills.len(),
"every packet the probe took comes back"
);
for (packet, &fill) in p.unconsumed_packets().iter().zip(&fills) {
assert!(
holds(packet, fill),
"{fill:#04x}: the bytes it was sent with"
);
assert_eq!(
payload_of(packet).1,
1,
"{fill:#04x}: referenced by the rescued packet alone, beside a live candidate",
);
}
assert_eq!(
dec.backend(),
Backend::Cuda,
"and that candidate is still live"
);
}
#[test]
fn the_attempt_log_is_in_the_order_the_backends_were_tried() {
let mut dec = raw_auditioning_decoder();
let first = dec.backend();
{
let probe = dec.probe_mut().expect("on trial");
probe.parameters = raw_parameters(32);
probe.build = VideoDecoder::software_state_for_test;
probe.remaining_backends = vec![Backend::Cuda];
probe.buffered_packets = vec![raw_packet(0x11)];
probe.fed = 1;
probe.buffered_bytes = RAW_PICTURE;
}
let Err(Error::ProbeExhausted(p)) = dec.advance_probe(BackendAnswer::of_call(Error::Ffmpeg(
ffmpeg_next::Error::InvalidData,
))) else {
panic!("the next candidate refuses its replay, and the probe exhausts");
};
let tried: Vec<Backend> = p.attempts().iter().map(|(backend, _)| *backend).collect();
assert_eq!(tried, vec![first, Backend::Cuda], "in the order tried");
assert!(
matches!(
*p.attempts()[0].1,
Error::Ffmpeg(ffmpeg_next::Error::InvalidData)
),
"the first candidate's own answer: {:?}",
p.attempts()
);
}
fn software_state_then_capped(
parameters: codec::Parameters,
codec: Codec,
backend: Backend,
limits: crate::limits::DecoderLimits,
options: crate::DecoderOptions,
pkt_timebase: Option<mediadecode::Timebase>,
) -> std::result::Result<DecoderState, BuildFailure> {
let built = VideoDecoder::software_state_for_test(
parameters,
codec,
backend,
limits,
options,
pkt_timebase,
);
unsafe { mediaio_ffmpeg::test_support::cap_ffmpeg_allocations(512) };
built
}
#[test]
fn a_replay_makes_no_copy_so_one_that_could_not_be_made_charges_no_backend() {
use mediaio_ffmpeg::test_support::{in_subprocess, uncap_ffmpeg_allocations};
in_subprocess(
"decoder::tests::a_replay_makes_no_copy_so_one_that_could_not_be_made_charges_no_backend",
|| {
let mut dec = raw_auditioning_decoder();
dec
.probe_mut()
.expect("on trial")
.remaining_backends
.push(Backend::Cuda);
let fills = [0x11u8, 0x22];
let sent: Vec<Packet> = fills
.iter()
.map(|&fill| crate::boundary::try_packet_copy(&[fill; 8192]).expect("a packet"))
.collect();
send_all(&mut dec, &sent);
let advanced = dec.advance_probe_with(
BackendAnswer::of_call(Error::Ffmpeg(ffmpeg_next::Error::InvalidData)),
software_state_then_capped,
);
uncap_ffmpeg_allocations();
assert!(advanced.is_ok(), "installed: {advanced:?}");
assert_eq!(
dec.backend(),
Backend::Cuda,
"the candidate is the session's"
);
let probe = dec.probe().expect("on trial");
assert_eq!(probe.fed, fills.len(), "it took the whole history");
assert_eq!(
probe.attempts.len(),
1,
"the failed backend's answer alone: {:?}",
probe.attempts,
);
assert!(
holds_a_picture(&mut dec),
"the candidate decodes what it was replayed"
);
},
);
}
#[test]
fn a_replay_drains_nothing_so_a_long_history_charges_no_backend() {
const PICTURES: usize = 40;
let mut dec = raw_auditioning_decoder();
{
let probe = dec.probe_mut().expect("on trial");
probe.remaining_backends = vec![Backend::Cuda];
probe.build = VideoDecoder::software_state_for_test;
probe.buffered_packets = (0..PICTURES).map(|index| raw_packet(index as u8)).collect();
probe.fed = PICTURES;
probe.buffered_bytes = PICTURES * RAW_PICTURE;
}
dec
.advance_probe(BackendAnswer::of_call(Error::Ffmpeg(
ffmpeg_next::Error::InvalidData,
)))
.expect("installed");
assert_eq!(dec.backend(), Backend::Cuda, "the candidate's");
let probe = dec.probe().expect("on trial");
assert_eq!(
probe.fed, 2,
"the replay stopped at the candidate's first output: a picture waiting, a packet queued \
behind it"
);
assert_eq!(probe.attempts.len(), 1, "the failed backend's answer alone");
let mut picture = frame::Video::empty();
let mut delivered = Vec::new();
loop {
match dec.decoder_mut().inner.receive_frame(&mut picture) {
Ok(()) => delivered.push(picture.data(0)[0]),
Err(e) if is_eagain(&e) && dec.owes() => {
let fed = dec.feed().expect("the candidate takes what it is owed");
assert!(fed.taken > 0, "a decoder that asks for input takes some");
}
Err(e) if is_eagain(&e) => break,
Err(e) => panic!("picture {}: {e}", delivered.len()),
}
}
assert_eq!(
delivered,
(0..PICTURES).map(|index| index as u8).collect::<Vec<u8>>(),
"every picture, in order"
);
assert_eq!(
dec.probe().map(|probe| probe.fed),
Some(PICTURES),
"the whole history fed, and the candidate still on trial"
);
}
#[test]
fn a_copy_an_advance_cannot_make_keeps_the_next_backend_and_the_history() {
let mut dec = raw_auditioning_decoder();
{
let probe = dec.probe_mut().expect("on trial");
probe.parameters = with_extradata(raw_parameters(RAW_SIDE), 64);
probe.remaining_backends = vec![Backend::Cuda];
probe.build = VideoDecoder::software_state_for_test;
probe.buffered_packets = vec![raw_packet(0x11)];
probe.fed = 1;
probe.buffered_bytes = RAW_PICTURE;
}
let failed = || BackendAnswer::of_call(Error::Ffmpeg(ffmpeg_next::Error::InvalidData));
dec.frame_limits = crate::limits::DecoderLimits::new().with_max_codec_parameter_bytes(16);
let answer = dec.advance_probe(failed());
assert!(
matches!(answer, Err(Error::ParametersTooLarge(_))),
"surfaced as it is: {answer:?}"
);
assert_eq!(dec.session_kind(), SessionKind::Owed, "the advance, owed");
let probe = dec.probe().expect("the probe's record");
assert_eq!(
probe.remaining_backends,
vec![Backend::Cuda],
"the backend still to try"
);
assert_eq!(probe.buffered_packets.len(), 1, "the history");
assert!(
probe.attempts.is_empty(),
"no backend charged: {:?}",
probe.attempts
);
dec.frame_limits = crate::limits::DecoderLimits::new();
dec.resume().expect("installed");
assert_eq!(dec.backend(), Backend::Cuda, "the backend kept");
}
fn software_state_under_a_cap(
parameters: codec::Parameters,
codec: Codec,
backend: Backend,
limits: crate::limits::DecoderLimits,
options: crate::DecoderOptions,
pkt_timebase: Option<mediadecode::Timebase>,
) -> std::result::Result<DecoderState, BuildFailure> {
unsafe { mediaio_ffmpeg::test_support::cap_ffmpeg_allocations(512) };
let built = VideoDecoder::software_state_for_test(
parameters,
codec,
backend,
limits,
options,
pkt_timebase,
);
mediaio_ffmpeg::test_support::uncap_ffmpeg_allocations();
built
}
#[test]
fn a_context_an_advance_cannot_allocate_keeps_the_next_backend() {
use mediaio_ffmpeg::test_support::in_subprocess;
in_subprocess(
"decoder::tests::a_context_an_advance_cannot_allocate_keeps_the_next_backend",
|| {
let mut dec = raw_auditioning_decoder();
{
let probe = dec.probe_mut().expect("on trial");
probe.remaining_backends = vec![Backend::Cuda];
probe.buffered_packets = vec![raw_packet(0x11)];
probe.fed = 1;
probe.buffered_bytes = RAW_PICTURE;
}
let answer = dec.advance_probe_with(
BackendAnswer::of_call(Error::Ffmpeg(ffmpeg_next::Error::InvalidData)),
software_state_under_a_cap,
);
assert!(
matches!(
answer,
Err(Error::Ffmpeg(ffmpeg_next::Error::Other { errno })) if errno == libc::ENOMEM
),
"surfaced as it is: {answer:?}"
);
let probe = dec.probe().expect("still on trial");
assert_eq!(
probe.remaining_backends,
vec![Backend::Cuda],
"the backend still to try"
);
assert_eq!(probe.buffered_packets.len(), 1, "the history");
assert!(
probe.attempts.is_empty(),
"no backend charged: {:?}",
probe.attempts
);
},
);
}
#[test]
fn an_open_whose_context_cannot_be_allocated_charges_no_backend() {
use mediaio_ffmpeg::test_support::{
cap_ffmpeg_allocations, in_subprocess, uncap_ffmpeg_allocations,
};
in_subprocess(
"decoder::tests::an_open_whose_context_cannot_be_allocated_charges_no_backend",
|| {
ffmpeg_next::init().expect("ffmpeg init");
let order = [Backend::Cuda, Backend::Vaapi];
let open = || {
VideoDecoder::open_probing(
raw_parameters(RAW_SIDE),
crate::limits::DecoderLimits::new(),
crate::DecoderOptions::new(),
None,
&order,
)
};
assert!(
matches!(open(), Err(Error::ProbeExhausted(_))),
"the control: every backend refuses"
);
unsafe { cap_ffmpeg_allocations(512) };
let opened = open();
uncap_ffmpeg_allocations();
assert!(
matches!(
opened,
Err(Error::Ffmpeg(ffmpeg_next::Error::Other { errno })) if errno == libc::ENOMEM
),
"answered as it is: {:?}",
opened.as_ref().map(|_| ())
);
},
);
}
#[test]
fn an_open_whose_own_copy_cannot_be_made_charges_no_backend() {
ffmpeg_next::init().expect("ffmpeg init");
let opened = VideoDecoder::open_probing(
with_extradata(raw_parameters(RAW_SIDE), 64),
crate::limits::DecoderLimits::new().with_max_codec_parameter_bytes(16),
crate::DecoderOptions::new(),
None,
&[Backend::Cuda, Backend::Vaapi],
);
assert!(
matches!(opened, Err(Error::ParametersTooLarge(_))),
"answered as it is: {:?}",
opened.as_ref().map(|_| ())
);
}
#[cfg(target_vendor = "apple")]
#[test]
#[ignore = "decodes VP9 on VideoToolbox, which a virtual machine may not offer"]
fn on_videotoolbox_a_picture_over_the_frame_limit_is_no_delivery_and_charges_nothing() {
ffmpeg_next::init().expect("ffmpeg init");
let opened = || {
let mut dec =
VideoDecoder::open(coded_parameters(AVCodecID::AV_CODEC_ID_VP9)).expect("open on the probe");
assert_eq!(dec.backend(), Backend::VideoToolbox);
send_all(&mut dec, &[coded(&VP9_KEY_FRAME)]);
assert_eq!(
dec.phase(),
SessionPhase::Auditioning,
"on trial: no picture yet"
);
dec
};
let mut frame = crate::Frame::empty().expect("frame slot");
let mut dec = opened();
assert_eq!(
dec.receive_frame(&mut frame).expect("the picture"),
Received::Produced
);
let mut dec = opened();
dec.frame_limits = crate::limits::DecoderLimits::new()
.with_frame(crate::FrameLimits::new().with_max_frame_bytes(1024));
let received = dec.receive_frame(&mut frame);
assert!(
matches!(received, Err(Error::HwTransferTooLarge(_))),
"the caller's limit, by name: {received:?}"
);
assert_eq!(
dec.phase(),
SessionPhase::Auditioning,
"no delivery: still on trial"
);
{
let probe = dec.probe().expect("on trial");
assert!(
probe.attempts.is_empty(),
"nothing charged: {:?}",
probe.attempts
);
assert_eq!(probe.buffered_packets.len(), 1, "the history kept");
}
dec.frame_limits = crate::limits::DecoderLimits::new();
send_all(&mut dec, &[coded(&VP9_KEY_FRAME)]);
assert_eq!(
dec.receive_frame(&mut frame).expect("the next picture"),
Received::Produced
);
assert_eq!(
(dec.phase(), dec.backend()),
(SessionPhase::Streaming, Backend::VideoToolbox),
"committed at its first delivered picture"
);
}
fn discarded(fill: u8) -> Packet {
let mut packet = raw_packet(fill);
unsafe { (*packet.as_mut_ptr()).flags |= ffmpeg_next::ffi::AV_PKT_FLAG_DISCARD };
packet
}
fn send_all(dec: &mut VideoDecoder, packets: &[Packet]) {
for (index, packet) in packets.iter().enumerate() {
let sent = unsafe { dec.send_av_packet(packet) };
assert!(
matches!(sent, Ok(Sent::Accepted)),
"packet {index} of {}: {sent:?}",
packets.len()
);
}
}
#[test]
fn a_clean_end_completes_the_candidate_whatever_it_was_fed() {
let fed: [(&str, VideoDecoder, Vec<Packet>); 3] = [
("nothing", auditioning_decoder(), Vec::new()),
("pre-roll", raw_auditioning_decoder(), vec![discarded(0x11)]),
(
"a hidden VP9 frame",
auditioning_by_name("vp9", AVCodecID::AV_CODEC_ID_VP9, crate::Discard::Default),
vec![coded(&hidden(VP9_KEY_FRAME))],
),
];
for (what, mut dec, packets) in fed {
send_all(&mut dec, &packets);
assert_eq!(
dec.send_eof().expect("the end taken"),
Sent::Accepted,
"{what}"
);
assert_eq!(dec.phase(), SessionPhase::AuditioningPastEnd, "{what}");
let mut frame = crate::Frame::empty().expect("frame slot");
assert_eq!(
dec
.receive_frame(&mut frame)
.expect("a clean end, not a backend's failure"),
Received::Ended,
"{what}"
);
assert_eq!(
dec.phase(),
SessionPhase::Draining,
"{what}: the candidate is committed"
);
}
}
#[test]
fn an_end_after_pre_roll_alone_completes_the_candidate_at_every_skip_level() {
for skip in crate::Discard::ALL {
for count in [120, 300] {
let mut dec =
raw_auditioning_decoder_with(RAW_SIDE, crate::DecoderOptions::new().with_skip_frame(skip));
let packets: Vec<Packet> = (0..count).map(|index| discarded(index as u8)).collect();
send_all(&mut dec, &packets);
assert_eq!(
dec.probe().map(|probe| probe.buffered_packets.len()),
(count <= MAX_PROBE_PACKETS).then_some(count),
"{skip}, {count}: on trial with every packet held, or committed at the ceiling"
);
assert_eq!(dec.send_eof().expect("the end taken"), Sent::Accepted);
let mut frame = crate::Frame::empty().expect("frame slot");
assert_eq!(
dec
.receive_frame(&mut frame)
.expect("the stream's end, not a backend's failure"),
Received::Ended,
"{skip}, {count}"
);
assert_eq!(
dec.session_kind(),
SessionKind::Committed,
"{skip}, {count}: the candidate is committed"
);
}
}
}
#[test]
fn the_ceiling_commits_the_candidate_once_it_takes_the_packet_past_it() {
let mut dec = raw_auditioning_decoder();
let preroll: Vec<Packet> = (0..MAX_PROBE_PACKETS)
.map(|index| discarded(index as u8))
.collect();
send_all(&mut dec, &preroll);
assert_eq!(
dec.probe().map(|probe| probe.buffered_packets.len()),
Some(MAX_PROBE_PACKETS),
"on trial at the ceiling"
);
let past = raw_packet(0x5A);
let sent = unsafe { dec.send_av_packet(&past) };
assert!(
matches!(sent, Ok(Sent::Accepted)),
"taken, and committed: {sent:?}"
);
assert_eq!(dec.phase(), SessionPhase::Streaming);
assert!(
holds_a_picture(&mut dec),
"its picture, the committed candidate's"
);
let mut dec = raw_auditioning_decoder();
{
let probe = dec.probe_mut().expect("on trial");
probe.buffered_packets.push(discarded(0x22));
probe.fed = 1;
probe.buffered_bytes = MAX_PROBE_PACKET_BYTES;
}
let sent = unsafe { dec.send_av_packet(&past) };
assert!(
matches!(sent, Ok(Sent::Accepted)),
"taken, and committed: {sent:?}"
);
assert_eq!(dec.phase(), SessionPhase::Streaming);
}
#[test]
fn a_candidate_that_refuses_the_packet_past_the_ceiling_fails_and_the_next_takes_the_whole_history()
{
let history = || {
(0..MAX_PROBE_PACKETS)
.map(|index| discarded(index as u8))
.collect::<Vec<Packet>>()
};
let at_the_ceiling = |remaining: Vec<Backend>| {
let mut dec = raw_auditioning_decoder_with(32, crate::DecoderOptions::new());
let probe = dec.probe_mut().expect("on trial");
probe.parameters = raw_parameters(RAW_SIDE);
probe.build = VideoDecoder::software_state_for_test;
probe.remaining_backends = remaining;
probe.buffered_packets = history();
probe.fed = MAX_PROBE_PACKETS;
probe.buffered_bytes = MAX_PROBE_PACKETS * RAW_PICTURE;
dec
};
let past = raw_packet(0x5A);
let mut dec = at_the_ceiling(vec![Backend::Cuda]);
let sent = unsafe { dec.send_av_packet(&past) };
assert!(
matches!(sent, Ok(Sent::Accepted)),
"the next candidate took it: {sent:?}"
);
assert_eq!(dec.backend(), Backend::Cuda, "the next candidate's");
assert_eq!(dec.phase(), SessionPhase::Streaming, "and committed");
assert!(
holds_a_picture(&mut dec),
"its picture, the committed candidate's"
);
let mut dec = at_the_ceiling(Vec::new());
let Err(Error::ProbeExhausted(p)) = (unsafe { dec.send_av_packet(&past) }) else {
panic!("the candidate fails on its own refusal, and the probe exhausts");
};
assert_eq!(p.attempts().len(), 1, "{:?}", p.attempts());
assert!(
matches!(
*p.attempts()[0].1,
Error::Ffmpeg(ffmpeg_next::Error::Other { errno }) if errno == libc::EINVAL
),
"the candidate's own refusal: {:?}",
p.attempts()
);
assert_eq!(
p.unconsumed_packets().len(),
MAX_PROBE_PACKETS,
"the whole history"
);
}
#[test]
fn a_flush_mid_probe_forgets_the_history_and_the_ceiling_counts_from_it() {
let held = |dec: &VideoDecoder| {
dec
.probe()
.map(|probe| (probe.buffered_packets.len(), probe.buffered_bytes))
};
let preroll = |count: usize| {
(0..count)
.map(|index| discarded(index as u8))
.collect::<Vec<Packet>>()
};
let mut dec = raw_auditioning_decoder();
send_all(&mut dec, &preroll(200));
assert_eq!(held(&dec), Some((200, 200 * RAW_PICTURE)));
dec.flush();
assert_eq!(held(&dec), Some((0, 0)), "forgotten, and still on trial");
send_all(&mut dec, &preroll(MAX_PROBE_PACKETS));
assert_eq!(
held(&dec),
Some((MAX_PROBE_PACKETS, MAX_PROBE_PACKETS * RAW_PICTURE)),
"the ceiling counts from the flush"
);
send_all(&mut dec, &preroll(1));
assert_eq!(held(&dec), None, "committed at the ceiling");
}
const VP9_KEY_FRAME: [u8; 26] = [
0x82, 0x49, 0x83, 0x42, 0x00, 0x03, 0xF0, 0x03, 0xF6, 0x00, 0x38, 0x24, 0x1C, 0x19, 0x70, 0x00,
0x00, 0x30, 0x60, 0x00, 0x00, 0x27, 0x82, 0x70, 0x00, 0x00,
];
fn hidden(mut frame: [u8; 26]) -> [u8; 26] {
frame[0] &= !0b10;
frame
}
const AV1_KEY_FRAME: [u8; 29] = [
0x12, 0x00, 0x0A, 0x0B, 0x02, 0x00, 0x00, 0x05, 0x15, 0x7F, 0xFC, 0x4A, 0xF9, 0x00, 0x40, 0x32,
0x0C, 0x10, 0x00, 0xF3, 0x82, 0x3F, 0xFE, 0x69, 0x83, 0x00, 0x00, 0x05, 0x80,
];
fn coded(bytes: &[u8]) -> Packet {
crate::boundary::try_packet_copy(bytes).expect("a submittable packet")
}
fn coded_parameters(id: AVCodecID) -> codec::Parameters {
let mut parameters = codec::Parameters::new();
unsafe {
let raw = parameters.as_mut_ptr();
(*raw).codec_type = AVMediaType::AVMEDIA_TYPE_VIDEO;
(*raw).codec_id = id;
(*raw).width = 64;
(*raw).height = 64;
}
parameters
}
fn maybe_auditioning_by_name(
name: &str,
id: AVCodecID,
skip: crate::Discard,
) -> Option<VideoDecoder> {
ffmpeg_next::init().expect("ffmpeg init");
let codec = ffmpeg_next::decoder::find_by_name(name)?;
Some(
VideoDecoder::from_software_for_test_as(
coded_parameters(id),
codec,
crate::limits::DecoderLimits::default(),
crate::DecoderOptions::new().with_skip_frame(skip),
true,
)
.expect("a software-backed candidate on trial"),
)
}
fn auditioning_by_name(name: &str, id: AVCodecID, skip: crate::Discard) -> VideoDecoder {
maybe_auditioning_by_name(name, id, skip).unwrap_or_else(|| panic!("FFmpeg links {name}"))
}
fn pictures(dec: &mut VideoDecoder) -> (usize, Result<Received>) {
let mut frame = crate::Frame::empty().expect("frame slot");
let mut count = 0;
loop {
match dec.receive_frame(&mut frame) {
Ok(Received::Produced) => count += 1,
other => return (count, other),
}
}
}
fn holds_a_picture(dec: &mut VideoDecoder) -> bool {
let mut picture = frame::Video::empty();
dec.decoder_mut().inner.receive_frame(&mut picture).is_ok()
}
#[test]
fn a_hidden_vp9_frame_ends_the_trial_cleanly_at_every_skip_level() {
for skip in crate::Discard::ALL {
let mut dec = auditioning_by_name("vp9", AVCodecID::AV_CODEC_ID_VP9, skip);
send_all(&mut dec, &[coded(&hidden(VP9_KEY_FRAME))]);
assert_eq!(dec.send_eof().expect("the end taken"), Sent::Accepted);
let (count, end) = pictures(&mut dec);
assert!(
count == 0 && matches!(end, Ok(Received::Ended)),
"{skip}, hidden: {count} pictures, then {end:?}"
);
assert_eq!(dec.phase(), SessionPhase::Draining, "{skip}: committed");
let mut dec = auditioning_by_name("vp9", AVCodecID::AV_CODEC_ID_VP9, skip);
send_all(&mut dec, &[coded(&VP9_KEY_FRAME)]);
assert!(holds_a_picture(&mut dec), "{skip}, shown: its picture");
}
}
#[test]
fn the_native_av1_decoder_answers_for_itself_under_every_skip_level() {
for skip in crate::Discard::ALL {
let mut dec = auditioning_by_name("av1", AVCodecID::AV_CODEC_ID_AV1, skip);
let tu = coded(&AV1_KEY_FRAME);
let sent = unsafe { dec.send_av_packet(&tu) };
if skip == crate::Discard::All {
assert!(matches!(sent, Ok(Sent::Accepted)), "{skip}: {sent:?}");
assert_eq!(dec.send_eof().expect("the end taken"), Sent::Accepted);
let (count, end) = pictures(&mut dec);
assert!(
count == 0 && matches!(end, Ok(Received::Ended)),
"{skip}: {count} pictures, then {end:?}"
);
assert_eq!(dec.phase(), SessionPhase::Draining, "{skip}: committed");
} else {
let Err(Error::ProbeExhausted(p)) = sent else {
panic!("{skip}: the candidate fails on its own error, got {sent:?}");
};
assert!(
matches!(
p.attempts(),
[(_, error)] if matches!(
**error,
Error::Ffmpeg(ffmpeg_next::Error::Other { errno }) if errno == libc::ENOSYS
)
),
"{skip}: {:?}",
p.attempts()
);
}
}
}
#[test]
fn libdav1d_gives_its_key_picture_under_every_skip_level() {
for skip in crate::Discard::ALL {
let Some(mut dec) = maybe_auditioning_by_name("libdav1d", AVCodecID::AV_CODEC_ID_AV1, skip)
else {
eprintln!("skip: this FFmpeg links no libdav1d");
return;
};
send_all(&mut dec, &[coded(&AV1_KEY_FRAME)]);
assert_eq!(dec.send_eof().expect("the end taken"), Sent::Accepted);
assert!(holds_a_picture(&mut dec), "{skip}: its key picture");
}
}
#[cfg(target_vendor = "apple")]
#[test]
#[ignore = "decodes VP9 on VideoToolbox, which a virtual machine may not offer"]
fn on_videotoolbox_a_hidden_vp9_frame_ends_cleanly_at_every_skip_level() {
use mediadecode::decoder::VideoStreamDecoder;
ffmpeg_next::init().expect("ffmpeg init");
let timebase = mediadecode::Timebase::new(1, core::num::NonZeroI32::new(25).expect("non-zero"));
for skip in crate::Discard::ALL {
let options = crate::DecoderOptions::new().with_skip_frame(skip);
for (what, frame, shown) in [
("hidden", hidden(VP9_KEY_FRAME), 0),
("shown", VP9_KEY_FRAME, 1),
] {
let mut dec = VideoDecoder::open_from(
VideoDecoderOpen::new().with_options(options),
coded_parameters(AVCodecID::AV_CODEC_ID_VP9),
)
.expect("open on the probe");
send_all(&mut dec, &[coded(&frame)]);
assert_eq!(dec.send_eof().expect("the end taken"), Sent::Accepted);
let (count, end) = pictures(&mut dec);
assert!(
count == shown && matches!(end, Ok(Received::Ended)),
"{skip}, {what}: {count} pictures, then {end:?}"
);
assert_eq!(dec.backend(), Backend::VideoToolbox, "{skip}, {what}");
assert_eq!(dec.phase(), SessionPhase::Draining, "{skip}, {what}");
let mut auto = crate::FfmpegOwnedVideoStreamDecoder::open_from(
crate::VideoStreamDecoderOpen::new().with_options(options),
coded_parameters(AVCodecID::AV_CODEC_ID_VP9),
timebase,
crate::limits::DecoderLimits::default(),
)
.expect("open on Auto");
let packet = mediadecode::packet::VideoPacket::new(
crate::FfmpegBytes::try_copy_from_slice(&frame).expect("the packet's bytes"),
crate::extras::VideoPacketExtra::new(0),
)
.with_pts(Some(mediadecode::Timestamp::new(0, timebase)));
assert_eq!(auto.send_packet(&packet).expect("taken"), Sent::Accepted);
assert_eq!(auto.send_eof().expect("the end taken"), Sent::Accepted);
let mut picture = crate::empty_owned_video_frame();
let mut count = 0;
while let Received::Produced = auto.receive_frame(&mut picture).expect("decode cleanly") {
count += 1;
}
assert_eq!(count, shown, "{skip}, {what}: Auto");
assert!(
!auto.is_software(),
"{skip}, {what}: Auto stays on hardware"
);
}
}
}
#[test]
fn dropping_a_packet_can_move_a_later_best_effort_timestamp_and_never_a_pts() {
let decoded = |stamps: &[(i64, i64)]| -> Vec<(Option<i64>, i64)> {
let mut dec = VideoDecoder::from_software_for_test(
raw_parameters(RAW_SIDE),
crate::limits::DecoderLimits::default(),
false,
)
.expect("a software-backed rawvideo decoder");
stamps
.iter()
.map(|&(pts, dts)| {
let mut packet = raw_packet(0x33);
packet.set_pts(Some(pts));
packet.set_dts(Some(dts));
dec.decoder_mut().inner.send_packet(&packet).expect("taken");
let mut picture = frame::Video::empty();
dec
.decoder_mut()
.inner
.receive_frame(&mut picture)
.expect("its picture");
let guess = unsafe { (*picture.as_ptr()).best_effort_timestamp };
(picture.pts(), guess)
})
.collect()
};
let kept = decoded(&[(10, 0), (10, 1), (100, 2)]);
let dropped = decoded(&[(10, 0), (100, 2)]);
assert_eq!(kept[2], (Some(100), 2), "the second decoded: {kept:?}");
assert_eq!(
dropped[1],
(Some(100), 100),
"the second dropped: {dropped:?}"
);
}
fn vp9_auditioning_under(bytes: usize) -> VideoDecoder {
ffmpeg_next::init().expect("ffmpeg init");
let codec = ffmpeg_next::decoder::find_by_name("vp9").expect("FFmpeg links vp9");
VideoDecoder::from_software_for_test_as(
coded_parameters(AVCodecID::AV_CODEC_ID_VP9),
codec,
crate::limits::DecoderLimits::new()
.with_frame(crate::FrameLimits::new().with_max_frame_bytes(bytes)),
crate::DecoderOptions::new(),
true,
)
.expect("a software-backed candidate on trial")
}
#[test]
fn a_packet_the_candidate_took_before_a_local_refusal_is_kept_as_taken() {
let mut dec = vp9_auditioning_under(1024);
let packet = coded(&VP9_KEY_FRAME);
let sent = unsafe { dec.send_av_packet(&packet) };
assert!(
matches!(sent, Err(Error::FrameBudgetExceeded(_))),
"the budget, by name: {sent:?}"
);
let probe = dec.probe().expect("still on trial");
assert_eq!(
(probe.buffered_packets.len(), probe.fed),
(1, 1),
"the packet, kept as taken"
);
let mut dec = vp9_auditioning_under(1024);
let crowded = with_side_data_entries(
coded(&VP9_KEY_FRAME),
MAX_PROBE_PACKET_SIDE_DATA_ENTRIES + 1,
);
let sent = unsafe { dec.send_av_packet(&crowded) };
assert!(
matches!(sent, Err(Error::FrameBudgetExceeded(_))),
"the budget, by name: {sent:?}"
);
assert_eq!(
dec.phase(),
SessionPhase::Streaming,
"a packet the history cannot keep: committed"
);
}
thread_local! {
static CUDA_FAILS: core::cell::Cell<bool> = const { core::cell::Cell::new(false) };
}
fn vaapi_refuses_and_cuda_may_fail(
parameters: codec::Parameters,
codec: Codec,
backend: Backend,
limits: crate::limits::DecoderLimits,
options: crate::DecoderOptions,
pkt_timebase: Option<mediadecode::Timebase>,
) -> std::result::Result<DecoderState, BuildFailure> {
match backend {
Backend::Vaapi => Err(BuildFailure::Refused(BackendAnswer::of_open(
Error::BackendUnsupportedByCodec(backend),
))),
_ if CUDA_FAILS.with(core::cell::Cell::get) => Err(BuildFailure::Local(Error::Ffmpeg(
ffmpeg_next::Error::Other {
errno: libc::ENOMEM,
},
))),
_ => VideoDecoder::software_state_for_test(
parameters,
codec,
backend,
limits,
options,
pkt_timebase,
),
}
}
fn tried(probe: &ProbeState) -> Vec<Backend> {
probe.attempts.iter().map(|(backend, _)| *backend).collect()
}
#[test]
fn an_advance_stopped_by_this_crates_own_failure_is_resumed_before_the_failed_candidate_is_called()
{
let mut dec = raw_auditioning_decoder();
let first = dec.backend();
{
let probe = dec.probe_mut().expect("on trial");
probe.build = vaapi_refuses_and_cuda_may_fail;
probe.remaining_backends = vec![Backend::Vaapi, Backend::Cuda];
}
send_all(&mut dec, &[discarded(0x11)]);
CUDA_FAILS.with(|fails| fails.set(true));
let stopped = dec.advance_probe(BackendAnswer::of_call(Error::Ffmpeg(
ffmpeg_next::Error::InvalidData,
)));
CUDA_FAILS.with(|fails| fails.set(false));
assert!(
matches!(
stopped,
Err(Error::Ffmpeg(ffmpeg_next::Error::Other { errno })) if errno == libc::ENOMEM
),
"this crate's own, surfaced: {stopped:?}"
);
{
assert_eq!(dec.session_kind(), SessionKind::Owed, "the advance, owed");
assert!(dec.live().is_none(), "no decoder lives while it is owed");
let probe = dec.probe().expect("the probe's record");
let pending = dec.pending().expect("the advance, owed");
assert_eq!((pending.backend, pending.tried_at), (first, 0));
assert_eq!(
tried(probe),
vec![Backend::Vaapi],
"the refusal before the stop"
);
}
assert_eq!(
dec.send_eof().expect("the end"),
Sent::Accepted,
"the end, taken by the resumed advance's candidate"
);
assert_eq!(dec.backend(), Backend::Cuda, "the advance, resumed");
{
assert_eq!(dec.session_kind(), SessionKind::Trial, "owed no more");
let probe = dec.probe().expect("still on trial");
assert_eq!(
tried(probe),
vec![first, Backend::Vaapi],
"in the order tried"
);
}
let mut frame = crate::Frame::empty().expect("frame slot");
assert_eq!(
dec.receive_frame(&mut frame).expect("a clean end"),
Received::Ended
);
assert_eq!(
(dec.backend(), dec.phase()),
(Backend::Cuda, SessionPhase::Draining),
"the new candidate committed, never the failed one"
);
}
const UYVY: i32 = ffmpeg_next::ffi::AVPixelFormat::AV_PIX_FMT_UYVY422 as i32;
fn cuda_alone_delivers(backend: Backend) -> Option<i32> {
(backend != Backend::Cuda).then_some(UYVY)
}
fn none_deliver(_: Backend) -> Option<i32> {
Some(UYVY)
}
fn all_deliver(_: Backend) -> Option<i32> {
None
}
fn vaapi_then_cuda() -> VideoDecoder {
let mut dec = raw_auditioning_decoder();
dec.decoder_mut().backend = Backend::Vaapi;
let probe = dec.probe_mut().expect("on trial");
probe.build = VideoDecoder::software_state_for_test;
probe.remaining_backends = vec![Backend::Cuda];
dec
}
fn delivered(dec: &mut VideoDecoder, frame: &mut crate::Frame) -> Vec<u8> {
let mut fills = Vec::new();
loop {
match dec
.receive_frame(frame)
.expect("a picture or a request for input")
{
Received::Produced => fills.push(frame.row(0, 0).expect("a gray picture's first row")[0]),
Received::NeedsInput | Received::Ended => return fills,
}
}
}
#[test]
fn at_the_unfit_picture_the_probe_holds_its_history_and_nothing_is_committed() {
let mut dec = vaapi_then_cuda();
let fills = [0x11u8, 0x22];
send_all(&mut dec, &fills.map(raw_packet));
let mut frame = crate::Frame::empty().expect("frame slot");
let routed = dec.route_download(Err(TransferFailure::Layout(UYVY)), &mut frame);
assert!(
matches!(routed, Ok(None)),
"left, the receive to start again: {routed:?}"
);
assert_eq!(
(dec.backend(), dec.phase()),
(Backend::Cuda, SessionPhase::Auditioning),
"the next candidate, on trial: nothing committed"
);
let probe = dec.probe().expect("still on trial");
assert_eq!(
probe.buffered_packets.len(),
fills.len(),
"the history whole"
);
assert_eq!(probe.fed, fills.len(), "replayed whole");
for (packet, &fill) in probe.buffered_packets.iter().zip(&fills) {
assert_eq!(
payload_of(packet).1,
2,
"{fill:#04x}: the history's own packet, shared with the candidate once"
);
}
assert!(
matches!(
probe.attempts.as_slice(),
[(Backend::Vaapi, Attempt::Unfit(layout))] if layout.format() == UYVY
),
"the unfitness, naming the layout: {:?}",
probe.attempts
);
}
#[test]
fn a_probe_leaves_an_unfit_candidate_and_the_next_commits_on_its_first_delivered_picture() {
let _downloads = DownloadsForTest::set(cuda_alone_delivers);
let mut dec = vaapi_then_cuda();
let fills = [0x11u8, 0x22];
send_all(&mut dec, &fills.map(raw_packet));
let mut frame = crate::Frame::empty().expect("frame slot");
let first = dec.receive_frame(&mut frame);
assert!(
matches!(first, Ok(Received::Produced)),
"the next candidate commits: {first:?}"
);
assert_eq!(
(dec.backend(), dec.phase()),
(Backend::Cuda, SessionPhase::Streaming),
"committed on its first delivered picture"
);
let mut pictures = vec![frame.row(0, 0).expect("a gray picture")[0]];
pictures.extend(delivered(&mut dec, &mut frame));
assert_eq!(pictures, fills, "every picture, in order, each once");
}
#[test]
fn a_probe_whose_last_candidate_is_unfit_is_exhausted_with_every_attempt_and_every_packet() {
let _downloads = DownloadsForTest::set(none_deliver);
let fills = [0x11u8, 0x22];
let mut frame = crate::Frame::empty().expect("frame slot");
for (name, mut dec, tried) in [
("its only candidate", raw_auditioning_decoder(), 1),
("the last of two", vaapi_then_cuda(), 2),
] {
send_all(&mut dec, &fills.map(raw_packet));
let exhausted = dec.receive_frame(&mut frame);
let Err(Error::ProbeExhausted(exhausted)) = exhausted else {
panic!("{name}: the probe exhausted: {exhausted:?}");
};
assert_eq!(exhausted.attempts().len(), tried, "{name}: every attempt");
for (backend, attempt) in exhausted.attempts() {
assert!(
matches!(**attempt, Error::DownloadFormatUnsupported(layout) if layout.format() == UYVY),
"{name}: {backend:?}'s unfitness, naming the layout, no errno: {attempt:?}"
);
}
assert_eq!(
exhausted.unconsumed_packets().len(),
fills.len(),
"{name}: every packet"
);
for (packet, &fill) in exhausted.unconsumed_packets().iter().zip(&fills) {
assert!(holds(packet, fill), "{name}: {fill:#04x}, in order");
assert_eq!(
payload_of(packet).1,
1,
"{name}: {fill:#04x}, referenced by the rescued packet alone"
);
}
}
}
fn raw_pinned_decoder() -> VideoDecoder {
ffmpeg_next::init().expect("ffmpeg init");
let parameters = raw_parameters(RAW_SIDE);
let codec = find_decoder(¶meters).expect("rawvideo");
VideoDecoder::from_software_for_test_as(
parameters,
codec,
crate::limits::DecoderLimits::default(),
crate::DecoderOptions::new(),
false,
)
.expect("a software-backed pinned decoder")
}
#[test]
fn a_backend_pinned_alone_answers_an_uncarriable_layout_by_name() {
let _downloads = DownloadsForTest::set(none_deliver);
let mut dec = raw_pinned_decoder();
send_all(&mut dec, &[raw_packet(0x11)]);
let mut frame = crate::Frame::empty().expect("frame slot");
let received = dec.receive_frame(&mut frame);
assert!(
matches!(received, Err(Error::DownloadFormatUnsupported(layout)) if layout.format() == UYVY),
"by name: {received:?}"
);
}
#[test]
fn after_the_commit_an_uncarriable_layout_is_the_committed_decoders_error() {
let mut frame = crate::Frame::empty().expect("frame slot");
let mut dec = raw_auditioning_decoder();
let backend = dec.backend();
send_all(&mut dec, &[raw_packet(0x11)]);
{
let _downloads = DownloadsForTest::set(all_deliver);
assert_eq!(delivered(&mut dec, &mut frame), vec![0x11], "delivered");
}
assert_eq!(dec.phase(), SessionPhase::Streaming, "committed");
send_all(&mut dec, &[raw_packet(0x22)]);
{
let _downloads = DownloadsForTest::set(none_deliver);
let received = dec.receive_frame(&mut frame);
assert!(
matches!(received, Err(Error::DownloadFormatUnsupported(layout)) if layout.format() == UYVY),
"the committed decoder's, by name: {received:?}"
);
}
assert_eq!(
(dec.backend(), dec.phase()),
(backend, SessionPhase::Streaming),
"still the session's decoder"
);
send_all(&mut dec, &[raw_packet(0x33)]);
{
let _downloads = DownloadsForTest::set(all_deliver);
assert_eq!(
delivered(&mut dec, &mut frame),
vec![0x33],
"the next picture"
);
}
let mut dec = raw_auditioning_decoder();
let crowded = with_side_data_entries(raw_packet(0x44), MAX_PROBE_PACKET_SIDE_DATA_ENTRIES + 1);
send_all(&mut dec, &[crowded]);
assert_eq!(
dec.phase(),
SessionPhase::Streaming,
"committed by a packet the history cannot keep"
);
let _downloads = DownloadsForTest::set(none_deliver);
let received = dec.receive_frame(&mut frame);
assert!(
matches!(received, Err(Error::DownloadFormatUnsupported(layout)) if layout.format() == UYVY),
"the committed decoder's, by name: {received:?}"
);
}
#[test]
fn a_picture_whose_properties_cannot_be_copied_is_no_delivery_and_charges_nothing() {
let mut dec = vaapi_then_cuda();
send_all(&mut dec, &[raw_packet(0x11)]);
let mut frame = crate::Frame::empty().expect("frame slot");
let enomem = ffmpeg_next::Error::Other {
errno: libc::ENOMEM,
};
let routed = dec.route_download(Err(TransferFailure::Properties(enomem)), &mut frame);
assert!(
matches!(routed, Err(Error::Ffmpeg(ffmpeg_next::Error::Other { errno })) if errno == libc::ENOMEM),
"this crate's own, as it is: {routed:?}"
);
assert_eq!(
(dec.backend(), dec.phase()),
(Backend::Vaapi, SessionPhase::Auditioning),
"no delivery: still on trial"
);
let probe = dec.probe().expect("on trial");
assert!(
probe.attempts.is_empty(),
"nothing charged: {:?}",
probe.attempts
);
assert_eq!(
probe.remaining_backends,
vec![Backend::Cuda],
"the order kept"
);
assert_eq!(probe.buffered_packets.len(), 1, "the history kept");
}
#[cfg(target_vendor = "apple")]
#[test]
fn a_videotoolbox_download_in_a_layout_a_frame_cannot_carry_is_told_apart() {
use ffmpeg_next::ffi::{
AVHWDeviceType, AVHWFramesContext, AVPixelFormat, av_buffer_unref, av_hwdevice_ctx_create,
av_hwframe_ctx_alloc, av_hwframe_ctx_init, av_hwframe_get_buffer,
};
ffmpeg_next::init().expect("ffmpeg init");
let download = |sw_format: AVPixelFormat| -> std::result::Result<(), TransferFailure> {
unsafe {
let mut device = ptr::null_mut();
assert!(
av_hwdevice_ctx_create(
&mut device,
AVHWDeviceType::AV_HWDEVICE_TYPE_VIDEOTOOLBOX,
ptr::null(),
ptr::null_mut(),
0
) >= 0,
"a VideoToolbox device"
);
let mut frames = av_hwframe_ctx_alloc(device);
assert!(!frames.is_null(), "a frames context");
let ctx = (*frames).data.cast::<AVHWFramesContext>();
(*ctx).format = AVPixelFormat::AV_PIX_FMT_VIDEOTOOLBOX;
(*ctx).sw_format = sw_format;
(*ctx).width = 16;
(*ctx).height = 16;
assert!(av_hwframe_ctx_init(frames) >= 0, "the frames context");
let mut src = frame::Video::empty();
assert!(
av_hwframe_get_buffer(frames, src.as_mut_ptr(), 0) >= 0,
"a VideoToolbox frame"
);
let mut dst = crate::Frame::empty().expect("frame slot");
let downloaded =
download_format_of(&src).and_then(|format| transfer_hw_frame(&mut dst, &src, format));
drop(src);
av_buffer_unref(&mut frames);
av_buffer_unref(&mut device);
downloaded
}
};
assert!(
matches!(
download(AVPixelFormat::AV_PIX_FMT_UYVY422),
Err(TransferFailure::Layout(format)) if format == AVPixelFormat::AV_PIX_FMT_UYVY422 as i32
),
"the layout, told apart"
);
assert!(
download(AVPixelFormat::AV_PIX_FMT_NV12).is_ok(),
"NV12 downloads whole"
);
}
const CAPPED: usize = 2048;
const SIDE_DATA_PAST_THE_CAP: usize = 4096;
fn raw_packet_with_side_data(fill: u8, bytes: usize) -> Packet {
use ffmpeg_next::ffi::{AVPacketSideDataType, av_packet_new_side_data};
let mut packet = raw_packet(fill);
let entry = unsafe {
av_packet_new_side_data(
packet.as_mut_ptr(),
AVPacketSideDataType::AV_PKT_DATA_MPEGTS_STREAM_ID,
bytes,
)
};
assert!(!entry.is_null(), "the side-data entry");
packet
}
fn fills(packets: &[Packet]) -> Vec<u8> {
packets
.iter()
.map(|packet| packet.data().expect("a payload")[0])
.collect()
}
fn committed_with_backlog(
history: Vec<Packet>,
frame: &mut crate::Frame,
) -> (VideoDecoder, Vec<u8>) {
let mut dec = vaapi_then_cuda();
{
let probe = dec.probe_mut().expect("on trial");
probe.buffered_bytes = history.len() * RAW_PICTURE;
probe.fed = history.len();
probe.buffered_packets = history;
}
dec
.advance_probe(BackendAnswer::of_call(Error::Ffmpeg(
ffmpeg_next::Error::InvalidData,
)))
.expect("CUDA installed");
assert_eq!(
dec.probe().map(|probe| probe.fed),
Some(2),
"the replay stopped at the candidate's first output"
);
let _downloads = DownloadsForTest::set(all_deliver);
let mut pictures = Vec::new();
for _ in 0..2 {
let received = dec.receive_frame(frame);
assert!(
matches!(received, Ok(Received::Produced)),
"a picture: {received:?}"
);
pictures.push(frame.row(0, 0).expect("a gray picture")[0]);
}
assert_eq!(
(dec.backend(), dec.phase()),
(Backend::Cuda, SessionPhase::Streaming),
"committed at its first delivered picture"
);
(dec, pictures)
}
fn uncapped_then_software_state(
parameters: codec::Parameters,
codec: Codec,
backend: Backend,
limits: crate::limits::DecoderLimits,
options: crate::DecoderOptions,
pkt_timebase: Option<mediadecode::Timebase>,
) -> std::result::Result<DecoderState, BuildFailure> {
mediaio_ffmpeg::test_support::uncap_ffmpeg_allocations();
VideoDecoder::software_state_for_test(parameters, codec, backend, limits, options, pkt_timebase)
}
#[test]
fn every_answer_to_a_send_is_classed_by_its_source() {
use ffmpeg_next::Error as E;
let errno = |code| E::Other { errno: code };
let (eagain, einval, enomem) = (
errno(ffmpeg_next::error::EAGAIN),
errno(libc::EINVAL),
errno(libc::ENOMEM),
);
let packet_cells = [
(false, Ok(()), Acceptance::Accepted),
(false, Err(eagain), Acceptance::NotAccepted(eagain)),
(true, Err(eagain), Acceptance::NotAccepted(eagain)),
(false, Err(E::Eof), Acceptance::NotAccepted(E::Eof)),
(true, Err(einval), Acceptance::NotAccepted(einval)),
(false, Err(einval), Acceptance::AcceptedWithError(einval)),
(false, Err(enomem), Acceptance::Ambiguous(enomem)),
(true, Err(enomem), Acceptance::Ambiguous(enomem)),
(
false,
Err(E::InvalidData),
Acceptance::AcceptedWithError(E::InvalidData),
),
(
false,
Err(E::External),
Acceptance::AcceptedWithError(E::External),
),
(
false,
Err(errno(libc::ENOSYS)),
Acceptance::AcceptedWithError(errno(libc::ENOSYS)),
),
(false, Err(E::Bug), Acceptance::AcceptedWithError(E::Bug)),
];
for (shape, answer, class) in packet_cells {
assert_eq!(
Acceptance::of_packet(shape, answer),
class,
"a packet, shape refused {shape}, answered {answer:?}"
);
}
for (answer, class) in [
(Ok(()), Acceptance::Accepted),
(Err(E::Eof), Acceptance::NotAccepted(E::Eof)),
(Err(eagain), Acceptance::NotAccepted(eagain)),
(Err(einval), Acceptance::NotAccepted(einval)),
(Err(enomem), Acceptance::NotAccepted(enomem)),
] {
assert_eq!(
Acceptance::of_end(answer),
class,
"the end, answered {answer:?}"
);
}
static BYTES: [u8; 64] = [0; 64];
let zero_size = crate::boundary::try_packet_copy(&[]).expect("data with no size");
let unbuffered = |size: i32| {
let mut packet = Packet::empty();
unsafe {
let raw = packet.as_mut_ptr();
(*raw).data = BYTES.as_ptr().cast_mut();
(*raw).size = size;
}
packet
};
assert!(refused_before_reference(&zero_size), "data with no size");
assert!(refused_before_reference(&unbuffered(-1)), "a negative size");
assert!(
refused_before_reference(&unbuffered(i32::MAX - 64)),
"a size past what packet_alloc allocates"
);
assert!(
!refused_before_reference(&unbuffered(64)),
"an unbuffered packet libavcodec copies"
);
assert!(!refused_before_reference(&raw_packet(0x11)), "a picture");
assert!(
is_flush_packet(&Packet::empty()),
"the end, in a packet's shape"
);
assert!(!is_flush_packet(&raw_packet(0x11)), "a picture");
let mut dec = raw_pinned_decoder();
assert_eq!(
send(
&mut dec.decoder_mut().inner,
Sending::Packet(&Packet::empty())
),
Acceptance::NotAccepted(einval),
"the end in a packet's shape, refused unsent"
);
assert_eq!(
send(&mut dec.decoder_mut().inner, Sending::End),
Acceptance::Accepted,
"the end, taken: nothing had started the drain"
);
let mut dec = raw_pinned_decoder();
assert_eq!(
send(&mut dec.decoder_mut().inner, Sending::Packet(&zero_size)),
Acceptance::NotAccepted(einval),
"the shape's"
);
let short = crate::boundary::try_packet_copy(&[0x77; RAW_SIDE as usize]).expect("a short packet");
assert_eq!(
send(&mut dec.decoder_mut().inner, Sending::Packet(&short)),
Acceptance::AcceptedWithError(einval),
"the decode's"
);
let mut refused = unbuffered(-1);
assert_eq!(
send(&mut dec.decoder_mut().inner, Sending::Packet(&refused)),
Acceptance::NotAccepted(einval),
"packet_alloc's, inside av_packet_ref"
);
unsafe { (*refused.as_mut_ptr()).data = ptr::null_mut() };
}
#[test]
fn an_ambiguous_send_of_the_backlogs_front_ends_the_decoder_and_hands_it_back() {
use mediaio_ffmpeg::test_support::{
cap_ffmpeg_allocations, in_subprocess, uncap_ffmpeg_allocations,
};
in_subprocess(
"decoder::tests::an_ambiguous_send_of_the_backlogs_front_ends_the_decoder_and_hands_it_back",
|| {
let mut frame = crate::Frame::empty().expect("frame slot");
let history = vec![
raw_packet(0x11),
raw_packet(0x22),
raw_packet_with_side_data(0x33, SIDE_DATA_PAST_THE_CAP),
raw_packet(0x44),
];
let (mut dec, mut pictures) = committed_with_backlog(history, &mut frame);
assert_eq!(pictures, vec![0x11, 0x22], "the committed decoder's");
assert_eq!(
dec
.backlog()
.iter()
.map(|packet| packet.data().expect("a payload")[0])
.collect::<Vec<u8>>(),
vec![0x33, 0x44],
"the replay's rest, owed"
);
let _downloads = DownloadsForTest::set(all_deliver);
unsafe { cap_ffmpeg_allocations(CAPPED) };
let ended = dec.receive_frame(&mut frame);
uncap_ffmpeg_allocations();
let Err(Error::AmbiguousSend(ended)) = ended else {
panic!("ended: {ended:?}");
};
assert!(
matches!(
ended.reason(),
Error::Ffmpeg(ffmpeg_next::Error::Other { errno }) if *errno == libc::ENOMEM
),
"libavcodec's answer, as it returned it: {ended:?}"
);
assert_eq!(ended.backend(), Backend::Cuda, "the committed decoder's");
assert!(!ended.end_follows(), "no end was given");
assert_eq!(
fills(ended.residual()),
vec![0x33, 0x44],
"the front, and every packet behind it"
);
for packet in ended.residual() {
assert_eq!(payload_of(packet).1, 1, "referenced by its packet alone");
}
assert_spent(&mut dec, crate::Spent::AmbiguousSend, SessionKind::Ended);
let mut next = raw_pinned_decoder();
send_all(&mut next, ended.residual());
pictures.extend(delivered(&mut next, &mut frame));
assert_eq!(
pictures,
vec![0x11, 0x22, 0x33, 0x44],
"every picture once, in order"
);
},
);
}
#[test]
fn back_pressure_on_the_backlog_keeps_its_front() {
let mut frame = crate::Frame::empty().expect("frame slot");
let sent = [0x11u8, 0x22, 0x33, 0x44, 0x55, 0x66];
let (mut dec, mut pictures) = committed_with_backlog(Vec::from(sent.map(raw_packet)), &mut frame);
assert_eq!(dec.backlog().len(), 4, "the replay's rest");
let fed = dec.feed().expect("taken, then back pressure");
assert_eq!(
fed,
Fed {
taken: 2,
all: false
},
"two taken, the third not"
);
assert!(
holds(dec.backlog().front().expect("a front"), 0x55),
"the packet it did not take, kept"
);
assert_eq!(dec.backlog().len(), 2, "the rest behind it");
let _downloads = DownloadsForTest::set(all_deliver);
pictures.extend(delivered(&mut dec, &mut frame));
assert_eq!(pictures, sent, "every picture once, in order");
}
#[test]
fn on_trial_an_ambiguous_send_beside_a_refusal_of_this_crates_own_ends_the_decoder() {
use mediaio_ffmpeg::test_support::{
cap_ffmpeg_allocations, in_subprocess, uncap_ffmpeg_allocations,
};
in_subprocess(
"decoder::tests::on_trial_an_ambiguous_send_beside_a_refusal_of_this_crates_own_ends_the_decoder",
|| {
let mut dec = raw_auditioning_decoder();
send_all(&mut dec, &[raw_packet(0x11)]);
let caller = raw_packet_with_side_data(0x22, SIDE_DATA_PAST_THE_CAP);
unsafe {
crate::ffi::declare_frame_budget_declined_for_test(dec.decoder().callback_state, 12_582_912)
};
unsafe { cap_ffmpeg_allocations(CAPPED) };
let answer = unsafe { dec.send_av_packet(&caller) };
uncap_ffmpeg_allocations();
let Err(Error::AmbiguousSend(ended)) = answer else {
panic!("ended: {answer:?}");
};
assert!(
matches!(ended.reason(), Error::FrameBudgetExceeded(p) if p.bytes() == 12_582_912),
"this crate's refusal, by name: {ended:?}"
);
assert_eq!(
fills(ended.residual()),
vec![0x11],
"the whole history, and not the caller's packet"
);
assert_eq!(
dec.session_kind(),
SessionKind::Ended,
"the probe ended with it"
);
for packet in ended.residual() {
assert_eq!(payload_of(packet).1, 1, "referenced by its packet alone");
}
assert_eq!(
payload_of(&caller).1,
1,
"the caller's, referenced by it alone"
);
assert_spent(&mut dec, crate::Spent::AmbiguousSend, SessionKind::Ended);
},
);
}
#[test]
fn on_trial_an_ambiguous_send_leaves_the_candidate_and_the_next_takes_the_packet_once() {
use mediaio_ffmpeg::test_support::{
cap_ffmpeg_allocations, in_subprocess, uncap_ffmpeg_allocations,
};
in_subprocess(
"decoder::tests::on_trial_an_ambiguous_send_leaves_the_candidate_and_the_next_takes_the_packet_once",
|| {
let mut dec = vaapi_then_cuda();
dec.probe_mut().expect("on trial").build = uncapped_then_software_state;
send_all(&mut dec, &[raw_packet(0x11)]);
let caller = raw_packet_with_side_data(0x22, SIDE_DATA_PAST_THE_CAP);
unsafe { cap_ffmpeg_allocations(CAPPED) };
let answer = unsafe { dec.send_av_packet(&caller) };
uncap_ffmpeg_allocations();
assert!(
matches!(answer, Ok(Sent::Accepted)),
"the next candidate took it: {answer:?}"
);
assert_eq!(dec.backend(), Backend::Cuda, "the next candidate's");
{
let probe = dec.probe().expect("still on trial");
assert!(
matches!(
probe.attempts.as_slice(),
[(Backend::Vaapi, Attempt::Failed(BackendAnswer(Error::Ffmpeg(ffmpeg_next::Error::Other { errno }))))]
if *errno == libc::ENOMEM
),
"the candidate's answer, as libavcodec returned it: {:?}",
probe.attempts
);
assert_eq!(
fills(&probe.buffered_packets),
vec![0x11, 0x22],
"recorded once, in order"
);
assert_eq!(probe.fed, 2, "fed whole");
}
let _downloads = DownloadsForTest::set(all_deliver);
let mut frame = crate::Frame::empty().expect("frame slot");
assert_eq!(
delivered(&mut dec, &mut frame),
vec![0x11, 0x22],
"every picture once, in order"
);
},
);
}
#[test]
fn while_the_decoder_is_owed_a_send_takes_nothing_and_an_owed_packets_answer_is_the_receives() {
let mut frame = crate::Frame::empty().expect("frame slot");
let short = crate::boundary::try_packet_copy(&[0x77; 8]).expect("a packet short of a picture");
let history = vec![raw_packet(0x11), raw_packet(0x22), short, raw_packet(0x44)];
let (mut dec, mut pictures) = committed_with_backlog(history, &mut frame);
let caller = raw_packet(0x55);
let sent = unsafe { dec.send_av_packet(&caller) };
assert!(
matches!(sent, Ok(Sent::MustDrain)),
"nothing sent while the decoder is owed: {sent:?}"
);
assert_eq!(dec.backlog().len(), 2, "the backlog, untouched");
let _downloads = DownloadsForTest::set(all_deliver);
let refused = dec.receive_frame(&mut frame);
assert!(
matches!(refused, Err(Error::Ffmpeg(ffmpeg_next::Error::InvalidData))),
"the owed packet's own error, on the receive road: {refused:?}"
);
pictures.extend(delivered(&mut dec, &mut frame));
let sent = unsafe { dec.send_av_packet(&caller) };
assert!(matches!(sent, Ok(Sent::Accepted)), "then taken: {sent:?}");
pictures.extend(delivered(&mut dec, &mut frame));
assert_eq!(
pictures,
vec![0x11, 0x22, 0x44, 0x55],
"every picture the decoder could give, once, in order"
);
}
#[test]
fn a_packet_with_neither_payload_nor_side_data_is_the_end() {
let mut dec = raw_auditioning_decoder();
send_all(&mut dec, &[raw_packet(0x11)]);
let empty = Packet::empty();
let sent = unsafe { dec.send_av_packet(&empty) };
assert!(matches!(sent, Ok(Sent::Accepted)), "the end: {sent:?}");
assert_eq!(dec.end(), Some(End::Taken), "recorded as the end");
assert_eq!(
dec.probe().map(|probe| probe.buffered_packets.len()),
Some(1),
"the history holds no packet for it"
);
let again = unsafe { dec.send_av_packet(&empty) };
assert!(
matches!(again, Err(Error::Ffmpeg(ffmpeg_next::Error::Eof))),
"a second end: {again:?}"
);
let _downloads = DownloadsForTest::set(all_deliver);
let mut frame = crate::Frame::empty().expect("frame slot");
assert_eq!(delivered(&mut dec, &mut frame), vec![0x11], "its picture");
assert_eq!(
dec.phase(),
SessionPhase::Draining,
"committed, the stream over"
);
}
#[test]
fn a_packet_of_a_shape_libavcodec_refuses_charges_no_candidate() {
let mut dec = raw_auditioning_decoder();
send_all(&mut dec, &[raw_packet(0x11)]);
let zero_size = crate::boundary::try_packet_copy(&[]).expect("data with no size");
let refused = unsafe { dec.send_av_packet(&zero_size) };
assert!(
matches!(
refused,
Err(Error::Ffmpeg(ffmpeg_next::Error::Other { errno })) if errno == libc::EINVAL
),
"refused: {refused:?}"
);
assert_eq!(dec.phase(), SessionPhase::Auditioning, "still on trial");
let probe = dec.probe().expect("on trial");
assert!(
probe.attempts.is_empty(),
"no candidate charged: {:?}",
probe.attempts
);
assert_eq!(probe.buffered_packets.len(), 1, "nothing recorded");
}
#[test]
fn a_packet_with_no_payload_is_copied_with_none_and_libavcodec_takes_the_copy() {
use ffmpeg_next::ffi::{AVPacketSideDataType, av_packet_new_side_data};
let mut source = Packet::empty();
let entry = unsafe {
av_packet_new_side_data(
source.as_mut_ptr(),
AVPacketSideDataType::AV_PKT_DATA_MPEGTS_STREAM_ID,
1,
)
};
assert!(!entry.is_null(), "the side-data entry");
assert!(
!is_flush_packet(&source) && !refused_before_reference(&source),
"a packet libavcodec takes"
);
let copy = unsafe { crate::boundary::try_clone_packet(&source) }.expect("a copy");
let (data, entries) = unsafe {
let raw = copy.as_ptr();
((*raw).data, (*raw).side_data_elems)
};
assert!(data.is_null(), "no payload");
assert_eq!(entries, 1, "the side data, copied");
assert!(!refused_before_reference(©), "a shape libavcodec takes");
let mut dec = raw_pinned_decoder();
let picture = raw_packet(0x11);
assert_eq!(
send(&mut dec.decoder_mut().inner, Sending::Packet(&picture)),
Acceptance::Accepted
);
assert_eq!(
send(&mut dec.decoder_mut().inner, Sending::Packet(©)),
Acceptance::Accepted,
"taken, as the source is"
);
}
#[test]
fn a_send_whose_advance_is_owed_does_not_take_the_packet_and_the_next_candidate_does() {
let mut dec = raw_auditioning_decoder_with(32, crate::DecoderOptions::new());
let first = dec.backend();
{
let probe = dec.probe_mut().expect("on trial");
probe.parameters = raw_parameters(RAW_SIDE);
probe.build = vaapi_refuses_and_cuda_may_fail;
probe.remaining_backends = vec![Backend::Vaapi, Backend::Cuda];
}
let picture = raw_packet(0x11);
CUDA_FAILS.with(|fails| fails.set(true));
let sent = unsafe { dec.send_av_packet(&picture) };
CUDA_FAILS.with(|fails| fails.set(false));
assert!(
matches!(sent, Ok(Sent::MustDrain)),
"not taken, the advance owed: {sent:?}"
);
{
assert_eq!(dec.session_kind(), SessionKind::Owed, "the advance, owed");
let probe = dec.probe().expect("the probe's record");
assert!(
probe.buffered_packets.is_empty(),
"the history never held it"
);
assert_eq!(
tried(probe),
vec![Backend::Vaapi],
"the refusal before the stop"
);
}
let mut frame = crate::Frame::empty().expect("frame slot");
assert_eq!(
dec
.receive_frame(&mut frame)
.expect("the drain resumes the advance"),
Received::NeedsInput
);
assert_eq!(dec.backend(), Backend::Cuda, "the advance, resumed");
let sent = unsafe { dec.send_av_packet(&picture) };
assert!(
matches!(sent, Ok(Sent::Accepted)),
"the next candidate's: {sent:?}"
);
{
let probe = dec.probe().expect("on trial");
assert_eq!(fills(&probe.buffered_packets), vec![0x11], "recorded once");
assert_eq!(
tried(probe),
vec![first, Backend::Vaapi],
"in the order tried"
);
}
let _downloads = DownloadsForTest::set(all_deliver);
assert_eq!(
delivered(&mut dec, &mut frame),
vec![0x11],
"its picture, once"
);
}
#[test]
fn back_pressure_on_a_send_is_answered_for_the_packet_alone() {
let mut dec = raw_pinned_decoder();
send_all(&mut dec, &[raw_packet(0x11), raw_packet(0x22)]);
unsafe {
crate::ffi::declare_frame_budget_declined_for_test(dec.decoder().callback_state, 12_582_912)
};
let third = raw_packet(0x33);
let sent = unsafe { dec.send_av_packet(&third) };
assert!(
matches!(sent, Ok(Sent::MustDrain)),
"not taken: back pressure, and nothing else: {sent:?}"
);
let _downloads = DownloadsForTest::set(all_deliver);
let mut frame = crate::Frame::empty().expect("frame slot");
let mut pictures = Vec::new();
loop {
match dec.receive_frame(&mut frame) {
Ok(Received::Produced) => pictures.push(frame.row(0, 0).expect("a gray picture")[0]),
Err(Error::FrameBudgetExceeded(p)) => {
assert_eq!(p.bytes(), 12_582_912, "the refusal's own numbers");
break;
}
other => panic!("a picture, or the refusal: {other:?}"),
}
}
assert_eq!(pictures, vec![0x11, 0x22], "both pictures first");
let sent = unsafe { dec.send_av_packet(&third) };
assert!(matches!(sent, Ok(Sent::Accepted)), "then taken: {sent:?}");
assert_eq!(delivered(&mut dec, &mut frame), vec![0x33], "its picture");
}
fn decoders_dropped() -> usize {
DECODER_STATES_DROPPED.with(core::cell::Cell::get)
}
fn every_backend_refuses(
_: codec::Parameters,
_: Codec,
backend: Backend,
_: crate::limits::DecoderLimits,
_: crate::DecoderOptions,
_: Option<mediadecode::Timebase>,
) -> std::result::Result<DecoderState, BuildFailure> {
Err(BuildFailure::Refused(BackendAnswer::of_open(
Error::BackendUnsupportedByCodec(backend),
)))
}
fn uncapped_vaapi_refuses_and_cuda_may_fail(
parameters: codec::Parameters,
codec: Codec,
backend: Backend,
limits: crate::limits::DecoderLimits,
options: crate::DecoderOptions,
pkt_timebase: Option<mediadecode::Timebase>,
) -> std::result::Result<DecoderState, BuildFailure> {
mediaio_ffmpeg::test_support::uncap_ffmpeg_allocations();
vaapi_refuses_and_cuda_may_fail(parameters, codec, backend, limits, options, pkt_timebase)
}
fn on_trial_with(build: BuildCandidate, remaining: Vec<Backend>) -> VideoDecoder {
let mut dec = raw_auditioning_decoder();
let probe = dec.probe_mut().expect("on trial");
probe.build = build;
probe.remaining_backends = remaining;
dec
}
fn its_own_failure() -> BackendAnswer {
BackendAnswer::of_call(Error::Ffmpeg(ffmpeg_next::Error::InvalidData))
}
fn is_own_enomem<T>(answer: &Result<T>) -> bool {
matches!(answer, Err(Error::Ffmpeg(ffmpeg_next::Error::Other { errno })) if *errno == libc::ENOMEM)
}
#[test]
fn a_candidate_left_on_a_send_is_torn_down_before_the_send_answers_not_taken() {
let mut dec = raw_auditioning_decoder_with(32, crate::DecoderOptions::new());
let first = dec.backend();
{
let probe = dec.probe_mut().expect("on trial");
probe.parameters = raw_parameters(RAW_SIDE);
probe.build = vaapi_refuses_and_cuda_may_fail;
probe.remaining_backends = vec![Backend::Vaapi, Backend::Cuda];
}
let picture = raw_packet(0x11);
let dropped = decoders_dropped();
CUDA_FAILS.with(|fails| fails.set(true));
let sent = unsafe { dec.send_av_packet(&picture) };
CUDA_FAILS.with(|fails| fails.set(false));
assert!(
matches!(sent, Ok(Sent::MustDrain)),
"not taken, the advance owed: {sent:?}"
);
assert_eq!(
decoders_dropped() - dropped,
1,
"the candidate left, torn down before the answer"
);
assert_eq!(dec.session_kind(), SessionKind::Owed, "the advance, owed");
assert!(dec.live().is_none(), "no decoder lives while it is owed");
assert_eq!(
payload_of(&picture).1,
1,
"the caller's packet, referenced by its own alone"
);
{
let pending = dec.pending().expect("the advance, owed");
assert_eq!(pending.backend, first, "the candidate left");
assert!(
matches!(pending.attempt, Attempt::Failed(_)),
"its own answer: {:?}",
pending.attempt
);
}
let mut frame = crate::Frame::empty().expect("frame slot");
assert_eq!(
dec
.receive_frame(&mut frame)
.expect("the drain resumes the advance"),
Received::NeedsInput
);
assert_eq!(
(dec.backend(), dec.session_kind()),
(Backend::Cuda, SessionKind::Trial),
"the next candidate, on trial"
);
let sent = unsafe { dec.send_av_packet(&picture) };
assert!(
matches!(sent, Ok(Sent::Accepted)),
"fed to the next candidate: {sent:?}"
);
assert_eq!(
fills(&dec.probe().expect("on trial").buffered_packets),
vec![0x11],
"recorded once"
);
let _downloads = DownloadsForTest::set(all_deliver);
assert_eq!(
delivered(&mut dec, &mut frame),
vec![0x11],
"its picture, once"
);
}
#[test]
fn an_ambiguous_send_whose_advance_is_owed_leaves_no_reference_behind() {
use mediaio_ffmpeg::test_support::{
cap_ffmpeg_allocations, in_subprocess, uncap_ffmpeg_allocations,
};
in_subprocess(
"decoder::tests::an_ambiguous_send_whose_advance_is_owed_leaves_no_reference_behind",
|| {
let mut dec = on_trial_with(
uncapped_vaapi_refuses_and_cuda_may_fail,
vec![Backend::Vaapi, Backend::Cuda],
);
let first = dec.backend();
let held = raw_packet(0x11);
let sent = unsafe { dec.send_av_packet(&held) };
assert!(matches!(sent, Ok(Sent::Accepted)), "taken: {sent:?}");
assert_eq!(
payload_of(&held).1,
2,
"the candidate holds the caller's first picture, by reference"
);
let caller = raw_packet_with_side_data(0x22, SIDE_DATA_PAST_THE_CAP);
let dropped = decoders_dropped();
CUDA_FAILS.with(|fails| fails.set(true));
unsafe { cap_ffmpeg_allocations(CAPPED) };
let sent = unsafe { dec.send_av_packet(&caller) };
uncap_ffmpeg_allocations();
CUDA_FAILS.with(|fails| fails.set(false));
assert!(
matches!(sent, Ok(Sent::MustDrain)),
"not taken, the advance owed: {sent:?}"
);
assert_eq!(
decoders_dropped() - dropped,
1,
"the candidate left, torn down before the answer"
);
assert_eq!(dec.session_kind(), SessionKind::Owed, "the advance, owed");
assert!(dec.live().is_none(), "no decoder lives while it is owed");
assert_eq!(
payload_of(&held).1,
1,
"the caller's first packet, released"
);
assert_eq!(
payload_of(&caller).1,
1,
"the caller's second, referenced by its own alone"
);
{
let probe = dec.probe().expect("the probe's record");
assert_eq!(fills(&probe.buffered_packets), vec![0x11], "the history");
for packet in &probe.buffered_packets {
assert_eq!(
payload_of(packet).1,
1,
"the history's copy, by its own alone"
);
}
let pending = dec.pending().expect("the advance, owed");
assert!(
matches!(
&pending.attempt,
Attempt::Failed(BackendAnswer(Error::Ffmpeg(ffmpeg_next::Error::Other { errno })))
if *errno == libc::ENOMEM
),
"{first:?}'s answer, the errno as libavcodec returned it: {:?}",
pending.attempt
);
}
let _downloads = DownloadsForTest::set(all_deliver);
let mut frame = crate::Frame::empty().expect("frame slot");
let mut pictures = delivered(&mut dec, &mut frame);
assert_eq!(dec.backend(), Backend::Cuda, "the advance, resumed");
let sent = unsafe { dec.send_av_packet(&caller) };
assert!(matches!(sent, Ok(Sent::Accepted)), "then taken: {sent:?}");
pictures.extend(delivered(&mut dec, &mut frame));
assert_eq!(pictures, vec![0x11, 0x22], "every picture once, in order");
},
);
}
#[test]
fn after_the_end_and_the_probes_exhaustion_no_packet_is_ever_taken() {
let _downloads = DownloadsForTest::set(none_deliver);
let mut dec = raw_auditioning_decoder();
send_all(&mut dec, &[raw_packet(0x11)]);
assert_eq!(dec.send_eof().expect("the end"), Sent::Accepted);
let mut frame = crate::Frame::empty().expect("frame slot");
let exhausted = dec.receive_frame(&mut frame);
let Err(Error::ProbeExhausted(exhausted)) = exhausted else {
panic!("the probe exhausted: {exhausted:?}");
};
assert_eq!(
fills(exhausted.unconsumed_packets()),
vec![0x11],
"every packet back"
);
for when in ["at once", "after a flush"] {
let packet = raw_packet(0x22);
let sent = unsafe { dec.send_av_packet(&packet) };
assert!(
matches!(sent, Err(Error::Spent(crate::Spent::ProbeExhausted))),
"{when}: never taken: {sent:?}"
);
dec.flush();
}
assert_spent(
&mut dec,
crate::Spent::ProbeExhausted,
SessionKind::Exhausted,
);
}
#[test]
fn an_exhausted_probe_is_spent_on_every_road_it_exhausts_on() {
for road in ["a send", "the end"] {
let mut dec = raw_auditioning_decoder();
assert_eq!(dec.send_eof().expect("the end"), Sent::Accepted);
unsafe {
crate::ffi::declare_ceiling_declined_for_test(
dec.decoder().callback_state,
8_294_400,
2_073_600,
)
};
let packet = raw_packet(0x11);
let answer = if road == "a send" {
unsafe { dec.send_av_packet(&packet) }
} else {
dec.send_eof()
};
assert!(
matches!(answer, Err(Error::ProbeExhausted(_))),
"{road}: exhausted: {answer:?}"
);
assert_spent(
&mut dec,
crate::Spent::ProbeExhausted,
SessionKind::Exhausted,
);
}
let _downloads = DownloadsForTest::set(none_deliver);
let mut dec = raw_auditioning_decoder();
send_all(&mut dec, &[raw_packet(0x11)]);
let mut frame = crate::Frame::empty().expect("frame slot");
let answer = dec.receive_frame(&mut frame);
assert!(
matches!(answer, Err(Error::ProbeExhausted(_))),
"a receive: exhausted: {answer:?}"
);
assert_spent(
&mut dec,
crate::Spent::ProbeExhausted,
SessionKind::Exhausted,
);
}
#[test]
fn a_downloads_own_allocation_charges_no_backend_and_the_backends_transfer_does() {
let enomem = ffmpeg_next::Error::Other {
errno: libc::ENOMEM,
};
let mut dec = vaapi_then_cuda();
send_all(&mut dec, &[raw_packet(0x11)]);
let mut frame = crate::Frame::empty().expect("frame slot");
let routed = dec.route_download(Err(TransferFailure::Unattributed(enomem)), &mut frame);
assert!(
is_own_enomem(&routed),
"this crate's own, as it is: {routed:?}"
);
assert_eq!(
(dec.backend(), dec.session_kind()),
(Backend::Vaapi, SessionKind::Trial),
"no delivery: still on trial"
);
{
let probe = dec.probe().expect("on trial");
assert!(
probe.attempts.is_empty(),
"nothing charged: {:?}",
probe.attempts
);
assert_eq!(
probe.remaining_backends,
vec![Backend::Cuda],
"the order kept"
);
assert_eq!(
fills(&probe.buffered_packets),
vec![0x11],
"the history kept"
);
}
let routed = dec.route_download(Err(TransferFailure::Download(enomem)), &mut frame);
assert!(
matches!(routed, Ok(None)),
"the backend's: left, the receive to start again: {routed:?}"
);
assert_eq!(
(dec.backend(), dec.session_kind()),
(Backend::Cuda, SessionKind::Trial),
"the next candidate, on trial"
);
{
let probe = dec.probe().expect("on trial");
assert!(
matches!(
probe.attempts.as_slice(),
[(Backend::Vaapi, Attempt::Failed(BackendAnswer(Error::Ffmpeg(ffmpeg_next::Error::Other { errno }))))]
if *errno == libc::ENOMEM
),
"the backend's answer, recorded: {:?}",
probe.attempts
);
}
let mut dec = raw_pinned_decoder();
for failure in [
TransferFailure::Unattributed(enomem),
TransferFailure::Download(enomem),
] {
let routed = dec.route_download(Err(failure), &mut frame);
assert!(
is_own_enomem(&routed),
"the picture's own error: {routed:?}"
);
assert_eq!(
dec.session_kind(),
SessionKind::Committed,
"the decoder stays"
);
}
}
#[cfg(target_vendor = "apple")]
struct VideoToolboxFrame {
frame: frame::Video,
frames: *mut AVBufferRef,
device: *mut AVBufferRef,
}
#[cfg(target_vendor = "apple")]
impl VideoToolboxFrame {
fn new(sw_format: ffmpeg_next::ffi::AVPixelFormat, side: i32) -> Self {
use ffmpeg_next::ffi::{
AVHWDeviceType, AVPixelFormat, av_hwdevice_ctx_create, av_hwframe_ctx_alloc,
av_hwframe_ctx_init, av_hwframe_get_buffer,
};
ffmpeg_next::init().expect("ffmpeg init");
unsafe {
let mut device = ptr::null_mut();
assert!(
av_hwdevice_ctx_create(
&mut device,
AVHWDeviceType::AV_HWDEVICE_TYPE_VIDEOTOOLBOX,
ptr::null(),
ptr::null_mut(),
0
) >= 0,
"a VideoToolbox device"
);
let frames = av_hwframe_ctx_alloc(device);
assert!(!frames.is_null(), "a frames context");
let ctx = (*frames).data.cast::<AVHWFramesContext>();
(*ctx).format = AVPixelFormat::AV_PIX_FMT_VIDEOTOOLBOX;
(*ctx).sw_format = sw_format;
(*ctx).width = side;
(*ctx).height = side;
assert!(av_hwframe_ctx_init(frames) >= 0, "the frames context");
let mut frame = frame::Video::empty();
assert!(
av_hwframe_get_buffer(frames, frame.as_mut_ptr(), 0) >= 0,
"a VideoToolbox frame"
);
Self {
frame,
frames,
device,
}
}
}
}
#[cfg(target_vendor = "apple")]
impl Drop for VideoToolboxFrame {
fn drop(&mut self) {
unsafe {
av_frame_unref(self.frame.as_mut_ptr());
av_buffer_unref(&mut self.frames);
av_buffer_unref(&mut self.device);
}
}
}
fn download_answer(answer: &std::result::Result<(), TransferFailure>) -> String {
match answer {
Ok(()) => "delivered".to_owned(),
Err(failure) => failure.to_string(),
}
}
#[cfg(target_vendor = "apple")]
#[test]
fn on_videotoolbox_the_download_destination_is_this_crates_own_and_the_callback_the_backends() {
use ffmpeg_next::ffi::AVPixelFormat;
use mediaio_ffmpeg::test_support::{
cap_ffmpeg_allocations, in_subprocess, uncap_ffmpeg_allocations,
};
in_subprocess(
"decoder::tests::on_videotoolbox_the_download_destination_is_this_crates_own_and_the_callback_the_backends",
|| {
let src = VideoToolboxFrame::new(AVPixelFormat::AV_PIX_FMT_NV12, 256);
unsafe {
let format = download_format_of(&src.frame).map_err(|failure| failure.to_string());
assert_eq!(
format,
Ok(DownloadFormat::Picked(NV12)),
"the format, picked first"
);
let format = DownloadFormat::Picked(NV12);
let mut dst = crate::Frame::empty().expect("frame slot");
cap_ffmpeg_allocations(16 * 1024);
let capped = transfer_hw_frame(&mut dst, &src.frame, format);
uncap_ffmpeg_allocations();
assert!(
matches!(&capped, Err(TransferFailure::Unattributed(e)) if is_errno(e, libc::ENOMEM)),
"the destination's allocation, this crate's own: {}",
download_answer(&capped)
);
assert_eq!(
(*dst.as_inner_mut().as_ptr()).format,
AVPixelFormat::AV_PIX_FMT_NONE as i32,
"the slot left empty"
);
let delivered = transfer_hw_frame(&mut dst, &src.frame, format);
assert!(
delivered.is_ok(),
"uncapped, delivered: {}",
download_answer(&delivered)
);
let raw = dst.as_inner_mut().as_ptr();
assert_eq!(
((*raw).format, (*raw).width, (*raw).height),
(NV12, 256, 256),
"in the format picked, at the display extent"
);
let p010 = AVPixelFormat::AV_PIX_FMT_P010LE as i32;
let mismatched = transfer_hw_frame(&mut dst, &src.frame, DownloadFormat::Picked(p010));
assert!(
matches!(
mismatched,
Err(TransferFailure::Download(ffmpeg_next::Error::Unknown))
),
"the callback's own answer, the backend's: {}",
download_answer(&mismatched)
);
cap_ffmpeg_allocations(1);
let listed = download_format_of(&src.frame);
uncap_ffmpeg_allocations();
assert!(
matches!(&listed, Err(TransferFailure::Unattributed(e)) if is_errno(e, libc::ENOMEM)),
"the list's allocation, this crate's own: {:?}",
listed.map_err(|failure| failure.to_string())
);
}
},
);
}
fn owed_the_history(end_given: bool) -> VideoDecoder {
let mut dec = vaapi_then_cuda();
{
let probe = dec.probe_mut().expect("on trial");
probe.buffered_packets = [0x11, 0x22, 0x33, 0x44].map(raw_packet).into();
probe.fed = 4;
probe.buffered_bytes = 4 * RAW_PICTURE;
}
if end_given {
dec.set_end(End::Taken);
}
dec
.advance_probe(its_own_failure())
.expect("CUDA installed");
assert_eq!(
(dec.backend(), dec.session_kind()),
(Backend::Cuda, SessionKind::Trial),
"the next candidate, on trial"
);
assert_eq!(
dec.probe().map(|probe| probe.fed),
Some(2),
"the replay stopped at the candidate's first output"
);
dec
}
#[test]
fn on_trial_with_history_owed_a_send_and_the_end_drain_first() {
let mut dec = owed_the_history(false);
let packet = raw_packet(0x55);
let sent = unsafe { dec.send_av_packet(&packet) };
assert!(matches!(sent, Ok(Sent::MustDrain)), "a send: {sent:?}");
let ended = dec.send_eof();
assert!(matches!(ended, Ok(Sent::MustDrain)), "the end: {ended:?}");
let probe = dec.probe().expect("on trial");
assert_eq!(
(probe.buffered_packets.len(), probe.fed),
(4, 2),
"nothing sent, nothing recorded"
);
assert_eq!(dec.end(), Some(End::Open), "the end not taken");
let mut dec = raw_auditioning_decoder();
send_all(&mut dec, &[raw_packet(0x11), raw_packet(0x22)]);
let sent = unsafe { dec.send_av_packet(&packet) };
assert!(
matches!(sent, Ok(Sent::MustDrain)),
"back pressure: {sent:?}"
);
assert_eq!(
fills(&dec.probe().expect("on trial").buffered_packets),
vec![0x11, 0x22],
"the copy dropped"
);
}
#[test]
fn with_the_end_owed_a_send_and_a_second_end_are_refused_on_trial_and_committed() {
let mut dec = owed_the_history(true);
assert_eq!(dec.end(), Some(End::Owed), "the end, owed to the candidate");
assert_eq!(dec.phase(), SessionPhase::AuditioningPastEnd);
let packet = raw_packet(0x55);
for state in ["on trial", "committed"] {
let sent = unsafe { dec.send_av_packet(&packet) };
assert!(
matches!(sent, Err(Error::Ffmpeg(ffmpeg_next::Error::Eof))),
"{state}: a send after the end: {sent:?}"
);
let ended = dec.send_eof();
assert!(
matches!(ended, Err(Error::Ffmpeg(ffmpeg_next::Error::Eof))),
"{state}: a second end: {ended:?}"
);
if state == "on trial" {
let probe = dec.probe().expect("on trial");
assert_eq!(
(probe.buffered_packets.len(), probe.fed),
(4, 2),
"nothing sent"
);
let _downloads = DownloadsForTest::set(all_deliver);
let mut frame = crate::Frame::empty().expect("frame slot");
assert_eq!(
dec.receive_frame(&mut frame).expect("the first picture"),
Received::Produced
);
assert_eq!(dec.session_kind(), SessionKind::Committed, "committed");
assert_eq!(dec.end(), Some(End::Owed), "the end, still owed");
assert_eq!(
dec
.backlog()
.iter()
.map(|packet| packet.data().expect("a payload")[0])
.collect::<Vec<u8>>(),
vec![0x33, 0x44],
"the backlog"
);
}
}
let _downloads = DownloadsForTest::set(all_deliver);
let mut frame = crate::Frame::empty().expect("frame slot");
assert_eq!(
delivered(&mut dec, &mut frame),
vec![0x22, 0x33, 0x44],
"the rest, in order"
);
assert_eq!(
dec.receive_frame(&mut frame).expect("the end"),
Received::Ended,
"the end, fed after the backlog"
);
assert_eq!(dec.end(), Some(End::Taken));
}
#[test]
fn committed_with_a_backlog_owed_the_end_drains_first() {
let mut frame = crate::Frame::empty().expect("frame slot");
let history = [0x11, 0x22, 0x33, 0x44].map(raw_packet).into();
let (mut dec, mut pictures) = committed_with_backlog(history, &mut frame);
assert_eq!(dec.backlog().len(), 2, "the replay's rest, owed");
let ended = dec.send_eof();
assert!(
matches!(ended, Ok(Sent::MustDrain)),
"the end drains first: {ended:?}"
);
assert_eq!(dec.end(), Some(End::Open), "not taken");
let _downloads = DownloadsForTest::set(all_deliver);
pictures.extend(delivered(&mut dec, &mut frame));
assert_eq!(
dec.send_eof().expect("the end"),
Sent::Accepted,
"then taken"
);
pictures.extend(delivered(&mut dec, &mut frame));
assert_eq!(
pictures,
vec![0x11, 0x22, 0x33, 0x44],
"every picture, in order"
);
}
#[test]
fn an_owed_advance_answers_every_entry_by_its_state() {
let owed = |end_given: bool, remaining: Vec<Backend>| {
let mut dec = on_trial_with(vaapi_refuses_and_cuda_may_fail, remaining);
send_all(&mut dec, &[raw_packet(0x11)]);
if end_given {
assert_eq!(dec.send_eof().expect("the end"), Sent::Accepted);
}
CUDA_FAILS.with(|fails| fails.set(true));
let stopped = dec.advance_probe(its_own_failure());
assert!(
is_own_enomem(&stopped),
"this crate's own, surfaced: {stopped:?}"
);
assert_eq!(dec.session_kind(), SessionKind::Owed, "the advance, owed");
dec
};
let packet = raw_packet(0x22);
let mut frame = crate::Frame::empty().expect("frame slot");
let mut dec = owed(false, vec![Backend::Vaapi, Backend::Cuda]);
let sent = unsafe { dec.send_av_packet(&packet) };
assert!(
matches!(sent, Ok(Sent::MustDrain)),
"a send, stopped again: {sent:?}"
);
let ended = dec.send_eof();
assert!(
matches!(ended, Ok(Sent::MustDrain)),
"the end, stopped again: {ended:?}"
);
let received = dec.receive_frame(&mut frame);
assert!(
is_own_enomem(&received),
"a receive: the failure: {received:?}"
);
assert_eq!(dec.session_kind(), SessionKind::Owed, "still owed");
assert!(dec.live().is_none(), "no decoder lives");
dec.flush();
assert_eq!(
dec.session_kind(),
SessionKind::Owed,
"a flush keeps it owed"
);
assert!(
dec.probe().expect("the record").buffered_packets.is_empty(),
"and forgets the history"
);
CUDA_FAILS.with(|fails| fails.set(false));
let sent = unsafe { dec.send_av_packet(&packet) };
assert!(
matches!(sent, Ok(Sent::Accepted)),
"runs on the candidate installed: {sent:?}"
);
assert_eq!(
(dec.backend(), dec.session_kind()),
(Backend::Cuda, SessionKind::Trial)
);
assert_eq!(
fills(&dec.probe().expect("on trial").buffered_packets),
vec![0x22]
);
let mut dec = owed(true, vec![Backend::Vaapi, Backend::Cuda]);
let sent = unsafe { dec.send_av_packet(&packet) };
assert!(
matches!(sent, Err(Error::Ffmpeg(ffmpeg_next::Error::Eof))),
"a send after the end: {sent:?}"
);
let ended = dec.send_eof();
assert!(
matches!(ended, Err(Error::Ffmpeg(ffmpeg_next::Error::Eof))),
"a second end: {ended:?}"
);
let received = dec.receive_frame(&mut frame);
assert!(
is_own_enomem(&received),
"a receive, stopped again: {received:?}"
);
CUDA_FAILS.with(|fails| fails.set(false));
let _downloads = DownloadsForTest::set(all_deliver);
assert_eq!(
delivered(&mut dec, &mut frame),
vec![0x11],
"the next candidate, replayed the history"
);
assert_eq!(
dec.receive_frame(&mut frame).expect("and the end"),
Received::Ended
);
let mut dec = owed(false, vec![Backend::Cuda]);
CUDA_FAILS.with(|fails| fails.set(false));
dec.probe_mut().expect("the record").build = every_backend_refuses;
let sent = unsafe { dec.send_av_packet(&packet) };
let Err(Error::ProbeExhausted(exhausted)) = sent else {
panic!("exhausted: {sent:?}");
};
assert_eq!(
exhausted.attempts().len(),
2,
"the candidate left, then CUDA"
);
assert_spent(
&mut dec,
crate::Spent::ProbeExhausted,
SessionKind::Exhausted,
);
}
#[test]
fn every_state_frees_what_it_holds_when_it_drops() {
let drops = |dec: VideoDecoder| {
let before = decoders_dropped();
drop(dec);
decoders_dropped() - before
};
assert_eq!(drops(raw_auditioning_decoder()), 1, "on trial");
assert_eq!(drops(raw_pinned_decoder()), 1, "committed");
let mut dec = on_trial_with(vaapi_refuses_and_cuda_may_fail, vec![Backend::Cuda]);
CUDA_FAILS.with(|fails| fails.set(true));
let stopped = dec.advance_probe(its_own_failure());
CUDA_FAILS.with(|fails| fails.set(false));
assert!(is_own_enomem(&stopped), "owed: {stopped:?}");
assert_eq!(drops(dec), 0, "an advance owed");
let mut dec = raw_auditioning_decoder();
let exhausted = dec.advance_probe(its_own_failure());
assert!(
matches!(exhausted, Err(Error::ProbeExhausted(_))),
"{exhausted:?}"
);
assert_eq!(drops(dec), 0, "exhausted");
let mut dec = raw_auditioning_decoder();
let ended = dec.end_ambiguously(Error::Ffmpeg(ffmpeg_next::Error::Other {
errno: libc::ENOMEM,
}));
assert!(matches!(ended, Error::AmbiguousSend(_)), "{ended:?}");
assert_eq!(dec.session_kind(), SessionKind::Ended);
assert_eq!(drops(dec), 0, "ended");
}
#[test]
fn on_trial_the_feed_routes_by_class() {
let enomem = ffmpeg_next::Error::Other {
errno: libc::ENOMEM,
};
for (name, error, ambiguous) in [
(
"taken with the candidate's error",
ffmpeg_next::Error::InvalidData,
false,
),
("ENOMEM", enomem, true),
] {
let mut dec = vaapi_then_cuda();
send_all(&mut dec, &[raw_packet(0x11)]);
let routed = dec.route_fed(Stopped::at(Owing::History, error, ambiguous));
assert!(routed.is_ok(), "{name}: leaves the candidate: {routed:?}");
assert_eq!(
(dec.backend(), dec.session_kind()),
(Backend::Cuda, SessionKind::Trial),
"{name}: the next, on trial"
);
let probe = dec.probe().expect("on trial");
assert_eq!(
fills(&probe.buffered_packets),
vec![0x11],
"{name}: the history whole"
);
assert_eq!(
tried(probe),
vec![Backend::Vaapi],
"{name}: the candidate's own answer"
);
}
let budget = |dec: &VideoDecoder| {
unsafe {
crate::ffi::declare_frame_budget_declined_for_test(dec.decoder().callback_state, 12_582_912)
}
};
let mut dec = vaapi_then_cuda();
send_all(&mut dec, &[raw_packet(0x11)]);
budget(&dec);
let routed = dec.route_fed(Stopped::at(
Owing::History,
ffmpeg_next::Error::InvalidData,
false,
));
assert!(
matches!(routed, Err(Error::FrameBudgetExceeded(_))),
"taken beside this crate's refusal: the refusal: {routed:?}"
);
assert_eq!(
(dec.backend(), dec.session_kind()),
(Backend::Vaapi, SessionKind::Trial),
"the candidate stays"
);
assert!(
dec.probe().expect("on trial").attempts.is_empty(),
"nothing charged"
);
let mut dec = vaapi_then_cuda();
send_all(&mut dec, &[raw_packet(0x11)]);
budget(&dec);
let routed = dec.route_fed(Stopped::at(Owing::History, enomem, true));
let Err(Error::AmbiguousSend(ended)) = routed else {
panic!("ENOMEM beside this crate's refusal ends the decoder: {routed:?}");
};
assert_eq!(
fills(ended.residual()),
vec![0x11],
"the whole history back"
);
assert_eq!(dec.session_kind(), SessionKind::Ended);
let mut dec = raw_auditioning_decoder();
budget(&dec);
let mut frame = crate::Frame::empty().expect("frame slot");
let received = dec.receive_frame(&mut frame);
assert!(
matches!(received, Err(Error::FrameBudgetExceeded(_))),
"the receive's own refusal: {received:?}"
);
assert_eq!(dec.session_kind(), SessionKind::Trial, "on trial still");
}
#[test]
fn on_trial_a_send_after_the_end_the_candidate_took_is_refused_for_good() {
let mut dec = raw_auditioning_decoder();
send_all(&mut dec, &[raw_packet(0x11)]);
assert_eq!(dec.send_eof().expect("the end"), Sent::Accepted);
assert_eq!(dec.end(), Some(End::Taken));
let packet = raw_packet(0x22);
let sent = unsafe { dec.send_av_packet(&packet) };
assert!(
matches!(sent, Err(Error::Ffmpeg(ffmpeg_next::Error::Eof))),
"refused for good: {sent:?}"
);
assert_eq!(dec.session_kind(), SessionKind::Trial, "on trial still");
let probe = dec.probe().expect("on trial");
assert!(
probe.attempts.is_empty(),
"nothing charged: {:?}",
probe.attempts
);
assert_eq!(
fills(&probe.buffered_packets),
vec![0x11],
"nothing recorded"
);
}
#[test]
fn on_trial_a_send_taken_with_the_candidates_error_goes_to_the_next_in_the_same_call() {
let mut dec = raw_auditioning_decoder_with(32, crate::DecoderOptions::new());
let first = dec.backend();
{
let probe = dec.probe_mut().expect("on trial");
probe.parameters = raw_parameters(RAW_SIDE);
probe.build = VideoDecoder::software_state_for_test;
probe.remaining_backends = vec![Backend::Cuda];
}
let packet = raw_packet(0x11);
let dropped = decoders_dropped();
let sent = unsafe { dec.send_av_packet(&packet) };
assert!(
matches!(sent, Ok(Sent::Accepted)),
"the next candidate's, in this call: {sent:?}"
);
assert_eq!(
decoders_dropped() - dropped,
1,
"the candidate left, torn down"
);
assert_eq!(
(dec.backend(), dec.session_kind()),
(Backend::Cuda, SessionKind::Trial)
);
{
let probe = dec.probe().expect("on trial");
assert_eq!(tried(probe), vec![first], "the candidate's own answer");
assert_eq!(fills(&probe.buffered_packets), vec![0x11], "recorded once");
}
let _downloads = DownloadsForTest::set(all_deliver);
let mut frame = crate::Frame::empty().expect("frame slot");
assert_eq!(
delivered(&mut dec, &mut frame),
vec![0x11],
"its picture, once"
);
}
const NONE_LISTED: i32 = ffmpeg_next::ffi::AVPixelFormat::AV_PIX_FMT_NONE as i32;
fn nothing_listed(_: Backend) -> Option<i32> {
Some(NONE_LISTED)
}
fn vaapi_lists_nothing(backend: Backend) -> Option<i32> {
(backend == Backend::Vaapi).then_some(NONE_LISTED)
}
#[test]
fn a_backend_that_lists_no_download_format_is_unfit_and_no_errno_is_made() {
let sent = [0x11u8, 0x22];
let mut frame = crate::Frame::empty().expect("frame slot");
{
let _downloads = DownloadsForTest::set(nothing_listed);
let mut dec = vaapi_then_cuda();
send_all(&mut dec, &sent.map(raw_packet));
let exhausted = dec.receive_frame(&mut frame);
let Err(Error::ProbeExhausted(exhausted)) = exhausted else {
panic!("the probe exhausted: {exhausted:?}");
};
let attempts: Vec<_> = exhausted
.attempts()
.iter()
.map(|(backend, attempt)| (*backend, attempt.as_ref()))
.collect();
assert!(
matches!(
attempts.as_slice(),
[
(Backend::Vaapi, Error::DownloadFormatUnsupported(a)),
(Backend::Cuda, Error::DownloadFormatUnsupported(b)),
] if a.format() == NONE_LISTED && b.format() == NONE_LISTED
),
"each candidate unfit, naming NONE, and no errno anywhere: {attempts:?}"
);
assert_eq!(
fills(exhausted.unconsumed_packets()),
sent.to_vec(),
"every packet back, in order"
);
}
{
let _downloads = DownloadsForTest::set(vaapi_lists_nothing);
let mut dec = vaapi_then_cuda();
send_all(&mut dec, &sent.map(raw_packet));
let first = dec.receive_frame(&mut frame);
assert!(
matches!(first, Ok(Received::Produced)),
"the next candidate delivers: {first:?}"
);
assert_eq!(
(dec.backend(), dec.session_kind()),
(Backend::Cuda, SessionKind::Committed),
"committed on its first delivered picture"
);
let mut pictures = vec![frame.row(0, 0).expect("a gray picture")[0]];
pictures.extend(delivered(&mut dec, &mut frame));
assert_eq!(
pictures, sent,
"the history whole: every picture, once, in order"
);
}
let _downloads = DownloadsForTest::set(nothing_listed);
let mut dec = raw_pinned_decoder();
send_all(&mut dec, &[raw_packet(0x11)]);
let received = dec.receive_frame(&mut frame);
let Err(Error::DownloadFormatUnsupported(layout)) = received else {
panic!("a pinned backend answers it by name: {received:?}");
};
assert_eq!(layout.format(), NONE_LISTED, "naming NONE");
assert!(
layout.to_string().contains("lists no CPU pixel format"),
"the message names the case: {layout}"
);
}
fn fail_next_download_at(step: LocalStep) {
LOCAL_FAILURES_FOR_TEST.with(|cell| cell.set((1, step)));
}
const LOCAL_STEPS: [(LocalStep, &str); 3] = [
(LocalStep::List, "the format list"),
(LocalStep::Destination, "the destination"),
(LocalStep::Properties, "the properties' copy"),
];
struct FittedRoad;
impl FittedRoad {
fn answers(answers: impl IntoIterator<Item = FittedForTest>) -> Self {
FITTED_FOR_TEST.with(|road| *road.borrow_mut() = Some(answers.into_iter().collect()));
Self
}
fn left() -> usize {
FITTED_FOR_TEST.with(|road| road.borrow().as_ref().map_or(0, VecDeque::len))
}
}
impl Drop for FittedRoad {
fn drop(&mut self) {
FITTED_FOR_TEST.with(|road| *road.borrow_mut() = None);
}
}
fn delivered_within(dec: &mut VideoDecoder, frame: &mut crate::Frame, bound: usize) -> Vec<u8> {
let mut fills = Vec::new();
loop {
match dec
.receive_frame(frame)
.expect("a picture or a request for input")
{
Received::Produced => {
fills.push(frame.row(0, 0).expect("a gray picture's first row")[0]);
assert!(
fills.len() <= bound,
"more pictures than were sent: {fills:02x?}"
);
}
Received::NeedsInput | Received::Ended => return fills,
}
}
}
fn picture_in(frame: &crate::Frame) -> u8 {
frame.row(0, 0).expect("a gray picture's first row")[0]
}
fn raw_packet_of(side: usize, fill: u8) -> Packet {
crate::boundary::try_packet_copy(&vec![fill; side * side]).expect("a submittable packet")
}
fn raw_packet_at(fill: u8, pts: i64) -> Packet {
let mut packet = raw_packet(fill);
unsafe { (*packet.as_mut_ptr()).pts = pts };
packet
}
fn holding(dec: &mut VideoDecoder, frame: &mut crate::Frame, packet: &Packet) {
let sent = unsafe { dec.send_av_packet(packet) };
assert!(matches!(sent, Ok(Sent::Accepted)), "taken: {sent:?}");
fail_next_download_at(LocalStep::Destination);
let received = dec.receive_frame(frame);
assert!(is_own_enomem(&received), "this crate's own: {received:?}");
assert_eq!(dec.held(), Held::Pending, "the picture held");
assert_eq!(
payload_of(packet).1,
2,
"the picture held references the caller's packet"
);
}
#[test]
fn a_picture_whose_download_runs_out_of_memory_is_held_and_delivered_next_in_order() {
let _downloads = DownloadsForTest::set(all_deliver);
let mut frame = crate::Frame::empty().expect("frame slot");
for (step, name) in LOCAL_STEPS {
let mut dec = vaapi_then_cuda();
send_all(&mut dec, &[0x11, 0x22].map(raw_packet));
fail_next_download_at(step);
let received = dec.receive_frame(&mut frame);
assert!(
is_own_enomem(&received),
"{name}, on trial: this crate's own: {received:?}"
);
assert_eq!(dec.held(), Held::Pending, "{name}, on trial: held");
assert_eq!(
(dec.backend(), dec.session_kind()),
(Backend::Vaapi, SessionKind::Trial),
"{name}: nothing committed"
);
{
let probe = dec.probe().expect("on trial");
assert!(
probe.attempts.is_empty(),
"{name}: nothing charged: {:?}",
probe.attempts
);
assert_eq!(
probe.remaining_backends,
vec![Backend::Cuda],
"{name}: the order kept"
);
assert_eq!(
fills(&probe.buffered_packets),
vec![0x11, 0x22],
"{name}: the history kept"
);
}
let first = dec.receive_frame(&mut frame);
assert!(
matches!(first, Ok(Received::Produced)),
"{name}: delivered next: {first:?}"
);
assert_eq!(
picture_in(&frame),
0x11,
"{name}: that picture, delivered next"
);
assert_eq!(
(dec.backend(), dec.session_kind()),
(Backend::Vaapi, SessionKind::Committed),
"{name}: committed at its first delivered picture"
);
assert_eq!(
dec.held(),
Held::Nothing,
"{name}: nothing held once delivered"
);
fail_next_download_at(step);
let received = dec.receive_frame(&mut frame);
assert!(
is_own_enomem(&received),
"{name}, committed: this crate's own: {received:?}"
);
assert_eq!(dec.held(), Held::Pending, "{name}, committed: held");
assert_eq!(
delivered_within(&mut dec, &mut frame, 1),
vec![0x22],
"{name}, committed: that picture, delivered next, and nothing after"
);
let mut dec = raw_pinned_decoder();
send_all(&mut dec, &[0x33, 0x44].map(raw_packet));
fail_next_download_at(step);
let received = dec.receive_frame(&mut frame);
assert!(
is_own_enomem(&received),
"{name}, pinned: this crate's own: {received:?}"
);
assert_eq!(dec.held(), Held::Pending, "{name}, pinned: held");
assert_eq!(
delivered_within(&mut dec, &mut frame, 2),
vec![0x33, 0x44],
"{name}, pinned: that picture, then the next, in order, with no gap"
);
}
}
#[test]
fn a_send_never_writes_the_picture_held_and_the_receive_delivers_it_first() {
let _downloads = DownloadsForTest::set(all_deliver);
let mut frame = crate::Frame::empty().expect("frame slot");
let mut dec = vaapi_then_cuda();
holding(&mut dec, &mut frame, &raw_packet(0x11));
send_all(&mut dec, &[raw_packet(0x22)]);
assert_eq!(dec.held(), Held::Pending, "on trial: held across the send");
assert_eq!(
fills(&dec.probe().expect("on trial").buffered_packets),
vec![0x11, 0x22],
"on trial: the packet recorded"
);
assert_eq!(
delivered_within(&mut dec, &mut frame, 2),
vec![0x11, 0x22],
"on trial: the held picture first, then the packet's"
);
let mut dec = vaapi_then_cuda();
holding(&mut dec, &mut frame, &raw_packet(0x11));
let crowded = with_side_data_entries(raw_packet(0x22), MAX_PROBE_PACKET_SIDE_DATA_ENTRIES + 1);
send_all(&mut dec, &[crowded]);
assert_eq!(
dec.session_kind(),
SessionKind::Committed,
"committed by a packet the history cannot keep"
);
assert_eq!(dec.held(), Held::Pending, "held across the commit");
assert_eq!(
delivered_within(&mut dec, &mut frame, 2),
vec![0x11, 0x22],
"across the commit: the held picture first, then the packet's"
);
let mut dec = vaapi_then_cuda();
holding(&mut dec, &mut frame, &raw_packet(0x11));
assert_eq!(
dec.send_eof().expect("the end"),
Sent::Accepted,
"the end, taken"
);
assert_eq!(dec.held(), Held::Pending, "on trial: held across the end");
assert_eq!(
delivered_within(&mut dec, &mut frame, 1),
vec![0x11],
"on trial: the held picture"
);
assert_eq!(
dec.receive_frame(&mut frame).expect("the end"),
Received::Ended,
"then the end"
);
let mut dec = raw_pinned_decoder();
holding(&mut dec, &mut frame, &raw_packet(0x33));
send_all(&mut dec, &[raw_packet(0x44)]);
assert_eq!(dec.held(), Held::Pending, "committed: held across the send");
assert_eq!(
delivered_within(&mut dec, &mut frame, 2),
vec![0x33, 0x44],
"committed: the held picture first, then the packet's"
);
}
#[test]
fn a_flush_a_rejection_or_a_terminal_state_clears_the_picture_held() {
let _downloads = DownloadsForTest::set(all_deliver);
let mut frame = crate::Frame::empty().expect("frame slot");
for (name, mut dec) in [
("on trial", raw_auditioning_decoder()),
("committed", raw_pinned_decoder()),
] {
let packet = raw_packet(0x11);
holding(&mut dec, &mut frame, &packet);
dec.flush();
assert_eq!(dec.held(), Held::Nothing, "{name}: a flush: nothing held");
assert_eq!(
payload_of(&packet).1,
1,
"{name}: a flush: the caller's packet, by its own alone"
);
assert_eq!(
dec.receive_frame(&mut frame).expect("after the flush"),
Received::NeedsInput,
"{name}: nothing to deliver after the flush"
);
send_all(&mut dec, &[raw_packet(0x22)]);
assert_eq!(
delivered_within(&mut dec, &mut frame, 1),
vec![0x22],
"{name}: the new position's picture"
);
}
let rejecting = |remaining: Vec<Backend>| {
let mut dec = raw_auditioning_decoder_with(32, crate::DecoderOptions::new());
let probe = dec.probe_mut().expect("on trial");
probe.parameters = raw_parameters(RAW_SIDE);
probe.build = VideoDecoder::software_state_for_test;
probe.remaining_backends = remaining;
dec
};
let short = raw_packet(0x22);
let mut dec = rejecting(vec![Backend::Cuda]);
let first = dec.backend();
let picture = raw_packet_of(32, 0x11);
holding(&mut dec, &mut frame, &picture);
let sent = unsafe { dec.send_av_packet(&short) };
assert!(
matches!(sent, Ok(Sent::Accepted)),
"the next candidate takes it: {sent:?}"
);
assert_eq!(
(dec.backend(), dec.session_kind()),
(Backend::Cuda, SessionKind::Trial),
"the candidate left"
);
assert_eq!(
tried(dec.probe().expect("on trial")),
vec![first],
"the candidate's own answer"
);
assert_eq!(dec.held(), Held::Nothing, "a rejection: nothing held");
assert_eq!(
payload_of(&picture).1,
1,
"a rejection: the caller's packet, by its own alone"
);
assert_eq!(
delivered_within(&mut dec, &mut frame, 2),
vec![0x11, 0x22],
"the history whole: every picture, once, in order"
);
let mut dec = rejecting(Vec::new());
let picture = raw_packet_of(32, 0x11);
holding(&mut dec, &mut frame, &picture);
let sent = unsafe { dec.send_av_packet(&short) };
let Err(Error::ProbeExhausted(exhausted)) = sent else {
panic!("the probe exhausted: {sent:?}");
};
assert_eq!(dec.session_kind(), SessionKind::Exhausted);
assert_eq!(dec.held(), Held::Nothing, "exhausted: nothing held");
assert_eq!(
payload_of(&picture).1,
1,
"exhausted: the caller's packet, by its own alone"
);
assert_eq!(
fills(exhausted.unconsumed_packets()),
vec![0x11],
"exhausted: the history handed back, the held picture's packet in it"
);
for (name, mut dec) in [
("on trial", raw_auditioning_decoder()),
("committed", raw_pinned_decoder()),
] {
let packet = raw_packet(0x11);
holding(&mut dec, &mut frame, &packet);
let ended = dec.end_ambiguously(Error::Ffmpeg(ffmpeg_next::Error::Other {
errno: libc::ENOMEM,
}));
assert!(
matches!(ended, Error::AmbiguousSend(_)),
"{name}: {ended:?}"
);
assert_eq!(dec.session_kind(), SessionKind::Ended, "{name}: ended");
assert_eq!(dec.held(), Held::Nothing, "{name}: ended: nothing held");
assert_eq!(
payload_of(&packet).1,
1,
"{name}: ended: the caller's packet, by its own alone"
);
}
}
fn nv12_listed(_: Backend) -> Option<i32> {
Some(NV12)
}
#[test]
fn a_picture_over_the_frame_limit_is_dropped_by_its_pts_and_charges_nothing() {
let mut frame = crate::Frame::empty().expect("frame slot");
for (name, mut dec) in [
("on trial", vaapi_then_cuda()),
("committed", raw_pinned_decoder()),
] {
let on_trial = dec.session_kind() == SessionKind::Trial;
send_all(
&mut dec,
&[raw_packet_at(0x11, 100), raw_packet_at(0x22, 200)],
);
let refused = {
let _downloads = DownloadsForTest::set(nv12_listed);
dec.receive_frame(&mut frame)
};
let Err(Error::HwTransferTooLarge(refusal)) = refused else {
panic!("{name}: the caller's limit, by name: {refused:?}");
};
assert_eq!(refusal.pts(), Some(100), "{name}: naming its pts");
assert!(
refusal
.to_string()
.ends_with("the picture is dropped (pts 100)"),
"{name}: the message says so: {refusal}"
);
assert_eq!(dec.held(), Held::Nothing, "{name}: dropped, not held");
if on_trial {
let probe = dec.probe().expect("on trial");
assert!(
probe.attempts.is_empty(),
"{name}: nothing charged: {:?}",
probe.attempts
);
assert_eq!(
probe.remaining_backends,
vec![Backend::Cuda],
"{name}: the order kept"
);
assert_eq!(
fills(&probe.buffered_packets),
vec![0x11, 0x22],
"{name}: the history kept"
);
}
let _downloads = DownloadsForTest::set(all_deliver);
assert_eq!(
delivered_within(&mut dec, &mut frame, 1),
vec![0x22],
"{name}: the next picture, never the dropped one"
);
assert_eq!(
dec.session_kind(),
SessionKind::Committed,
"{name}: committed"
);
}
let mut dec = raw_pinned_decoder();
send_all(&mut dec, &[raw_packet(0x33)]);
let _downloads = DownloadsForTest::set(nv12_listed);
let refused = dec.receive_frame(&mut frame);
let Err(Error::HwTransferTooLarge(refusal)) = refused else {
panic!("the caller's limit, by name: {refused:?}");
};
assert_eq!(refusal.pts(), None, "a picture with no pts, named so");
assert!(
refusal.to_string().ends_with("(no pts)"),
"the message says so: {refusal}"
);
}
#[test]
fn a_fitted_download_keeps_every_failures_class_and_only_the_stage_stands_down() {
let _downloads = DownloadsForTest::set(all_deliver);
let mut frame = crate::Frame::empty().expect("frame slot");
let eio = ffmpeg_next::Error::Other { errno: libc::EIO };
let enomem = ffmpeg_next::Error::Other {
errno: libc::ENOMEM,
};
let is_eio = |e: &Error| matches!(e, Error::Ffmpeg(ffmpeg_next::Error::Other { errno }) if *errno == libc::EIO);
let mut dec = vaapi_then_cuda();
send_all(&mut dec, &[0x11, 0x22].map(raw_packet));
{
let _road = FittedRoad::answers([
FittedForTest::Fails(TransferFailure::Download(eio)),
FittedForTest::Fails(TransferFailure::Unattributed(enomem)),
]);
let received = dec.receive_frame(&mut frame);
assert!(
is_own_enomem(&received),
"the backend's, recorded: the candidate left, the next one's picture held: {received:?}"
);
assert_eq!(
(dec.backend(), dec.session_kind()),
(Backend::Cuda, SessionKind::Trial),
"the backend's, recorded: the candidate left"
);
let probe = dec.probe().expect("on trial");
assert!(
matches!(
probe.attempts.as_slice(),
[(Backend::Vaapi, Attempt::Failed(BackendAnswer(e)))] if is_eio(e)
),
"the backend's, recorded: {:?}",
probe.attempts
);
assert_eq!(
fills(&probe.buffered_packets),
vec![0x11, 0x22],
"the history whole"
);
assert_eq!(
delivered_within(&mut dec, &mut frame, 2),
vec![0x11, 0x22],
"every picture, once, in order"
);
}
let mut dec = vaapi_then_cuda();
send_all(&mut dec, &[0x11, 0x22].map(raw_packet));
{
let _road = FittedRoad::answers([
FittedForTest::Fails(TransferFailure::Download(eio)),
FittedForTest::Fails(TransferFailure::Download(eio)),
]);
let exhausted = dec.receive_frame(&mut frame);
let Err(Error::ProbeExhausted(exhausted)) = exhausted else {
panic!("the probe's policy: exhausted: {exhausted:?}");
};
let attempts: Vec<_> = exhausted
.attempts()
.iter()
.map(|(backend, attempt)| (*backend, attempt.as_ref()))
.collect();
assert!(
matches!(
attempts.as_slice(),
[(Backend::Vaapi, a), (Backend::Cuda, b)] if is_eio(a) && is_eio(b)
),
"each the backend's answer: {attempts:?}"
);
assert_eq!(
fills(exhausted.unconsumed_packets()),
vec![0x11, 0x22],
"every packet back, in order"
);
}
let mut dec = raw_pinned_decoder();
send_all(&mut dec, &[0x33, 0x44].map(raw_packet));
{
let _road = FittedRoad::answers([FittedForTest::Fails(TransferFailure::Download(eio))]);
let received = dec.receive_frame(&mut frame);
assert!(
matches!(&received, Err(e) if is_eio(e)),
"committed: the picture's own error: {received:?}"
);
assert_eq!(dec.held(), Held::Nothing, "committed: the picture dropped");
assert_eq!(
dec.session_kind(),
SessionKind::Committed,
"committed: the decoder stays"
);
assert_eq!(
delivered_within(&mut dec, &mut frame, 1),
vec![0x44],
"committed: the next picture"
);
}
let mut dec = vaapi_then_cuda();
send_all(&mut dec, &[0x11, 0x22].map(raw_packet));
{
let _road = FittedRoad::answers([
FittedForTest::Fails(TransferFailure::Layout(UYVY)),
FittedForTest::Fails(TransferFailure::Unattributed(enomem)),
]);
let received = dec.receive_frame(&mut frame);
assert!(is_own_enomem(&received), "unfit: {received:?}");
let probe = dec.probe().expect("on trial");
assert!(
matches!(
probe.attempts.as_slice(),
[(Backend::Vaapi, Attempt::Unfit(layout))] if layout.format() == UYVY
),
"unfit, naming the layout: {:?}",
probe.attempts
);
}
let mut dec = raw_pinned_decoder();
send_all(&mut dec, &[0x33, 0x44].map(raw_packet));
{
let _road = FittedRoad::answers([FittedForTest::Fails(TransferFailure::Layout(UYVY))]);
let received = dec.receive_frame(&mut frame);
assert!(
matches!(&received, Err(Error::DownloadFormatUnsupported(layout)) if layout.format() == UYVY),
"committed: by name: {received:?}"
);
assert_eq!(dec.held(), Held::Nothing, "committed: the picture dropped");
assert_eq!(
delivered_within(&mut dec, &mut frame, 1),
vec![0x44],
"committed: the next picture"
);
}
let local: [(fn(ffmpeg_next::Error) -> TransferFailure, &str); 2] = [
(TransferFailure::Unattributed, "the destination"),
(TransferFailure::Properties, "the properties' copy"),
];
for (failure, name) in local {
for (state, mut dec) in [
("on trial", vaapi_then_cuda()),
("committed", raw_pinned_decoder()),
] {
send_all(&mut dec, &[0x11, 0x22].map(raw_packet));
let _road = FittedRoad::answers([
FittedForTest::Fails(failure(enomem)),
FittedForTest::Delivers,
]);
let received = dec.receive_frame(&mut frame);
assert!(
is_own_enomem(&received),
"{name}, {state}: this crate's own: {received:?}"
);
assert_eq!(dec.held(), Held::Pending, "{name}, {state}: held");
assert!(
dec.probe().is_none_or(|probe| probe.attempts.is_empty()),
"{name}, {state}: nothing charged"
);
let first = dec.receive_frame(&mut frame);
assert!(
matches!(first, Ok(Received::Produced)),
"{name}, {state}: delivered next: {first:?}"
);
assert_eq!(picture_in(&frame), 0x11, "{name}, {state}: that picture");
assert_eq!(
FittedRoad::left(),
0,
"{name}, {state}: downloaded again on the fitted road"
);
assert_eq!(
delivered_within(&mut dec, &mut frame, 1),
vec![0x22],
"{name}, {state}: then the next"
);
}
}
let mut dec = vaapi_then_cuda();
send_all(&mut dec, &[0x11, 0x22].map(raw_packet));
let _road = FittedRoad::answers([FittedForTest::StandsDown, FittedForTest::StandsDown]);
assert_eq!(
delivered_within(&mut dec, &mut frame, 2),
vec![0x11, 0x22],
"standing down: the full-size download delivers"
);
assert_eq!(
(dec.backend(), dec.session_kind()),
(Backend::Vaapi, SessionKind::Committed),
"standing down: nothing charged"
);
}
#[test]
fn on_trial_a_failed_download_a_refusal_outranks_holds_the_picture_and_a_second_failure_is_charged()
{
let _downloads = DownloadsForTest::set(all_deliver);
let mut frame = crate::Frame::empty().expect("frame slot");
let eio = ffmpeg_next::Error::Other { errno: libc::EIO };
let enomem = ffmpeg_next::Error::Other {
errno: libc::ENOMEM,
};
let budget = |dec: &VideoDecoder| {
unsafe {
crate::ffi::declare_frame_budget_declined_for_test(dec.decoder().callback_state, 12_582_912)
}
};
let outranked = |dec: &mut VideoDecoder, frame: &mut crate::Frame| {
let received = dec.receive_frame(frame);
assert!(
matches!(received, Err(Error::FrameBudgetExceeded(_))),
"the refusal: {received:?}"
);
assert_eq!(
(dec.backend(), dec.session_kind(), dec.held()),
(Backend::Vaapi, SessionKind::Trial, Held::Pending),
"the candidate stays, its picture held"
);
assert!(
dec.probe().expect("on trial").attempts.is_empty(),
"nothing charged"
);
};
let mut dec = vaapi_then_cuda();
send_all(&mut dec, &[0x11, 0x22].map(raw_packet));
budget(&dec);
{
let _road = FittedRoad::answers([FittedForTest::Fails(TransferFailure::Download(eio))]);
outranked(&mut dec, &mut frame);
assert_eq!(
delivered_within(&mut dec, &mut frame, 2),
vec![0x11, 0x22],
"delivered: every picture, once, in order"
);
assert_eq!(
(dec.backend(), dec.session_kind()),
(Backend::Vaapi, SessionKind::Committed),
"delivered: the candidate committed"
);
}
let mut dec = vaapi_then_cuda();
send_all(&mut dec, &[0x11, 0x22].map(raw_packet));
budget(&dec);
{
let _road = FittedRoad::answers([
FittedForTest::Fails(TransferFailure::Download(eio)),
FittedForTest::Fails(TransferFailure::Download(eio)),
FittedForTest::Fails(TransferFailure::Unattributed(enomem)),
]);
outranked(&mut dec, &mut frame);
let received = dec.receive_frame(&mut frame);
assert!(
is_own_enomem(&received),
"failing again: the candidate left, the next one's picture held: {received:?}"
);
assert_eq!(
(dec.backend(), dec.session_kind()),
(Backend::Cuda, SessionKind::Trial),
"failing again: the candidate left"
);
let probe = dec.probe().expect("on trial");
assert!(
matches!(
probe.attempts.as_slice(),
[(Backend::Vaapi, Attempt::Failed(BackendAnswer(Error::Ffmpeg(ffmpeg_next::Error::Other { errno }))))]
if *errno == libc::EIO
),
"failing again: the backend's answer, recorded: {:?}",
probe.attempts
);
assert_eq!(
delivered_within(&mut dec, &mut frame, 2),
vec![0x11, 0x22],
"failing again: the history whole, every picture once, in order"
);
}
let mut dec = raw_pinned_decoder();
send_all(&mut dec, &[0x33, 0x44].map(raw_packet));
budget(&dec);
let _road = FittedRoad::answers([FittedForTest::Fails(TransferFailure::Download(eio))]);
let received = dec.receive_frame(&mut frame);
assert!(
matches!(received, Err(Error::FrameBudgetExceeded(_))),
"committed: the refusal: {received:?}"
);
assert_eq!(dec.held(), Held::Nothing, "committed: the picture dropped");
assert_eq!(
delivered_within(&mut dec, &mut frame, 1),
vec![0x44],
"committed: the next picture"
);
}
#[cfg(target_vendor = "apple")]
const CAPPED_DOWNLOAD: usize = 64 * 1024;
#[cfg(target_vendor = "apple")]
#[derive(Debug, PartialEq, Eq)]
struct CappedReceive {
own_enomem: bool,
held: Held,
kind: SessionKind,
nothing_charged: bool,
}
#[cfg(target_vendor = "apple")]
type DeliveredPicture = (Option<i64>, u32, u32);
#[cfg(target_vendor = "apple")]
fn on_videotoolbox_pictures(
scaled: Option<(u32, u32)>,
count: usize,
capped_at: Option<usize>,
) -> (Vec<DeliveredPicture>, Option<CappedReceive>, bool) {
use ffmpeg_next::{format, media};
let path = std::env::var_os("HWDECODE_SAMPLE_VIDEO")
.expect("HWDECODE_SAMPLE_VIDEO must be set for this test");
let mut input = format::input(&path).expect("open input");
let (stream_index, parameters) = {
let stream = input
.streams()
.best(media::Type::Video)
.expect("video stream");
(stream.index(), stream.parameters())
};
let mut dec = VideoDecoder::open(parameters).expect("open decoder");
assert_eq!(dec.backend(), Backend::VideoToolbox, "on VideoToolbox");
if let Some(size) = scaled {
assert!(
dec.request_scaled_output(size).is_supported(),
"the scaled output, accepted"
);
}
CAP_NEXT_TRANSFER_FOR_TEST.with(|cap| cap.set(None));
let mut frame = crate::Frame::empty().expect("frame slot");
let mut pictures = Vec::new();
let mut capped = None;
let mut packets = input.packets();
loop {
loop {
if capped_at == Some(pictures.len()) && capped.is_none() {
CAP_NEXT_TRANSFER_FOR_TEST.with(|cap| cap.set(Some(CAPPED_DOWNLOAD)));
}
match dec.receive_frame(&mut frame) {
Ok(Received::Produced) => {
pictures.push((frame.pts(), frame.width(), frame.height()));
if pictures.len() == count {
assert_eq!(
CAP_NEXT_TRANSFER_FOR_TEST.with(core::cell::Cell::get),
None,
"the capped download was made"
);
let promise = dec.scaled_output_capability().is_supported();
return (pictures, capped, promise);
}
}
Ok(Received::NeedsInput) => break,
Ok(Received::Ended) => panic!("the sample ended before {count} pictures"),
Err(e) => {
assert!(
capped.is_none() && capped_at.is_some(),
"one failure, the capped download's: {e:?}"
);
capped = Some(CappedReceive {
own_enomem: matches!(&e, Error::Ffmpeg(ffmpeg_next::Error::Other { errno }) if *errno == libc::ENOMEM),
held: dec.held(),
kind: dec.session_kind(),
nothing_charged: dec.probe().is_none_or(|probe| probe.attempts.is_empty()),
});
}
}
}
let packet = loop {
let (stream, packet) = packets
.next()
.expect("the sample ended before the pictures did");
if stream.index() == stream_index {
break packet;
}
};
let sent = unsafe { dec.send_av_packet(&packet) };
assert!(matches!(sent, Ok(Sent::Accepted)), "taken: {sent:?}");
}
}
#[cfg(target_vendor = "apple")]
#[test]
#[ignore = "requires HWDECODE_SAMPLE_VIDEO and a working VideoToolbox backend"]
fn on_videotoolbox_a_picture_whose_download_runs_out_of_memory_is_held_and_delivered_next() {
use mediaio_ffmpeg::test_support::in_subprocess;
in_subprocess(
"decoder::tests::on_videotoolbox_a_picture_whose_download_runs_out_of_memory_is_held_and_delivered_next",
|| {
ffmpeg_next::init().expect("ffmpeg init");
for (road, scaled) in [("full size", None), ("scaled", Some((640, 360)))] {
let (control, none, promise) = on_videotoolbox_pictures(scaled, 8, None);
assert_eq!(none, None, "{road}: the control fails nothing");
if let Some((width, height)) = scaled {
assert!(
control.iter().all(|&(_, w, h)| (w, h) == (width, height)),
"{road}: every picture at the extent requested: {control:?}"
);
assert!(promise, "{road}: the promise stands");
}
for (at, kind) in [(0, SessionKind::Trial), (3, SessionKind::Committed)] {
let (pictures, capped, promise) = on_videotoolbox_pictures(scaled, 8, Some(at));
assert_eq!(
pictures, control,
"{road}, picture {at}: the control's pictures, in order, with no gap and none twice"
);
assert_eq!(
capped,
Some(CappedReceive {
own_enomem: true,
held: Held::Pending,
kind,
nothing_charged: true,
}),
"{road}, picture {at}: this crate's own ENOMEM, nothing charged, the picture held"
);
if scaled.is_some() {
assert!(promise, "{road}, picture {at}: the promise still stands");
}
}
}
},
);
}