use std::{
ffi::{c_char, c_int, c_uint},
ptr,
};
use ffmpeg_next::ffi::{
AV_CODEC_HW_CONFIG_METHOD_HW_DEVICE_CTX, AVCodec, AVCodecContext, AVHWDeviceType, AVPacket,
AVPacketSideDataType, AVPixelFormat, avcodec_get_hw_config,
};
unsafe extern "C" {
fn av_get_pix_fmt_name(pix_fmt: c_int) -> *const c_char;
}
unsafe extern "C" {
#[link_name = "av_packet_new_side_data"]
fn av_packet_new_side_data_raw(pkt: *mut AVPacket, kind: c_uint, size: usize) -> *mut u8;
#[link_name = "av_packet_side_data_name"]
fn av_packet_side_data_name_raw(kind: c_int) -> *const c_char;
}
pub(crate) fn runtime_names_side_data(kind: i32) -> bool {
#[cfg(test)]
if side_data_namer_stub::names(kind) {
return true;
}
!unsafe { av_packet_side_data_name_raw(kind) }.is_null()
}
#[cfg(test)]
pub(crate) mod side_data_namer_stub {
use core::cell::Cell;
std::thread_local! {
static EXTRA: Cell<Option<i32>> = const { Cell::new(None) };
}
pub(crate) fn name_also(kind: i32) -> Guard {
EXTRA.with(|extra| extra.set(Some(kind)));
Guard(())
}
pub(crate) struct Guard(());
impl Drop for Guard {
fn drop(&mut self) {
EXTRA.with(|extra| extra.set(None));
}
}
pub(crate) fn names(kind: i32) -> bool {
EXTRA.with(|extra| extra.get() == Some(kind))
}
}
pub(crate) unsafe fn packet_new_side_data(
pkt: *mut AVPacket,
kind: i32,
size: usize,
) -> Option<*mut u8> {
if !runtime_names_side_data(kind) {
return None;
}
let ptr = unsafe { av_packet_new_side_data_raw(pkt, kind as c_uint, size) };
(!ptr.is_null()).then_some(ptr)
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub(crate) const fn side_data_type_count() -> i32 {
AVPacketSideDataType::AV_PKT_DATA_NB as i32
}
const PIX_FMT_NAME_MAX_BYTES: usize = 64;
pub(crate) unsafe fn table_text(ptr: *const c_char, max_bytes: usize) -> Option<&'static str> {
if ptr.is_null() {
return None;
}
for len in 0..max_bytes {
if unsafe { *ptr.add(len).cast::<u8>() } == 0 {
let bytes = unsafe { std::slice::from_raw_parts(ptr.cast::<u8>(), len) };
return std::str::from_utf8(bytes).ok();
}
}
None
}
pub(crate) fn pix_fmt_name_static(raw: i32) -> Option<&'static str> {
let ptr = unsafe { av_get_pix_fmt_name(raw) };
if ptr.is_null() {
return None;
}
for len in 0..PIX_FMT_NAME_MAX_BYTES {
if unsafe { *ptr.add(len).cast::<u8>() } == 0 {
let bytes = unsafe { std::slice::from_raw_parts(ptr.cast::<u8>(), len) };
return std::str::from_utf8(bytes).ok();
}
}
None
}
#[repr(C)]
pub struct CallbackState {
pub(crate) wanted: AVPixelFormat,
pub(crate) wanted_int: i32,
pub(crate) ceiling_declined: core::sync::atomic::AtomicBool,
pub(crate) declined_pixels: core::sync::atomic::AtomicI64,
pub(crate) declined_limit: core::sync::atomic::AtomicI64,
pub(crate) max_frame_bytes: u64,
pub(crate) frame_budget_declined: core::sync::atomic::AtomicBool,
pub(crate) declined_frame_bytes: core::sync::atomic::AtomicU64,
pub(crate) declined_frame_audio: core::sync::atomic::AtomicBool,
}
impl CallbackState {
pub fn new(max_frame_bytes: u64) -> Self {
Self {
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,
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),
}
}
pub fn set_wanted(&mut self, wanted: AVPixelFormat) {
self.wanted = wanted;
self.wanted_int = wanted as i32;
}
}
pub unsafe fn take_frame_budget_declination(
state: *const CallbackState,
) -> Option<(u64, u64, bool)> {
use core::sync::atomic::Ordering;
if state.is_null() {
return None;
}
let (declined, bytes, limit, audio) = unsafe {
(
(*state)
.frame_budget_declined
.swap(false, Ordering::Acquire),
(*state).declined_frame_bytes.load(Ordering::Relaxed),
(*state).max_frame_bytes,
(*state).declined_frame_audio.load(Ordering::Relaxed),
)
};
declined.then_some((bytes, limit, audio))
}
pub unsafe fn take_ceiling_declination(state: *const CallbackState) -> Option<(i64, i64)> {
use core::sync::atomic::Ordering;
if state.is_null() {
return None;
}
let (declined, pixels, limit) = unsafe {
(
(*state).ceiling_declined.swap(false, Ordering::Acquire),
(*state).declined_pixels.load(Ordering::Relaxed),
(*state).declined_limit.load(Ordering::Relaxed),
)
};
declined.then_some((pixels, limit))
}
#[cfg(any(test, feature = "test-support"))]
pub unsafe fn declare_frame_budget_declined_for_test(state: *mut CallbackState, bytes: u64) {
use core::sync::atomic::Ordering;
if state.is_null() {
return;
}
unsafe {
(*state)
.declined_frame_bytes
.store(bytes, Ordering::Relaxed);
(*state)
.declined_frame_audio
.store(false, Ordering::Relaxed);
(*state)
.frame_budget_declined
.store(true, Ordering::Release);
}
}
pub unsafe extern "C" fn get_hw_format(
ctx: *mut AVCodecContext,
pix_fmts: *const AVPixelFormat,
) -> AVPixelFormat {
debug_assert!(!ctx.is_null());
debug_assert!(!pix_fmts.is_null());
let state = unsafe { (*ctx).opaque as *const CallbackState };
let (wanted, wanted_int) = if state.is_null() {
(
AVPixelFormat::AV_PIX_FMT_NONE,
AVPixelFormat::AV_PIX_FMT_NONE as i32,
)
} else {
unsafe { ((*state).wanted, (*state).wanted_int) }
};
let mut p = pix_fmts as *const i32;
let none_int = AVPixelFormat::AV_PIX_FMT_NONE as i32;
loop {
let v = unsafe { ptr::read(p) };
if v == none_int {
return AVPixelFormat::AV_PIX_FMT_NONE;
}
if v == wanted_int {
let budget = if state.is_null() {
u64::MAX
} else {
unsafe { (*state).max_frame_bytes }
};
if let Some((cost, limit)) = unsafe { coded_extent_over_ceiling(ctx, wanted, budget) } {
if !state.is_null() {
use core::sync::atomic::Ordering;
unsafe {
(*state)
.declined_pixels
.store(i64::try_from(cost).unwrap_or(i64::MAX), Ordering::Relaxed);
(*state)
.declined_limit
.store(i64::try_from(limit).unwrap_or(i64::MAX), Ordering::Relaxed);
(*state).ceiling_declined.store(true, Ordering::Release);
}
}
return AVPixelFormat::AV_PIX_FMT_NONE;
}
return wanted;
}
p = unsafe { p.add(1) };
}
}
unsafe fn coded_extent_over_ceiling(
ctx: *mut AVCodecContext,
wanted: AVPixelFormat,
budget: u64,
) -> Option<(u64, u64)> {
let device_ref = unsafe { (*ctx).hw_device_ctx };
if device_ref.is_null() {
return None;
}
let mut frames_ref: *mut ffmpeg_next::ffi::AVBufferRef = core::ptr::null_mut();
let rc = unsafe {
ffmpeg_next::ffi::avcodec_get_hw_frames_parameters(ctx, device_ref, wanted, &mut frames_ref)
};
if rc < 0 || frames_ref.is_null() {
return Some((u64::MAX, budget));
}
let (pool_w, pool_h, sw_format) = unsafe {
let fc = (*frames_ref).data as *const ffmpeg_next::ffi::AVHWFramesContext;
(
(*fc).width,
(*fc).height,
core::ptr::read(core::ptr::addr_of!((*fc).sw_format) as *const libc::c_int),
)
};
unsafe { ffmpeg_next::ffi::av_buffer_unref(&mut frames_ref) };
let Some(cost) = pool_bytes(sw_format, pool_w, pool_h) else {
return Some((u64::MAX, budget));
};
if cost > budget {
tracing::warn!(
pool_width = pool_w,
pool_height = pool_h,
cost,
budget,
"hwdecode: the hardware surface pool exceeds the frame ceiling; declining the \
hardware format so the decode falls back to software, where the same ceiling applies"
);
return Some((cost, budget));
}
None
}
fn pool_bytes(sw_format: libc::c_int, w: libc::c_int, h: libc::c_int) -> Option<u64> {
crate::footprint::video_frame_bytes(sw_format, w, h)
.or_else(|| crate::footprint::video_frame_bytes_upper_bound(w, h))
.map(|bytes| bytes as u64)
}
pub unsafe fn codec_supports_hwaccel(
codec: *const AVCodec,
device_type: AVHWDeviceType,
wanted_pix_fmt: i32,
) -> bool {
debug_assert!(!codec.is_null());
let device_type_int = device_type as i32;
let mut i = 0;
loop {
let cfg = unsafe { avcodec_get_hw_config(codec, i) };
if cfg.is_null() {
return false;
}
let methods: i32 = unsafe { ptr::read(ptr::addr_of!((*cfg).methods)) };
let cfg_device_type_int: i32 =
unsafe { ptr::read(ptr::addr_of!((*cfg).device_type) as *const i32) };
let cfg_pix_fmt_int: i32 = unsafe { ptr::read(ptr::addr_of!((*cfg).pix_fmt) as *const i32) };
if methods & (AV_CODEC_HW_CONFIG_METHOD_HW_DEVICE_CTX as i32) != 0
&& cfg_device_type_int == device_type_int
&& cfg_pix_fmt_int == wanted_pix_fmt
{
return true;
}
i += 1;
}
}
#[cfg(any(test, feature = "test-support"))]
pub unsafe fn declare_ceiling_declined_for_test(
state: *mut CallbackState,
pixels: i64,
limit: i64,
) {
use core::sync::atomic::Ordering;
if state.is_null() {
return;
}
unsafe {
(*state).declined_pixels.store(pixels, Ordering::Relaxed);
(*state).declined_limit.store(limit, Ordering::Relaxed);
(*state).ceiling_declined.store(true, Ordering::Release);
}
}
pub(crate) mod c_shims {
use libc::c_int;
use super::AVCodec;
unsafe extern "C" {
pub(crate) fn avcodec_find_decoder(id: c_int) -> *const AVCodec;
pub(crate) fn avcodec_descriptor_get(id: c_int) -> *const ffmpeg_next::ffi::AVCodecDescriptor;
pub(crate) fn av_pix_fmt_desc_get_id(
desc: *const ffmpeg_next::ffi::AVPixFmtDescriptor,
) -> c_int;
pub(crate) fn av_image_get_buffer_size(
pix_fmt: c_int,
width: c_int,
height: c_int,
align: c_int,
) -> c_int;
#[cfg(test)]
pub(crate) fn av_get_bytes_per_sample(sample_fmt: c_int) -> c_int;
pub(crate) fn av_samples_get_buffer_size(
linesize: *mut c_int,
nb_channels: c_int,
nb_samples: c_int,
sample_fmt: c_int,
align: c_int,
) -> c_int;
pub(crate) fn av_hwframe_transfer_get_formats(
hwframe_ctx: *mut ffmpeg_next::ffi::AVBufferRef,
dir: c_int,
formats: *mut *mut c_int,
flags: c_int,
) -> c_int;
}
}
pub fn find_decoder_raw(id: c_int) -> Option<ptr::NonNull<AVCodec>> {
ptr::NonNull::new(unsafe { c_shims::avcodec_find_decoder(id) }.cast_mut())
}
pub unsafe fn visit_hwframe_download_formats<B>(
frames_ctx: *mut ffmpeg_next::ffi::AVBufferRef,
mut visit: impl FnMut(c_int) -> core::ops::ControlFlow<B>,
) -> Result<Option<B>, c_int> {
let mut list: *mut c_int = ptr::null_mut();
let rc = unsafe { c_shims::av_hwframe_transfer_get_formats(frames_ctx, 0, &mut list, 0) };
if rc < 0 {
return Err(rc);
}
if list.is_null() {
return Ok(None);
}
let list = FormatList(list);
let none = AVPixelFormat::AV_PIX_FMT_NONE as c_int;
let mut cursor = list.0.cast_const();
loop {
let candidate = unsafe { cursor.read() };
if candidate == none {
return Ok(None);
}
if let core::ops::ControlFlow::Break(value) = visit(candidate) {
return Ok(Some(value));
}
cursor = unsafe { cursor.add(1) };
}
}
struct FormatList(*mut c_int);
impl Drop for FormatList {
fn drop(&mut self) {
#[cfg(test)]
format_list_probe::freed();
unsafe { ffmpeg_next::ffi::av_freep(ptr::addr_of_mut!(self.0).cast()) };
}
}
#[cfg(test)]
pub(crate) mod format_list_probe {
use core::cell::Cell;
std::thread_local! {
static FREED: Cell<u32> = const { Cell::new(0) };
}
pub(crate) fn freed() {
FREED.with(|freed| freed.set(freed.get() + 1));
}
pub(crate) fn frees() -> u32 {
FREED.with(Cell::get)
}
}
pub(crate) unsafe extern "C" fn judge_buffer(
ctx: *mut ffmpeg_next::ffi::AVCodecContext,
frame: *mut ffmpeg_next::ffi::AVFrame,
flags: libc::c_int,
) -> libc::c_int {
let (width, height) = unsafe { ((*frame).width, (*frame).height) };
let hw_frames = unsafe { (*frame).hw_frames_ctx };
if hw_frames.is_null() {
let state = unsafe { (*ctx).opaque } as *const CallbackState;
if state.is_null() {
return -(libc::EINVAL);
}
let byte_ceiling = u128::from(unsafe { (*state).max_frame_bytes });
let (format_raw, nb_samples) = unsafe { ((*frame).format, (*frame).nb_samples) };
let priced = if width > 0 && height > 0 {
crate::footprint::video_frame_bytes(format_raw, width, height)
} else if nb_samples > 0 {
let channels = unsafe { (*frame).ch_layout.nb_channels };
if channels <= 0 {
return -(libc::EINVAL);
}
crate::footprint::audio_frame_bytes(format_raw, nb_samples as usize, channels as usize)
} else {
Some(0)
};
let record = |bytes: u64| {
use core::sync::atomic::Ordering;
unsafe {
(*state)
.declined_frame_bytes
.store(bytes, Ordering::Relaxed);
(*state)
.declined_frame_audio
.store(width <= 0 && height <= 0, Ordering::Relaxed);
(*state)
.frame_budget_declined
.store(true, Ordering::Release);
}
-(libc::EINVAL)
};
match priced {
None => return record(u64::MAX),
Some(0) => {}
Some(bytes) if byte_ceiling == 0 => return record(bytes as u64),
Some(bytes) if bytes as u128 > byte_ceiling => return record(bytes as u64),
Some(_) => {}
}
}
unsafe { ffmpeg_next::ffi::avcodec_default_get_buffer2(ctx, frame, flags) }
}
pub unsafe fn install_allocator_judge(
ctx: *mut ffmpeg_next::ffi::AVCodecContext,
state: *mut CallbackState,
) {
unsafe {
(*ctx).opaque = state.cast();
(*ctx).get_buffer2 = Some(judge_buffer);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn pix_fmt_name_reads_the_linked_librarys_own_table() {
assert_eq!(
pix_fmt_name_static(AVPixelFormat::AV_PIX_FMT_YUV420P as i32),
Some("yuv420p")
);
assert_eq!(
pix_fmt_name_static(AVPixelFormat::AV_PIX_FMT_NV12 as i32),
Some("nv12")
);
assert_eq!(
pix_fmt_name_static(AVPixelFormat::AV_PIX_FMT_VIDEOTOOLBOX as i32),
Some("videotoolbox_vld")
);
}
#[test]
fn pix_fmt_name_refuses_what_ffmpeg_cannot_name() {
assert_eq!(
pix_fmt_name_static(AVPixelFormat::AV_PIX_FMT_NONE as i32),
None
);
assert_eq!(pix_fmt_name_static(-99_999), None);
assert_eq!(pix_fmt_name_static(i32::MIN), None);
assert_eq!(pix_fmt_name_static(i32::MAX), None);
assert_eq!(
pix_fmt_name_static(AVPixelFormat::AV_PIX_FMT_NB as i32),
None
);
}
struct FakeCtx(*mut AVCodecContext);
impl FakeCtx {
fn new(state: *mut CallbackState) -> Self {
let boxed: Box<AVCodecContext> = unsafe { Box::new(std::mem::zeroed()) };
let raw = Box::into_raw(boxed);
unsafe { (*raw).opaque = state.cast() };
Self(raw)
}
}
impl Drop for FakeCtx {
fn drop(&mut self) {
unsafe { drop(Box::from_raw(self.0)) };
}
}
fn make_state(wanted: AVPixelFormat) -> CallbackState {
CallbackState {
wanted,
wanted_int: wanted 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),
}
}
fn run(state: &CallbackState, mut offered: Vec<i32>) -> AVPixelFormat {
offered.push(AVPixelFormat::AV_PIX_FMT_NONE as i32);
let ctx = FakeCtx::new(state as *const _ as *mut _);
unsafe { get_hw_format(ctx.0, offered.as_ptr() as *const AVPixelFormat) }
}
#[test]
fn returns_wanted_hw_format_when_offered() {
let state = make_state(AVPixelFormat::AV_PIX_FMT_VIDEOTOOLBOX);
let got = run(
&state,
vec![
AVPixelFormat::AV_PIX_FMT_VIDEOTOOLBOX as i32,
AVPixelFormat::AV_PIX_FMT_NV12 as i32,
],
);
assert_eq!(got, AVPixelFormat::AV_PIX_FMT_VIDEOTOOLBOX);
}
#[test]
fn returns_none_when_wanted_absent() {
let state = make_state(AVPixelFormat::AV_PIX_FMT_VIDEOTOOLBOX);
let got = run(
&state,
vec![
AVPixelFormat::AV_PIX_FMT_NV12 as i32,
AVPixelFormat::AV_PIX_FMT_YUV420P as i32,
],
);
assert_eq!(got, AVPixelFormat::AV_PIX_FMT_NONE);
}
#[test]
fn null_opaque_is_treated_as_strict() {
let boxed: Box<AVCodecContext> = unsafe { Box::new(std::mem::zeroed()) };
let ctx_raw = Box::into_raw(boxed);
unsafe { (*ctx_raw).opaque = ptr::null_mut() };
let offered = [
AVPixelFormat::AV_PIX_FMT_NV12 as i32,
AVPixelFormat::AV_PIX_FMT_NONE as i32,
];
let got = unsafe { get_hw_format(ctx_raw, offered.as_ptr() as *const AVPixelFormat) };
assert_eq!(got, AVPixelFormat::AV_PIX_FMT_NONE);
unsafe { drop(Box::from_raw(ctx_raw)) };
}
#[test]
fn unknown_offered_value_is_skipped_without_ub() {
let state = make_state(AVPixelFormat::AV_PIX_FMT_VIDEOTOOLBOX);
let got = run(
&state,
vec![
99_999_i32, AVPixelFormat::AV_PIX_FMT_NV12 as i32,
],
);
assert_eq!(got, AVPixelFormat::AV_PIX_FMT_NONE);
}
#[cfg(target_os = "macos")]
#[test]
fn codec_supports_hwaccel_requires_matching_pix_fmt() {
use ffmpeg_next::ffi::{AVCodecID, AVHWDeviceType, AVPixelFormat, avcodec_find_decoder};
let codec_ptr = unsafe { avcodec_find_decoder(AVCodecID::AV_CODEC_ID_H264) };
assert!(!codec_ptr.is_null(), "H.264 decoder must be present");
let device = AVHWDeviceType::AV_HWDEVICE_TYPE_VIDEOTOOLBOX;
let videotoolbox = AVPixelFormat::AV_PIX_FMT_VIDEOTOOLBOX as i32;
let nv12 = AVPixelFormat::AV_PIX_FMT_NV12 as i32;
assert!(
unsafe { codec_supports_hwaccel(codec_ptr, device, videotoolbox) },
"VideoToolbox + AV_PIX_FMT_VIDEOTOOLBOX must be advertised by FFmpeg's H.264 decoder"
);
assert!(
!unsafe { codec_supports_hwaccel(codec_ptr, device, nv12) },
"VideoToolbox + AV_PIX_FMT_NV12 must NOT match the codec's HW config — \
the strict get_format would have no offered HW format to return"
);
}
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);
}
let mut state = Box::new(CallbackState::new(self.max_frame_bytes));
install_allocator_judge(ctx, &raw mut *state);
assert_eq!(
ffi::avcodec_open2(ctx, codec, core::ptr::null_mut()),
0,
"the harness codec must open",
);
(*ctx).max_pixels = self.max_pixels;
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 get_buffer2 = (*ctx).get_buffer2.expect("the door installs the judge");
let rc = get_buffer2(ctx, frame, 0);
ffi::av_frame_free(&mut (frame as *mut _));
ffi::avcodec_free_context(&mut (ctx as *mut _));
drop(state);
if rc < 0 { Err(rc) } else { Ok(()) }
}
}
}
#[test]
fn the_raw_decoder_door_answers_every_integer() {
let h264 = ffmpeg_next::ffi::AVCodecID::AV_CODEC_ID_H264 as c_int;
assert!(
find_decoder_raw(h264).is_some(),
"H.264 is decodable in this build"
);
for id in [-1, i32::MAX, i32::MIN] {
assert!(find_decoder_raw(id).is_none(), "no decoder for {id}");
}
}
#[test]
#[cfg(target_os = "macos")]
fn a_panicking_visitor_leaves_no_download_list_behind() {
use core::ops::ControlFlow;
use ffmpeg_next::ffi::{
AVBufferRef, AVHWDeviceType, AVHWFramesContext, av_buffer_unref, av_hwdevice_ctx_create,
av_hwframe_ctx_alloc,
};
let mut device: *mut AVBufferRef = ptr::null_mut();
let rc = unsafe {
av_hwdevice_ctx_create(
&mut device,
AVHWDeviceType::AV_HWDEVICE_TYPE_VIDEOTOOLBOX,
ptr::null(),
ptr::null_mut(),
0,
)
};
assert_eq!(rc, 0, "a VideoToolbox device context");
let mut frames = unsafe { av_hwframe_ctx_alloc(device) };
assert!(!frames.is_null(), "a frames context");
unsafe {
let ctx = (*frames).data.cast::<AVHWFramesContext>();
(*ctx).format = AVPixelFormat::AV_PIX_FMT_VIDEOTOOLBOX;
(*ctx).sw_format = AVPixelFormat::AV_PIX_FMT_NV12;
(*ctx).width = 64;
(*ctx).height = 64;
}
let before = format_list_probe::frees();
let mut seen = Vec::new();
let walked = unsafe {
visit_hwframe_download_formats::<()>(frames, |format| {
seen.push(format);
ControlFlow::Continue(())
})
};
assert_eq!(walked, Ok(None));
assert_eq!(seen, [AVPixelFormat::AV_PIX_FMT_NV12 as c_int]);
assert_eq!(
format_list_probe::frees() - before,
1,
"freed on the way out"
);
let before = format_list_probe::frees();
let caught = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
unsafe { visit_hwframe_download_formats::<()>(frames, |_| panic!("a visitor that panics")) }
}));
assert!(caught.is_err(), "the visitor's panic reaches the caller");
assert_eq!(
format_list_probe::frees() - before,
1,
"the guard freed the list while the panic unwound",
);
unsafe {
av_buffer_unref(&mut frames);
av_buffer_unref(&mut device);
}
}
#[test]
fn the_door_installs_the_judge_with_its_state() {
unsafe {
let mut ctx = ffmpeg_next::ffi::avcodec_alloc_context3(core::ptr::null());
assert!(!ctx.is_null());
let mut state = Box::new(CallbackState::new(1 << 20));
let state_ptr = &raw mut *state;
install_allocator_judge(ctx, state_ptr);
assert_eq!((*ctx).opaque, state_ptr.cast::<core::ffi::c_void>());
assert_eq!(
(*ctx)
.get_buffer2
.map(|callback| callback as *const () as usize),
Some(judge_buffer as *const () as usize),
"the door installs the judge itself",
);
ffmpeg_next::ffi::avcodec_free_context(&mut ctx);
drop(state);
}
}
#[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",
);
}
}