use std::fmt::Debug;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[must_use = "TimelineLost obliges the caller to restart its own timeline and drop its mirror"]
pub enum Admission {
Queued,
TimelineLost,
}
#[derive(Clone, Debug)]
pub struct PlayoutBuffer<T> {
queued: Vec<(u64, u64, T)>,
max_queued: usize,
lost_ahead: u64,
underruns: u64,
restarts: u64,
}
impl<T> PlayoutBuffer<T> {
pub fn new(max_queued: usize, lost_ahead: u64) -> Self {
Self {
queued: Vec::new(),
max_queued: max_queued.max(1),
lost_ahead,
underruns: 0,
restarts: 0,
}
}
pub fn push(&mut self, stamp: u64, order: u64, item: T, render_at: Option<u64>) -> Admission {
if let Some(at) = render_at {
let late_by = at.saturating_sub(stamp);
if late_by > 0 && late_by < self.lost_ahead {
self.underruns += 1;
}
}
let idx = self.queued.partition_point(|(_, o, _)| *o <= order);
self.queued.insert(idx, (stamp, order, item));
let ahead = render_at.map(|at| stamp.saturating_sub(at)).unwrap_or(0);
if ahead > self.lost_ahead || self.queued.len() > self.max_queued {
self.restart();
return Admission::TimelineLost;
}
Admission::Queued
}
pub fn pop_due(&mut self, render_at: u64) -> Option<T> {
if self.queued.first().is_some_and(|(stamp, _, _)| *stamp <= render_at) {
return Some(self.queued.remove(0).2);
}
None
}
pub fn timeline_lost(&mut self) {
self.restart();
}
fn restart(&mut self) {
self.restarts += 1;
let newest = self.queued.pop();
self.queued.clear();
self.queued.extend(newest);
}
pub fn underruns(&self) -> u64 {
self.underruns
}
pub fn restarts(&self) -> u64 {
self.restarts
}
pub fn iter(&self) -> impl Iterator<Item = &T> {
self.queued.iter().map(|(_, _, item)| item)
}
pub fn len(&self) -> usize {
self.queued.len()
}
pub fn is_empty(&self) -> bool {
self.queued.is_empty()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn buffer() -> PlayoutBuffer<&'static str> {
PlayoutBuffer::new(8, 3_000)
}
#[test]
fn packets_play_out_in_sequence_order_at_their_instants() {
let mut b = buffer();
assert_eq!(b.push(200, 2, "second", Some(100)), Admission::Queued);
assert_eq!(b.push(150, 1, "first", Some(100)), Admission::Queued);
assert!(b.pop_due(120).is_none(), "nothing is due before its instant");
assert_eq!(b.pop_due(200), Some("first"));
assert_eq!(b.pop_due(200), Some("second"));
assert_eq!(b.pop_due(200), None);
}
#[test]
fn jitter_scale_lateness_is_an_underrun_and_still_plays() {
let mut b = buffer();
let _ = b.push(980, 1, "late", Some(1_000));
assert_eq!(b.underruns(), 1);
assert_eq!(b.pop_due(1_000), Some("late"), "late is still better than never");
}
#[test]
fn lateness_past_the_threshold_is_a_stall_not_an_underrun() {
let mut b = buffer();
let _ = b.push(10_000, 1, "stale", Some(60_000));
assert_eq!(b.underruns(), 0);
}
#[test]
fn an_arrival_far_ahead_of_the_render_instant_restarts_the_timeline() {
let mut b = buffer();
assert_eq!(b.push(1_000, 1, "old", Some(1_000)), Admission::Queued);
assert_eq!(b.push(61_000, 2, "now", Some(1_000)), Admission::TimelineLost);
assert_eq!(b.restarts(), 1);
assert_eq!(b.len(), 1, "only the newest survives");
assert_eq!(b.pop_due(61_000), Some("now"), "and it is the packet the clock re-anchors on");
}
#[test]
fn overflow_restarts_even_inside_the_time_threshold() {
let mut b = buffer();
let mut lost = 0;
for seq in 0..20u64 {
if b.push(1_000, seq, "packet", Some(1_000)) == Admission::TimelineLost {
lost += 1;
}
}
assert!(lost > 0, "a queue fed by a peer and drained by a clock must bound itself");
assert!(b.len() <= 9, "held {}", b.len());
}
#[test]
fn an_external_verdict_restarts_once_and_keeps_the_newest() {
let mut b = buffer();
let _ = b.push(100, 1, "old", None);
let _ = b.push(200, 2, "newest", None);
b.timeline_lost();
assert_eq!(b.restarts(), 1);
assert_eq!(b.pop_due(200), Some("newest"));
assert_eq!(b.pop_due(200), None);
}
#[test]
fn before_the_timeline_starts_nothing_is_late_and_nothing_is_lost() {
let mut b = buffer();
assert_eq!(b.push(5_000, 1, "join", None), Admission::Queued);
assert_eq!(b.underruns(), 0);
assert_eq!(b.restarts(), 0);
}
}