Skip to main content

avplayer/
player.rs

1#![allow(clippy::missing_errors_doc, clippy::must_use_candidate)]
2
3use core::ffi::{c_char, c_void};
4use core::ptr;
5use std::ffi::{CStr, CString};
6use std::path::Path;
7
8use serde::de::DeserializeOwned;
9use serde::Deserialize;
10
11use crate::asset::{Asset, Size};
12use crate::error::{from_swift, AVPlayerError};
13use crate::ffi;
14use crate::metadata::MetadataItem;
15use crate::time::Time;
16
17#[derive(Debug, Clone, PartialEq, Deserialize)]
18#[serde(rename_all = "camelCase")]
19struct PlayerInfoPayload {
20    status: i32,
21    error_message: Option<String>,
22    rate: f32,
23    current_time: Time,
24    duration: Time,
25}
26
27#[derive(Debug, Clone, PartialEq, Deserialize)]
28#[serde(rename_all = "camelCase")]
29struct PlayerItemInfoPayload {
30    status: i32,
31    error_message: Option<String>,
32    duration: Time,
33    presentation_size: Size,
34    metadata: Vec<MetadataItem>,
35}
36
37#[derive(Debug, Clone, PartialEq, Deserialize)]
38#[serde(rename_all = "camelCase")]
39struct PlayerItemEventPayload {
40    event: String,
41    status: Option<i32>,
42    error_message: Option<String>,
43    presentation_size: Option<Size>,
44    has_originating_participant: Option<bool>,
45    recommended_time_offset_from_live: Option<Time>,
46}
47
48/// `AVPlayerStatus`.
49#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
50#[non_exhaustive]
51pub enum PlayerStatus {
52/// Mirrors the `AVPlayer` framework case `Unknown`.
53    Unknown,
54/// Mirrors the `AVPlayer` framework case `ReadyToPlay`.
55    ReadyToPlay,
56/// Mirrors the `AVPlayer` framework case `Failed`.
57    Failed,
58}
59
60impl PlayerStatus {
61/// Mirrors the `AVPlayer` framework constant `fn`.
62    #[must_use]
63    pub const fn from_raw(raw: i32) -> Self {
64        match raw {
65            1 => Self::ReadyToPlay,
66            2 => Self::Failed,
67            _ => Self::Unknown,
68        }
69    }
70}
71
72/// `AVPlayerItemStatus`.
73#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
74#[non_exhaustive]
75pub enum PlayerItemStatus {
76/// Mirrors the `AVPlayer` framework case `Unknown`.
77    Unknown,
78/// Mirrors the `AVPlayer` framework case `ReadyToPlay`.
79    ReadyToPlay,
80/// Mirrors the `AVPlayer` framework case `Failed`.
81    Failed,
82}
83
84impl PlayerItemStatus {
85/// Mirrors the `AVPlayer` framework constant `fn`.
86    #[must_use]
87    pub const fn from_raw(raw: i32) -> Self {
88        match raw {
89            1 => Self::ReadyToPlay,
90            2 => Self::Failed,
91            _ => Self::Unknown,
92        }
93    }
94}
95
96/// Events emitted by `PlayerItemObserver`.
97#[derive(Debug, Clone, PartialEq)]
98#[non_exhaustive]
99pub enum PlayerItemEvent {
100/// Mirrors the `AVPlayer` framework case `StatusChanged`.
101    StatusChanged {
102        status: PlayerItemStatus,
103        error_message: Option<String>,
104    },
105/// Mirrors the `AVPlayer` framework case `PresentationSizeChanged`.
106    PresentationSizeChanged(Size),
107/// Mirrors the `AVPlayer` framework case `TimeJumped`.
108    TimeJumped {
109        has_originating_participant: bool,
110    },
111/// Mirrors the `AVPlayer` framework case `DidPlayToEnd`.
112    DidPlayToEnd,
113/// Mirrors the `AVPlayer` framework case `FailedToPlayToEnd`.
114    FailedToPlayToEnd {
115        error_message: Option<String>,
116    },
117/// Mirrors the `AVPlayer` framework case `PlaybackStalled`.
118    PlaybackStalled,
119/// Mirrors the `AVPlayer` framework case `NewAccessLogEntry`.
120    NewAccessLogEntry,
121/// Mirrors the `AVPlayer` framework case `NewErrorLogEntry`.
122    NewErrorLogEntry,
123/// Mirrors the `AVPlayer` framework case `RecommendedTimeOffsetFromLiveDidChange`.
124    RecommendedTimeOffsetFromLiveDidChange(Time),
125/// Mirrors the `AVPlayer` framework case `MediaSelectionChanged`.
126    MediaSelectionChanged,
127}
128
129struct PlayerItemObserverState {
130    callback: Box<dyn Fn(PlayerItemEvent) + Send + 'static>,
131}
132
133struct PeriodicTimeObserverState {
134    callback: Box<dyn FnMut(Time) + Send + 'static>,
135}
136
137struct BoundaryTimeObserverState {
138    callback: Box<dyn FnMut() + Send + 'static>,
139}
140
141/// Safe wrapper around `AVPlayerItem`.
142#[derive(Debug)]
143pub struct PlayerItem {
144    pub(crate) ptr: *mut c_void,
145}
146
147impl Drop for PlayerItem {
148    fn drop(&mut self) {
149        if !self.ptr.is_null() {
150            // SAFETY: `self.ptr` is a valid, non-null handle returned by the corresponding
151            // ffi create function and has not been released.
152            unsafe { ffi::av_player_item_release(self.ptr) };
153            self.ptr = ptr::null_mut();
154        }
155    }
156}
157
158impl PlayerItem {
159    /// Create a player item from a file path.
160    pub fn from_file_path(path: impl AsRef<Path>) -> Result<Self, AVPlayerError> {
161        let path = path
162            .as_ref()
163            .to_str()
164            .ok_or_else(|| AVPlayerError::InvalidArgument("path is not valid UTF-8".into()))?;
165        Self::from_url_internal(path, true)
166    }
167
168    /// Create a player item from a remote URL.
169    pub fn from_remote_url(url: impl AsRef<str>) -> Result<Self, AVPlayerError> {
170        Self::from_url_internal(url.as_ref(), false)
171    }
172
173    /// Create a player item from an existing asset.
174    pub fn from_asset(asset: &Asset) -> Result<Self, AVPlayerError> {
175        let keys_json =
176            CString::new("[\"duration\",\"tracks\",\"metadata\"]").map_err(|error| {
177                AVPlayerError::InvalidArgument(format!("asset-key JSON contains NUL byte: {error}"))
178            })?;
179        let mut err: *mut c_char = ptr::null_mut();
180        // SAFETY: `asset.ptr` is a valid borrowed AVAsset handle, `keys_json` is a
181        // NUL-terminated string, and `err` points to writable storage for the bridge.
182        let ptr = unsafe {
183            ffi::av_player_item_create_with_asset(asset.ptr, keys_json.as_ptr(), &mut err)
184        };
185        if ptr.is_null() {
186            // SAFETY: On failure the bridge initializes `err` to an owned Swift error
187            // payload that `from_swift` consumes.
188            return Err(unsafe { from_swift(ffi::status::PLAYER_CREATE_FAILED, err) });
189        }
190        Ok(Self { ptr })
191    }
192
193    fn from_url_internal(url: &str, is_file_url: bool) -> Result<Self, AVPlayerError> {
194        let url = CString::new(url).map_err(|error| {
195            AVPlayerError::InvalidArgument(format!("URL contains NUL byte: {error}"))
196        })?;
197        let keys_json =
198            CString::new("[\"duration\",\"tracks\",\"metadata\"]").map_err(|error| {
199                AVPlayerError::InvalidArgument(format!("asset-key JSON contains NUL byte: {error}"))
200            })?;
201        let mut err: *mut c_char = ptr::null_mut();
202        // SAFETY: `url` and `keys_json` are NUL-terminated strings owned by this
203        // frame, and `err` points to writable storage for the bridge.
204        let ptr = unsafe {
205            ffi::av_player_item_create_with_url(
206                url.as_ptr(),
207                is_file_url,
208                keys_json.as_ptr(),
209                &mut err,
210            )
211        };
212        if ptr.is_null() {
213            // SAFETY: On failure the bridge initializes `err` to an owned Swift error
214            // payload that `from_swift` consumes.
215            return Err(unsafe { from_swift(ffi::status::PLAYER_CREATE_FAILED, err) });
216        }
217        Ok(Self { ptr })
218    }
219
220    fn info(&self) -> Result<PlayerItemInfoPayload, AVPlayerError> {
221        let mut err: *mut c_char = ptr::null_mut();
222        // SAFETY: `self.ptr` is a valid AVPlayerItem handle and `err` points to
223        // writable storage for the bridge.
224        let json_ptr = unsafe { ffi::av_player_item_info_json(self.ptr, &mut err) };
225        if json_ptr.is_null() {
226            // SAFETY: On failure the bridge initializes `err` to an owned Swift error
227            // payload that `from_swift` consumes.
228            return Err(unsafe { from_swift(ffi::status::OPERATION_FAILED, err) });
229        }
230        parse_json_and_free(json_ptr)
231    }
232
233/// Calls the `AVPlayer` framework counterpart for `status`.
234    pub fn status(&self) -> Result<PlayerItemStatus, AVPlayerError> {
235        Ok(PlayerItemStatus::from_raw(self.info()?.status))
236    }
237
238/// Calls the `AVPlayer` framework counterpart for `error`.
239    pub fn error(&self) -> Result<Option<String>, AVPlayerError> {
240        Ok(self.info()?.error_message)
241    }
242
243/// Calls the `AVPlayer` framework counterpart for `duration`.
244    pub fn duration(&self) -> Result<Time, AVPlayerError> {
245        Ok(self.info()?.duration)
246    }
247
248/// Calls the `AVPlayer` framework counterpart for `presentation_size`.
249    pub fn presentation_size(&self) -> Result<Size, AVPlayerError> {
250        Ok(self.info()?.presentation_size)
251    }
252
253    /// The current macOS SDK does not expose `externalMetadata`; this returns
254    /// the underlying asset metadata instead.
255    pub fn metadata(&self) -> Result<Vec<MetadataItem>, AVPlayerError> {
256        Ok(self.info()?.metadata)
257    }
258
259/// Calls the `AVPlayer` framework counterpart for `observe`.
260    pub fn observe<F>(&self, callback: F) -> Result<PlayerItemObserver, AVPlayerError>
261    where
262        F: Fn(PlayerItemEvent) + Send + 'static,
263    {
264        let state = Box::new(PlayerItemObserverState {
265            callback: Box::new(callback),
266        });
267        let userdata = Box::into_raw(state).cast::<c_void>();
268        let mut err: *mut c_char = ptr::null_mut();
269        // SAFETY: `self.ptr` is a valid AVPlayerItem handle, `userdata` is a Box
270        // allocation that remains alive until the drop callback runs, and `err`
271        // points to writable storage for the bridge.
272        let token = unsafe {
273            ffi::av_player_item_add_observer(
274                self.ptr,
275                Some(player_item_event_trampoline),
276                userdata,
277                Some(player_item_observer_drop),
278                &mut err,
279            )
280        };
281        if token.is_null() {
282            // SAFETY: Observer creation failed before ownership of `userdata` was
283            // transferred to the bridge, so we must reclaim it locally.
284            unsafe { player_item_observer_drop(userdata) };
285            // SAFETY: On failure the bridge initializes `err` to an owned Swift error
286            // payload that `from_swift` consumes.
287            return Err(unsafe { from_swift(ffi::status::OBSERVER_FAILED, err) });
288        }
289        Ok(PlayerItemObserver { token })
290    }
291}
292
293/// KVO + notification observer for `AVPlayerItem`.
294#[derive(Debug)]
295pub struct PlayerItemObserver {
296    token: *mut c_void,
297}
298
299impl Drop for PlayerItemObserver {
300    fn drop(&mut self) {
301        if !self.token.is_null() {
302            // SAFETY: `self.token` is a valid observer token returned by the bridge
303            // and has not been released yet.
304            unsafe { ffi::av_player_item_observer_release(self.token) };
305            self.token = ptr::null_mut();
306        }
307    }
308}
309
310/// Safe wrapper around `AVPlayer`.
311#[derive(Debug)]
312pub struct Player {
313    pub(crate) ptr: *mut c_void,
314}
315
316impl Drop for Player {
317    fn drop(&mut self) {
318        if !self.ptr.is_null() {
319            // SAFETY: `self.ptr` is a valid, non-null handle returned by the corresponding
320            // ffi create function and has not been released.
321            unsafe { ffi::av_player_release(self.ptr) };
322            self.ptr = ptr::null_mut();
323        }
324    }
325}
326
327impl Player {
328    /// Create a player from a file path.
329    pub fn from_file_path(path: impl AsRef<Path>) -> Result<Self, AVPlayerError> {
330        let path = path
331            .as_ref()
332            .to_str()
333            .ok_or_else(|| AVPlayerError::InvalidArgument("path is not valid UTF-8".into()))?;
334        Self::from_url_internal(path, true)
335    }
336
337    /// Create a player from a remote URL.
338    pub fn from_remote_url(url: impl AsRef<str>) -> Result<Self, AVPlayerError> {
339        Self::from_url_internal(url.as_ref(), false)
340    }
341
342    /// Create a player that plays the supplied asset.
343    pub fn from_asset(asset: &Asset) -> Result<Self, AVPlayerError> {
344        let mut err: *mut c_char = ptr::null_mut();
345        // SAFETY: `asset.ptr` is a valid borrowed AVAsset handle and `err` points
346        // to writable storage for the bridge.
347        let ptr = unsafe { ffi::av_player_create_with_asset(asset.ptr, &mut err) };
348        if ptr.is_null() {
349            // SAFETY: On failure the bridge initializes `err` to an owned Swift error
350            // payload that `from_swift` consumes.
351            return Err(unsafe { from_swift(ffi::status::PLAYER_CREATE_FAILED, err) });
352        }
353        Ok(Self { ptr })
354    }
355
356    /// Create a player from an already-configured item.
357    pub fn from_item(item: &PlayerItem) -> Result<Self, AVPlayerError> {
358        let mut err: *mut c_char = ptr::null_mut();
359        // SAFETY: `item.ptr` is a valid borrowed AVPlayerItem handle and `err`
360        // points to writable storage for the bridge.
361        let ptr = unsafe { ffi::av_player_create_with_item(item.ptr, &mut err) };
362        if ptr.is_null() {
363            // SAFETY: On failure the bridge initializes `err` to an owned Swift error
364            // payload that `from_swift` consumes.
365            return Err(unsafe { from_swift(ffi::status::PLAYER_CREATE_FAILED, err) });
366        }
367        Ok(Self { ptr })
368    }
369
370    fn from_url_internal(url: &str, is_file_url: bool) -> Result<Self, AVPlayerError> {
371        let url = CString::new(url).map_err(|error| {
372            AVPlayerError::InvalidArgument(format!("URL contains NUL byte: {error}"))
373        })?;
374        let mut err: *mut c_char = ptr::null_mut();
375        // SAFETY: `url` is a NUL-terminated string owned by this frame and `err`
376        // points to writable storage for the bridge.
377        let ptr = unsafe { ffi::av_player_create_with_url(url.as_ptr(), is_file_url, &mut err) };
378        if ptr.is_null() {
379            // SAFETY: On failure the bridge initializes `err` to an owned Swift error
380            // payload that `from_swift` consumes.
381            return Err(unsafe { from_swift(ffi::status::PLAYER_CREATE_FAILED, err) });
382        }
383        Ok(Self { ptr })
384    }
385
386    fn info(&self) -> Result<PlayerInfoPayload, AVPlayerError> {
387        let mut err: *mut c_char = ptr::null_mut();
388        // SAFETY: `self.ptr` is a valid AVPlayer handle and `err` points to
389        // writable storage for the bridge.
390        let json_ptr = unsafe { ffi::av_player_info_json(self.ptr, &mut err) };
391        if json_ptr.is_null() {
392            // SAFETY: On failure the bridge initializes `err` to an owned Swift error
393            // payload that `from_swift` consumes.
394            return Err(unsafe { from_swift(ffi::status::OPERATION_FAILED, err) });
395        }
396        parse_json_and_free(json_ptr)
397    }
398
399/// Calls the `AVPlayer` framework counterpart for `status`.
400    pub fn status(&self) -> Result<PlayerStatus, AVPlayerError> {
401        Ok(PlayerStatus::from_raw(self.info()?.status))
402    }
403
404/// Calls the `AVPlayer` framework counterpart for `error`.
405    pub fn error(&self) -> Result<Option<String>, AVPlayerError> {
406        Ok(self.info()?.error_message)
407    }
408
409/// Calls the `AVPlayer` framework counterpart for `rate`.
410    pub fn rate(&self) -> Result<f32, AVPlayerError> {
411        Ok(self.info()?.rate)
412    }
413
414/// Calls the `AVPlayer` framework counterpart for `current_time`.
415    pub fn current_time(&self) -> Result<Time, AVPlayerError> {
416        Ok(self.info()?.current_time)
417    }
418
419/// Calls the `AVPlayer` framework counterpart for `duration`.
420    pub fn duration(&self) -> Result<Time, AVPlayerError> {
421        Ok(self.info()?.duration)
422    }
423
424/// Calls the `AVPlayer` framework counterpart for `current_item`.
425    pub fn current_item(&self) -> Option<PlayerItem> {
426        // SAFETY: `self.ptr` is a valid AVPlayer handle; the bridge returns either
427        // a retained current item or null when no item is set.
428        let ptr = unsafe { ffi::av_player_copy_current_item(self.ptr) };
429        if ptr.is_null() {
430            None
431        } else {
432            Some(PlayerItem { ptr })
433        }
434    }
435
436/// Calls the `AVPlayer` framework counterpart for `play`.
437    pub fn play(&self) {
438        // SAFETY: `self.ptr` is a valid, non-null handle returned by the corresponding
439        // ffi create function and has not been released.
440        unsafe { ffi::av_player_play(self.ptr) };
441    }
442
443/// Calls the `AVPlayer` framework counterpart for `pause`.
444    pub fn pause(&self) {
445        // SAFETY: `self.ptr` is a valid, non-null handle returned by the corresponding
446        // ffi create function and has not been released.
447        unsafe { ffi::av_player_pause(self.ptr) };
448    }
449
450/// Calls the `AVPlayer` framework counterpart for `set_rate`.
451    pub fn set_rate(&self, rate: f32) {
452        // SAFETY: `self.ptr` is a valid, non-null handle returned by the corresponding
453        // ffi create function and has not been released.
454        unsafe { ffi::av_player_set_rate(self.ptr, rate) };
455    }
456
457/// Calls the `AVPlayer` framework counterpart for `seek_to`.
458    pub fn seek_to(&self, time: Time) -> Result<(), AVPlayerError> {
459        let mut err: *mut c_char = ptr::null_mut();
460        let (value, timescale, kind) = time.to_raw();
461        // SAFETY: `self.ptr` is a valid AVPlayer handle and `err` points to
462        // writable storage for the bridge.
463        let status = unsafe { ffi::av_player_seek(self.ptr, value, timescale, kind, &mut err) };
464        if status != ffi::status::OK {
465            // SAFETY: On failure the bridge initializes `err` to an owned Swift error
466            // payload that `from_swift` consumes.
467            return Err(unsafe { from_swift(status, err) });
468        }
469        Ok(())
470    }
471
472/// Calls the `AVPlayer` framework counterpart for `add_periodic_time_observer`.
473    pub fn add_periodic_time_observer<F>(
474        &self,
475        interval: Time,
476        queue_label: Option<&str>,
477        callback: F,
478    ) -> Result<PeriodicTimeObserver, AVPlayerError>
479    where
480        F: FnMut(Time) + Send + 'static,
481    {
482        let queue_label = queue_label_cstring(queue_label)?;
483        let state = Box::new(PeriodicTimeObserverState {
484            callback: Box::new(callback),
485        });
486        let userdata = Box::into_raw(state).cast::<c_void>();
487        let (value, timescale, kind) = interval.to_raw();
488        let mut err: *mut c_char = ptr::null_mut();
489        // SAFETY: `self.ptr` is a valid AVPlayer handle, `userdata` is a Box
490        // allocation that remains alive until the drop callback runs, and `err`
491        // points to writable storage for the bridge.
492        let token = unsafe {
493            ffi::av_player_add_periodic_time_observer(
494                self.ptr,
495                value,
496                timescale,
497                kind,
498                queue_label
499                    .as_ref()
500                    .map_or(ptr::null(), |label| label.as_ptr()),
501                Some(periodic_time_observer_trampoline),
502                userdata,
503                Some(periodic_time_observer_drop),
504                &mut err,
505            )
506        };
507        if token.is_null() {
508            // SAFETY: Observer creation failed before ownership of `userdata` was
509            // transferred to the bridge, so we must reclaim it locally.
510            unsafe { periodic_time_observer_drop(userdata) };
511            // SAFETY: On failure the bridge initializes `err` to an owned Swift error
512            // payload that `from_swift` consumes.
513            return Err(unsafe { from_swift(ffi::status::OBSERVER_FAILED, err) });
514        }
515        Ok(PeriodicTimeObserver { token })
516    }
517
518/// Calls the `AVPlayer` framework counterpart for `add_boundary_time_observer`.
519    pub fn add_boundary_time_observer<F>(
520        &self,
521        times: &[Time],
522        queue_label: Option<&str>,
523        callback: F,
524    ) -> Result<BoundaryTimeObserver, AVPlayerError>
525    where
526        F: FnMut() + Send + 'static,
527    {
528        let queue_label = queue_label_cstring(queue_label)?;
529        let times_json = serde_json::to_string(times).map_err(|error| {
530            AVPlayerError::InvalidArgument(format!("failed to encode boundary times: {error}"))
531        })?;
532        let times_json = CString::new(times_json).map_err(|error| {
533            AVPlayerError::InvalidArgument(format!(
534                "boundary times JSON contains NUL byte: {error}"
535            ))
536        })?;
537        let state = Box::new(BoundaryTimeObserverState {
538            callback: Box::new(callback),
539        });
540        let userdata = Box::into_raw(state).cast::<c_void>();
541        let mut err: *mut c_char = ptr::null_mut();
542        // SAFETY: `self.ptr` is a valid AVPlayer handle, `times_json` is a
543        // NUL-terminated string owned by this frame, `userdata` is kept alive
544        // until the drop callback runs, and `err` points to writable storage.
545        let token = unsafe {
546            ffi::av_player_add_boundary_time_observer(
547                self.ptr,
548                times_json.as_ptr(),
549                queue_label
550                    .as_ref()
551                    .map_or(ptr::null(), |label| label.as_ptr()),
552                Some(boundary_time_observer_trampoline),
553                userdata,
554                Some(boundary_time_observer_drop),
555                &mut err,
556            )
557        };
558        if token.is_null() {
559            // SAFETY: Observer creation failed before ownership of `userdata` was
560            // transferred to the bridge, so we must reclaim it locally.
561            unsafe { boundary_time_observer_drop(userdata) };
562            // SAFETY: On failure the bridge initializes `err` to an owned Swift error
563            // payload that `from_swift` consumes.
564            return Err(unsafe { from_swift(ffi::status::OBSERVER_FAILED, err) });
565        }
566        Ok(BoundaryTimeObserver { token })
567    }
568}
569
570/// RAII token for `addPeriodicTimeObserver`.
571#[derive(Debug)]
572pub struct PeriodicTimeObserver {
573    token: *mut c_void,
574}
575
576impl Drop for PeriodicTimeObserver {
577    fn drop(&mut self) {
578        if !self.token.is_null() {
579            // SAFETY: `self.token` is a valid observer token returned by the bridge
580            // and has not been released yet.
581            unsafe { ffi::av_player_time_observer_release(self.token) };
582            self.token = ptr::null_mut();
583        }
584    }
585}
586
587/// RAII token for `addBoundaryTimeObserver`.
588#[derive(Debug)]
589pub struct BoundaryTimeObserver {
590    token: *mut c_void,
591}
592
593impl Drop for BoundaryTimeObserver {
594    fn drop(&mut self) {
595        if !self.token.is_null() {
596            // SAFETY: `self.token` is a valid observer token returned by the bridge
597            // and has not been released yet.
598            unsafe { ffi::av_player_time_observer_release(self.token) };
599            self.token = ptr::null_mut();
600        }
601    }
602}
603
604// SAFETY: AVPlayer / AVPlayerItem ObjC handles and observer tokens are safe to
605// transfer across thread boundaries; method calls are internally dispatched
606// safely.
607unsafe impl Send for PlayerItem {}
608unsafe impl Send for PlayerItemObserver {}
609unsafe impl Send for Player {}
610unsafe impl Send for PeriodicTimeObserver {}
611unsafe impl Send for BoundaryTimeObserver {}
612
613unsafe extern "C" fn player_item_event_trampoline(
614    userdata: *mut c_void,
615    payload_json: *const c_char,
616) {
617    if userdata.is_null() || payload_json.is_null() {
618        return;
619    }
620
621    // SAFETY: `userdata` is a valid `*mut PlayerItemObserverState` allocated in
622    // `observe()` and kept alive for the lifetime of the token; null is checked above.
623    let callback = unsafe { &*userdata.cast::<PlayerItemObserverState>() };
624    // SAFETY: `payload_json` is a non-null, NUL-terminated string owned by the
625    // bridge for the duration of this callback.
626    let Ok(payload) = unsafe { CStr::from_ptr(payload_json) }.to_str() else {
627        return;
628    };
629    let Ok(payload) = serde_json::from_str::<PlayerItemEventPayload>(payload) else {
630        return;
631    };
632
633    let event = match payload.event.as_str() {
634        "status_changed" => PlayerItemEvent::StatusChanged {
635            status: PlayerItemStatus::from_raw(payload.status.unwrap_or_default()),
636            error_message: payload.error_message,
637        },
638        "presentation_size_changed" => match payload.presentation_size {
639            Some(size) => PlayerItemEvent::PresentationSizeChanged(size),
640            None => return,
641        },
642        "time_jumped" => PlayerItemEvent::TimeJumped {
643            has_originating_participant: payload.has_originating_participant.unwrap_or(false),
644        },
645        "did_play_to_end" => PlayerItemEvent::DidPlayToEnd,
646        "failed_to_play_to_end" => PlayerItemEvent::FailedToPlayToEnd {
647            error_message: payload.error_message,
648        },
649        "playback_stalled" => PlayerItemEvent::PlaybackStalled,
650        "new_access_log_entry" => PlayerItemEvent::NewAccessLogEntry,
651        "new_error_log_entry" => PlayerItemEvent::NewErrorLogEntry,
652        "recommended_time_offset_from_live_did_change" => {
653            match payload.recommended_time_offset_from_live {
654                Some(time) => PlayerItemEvent::RecommendedTimeOffsetFromLiveDidChange(time),
655                None => return,
656            }
657        }
658        "media_selection_changed" => PlayerItemEvent::MediaSelectionChanged,
659        _ => return,
660    };
661
662    crate::util::catch_cb_panic("player_item_event_trampoline", || {
663        (callback.callback)(event);
664    });
665}
666
667unsafe extern "C" fn player_item_observer_drop(userdata: *mut c_void) {
668    if !userdata.is_null() {
669        // SAFETY: `userdata` is the unique Box pointer created in `observe()`; the
670        // Swift bridge calls this drop callback exactly once.
671        drop(unsafe { Box::from_raw(userdata.cast::<PlayerItemObserverState>()) });
672    }
673}
674
675unsafe extern "C" fn periodic_time_observer_trampoline(
676    userdata: *mut c_void,
677    value: i64,
678    timescale: i32,
679    kind: i32,
680) {
681    if userdata.is_null() {
682        return;
683    }
684    // SAFETY: `userdata` is a valid `*mut PeriodicTimeObserverState` allocated in
685    // `add_periodic_time_observer()` and kept alive for the lifetime of the token;
686    // null is checked above.
687    let state = unsafe { &mut *userdata.cast::<PeriodicTimeObserverState>() };
688    crate::util::catch_cb_panic("periodic_time_observer_trampoline", || {
689        (state.callback)(Time::from_raw(value, timescale, kind));
690    });
691}
692
693unsafe extern "C" fn periodic_time_observer_drop(userdata: *mut c_void) {
694    if !userdata.is_null() {
695        // SAFETY: `userdata` is the unique Box pointer created in
696        // `add_periodic_time_observer()`; the Swift bridge calls this drop callback
697        // exactly once.
698        drop(unsafe { Box::from_raw(userdata.cast::<PeriodicTimeObserverState>()) });
699    }
700}
701
702unsafe extern "C" fn boundary_time_observer_trampoline(userdata: *mut c_void) {
703    if userdata.is_null() {
704        return;
705    }
706    // SAFETY: `userdata` is a valid `*mut BoundaryTimeObserverState` allocated in
707    // `add_boundary_time_observer()` and kept alive for the lifetime of the token;
708    // null is checked above.
709    let state = unsafe { &mut *userdata.cast::<BoundaryTimeObserverState>() };
710    crate::util::catch_cb_panic("boundary_time_observer_trampoline", || {
711        (state.callback)();
712    });
713}
714
715unsafe extern "C" fn boundary_time_observer_drop(userdata: *mut c_void) {
716    if !userdata.is_null() {
717        // SAFETY: `userdata` is the unique Box pointer created in
718        // `add_boundary_time_observer()`; the Swift bridge calls this drop callback
719        // exactly once.
720        drop(unsafe { Box::from_raw(userdata.cast::<BoundaryTimeObserverState>()) });
721    }
722}
723
724fn queue_label_cstring(queue_label: Option<&str>) -> Result<Option<CString>, AVPlayerError> {
725    queue_label
726        .map(|label| {
727            CString::new(label).map_err(|error| {
728                AVPlayerError::InvalidArgument(format!("queue label contains NUL byte: {error}"))
729            })
730        })
731        .transpose()
732}
733
734fn parse_json_and_free<T: DeserializeOwned>(json_ptr: *mut c_char) -> Result<T, AVPlayerError> {
735    // SAFETY: `json_ptr` is a non-null, NUL-terminated C string returned by the
736    // bridge; it remains valid until freed with `avp_string_free`.
737    let json = unsafe { CStr::from_ptr(json_ptr) }
738        .to_string_lossy()
739        .into_owned();
740    // SAFETY: `json_ptr` was returned by the FFI and has not been freed yet.
741    unsafe { ffi::avp_string_free(json_ptr) };
742    serde_json::from_str::<T>(&json).map_err(|error| {
743        AVPlayerError::OperationFailed(format!("failed to decode bridge JSON: {error}"))
744    })
745}