Skip to main content

avplayer/
player_interstitial_event.rs

1#![allow(
2    clippy::derive_partial_eq_without_eq,
3    clippy::missing_errors_doc,
4    clippy::must_use_candidate,
5    clippy::struct_excessive_bools
6)]
7
8use core::ffi::{c_char, c_void};
9use core::ops::{BitOr, BitOrAssign};
10use core::ptr;
11
12use serde::Deserialize;
13use serde_json::Value;
14
15use crate::error::{from_swift, AVPlayerError};
16use crate::ffi;
17use crate::player::{Player, PlayerItem};
18use crate::time::Time;
19use crate::util::{parse_json_and_free, to_cstring};
20
21/// Mirrors the `AVPlayer` framework counterpart for `PlayerInterstitialEventRestrictions`.
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
23pub struct PlayerInterstitialEventRestrictions(u64);
24
25impl PlayerInterstitialEventRestrictions {
26    /// Mirrors the `AVPlayer` framework constant `NONE`.
27    pub const NONE: Self = Self(0);
28    /// Mirrors the `AVPlayer` framework constant `CONSTRAINS_SEEKING_FORWARD_IN_PRIMARY_CONTENT`.
29    pub const CONSTRAINS_SEEKING_FORWARD_IN_PRIMARY_CONTENT: Self = Self(1 << 0);
30    /// Mirrors the `AVPlayer` framework constant `REQUIRES_PLAYBACK_AT_PREFERRED_RATE_FOR_ADVANCEMENT`.
31    pub const REQUIRES_PLAYBACK_AT_PREFERRED_RATE_FOR_ADVANCEMENT: Self = Self(1 << 2);
32    /// Mirrors the `AVPlayer` framework constant `DEFAULT_POLICY`.
33    pub const DEFAULT_POLICY: Self = Self::NONE;
34
35    /// Mirrors the `AVPlayer` framework constant `fn`.
36    pub const fn bits(self) -> u64 {
37        self.0
38    }
39
40    /// Mirrors the `AVPlayer` framework constant `fn`.
41    pub const fn contains(self, other: Self) -> bool {
42        (self.0 & other.0) == other.0
43    }
44
45    const fn from_bits(bits: u64) -> Self {
46        Self(bits)
47    }
48}
49
50impl BitOr for PlayerInterstitialEventRestrictions {
51    type Output = Self;
52
53    fn bitor(self, rhs: Self) -> Self::Output {
54        Self(self.0 | rhs.0)
55    }
56}
57
58impl BitOrAssign for PlayerInterstitialEventRestrictions {
59    fn bitor_assign(&mut self, rhs: Self) {
60        self.0 |= rhs.0;
61    }
62}
63
64/// Mirrors the `AVPlayer` framework counterpart for `PlayerInterstitialEventCue`.
65#[derive(Debug, Clone, PartialEq, Eq, Hash)]
66#[non_exhaustive]
67pub enum PlayerInterstitialEventCue {
68    /// Mirrors the `AVPlayer` framework case `NoCue`.
69    NoCue,
70    /// Mirrors the `AVPlayer` framework case `JoinCue`.
71    JoinCue,
72    /// Mirrors the `AVPlayer` framework case `LeaveCue`.
73    LeaveCue,
74    /// Mirrors the `AVPlayer` framework case `Unknown`.
75    Unknown(String),
76}
77
78impl PlayerInterstitialEventCue {
79    fn from_raw(raw: &str) -> Self {
80        match raw {
81            "no_cue" => Self::NoCue,
82            "join_cue" => Self::JoinCue,
83            "leave_cue" => Self::LeaveCue,
84            other => Self::Unknown(other.to_owned()),
85        }
86    }
87
88    fn as_raw(&self) -> &str {
89        match self {
90            Self::NoCue => "no_cue",
91            Self::JoinCue => "join_cue",
92            Self::LeaveCue => "leave_cue",
93            Self::Unknown(raw) => raw,
94        }
95    }
96}
97
98/// Mirrors the `AVPlayer` framework counterpart for `PlayerInterstitialEventTimelineOccupancy`.
99#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
100#[non_exhaustive]
101pub enum PlayerInterstitialEventTimelineOccupancy {
102    /// Mirrors the `AVPlayer` framework case `SinglePoint`.
103    SinglePoint,
104    /// Mirrors the `AVPlayer` framework case `Fill`.
105    Fill,
106    /// Mirrors the `AVPlayer` framework case `Unknown`.
107    Unknown(i32),
108}
109
110impl PlayerInterstitialEventTimelineOccupancy {
111    const fn from_raw(raw: i32) -> Self {
112        match raw {
113            0 => Self::SinglePoint,
114            1 => Self::Fill,
115            other => Self::Unknown(other),
116        }
117    }
118
119    const fn raw(self) -> i32 {
120        match self {
121            Self::SinglePoint => 0,
122            Self::Fill => 1,
123            Self::Unknown(raw) => raw,
124        }
125    }
126}
127
128/// Mirrors the `AVPlayer` framework counterpart for `PlayerInterstitialEventAssetListResponseStatus`.
129#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
130#[non_exhaustive]
131pub enum PlayerInterstitialEventAssetListResponseStatus {
132    /// Mirrors the `AVPlayer` framework case `Available`.
133    Available,
134    /// Mirrors the `AVPlayer` framework case `Cleared`.
135    Cleared,
136    /// Mirrors the `AVPlayer` framework case `Unavailable`.
137    Unavailable,
138    /// Mirrors the `AVPlayer` framework case `Unknown`.
139    Unknown(i32),
140}
141
142impl PlayerInterstitialEventAssetListResponseStatus {
143    const fn from_raw(raw: i32) -> Self {
144        match raw {
145            0 => Self::Available,
146            1 => Self::Cleared,
147            2 => Self::Unavailable,
148            other => Self::Unknown(other),
149        }
150    }
151}
152
153/// Mirrors the `AVPlayer` framework counterpart for `PlayerInterstitialEventSkippableEventState`.
154#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
155#[non_exhaustive]
156pub enum PlayerInterstitialEventSkippableEventState {
157    /// Mirrors the `AVPlayer` framework case `NotSkippable`.
158    NotSkippable,
159    /// Mirrors the `AVPlayer` framework case `NotYetEligible`.
160    NotYetEligible,
161    /// Mirrors the `AVPlayer` framework case `Eligible`.
162    Eligible,
163    /// Mirrors the `AVPlayer` framework case `NoLongerEligible`.
164    NoLongerEligible,
165    /// Mirrors the `AVPlayer` framework case `Unknown`.
166    Unknown(i32),
167}
168
169impl PlayerInterstitialEventSkippableEventState {
170    const fn from_raw(raw: i32) -> Self {
171        match raw {
172            0 => Self::NotSkippable,
173            1 => Self::NotYetEligible,
174            2 => Self::Eligible,
175            3 => Self::NoLongerEligible,
176            other => Self::Unknown(other),
177        }
178    }
179}
180
181/// Mirrors the `AVPlayer` framework counterpart for `PlayerInterstitialEventInfoPayload`.
182#[derive(Debug, Clone, PartialEq, Deserialize)]
183#[serde(rename_all = "camelCase")]
184pub struct PlayerInterstitialEventInfoPayload {
185    identifier: String,
186    time: Time,
187    date: Option<String>,
188    template_item_count: usize,
189    restrictions: u64,
190    resumption_offset: Time,
191    playout_limit: Time,
192    aligns_start_with_primary_segment_boundary: bool,
193    aligns_resumption_with_primary_segment_boundary: bool,
194    cue: Option<String>,
195    will_play_once: bool,
196    user_defined_attributes_json: Option<String>,
197    asset_list_response_json: Option<String>,
198    timeline_occupancy_raw: Option<i32>,
199    supplements_primary_content: Option<bool>,
200    content_may_vary: Option<bool>,
201    has_primary_item: bool,
202}
203
204/// Mirrors the `AVPlayer` framework counterpart for `PlayerInterstitialEventInfo`.
205#[derive(Debug, Clone, PartialEq)]
206pub struct PlayerInterstitialEventInfo {
207    /// Mirrors the `AVPlayer` framework property for `identifier`.
208    pub identifier: String,
209    /// Mirrors the `AVPlayer` framework property for `time`.
210    pub time: Time,
211    /// Mirrors the `AVPlayer` framework property for `date`.
212    pub date: Option<String>,
213    /// Mirrors the `AVPlayer` framework property for `template_item_count`.
214    pub template_item_count: usize,
215    /// Mirrors the `AVPlayer` framework property for `restrictions`.
216    pub restrictions: PlayerInterstitialEventRestrictions,
217    /// Mirrors the `AVPlayer` framework property for `resumption_offset`.
218    pub resumption_offset: Time,
219    /// Mirrors the `AVPlayer` framework property for `playout_limit`.
220    pub playout_limit: Time,
221    /// Mirrors the `AVPlayer` framework property for `aligns_start_with_primary_segment_boundary`.
222    pub aligns_start_with_primary_segment_boundary: bool,
223    /// Mirrors the `AVPlayer` framework property for `aligns_resumption_with_primary_segment_boundary`.
224    pub aligns_resumption_with_primary_segment_boundary: bool,
225    /// Mirrors the `AVPlayer` framework property for `cue`.
226    pub cue: Option<PlayerInterstitialEventCue>,
227    /// Mirrors the `AVPlayer` framework property for `will_play_once`.
228    pub will_play_once: bool,
229    /// Mirrors the `AVPlayer` framework property for `user_defined_attributes`.
230    pub user_defined_attributes: Option<Value>,
231    /// Mirrors the `AVPlayer` framework property for `asset_list_response`.
232    pub asset_list_response: Option<Value>,
233    /// Mirrors the `AVPlayer` framework property for `timeline_occupancy`.
234    pub timeline_occupancy: Option<PlayerInterstitialEventTimelineOccupancy>,
235    /// Mirrors the `AVPlayer` framework property for `supplements_primary_content`.
236    pub supplements_primary_content: Option<bool>,
237    /// Mirrors the `AVPlayer` framework property for `content_may_vary`.
238    pub content_may_vary: Option<bool>,
239    /// Mirrors the `AVPlayer` framework property for `has_primary_item`.
240    pub has_primary_item: bool,
241}
242
243impl TryFrom<PlayerInterstitialEventInfoPayload> for PlayerInterstitialEventInfo {
244    type Error = AVPlayerError;
245
246    fn try_from(payload: PlayerInterstitialEventInfoPayload) -> Result<Self, Self::Error> {
247        Ok(Self {
248            identifier: payload.identifier,
249            time: payload.time,
250            date: payload.date,
251            template_item_count: payload.template_item_count,
252            restrictions: PlayerInterstitialEventRestrictions::from_bits(payload.restrictions),
253            resumption_offset: payload.resumption_offset,
254            playout_limit: payload.playout_limit,
255            aligns_start_with_primary_segment_boundary: payload
256                .aligns_start_with_primary_segment_boundary,
257            aligns_resumption_with_primary_segment_boundary: payload
258                .aligns_resumption_with_primary_segment_boundary,
259            cue: payload
260                .cue
261                .as_deref()
262                .map(PlayerInterstitialEventCue::from_raw),
263            will_play_once: payload.will_play_once,
264            user_defined_attributes: parse_json_value(payload.user_defined_attributes_json)?,
265            asset_list_response: parse_json_value(payload.asset_list_response_json)?,
266            timeline_occupancy: payload
267                .timeline_occupancy_raw
268                .map(PlayerInterstitialEventTimelineOccupancy::from_raw),
269            supplements_primary_content: payload.supplements_primary_content,
270            content_may_vary: payload.content_may_vary,
271            has_primary_item: payload.has_primary_item,
272        })
273    }
274}
275
276#[derive(Debug, Clone, PartialEq, Deserialize)]
277#[serde(rename_all = "camelCase")]
278struct PlayerInterstitialMonitorStatePayload {
279    events: Vec<PlayerInterstitialEventInfoPayload>,
280    current_event: Option<PlayerInterstitialEventInfoPayload>,
281    current_event_skippable_state_raw: Option<i32>,
282    current_event_skip_control_label: Option<String>,
283}
284
285/// Mirrors the `AVPlayer` framework counterpart for `PlayerInterstitialEventMonitorState`.
286#[derive(Debug, Clone, PartialEq)]
287pub struct PlayerInterstitialEventMonitorState {
288    /// Mirrors the `AVPlayer` framework property for `events`.
289    pub events: Vec<PlayerInterstitialEventInfo>,
290    /// Mirrors the `AVPlayer` framework property for `current_event`.
291    pub current_event: Option<PlayerInterstitialEventInfo>,
292    /// Mirrors the `AVPlayer` framework property for `current_event_skippable_state`.
293    pub current_event_skippable_state: Option<PlayerInterstitialEventSkippableEventState>,
294    /// Mirrors the `AVPlayer` framework property for `current_event_skip_control_label`.
295    pub current_event_skip_control_label: Option<String>,
296}
297
298impl TryFrom<PlayerInterstitialMonitorStatePayload> for PlayerInterstitialEventMonitorState {
299    type Error = AVPlayerError;
300
301    fn try_from(payload: PlayerInterstitialMonitorStatePayload) -> Result<Self, Self::Error> {
302        Ok(Self {
303            events: payload
304                .events
305                .into_iter()
306                .map(PlayerInterstitialEventInfo::try_from)
307                .collect::<Result<_, _>>()?,
308            current_event: payload
309                .current_event
310                .map(PlayerInterstitialEventInfo::try_from)
311                .transpose()?,
312            current_event_skippable_state: payload
313                .current_event_skippable_state_raw
314                .map(PlayerInterstitialEventSkippableEventState::from_raw),
315            current_event_skip_control_label: payload.current_event_skip_control_label,
316        })
317    }
318}
319
320#[derive(Debug, Clone, PartialEq, Deserialize)]
321#[serde(rename_all = "camelCase")]
322struct PlayerInterstitialMonitorEventPayload {
323    event: String,
324    interstitial_event: Option<PlayerInterstitialEventInfoPayload>,
325    asset_list_response_status_raw: Option<i32>,
326    skippable_state_raw: Option<i32>,
327    skip_control_label: Option<String>,
328    error_message: Option<String>,
329    playout_time: Option<Time>,
330    did_play_entire_event: Option<bool>,
331}
332
333/// Mirrors the `AVPlayer` framework counterpart for `PlayerInterstitialEventMonitorEvent`.
334#[derive(Debug, Clone, PartialEq)]
335#[non_exhaustive]
336pub enum PlayerInterstitialEventMonitorEvent {
337    /// Mirrors the `AVPlayer` framework case `EventsDidChange`.
338    EventsDidChange,
339    /// Mirrors the `AVPlayer` framework case `CurrentEventDidChange`.
340    CurrentEventDidChange,
341    /// Mirrors the `AVPlayer` framework case `AssetListResponseStatusDidChange`.
342    AssetListResponseStatusDidChange {
343        interstitial_event: Option<PlayerInterstitialEventInfo>,
344        status: PlayerInterstitialEventAssetListResponseStatus,
345        error_message: Option<String>,
346    },
347    /// Mirrors the `AVPlayer` framework case `CurrentEventSkippableStateDidChange`.
348    CurrentEventSkippableStateDidChange {
349        interstitial_event: Option<PlayerInterstitialEventInfo>,
350        state: PlayerInterstitialEventSkippableEventState,
351        skip_control_label: Option<String>,
352    },
353    /// Mirrors the `AVPlayer` framework case `CurrentEventSkipped`.
354    CurrentEventSkipped {
355        interstitial_event: Option<PlayerInterstitialEventInfo>,
356    },
357    /// Mirrors the `AVPlayer` framework case `InterstitialEventWasUnscheduled`.
358    InterstitialEventWasUnscheduled {
359        interstitial_event: Option<PlayerInterstitialEventInfo>,
360        error_message: Option<String>,
361    },
362    /// Mirrors the `AVPlayer` framework case `InterstitialEventDidFinish`.
363    InterstitialEventDidFinish {
364        interstitial_event: Option<PlayerInterstitialEventInfo>,
365        playout_time: Option<Time>,
366        did_play_entire_event: Option<bool>,
367    },
368}
369
370struct PlayerInterstitialEventMonitorObserverState {
371    callback: Box<dyn Fn(PlayerInterstitialEventMonitorEvent) + Send + 'static>,
372}
373
374/// Mirrors the `AVPlayer` framework counterpart for `PlayerInterstitialEvent`.
375#[derive(Debug)]
376pub struct PlayerInterstitialEvent {
377    pub(crate) ptr: *mut c_void,
378}
379
380impl Drop for PlayerInterstitialEvent {
381    fn drop(&mut self) {
382        if !self.ptr.is_null() {
383            unsafe { ffi::av_player_interstitial_event_release(self.ptr) };
384            self.ptr = ptr::null_mut();
385        }
386    }
387}
388
389impl PlayerInterstitialEvent {
390    /// Calls the `AVPlayer` framework counterpart for `new`.
391    pub fn new(primary_item: &PlayerItem, time: Time) -> Result<Self, AVPlayerError> {
392        let (value, timescale, kind) = time.to_raw();
393        let mut err: *mut c_char = ptr::null_mut();
394        let ptr = unsafe {
395            ffi::av_player_interstitial_event_create_with_time(
396                primary_item.ptr,
397                value,
398                timescale,
399                kind,
400                &mut err,
401            )
402        };
403        if ptr.is_null() {
404            return Err(unsafe { from_swift(ffi::status::OPERATION_FAILED, err) });
405        }
406        Ok(Self { ptr })
407    }
408
409    /// Calls the `AVPlayer` framework counterpart for `info`.
410    pub fn info(&self) -> Result<PlayerInterstitialEventInfo, AVPlayerError> {
411        let mut err: *mut c_char = ptr::null_mut();
412        let json_ptr = unsafe { ffi::av_player_interstitial_event_info_json(self.ptr, &mut err) };
413        if json_ptr.is_null() {
414            return Err(unsafe { from_swift(ffi::status::OPERATION_FAILED, err) });
415        }
416        PlayerInterstitialEventInfo::try_from(parse_json_and_free::<
417            PlayerInterstitialEventInfoPayload,
418        >(json_ptr)?)
419    }
420
421    /// Calls the `AVPlayer` framework counterpart for `set_identifier`.
422    pub fn set_identifier(&self, identifier: &str) -> Result<(), AVPlayerError> {
423        let identifier = to_cstring(identifier, "interstitial event identifier")?;
424        unsafe { ffi::av_player_interstitial_event_set_identifier(self.ptr, identifier.as_ptr()) };
425        Ok(())
426    }
427
428    /// Calls the `AVPlayer` framework counterpart for `set_restrictions`.
429    pub fn set_restrictions(&self, restrictions: PlayerInterstitialEventRestrictions) {
430        unsafe {
431            ffi::av_player_interstitial_event_set_restrictions(self.ptr, restrictions.bits());
432        }
433    }
434
435    /// Calls the `AVPlayer` framework counterpart for `set_resumption_offset`.
436    pub fn set_resumption_offset(&self, value: Time) {
437        let (time_value, timescale, kind) = value.to_raw();
438        unsafe {
439            ffi::av_player_interstitial_event_set_resumption_offset(
440                self.ptr, time_value, timescale, kind,
441            );
442        }
443    }
444
445    /// Calls the `AVPlayer` framework counterpart for `set_playout_limit`.
446    pub fn set_playout_limit(&self, value: Time) {
447        let (time_value, timescale, kind) = value.to_raw();
448        unsafe {
449            ffi::av_player_interstitial_event_set_playout_limit(
450                self.ptr, time_value, timescale, kind,
451            );
452        }
453    }
454
455    /// Calls the `AVPlayer` framework counterpart for `set_aligns_start_with_primary_segment_boundary`.
456    pub fn set_aligns_start_with_primary_segment_boundary(&self, enabled: bool) {
457        unsafe {
458            ffi::av_player_interstitial_event_set_aligns_start_with_primary_segment_boundary(
459                self.ptr, enabled,
460            );
461        }
462    }
463
464    /// Calls the `AVPlayer` framework counterpart for `set_aligns_resumption_with_primary_segment_boundary`.
465    pub fn set_aligns_resumption_with_primary_segment_boundary(&self, enabled: bool) {
466        unsafe {
467            ffi::av_player_interstitial_event_set_aligns_resumption_with_primary_segment_boundary(
468                self.ptr, enabled,
469            );
470        }
471    }
472
473    /// Calls the `AVPlayer` framework counterpart for `set_cue`.
474    pub fn set_cue(&self, cue: &PlayerInterstitialEventCue) -> Result<(), AVPlayerError> {
475        let cue = to_cstring(cue.as_raw(), "interstitial event cue")?;
476        unsafe { ffi::av_player_interstitial_event_set_cue(self.ptr, cue.as_ptr()) };
477        Ok(())
478    }
479
480    /// Calls the `AVPlayer` framework counterpart for `set_will_play_once`.
481    pub fn set_will_play_once(&self, enabled: bool) {
482        unsafe { ffi::av_player_interstitial_event_set_will_play_once(self.ptr, enabled) };
483    }
484
485    /// Calls the `AVPlayer` framework counterpart for `set_timeline_occupancy`.
486    pub fn set_timeline_occupancy(&self, occupancy: PlayerInterstitialEventTimelineOccupancy) {
487        unsafe {
488            ffi::av_player_interstitial_event_set_timeline_occupancy(self.ptr, occupancy.raw());
489        }
490    }
491
492    /// Calls the `AVPlayer` framework counterpart for `set_supplements_primary_content`.
493    pub fn set_supplements_primary_content(&self, enabled: bool) {
494        unsafe {
495            ffi::av_player_interstitial_event_set_supplements_primary_content(self.ptr, enabled);
496        }
497    }
498
499    /// Calls the `AVPlayer` framework counterpart for `set_content_may_vary`.
500    pub fn set_content_may_vary(&self, enabled: bool) {
501        unsafe { ffi::av_player_interstitial_event_set_content_may_vary(self.ptr, enabled) };
502    }
503}
504
505/// Mirrors the `AVPlayer` framework counterpart for `PlayerInterstitialEventMonitor`.
506#[derive(Debug)]
507pub struct PlayerInterstitialEventMonitor {
508    pub(crate) ptr: *mut c_void,
509}
510
511impl Drop for PlayerInterstitialEventMonitor {
512    fn drop(&mut self) {
513        if !self.ptr.is_null() {
514            unsafe { ffi::av_player_interstitial_event_monitor_release(self.ptr) };
515            self.ptr = ptr::null_mut();
516        }
517    }
518}
519
520impl PlayerInterstitialEventMonitor {
521    /// Calls the `AVPlayer` framework counterpart for `new`.
522    pub fn new(player: &Player) -> Result<Self, AVPlayerError> {
523        let mut err: *mut c_char = ptr::null_mut();
524        let ptr = unsafe { ffi::av_player_interstitial_event_monitor_create(player.ptr, &mut err) };
525        if ptr.is_null() {
526            return Err(unsafe { from_swift(ffi::status::OPERATION_FAILED, err) });
527        }
528        Ok(Self { ptr })
529    }
530
531    /// Calls the `AVPlayer` framework counterpart for `state`.
532    pub fn state(&self) -> Result<PlayerInterstitialEventMonitorState, AVPlayerError> {
533        let mut err: *mut c_char = ptr::null_mut();
534        let json_ptr =
535            unsafe { ffi::av_player_interstitial_event_monitor_info_json(self.ptr, &mut err) };
536        if json_ptr.is_null() {
537            return Err(unsafe { from_swift(ffi::status::OPERATION_FAILED, err) });
538        }
539        PlayerInterstitialEventMonitorState::try_from(parse_json_and_free::<
540            PlayerInterstitialMonitorStatePayload,
541        >(json_ptr)?)
542    }
543
544    /// Calls the `AVPlayer` framework counterpart for `observe`.
545    pub fn observe<F>(
546        &self,
547        callback: F,
548    ) -> Result<PlayerInterstitialEventMonitorObserver, AVPlayerError>
549    where
550        F: Fn(PlayerInterstitialEventMonitorEvent) + Send + 'static,
551    {
552        let state = Box::new(PlayerInterstitialEventMonitorObserverState {
553            callback: Box::new(callback),
554        });
555        let userdata = Box::into_raw(state).cast::<c_void>();
556        let mut err: *mut c_char = ptr::null_mut();
557        let token = unsafe {
558            ffi::av_player_interstitial_event_monitor_add_observer(
559                self.ptr,
560                Some(player_interstitial_event_monitor_event_trampoline),
561                userdata,
562                Some(player_interstitial_event_monitor_observer_drop),
563                &mut err,
564            )
565        };
566        if token.is_null() {
567            unsafe { player_interstitial_event_monitor_observer_drop(userdata) };
568            return Err(unsafe { from_swift(ffi::status::OBSERVER_FAILED, err) });
569        }
570        Ok(PlayerInterstitialEventMonitorObserver { token })
571    }
572}
573
574/// Mirrors the `AVPlayer` framework counterpart for `PlayerInterstitialEventController`.
575#[derive(Debug)]
576pub struct PlayerInterstitialEventController {
577    pub(crate) ptr: *mut c_void,
578}
579
580impl Drop for PlayerInterstitialEventController {
581    fn drop(&mut self) {
582        if !self.ptr.is_null() {
583            unsafe { ffi::av_player_interstitial_event_controller_release(self.ptr) };
584            self.ptr = ptr::null_mut();
585        }
586    }
587}
588
589impl PlayerInterstitialEventController {
590    /// Calls the `AVPlayer` framework counterpart for `new`.
591    pub fn new(player: &Player) -> Result<Self, AVPlayerError> {
592        let mut err: *mut c_char = ptr::null_mut();
593        let ptr =
594            unsafe { ffi::av_player_interstitial_event_controller_create(player.ptr, &mut err) };
595        if ptr.is_null() {
596            return Err(unsafe { from_swift(ffi::status::OPERATION_FAILED, err) });
597        }
598        Ok(Self { ptr })
599    }
600
601    /// Calls the `AVPlayer` framework counterpart for `state`.
602    pub fn state(&self) -> Result<PlayerInterstitialEventMonitorState, AVPlayerError> {
603        let mut err: *mut c_char = ptr::null_mut();
604        let json_ptr =
605            unsafe { ffi::av_player_interstitial_event_controller_info_json(self.ptr, &mut err) };
606        if json_ptr.is_null() {
607            return Err(unsafe { from_swift(ffi::status::OPERATION_FAILED, err) });
608        }
609        PlayerInterstitialEventMonitorState::try_from(parse_json_and_free::<
610            PlayerInterstitialMonitorStatePayload,
611        >(json_ptr)?)
612    }
613
614    /// Calls the `AVPlayer` framework counterpart for `set_events`.
615    pub fn set_events(&self, events: &[&PlayerInterstitialEvent]) -> Result<(), AVPlayerError> {
616        let mut err: *mut c_char = ptr::null_mut();
617        let event_ptrs = events.iter().map(|event| event.ptr).collect::<Vec<_>>();
618        let status = unsafe {
619            ffi::av_player_interstitial_event_controller_set_events(
620                self.ptr,
621                event_ptrs.as_ptr(),
622                event_ptrs.len(),
623                &mut err,
624            )
625        };
626        if status != ffi::status::OK {
627            return Err(unsafe { from_swift(status, err) });
628        }
629        Ok(())
630    }
631
632    /// Calls the `AVPlayer` framework counterpart for `cancel_current_event_with_resumption_offset`.
633    pub fn cancel_current_event_with_resumption_offset(&self, value: Time) {
634        let (time_value, timescale, kind) = value.to_raw();
635        unsafe {
636            ffi::av_player_interstitial_event_controller_cancel_current_event_with_resumption_offset(
637                self.ptr,
638                time_value,
639                timescale,
640                kind,
641            );
642        }
643    }
644
645    /// Calls the `AVPlayer` framework counterpart for `skip_current_event`.
646    pub fn skip_current_event(&self) {
647        unsafe { ffi::av_player_interstitial_event_controller_skip_current_event(self.ptr) };
648    }
649}
650
651/// Mirrors the `AVPlayer` framework counterpart for `PlayerInterstitialEventMonitorObserver`.
652#[derive(Debug)]
653pub struct PlayerInterstitialEventMonitorObserver {
654    token: *mut c_void,
655}
656
657impl Drop for PlayerInterstitialEventMonitorObserver {
658    fn drop(&mut self) {
659        if !self.token.is_null() {
660            unsafe { ffi::av_player_interstitial_event_monitor_observer_release(self.token) };
661            self.token = ptr::null_mut();
662        }
663    }
664}
665
666// SAFETY: These AVFoundation interstitial-event handles are safe to transfer
667// across thread boundaries; method calls are internally dispatched safely.
668unsafe impl Send for PlayerInterstitialEvent {}
669unsafe impl Send for PlayerInterstitialEventMonitor {}
670unsafe impl Send for PlayerInterstitialEventController {}
671unsafe impl Send for PlayerInterstitialEventMonitorObserver {}
672
673/// Calls the `AVPlayer` framework counterpart for `player_waiting_during_interstitial_event_reason`.
674pub fn player_waiting_during_interstitial_event_reason() -> Result<String, AVPlayerError> {
675    let mut err: *mut c_char = ptr::null_mut();
676    let string_ptr = unsafe { ffi::av_player_waiting_during_interstitial_event_reason(&mut err) };
677    if string_ptr.is_null() {
678        return Err(unsafe { from_swift(ffi::status::OPERATION_FAILED, err) });
679    }
680    parse_json_and_free::<String>(string_ptr)
681}
682
683fn parse_json_value(value: Option<String>) -> Result<Option<Value>, AVPlayerError> {
684    value
685        .map(|value| {
686            serde_json::from_str::<Value>(&value).map_err(|error| {
687                AVPlayerError::OperationFailed(format!(
688                    "failed to decode interstitial event JSON payload: {error}"
689                ))
690            })
691        })
692        .transpose()
693}
694
695unsafe extern "C" fn player_interstitial_event_monitor_event_trampoline(
696    userdata: *mut c_void,
697    payload_json: *const c_char,
698) {
699    if userdata.is_null() || payload_json.is_null() {
700        return;
701    }
702
703    let callback = &*userdata.cast::<PlayerInterstitialEventMonitorObserverState>();
704    let Ok(payload) = core::ffi::CStr::from_ptr(payload_json).to_str() else {
705        return;
706    };
707    let Ok(payload) = serde_json::from_str::<PlayerInterstitialMonitorEventPayload>(payload) else {
708        return;
709    };
710
711    let interstitial_event = payload
712        .interstitial_event
713        .map(PlayerInterstitialEventInfo::try_from)
714        .transpose()
715        .ok()
716        .flatten();
717
718    let event = match payload.event.as_str() {
719        "events_did_change" => PlayerInterstitialEventMonitorEvent::EventsDidChange,
720        "current_event_did_change" => PlayerInterstitialEventMonitorEvent::CurrentEventDidChange,
721        "asset_list_response_status_did_change" => {
722            PlayerInterstitialEventMonitorEvent::AssetListResponseStatusDidChange {
723                interstitial_event,
724                status: PlayerInterstitialEventAssetListResponseStatus::from_raw(
725                    payload.asset_list_response_status_raw.unwrap_or_default(),
726                ),
727                error_message: payload.error_message,
728            }
729        }
730        "current_event_skippable_state_did_change" => {
731            PlayerInterstitialEventMonitorEvent::CurrentEventSkippableStateDidChange {
732                interstitial_event,
733                state: PlayerInterstitialEventSkippableEventState::from_raw(
734                    payload.skippable_state_raw.unwrap_or_default(),
735                ),
736                skip_control_label: payload.skip_control_label,
737            }
738        }
739        "current_event_skipped" => {
740            PlayerInterstitialEventMonitorEvent::CurrentEventSkipped { interstitial_event }
741        }
742        "interstitial_event_was_unscheduled" => {
743            PlayerInterstitialEventMonitorEvent::InterstitialEventWasUnscheduled {
744                interstitial_event,
745                error_message: payload.error_message,
746            }
747        }
748        "interstitial_event_did_finish" => {
749            PlayerInterstitialEventMonitorEvent::InterstitialEventDidFinish {
750                interstitial_event,
751                playout_time: payload.playout_time,
752                did_play_entire_event: payload.did_play_entire_event,
753            }
754        }
755        _ => return,
756    };
757
758    crate::util::catch_cb_panic("player_interstitial_event_monitor_event_trampoline", || {
759        (callback.callback)(event);
760    });
761}
762
763unsafe extern "C" fn player_interstitial_event_monitor_observer_drop(userdata: *mut c_void) {
764    if !userdata.is_null() {
765        drop(Box::from_raw(
766            userdata.cast::<PlayerInterstitialEventMonitorObserverState>(),
767        ));
768    }
769}
770
771#[cfg(test)]
772mod tests {
773    use super::*;
774    use serde_json::json;
775
776    #[test]
777    fn restrictions_support_bitwise_composition_and_contains() {
778        let restrictions = PlayerInterstitialEventRestrictions::CONSTRAINS_SEEKING_FORWARD_IN_PRIMARY_CONTENT
779            | PlayerInterstitialEventRestrictions::REQUIRES_PLAYBACK_AT_PREFERRED_RATE_FOR_ADVANCEMENT;
780
781        assert!(restrictions.contains(
782            PlayerInterstitialEventRestrictions::CONSTRAINS_SEEKING_FORWARD_IN_PRIMARY_CONTENT,
783        ));
784        assert!(restrictions.contains(
785            PlayerInterstitialEventRestrictions::REQUIRES_PLAYBACK_AT_PREFERRED_RATE_FOR_ADVANCEMENT,
786        ));
787        assert_eq!(restrictions.bits(), (1 << 0) | (1 << 2));
788    }
789
790    #[test]
791    fn cue_round_trips_known_values() {
792        for cue in [
793            PlayerInterstitialEventCue::NoCue,
794            PlayerInterstitialEventCue::JoinCue,
795            PlayerInterstitialEventCue::LeaveCue,
796        ] {
797            assert_eq!(PlayerInterstitialEventCue::from_raw(cue.as_raw()), cue);
798        }
799    }
800
801    #[test]
802    fn cue_preserves_unknown_values() {
803        let cue = PlayerInterstitialEventCue::from_raw("custom_cue");
804
805        assert_eq!(
806            cue,
807            PlayerInterstitialEventCue::Unknown("custom_cue".into())
808        );
809        assert_eq!(cue.as_raw(), "custom_cue");
810    }
811
812    #[test]
813    fn timeline_occupancy_round_trips_known_and_unknown_values() {
814        for (raw, occupancy) in [
815            (0, PlayerInterstitialEventTimelineOccupancy::SinglePoint),
816            (1, PlayerInterstitialEventTimelineOccupancy::Fill),
817            (9, PlayerInterstitialEventTimelineOccupancy::Unknown(9)),
818        ] {
819            assert_eq!(
820                PlayerInterstitialEventTimelineOccupancy::from_raw(raw),
821                occupancy
822            );
823            assert_eq!(occupancy.raw(), raw);
824        }
825    }
826
827    #[test]
828    fn parse_json_value_decodes_json_objects() {
829        let value = parse_json_value(Some(r#"{"kind":"midroll"}"#.into())).unwrap();
830
831        assert_eq!(value, Some(json!({"kind": "midroll"})));
832    }
833
834    #[test]
835    fn parse_json_value_rejects_invalid_json() {
836        let error = parse_json_value(Some("{".into())).unwrap_err();
837
838        assert!(matches!(
839            error,
840            AVPlayerError::OperationFailed(ref message)
841                if message.starts_with("failed to decode interstitial event JSON payload:")
842        ));
843    }
844
845    #[test]
846    fn payload_conversion_maps_json_and_enum_fields() {
847        let payload = PlayerInterstitialEventInfoPayload {
848            identifier: "midroll".into(),
849            time: Time::new(90, 1),
850            date: Some("2026-05-20T12:00:00Z".into()),
851            template_item_count: 2,
852            restrictions:
853                PlayerInterstitialEventRestrictions::CONSTRAINS_SEEKING_FORWARD_IN_PRIMARY_CONTENT
854                    .bits(),
855            resumption_offset: Time::new(3, 1),
856            playout_limit: Time::new(30, 1),
857            aligns_start_with_primary_segment_boundary: true,
858            aligns_resumption_with_primary_segment_boundary: false,
859            cue: Some("join_cue".into()),
860            will_play_once: true,
861            user_defined_attributes_json: Some(r#"{"kind":"midroll"}"#.into()),
862            asset_list_response_json: Some(r#"{"status":"available"}"#.into()),
863            timeline_occupancy_raw: Some(1),
864            supplements_primary_content: Some(true),
865            content_may_vary: Some(false),
866            has_primary_item: true,
867        };
868
869        let info = PlayerInterstitialEventInfo::try_from(payload).unwrap();
870
871        assert_eq!(info.identifier, "midroll");
872        assert_eq!(info.time, Time::new(90, 1));
873        assert_eq!(
874            info.restrictions,
875            PlayerInterstitialEventRestrictions::CONSTRAINS_SEEKING_FORWARD_IN_PRIMARY_CONTENT,
876        );
877        assert_eq!(info.cue, Some(PlayerInterstitialEventCue::JoinCue));
878        assert_eq!(
879            info.user_defined_attributes,
880            Some(json!({"kind": "midroll"}))
881        );
882        assert_eq!(
883            info.asset_list_response,
884            Some(json!({"status": "available"})),
885        );
886        assert_eq!(
887            info.timeline_occupancy,
888            Some(PlayerInterstitialEventTimelineOccupancy::Fill),
889        );
890        assert_eq!(info.supplements_primary_content, Some(true));
891        assert_eq!(info.content_may_vary, Some(false));
892        assert!(info.has_primary_item);
893    }
894}