use crate::schedule::{Schedule, ScheduleEntry};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TransitionPlan {
pub at_pts: u64,
pub pts_rebase_offset: i128,
pub discontinuity: bool,
}
impl TransitionPlan {
#[must_use]
pub fn plan(from: &ScheduleEntry, to: &ScheduleEntry) -> Self {
let at_pts = to.planned_start;
let pts_rebase_offset = i128::from(at_pts) - i128::from(to.source_start_pts);
TransitionPlan {
at_pts,
pts_rebase_offset,
discontinuity: from.codec_config != to.codec_config,
}
}
#[must_use]
pub fn rebase(&self, source_pts: u64) -> Option<u64> {
let rebased = i128::from(source_pts) + self.pts_rebase_offset;
u64::try_from(rebased).ok()
}
#[must_use]
pub fn rebase_wrapped(&self, raw_pts: u64, last_unwrapped_pts: u64) -> Option<u64> {
use broadcast_common::clock33::{WRAP_33BIT, unwrap_delta};
if raw_pts >= WRAP_33BIT {
return None;
}
let prev_raw = last_unwrapped_pts % WRAP_33BIT;
let unwrapped = unwrap_delta(i128::from(last_unwrapped_pts), prev_raw, raw_pts);
u64::try_from(unwrapped + self.pts_rebase_offset).ok()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PlannedTransition<'a> {
pub from: &'a ScheduleEntry,
pub to: &'a ScheduleEntry,
pub plan: TransitionPlan,
}
#[must_use]
pub fn next_transition(schedule: &Schedule, now_pts: u64) -> Option<PlannedTransition<'_>> {
let idx = schedule.active_index_at(now_pts)?;
let entries = schedule.entries();
let to = entries.get(idx + 1)?;
let from = &entries[idx];
Some(PlannedTransition {
from,
to,
plan: TransitionPlan::plan(from, to),
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::schedule::{CodecConfigId, EntryKind, ScheduleEntry};
fn entry(
id: &str,
kind: EntryKind,
planned_start: u64,
source_start_pts: u64,
codec_config: u64,
) -> ScheduleEntry {
ScheduleEntry {
id: id.into(),
kind,
planned_start,
source_start_pts,
codec_config: CodecConfigId(codec_config),
}
}
#[test]
fn rebase_lands_the_incoming_source_continuously_on_the_channel_timeline() {
let programme = entry("prog-1", EntryKind::Programme, 0, 0, 1);
let ad = entry("ad-1", EntryKind::Ad, 1_000_000, 5_000, 1);
let plan = TransitionPlan::plan(&programme, &ad);
assert_eq!(plan.at_pts, 1_000_000);
assert_eq!(plan.rebase(5_000), Some(1_000_000));
assert_eq!(plan.rebase(5_000 + 90_000), Some(1_090_000));
assert_eq!(plan.rebase(5_000 + 900_000), Some(1_900_000));
}
#[test]
fn rebase_offset_can_be_negative_when_source_starts_after_the_join_instant() {
let from = entry("prog-1", EntryKind::Programme, 0, 0, 1);
let to = entry("ad-1", EntryKind::Ad, 100, 500, 1);
let plan = TransitionPlan::plan(&from, &to);
assert_eq!(plan.pts_rebase_offset, -400);
assert_eq!(plan.rebase(500), Some(100));
assert_eq!(plan.rebase(300), None);
}
#[test]
fn rebase_returns_none_rather_than_wrapping_out_of_range() {
let from = entry("prog-1", EntryKind::Programme, 0, 0, 1);
let to = entry("ad-1", EntryKind::Ad, 0, u64::MAX, 1);
let plan = TransitionPlan::plan(&from, &to);
assert_eq!(plan.rebase(u64::MAX), Some(0));
assert_eq!(plan.rebase(0), None);
}
#[test]
fn discontinuity_flags_a_codec_config_change_across_the_join() {
let from = entry("prog-1", EntryKind::Programme, 0, 0, 1);
let same = entry("prog-2", EntryKind::Programme, 1_000, 0, 1);
let different = entry("ad-1", EntryKind::Ad, 1_000, 0, 2);
assert!(!TransitionPlan::plan(&from, &same).discontinuity);
assert!(TransitionPlan::plan(&from, &different).discontinuity);
}
#[test]
fn next_transition_walks_the_schedule() {
let mut sched = Schedule::default();
sched
.push(entry("prog-1", EntryKind::Programme, 0, 0, 1))
.unwrap();
sched
.push(entry("ad-1", EntryKind::Ad, 1_000, 0, 2))
.unwrap();
sched
.push(entry("prog-2", EntryKind::Programme, 2_000, 0, 1))
.unwrap();
let t = next_transition(&sched, 500).unwrap();
assert_eq!(t.from.id, "prog-1");
assert_eq!(t.to.id, "ad-1");
assert!(t.plan.discontinuity);
let t = next_transition(&sched, 1_500).unwrap();
assert_eq!(t.from.id, "ad-1");
assert_eq!(t.to.id, "prog-2");
assert!(next_transition(&sched, 2_500).is_none());
assert!(next_transition(&sched, 0).is_some());
}
#[test]
fn rebase_wrapped_handles_a_source_pts_wrap() {
use broadcast_common::clock33::WRAP_33BIT;
let start = WRAP_33BIT - 90_000;
let from = entry("prog-1", EntryKind::Programme, 0, 0, 1);
let to = entry("prog-2", EntryKind::Programme, 1_000_000, start, 1);
let plan = TransitionPlan::plan(&from, &to);
assert_eq!(plan.rebase_wrapped(start, start), Some(1_000_000));
assert_eq!(
plan.rebase_wrapped(90_000, start),
Some(1_000_000 + 180_000)
);
assert_ne!(plan.rebase(90_000), Some(1_180_000));
assert_eq!(
plan.rebase_wrapped(180_000, WRAP_33BIT + 90_000),
Some(1_000_000 + 270_000)
);
assert_eq!(plan.rebase_wrapped(WRAP_33BIT, start), None);
}
}