#![allow(clippy::missing_errors_doc, clippy::must_use_candidate)]
use core::ffi::{c_char, c_void};
use core::ptr;
use std::ffi::CStr;
use serde::Deserialize;
use crate::error::{from_swift, AVPlayerError};
use crate::ffi;
use crate::player::Player;
use crate::util::{json_cstring, parse_json_and_free, to_cstring};
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "camelCase")]
struct CriteriaPayload {
preferred_languages: Option<Vec<String>>,
preferred_media_characteristics: Option<Vec<String>>,
principal_media_characteristics: Option<Vec<String>>,
}
#[derive(Debug, Clone, PartialEq, Deserialize)]
#[serde(rename_all = "camelCase")]
struct PlayerInfoPayload {
time_control_status: Option<i32>,
reason_for_waiting_to_play: Option<String>,
action_at_item_end: Option<i32>,
volume: Option<f32>,
muted: Option<bool>,
automatically_waits_to_minimize_stalling: Option<bool>,
applies_media_selection_criteria_automatically: Option<bool>,
eligible_for_hdr_playback: Option<bool>,
audiovisual_background_playback_policy: Option<i32>,
network_resource_priority: Option<i32>,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum MediaCharacteristic {
Audible,
Legible,
Visual,
ContainsOnlyForcedSubtitles,
TranscribesSpokenDialogForAccessibility,
DescribesMusicAndSoundForAccessibility,
DescribesVideoForAccessibility,
EasyToRead,
LanguageTranslation,
DubbedTranslation,
VoiceOverTranslation,
IsOriginalContent,
Unknown(String),
}
impl MediaCharacteristic {
#[must_use]
pub fn as_raw(&self) -> &str {
match self {
Self::Audible => "audible",
Self::Legible => "legible",
Self::Visual => "visual",
Self::ContainsOnlyForcedSubtitles => "contains_only_forced_subtitles",
Self::TranscribesSpokenDialogForAccessibility => {
"transcribes_spoken_dialog_for_accessibility"
}
Self::DescribesMusicAndSoundForAccessibility => {
"describes_music_and_sound_for_accessibility"
}
Self::DescribesVideoForAccessibility => "describes_video_for_accessibility",
Self::EasyToRead => "easy_to_read",
Self::LanguageTranslation => "language_translation",
Self::DubbedTranslation => "dubbed_translation",
Self::VoiceOverTranslation => "voice_over_translation",
Self::IsOriginalContent => "is_original_content",
Self::Unknown(raw) => raw,
}
}
#[must_use]
pub fn from_raw(raw: &str) -> Self {
match raw {
"audible" => Self::Audible,
"legible" => Self::Legible,
"visual" => Self::Visual,
"contains_only_forced_subtitles" => Self::ContainsOnlyForcedSubtitles,
"transcribes_spoken_dialog_for_accessibility" => {
Self::TranscribesSpokenDialogForAccessibility
}
"describes_music_and_sound_for_accessibility" => {
Self::DescribesMusicAndSoundForAccessibility
}
"describes_video_for_accessibility" => Self::DescribesVideoForAccessibility,
"easy_to_read" => Self::EasyToRead,
"language_translation" => Self::LanguageTranslation,
"dubbed_translation" => Self::DubbedTranslation,
"voice_over_translation" => Self::VoiceOverTranslation,
"is_original_content" => Self::IsOriginalContent,
other => Self::Unknown(other.to_owned()),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum PlayerActionAtItemEnd {
Advance,
Pause,
None,
}
impl PlayerActionAtItemEnd {
#[must_use]
pub const fn from_raw(raw: i32) -> Self {
match raw {
0 => Self::Advance,
2 => Self::None,
_ => Self::Pause,
}
}
#[must_use]
pub const fn as_raw(self) -> i32 {
match self {
Self::Advance => 0,
Self::Pause => 1,
Self::None => 2,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum PlayerTimeControlStatus {
Paused,
WaitingToPlayAtSpecifiedRate,
Playing,
Unknown(i32),
}
impl PlayerTimeControlStatus {
#[must_use]
pub const fn from_raw(raw: i32) -> Self {
match raw {
0 => Self::Paused,
1 => Self::WaitingToPlayAtSpecifiedRate,
2 => Self::Playing,
other => Self::Unknown(other),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum PlayerWaitingReason {
ToMinimizeStalls,
WhileEvaluatingBufferingRate,
WithNoItemToPlay,
ForCoordinatedPlayback,
Unknown(String),
}
impl PlayerWaitingReason {
fn from_raw(raw: &str) -> Self {
match raw {
"AVPlayerWaitingToMinimizeStallsReason" => Self::ToMinimizeStalls,
"AVPlayerWaitingWhileEvaluatingBufferingRateReason" => {
Self::WhileEvaluatingBufferingRate
}
"AVPlayerWaitingWithNoItemToPlayReason" => Self::WithNoItemToPlay,
"AVPlayerWaitingForCoordinatedPlaybackReason" => Self::ForCoordinatedPlayback,
other => Self::Unknown(other.to_owned()),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum PlayerAudiovisualBackgroundPlaybackPolicy {
Automatic,
Pauses,
ContinuesIfPossible,
Unknown(i32),
}
impl PlayerAudiovisualBackgroundPlaybackPolicy {
const fn from_raw(raw: i32) -> Self {
match raw {
1 => Self::Automatic,
2 => Self::Pauses,
3 => Self::ContinuesIfPossible,
other => Self::Unknown(other),
}
}
const fn as_raw(self) -> i32 {
match self {
Self::Automatic => 1,
Self::Pauses => 2,
Self::ContinuesIfPossible => 3,
Self::Unknown(raw) => raw,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum PlayerNetworkResourcePriority {
Default,
Low,
High,
Unknown(i32),
}
impl PlayerNetworkResourcePriority {
const fn from_raw(raw: i32) -> Self {
match raw {
0 => Self::Default,
1 => Self::Low,
2 => Self::High,
other => Self::Unknown(other),
}
}
const fn as_raw(self) -> i32 {
match self {
Self::Default => 0,
Self::Low => 1,
Self::High => 2,
Self::Unknown(raw) => raw,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum PlayerRateDidChangeReason {
SetRateCalled,
SetRateFailed,
AudioSessionInterrupted,
AppBackgrounded,
Unknown(String),
}
impl PlayerRateDidChangeReason {
fn from_raw(raw: &str) -> Self {
match raw {
"AVPlayerRateDidChangeReasonSetRateCalled" => Self::SetRateCalled,
"AVPlayerRateDidChangeReasonSetRateFailed" => Self::SetRateFailed,
"AVPlayerRateDidChangeReasonAudioSessionInterrupted" => Self::AudioSessionInterrupted,
"AVPlayerRateDidChangeReasonAppBackgrounded" => Self::AppBackgrounded,
other => Self::Unknown(other.to_owned()),
}
}
}
#[derive(Debug, Clone, PartialEq, Deserialize)]
#[serde(rename_all = "camelCase")]
struct PlayerRateDidChangeEventPayload {
rate: f32,
reason: Option<String>,
has_originating_participant: bool,
}
#[derive(Debug, Clone, PartialEq)]
pub struct PlayerRateDidChangeEvent {
pub rate: f32,
pub reason: Option<PlayerRateDidChangeReason>,
pub has_originating_participant: bool,
}
struct PlayerRateObserverState {
callback: Box<dyn Fn(PlayerRateDidChangeEvent) + Send + 'static>,
}
#[derive(Debug)]
pub struct PlayerMediaSelectionCriteria {
pub(crate) ptr: *mut c_void,
}
impl Drop for PlayerMediaSelectionCriteria {
fn drop(&mut self) {
if !self.ptr.is_null() {
unsafe { ffi::av_player_media_selection_criteria_release(self.ptr) };
self.ptr = ptr::null_mut();
}
}
}
impl PlayerMediaSelectionCriteria {
pub fn new(
preferred_languages: &[impl AsRef<str>],
preferred_media_characteristics: &[MediaCharacteristic],
) -> Result<Self, AVPlayerError> {
Self::with_principal_media_characteristics(
&[],
preferred_languages,
preferred_media_characteristics,
)
}
pub fn with_principal_media_characteristics(
principal_media_characteristics: &[MediaCharacteristic],
preferred_languages: &[impl AsRef<str>],
preferred_media_characteristics: &[MediaCharacteristic],
) -> Result<Self, AVPlayerError> {
let preferred_languages = preferred_languages
.iter()
.map(|language| language.as_ref().to_owned())
.collect::<Vec<_>>();
let preferred_media_characteristics = preferred_media_characteristics
.iter()
.map(MediaCharacteristic::as_raw)
.collect::<Vec<_>>();
let principal_media_characteristics = principal_media_characteristics
.iter()
.map(MediaCharacteristic::as_raw)
.collect::<Vec<_>>();
let preferred_languages = json_cstring(&preferred_languages, "preferred languages")?;
let preferred_media_characteristics = json_cstring(
&preferred_media_characteristics,
"preferred media characteristics",
)?;
let principal_media_characteristics = if principal_media_characteristics.is_empty() {
None
} else {
Some(json_cstring(
&principal_media_characteristics,
"principal media characteristics",
)?)
};
let mut err: *mut c_char = ptr::null_mut();
let ptr = unsafe {
ffi::av_player_media_selection_criteria_create(
preferred_languages.as_ptr(),
preferred_media_characteristics.as_ptr(),
principal_media_characteristics
.as_ref()
.map_or(ptr::null(), |value| value.as_ptr()),
&mut err,
)
};
if ptr.is_null() {
return Err(unsafe { from_swift(ffi::status::PLAYER_CREATE_FAILED, err) });
}
Ok(Self { ptr })
}
fn info(&self) -> Result<CriteriaPayload, AVPlayerError> {
let mut err: *mut c_char = ptr::null_mut();
let json_ptr =
unsafe { ffi::av_player_media_selection_criteria_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 preferred_languages(&self) -> Result<Vec<String>, AVPlayerError> {
Ok(self.info()?.preferred_languages.unwrap_or_default())
}
pub fn preferred_media_characteristics(
&self,
) -> Result<Vec<MediaCharacteristic>, AVPlayerError> {
Ok(self
.info()?
.preferred_media_characteristics
.unwrap_or_default()
.into_iter()
.map(|raw| MediaCharacteristic::from_raw(&raw))
.collect())
}
pub fn principal_media_characteristics(
&self,
) -> Result<Vec<MediaCharacteristic>, AVPlayerError> {
Ok(self
.info()?
.principal_media_characteristics
.unwrap_or_default()
.into_iter()
.map(|raw| MediaCharacteristic::from_raw(&raw))
.collect())
}
}
impl Player {
fn info_for_media_selection(&self) -> Result<PlayerInfoPayload, AVPlayerError> {
let mut err: *mut c_char = ptr::null_mut();
let json_ptr = unsafe { ffi::av_player_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 time_control_status(&self) -> Result<PlayerTimeControlStatus, AVPlayerError> {
Ok(PlayerTimeControlStatus::from_raw(
self.info_for_media_selection()?
.time_control_status
.unwrap_or_default(),
))
}
pub fn reason_for_waiting_to_play(&self) -> Result<Option<String>, AVPlayerError> {
Ok(self.info_for_media_selection()?.reason_for_waiting_to_play)
}
pub fn waiting_reason(&self) -> Result<Option<PlayerWaitingReason>, AVPlayerError> {
Ok(self
.info_for_media_selection()?
.reason_for_waiting_to_play
.as_deref()
.map(PlayerWaitingReason::from_raw))
}
pub fn action_at_item_end(&self) -> Result<PlayerActionAtItemEnd, AVPlayerError> {
Ok(PlayerActionAtItemEnd::from_raw(
self.info_for_media_selection()?
.action_at_item_end
.unwrap_or(1),
))
}
pub fn set_action_at_item_end(
&self,
action: PlayerActionAtItemEnd,
) -> Result<(), AVPlayerError> {
let mut err: *mut c_char = ptr::null_mut();
let status =
unsafe { ffi::av_player_set_action_at_item_end(self.ptr, action.as_raw(), &mut err) };
if status != ffi::status::OK {
return Err(unsafe { from_swift(status, err) });
}
Ok(())
}
pub fn volume(&self) -> Result<f32, AVPlayerError> {
Ok(self.info_for_media_selection()?.volume.unwrap_or(1.0))
}
pub fn set_volume(&self, volume: f32) {
unsafe { ffi::av_player_set_volume(self.ptr, volume) };
}
pub fn is_muted(&self) -> Result<bool, AVPlayerError> {
Ok(self.info_for_media_selection()?.muted.unwrap_or(false))
}
pub fn set_muted(&self, muted: bool) {
unsafe { ffi::av_player_set_muted(self.ptr, muted) };
}
pub fn automatically_waits_to_minimize_stalling(&self) -> Result<bool, AVPlayerError> {
Ok(self
.info_for_media_selection()?
.automatically_waits_to_minimize_stalling
.unwrap_or(false))
}
pub fn set_automatically_waits_to_minimize_stalling(&self, enabled: bool) {
unsafe { ffi::av_player_set_automatically_waits_to_minimize_stalling(self.ptr, enabled) };
}
pub fn applies_media_selection_criteria_automatically(&self) -> Result<bool, AVPlayerError> {
Ok(self
.info_for_media_selection()?
.applies_media_selection_criteria_automatically
.unwrap_or(false))
}
pub fn set_applies_media_selection_criteria_automatically(&self, enabled: bool) {
unsafe {
ffi::av_player_set_applies_media_selection_criteria_automatically(self.ptr, enabled);
}
}
pub fn eligible_for_hdr_playback(&self) -> Result<bool, AVPlayerError> {
self.info_for_media_selection()?
.eligible_for_hdr_playback
.ok_or_else(|| availability_error("AVPlayer.eligibleForHDRPlayback", "10.15"))
}
pub fn audiovisual_background_playback_policy(
&self,
) -> Result<PlayerAudiovisualBackgroundPlaybackPolicy, AVPlayerError> {
Ok(PlayerAudiovisualBackgroundPlaybackPolicy::from_raw(
self.info_for_media_selection()?
.audiovisual_background_playback_policy
.ok_or_else(|| {
availability_error("AVPlayer.audiovisualBackgroundPlaybackPolicy", "12.0")
})?,
))
}
pub fn set_audiovisual_background_playback_policy(
&self,
policy: PlayerAudiovisualBackgroundPlaybackPolicy,
) -> Result<(), AVPlayerError> {
let mut err: *mut c_char = ptr::null_mut();
let status = unsafe {
ffi::av_player_set_audiovisual_background_playback_policy(
self.ptr,
policy.as_raw(),
&mut err,
)
};
if status != ffi::status::OK {
return Err(unsafe { from_swift(status, err) });
}
Ok(())
}
pub fn network_resource_priority(
&self,
) -> Result<PlayerNetworkResourcePriority, AVPlayerError> {
Ok(PlayerNetworkResourcePriority::from_raw(
self.info_for_media_selection()?
.network_resource_priority
.ok_or_else(|| availability_error("AVPlayer.networkResourcePriority", "26.0"))?,
))
}
pub fn set_network_resource_priority(
&self,
priority: PlayerNetworkResourcePriority,
) -> Result<(), AVPlayerError> {
let mut err: *mut c_char = ptr::null_mut();
let status = unsafe {
ffi::av_player_set_network_resource_priority(self.ptr, priority.as_raw(), &mut err)
};
if status != ffi::status::OK {
return Err(unsafe { from_swift(status, err) });
}
Ok(())
}
pub fn observe_rate_changes<F>(
&self,
queue_label: Option<&str>,
callback: F,
) -> Result<PlayerRateDidChangeObserver, AVPlayerError>
where
F: Fn(PlayerRateDidChangeEvent) + Send + 'static,
{
let queue_label = queue_label
.map(|label| to_cstring(label, "player rate observer queue label"))
.transpose()?;
let state = Box::new(PlayerRateObserverState {
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_add_rate_observer(
self.ptr,
queue_label
.as_ref()
.map_or(ptr::null(), |label| label.as_ptr()),
Some(player_rate_event_trampoline),
userdata,
Some(player_rate_observer_drop),
&mut err,
)
};
if token.is_null() {
unsafe { player_rate_observer_drop(userdata) };
return Err(unsafe { from_swift(ffi::status::OBSERVER_FAILED, err) });
}
Ok(PlayerRateDidChangeObserver { token })
}
pub fn set_media_selection_criteria(
&self,
media_characteristic: &MediaCharacteristic,
criteria: Option<&PlayerMediaSelectionCriteria>,
) -> Result<(), AVPlayerError> {
let media_characteristic =
to_cstring(media_characteristic.as_raw(), "media characteristic")?;
let mut err: *mut c_char = ptr::null_mut();
let status = unsafe {
ffi::av_player_set_media_selection_criteria(
self.ptr,
media_characteristic.as_ptr(),
criteria.map_or(ptr::null_mut(), |criteria| criteria.ptr),
&mut err,
)
};
if status != ffi::status::OK {
return Err(unsafe { from_swift(status, err) });
}
Ok(())
}
pub fn media_selection_criteria(
&self,
media_characteristic: &MediaCharacteristic,
) -> Result<Option<PlayerMediaSelectionCriteria>, AVPlayerError> {
let media_characteristic =
to_cstring(media_characteristic.as_raw(), "media characteristic")?;
let mut err: *mut c_char = ptr::null_mut();
let ptr = unsafe {
ffi::av_player_copy_media_selection_criteria(
self.ptr,
media_characteristic.as_ptr(),
&mut err,
)
};
if ptr.is_null() {
if err.is_null() {
return Ok(None);
}
return Err(unsafe { from_swift(ffi::status::OPERATION_FAILED, err) });
}
Ok(Some(PlayerMediaSelectionCriteria { ptr }))
}
}
#[derive(Debug)]
pub struct PlayerRateDidChangeObserver {
token: *mut c_void,
}
impl Drop for PlayerRateDidChangeObserver {
fn drop(&mut self) {
if !self.token.is_null() {
unsafe { ffi::av_player_rate_observer_release(self.token) };
self.token = ptr::null_mut();
}
}
}
unsafe impl Send for PlayerMediaSelectionCriteria {}
unsafe impl Send for PlayerRateDidChangeObserver {}
pub fn player_eligible_for_hdr_playback_did_change_notification() -> Result<String, AVPlayerError> {
let mut err: *mut c_char = ptr::null_mut();
let string_ptr =
unsafe { ffi::av_player_eligible_for_hdr_playback_did_change_notification_name(&mut err) };
if string_ptr.is_null() {
return Err(unsafe { from_swift(ffi::status::OPERATION_FAILED, err) });
}
let value = unsafe { CStr::from_ptr(string_ptr) }
.to_string_lossy()
.into_owned();
unsafe { ffi::avp_string_free(string_ptr) };
Ok(value)
}
unsafe extern "C" fn player_rate_event_trampoline(
userdata: *mut c_void,
payload_json: *const c_char,
) {
if userdata.is_null() || payload_json.is_null() {
return;
}
let callback = &*userdata.cast::<PlayerRateObserverState>();
let Ok(payload) = CStr::from_ptr(payload_json).to_str() else {
return;
};
let Ok(payload) = serde_json::from_str::<PlayerRateDidChangeEventPayload>(payload) else {
return;
};
crate::util::catch_cb_panic("player_rate_event_trampoline", || {
(callback.callback)(PlayerRateDidChangeEvent {
rate: payload.rate,
reason: payload
.reason
.as_deref()
.map(PlayerRateDidChangeReason::from_raw),
has_originating_participant: payload.has_originating_participant,
});
});
}
unsafe extern "C" fn player_rate_observer_drop(userdata: *mut c_void) {
if !userdata.is_null() {
drop(Box::from_raw(userdata.cast::<PlayerRateObserverState>()));
}
}
fn availability_error(symbol: &str, macos_version: &str) -> AVPlayerError {
AVPlayerError::OperationFailed(format!("{symbol} requires macOS {macos_version}+"))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn media_characteristic_round_trips_known_values() {
for (characteristic, raw) in [
(MediaCharacteristic::Audible, "audible"),
(MediaCharacteristic::Legible, "legible"),
(MediaCharacteristic::Visual, "visual"),
(
MediaCharacteristic::IsOriginalContent,
"is_original_content",
),
] {
assert_eq!(characteristic.as_raw(), raw);
assert_eq!(MediaCharacteristic::from_raw(raw), characteristic);
}
}
#[test]
fn media_characteristic_preserves_unknown_values() {
let characteristic = MediaCharacteristic::from_raw("custom_characteristic");
assert_eq!(
characteristic,
MediaCharacteristic::Unknown("custom_characteristic".into()),
);
assert_eq!(characteristic.as_raw(), "custom_characteristic");
}
#[test]
fn action_at_item_end_round_trips_known_values() {
for (action, raw) in [
(PlayerActionAtItemEnd::Advance, 0),
(PlayerActionAtItemEnd::Pause, 1),
(PlayerActionAtItemEnd::None, 2),
] {
assert_eq!(action.as_raw(), raw);
assert_eq!(PlayerActionAtItemEnd::from_raw(raw), action);
}
}
#[test]
fn time_control_status_maps_unknown_values() {
assert_eq!(
PlayerTimeControlStatus::from_raw(99),
PlayerTimeControlStatus::Unknown(99),
);
}
#[test]
fn waiting_reason_maps_known_values() {
for (raw, expected) in [
(
"AVPlayerWaitingToMinimizeStallsReason",
PlayerWaitingReason::ToMinimizeStalls,
),
(
"AVPlayerWaitingWhileEvaluatingBufferingRateReason",
PlayerWaitingReason::WhileEvaluatingBufferingRate,
),
(
"AVPlayerWaitingWithNoItemToPlayReason",
PlayerWaitingReason::WithNoItemToPlay,
),
(
"AVPlayerWaitingForCoordinatedPlaybackReason",
PlayerWaitingReason::ForCoordinatedPlayback,
),
] {
assert_eq!(PlayerWaitingReason::from_raw(raw), expected);
}
}
#[test]
fn waiting_reason_preserves_unknown_values() {
assert_eq!(
PlayerWaitingReason::from_raw("custom_reason"),
PlayerWaitingReason::Unknown("custom_reason".into()),
);
}
#[test]
fn background_playback_policy_round_trips_known_and_unknown_values() {
for (raw, policy) in [
(1, PlayerAudiovisualBackgroundPlaybackPolicy::Automatic),
(2, PlayerAudiovisualBackgroundPlaybackPolicy::Pauses),
(
3,
PlayerAudiovisualBackgroundPlaybackPolicy::ContinuesIfPossible,
),
(9, PlayerAudiovisualBackgroundPlaybackPolicy::Unknown(9)),
] {
assert_eq!(
PlayerAudiovisualBackgroundPlaybackPolicy::from_raw(raw),
policy
);
assert_eq!(policy.as_raw(), raw);
}
}
#[test]
fn network_resource_priority_round_trips_known_and_unknown_values() {
for (raw, priority) in [
(0, PlayerNetworkResourcePriority::Default),
(1, PlayerNetworkResourcePriority::Low),
(2, PlayerNetworkResourcePriority::High),
(8, PlayerNetworkResourcePriority::Unknown(8)),
] {
assert_eq!(PlayerNetworkResourcePriority::from_raw(raw), priority);
assert_eq!(priority.as_raw(), raw);
}
}
#[test]
fn rate_change_reason_maps_known_and_unknown_values() {
for (raw, expected) in [
(
"AVPlayerRateDidChangeReasonSetRateCalled",
PlayerRateDidChangeReason::SetRateCalled,
),
(
"AVPlayerRateDidChangeReasonSetRateFailed",
PlayerRateDidChangeReason::SetRateFailed,
),
(
"AVPlayerRateDidChangeReasonAudioSessionInterrupted",
PlayerRateDidChangeReason::AudioSessionInterrupted,
),
(
"AVPlayerRateDidChangeReasonAppBackgrounded",
PlayerRateDidChangeReason::AppBackgrounded,
),
] {
assert_eq!(PlayerRateDidChangeReason::from_raw(raw), expected);
}
assert_eq!(
PlayerRateDidChangeReason::from_raw("custom_reason"),
PlayerRateDidChangeReason::Unknown("custom_reason".into()),
);
}
#[test]
fn availability_error_formats_symbol_and_version() {
assert_eq!(
availability_error("AVPlayer.someSymbol", "13.0"),
AVPlayerError::OperationFailed("AVPlayer.someSymbol requires macOS 13.0+".into()),
);
}
}