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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
23pub struct PlayerInterstitialEventRestrictions(u64);
24
25impl PlayerInterstitialEventRestrictions {
26 pub const NONE: Self = Self(0);
28 pub const CONSTRAINS_SEEKING_FORWARD_IN_PRIMARY_CONTENT: Self = Self(1 << 0);
30 pub const REQUIRES_PLAYBACK_AT_PREFERRED_RATE_FOR_ADVANCEMENT: Self = Self(1 << 2);
32 pub const DEFAULT_POLICY: Self = Self::NONE;
34
35 pub const fn bits(self) -> u64 {
37 self.0
38 }
39
40 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#[derive(Debug, Clone, PartialEq, Eq, Hash)]
66#[non_exhaustive]
67pub enum PlayerInterstitialEventCue {
68 NoCue,
70 JoinCue,
72 LeaveCue,
74 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
100#[non_exhaustive]
101pub enum PlayerInterstitialEventTimelineOccupancy {
102 SinglePoint,
104 Fill,
106 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
130#[non_exhaustive]
131pub enum PlayerInterstitialEventAssetListResponseStatus {
132 Available,
134 Cleared,
136 Unavailable,
138 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
155#[non_exhaustive]
156pub enum PlayerInterstitialEventSkippableEventState {
157 NotSkippable,
159 NotYetEligible,
161 Eligible,
163 NoLongerEligible,
165 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#[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#[derive(Debug, Clone, PartialEq)]
206pub struct PlayerInterstitialEventInfo {
207 pub identifier: String,
209 pub time: Time,
211 pub date: Option<String>,
213 pub template_item_count: usize,
215 pub restrictions: PlayerInterstitialEventRestrictions,
217 pub resumption_offset: Time,
219 pub playout_limit: Time,
221 pub aligns_start_with_primary_segment_boundary: bool,
223 pub aligns_resumption_with_primary_segment_boundary: bool,
225 pub cue: Option<PlayerInterstitialEventCue>,
227 pub will_play_once: bool,
229 pub user_defined_attributes: Option<Value>,
231 pub asset_list_response: Option<Value>,
233 pub timeline_occupancy: Option<PlayerInterstitialEventTimelineOccupancy>,
235 pub supplements_primary_content: Option<bool>,
237 pub content_may_vary: Option<bool>,
239 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#[derive(Debug, Clone, PartialEq)]
287pub struct PlayerInterstitialEventMonitorState {
288 pub events: Vec<PlayerInterstitialEventInfo>,
290 pub current_event: Option<PlayerInterstitialEventInfo>,
292 pub current_event_skippable_state: Option<PlayerInterstitialEventSkippableEventState>,
294 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#[derive(Debug, Clone, PartialEq)]
335#[non_exhaustive]
336pub enum PlayerInterstitialEventMonitorEvent {
337 EventsDidChange,
339 CurrentEventDidChange,
341 AssetListResponseStatusDidChange {
343 interstitial_event: Option<PlayerInterstitialEventInfo>,
344 status: PlayerInterstitialEventAssetListResponseStatus,
345 error_message: Option<String>,
346 },
347 CurrentEventSkippableStateDidChange {
349 interstitial_event: Option<PlayerInterstitialEventInfo>,
350 state: PlayerInterstitialEventSkippableEventState,
351 skip_control_label: Option<String>,
352 },
353 CurrentEventSkipped {
355 interstitial_event: Option<PlayerInterstitialEventInfo>,
356 },
357 InterstitialEventWasUnscheduled {
359 interstitial_event: Option<PlayerInterstitialEventInfo>,
360 error_message: Option<String>,
361 },
362 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#[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 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 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 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 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 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 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 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 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 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 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 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 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 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#[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 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 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 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#[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 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 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 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 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 pub fn skip_current_event(&self) {
647 unsafe { ffi::av_player_interstitial_event_controller_skip_current_event(self.ptr) };
648 }
649}
650
651#[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
666unsafe impl Send for PlayerInterstitialEvent {}
669unsafe impl Send for PlayerInterstitialEventMonitor {}
670unsafe impl Send for PlayerInterstitialEventController {}
671unsafe impl Send for PlayerInterstitialEventMonitorObserver {}
672
673pub 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}