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 std::path::Path;

use serde::Deserialize;

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

#[derive(Debug, Clone, PartialEq, Deserialize)]
#[serde(rename_all = "camelCase")]
struct FragmentedAssetMinderPayload {
    minding_interval: f64,
    asset_count: usize,
}

#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "camelCase")]
struct MediaExtensionPropertiesPayload {
    containing_bundle_name: Option<String>,
    containing_bundle_url: Option<String>,
    extension_identifier: Option<String>,
    extension_name: Option<String>,
    extension_url: Option<String>,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MediaExtensionPropertiesInfo {
    pub containing_bundle_name: Option<String>,
    pub containing_bundle_url: Option<String>,
    pub extension_identifier: Option<String>,
    pub extension_name: Option<String>,
    pub extension_url: Option<String>,
}

impl From<MediaExtensionPropertiesPayload> for MediaExtensionPropertiesInfo {
    fn from(value: MediaExtensionPropertiesPayload) -> Self {
        Self {
            containing_bundle_name: value.containing_bundle_name,
            containing_bundle_url: value.containing_bundle_url,
            extension_identifier: value.extension_identifier,
            extension_name: value.extension_name,
            extension_url: value.extension_url,
        }
    }
}

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

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

impl MediaExtensionProperties {
    const fn from_ptr(ptr: *mut c_void) -> Self {
        Self { ptr }
    }

    pub fn info(&self) -> Result<MediaExtensionPropertiesInfo, AVPlayerError> {
        let mut err: *mut c_char = ptr::null_mut();
        let json_ptr = unsafe { ffi::av_media_extension_properties_info_json(self.ptr, &mut err) };
        if json_ptr.is_null() {
            return Err(unsafe { from_swift(ffi::status::OPERATION_FAILED, err) });
        }
        Ok(MediaExtensionPropertiesInfo::from(parse_json_and_free::<
            MediaExtensionPropertiesPayload,
        >(json_ptr)?))
    }

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

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

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

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

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

#[derive(Debug)]
pub struct FragmentedAsset {
    asset: Asset,
}

impl FragmentedAsset {
    pub fn from_file_path(path: impl AsRef<Path>) -> Result<Self, AVPlayerError> {
        let path = path
            .as_ref()
            .to_str()
            .ok_or_else(|| AVPlayerError::InvalidArgument("path is not valid UTF-8".into()))?;
        Self::from_raw_url(path, true)
    }

    pub fn from_remote_url(url: impl AsRef<str>) -> Result<Self, AVPlayerError> {
        Self::from_raw_url(url.as_ref(), false)
    }

    fn from_raw_url(url: &str, is_file_url: bool) -> Result<Self, AVPlayerError> {
        let url = CString::new(url).map_err(|error| {
            AVPlayerError::InvalidArgument(format!("URL contains NUL byte: {error}"))
        })?;
        let mut err: *mut c_char = ptr::null_mut();
        let ptr = unsafe { ffi::av_fragmented_asset_create(url.as_ptr(), is_file_url, &mut err) };
        if ptr.is_null() {
            return Err(unsafe { from_swift(ffi::status::ASSET_CREATE_FAILED, err) });
        }
        Ok(Self {
            asset: Asset { ptr },
        })
    }

    pub const fn as_asset(&self) -> &Asset {
        &self.asset
    }

    pub fn into_asset(self) -> Asset {
        self.asset
    }

    pub fn url(&self) -> Result<Option<String>, AVPlayerError> {
        self.asset.url()
    }

    pub fn tracks(&self) -> Result<Vec<FragmentedAssetTrack>, AVPlayerError> {
        let count = unsafe { ffi::av_fragmented_asset_track_count(self.asset.ptr) };
        if count < 0 {
            return Err(AVPlayerError::OperationFailed(format!(
                "fragmented track count unexpectedly negative: {count}"
            )));
        }
        let capacity = usize::try_from(count).map_err(|error| {
            AVPlayerError::OperationFailed(format!("invalid fragmented track count: {error}"))
        })?;
        let mut tracks = Vec::with_capacity(capacity);
        for index in 0..count {
            let ptr =
                unsafe { ffi::av_fragmented_asset_copy_track_at_index(self.asset.ptr, index) };
            if ptr.is_null() {
                return Err(AVPlayerError::OperationFailed(format!(
                    "bridge returned null fragmented track at index {index}"
                )));
            }
            tracks.push(FragmentedAssetTrack::from_ptr(ptr));
        }
        Ok(tracks)
    }

    pub fn track_with_id(&self, track_id: i32) -> Option<FragmentedAssetTrack> {
        let ptr = unsafe { ffi::av_fragmented_asset_copy_track_with_id(self.asset.ptr, track_id) };
        (!ptr.is_null()).then(|| FragmentedAssetTrack::from_ptr(ptr))
    }

    pub fn is_associated_with_minder(&self) -> bool {
        unsafe { ffi::av_fragmented_asset_is_associated_with_minder(self.asset.ptr) }
    }

    pub fn media_extension_properties(&self) -> Option<MediaExtensionProperties> {
        let ptr = unsafe { ffi::av_url_asset_copy_media_extension_properties(self.asset.ptr) };
        (!ptr.is_null()).then(|| MediaExtensionProperties::from_ptr(ptr))
    }

    pub fn expects_property_revised_notifications() -> bool {
        unsafe { ffi::av_fragmented_asset_expects_property_revised_notifications() }
    }

    pub fn is_playable_extended_mime_type(
        mime_type: impl AsRef<str>,
    ) -> Result<bool, AVPlayerError> {
        let mime_type = CString::new(mime_type.as_ref()).map_err(|error| {
            AVPlayerError::InvalidArgument(format!("MIME type contains NUL byte: {error}"))
        })?;
        Ok(unsafe { ffi::av_fragmented_asset_is_playable_extended_mime_type(mime_type.as_ptr()) })
    }
}

impl UrlAsset {
    pub fn media_extension_properties(&self) -> Option<MediaExtensionProperties> {
        let ptr = unsafe { ffi::av_url_asset_copy_media_extension_properties(self.asset.ptr) };
        (!ptr.is_null()).then(|| MediaExtensionProperties::from_ptr(ptr))
    }
}

#[derive(Debug)]
pub struct FragmentedAssetTrack {
    track: AssetTrack,
}

impl FragmentedAssetTrack {
    const fn from_ptr(ptr: *mut c_void) -> Self {
        Self {
            track: AssetTrack { ptr },
        }
    }

    pub const fn as_asset_track(&self) -> &AssetTrack {
        &self.track
    }

    pub fn segment_count(&self) -> Result<usize, AVPlayerError> {
        let count = unsafe { ffi::av_fragmented_asset_track_segment_count(self.track.ptr) };
        usize::try_from(count).map_err(|error| {
            AVPlayerError::OperationFailed(format!("invalid fragmented segment count: {error}"))
        })
    }

    pub fn track_id(&self) -> Result<i32, AVPlayerError> {
        self.track.track_id()
    }

    pub fn media_type(&self) -> Result<crate::asset::MediaType, AVPlayerError> {
        self.track.media_type()
    }

    pub fn natural_size(&self) -> Result<crate::asset::Size, AVPlayerError> {
        self.track.natural_size()
    }

    pub fn nominal_frame_rate(&self) -> Result<Option<f32>, AVPlayerError> {
        self.track.nominal_frame_rate()
    }

    pub fn estimated_data_rate(&self) -> Result<Option<f32>, AVPlayerError> {
        self.track.estimated_data_rate()
    }
}

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

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

impl FragmentedAssetMinder {
    pub fn new(asset: &FragmentedAsset, minding_interval: f64) -> Result<Self, AVPlayerError> {
        let mut err: *mut c_char = ptr::null_mut();
        let ptr = unsafe {
            ffi::av_fragmented_asset_minder_create(asset.asset.ptr, minding_interval, &mut err)
        };
        if ptr.is_null() {
            return Err(unsafe { from_swift(ffi::status::OPERATION_FAILED, err) });
        }
        Ok(Self { ptr })
    }

    fn info(&self) -> Result<FragmentedAssetMinderPayload, AVPlayerError> {
        let mut err: *mut c_char = ptr::null_mut();
        let json_ptr = unsafe { ffi::av_fragmented_asset_minder_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 minding_interval(&self) -> Result<f64, AVPlayerError> {
        Ok(self.info()?.minding_interval)
    }

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

    pub fn set_minding_interval(&self, minding_interval: f64) {
        unsafe { ffi::av_fragmented_asset_minder_set_interval(self.ptr, minding_interval) };
    }

    pub fn add_asset(&self, asset: &FragmentedAsset) {
        unsafe { ffi::av_fragmented_asset_minder_add_asset(self.ptr, asset.asset.ptr) };
    }

    pub fn remove_asset(&self, asset: &FragmentedAsset) {
        unsafe { ffi::av_fragmented_asset_minder_remove_asset(self.ptr, asset.asset.ptr) };
    }

    pub fn assets(&self) -> Result<Vec<FragmentedAsset>, AVPlayerError> {
        let count = self.asset_count()?;
        let mut assets = Vec::with_capacity(count);
        for index in 0..count {
            let index_i32 = i32::try_from(index).map_err(|error| {
                AVPlayerError::OperationFailed(format!("fragmented asset index overflow: {error}"))
            })?;
            let ptr =
                unsafe { ffi::av_fragmented_asset_minder_copy_asset_at_index(self.ptr, index_i32) };
            if ptr.is_null() {
                return Err(AVPlayerError::OperationFailed(format!(
                    "bridge returned null fragmented asset at index {index}"
                )));
            }
            assets.push(FragmentedAsset {
                asset: Asset { ptr },
            });
        }
        Ok(assets)
    }
}

unsafe impl Send for MediaExtensionProperties {}
unsafe impl Send for FragmentedAsset {}
unsafe impl Send for FragmentedAssetTrack {}
unsafe impl Send for FragmentedAssetMinder {}