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,
    clippy::struct_excessive_bools
)]

use core::ffi::c_char;
use core::ops::{BitOr, BitOrAssign};
use core::ptr;
use std::ffi::CString;

use serde::Deserialize;

use crate::asset::Size;
use crate::error::{from_swift, AVPlayerError};
use crate::ffi;
use crate::player::PlayerItem;
use crate::time::TimeRange;
use crate::util::parse_json_and_free;

#[derive(Debug, Clone, PartialEq, Deserialize)]
#[serde(rename_all = "camelCase")]
struct ExtendedPlayerItemInfoPayload {
    automatically_loaded_asset_keys: Vec<String>,
    seekable_time_ranges: Vec<TimeRange>,
    loaded_time_ranges: Vec<TimeRange>,
    can_use_network_resources_for_live_streaming_while_paused: bool,
    preferred_forward_buffer_duration: f64,
    preferred_peak_bit_rate: f64,
    preferred_peak_bit_rate_for_expensive_networks: f64,
    preferred_maximum_resolution: Size,
    preferred_maximum_resolution_for_expensive_networks: Size,
    audio_time_pitch_algorithm: String,
    output_count: usize,
    track_count: usize,
    variant_preferences: Option<u64>,
    authorization_required_for_playback: bool,
    application_authorized_for_playback: bool,
    content_authorized_for_playback: bool,
    content_authorization_request_status: i32,
    custom_video_compositor: Option<PlayerItemVideoCompositorPayload>,
}

#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum AudioTimePitchAlgorithm {
    Spectral,
    TimeDomain,
    Varispeed,
    LowQualityZeroLatency,
    Unknown(String),
}

impl AudioTimePitchAlgorithm {
    #[must_use]
    pub fn from_raw(raw: &str) -> Self {
        match raw {
            "spectral" => Self::Spectral,
            "time_domain" => Self::TimeDomain,
            "varispeed" => Self::Varispeed,
            "low_quality_zero_latency" => Self::LowQualityZeroLatency,
            other => Self::Unknown(other.to_owned()),
        }
    }

    #[must_use]
    pub fn as_raw(&self) -> &str {
        match self {
            Self::Spectral => "spectral",
            Self::TimeDomain => "time_domain",
            Self::Varispeed => "varispeed",
            Self::LowQualityZeroLatency => "low_quality_zero_latency",
            Self::Unknown(raw) => raw,
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub struct VariantPreferences(u64);

impl VariantPreferences {
    pub const NONE: Self = Self(0);
    pub const SCALABILITY_TO_LOSSLESS_AUDIO: Self = Self(1 << 0);

    #[must_use]
    pub const fn bits(self) -> u64 {
        self.0
    }

    #[must_use]
    pub const fn contains(self, other: Self) -> bool {
        (self.0 & other.0) == other.0
    }

    const fn from_bits(bits: u64) -> Self {
        Self(bits)
    }
}

impl BitOr for VariantPreferences {
    type Output = Self;

    fn bitor(self, rhs: Self) -> Self::Output {
        Self(self.0 | rhs.0)
    }
}

impl BitOrAssign for VariantPreferences {
    fn bitor_assign(&mut self, rhs: Self) {
        self.0 |= rhs.0;
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum ContentAuthorizationStatus {
    Unknown,
    Completed,
    Cancelled,
    TimedOut,
    Busy,
    NotAvailable,
    NotPossible,
    Other(i32),
}

impl ContentAuthorizationStatus {
    const fn from_raw(raw: i32) -> Self {
        match raw {
            0 => Self::Unknown,
            1 => Self::Completed,
            2 => Self::Cancelled,
            3 => Self::TimedOut,
            4 => Self::Busy,
            5 => Self::NotAvailable,
            6 => Self::NotPossible,
            other => Self::Other(other),
        }
    }
}

#[derive(Debug, Clone, PartialEq, Deserialize)]
#[serde(rename_all = "camelCase")]
struct PlayerItemVideoCompositorPayload {
    class_name: String,
    supports_wide_color_source_frames: Option<bool>,
    supports_hdr_source_frames: Option<bool>,
    supports_source_tagged_buffers: Option<bool>,
    can_conform_color_of_source_frames: Option<bool>,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PlayerItemVideoCompositorInfo {
    pub class_name: String,
    pub supports_wide_color_source_frames: Option<bool>,
    pub supports_hdr_source_frames: Option<bool>,
    pub supports_source_tagged_buffers: Option<bool>,
    pub can_conform_color_of_source_frames: Option<bool>,
}

impl From<PlayerItemVideoCompositorPayload> for PlayerItemVideoCompositorInfo {
    fn from(payload: PlayerItemVideoCompositorPayload) -> Self {
        Self {
            class_name: payload.class_name,
            supports_wide_color_source_frames: payload.supports_wide_color_source_frames,
            supports_hdr_source_frames: payload.supports_hdr_source_frames,
            supports_source_tagged_buffers: payload.supports_source_tagged_buffers,
            can_conform_color_of_source_frames: payload.can_conform_color_of_source_frames,
        }
    }
}

impl PlayerItem {
    fn extended_info(&self) -> Result<ExtendedPlayerItemInfoPayload, AVPlayerError> {
        let mut err: *mut c_char = ptr::null_mut();
        let json_ptr = unsafe { ffi::av_player_item_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 automatically_loaded_asset_keys(&self) -> Result<Vec<String>, AVPlayerError> {
        Ok(self.extended_info()?.automatically_loaded_asset_keys)
    }

    pub fn seekable_time_ranges(&self) -> Result<Vec<TimeRange>, AVPlayerError> {
        Ok(self.extended_info()?.seekable_time_ranges)
    }

    pub fn loaded_time_ranges(&self) -> Result<Vec<TimeRange>, AVPlayerError> {
        Ok(self.extended_info()?.loaded_time_ranges)
    }

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

    pub fn set_can_use_network_resources_for_live_streaming_while_paused(&self, enabled: bool) {
        unsafe {
            ffi::av_player_item_set_can_use_network_resources_for_live_streaming_while_paused(
                self.ptr, enabled,
            );
        }
    }

    pub fn preferred_forward_buffer_duration(&self) -> Result<f64, AVPlayerError> {
        Ok(self.extended_info()?.preferred_forward_buffer_duration)
    }

    pub fn set_preferred_forward_buffer_duration(&self, duration: f64) {
        unsafe { ffi::av_player_item_set_preferred_forward_buffer_duration(self.ptr, duration) };
    }

    pub fn preferred_peak_bit_rate(&self) -> Result<f64, AVPlayerError> {
        Ok(self.extended_info()?.preferred_peak_bit_rate)
    }

    pub fn set_preferred_peak_bit_rate(&self, value: f64) {
        unsafe { ffi::av_player_item_set_preferred_peak_bit_rate(self.ptr, value) };
    }

    pub fn preferred_peak_bit_rate_for_expensive_networks(&self) -> Result<f64, AVPlayerError> {
        Ok(self
            .extended_info()?
            .preferred_peak_bit_rate_for_expensive_networks)
    }

    pub fn set_preferred_peak_bit_rate_for_expensive_networks(&self, value: f64) {
        unsafe {
            ffi::av_player_item_set_preferred_peak_bit_rate_for_expensive_networks(self.ptr, value);
        }
    }

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

    pub fn set_preferred_maximum_resolution(&self, value: Size) {
        unsafe {
            ffi::av_player_item_set_preferred_maximum_resolution(
                self.ptr,
                value.width,
                value.height,
            );
        }
    }

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

    pub fn set_preferred_maximum_resolution_for_expensive_networks(&self, value: Size) {
        unsafe {
            ffi::av_player_item_set_preferred_maximum_resolution_for_expensive_networks(
                self.ptr,
                value.width,
                value.height,
            );
        }
    }

    pub fn audio_time_pitch_algorithm(&self) -> Result<AudioTimePitchAlgorithm, AVPlayerError> {
        Ok(AudioTimePitchAlgorithm::from_raw(
            &self.extended_info()?.audio_time_pitch_algorithm,
        ))
    }

    pub fn set_audio_time_pitch_algorithm(
        &self,
        algorithm: &AudioTimePitchAlgorithm,
    ) -> Result<(), AVPlayerError> {
        let algorithm = CString::new(algorithm.as_raw()).map_err(|error| {
            AVPlayerError::InvalidArgument(format!(
                "audio time pitch algorithm contains NUL byte: {error}"
            ))
        })?;
        unsafe { ffi::av_player_item_set_audio_time_pitch_algorithm(self.ptr, algorithm.as_ptr()) };
        Ok(())
    }

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

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

    pub fn variant_preferences(&self) -> Result<VariantPreferences, AVPlayerError> {
        Ok(VariantPreferences::from_bits(
            self.extended_info()?
                .variant_preferences
                .ok_or_else(|| availability_error("AVPlayerItem.variantPreferences", "11.3"))?,
        ))
    }

    pub fn set_variant_preferences(&self, preferences: VariantPreferences) -> Result<(), AVPlayerError> {
        let mut err: *mut c_char = ptr::null_mut();
        let status = unsafe {
            ffi::av_player_item_set_variant_preferences(self.ptr, preferences.bits(), &mut err)
        };
        if status != ffi::status::OK {
            return Err(unsafe { from_swift(status, err) });
        }
        Ok(())
    }

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

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

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

    pub fn content_authorization_request_status(
        &self,
    ) -> Result<ContentAuthorizationStatus, AVPlayerError> {
        Ok(ContentAuthorizationStatus::from_raw(
            self.extended_info()?.content_authorization_request_status,
        ))
    }

    pub fn custom_video_compositor(
        &self,
    ) -> Result<Option<PlayerItemVideoCompositorInfo>, AVPlayerError> {
        Ok(self
            .extended_info()?
            .custom_video_compositor
            .map(PlayerItemVideoCompositorInfo::from))
    }
}

fn availability_error(symbol: &str, macos_version: &str) -> AVPlayerError {
    AVPlayerError::OperationFailed(format!("{symbol} requires macOS {macos_version}+"))
}