use crate::error::{Error, Result};
use alloc::string::String;
use alloc::vec::Vec;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub enum EntryKind {
Programme,
Ad,
Slate,
}
impl EntryKind {
pub fn name(&self) -> &'static str {
match self {
EntryKind::Programme => "programme",
EntryKind::Ad => "ad",
EntryKind::Slate => "slate",
}
}
}
broadcast_common::impl_spec_display!(EntryKind);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct CodecConfigId(pub u64);
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ScheduleEntry {
pub id: String,
pub kind: EntryKind,
pub planned_start: u64,
pub source_start_pts: u64,
pub codec_config: CodecConfigId,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Schedule {
entries: Vec<ScheduleEntry>,
}
impl Schedule {
#[must_use]
pub fn new() -> Self {
Schedule {
entries: Vec::new(),
}
}
pub fn push(&mut self, entry: ScheduleEntry) -> Result<()> {
if let Some(last) = self.entries.last()
&& entry.planned_start <= last.planned_start
{
return Err(Error::OutOfOrder {
prev: last.planned_start,
next: entry.planned_start,
});
}
self.entries.push(entry);
Ok(())
}
#[must_use]
pub fn entries(&self) -> &[ScheduleEntry] {
&self.entries
}
#[must_use]
pub fn len(&self) -> usize {
self.entries.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
#[must_use]
pub fn active_at(&self, pts: u64) -> Option<&ScheduleEntry> {
self.active_index_at(pts).map(|i| &self.entries[i])
}
pub(crate) fn active_index_at(&self, pts: u64) -> Option<usize> {
if self.entries.is_empty() {
return None;
}
let idx = self.entries.partition_point(|e| e.planned_start <= pts);
if idx == 0 { None } else { Some(idx - 1) }
}
}
#[cfg(test)]
mod tests {
use super::*;
fn entry(id: &str, kind: EntryKind, planned_start: u64) -> ScheduleEntry {
ScheduleEntry {
id: id.into(),
kind,
planned_start,
source_start_pts: 0,
codec_config: CodecConfigId(1),
}
}
#[test]
fn push_accepts_strictly_increasing_starts() {
let mut sched = Schedule::new();
sched
.push(entry("prog-1", EntryKind::Programme, 0))
.unwrap();
sched.push(entry("ad-1", EntryKind::Ad, 1_000)).unwrap();
assert_eq!(sched.len(), 2);
}
#[test]
fn push_rejects_non_increasing_start() {
let mut sched = Schedule::new();
sched
.push(entry("prog-1", EntryKind::Programme, 1_000))
.unwrap();
let err = sched.push(entry("ad-1", EntryKind::Ad, 1_000)).unwrap_err();
match err {
Error::OutOfOrder { prev, next } => {
assert_eq!(prev, 1_000);
assert_eq!(next, 1_000);
}
other => panic!("expected OutOfOrder, got {other:?}"),
}
let err = sched.push(entry("ad-2", EntryKind::Ad, 500)).unwrap_err();
assert!(matches!(
err,
Error::OutOfOrder {
prev: 1_000,
next: 500
}
));
}
#[test]
fn active_at_finds_the_last_entry_starting_at_or_before_pts() {
let mut sched = Schedule::new();
sched
.push(entry("prog-1", EntryKind::Programme, 0))
.unwrap();
sched.push(entry("ad-1", EntryKind::Ad, 1_000)).unwrap();
sched
.push(entry("prog-2", EntryKind::Programme, 2_000))
.unwrap();
assert_eq!(sched.active_at(0).unwrap().id, "prog-1");
assert_eq!(sched.active_at(999).unwrap().id, "prog-1");
assert_eq!(sched.active_at(1_000).unwrap().id, "ad-1");
assert_eq!(sched.active_at(1_999).unwrap().id, "ad-1");
assert_eq!(sched.active_at(2_000).unwrap().id, "prog-2");
assert_eq!(sched.active_at(1_000_000).unwrap().id, "prog-2");
}
#[test]
fn active_at_before_the_first_entry_is_none() {
let mut sched = Schedule::new();
sched
.push(entry("prog-1", EntryKind::Programme, 1_000))
.unwrap();
assert!(sched.active_at(999).is_none());
}
#[test]
fn empty_schedule_has_no_active_entry() {
let sched = Schedule::new();
assert!(sched.is_empty());
assert!(sched.active_at(0).is_none());
}
#[test]
fn label_convention() {
assert_eq!(EntryKind::Programme.name(), "programme");
assert_eq!(alloc::format!("{}", EntryKind::Ad), "ad");
assert_eq!(EntryKind::Slate.name(), "slate");
}
}