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