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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
50#[non_exhaustive]
51pub enum PlayerStatus {
52Unknown,
54ReadyToPlay,
56Failed,
58}
59
60impl PlayerStatus {
61#[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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
74#[non_exhaustive]
75pub enum PlayerItemStatus {
76Unknown,
78ReadyToPlay,
80Failed,
82}
83
84impl PlayerItemStatus {
85#[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#[derive(Debug, Clone, PartialEq)]
98#[non_exhaustive]
99pub enum PlayerItemEvent {
100StatusChanged {
102 status: PlayerItemStatus,
103 error_message: Option<String>,
104 },
105PresentationSizeChanged(Size),
107TimeJumped {
109 has_originating_participant: bool,
110 },
111DidPlayToEnd,
113FailedToPlayToEnd {
115 error_message: Option<String>,
116 },
117PlaybackStalled,
119NewAccessLogEntry,
121NewErrorLogEntry,
123RecommendedTimeOffsetFromLiveDidChange(Time),
125MediaSelectionChanged,
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#[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 unsafe { ffi::av_player_item_release(self.ptr) };
153 self.ptr = ptr::null_mut();
154 }
155 }
156}
157
158impl PlayerItem {
159 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 pub fn from_remote_url(url: impl AsRef<str>) -> Result<Self, AVPlayerError> {
170 Self::from_url_internal(url.as_ref(), false)
171 }
172
173 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 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 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 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 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 let json_ptr = unsafe { ffi::av_player_item_info_json(self.ptr, &mut err) };
225 if json_ptr.is_null() {
226 return Err(unsafe { from_swift(ffi::status::OPERATION_FAILED, err) });
229 }
230 parse_json_and_free(json_ptr)
231 }
232
233pub fn status(&self) -> Result<PlayerItemStatus, AVPlayerError> {
235 Ok(PlayerItemStatus::from_raw(self.info()?.status))
236 }
237
238pub fn error(&self) -> Result<Option<String>, AVPlayerError> {
240 Ok(self.info()?.error_message)
241 }
242
243pub fn duration(&self) -> Result<Time, AVPlayerError> {
245 Ok(self.info()?.duration)
246 }
247
248pub fn presentation_size(&self) -> Result<Size, AVPlayerError> {
250 Ok(self.info()?.presentation_size)
251 }
252
253 pub fn metadata(&self) -> Result<Vec<MetadataItem>, AVPlayerError> {
256 Ok(self.info()?.metadata)
257 }
258
259pub 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 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 unsafe { player_item_observer_drop(userdata) };
285 return Err(unsafe { from_swift(ffi::status::OBSERVER_FAILED, err) });
288 }
289 Ok(PlayerItemObserver { token })
290 }
291}
292
293#[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 unsafe { ffi::av_player_item_observer_release(self.token) };
305 self.token = ptr::null_mut();
306 }
307 }
308}
309
310#[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 unsafe { ffi::av_player_release(self.ptr) };
322 self.ptr = ptr::null_mut();
323 }
324 }
325}
326
327impl Player {
328 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 pub fn from_remote_url(url: impl AsRef<str>) -> Result<Self, AVPlayerError> {
339 Self::from_url_internal(url.as_ref(), false)
340 }
341
342 pub fn from_asset(asset: &Asset) -> Result<Self, AVPlayerError> {
344 let mut err: *mut c_char = ptr::null_mut();
345 let ptr = unsafe { ffi::av_player_create_with_asset(asset.ptr, &mut err) };
348 if ptr.is_null() {
349 return Err(unsafe { from_swift(ffi::status::PLAYER_CREATE_FAILED, err) });
352 }
353 Ok(Self { ptr })
354 }
355
356 pub fn from_item(item: &PlayerItem) -> Result<Self, AVPlayerError> {
358 let mut err: *mut c_char = ptr::null_mut();
359 let ptr = unsafe { ffi::av_player_create_with_item(item.ptr, &mut err) };
362 if ptr.is_null() {
363 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 let ptr = unsafe { ffi::av_player_create_with_url(url.as_ptr(), is_file_url, &mut err) };
378 if ptr.is_null() {
379 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 let json_ptr = unsafe { ffi::av_player_info_json(self.ptr, &mut err) };
391 if json_ptr.is_null() {
392 return Err(unsafe { from_swift(ffi::status::OPERATION_FAILED, err) });
395 }
396 parse_json_and_free(json_ptr)
397 }
398
399pub fn status(&self) -> Result<PlayerStatus, AVPlayerError> {
401 Ok(PlayerStatus::from_raw(self.info()?.status))
402 }
403
404pub fn error(&self) -> Result<Option<String>, AVPlayerError> {
406 Ok(self.info()?.error_message)
407 }
408
409pub fn rate(&self) -> Result<f32, AVPlayerError> {
411 Ok(self.info()?.rate)
412 }
413
414pub fn current_time(&self) -> Result<Time, AVPlayerError> {
416 Ok(self.info()?.current_time)
417 }
418
419pub fn duration(&self) -> Result<Time, AVPlayerError> {
421 Ok(self.info()?.duration)
422 }
423
424pub fn current_item(&self) -> Option<PlayerItem> {
426 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
436pub fn play(&self) {
438 unsafe { ffi::av_player_play(self.ptr) };
441 }
442
443pub fn pause(&self) {
445 unsafe { ffi::av_player_pause(self.ptr) };
448 }
449
450pub fn set_rate(&self, rate: f32) {
452 unsafe { ffi::av_player_set_rate(self.ptr, rate) };
455 }
456
457pub 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 let status = unsafe { ffi::av_player_seek(self.ptr, value, timescale, kind, &mut err) };
464 if status != ffi::status::OK {
465 return Err(unsafe { from_swift(status, err) });
468 }
469 Ok(())
470 }
471
472pub 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 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 unsafe { periodic_time_observer_drop(userdata) };
511 return Err(unsafe { from_swift(ffi::status::OBSERVER_FAILED, err) });
514 }
515 Ok(PeriodicTimeObserver { token })
516 }
517
518pub 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 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 unsafe { boundary_time_observer_drop(userdata) };
562 return Err(unsafe { from_swift(ffi::status::OBSERVER_FAILED, err) });
565 }
566 Ok(BoundaryTimeObserver { token })
567 }
568}
569
570#[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 unsafe { ffi::av_player_time_observer_release(self.token) };
582 self.token = ptr::null_mut();
583 }
584 }
585}
586
587#[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 unsafe { ffi::av_player_time_observer_release(self.token) };
599 self.token = ptr::null_mut();
600 }
601 }
602}
603
604unsafe 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 let callback = unsafe { &*userdata.cast::<PlayerItemObserverState>() };
624 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 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 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 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 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 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 let json = unsafe { CStr::from_ptr(json_ptr) }
738 .to_string_lossy()
739 .into_owned();
740 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}