videotoolbox 0.21.0

Safe Rust bindings for Apple's VideoToolbox framework — hardware H.264/HEVC/ProRes encode and decode on macOS
Documentation
#![allow(non_snake_case)]

use core::ffi::{c_char, c_void};
use core::ptr::NonNull;
use core::sync::atomic::{AtomicPtr, Ordering};

use crate::error::VTError;

extern "C" {
    fn dlsym(handle: *mut c_void, symbol: *const c_char) -> *mut c_void;
}

const RTLD_DEFAULT: *mut c_void = -2_isize as *mut c_void;
const UNRESOLVED: *mut c_void = NonNull::<c_void>::dangling().as_ptr();

pub struct Symbol {
    name: &'static str,
    minimum: &'static str,
    address: AtomicPtr<c_void>,
}

impl Symbol {
    pub const fn new(name: &'static str, minimum: &'static str) -> Self {
        Self {
            name,
            minimum,
            address: AtomicPtr::new(UNRESOLVED),
        }
    }

    pub fn resolve(&self) -> Result<NonNull<c_void>, VTError> {
        let mut address = self.address.load(Ordering::Acquire);
        if address == UNRESOLVED {
            address = unsafe { dlsym(RTLD_DEFAULT, self.name.as_ptr().cast()) };
            self.address.store(address, Ordering::Release);
        }
        NonNull::new(address).ok_or_else(|| VTError::Unsupported {
            api: self.name.trim_end_matches('\0'),
            minimum: self.minimum,
        })
    }
}

macro_rules! dynamic_functions {
    ($($(#[$attr:meta])* $minimum:literal $name:ident: fn($($arg:ty),* $(,)?) $(-> $ret:ty)?;)+) => {
        $(
            $(#[$attr])*
            pub fn $name() -> Result<unsafe extern "C" fn($($arg),*) $(-> $ret)?, VTError> {
                static SYMBOL: Symbol = Symbol::new(concat!(stringify!($name), "\0"), $minimum);
                SYMBOL.resolve().map(|address| unsafe {
                    core::mem::transmute::<*mut c_void, unsafe extern "C" fn($($arg),*) $(-> $ret)?>(
                        address.as_ptr(),
                    )
                })
            }
        )+
    };
}

macro_rules! dynamic_constants {
    ($($(#[$attr:meta])* $minimum:literal $name:ident: $ty:ty;)+) => {
        $(
            $(#[$attr])*
            pub fn $name() -> Result<$ty, VTError> {
                static SYMBOL: Symbol = Symbol::new(concat!(stringify!($name), "\0"), $minimum);
                SYMBOL
                    .resolve()
                    .map(|address| unsafe { address.cast::<$ty>().as_ptr().read() })
            }
        )+
    };
}

dynamic_functions! {
    "14.0" CMTaggedBufferGroupGetTypeID: fn() -> usize;
    "14.0" CMTaggedBufferGroupGetCount: fn(super::CMTaggedBufferGroupRef) -> super::CMItemCount;
    "14.0" CMTaggedBufferGroupGetCVPixelBufferAtIndex:
        fn(super::CMTaggedBufferGroupRef, isize) -> super::CVPixelBufferRef;
    "14.0" CMTaggedBufferGroupGetCMSampleBufferAtIndex:
        fn(super::CMTaggedBufferGroupRef, isize) -> super::CMSampleBufferRef;
    #[cfg(feature = "compression")]
    "14.0" VTCompressionSessionEncodeMultiImageFrame: fn(
        super::VTCompressionSessionRef,
        super::CMTaggedBufferGroupRef,
        super::CMTime,
        super::CMTime,
        super::CFDictionaryRef,
        *mut c_void,
        *mut super::VTEncodeInfoFlags,
    ) -> super::OSStatus;
    #[cfg(feature = "compression")]
    "14.0" VTIsStereoMVHEVCEncodeSupported: fn() -> super::Boolean;
    "14.0" VTIsStereoMVHEVCDecodeSupported: fn() -> super::Boolean;
    "14.0" VTDecompressionSessionSetMultiImageCallback: fn(
        super::VTDecompressionSessionRef,
        super::VTDecompressionOutputMultiImageCallback,
        *mut c_void,
    ) -> super::OSStatus;
    "15.0" VTDecompressionSessionDecodeFrameWithOptions: fn(
        super::VTDecompressionSessionRef,
        super::CMSampleBufferRef,
        super::VTDecodeFrameFlags,
        super::CFDictionaryRef,
        *mut c_void,
        *mut super::VTDecodeInfoFlags,
    ) -> super::OSStatus;
    "15.0" VTCopyVideoDecoderExtensionProperties:
        fn(super::CMFormatDescriptionRef, *mut super::CFDictionaryRef) -> super::OSStatus;
    "15.0" VTCopyRAWProcessorExtensionProperties:
        fn(super::CMFormatDescriptionRef, *mut super::CFDictionaryRef) -> super::OSStatus;
    "15.0" VTHDRPerFrameMetadataGenerationSessionGetTypeID: fn() -> usize;
    "15.0" VTHDRPerFrameMetadataGenerationSessionCreate: fn(
        super::CFAllocatorRef,
        f32,
        super::CFDictionaryRef,
        *mut super::VTHDRPerFrameMetadataGenerationSessionRef,
    ) -> super::OSStatus;
    "15.0" VTHDRPerFrameMetadataGenerationSessionAttachMetadata: fn(
        super::VTHDRPerFrameMetadataGenerationSessionRef,
        super::CVPixelBufferRef,
        super::Boolean,
    ) -> super::OSStatus;
    #[cfg(feature = "frame_processor")]
    "15.0" VTRAWProcessingSessionGetTypeID: fn() -> usize;
    #[cfg(feature = "frame_processor")]
    "15.0" VTRAWProcessingSessionCreate: fn(
        super::CFAllocatorRef,
        *const c_void,
        super::CFDictionaryRef,
        super::CFDictionaryRef,
        *mut super::VTRAWProcessingSessionRef,
    ) -> super::OSStatus;
    #[cfg(feature = "frame_processor")]
    "15.0" VTRAWProcessingSessionInvalidate: fn(super::VTRAWProcessingSessionRef);
    #[cfg(feature = "frame_processor")]
    "15.0" VTRAWProcessingSessionCompleteFrames:
        fn(super::VTRAWProcessingSessionRef) -> super::OSStatus;
    #[cfg(feature = "frame_processor")]
    "15.0" VTRAWProcessingSessionCopyProcessingParameters:
        fn(super::VTRAWProcessingSessionRef, *mut super::CFArrayRef) -> super::OSStatus;
    #[cfg(feature = "frame_processor")]
    "15.0" VTRAWProcessingSessionSetProcessingParameters:
        fn(super::VTRAWProcessingSessionRef, super::CFDictionaryRef) -> super::OSStatus;
    #[cfg(feature = "frame_processor")]
    "26.0" VTMotionEstimationSessionGetTypeID: fn() -> usize;
    #[cfg(feature = "frame_processor")]
    "26.0" VTMotionEstimationSessionCreate: fn(
        super::CFAllocatorRef,
        super::CFDictionaryRef,
        u32,
        u32,
        *mut super::VTMotionEstimationSessionRef,
    ) -> super::OSStatus;
    #[cfg(feature = "frame_processor")]
    "26.0" VTMotionEstimationSessionInvalidate: fn(super::VTMotionEstimationSessionRef);
    #[cfg(feature = "frame_processor")]
    "26.0" VTMotionEstimationSessionCopySourcePixelBufferAttributes:
        fn(super::VTMotionEstimationSessionRef, *mut super::CFDictionaryRef) -> super::OSStatus;
    #[cfg(feature = "frame_processor")]
    "26.0" VTMotionEstimationSessionCompleteFrames:
        fn(super::VTMotionEstimationSessionRef) -> super::OSStatus;
}

dynamic_constants! {
    "15.0" kVTHDRPerFrameMetadataGenerationHDRFormatType_DolbyVision:
        super::VTHDRPerFrameMetadataGenerationHDRFormatType;
    "15.0" kVTHDRPerFrameMetadataGenerationOptionsKey_HDRFormats: super::CFStringRef;
    #[cfg(feature = "frame_processor")]
    "15.0" kVTRAWProcessingParameter_Key: super::CFStringRef;
    #[cfg(feature = "frame_processor")]
    "15.0" kVTRAWProcessingParameter_Name: super::CFStringRef;
    #[cfg(feature = "frame_processor")]
    "15.0" kVTRAWProcessingParameter_Description: super::CFStringRef;
    #[cfg(feature = "frame_processor")]
    "15.0" kVTRAWProcessingParameter_ValueType: super::CFStringRef;
    #[cfg(feature = "frame_processor")]
    "15.0" kVTRAWProcessingParameter_CurrentValue: super::CFStringRef;
    #[cfg(feature = "frame_processor")]
    "15.0" kVTRAWProcessingParameter_MinimumValue: super::CFStringRef;
    #[cfg(feature = "frame_processor")]
    "15.0" kVTRAWProcessingParameter_MaximumValue: super::CFStringRef;
    #[cfg(feature = "frame_processor")]
    "15.0" kVTRAWProcessingParameter_InitialValue: super::CFStringRef;
    #[cfg(feature = "frame_processor")]
    "15.0" kVTRAWProcessingParameter_CameraValue: super::CFStringRef;
    #[cfg(feature = "frame_processor")]
    "15.0" kVTRAWProcessingParameter_NeutralValue: super::CFStringRef;
    #[cfg(feature = "frame_processor")]
    "15.0" kVTRAWProcessingPropertyKey_MetalDeviceRegistryID: super::CFStringRef;
    #[cfg(feature = "frame_processor")]
    "15.0" kVTRAWProcessingPropertyKey_OutputColorAttachments: super::CFStringRef;
    #[cfg(feature = "frame_processor")]
    "26.0" kVTRAWProcessingPropertyKey_MetadataForSidecarFile: super::CFStringRef;
    #[cfg(feature = "frame_processor")]
    "26.0" kVTMotionEstimationSessionCreationOption_MotionVectorSize: super::CFStringRef;
    #[cfg(feature = "frame_processor")]
    "26.0" kVTMotionEstimationSessionCreationOption_UseMultiPassSearch: super::CFStringRef;
    #[cfg(feature = "frame_processor")]
    "26.0" kVTMotionEstimationSessionCreationOption_Label: super::CFStringRef;
}

#[cfg(test)]
mod tests {
    use super::Symbol;
    use crate::error::VTError;

    #[test]
    fn missing_symbol_reports_the_api_and_minimum() {
        static SYMBOL: Symbol = Symbol::new("VTNoSuchFunctionForTesting\0", "99.0");

        assert_eq!(
            SYMBOL.resolve(),
            Err(VTError::Unsupported {
                api: "VTNoSuchFunctionForTesting",
                minimum: "99.0",
            })
        );
        assert!(SYMBOL.resolve().is_err());
    }

    #[test]
    fn present_symbol_resolves_to_the_linked_address() {
        static SYMBOL: Symbol = Symbol::new("VTCompressionSessionGetTypeID\0", "10.8");

        let address = SYMBOL.resolve().expect("VideoToolbox is loaded");
        let linked = crate::ffi::VTCompressionSessionGetTypeID as unsafe extern "C" fn() -> usize;

        assert_eq!(
            address.as_ptr().cast_const(),
            linked as *const core::ffi::c_void
        );
        assert_eq!(SYMBOL.resolve(), Ok(address));
    }

    #[test]
    fn present_constant_matches_the_linked_value() {
        let Ok(dynamic) = super::kVTHDRPerFrameMetadataGenerationOptionsKey_HDRFormats() else {
            return;
        };
        let linked = unsafe { crate::ffi::kVTHDRPerFrameMetadataGenerationOptionsKey_HDRFormats };

        assert!(!dynamic.is_null());
        assert_eq!(dynamic, linked);
    }
}