avplayer 0.3.3

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::player::PlayerItem;
use crate::queue_player::QueuePlayer;
use crate::time::TimeRange;
use crate::util::parse_json_and_free;

#[derive(Debug, Clone, PartialEq, Deserialize)]
#[serde(rename_all = "camelCase")]
struct PlayerLooperInfoPayload {
    status: i32,
    error_message: Option<String>,
    loop_count: i64,
    looping_item_count: i64,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum PlayerLooperStatus {
    Unknown,
    Ready,
    Failed,
    Cancelled,
}

impl PlayerLooperStatus {
    #[must_use]
    pub const fn from_raw(raw: i32) -> Self {
        match raw {
            1 => Self::Ready,
            2 => Self::Failed,
            3 => Self::Cancelled,
            _ => Self::Unknown,
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum PlayerLooperItemOrdering {
    LoopingItemsPrecedeExistingItems,
    LoopingItemsFollowExistingItems,
}

impl PlayerLooperItemOrdering {
    #[must_use]
    pub const fn as_raw(self) -> i32 {
        match self {
            Self::LoopingItemsPrecedeExistingItems => 0,
            Self::LoopingItemsFollowExistingItems => 1,
        }
    }
}

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

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

// SAFETY: AVPlayerLooper ObjC handles are safe to transfer across thread
// boundaries; method calls are internally dispatched safely.
unsafe impl Send for PlayerLooper {}

impl PlayerLooper {
    pub fn new(player: &QueuePlayer, template_item: &PlayerItem) -> Result<Self, AVPlayerError> {
        Self::with_time_range_and_ordering(
            player,
            template_item,
            None,
            PlayerLooperItemOrdering::LoopingItemsPrecedeExistingItems,
        )
    }

    pub fn with_time_range(
        player: &QueuePlayer,
        template_item: &PlayerItem,
        loop_range: TimeRange,
    ) -> Result<Self, AVPlayerError> {
        Self::with_time_range_and_ordering(
            player,
            template_item,
            Some(loop_range),
            PlayerLooperItemOrdering::LoopingItemsPrecedeExistingItems,
        )
    }

    pub fn with_time_range_and_ordering(
        player: &QueuePlayer,
        template_item: &PlayerItem,
        loop_range: Option<TimeRange>,
        item_ordering: PlayerLooperItemOrdering,
    ) -> Result<Self, AVPlayerError> {
        let (
            start_value,
            start_timescale,
            start_kind,
            duration_value,
            duration_timescale,
            duration_kind,
        ) = loop_range.map_or((0, 0, 1, 0, 0, 1), |loop_range| {
            let (start_value, start_timescale, start_kind) = loop_range.start.to_raw();
            let (duration_value, duration_timescale, duration_kind) = loop_range.duration.to_raw();
            (
                start_value,
                start_timescale,
                start_kind,
                duration_value,
                duration_timescale,
                duration_kind,
            )
        });
        let mut err: *mut c_char = ptr::null_mut();
        let ptr = unsafe {
            ffi::av_player_looper_create(
                player.ptr,
                template_item.ptr,
                loop_range.is_some(),
                start_value,
                start_timescale,
                start_kind,
                duration_value,
                duration_timescale,
                duration_kind,
                item_ordering.as_raw(),
                &mut err,
            )
        };
        if ptr.is_null() {
            return Err(unsafe { from_swift(ffi::status::PLAYER_CREATE_FAILED, err) });
        }
        Ok(Self { ptr })
    }

    fn info(&self) -> Result<PlayerLooperInfoPayload, AVPlayerError> {
        let mut err: *mut c_char = ptr::null_mut();
        let json_ptr = unsafe { ffi::av_player_looper_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 status(&self) -> Result<PlayerLooperStatus, AVPlayerError> {
        Ok(PlayerLooperStatus::from_raw(self.info()?.status))
    }

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

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

    pub fn looping_items(&self) -> Result<Vec<PlayerItem>, AVPlayerError> {
        let count = self.info()?.looping_item_count;
        if count < 0 {
            return Err(AVPlayerError::OperationFailed(format!(
                "looping item count unexpectedly negative: {count}"
            )));
        }
        let count = usize::try_from(count).map_err(|error| {
            AVPlayerError::OperationFailed(format!(
                "looping item count exceeds addressable size: {error}"
            ))
        })?;

        let mut items = Vec::with_capacity(count);
        for index in 0..count {
            let ptr = unsafe {
                ffi::av_player_looper_copy_looping_item_at_index(
                    self.ptr,
                    i32::try_from(index).map_err(|error| {
                        AVPlayerError::OperationFailed(format!(
                            "looping item index exceeds bridge range: {error}"
                        ))
                    })?,
                )
            };
            if ptr.is_null() {
                return Err(AVPlayerError::OperationFailed(format!(
                    "bridge returned null looping item at index {index}"
                )));
            }
            items.push(PlayerItem { ptr });
        }
        Ok(items)
    }

    pub fn disable_looping(&self) {
        unsafe { ffi::av_player_looper_disable_looping(self.ptr) };
    }
}