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}"
);
}
}
}
}