use super::*;
#[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 is_transient_recognises_eagain_and_eof() {
let eagain = ffmpeg_next::Error::Other {
errno: ffmpeg_next::error::EAGAIN,
};
assert!(is_transient(&eagain));
assert!(is_transient(&ffmpeg_next::Error::Eof));
let other = ffmpeg_next::Error::InvalidData;
assert!(!is_transient(&other));
}
#[test]
fn is_hw_decode_failure_covers_hw_failures_excludes_transient_and_eof() {
assert!(is_hw_decode_failure(&ffmpeg_next::Error::External));
assert!(is_hw_decode_failure(&ffmpeg_next::Error::Bug));
assert!(is_hw_decode_failure(&ffmpeg_next::Error::Bug2));
assert!(is_hw_decode_failure(&ffmpeg_next::Error::Unknown));
assert!(is_hw_decode_failure(&ffmpeg_next::Error::InvalidData));
assert!(is_hw_decode_failure(&ffmpeg_next::Error::Other {
errno: libc::EINVAL,
}));
assert!(!is_hw_decode_failure(&ffmpeg_next::Error::Eof));
assert!(!is_hw_decode_failure(&ffmpeg_next::Error::Other {
errno: ffmpeg_next::error::EAGAIN,
}));
assert!(!is_hw_decode_failure(&ffmpeg_next::Error::Other {
errno: libc::ENOMEM,
}));
}
#[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 cpu_frame_bytes_rejects_negative_first_plane_linesize() {
let mut f = frame::Video::empty();
unsafe {
let raw = f.as_mut_ptr();
(*raw).format = ffmpeg_next::ffi::AVPixelFormat::AV_PIX_FMT_NV12 as i32;
(*raw).width = 1920;
(*raw).height = 1080;
(*raw).linesize[0] = -1920;
(*raw).linesize[1] = -1920;
}
assert!(
cpu_frame_bytes(&f).is_none(),
"negative linesize must be unsizeable, not Some(0)"
);
}
fn make_hw_frames_ctx_ref(w: i32, h: i32) -> *mut ffmpeg_next::ffi::AVBufferRef {
use ffmpeg_next::ffi::av_buffer_alloc;
use std::mem::size_of;
unsafe {
let buf = av_buffer_alloc(size_of::<AVHWFramesContext>());
assert!(!buf.is_null(), "av_buffer_alloc returned NULL");
let data = (*buf).data as *mut AVHWFramesContext;
std::ptr::write_bytes(data, 0, 1);
(*data).width = w;
(*data).height = h;
buf
}
}
#[test]
fn cpu_frame_bytes_sums_buf_sizes() {
use ffmpeg_next::ffi::av_buffer_alloc;
let mut f = frame::Video::empty();
let buf0 = unsafe { av_buffer_alloc(4096) };
let buf1 = unsafe { av_buffer_alloc(2048) };
assert!(!buf0.is_null() && !buf1.is_null());
unsafe {
let raw = f.as_mut_ptr();
(*raw).buf[0] = buf0;
(*raw).buf[1] = buf1;
(*raw).linesize[0] = 256;
}
assert_eq!(cpu_frame_bytes(&f), Some(4096 + 2048));
}
#[test]
fn cpu_frame_bytes_zero_for_empty_frame() {
let f = frame::Video::empty();
assert_eq!(cpu_frame_bytes(&f), Some(0));
}
#[test]
fn cpu_frame_bytes_uses_buf_size_independent_of_display_height() {
use ffmpeg_next::ffi::av_buffer_alloc;
let buf0 = unsafe { av_buffer_alloc(256) };
assert!(!buf0.is_null());
let mut f = frame::Video::empty();
unsafe {
let raw = f.as_mut_ptr();
(*raw).format = ffmpeg_next::ffi::AVPixelFormat::AV_PIX_FMT_NV12 as i32;
(*raw).width = 1;
(*raw).height = 1;
(*raw).linesize[0] = 32;
(*raw).buf[0] = buf0;
}
assert_eq!(
cpu_frame_bytes(&f),
Some(256),
"cropped/aligned frames must be sized by buf[i].size, not display dims"
);
}
#[test]
fn estimate_transfer_bytes_reads_alloc_dims_from_hw_frames_ctx() {
let buf = make_hw_frames_ctx_ref(8192, 8192);
let mut f = frame::Video::empty();
unsafe {
let raw = f.as_mut_ptr();
(*raw).width = 100;
(*raw).height = 100;
(*raw).hw_frames_ctx = buf;
}
assert_eq!(
estimate_transfer_bytes(&f),
Some(8192usize * 8192 * WORST_CASE_BYTES_PER_PIXEL),
);
}
#[test]
fn estimate_transfer_bytes_returns_none_without_hw_frames_ctx() {
let mut f = frame::Video::empty();
unsafe {
let raw = f.as_mut_ptr();
(*raw).width = 1920;
(*raw).height = 1080;
}
assert!(estimate_transfer_bytes(&f).is_none());
}
#[test]
fn estimate_transfer_bytes_rejects_non_positive_alloc_dimensions() {
let mut f = frame::Video::empty();
let buf = make_hw_frames_ctx_ref(0, 1080);
unsafe {
(*f.as_mut_ptr()).hw_frames_ctx = buf;
}
assert!(estimate_transfer_bytes(&f).is_none());
}
#[test]
fn estimate_transfer_bytes_8k_fits_default_cap() {
let buf = make_hw_frames_ctx_ref(7680, 4320);
let mut f = frame::Video::empty();
unsafe {
(*f.as_mut_ptr()).hw_frames_ctx = buf;
}
let estimate = estimate_transfer_bytes(&f).expect("8K is sizable");
assert!(
estimate <= DEFAULT_MAX_PROBE_PENDING_BYTES,
"8K estimate {estimate} must fit DEFAULT_MAX_PROBE_PENDING_BYTES \
{DEFAULT_MAX_PROBE_PENDING_BYTES}; otherwise the default cap rejects \
even a single 8K frame at probe time"
);
assert!(
estimate > 96 * 1024 * 1024,
"estimate must over-charge real 8K P010 to bound the worst case; got {estimate}"
);
}
#[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::{AVPixelFormat, 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 {
wanted: AVPixelFormat::AV_PIX_FMT_NONE,
wanted_int: AVPixelFormat::AV_PIX_FMT_NONE as i32,
ceiling_declined: core::sync::atomic::AtomicBool::new(false),
declined_pixels: core::sync::atomic::AtomicI64::new(0),
declined_limit: core::sync::atomic::AtomicI64::new(0),
max_frame_bytes: u64::MAX,
frame_budget_declined: core::sync::atomic::AtomicBool::new(false),
declined_frame_bytes: core::sync::atomic::AtomicU64::new(0),
declined_frame_audio: core::sync::atomic::AtomicBool::new(false),
}));
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 cap_overflow_does_not_consume_packet_and_preserves_pending() {
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!(
decoder.probe.is_some(),
"probe must be active immediately after open"
);
decoder.pending_frames.push_back(frame::Video::empty());
decoder.pending_frames.push_back(frame::Video::empty());
let pending_before = decoder.pending_frames.len();
let pre_existing = Packet::new(8);
decoder
.probe
.as_mut()
.expect("probe present")
.buffered_packets
.push(pre_existing);
decoder
.probe
.as_mut()
.expect("probe present")
.buffered_bytes = MAX_PROBE_PACKET_BYTES;
let mut hit_bailout = false;
for (s, packet) in input.packets() {
if s.index() != stream_index {
continue;
}
match decoder.send_packet(&packet) {
Err(Error::AllBackendsFailed(p)) => {
let attempts = p.attempts();
let unconsumed_packets = p.unconsumed_packets();
assert_eq!(
unconsumed_packets.len(),
1,
"rescue history must contain the pre-existing packet only — \
the triggering packet must NOT have been consumed"
);
assert_eq!(
unconsumed_packets[0].size(),
8,
"the pre-existing packet must come back unmodified"
);
assert!(
attempts.is_empty(),
"no backend failure occurred; attempts must be empty when \
bailout fires from cap overflow alone"
);
hit_bailout = true;
break;
}
Ok(_) => panic!("send_packet must bail out when probe is at the byte cap"),
Err(other) => panic!("expected AllBackendsFailed bailout, got {other:?}"),
}
}
assert!(
hit_bailout,
"expected at least one send_packet to trip the cap-overflow bailout"
);
assert!(
decoder.probe.is_none(),
"probe must be abandoned after cap overflow"
);
assert_eq!(
decoder.pending_frames.len(),
pending_before,
"pending_frames belong to the active backend; abandon must not drop them"
);
}
#[test]
#[ignore = "requires HWDECODE_SAMPLE_VIDEO and a working hardware backend"]
fn all_backends_failed_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!(
decoder.probe.is_some(),
"probe must be active immediately after open"
);
let p1 = Packet::new(16);
let p2 = Packet::new(32);
{
let probe = decoder.probe.as_mut().expect("probe");
probe.buffered_packets.push(p1);
probe.buffered_packets.push(p2);
probe.remaining_backends.clear();
}
let result = decoder.advance_probe(Error::Ffmpeg(ffmpeg_next::Error::InvalidData));
match result {
Err(Error::AllBackendsFailed(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 AllBackendsFailed, got {other:?}"),
}
}
#[test]
#[ignore = "requires HWDECODE_SAMPLE_VIDEO and a working hardware backend"]
fn all_backends_failed_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.as_mut().expect("probe present");
probe
.attempts
.push((earlier_backend, Box::new(synthetic_earlier)));
probe.remaining_backends.clear(); }
let result = decoder.advance_probe(Error::Ffmpeg(ffmpeg_next::Error::InvalidData));
match result {
Err(Error::AllBackendsFailed(p)) => {
let attempts = p.attempts();
assert_eq!(
attempts.len(),
2,
"AllBackendsFailed 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 AllBackendsFailed, got {other:?}"),
}
}
#[test]
fn the_pre_allocation_rulers_are_what_the_docs_say() {
use super::{PROBE_PIXELS, worst_bytes_per_probe};
assert_eq!(
worst_bytes_per_probe(),
16 * PROBE_PIXELS,
"the worst pixel format is no longer 16 bytes per pixel",
);
let effective_pixels = crate::DEFAULT_MAX_FRAME_BYTES / 16;
assert_eq!(effective_pixels, 33_554_432);
assert!(
effective_pixels > 7680 * 4320,
"8K must fit the byte-derived pixel ceiling",
);
}
#[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(),
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(),
FrameLimits::new().with_max_frame_bytes(usize::MAX),
)
}
.is_ok(),
);
}
#[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(), FrameLimits::new()) }.is_err(),
"an unreadable pool extent must fail closed",
);
let plain = ffmpeg_next::frame::Video::empty();
assert!(unsafe { judge_hw_transfer(plain.as_ptr(), 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 crate::fault_subprocess::{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();
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;
use core::sync::atomic::{AtomicBool, AtomicI64, Ordering};
let quiet = CallbackState {
wanted: ffmpeg_next::ffi::AVPixelFormat::AV_PIX_FMT_VIDEOTOOLBOX,
wanted_int: ffmpeg_next::ffi::AVPixelFormat::AV_PIX_FMT_VIDEOTOOLBOX as i32,
ceiling_declined: AtomicBool::new(false),
declined_pixels: AtomicI64::new(0),
declined_limit: AtomicI64::new(0),
max_frame_bytes: u64::MAX,
frame_budget_declined: core::sync::atomic::AtomicBool::new(false),
declined_frame_bytes: core::sync::atomic::AtomicU64::new(0),
declined_frame_audio: core::sync::atomic::AtomicBool::new(false),
};
assert!(ceiling_declination_of(&quiet).is_none());
let declined = CallbackState {
wanted: ffmpeg_next::ffi::AVPixelFormat::AV_PIX_FMT_VIDEOTOOLBOX,
wanted_int: ffmpeg_next::ffi::AVPixelFormat::AV_PIX_FMT_VIDEOTOOLBOX as i32,
ceiling_declined: AtomicBool::new(true),
declined_pixels: AtomicI64::new(1920 * 1088),
declined_limit: AtomicI64::new(1_048_576),
max_frame_bytes: u64::MAX,
frame_budget_declined: core::sync::atomic::AtomicBool::new(false),
declined_frame_bytes: core::sync::atomic::AtomicU64::new(0),
declined_frame_audio: core::sync::atomic::AtomicBool::new(false),
};
match 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!(
ceiling_declination_of(&declined).is_none(),
"the refusal must be consumed, not latched",
);
assert!(!declined.ceiling_declined.load(Ordering::Relaxed));
}
struct JudgeCase {
ctx_channels: i32,
max_pixels: i64,
max_frame_bytes: u64,
format_raw: i32,
width: i32,
height: i32,
nb_samples: i32,
frame_channels: i32,
}
impl JudgeCase {
fn audio(format_raw: i32, nb_samples: i32, frame_channels: i32) -> Self {
Self {
ctx_channels: 1,
max_pixels: i64::MAX,
max_frame_bytes: u64::MAX,
format_raw,
width: 0,
height: 0,
nb_samples,
frame_channels,
}
}
fn video(format_raw: i32, width: i32, height: i32) -> Self {
Self {
ctx_channels: 0,
max_pixels: i64::MAX,
max_frame_bytes: u64::MAX,
format_raw,
width,
height,
nb_samples: 0,
frame_channels: 0,
}
}
fn with_max_pixels(mut self, v: i64) -> Self {
self.max_pixels = v;
self
}
fn with_max_frame_bytes(mut self, v: u64) -> Self {
self.max_frame_bytes = v;
self
}
fn run(&self) -> std::result::Result<(), i32> {
use ffmpeg_next::ffi;
unsafe {
let audio = self.width <= 0 && self.height <= 0;
let codec = ffi::avcodec_find_decoder(if audio {
ffi::AVCodecID::AV_CODEC_ID_PCM_S16LE
} else {
ffi::AVCodecID::AV_CODEC_ID_RAWVIDEO
});
let ctx = ffi::avcodec_alloc_context3(codec);
assert!(!ctx.is_null());
if audio {
(*ctx).sample_rate = 48_000;
(*ctx).sample_fmt = ffi::AVSampleFormat::AV_SAMPLE_FMT_S16;
ffi::av_channel_layout_default(
core::ptr::addr_of_mut!((*ctx).ch_layout),
self.ctx_channels.max(1),
);
} else {
(*ctx).width = self.width.max(1);
(*ctx).height = self.height.max(1);
(*ctx).pix_fmt = core::mem::transmute::<i32, ffi::AVPixelFormat>(self.format_raw);
}
assert_eq!(
ffi::avcodec_open2(ctx, codec, core::ptr::null_mut()),
0,
"the harness codec must open",
);
(*ctx).max_pixels = self.max_pixels;
let mut state = Box::new(crate::ffi::CallbackState {
wanted: ffi::AVPixelFormat::AV_PIX_FMT_NONE,
wanted_int: ffi::AVPixelFormat::AV_PIX_FMT_NONE as i32,
ceiling_declined: core::sync::atomic::AtomicBool::new(false),
declined_pixels: core::sync::atomic::AtomicI64::new(0),
declined_limit: core::sync::atomic::AtomicI64::new(0),
max_frame_bytes: self.max_frame_bytes,
frame_budget_declined: core::sync::atomic::AtomicBool::new(false),
declined_frame_bytes: core::sync::atomic::AtomicU64::new(0),
declined_frame_audio: core::sync::atomic::AtomicBool::new(false),
});
(*ctx).opaque = (&raw mut *state).cast();
let frame = ffi::av_frame_alloc();
assert!(!frame.is_null());
(*frame).format = self.format_raw;
(*frame).width = self.width;
(*frame).height = self.height;
(*frame).nb_samples = self.nb_samples;
if self.frame_channels > 0 {
ffi::av_channel_layout_default(
core::ptr::addr_of_mut!((*frame).ch_layout),
self.frame_channels,
);
}
let rc = super::judge_buffer(ctx, frame, 0);
drop(state);
ffi::av_frame_free(&mut (frame as *mut _));
ffi::avcodec_free_context(&mut (ctx as *mut _));
if rc < 0 { Err(rc) } else { Ok(()) }
}
}
}
#[test]
fn the_callback_prices_the_frames_layout_not_the_contexts() {
ffmpeg_next::init().expect("ffmpeg init");
const DBLP: i32 = ffmpeg_next::ffi::AVSampleFormat::AV_SAMPLE_FMT_DBLP as i32;
assert!(
JudgeCase::audio(DBLP, 130_000, 255)
.with_max_frame_bytes(16 * 1024 * 1024)
.run()
.is_err(),
"the callback priced the context's channel count, not the frame's",
);
JudgeCase::audio(DBLP, 130_000, 255)
.with_max_frame_bytes(u64::MAX)
.run()
.expect("an affordable frame must still be allocated");
assert!(JudgeCase::audio(DBLP, 1024, 0).run().is_err());
}
#[test]
fn the_callback_recovers_each_mediums_ceiling_independently() {
ffmpeg_next::init().expect("ffmpeg init");
const S16: i32 = ffmpeg_next::ffi::AVSampleFormat::AV_SAMPLE_FMT_S16 as i32;
const NV12: i32 = ffmpeg_next::ffi::AVPixelFormat::AV_PIX_FMT_NV12 as i32;
JudgeCase::audio(S16, 1024, 2)
.with_max_pixels(1)
.with_max_frame_bytes(u64::MAX)
.run()
.expect("a pixel ceiling must not refuse audio");
assert!(
JudgeCase::video(NV12, 16, 16)
.with_max_frame_bytes(0)
.run()
.is_err(),
"a zero byte budget admitted a picture",
);
assert!(
JudgeCase::audio(S16, 1024, 2)
.with_max_frame_bytes(0)
.run()
.is_err(),
"a zero byte budget admitted an audio frame",
);
assert!(
JudgeCase::audio(S16, 65_535, 8)
.with_max_pixels(i64::MAX)
.with_max_frame_bytes(16)
.run()
.is_err(),
);
const RGBAF32: i32 = ffmpeg_next::ffi::AVPixelFormat::AV_PIX_FMT_RGBAF32LE as i32;
JudgeCase::video(RGBAF32, 256, 256)
.with_max_pixels(65_536)
.with_max_frame_bytes(2 * 1024 * 1024)
.run()
.expect("a frame inside both of the caller's limits must be allocated");
assert!(
JudgeCase::video(RGBAF32, 256, 256)
.with_max_pixels(i64::MAX)
.with_max_frame_bytes(1024 * 1024)
.run()
.is_err(),
"a generous pixel ceiling admitted a frame past the byte ceiling",
);
}
#[test]
fn the_callback_judges_cost_and_leaves_logical_extent_to_libavcodec() {
ffmpeg_next::init().expect("ffmpeg init");
const GRAY8: i32 = ffmpeg_next::ffi::AVPixelFormat::AV_PIX_FMT_GRAY8 as i32;
JudgeCase::video(GRAY8, 65_536, 1)
.with_max_pixels(65_536)
.with_max_frame_bytes(8 * 1024 * 1024)
.run()
.expect("a frame inside both of the caller's limits must be allocated");
assert!(
JudgeCase::video(GRAY8, 65_536, 1)
.with_max_pixels(i64::MAX)
.with_max_frame_bytes(1024 * 1024)
.run()
.is_err(),
"the degenerate shape slipped its real cost past the byte ceiling",
);
}
#[test]
fn an_unpriceable_candidate_is_charged_the_conservative_bound() {
ffmpeg_next::init().expect("ffmpeg init");
use crate::footprint::{video_frame_bytes, video_frame_bytes_upper_bound};
const NV12: i32 = ffmpeg_next::ffi::AVPixelFormat::AV_PIX_FMT_NV12 as i32;
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 candidate {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 candidate",
);
let folded = [NV12, VT]
.iter()
.map(|&f| {
video_frame_bytes(f, w, h)
.or_else(|| video_frame_bytes_upper_bound(w, h))
.expect("every candidate must fold")
})
.max()
.expect("a non-empty list");
assert_eq!(
folded, bound,
"a mixed list must be judged at the unpriceable member's bound, not the cheap one",
);
assert_ne!(
folded, cheap,
"the old fold would have taken the cheap figure"
);
}
#[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::Frame => {}
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 = dec.send_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_past_the_end_that_produced_nothing_fails_the_probe() {
let mut dec = auditioning_decoder();
assert_eq!(dec.phase(), SessionPhase::Auditioning);
dec.eof_sent = true;
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!(
"credited a candidate that never decoded a frame with ending the stream, \
stopping the probe from trying the next backend",
),
Ok(Received::Frame) => panic!("a candidate fed only the end produced a frame"),
Err(Error::AllBackendsFailed(_)) => {}
Err(other) => panic!("expected the probe road, got {other:?}"),
}
assert_eq!(
dec.phase(),
SessionPhase::Draining,
"an exhausted probe leaves a committed session, not a candidate",
);
assert_eq!(
dec
.receive_frame(&mut frame)
.expect("no fault past the end"),
Received::Ended,
);
}
#[test]
fn a_latched_refusal_reaches_the_probe_instead_of_dying_unread() {
let mut dec = auditioning_decoder();
dec.eof_sent = true;
crate::ffi::declare_ceiling_declined_for_test(dec.state.callback_state, 8_294_400, 2_073_600);
let mut frame = crate::Frame::empty().expect("frame slot");
let Err(Error::AllBackendsFailed(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
.state
.inner
.send_eof()
.expect("the substrate takes the end");
},
SessionPhase::Streaming,
),
(
"draining x EAGAIN",
|dec: &mut VideoDecoder| {
dec.eof_sent = true;
},
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");
crate::ffi::declare_ceiling_declined_for_test(dec.state.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::Frame) => panic!("{name}: a declined surface produced a frame"),
Err(Error::AllBackendsFailed(p)) => {
let cause = format!("{:?}", p.attempts());
assert!(
cause.contains("HwSurfaceTooLarge") && cause.contains("8294400"),
"{name}: the refusal must reach the attempt log with its numbers: {cause}",
);
}
Err(other) => panic!("{name}: expected the latched refusal, got {other:?}"),
}
}
}
#[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);
crate::ffi::declare_ceiling_declined_for_test(dec.state.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::AllBackendsFailed(p)) => {
let cause = format!("{:?}", p.attempts());
assert!(
cause.contains("HwSurfaceTooLarge") && cause.contains("8294400"),
"the refusal must reach the attempt log with its numbers: {cause}",
);
}
Err(other) => panic!(
"the latched refusal must outrank libavcodec's report on the send road too, \
got {other:?}",
),
}
}
#[test]
fn the_post_commit_failure_records_the_verdict_it_was_given() {
let dec = committed_decoder();
crate::ffi::declare_ceiling_declined_for_test(dec.state.callback_state, 8_294_400, 2_073_600);
let recorded = dec.post_commit_hw_failure(Error::Ffmpeg(ffmpeg_next::Error::InvalidData));
let Error::AllBackendsFailed(p) = &recorded else {
panic!("expected AllBackendsFailed, got {recorded:?}");
};
let cause = format!("{:?}", p.attempts());
assert!(
cause.contains("Invalid data"),
"it must record the verdict it was handed: {cause}",
);
assert!(
!cause.contains("HwSurfaceTooLarge"),
"it must not mint a second verdict — that is the double-funnel: {cause}",
);
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 an untouched `post_commit_hw_failure`: {verdict:?}",
);
let threaded = dec.post_commit_hw_failure(verdict);
let Error::AllBackendsFailed(p) = &threaded else {
panic!("expected AllBackendsFailed, got {threaded:?}");
};
let cause = format!("{:?}", p.attempts());
assert!(
cause.contains("HwSurfaceTooLarge") && cause.contains("8294400"),
"the threaded verdict must reach the attempt log with its numbers: {cause}",
);
}
#[test]
fn a_funnel_consumes_what_it_collects() {
let dec = committed_decoder();
crate::ffi::declare_ceiling_declined_for_test(dec.state.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 };
assert!(
!is_hw_decode_failure(&hevc),
"if the errno list ever grows to cover this, the lane below stops \
testing the widening and must be rebuilt on a spelling it misses",
);
let dec = committed_decoder();
crate::ffi::declare_ceiling_declined_for_test(dec.state.callback_state, 8_294_400, 2_073_600);
let out = reported(dec.hw_failure(hevc, BareVerdict::CandidateFailure));
let Error::AllBackendsFailed(p) = &out else {
panic!("a declined surface must ask for the software fallback, got {out:?}");
};
let cause = format!("{:?}", p.attempts());
assert!(
cause.contains("HwSurfaceTooLarge") && cause.contains("8294400"),
"the verdict must carry its own numbers, not the codec's errno: {cause}",
);
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_never_triggers_the_fallback_whatever_errno_rides_with_it() {
let einval = ffmpeg_next::Error::Other {
errno: libc::EINVAL,
};
assert!(
is_hw_decode_failure(&einval),
"the errno arm must match `judge_buffer`'s own answer, or this lane \
stops testing the exclusion it exists for",
);
for (name, raw) in [
("judge_buffer's own -EINVAL", einval),
("libavcodec's InvalidData", ffmpeg_next::Error::InvalidData),
] {
let dec = committed_decoder();
crate::ffi::declare_frame_budget_declined_for_test(dec.state.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 — rather than sending the caller down a \
fallback that will be refused by the same ceiling: {out:?}",
);
}
}
#[test]
fn an_unnamed_failure_is_still_judged_on_its_errno() {
let dec = committed_decoder();
let out = reported(dec.hw_failure(
ffmpeg_next::Error::InvalidData,
BareVerdict::CandidateFailure,
));
assert!(
matches!(out, Error::AllBackendsFailed(_)),
"an unnamed hardware decode failure must still ask for the fallback: {out:?}",
);
let dec = committed_decoder();
let out = reported(dec.hw_failure(
ffmpeg_next::Error::Other { errno: 1 },
BareVerdict::CandidateFailure,
));
assert!(
matches!(out, Error::Ffmpeg(ffmpeg_next::Error::Other { errno: 1 })),
"an unrecognised errno with nothing latched must travel as itself: {out:?}",
);
}
#[test]
fn the_committed_receive_route_lets_a_budget_refusal_travel_unwrapped() {
let mut dec = committed_decoder();
crate::ffi::declare_frame_budget_declined_for_test(dec.state.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(Error::AllBackendsFailed(p)) => panic!(
"a budget refusal took the fallback road — software will be refused by the \
same ceiling, and the actionable error is now buried: {:?}",
p.attempts(),
),
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:?})")
}
}
}
#[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 => crate::ffi::declare_ceiling_declined_for_test(
dec.state.callback_state,
8_294_400,
2_073_600,
),
Latch::Budget => {
crate::ffi::declare_frame_budget_declined_for_test(dec.state.callback_state, 12_582_912)
}
}
let packet = crate::boundary::try_packet_copy(&[0u8; 16]).expect("packet");
let out = match face {
Face::Packet => dec.send_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::AllBackendsFailed(p))) => {
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, other) => panic!(
"{name}: a declined surface must route, not exit plain — returned plain it \
reaches no fallback and advances no probe: {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, 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 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;
}
}