1use 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;
21use ssai_runtime::splice::PTS_MODULUS_33;
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30#[non_exhaustive]
31pub enum BreakEdge {
32 Enter,
34 Return,
36}
37
38impl BreakEdge {
39 pub fn name(&self) -> &'static str {
41 match self {
42 BreakEdge::Enter => "enter",
43 BreakEdge::Return => "return",
44 }
45 }
46}
47broadcast_common::impl_spec_display!(BreakEdge);
48
49pub fn build_splice_insert(
81 edge: BreakEdge,
82 splice_event_id: u32,
83 requested_pts: u64,
84 candidates: &[u64],
85 max_delta_ticks: u64,
86 break_duration_ticks: Option<u64>,
87) -> Result<(ConditionedSplicePoint, SpliceInsert)> {
88 if let Some(ticks) = break_duration_ticks
89 && ticks >= PTS_MODULUS_33
90 {
91 return Err(crate::Error::BreakDurationTooLong { ticks });
92 }
93 let reduced: Vec<u64> = candidates.iter().map(|c| c % PTS_MODULUS_33).collect();
101 let mut conditioned = ssai_runtime::splice::condition_splice_point_wrapping(
102 requested_pts % PTS_MODULUS_33,
103 &reduced,
104 max_delta_ticks,
105 PTS_MODULUS_33,
106 )?;
107 if let Some(original) = candidates
111 .iter()
112 .zip(&reduced)
113 .find_map(|(orig, red)| (*red == conditioned.snapped_pts).then_some(*orig))
114 {
115 conditioned.snapped_pts = original;
116 }
117 conditioned.requested_pts = requested_pts;
118 let insert = SpliceInsert {
119 splice_event_id,
120 splice_event_cancel_indicator: false,
121 out_of_network_indicator: matches!(edge, BreakEdge::Enter),
122 program_splice_flag: true,
123 splice_immediate_flag: false,
124 event_id_compliance_flag: true,
125 splice_time: Some(SpliceTime::with_pts(conditioned.snapped_pts)),
126 components: Vec::new(),
127 break_duration: break_duration_ticks.map(|duration| BreakDuration {
128 auto_return: true,
129 duration,
130 }),
131 unique_program_id: 0,
132 avail_num: 0,
133 avails_expected: 0,
134 };
135 Ok((conditioned, insert))
136}
137
138#[must_use]
143pub fn to_section<'a>(insert: SpliceInsert) -> SpliceInfoSection<'a> {
144 SpliceInfoSection::new_clear(AnyCommand::SpliceInsert(insert), &[])
145}
146
147#[cfg(test)]
148mod tests {
149 use super::*;
150 use broadcast_common::{Parse, Serialize};
151 use ssai_runtime::Error as SsaiError;
152
153 #[test]
154 fn builds_an_enter_break_cue_snapped_to_the_conditioned_point() {
155 let (conditioned, insert) = build_splice_insert(
156 BreakEdge::Enter,
157 7,
158 1_000,
159 &[1_050, 2_000],
160 100,
161 Some(1_800_000),
162 )
163 .unwrap();
164 assert_eq!(conditioned.snapped_pts, 1_050);
165 assert_eq!(insert.splice_event_id, 7);
166 assert!(insert.out_of_network_indicator);
167 assert_eq!(insert.splice_time.unwrap().pts_time, Some(1_050));
168 assert_eq!(insert.break_duration.unwrap().duration, 1_800_000);
169 assert!(insert.break_duration.unwrap().auto_return);
170
171 assert_ne!(insert.splice_time.unwrap().pts_time, Some(1_000));
174 }
175
176 #[test]
177 fn a_break_duration_past_33_bits_is_rejected_at_construction() {
178 let max = PTS_MODULUS_33 - 1;
179 assert!(build_splice_insert(BreakEdge::Enter, 1, 1_000, &[1_000], 0, Some(max)).is_ok());
180 let err = build_splice_insert(
181 BreakEdge::Enter,
182 1,
183 1_000,
184 &[1_000],
185 0,
186 Some(PTS_MODULUS_33),
187 )
188 .unwrap_err();
189 assert!(matches!(
190 err,
191 crate::Error::BreakDurationTooLong { ticks } if ticks == PTS_MODULUS_33
192 ));
193 }
194
195 #[test]
196 fn builds_a_return_break_cue_with_no_duration() {
197 let (_, insert) =
198 build_splice_insert(BreakEdge::Return, 8, 1_000, &[1_000], 0, None).unwrap();
199 assert!(!insert.out_of_network_indicator);
200 assert!(insert.break_duration.is_none());
201 }
202
203 #[test]
204 fn rejects_a_cue_with_no_boundary_within_tolerance() {
205 let err = build_splice_insert(BreakEdge::Enter, 1, 1_000, &[5_000], 10, None).unwrap_err();
206 match err {
207 crate::Error::SpliceConditioning(SsaiError::NoAlignedBoundary {
208 requested_pts,
209 tolerance_ticks,
210 nearest_delta_ticks,
211 }) => {
212 assert_eq!(requested_pts, 1_000);
213 assert_eq!(tolerance_ticks, 10);
214 assert_eq!(nearest_delta_ticks, 4_000);
215 }
216 other => panic!("expected SpliceConditioning(NoAlignedBoundary), got {other:?}"),
217 }
218 }
219
220 #[test]
221 fn section_round_trips_through_scte35_splices_own_serialize() {
222 let (_, insert) =
223 build_splice_insert(BreakEdge::Enter, 42, 1_000, &[1_000], 0, Some(900_000)).unwrap();
224 let section = to_section(insert);
225 let bytes = section.try_to_bytes().unwrap();
226 assert_eq!(bytes[0], scte35_splice::section::TABLE_ID);
227
228 let parsed = SpliceInfoSection::parse(&bytes).unwrap();
229 match parsed.clear.unwrap().command {
230 AnyCommand::SpliceInsert(reparsed) => {
231 assert_eq!(reparsed.splice_event_id, 42);
232 assert_eq!(reparsed.splice_time.unwrap().pts_time, Some(1_000));
233 assert_eq!(reparsed.break_duration.unwrap().duration, 900_000);
234 }
235 other => panic!("expected SpliceInsert, got {other:?}"),
236 }
237 }
238
239 #[test]
240 fn label_convention() {
241 assert_eq!(BreakEdge::Enter.name(), "enter");
242 assert_eq!(alloc::format!("{}", BreakEdge::Return), "return");
243 }
244
245 #[test]
249 fn a_cue_before_the_33_bit_wrap_snaps_to_a_boundary_after_it() {
250 let requested = PTS_MODULUS_33 - 100;
251 let (conditioned, insert) =
252 build_splice_insert(BreakEdge::Enter, 9, requested, &[50], 1_000, None).unwrap();
253 assert_eq!(conditioned.snapped_pts, 50);
254 assert_eq!(conditioned.delta_ticks, 150);
255 assert_eq!(conditioned.direction, ssai_runtime::SnapDirection::After);
256 assert_eq!(insert.splice_time.unwrap().pts_time, Some(50));
257 assert!(matches!(
259 build_splice_insert(BreakEdge::Enter, 9, requested, &[50], 149, None),
260 Err(crate::Error::SpliceConditioning(
261 SsaiError::NoAlignedBoundary {
262 nearest_delta_ticks: 150,
263 ..
264 }
265 ))
266 ));
267 }
268
269 #[test]
273 fn an_unwrapped_channel_clock_is_reduced_to_the_33_bit_circle() {
274 let requested = PTS_MODULUS_33 + 10;
275 let (conditioned, _) = build_splice_insert(
276 BreakEdge::Return,
277 1,
278 requested,
279 &[PTS_MODULUS_33 - 5],
280 1_000,
281 None,
282 )
283 .unwrap();
284 assert_eq!(conditioned.requested_pts, requested);
286 assert_eq!(conditioned.snapped_pts, PTS_MODULUS_33 - 5);
287 assert_eq!(conditioned.delta_ticks, 15);
288 assert_eq!(conditioned.direction, ssai_runtime::SnapDirection::Before);
289 }
290}