playout_runtime/
scte35.rs1use crate::error::Result;
15use alloc::vec::Vec;
16use scte35_splice::SpliceInfoSection;
17use scte35_splice::commands::AnyCommand;
18use scte35_splice::commands::SpliceInsert;
19use scte35_splice::time::{BreakDuration, SpliceTime};
20pub use ssai_runtime::splice::ConditionedSplicePoint;
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29#[non_exhaustive]
30pub enum BreakEdge {
31 Enter,
33 Return,
35}
36
37impl BreakEdge {
38 pub fn name(&self) -> &'static str {
40 match self {
41 BreakEdge::Enter => "enter",
42 BreakEdge::Return => "return",
43 }
44 }
45}
46broadcast_common::impl_spec_display!(BreakEdge);
47
48pub fn build_splice_insert(
60 edge: BreakEdge,
61 splice_event_id: u32,
62 requested_pts: u64,
63 candidates: &[u64],
64 max_delta_ticks: u64,
65 break_duration_ticks: Option<u64>,
66) -> Result<(ConditionedSplicePoint, SpliceInsert)> {
67 let conditioned =
68 ssai_runtime::splice::condition_splice_point(requested_pts, candidates, max_delta_ticks)?;
69 let insert = SpliceInsert {
70 splice_event_id,
71 splice_event_cancel_indicator: false,
72 out_of_network_indicator: matches!(edge, BreakEdge::Enter),
73 program_splice_flag: true,
74 splice_immediate_flag: false,
75 event_id_compliance_flag: true,
76 splice_time: Some(SpliceTime::with_pts(conditioned.snapped_pts)),
77 components: Vec::new(),
78 break_duration: break_duration_ticks.map(|duration| BreakDuration {
79 auto_return: true,
80 duration,
81 }),
82 unique_program_id: 0,
83 avail_num: 0,
84 avails_expected: 0,
85 };
86 Ok((conditioned, insert))
87}
88
89#[must_use]
94pub fn to_section<'a>(insert: SpliceInsert) -> SpliceInfoSection<'a> {
95 SpliceInfoSection::new_clear(AnyCommand::SpliceInsert(insert), &[])
96}
97
98#[cfg(test)]
99mod tests {
100 use super::*;
101 use broadcast_common::{Parse, Serialize};
102 use ssai_runtime::Error as SsaiError;
103
104 #[test]
105 fn builds_an_enter_break_cue_snapped_to_the_conditioned_point() {
106 let (conditioned, insert) = build_splice_insert(
107 BreakEdge::Enter,
108 7,
109 1_000,
110 &[1_050, 2_000],
111 100,
112 Some(1_800_000),
113 )
114 .unwrap();
115 assert_eq!(conditioned.snapped_pts, 1_050);
116 assert_eq!(insert.splice_event_id, 7);
117 assert!(insert.out_of_network_indicator);
118 assert_eq!(insert.splice_time.unwrap().pts_time, Some(1_050));
119 assert_eq!(insert.break_duration.unwrap().duration, 1_800_000);
120 assert!(insert.break_duration.unwrap().auto_return);
121
122 assert_ne!(insert.splice_time.unwrap().pts_time, Some(1_000));
125 }
126
127 #[test]
128 fn builds_a_return_break_cue_with_no_duration() {
129 let (_, insert) =
130 build_splice_insert(BreakEdge::Return, 8, 1_000, &[1_000], 0, None).unwrap();
131 assert!(!insert.out_of_network_indicator);
132 assert!(insert.break_duration.is_none());
133 }
134
135 #[test]
136 fn rejects_a_cue_with_no_boundary_within_tolerance() {
137 let err = build_splice_insert(BreakEdge::Enter, 1, 1_000, &[5_000], 10, None).unwrap_err();
138 match err {
139 crate::Error::SpliceConditioning(SsaiError::NoAlignedBoundary {
140 requested_pts,
141 tolerance_ticks,
142 nearest_delta_ticks,
143 }) => {
144 assert_eq!(requested_pts, 1_000);
145 assert_eq!(tolerance_ticks, 10);
146 assert_eq!(nearest_delta_ticks, 4_000);
147 }
148 other => panic!("expected SpliceConditioning(NoAlignedBoundary), got {other:?}"),
149 }
150 }
151
152 #[test]
153 fn section_round_trips_through_scte35_splices_own_serialize() {
154 let (_, insert) =
155 build_splice_insert(BreakEdge::Enter, 42, 1_000, &[1_000], 0, Some(900_000)).unwrap();
156 let section = to_section(insert);
157 let bytes = section.to_bytes();
158 assert_eq!(bytes[0], scte35_splice::section::TABLE_ID);
159
160 let parsed = SpliceInfoSection::parse(&bytes).unwrap();
161 match parsed.clear.unwrap().command {
162 AnyCommand::SpliceInsert(reparsed) => {
163 assert_eq!(reparsed.splice_event_id, 42);
164 assert_eq!(reparsed.splice_time.unwrap().pts_time, Some(1_000));
165 assert_eq!(reparsed.break_duration.unwrap().duration, 900_000);
166 }
167 other => panic!("expected SpliceInsert, got {other:?}"),
168 }
169 }
170
171 #[test]
172 fn label_convention() {
173 assert_eq!(BreakEdge::Enter.name(), "enter");
174 assert_eq!(alloc::format!("{}", BreakEdge::Return), "return");
175 }
176}