1use crate::error::{Error, Result};
15use alloc::string::String;
16use alloc::vec::Vec;
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
21#[non_exhaustive]
22pub enum EntryKind {
23 Programme,
25 Ad,
27 Slate,
30}
31
32impl EntryKind {
33 pub fn name(&self) -> &'static str {
35 match self {
36 EntryKind::Programme => "programme",
37 EntryKind::Ad => "ad",
38 EntryKind::Slate => "slate",
39 }
40 }
41}
42broadcast_common::impl_spec_display!(EntryKind);
43
44#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
58#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
59pub struct CodecConfigId(pub u64);
60
61#[derive(Debug, Clone, PartialEq, Eq)]
64#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
65pub struct ScheduleEntry {
66 pub id: String,
69 pub kind: EntryKind,
71 pub planned_start: u64,
75 pub source_start_pts: u64,
79 pub codec_config: CodecConfigId,
82}
83
84#[derive(Debug, Clone, Default, PartialEq, Eq)]
91#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
92pub struct Schedule {
93 entries: Vec<ScheduleEntry>,
94}
95
96impl Schedule {
97 #[must_use]
99 pub fn new() -> Self {
100 Schedule {
101 entries: Vec::new(),
102 }
103 }
104
105 pub fn push(&mut self, entry: ScheduleEntry) -> Result<()> {
108 if let Some(last) = self.entries.last()
109 && entry.planned_start <= last.planned_start
110 {
111 return Err(Error::OutOfOrder {
112 prev: last.planned_start,
113 next: entry.planned_start,
114 });
115 }
116 self.entries.push(entry);
117 Ok(())
118 }
119
120 #[must_use]
122 pub fn entries(&self) -> &[ScheduleEntry] {
123 &self.entries
124 }
125
126 #[must_use]
128 pub fn len(&self) -> usize {
129 self.entries.len()
130 }
131
132 #[must_use]
134 pub fn is_empty(&self) -> bool {
135 self.entries.is_empty()
136 }
137
138 #[must_use]
142 pub fn active_at(&self, pts: u64) -> Option<&ScheduleEntry> {
143 self.active_index_at(pts).map(|i| &self.entries[i])
144 }
145
146 pub(crate) fn active_index_at(&self, pts: u64) -> Option<usize> {
149 if self.entries.is_empty() {
150 return None;
151 }
152 let idx = self.entries.partition_point(|e| e.planned_start <= pts);
153 if idx == 0 { None } else { Some(idx - 1) }
154 }
155}
156
157#[cfg(test)]
158mod tests {
159 use super::*;
160
161 fn entry(id: &str, kind: EntryKind, planned_start: u64) -> ScheduleEntry {
162 ScheduleEntry {
163 id: id.into(),
164 kind,
165 planned_start,
166 source_start_pts: 0,
167 codec_config: CodecConfigId(1),
168 }
169 }
170
171 #[test]
172 fn push_accepts_strictly_increasing_starts() {
173 let mut sched = Schedule::new();
174 sched
175 .push(entry("prog-1", EntryKind::Programme, 0))
176 .unwrap();
177 sched.push(entry("ad-1", EntryKind::Ad, 1_000)).unwrap();
178 assert_eq!(sched.len(), 2);
179 }
180
181 #[test]
182 fn push_rejects_non_increasing_start() {
183 let mut sched = Schedule::new();
184 sched
185 .push(entry("prog-1", EntryKind::Programme, 1_000))
186 .unwrap();
187 let err = sched.push(entry("ad-1", EntryKind::Ad, 1_000)).unwrap_err();
188 match err {
189 Error::OutOfOrder { prev, next } => {
190 assert_eq!(prev, 1_000);
191 assert_eq!(next, 1_000);
192 }
193 other => panic!("expected OutOfOrder, got {other:?}"),
194 }
195
196 let err = sched.push(entry("ad-2", EntryKind::Ad, 500)).unwrap_err();
197 assert!(matches!(
198 err,
199 Error::OutOfOrder {
200 prev: 1_000,
201 next: 500
202 }
203 ));
204 }
205
206 #[test]
207 fn active_at_finds_the_last_entry_starting_at_or_before_pts() {
208 let mut sched = Schedule::new();
209 sched
210 .push(entry("prog-1", EntryKind::Programme, 0))
211 .unwrap();
212 sched.push(entry("ad-1", EntryKind::Ad, 1_000)).unwrap();
213 sched
214 .push(entry("prog-2", EntryKind::Programme, 2_000))
215 .unwrap();
216
217 assert_eq!(sched.active_at(0).unwrap().id, "prog-1");
218 assert_eq!(sched.active_at(999).unwrap().id, "prog-1");
219 assert_eq!(sched.active_at(1_000).unwrap().id, "ad-1");
220 assert_eq!(sched.active_at(1_999).unwrap().id, "ad-1");
221 assert_eq!(sched.active_at(2_000).unwrap().id, "prog-2");
222 assert_eq!(sched.active_at(1_000_000).unwrap().id, "prog-2");
223 }
224
225 #[test]
226 fn active_at_before_the_first_entry_is_none() {
227 let mut sched = Schedule::new();
228 sched
229 .push(entry("prog-1", EntryKind::Programme, 1_000))
230 .unwrap();
231 assert!(sched.active_at(999).is_none());
232 }
233
234 #[test]
235 fn empty_schedule_has_no_active_entry() {
236 let sched = Schedule::new();
237 assert!(sched.is_empty());
238 assert!(sched.active_at(0).is_none());
239 }
240
241 #[test]
242 fn label_convention() {
243 assert_eq!(EntryKind::Programme.name(), "programme");
244 assert_eq!(alloc::format!("{}", EntryKind::Ad), "ad");
245 assert_eq!(EntryKind::Slate.name(), "slate");
246 }
247}