avplayer 0.7.0

Safe Rust bindings for Apple's AVPlayer + AVAssetReader — playback and frame-by-frame asset reading on macOS
Documentation
#![allow(clippy::missing_errors_doc, clippy::must_use_candidate)]

use core::ffi::{c_char, c_void};
use core::ptr;
use std::ffi::CString;

use serde::Deserialize;

use crate::asset::{Asset, Size, UrlAsset};
use crate::error::{from_swift, AVPlayerError};
use crate::ffi;
use crate::time::Time;
use crate::util::parse_json_and_free;

#[derive(Debug, Clone, PartialEq, Deserialize)]
#[serde(rename_all = "camelCase")]
struct AssetImageGeneratorInfoPayload {
    applies_preferred_track_transform: bool,
    maximum_size: Size,
    aperture_mode: Option<String>,
    requested_time_tolerance_before: Time,
    requested_time_tolerance_after: Time,
    dynamic_range_policy: Option<String>,
    has_custom_video_compositor: bool,
    custom_video_compositor_class_name: Option<String>,
}

#[derive(Debug, Clone, PartialEq, Deserialize)]
#[serde(rename_all = "camelCase")]
struct AssetImagePayload {
    width: usize,
    height: usize,
    bits_per_component: usize,
    bits_per_pixel: usize,
    bytes_per_row: usize,
    alpha_info: u32,
    bitmap_info: u32,
    rendering_intent: u32,
}

#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum AssetImageGeneratorApertureMode {
    CleanAperture,
    ProductionAperture,
    EncodedPixels,
    Unknown(String),
}

impl AssetImageGeneratorApertureMode {
    fn from_raw(raw: &str) -> Self {
        match raw {
            "clean_aperture" => Self::CleanAperture,
            "production_aperture" => Self::ProductionAperture,
            "encoded_pixels" => Self::EncodedPixels,
            other => Self::Unknown(other.to_owned()),
        }
    }

    fn as_raw(&self) -> &str {
        match self {
            Self::CleanAperture => "clean_aperture",
            Self::ProductionAperture => "production_aperture",
            Self::EncodedPixels => "encoded_pixels",
            Self::Unknown(raw) => raw,
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum AssetImageGeneratorDynamicRangePolicy {
    ForceSdr,
    MatchSource,
    Unknown(String),
}

impl AssetImageGeneratorDynamicRangePolicy {
    fn from_raw(raw: &str) -> Self {
        match raw {
            "force_sdr" => Self::ForceSdr,
            "match_source" => Self::MatchSource,
            other => Self::Unknown(other.to_owned()),
        }
    }

    fn as_raw(&self) -> &str {
        match self {
            Self::ForceSdr => "force_sdr",
            Self::MatchSource => "match_source",
            Self::Unknown(raw) => raw,
        }
    }
}

#[derive(Debug)]
pub struct AssetImageGenerator {
    ptr: *mut c_void,
}

impl Drop for AssetImageGenerator {
    fn drop(&mut self) {
        if !self.ptr.is_null() {
            unsafe { ffi::av_ns_object_release(self.ptr) };
            self.ptr = ptr::null_mut();
        }
    }
}

#[derive(Debug)]
pub struct AssetImage {
    ptr: *mut c_void,
}

impl Drop for AssetImage {
    fn drop(&mut self) {
        if !self.ptr.is_null() {
            unsafe { ffi::av_ns_object_release(self.ptr) };
            self.ptr = ptr::null_mut();
        }
    }
}

#[derive(Debug)]
pub struct GeneratedAssetImage {
    pub image: AssetImage,
    pub actual_time: Time,
}

impl Asset {
    pub fn image_generator(&self) -> AssetImageGenerator {
        let ptr = unsafe { ffi::av_asset_image_generator_create(self.ptr) };
        AssetImageGenerator { ptr }
    }
}

impl UrlAsset {
    pub fn image_generator(&self) -> AssetImageGenerator {
        self.asset.image_generator()
    }
}

impl AssetImageGenerator {
    fn info(&self) -> Result<AssetImageGeneratorInfoPayload, AVPlayerError> {
        let mut err: *mut c_char = ptr::null_mut();
        let json_ptr = unsafe { ffi::av_asset_image_generator_info_json(self.ptr, &mut err) };
        if json_ptr.is_null() {
            return Err(unsafe { from_swift(ffi::status::OPERATION_FAILED, err) });
        }
        parse_json_and_free(json_ptr)
    }

    pub fn applies_preferred_track_transform(&self) -> Result<bool, AVPlayerError> {
        Ok(self.info()?.applies_preferred_track_transform)
    }

    pub fn set_applies_preferred_track_transform(&self, applies: bool) {
        unsafe {
            ffi::av_asset_image_generator_set_applies_preferred_track_transform(self.ptr, applies);
        }
    }

    pub fn maximum_size(&self) -> Result<Size, AVPlayerError> {
        Ok(self.info()?.maximum_size)
    }

    pub fn set_maximum_size(&self, size: Size) {
        unsafe {
            ffi::av_asset_image_generator_set_maximum_size(self.ptr, size.width, size.height);
        }
    }

    pub fn aperture_mode(&self) -> Result<Option<AssetImageGeneratorApertureMode>, AVPlayerError> {
        Ok(self
            .info()?
            .aperture_mode
            .as_deref()
            .map(AssetImageGeneratorApertureMode::from_raw))
    }

    pub fn set_aperture_mode(
        &self,
        mode: Option<AssetImageGeneratorApertureMode>,
    ) -> Result<(), AVPlayerError> {
        let mode = mode
            .map(|mode| {
                CString::new(mode.as_raw()).map_err(|error| {
                    AVPlayerError::InvalidArgument(format!(
                        "aperture mode contains NUL byte: {error}"
                    ))
                })
            })
            .transpose()?;
        unsafe {
            ffi::av_asset_image_generator_set_aperture_mode(
                self.ptr,
                mode.as_ref().map_or(ptr::null(), |mode| mode.as_ptr()),
            );
        }
        Ok(())
    }

    pub fn requested_time_tolerance_before(&self) -> Result<Time, AVPlayerError> {
        Ok(self.info()?.requested_time_tolerance_before)
    }

    pub fn set_requested_time_tolerance_before(&self, tolerance: Time) {
        let (value, timescale, kind) = tolerance.to_raw();
        unsafe {
            ffi::av_asset_image_generator_set_requested_time_tolerance_before(
                self.ptr, value, timescale, kind,
            );
        }
    }

    pub fn requested_time_tolerance_after(&self) -> Result<Time, AVPlayerError> {
        Ok(self.info()?.requested_time_tolerance_after)
    }

    pub fn set_requested_time_tolerance_after(&self, tolerance: Time) {
        let (value, timescale, kind) = tolerance.to_raw();
        unsafe {
            ffi::av_asset_image_generator_set_requested_time_tolerance_after(
                self.ptr, value, timescale, kind,
            );
        }
    }

    pub fn dynamic_range_policy(
        &self,
    ) -> Result<Option<AssetImageGeneratorDynamicRangePolicy>, AVPlayerError> {
        Ok(self
            .info()?
            .dynamic_range_policy
            .as_deref()
            .map(AssetImageGeneratorDynamicRangePolicy::from_raw))
    }

    pub fn set_dynamic_range_policy(
        &self,
        policy: &AssetImageGeneratorDynamicRangePolicy,
    ) -> Result<(), AVPlayerError> {
        let policy = CString::new(policy.as_raw()).map_err(|error| {
            AVPlayerError::InvalidArgument(format!(
                "dynamic-range policy contains NUL byte: {error}"
            ))
        })?;
        let mut err: *mut c_char = ptr::null_mut();
        let status = unsafe {
            ffi::av_asset_image_generator_set_dynamic_range_policy(
                self.ptr,
                policy.as_ptr(),
                &mut err,
            )
        };
        if status != ffi::status::OK {
            return Err(unsafe { from_swift(status, err) });
        }
        Ok(())
    }

    pub fn has_custom_video_compositor(&self) -> Result<bool, AVPlayerError> {
        Ok(self.info()?.has_custom_video_compositor)
    }

    pub fn custom_video_compositor_class_name(&self) -> Result<Option<String>, AVPlayerError> {
        Ok(self.info()?.custom_video_compositor_class_name)
    }

    pub fn copy_image_at_time(
        &self,
        requested_time: Time,
    ) -> Result<GeneratedAssetImage, AVPlayerError> {
        let (value, timescale, kind) = requested_time.to_raw();
        let mut actual_value = 0_i64;
        let mut actual_timescale = 0_i32;
        let mut actual_kind = 1_i32;
        let mut err: *mut c_char = ptr::null_mut();
        let image_ptr = unsafe {
            ffi::av_asset_image_generator_copy_image_at_time(
                self.ptr,
                value,
                timescale,
                kind,
                &mut actual_value,
                &mut actual_timescale,
                &mut actual_kind,
                &mut err,
            )
        };
        if image_ptr.is_null() {
            return Err(unsafe { from_swift(ffi::status::OPERATION_FAILED, err) });
        }
        Ok(GeneratedAssetImage {
            image: AssetImage { ptr: image_ptr },
            actual_time: Time::from_raw(actual_value, actual_timescale, actual_kind),
        })
    }

    pub fn cancel_all_image_generation(&self) {
        unsafe { ffi::av_asset_image_generator_cancel_all_image_generation(self.ptr) };
    }
}

impl AssetImage {
    fn info(&self) -> Result<AssetImagePayload, AVPlayerError> {
        let mut err: *mut c_char = ptr::null_mut();
        let json_ptr = unsafe { ffi::av_asset_image_info_json(self.ptr, &mut err) };
        if json_ptr.is_null() {
            return Err(unsafe { from_swift(ffi::status::OPERATION_FAILED, err) });
        }
        parse_json_and_free(json_ptr)
    }

    pub fn width(&self) -> Result<usize, AVPlayerError> {
        Ok(self.info()?.width)
    }

    pub fn height(&self) -> Result<usize, AVPlayerError> {
        Ok(self.info()?.height)
    }

    pub fn bits_per_component(&self) -> Result<usize, AVPlayerError> {
        Ok(self.info()?.bits_per_component)
    }

    pub fn bits_per_pixel(&self) -> Result<usize, AVPlayerError> {
        Ok(self.info()?.bits_per_pixel)
    }

    pub fn bytes_per_row(&self) -> Result<usize, AVPlayerError> {
        Ok(self.info()?.bytes_per_row)
    }

    pub fn alpha_info(&self) -> Result<u32, AVPlayerError> {
        Ok(self.info()?.alpha_info)
    }

    pub fn bitmap_info(&self) -> Result<u32, AVPlayerError> {
        Ok(self.info()?.bitmap_info)
    }

    pub fn rendering_intent(&self) -> Result<u32, AVPlayerError> {
        Ok(self.info()?.rendering_intent)
    }
}