avplayer 0.6.1

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 serde::Deserialize;

use crate::error::{from_swift, AVPlayerError};
use crate::ffi;
use crate::metadata::MetadataItem;
use crate::player::PlayerItem;
use crate::util::{json_cstring, parse_json_and_free};

/// Mirrors the `AVPlayer` framework counterpart for `PlayerItemMediaDataCollectorKind`.
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum PlayerItemMediaDataCollectorKind {
    /// Mirrors the `AVPlayer` framework case `MetadataCollector`.
    MetadataCollector,
    /// Mirrors the `AVPlayer` framework case `Unknown`.
    Unknown(String),
}

impl PlayerItemMediaDataCollectorKind {
    pub(crate) fn from_raw(raw: &str) -> Self {
        match raw {
            "metadata_collector" => Self::MetadataCollector,
            other => Self::Unknown(other.to_owned()),
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "camelCase")]
struct PlayerItemMediaDataCollectorInfoPayload {
    kind: String,
}

/// Mirrors the `AVPlayer` framework counterpart for `PlayerItemMediaDataCollectorInfo`.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PlayerItemMediaDataCollectorInfo {
    /// Mirrors the `AVPlayer` framework property for `kind`.
    pub kind: PlayerItemMediaDataCollectorKind,
}

impl From<PlayerItemMediaDataCollectorInfoPayload> for PlayerItemMediaDataCollectorInfo {
    fn from(payload: PlayerItemMediaDataCollectorInfoPayload) -> Self {
        Self {
            kind: PlayerItemMediaDataCollectorKind::from_raw(&payload.kind),
        }
    }
}

#[derive(Debug, Clone, PartialEq, Deserialize)]
#[serde(rename_all = "camelCase")]
struct MetadataCollectorInfoPayload {
    identifiers: Option<Vec<String>>,
    classifying_labels: Option<Vec<String>>,
    has_delegate: bool,
}

/// Mirrors the `AVPlayer` framework counterpart for `DateRangeMetadataGroup`.
#[derive(Debug, Clone, PartialEq, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DateRangeMetadataGroup {
    /// Mirrors the `AVPlayer` framework property for `start_date`.
    pub start_date: String,
    /// Mirrors the `AVPlayer` framework property for `end_date`.
    pub end_date: Option<String>,
    /// Mirrors the `AVPlayer` framework property for `classifying_label`.
    pub classifying_label: Option<String>,
    /// Mirrors the `AVPlayer` framework property for `unique_id`.
    pub unique_id: Option<String>,
    /// Mirrors the `AVPlayer` framework property for `items`.
    pub items: Vec<MetadataItem>,
}

#[derive(Debug, Clone, PartialEq, Deserialize)]
#[serde(rename_all = "camelCase")]
struct MetadataCollectorEventPayload {
    event: String,
    groups: Vec<DateRangeMetadataGroup>,
    new_indices: Vec<usize>,
    modified_indices: Vec<usize>,
}

/// Mirrors the `AVPlayer` framework counterpart for `MetadataCollectorEvent`.
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub enum MetadataCollectorEvent {
    /// Mirrors the `AVPlayer` framework case `DidCollectDateRangeMetadataGroups`.
    DidCollectDateRangeMetadataGroups {
        groups: Vec<DateRangeMetadataGroup>,
        new_indices: Vec<usize>,
        modified_indices: Vec<usize>,
    },
}

struct MetadataCollectorObserverState {
    callback: Box<dyn Fn(MetadataCollectorEvent) + Send + 'static>,
}

/// Mirrors the `AVPlayer` framework counterpart for `PlayerItemMetadataCollector`.
#[derive(Debug)]
pub struct PlayerItemMetadataCollector {
    pub(crate) ptr: *mut c_void,
}

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

impl PlayerItemMetadataCollector {
    /// Calls the `AVPlayer` framework counterpart for `new`.
    pub fn new(
        identifiers: Option<&[impl AsRef<str>]>,
        classifying_labels: Option<&[impl AsRef<str>]>,
    ) -> Result<Self, AVPlayerError> {
        let identifiers = identifiers.map(|values| {
            values
                .iter()
                .map(|value| value.as_ref().to_owned())
                .collect::<Vec<_>>()
        });
        let classifying_labels = classifying_labels.map(|values| {
            values
                .iter()
                .map(|value| value.as_ref().to_owned())
                .collect::<Vec<_>>()
        });
        let identifiers = identifiers
            .as_ref()
            .map(|values| json_cstring(values, "metadata collector identifiers"))
            .transpose()?;
        let classifying_labels = classifying_labels
            .as_ref()
            .map(|values| json_cstring(values, "metadata collector classifying labels"))
            .transpose()?;

        let mut err: *mut c_char = ptr::null_mut();
        let ptr = unsafe {
            ffi::av_player_item_metadata_collector_create(
                identifiers
                    .as_ref()
                    .map_or(ptr::null(), |values| values.as_ptr()),
                classifying_labels
                    .as_ref()
                    .map_or(ptr::null(), |values| values.as_ptr()),
                &mut err,
            )
        };
        if ptr.is_null() {
            return Err(unsafe { from_swift(ffi::status::OPERATION_FAILED, err) });
        }
        Ok(Self { ptr })
    }

    fn info(&self) -> Result<MetadataCollectorInfoPayload, AVPlayerError> {
        let mut err: *mut c_char = ptr::null_mut();
        let json_ptr =
            unsafe { ffi::av_player_item_metadata_collector_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)
    }

    /// Calls the `AVPlayer` framework counterpart for `identifiers`.
    pub fn identifiers(&self) -> Result<Vec<String>, AVPlayerError> {
        Ok(self.info()?.identifiers.unwrap_or_default())
    }

    /// Calls the `AVPlayer` framework counterpart for `classifying_labels`.
    pub fn classifying_labels(&self) -> Result<Vec<String>, AVPlayerError> {
        Ok(self.info()?.classifying_labels.unwrap_or_default())
    }

    /// Calls the `AVPlayer` framework counterpart for `has_delegate`.
    pub fn has_delegate(&self) -> Result<bool, AVPlayerError> {
        Ok(self.info()?.has_delegate)
    }

    /// Calls the `AVPlayer` framework counterpart for `observe`.
    pub fn observe<F>(
        &self,
        queue_label: Option<&str>,
        callback: F,
    ) -> Result<MetadataCollectorObserver, AVPlayerError>
    where
        F: Fn(MetadataCollectorEvent) + Send + 'static,
    {
        let queue_label = queue_label
            .map(|label| crate::util::to_cstring(label, "metadata collector queue label"))
            .transpose()?;
        let state = Box::new(MetadataCollectorObserverState {
            callback: Box::new(callback),
        });
        let userdata = Box::into_raw(state).cast::<c_void>();
        let mut err: *mut c_char = ptr::null_mut();
        let token = unsafe {
            ffi::av_player_item_metadata_collector_add_observer(
                self.ptr,
                queue_label
                    .as_ref()
                    .map_or(ptr::null(), |label| label.as_ptr()),
                Some(metadata_collector_event_trampoline),
                userdata,
                Some(metadata_collector_observer_drop),
                &mut err,
            )
        };
        if token.is_null() {
            unsafe { metadata_collector_observer_drop(userdata) };
            return Err(unsafe { from_swift(ffi::status::OBSERVER_FAILED, err) });
        }
        Ok(MetadataCollectorObserver { token })
    }
}

/// Mirrors the `AVPlayer` framework counterpart for `MetadataCollectorObserver`.
#[derive(Debug)]
pub struct MetadataCollectorObserver {
    token: *mut c_void,
}

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

// SAFETY: These metadata-collector handles are safe to transfer across thread
// boundaries; method calls are internally dispatched safely.
unsafe impl Send for PlayerItemMetadataCollector {}
unsafe impl Send for MetadataCollectorObserver {}

impl PlayerItem {
    /// Calls the `AVPlayer` framework counterpart for `add_metadata_collector`.
    pub fn add_metadata_collector(
        &self,
        collector: &PlayerItemMetadataCollector,
    ) -> Result<(), AVPlayerError> {
        let mut err: *mut c_char = ptr::null_mut();
        let status = unsafe {
            ffi::av_player_item_add_media_data_collector(self.ptr, collector.ptr, &mut err)
        };
        if status != ffi::status::OK {
            return Err(unsafe { from_swift(status, err) });
        }
        Ok(())
    }

    /// Calls the `AVPlayer` framework counterpart for `remove_metadata_collector`.
    pub fn remove_metadata_collector(&self, collector: &PlayerItemMetadataCollector) {
        unsafe { ffi::av_player_item_remove_media_data_collector(self.ptr, collector.ptr) };
    }

    /// Calls the `AVPlayer` framework counterpart for `media_data_collectors`.
    pub fn media_data_collectors(
        &self,
    ) -> Result<Vec<PlayerItemMediaDataCollectorInfo>, AVPlayerError> {
        let mut err: *mut c_char = ptr::null_mut();
        let json_ptr =
            unsafe { ffi::av_player_item_media_data_collectors_json(self.ptr, &mut err) };
        if json_ptr.is_null() {
            return Err(unsafe { from_swift(ffi::status::OPERATION_FAILED, err) });
        }
        Ok(
            parse_json_and_free::<Vec<PlayerItemMediaDataCollectorInfoPayload>>(json_ptr)?
                .into_iter()
                .map(PlayerItemMediaDataCollectorInfo::from)
                .collect(),
        )
    }
}

unsafe extern "C" fn metadata_collector_event_trampoline(
    userdata: *mut c_void,
    payload_json: *const c_char,
) {
    if userdata.is_null() || payload_json.is_null() {
        return;
    }

    let callback = &*userdata.cast::<MetadataCollectorObserverState>();
    let Ok(payload) = core::ffi::CStr::from_ptr(payload_json).to_str() else {
        return;
    };
    let Ok(payload) = serde_json::from_str::<MetadataCollectorEventPayload>(payload) else {
        return;
    };

    let event = match payload.event.as_str() {
        "did_collect_date_range_metadata_groups" => {
            MetadataCollectorEvent::DidCollectDateRangeMetadataGroups {
                groups: payload.groups,
                new_indices: payload.new_indices,
                modified_indices: payload.modified_indices,
            }
        }
        _ => return,
    };

    crate::util::catch_cb_panic("metadata_collector_event_trampoline", || {
        (callback.callback)(event);
    });
}

unsafe extern "C" fn metadata_collector_observer_drop(userdata: *mut c_void) {
    if !userdata.is_null() {
        drop(Box::from_raw(
            userdata.cast::<MetadataCollectorObserverState>(),
        ));
    }
}