use core::ptr::{self, NonNull, addr_of, addr_of_mut, read_unaligned, write_unaligned};
use ffmpeg_next::{
ffi::{
AVBufferRef, AVFrame, AVHWFramesContext, AVPixelFormat, av_buffer_unref, av_frame_unref,
av_hwframe_ctx_alloc, av_hwframe_ctx_init, av_hwframe_get_buffer,
},
frame,
};
use libc::{c_int, c_void};
use mediadecode::decoder::ScaledOutputCapability;
#[allow(non_snake_case)]
mod ffi {
use libc::c_void;
pub(super) type OsStatus = i32;
pub(super) type CVPixelBufferRef = *mut c_void;
pub(super) type CFTypeRef = *const c_void;
pub(super) type CFAllocatorRef = *const c_void;
#[repr(C)]
pub(super) struct OpaqueVtPixelTransferSession {
_private: [u8; 0],
}
pub(super) type VTPixelTransferSessionRef = *mut OpaqueVtPixelTransferSession;
unsafe extern "C" {
pub(super) fn VTPixelTransferSessionCreate(
allocator: CFAllocatorRef,
session_out: *mut VTPixelTransferSessionRef,
) -> OsStatus;
pub(super) fn VTPixelTransferSessionInvalidate(session: VTPixelTransferSessionRef);
pub(super) fn VTPixelTransferSessionTransferImage(
session: VTPixelTransferSessionRef,
source: CVPixelBufferRef,
destination: CVPixelBufferRef,
) -> OsStatus;
pub(super) fn CFRelease(cf: CFTypeRef);
pub(super) fn CVPixelBufferGetPixelFormatType(buffer: CVPixelBufferRef) -> u32;
pub(super) fn CVPixelBufferGetWidth(buffer: CVPixelBufferRef) -> usize;
pub(super) fn CVPixelBufferGetHeight(buffer: CVPixelBufferRef) -> usize;
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
struct ThreadOwner(std::thread::ThreadId);
impl ThreadOwner {
fn current() -> Self {
Self(std::thread::current().id())
}
fn is_current(self) -> bool {
self == Self::current()
}
}
#[cfg_attr(not(tarpaulin), inline)]
const fn gcd(mut a: i128, mut b: i128) -> i128 {
while b != 0 {
let t = b;
b = a % b;
a = t;
}
a
}
pub(crate) const fn scaled_sample_aspect_ratio(
sar: (i32, i32),
src: (u32, u32),
dst: (u32, u32),
) -> Option<(i32, i32)> {
let (num, den) = sar;
if num <= 0 || den <= 0 || src.0 == 0 || src.1 == 0 || dst.0 == 0 || dst.1 == 0 {
return Some(sar);
}
let numerator = (num as i128) * (src.0 as i128) * (dst.1 as i128);
let denominator = (den as i128) * (dst.0 as i128) * (src.1 as i128);
let divisor = gcd(numerator, denominator);
let numerator = numerator / divisor;
let denominator = denominator / divisor;
if numerator > i32::MAX as i128 || denominator > i32::MAX as i128 {
return None;
}
Some((numerator as i32, denominator as i32))
}
#[cfg_attr(not(tarpaulin), inline)]
pub(crate) const fn is_acceptable_request(size: (u32, u32), source: (u32, u32)) -> bool {
size.0 != 0 && size.1 != 0 && size.0 <= source.0 && size.1 <= source.1
}
struct Session {
handle: NonNull<ffi::OpaqueVtPixelTransferSession>,
owner: ThreadOwner,
}
impl Drop for Session {
fn drop(&mut self) {
let owned_here = self.owner.is_current();
unsafe {
if owned_here {
ffi::VTPixelTransferSessionInvalidate(self.handle.as_ptr());
}
ffi::CFRelease(self.handle.as_ptr().cast::<c_void>().cast_const());
}
if !owned_here {
tracing::debug!(
"mediadecode-ffmpeg: scaled-output session released from a thread other than the one that created it; skipped the optional invalidate"
);
}
}
}
struct FittedPool(*mut AVBufferRef);
impl Drop for FittedPool {
fn drop(&mut self) {
unsafe { av_buffer_unref(&mut self.0) };
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
struct StageKey {
source_cv_format: u32,
source: (u32, u32),
fitted: (u32, u32),
sw_format: c_int,
}
struct Built {
fitted: frame::Video,
key: StageKey,
session: Session,
pool: FittedPool,
}
enum Cache {
Empty,
Built(Built),
}
impl Cache {
fn built_key(&self) -> Option<StageKey> {
match self {
Self::Built(built) => Some(built.key),
Self::Empty => None,
}
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
enum CachePlan {
Reuse,
Build,
}
fn cache_plan(built: Option<StageKey>, want: StageKey) -> CachePlan {
if built == Some(want) {
CachePlan::Reuse
} else {
CachePlan::Build
}
}
fn is_stale(key: Option<StageKey>, cv_format: u32, extent: (u32, u32), sw_format: c_int) -> bool {
key.is_some_and(|key| {
key.source_cv_format != cv_format || key.source != extent || key.sw_format != sw_format
})
}
pub(crate) struct ScaledOutput {
request: Option<(u32, u32)>,
unhonored: bool,
cache: Cache,
}
struct Source {
pixbuf: ffi::CVPixelBufferRef,
device_ref: *mut AVBufferRef,
cv_format: u32,
sw_format: c_int,
extent: (u32, u32),
sar: (i32, i32),
}
impl ScaledOutput {
pub(crate) const fn new() -> Self {
Self {
request: None,
unhonored: false,
cache: Cache::Empty,
}
}
pub(crate) const fn supported() -> bool {
true
}
#[cfg(test)]
pub(crate) const fn staging_armed(&self) -> bool {
!self.unhonored
}
pub(crate) const fn promise_stands(&self) -> bool {
!self.unhonored
}
fn stand_down(&mut self) {
self.unhonored = true;
self.retire();
}
#[cfg(test)]
pub(crate) const fn requested(&self) -> Option<(u32, u32)> {
self.request
}
pub(crate) fn latch_failure(&mut self) {
self.stand_down();
}
pub(crate) fn cancel(&mut self) {
self.request = None;
self.retire();
}
pub(crate) fn retire(&mut self) {
self.cache = Cache::Empty;
}
fn retire_unless_source_is(&mut self, cv_format: u32, extent: (u32, u32), sw_format: c_int) {
if is_stale(self.cache.built_key(), cv_format, extent, sw_format) {
self.retire();
}
}
pub(crate) fn request(&mut self, size: (u32, u32), source: (u32, u32)) -> ScaledOutputCapability {
if !is_acceptable_request(size, source) {
tracing::debug!(
requested_width = size.0,
requested_height = size.1,
source_width = source.0,
source_height = source.1,
"mediadecode-ffmpeg: scaled-output request refused (zero or upscale); \
the session returns to full size"
);
self.request = None;
self.retire();
return ScaledOutputCapability::Unsupported;
}
self.retire();
self.request = Some(size);
self.unhonored = false;
ScaledOutputCapability::Supported
}
pub(crate) fn stage(&mut self, src: &frame::Video) -> Option<&frame::Video> {
let fitted = self.request?;
if self.unhonored {
return None;
}
let Some(extent) = frame_extent(src) else {
self.stand_down();
return None;
};
if fitted == extent {
self.retire();
return None;
}
let Some(source) = read_source(src) else {
self.retire();
self.stand_down();
return None;
};
self.retire_unless_source_is(source.cv_format, source.extent, source.sw_format);
if !is_acceptable_request(fitted, source.extent) {
self.stand_down();
tracing::debug!(
requested_width = fitted.0,
requested_height = fitted.1,
source_width = source.extent.0,
source_height = source.extent.1,
"mediadecode-ffmpeg: standing scaled-output request is not a downscale of this \
frame; delivering it full size"
);
return None;
}
let Some(sar) = scaled_sample_aspect_ratio(source.sar, source.extent, fitted) else {
self.stand_down();
tracing::debug!(
sar_num = source.sar.0,
sar_den = source.sar.1,
"mediadecode-ffmpeg: the scale-corrected sample aspect ratio is not representable; \
delivering this frame full size"
);
return None;
};
let key = StageKey {
source_cv_format: source.cv_format,
source: source.extent,
fitted,
sw_format: source.sw_format,
};
if let Cache::Built(built) = &self.cache
&& !built.session.owner.is_current()
{
tracing::debug!(
"mediadecode-ffmpeg: the decoder moved threads; rebuilding the scaled-output session on this one"
);
self.retire();
}
match cache_plan(self.cache.built_key(), key) {
CachePlan::Reuse => {}
CachePlan::Build => match Built::create(key, source.device_ref) {
Some(built) => self.cache = Cache::Built(built),
None => {
self.stand_down();
return None;
}
},
}
let Cache::Built(built) = &mut self.cache else {
self.stand_down();
return None;
};
if built.transfer(src, &source, sar).is_err() {
self.stand_down();
return None;
}
if !matches!(self.cache, Cache::Built(_)) {
self.stand_down();
return None;
}
match &self.cache {
Cache::Built(built) => Some(&built.fitted),
Cache::Empty => None,
}
}
}
impl Built {
fn create(key: StageKey, device_ref: *mut AVBufferRef) -> Option<Self> {
if device_ref.is_null() {
return None;
}
let fitted = frame::Video::empty();
if unsafe { fitted.as_ptr() }.is_null() {
tracing::warn!("mediadecode-ffmpeg: scaled output could not allocate its destination frame");
return None;
}
let pool = FittedPool(unsafe { av_hwframe_ctx_alloc(device_ref) });
if pool.0.is_null() {
tracing::warn!("mediadecode-ffmpeg: scaled output could not allocate a frames context");
return None;
}
unsafe {
let ctx = (*pool.0).data.cast::<AVHWFramesContext>();
(*ctx).format = AVPixelFormat::AV_PIX_FMT_VIDEOTOOLBOX;
write_unaligned(
addr_of_mut!((*ctx).sw_format).cast::<c_int>(),
key.sw_format,
);
(*ctx).width = key.fitted.0 as c_int;
(*ctx).height = key.fitted.1 as c_int;
}
let rc = unsafe { av_hwframe_ctx_init(pool.0) };
if rc < 0 {
tracing::warn!(
rc,
width = key.fitted.0,
height = key.fitted.1,
"mediadecode-ffmpeg: scaled output could not initialise its fitted frames context; \
frames stay full size"
);
return None;
}
let mut raw: ffi::VTPixelTransferSessionRef = ptr::null_mut();
let status = unsafe { ffi::VTPixelTransferSessionCreate(ptr::null(), &mut raw) };
let session = match NonNull::new(raw) {
Some(handle) if status == 0 => Session {
handle,
owner: ThreadOwner::current(),
},
other => {
if let Some(handle) = other {
drop(Session {
handle,
owner: ThreadOwner::current(),
});
}
tracing::warn!(
status,
"mediadecode-ffmpeg: VTPixelTransferSessionCreate failed; frames stay full size"
);
return None;
}
};
Some(Self {
fitted,
key,
session,
pool,
})
}
fn transfer(&mut self, src: &frame::Video, source: &Source, sar: (i32, i32)) -> Result<(), ()> {
unsafe { av_frame_unref(self.fitted.as_mut_ptr()) };
let rc = unsafe { av_hwframe_get_buffer(self.pool.0, self.fitted.as_mut_ptr(), 0) };
if rc < 0 {
tracing::warn!(
rc,
"mediadecode-ffmpeg: scaled output could not draw a fitted surface from its pool; \
frames stay full size"
);
return Err(());
}
let destination = unsafe { (*self.fitted.as_ptr()).data[3] }.cast::<c_void>();
if !destination.is_null() {
let produced = unsafe { ffi::CVPixelBufferGetPixelFormatType(destination) };
if produced != source.cv_format {
unsafe { av_frame_unref(self.fitted.as_mut_ptr()) };
tracing::debug!(
source_format = source.cv_format,
destination_format = produced,
"mediadecode-ffmpeg: the fitted pool's pixel format is not the source's, so a \
transfer would convert rather than resize; frames stay full size"
);
return Err(());
}
}
if destination.is_null() {
unsafe { av_frame_unref(self.fitted.as_mut_ptr()) };
tracing::warn!(
"mediadecode-ffmpeg: the fitted frames context produced no pixel buffer; \
frames stay full size"
);
return Err(());
}
let status = unsafe {
ffi::VTPixelTransferSessionTransferImage(
self.session.handle.as_ptr(),
source.pixbuf,
destination,
)
};
if status != 0 {
unsafe { av_frame_unref(self.fitted.as_mut_ptr()) };
tracing::warn!(
status,
"mediadecode-ffmpeg: VTPixelTransferSessionTransferImage failed; \
frames stay full size"
);
return Err(());
}
if unsafe { crate::decoder::copy_frame_props_minimal(self.fitted.as_mut_ptr(), src.as_ptr()) }
.is_err()
{
unsafe { av_frame_unref(self.fitted.as_mut_ptr()) };
tracing::warn!(
"mediadecode-ffmpeg: scaled output could not carry this frame's metadata across; \
frames stay full size"
);
return Err(());
}
unsafe {
let raw = self.fitted.as_mut_ptr();
(*raw).sample_aspect_ratio.num = sar.0;
(*raw).sample_aspect_ratio.den = sar.1;
}
Ok(())
}
}
fn frame_extent(src: &frame::Video) -> Option<(u32, u32)> {
unsafe {
let raw = src.as_ptr();
if raw.is_null() {
return None;
}
let (width, height) = ((*raw).width, (*raw).height);
if width <= 0 || height <= 0 {
return None;
}
Some((width as u32, height as u32))
}
}
fn read_source(src: &frame::Video) -> Option<Source> {
unsafe {
let raw = src.as_ptr();
if raw.is_null() {
return None;
}
let format = read_unaligned(addr_of!((*raw).format));
if format != AVPixelFormat::AV_PIX_FMT_VIDEOTOOLBOX as c_int {
return None;
}
if (*raw).crop_left != 0
|| (*raw).crop_top != 0
|| (*raw).crop_right != 0
|| (*raw).crop_bottom != 0
{
tracing::debug!("mediadecode-ffmpeg: scaled output stands down on a cropped frame");
return None;
}
if side_data_forbids_scaling(raw) {
return None;
}
let frames_ref = (*raw).hw_frames_ctx;
if frames_ref.is_null() {
return None;
}
let frames_ctx = (*frames_ref).data.cast::<AVHWFramesContext>();
if frames_ctx.is_null() {
return None;
}
let device_ref = (*frames_ctx).device_ref;
let sw_format = read_unaligned(addr_of!((*frames_ctx).sw_format).cast::<c_int>());
let pixbuf = (*raw).data[3].cast::<c_void>();
if pixbuf.is_null() {
return None;
}
let (width, height) = ((*raw).width, (*raw).height);
if width <= 0 || height <= 0 {
return None;
}
let extent = (width as u32, height as u32);
let buffer_extent = (
ffi::CVPixelBufferGetWidth(pixbuf),
ffi::CVPixelBufferGetHeight(pixbuf),
);
if buffer_extent != (extent.0 as usize, extent.1 as usize) {
tracing::debug!(
frame_width = extent.0,
frame_height = extent.1,
buffer_width = buffer_extent.0,
buffer_height = buffer_extent.1,
"mediadecode-ffmpeg: scaled output stands down — the pixel buffer's extent is not \
the picture's, so a full-buffer transfer would scale padding into it"
);
return None;
}
Some(Source {
pixbuf,
device_ref,
cv_format: ffi::CVPixelBufferGetPixelFormatType(pixbuf),
sw_format,
extent,
sar: (
(*raw).sample_aspect_ratio.num,
(*raw).sample_aspect_ratio.den,
),
})
}
}
unsafe fn side_data_forbids_scaling(frame: *const AVFrame) -> bool {
use ffmpeg_next::ffi::AVFrameSideDataType;
unsafe {
let count = (*frame).nb_side_data;
if count == 0 {
return false;
}
let entries = (*frame).side_data;
if count < 0
|| entries.is_null()
|| count as usize > crate::decoder::HW_COPY_SIDE_DATA_MAX_ENTRIES
{
tracing::debug!(
count,
cap = crate::decoder::HW_COPY_SIDE_DATA_MAX_ENTRIES,
"mediadecode-ffmpeg: scaled output stands down — this frame's side-data bookkeeping \
cannot be walked, so whether a resize would strand any of it is unknowable"
);
return true;
}
for index in 0..count as usize {
let entry = *entries.add(index);
if entry.is_null() {
tracing::debug!(
index,
"mediadecode-ffmpeg: scaled output stands down — a null side-data entry inside a \
non-empty array is malformed bookkeeping"
);
return true;
}
let kind_raw = read_unaligned(addr_of!((*entry).type_).cast::<c_int>());
let Some(kind) = crate::decoder::whitelisted_side_data_kind(kind_raw) else {
continue;
};
if matches!(
kind,
AVFrameSideDataType::AV_FRAME_DATA_PANSCAN
| AVFrameSideDataType::AV_FRAME_DATA_SPHERICAL
| AVFrameSideDataType::AV_FRAME_DATA_REGIONS_OF_INTEREST
) {
tracing::debug!(
kind_raw,
"mediadecode-ffmpeg: scaled output stands down — this frame carries side data whose \
meaning is tied to the picture's dimensions, and a resize would strand it"
);
return true;
}
}
false
}
}
#[cfg(test)]
#[path = "videotoolbox/tests.rs"]
mod tests;