use crate::error::Result;
use alloc::vec::Vec;
use scte35_splice::SpliceInfoSection;
use scte35_splice::commands::AnyCommand;
use scte35_splice::commands::SpliceInsert;
use scte35_splice::time::{BreakDuration, SpliceTime};
pub use ssai_runtime::splice::ConditionedSplicePoint;
use ssai_runtime::splice::PTS_MODULUS_33;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum BreakEdge {
Enter,
Return,
}
impl BreakEdge {
pub fn name(&self) -> &'static str {
match self {
BreakEdge::Enter => "enter",
BreakEdge::Return => "return",
}
}
}
broadcast_common::impl_spec_display!(BreakEdge);
pub fn build_splice_insert(
edge: BreakEdge,
splice_event_id: u32,
requested_pts: u64,
candidates: &[u64],
max_delta_ticks: u64,
break_duration_ticks: Option<u64>,
) -> Result<(ConditionedSplicePoint, SpliceInsert)> {
let reduced: Vec<u64> = candidates.iter().map(|c| c % PTS_MODULUS_33).collect();
let mut conditioned = ssai_runtime::splice::condition_splice_point_wrapping(
requested_pts % PTS_MODULUS_33,
&reduced,
max_delta_ticks,
PTS_MODULUS_33,
)?;
if let Some(original) = candidates
.iter()
.zip(&reduced)
.find_map(|(orig, red)| (*red == conditioned.snapped_pts).then_some(*orig))
{
conditioned.snapped_pts = original;
}
conditioned.requested_pts = requested_pts;
let insert = SpliceInsert {
splice_event_id,
splice_event_cancel_indicator: false,
out_of_network_indicator: matches!(edge, BreakEdge::Enter),
program_splice_flag: true,
splice_immediate_flag: false,
event_id_compliance_flag: true,
splice_time: Some(SpliceTime::with_pts(conditioned.snapped_pts)),
components: Vec::new(),
break_duration: break_duration_ticks.map(|duration| BreakDuration {
auto_return: true,
duration,
}),
unique_program_id: 0,
avail_num: 0,
avails_expected: 0,
};
Ok((conditioned, insert))
}
#[must_use]
pub fn to_section<'a>(insert: SpliceInsert) -> SpliceInfoSection<'a> {
SpliceInfoSection::new_clear(AnyCommand::SpliceInsert(insert), &[])
}
#[cfg(test)]
mod tests {
use super::*;
use broadcast_common::{Parse, Serialize};
use ssai_runtime::Error as SsaiError;
#[test]
fn builds_an_enter_break_cue_snapped_to_the_conditioned_point() {
let (conditioned, insert) = build_splice_insert(
BreakEdge::Enter,
7,
1_000,
&[1_050, 2_000],
100,
Some(1_800_000),
)
.unwrap();
assert_eq!(conditioned.snapped_pts, 1_050);
assert_eq!(insert.splice_event_id, 7);
assert!(insert.out_of_network_indicator);
assert_eq!(insert.splice_time.unwrap().pts_time, Some(1_050));
assert_eq!(insert.break_duration.unwrap().duration, 1_800_000);
assert!(insert.break_duration.unwrap().auto_return);
assert_ne!(insert.splice_time.unwrap().pts_time, Some(1_000));
}
#[test]
fn builds_a_return_break_cue_with_no_duration() {
let (_, insert) =
build_splice_insert(BreakEdge::Return, 8, 1_000, &[1_000], 0, None).unwrap();
assert!(!insert.out_of_network_indicator);
assert!(insert.break_duration.is_none());
}
#[test]
fn rejects_a_cue_with_no_boundary_within_tolerance() {
let err = build_splice_insert(BreakEdge::Enter, 1, 1_000, &[5_000], 10, None).unwrap_err();
match err {
crate::Error::SpliceConditioning(SsaiError::NoAlignedBoundary {
requested_pts,
tolerance_ticks,
nearest_delta_ticks,
}) => {
assert_eq!(requested_pts, 1_000);
assert_eq!(tolerance_ticks, 10);
assert_eq!(nearest_delta_ticks, 4_000);
}
other => panic!("expected SpliceConditioning(NoAlignedBoundary), got {other:?}"),
}
}
#[test]
fn section_round_trips_through_scte35_splices_own_serialize() {
let (_, insert) =
build_splice_insert(BreakEdge::Enter, 42, 1_000, &[1_000], 0, Some(900_000)).unwrap();
let section = to_section(insert);
let bytes = section.try_to_bytes().unwrap();
assert_eq!(bytes[0], scte35_splice::section::TABLE_ID);
let parsed = SpliceInfoSection::parse(&bytes).unwrap();
match parsed.clear.unwrap().command {
AnyCommand::SpliceInsert(reparsed) => {
assert_eq!(reparsed.splice_event_id, 42);
assert_eq!(reparsed.splice_time.unwrap().pts_time, Some(1_000));
assert_eq!(reparsed.break_duration.unwrap().duration, 900_000);
}
other => panic!("expected SpliceInsert, got {other:?}"),
}
}
#[test]
fn label_convention() {
assert_eq!(BreakEdge::Enter.name(), "enter");
assert_eq!(alloc::format!("{}", BreakEdge::Return), "return");
}
#[test]
fn a_cue_before_the_33_bit_wrap_snaps_to_a_boundary_after_it() {
let requested = PTS_MODULUS_33 - 100;
let (conditioned, insert) =
build_splice_insert(BreakEdge::Enter, 9, requested, &[50], 1_000, None).unwrap();
assert_eq!(conditioned.snapped_pts, 50);
assert_eq!(conditioned.delta_ticks, 150);
assert_eq!(conditioned.direction, ssai_runtime::SnapDirection::After);
assert_eq!(insert.splice_time.unwrap().pts_time, Some(50));
assert!(matches!(
build_splice_insert(BreakEdge::Enter, 9, requested, &[50], 149, None),
Err(crate::Error::SpliceConditioning(
SsaiError::NoAlignedBoundary {
nearest_delta_ticks: 150,
..
}
))
));
}
#[test]
fn an_unwrapped_channel_clock_is_reduced_to_the_33_bit_circle() {
let requested = PTS_MODULUS_33 + 10;
let (conditioned, _) = build_splice_insert(
BreakEdge::Return,
1,
requested,
&[PTS_MODULUS_33 - 5],
1_000,
None,
)
.unwrap();
assert_eq!(conditioned.requested_pts, requested);
assert_eq!(conditioned.snapped_pts, PTS_MODULUS_33 - 5);
assert_eq!(conditioned.delta_ticks, 15);
assert_eq!(conditioned.direction, ssai_runtime::SnapDirection::Before);
}
}