#![allow(dead_code)]
use std::cmp::Reverse;
use std::collections::BinaryHeap;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum JitterBufferState {
Filling,
Ok,
Underrun,
Overrun,
}
impl JitterBufferState {
#[must_use]
pub fn is_ok(&self) -> bool {
matches!(self, JitterBufferState::Ok)
}
#[must_use]
pub fn label(&self) -> &'static str {
match self {
JitterBufferState::Filling => "Filling",
JitterBufferState::Ok => "OK",
JitterBufferState::Underrun => "Underrun",
JitterBufferState::Overrun => "Overrun",
}
}
}
#[derive(Debug, Clone)]
pub struct JitterEntry {
pub seq: u32,
pub pts_samples: u64,
pub sample_rate: u32,
pub payload: Vec<f32>,
pub arrival_ms: u64,
pub playout_ms: u64,
}
impl JitterEntry {
#[must_use]
pub fn new(
seq: u32,
pts_samples: u64,
sample_rate: u32,
payload: Vec<f32>,
arrival_ms: u64,
playout_ms: u64,
) -> Self {
Self {
seq,
pts_samples,
sample_rate,
payload,
arrival_ms,
playout_ms,
}
}
#[must_use]
pub fn is_late(&self) -> bool {
self.arrival_ms > self.playout_ms
}
#[allow(clippy::cast_possible_wrap)]
#[must_use]
pub fn latency_ms(&self) -> i64 {
self.arrival_ms as i64 - self.playout_ms as i64
}
}
impl PartialEq for JitterEntry {
fn eq(&self, other: &Self) -> bool {
self.pts_samples == other.pts_samples
}
}
impl Eq for JitterEntry {}
impl PartialOrd for JitterEntry {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for JitterEntry {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.pts_samples.cmp(&other.pts_samples)
}
}
#[derive(Debug, Clone, Default)]
pub struct JitterStats {
pub total_inserted: u64,
pub total_drained: u64,
pub total_late: u64,
pub total_discarded: u64,
}
impl JitterStats {
#[allow(clippy::cast_precision_loss)]
#[must_use]
pub fn late_pct(&self) -> f64 {
if self.total_inserted == 0 {
return 0.0;
}
self.total_late as f64 / self.total_inserted as f64 * 100.0
}
}
pub struct JitterBuffer {
heap: BinaryHeap<Reverse<JitterEntry>>,
capacity: usize,
sample_rate: u32,
state: JitterBufferState,
min_fill: usize,
stats: JitterStats,
}
impl JitterBuffer {
#[must_use]
pub fn new(capacity: usize, min_fill: usize, sample_rate: u32) -> Self {
Self {
heap: BinaryHeap::new(),
capacity,
sample_rate,
state: JitterBufferState::Filling,
min_fill: min_fill.max(1),
stats: JitterStats::default(),
}
}
pub fn insert(&mut self, entry: JitterEntry) -> bool {
if entry.is_late() {
self.stats.total_late += 1;
}
if self.heap.len() >= self.capacity {
self.stats.total_discarded += 1;
self.state = JitterBufferState::Overrun;
return false;
}
self.stats.total_inserted += 1;
self.heap.push(Reverse(entry));
if self.state == JitterBufferState::Filling && self.heap.len() >= self.min_fill {
self.state = JitterBufferState::Ok;
} else if self.state == JitterBufferState::Overrun && self.heap.len() < self.capacity {
self.state = JitterBufferState::Ok;
}
true
}
pub fn drain(&mut self, up_to_pts: u64) -> Vec<JitterEntry> {
let mut out = Vec::new();
loop {
match self.heap.peek() {
Some(Reverse(e)) if e.pts_samples <= up_to_pts => {
let Reverse(entry) = self.heap.pop().unwrap();
out.push(entry);
self.stats.total_drained += 1;
}
_ => break,
}
}
if self.heap.is_empty() && self.state == JitterBufferState::Ok {
self.state = JitterBufferState::Underrun;
}
out
}
#[allow(clippy::cast_precision_loss)]
#[must_use]
pub fn depth_ms(&self) -> f64 {
if self.sample_rate == 0 || self.heap.is_empty() {
return 0.0;
}
let total_samples: usize = self.heap.iter().map(|Reverse(e)| e.payload.len()).sum();
total_samples as f64 / self.sample_rate as f64 * 1_000.0
}
#[must_use]
pub fn state(&self) -> JitterBufferState {
self.state
}
#[must_use]
pub fn stats(&self) -> &JitterStats {
&self.stats
}
#[must_use]
pub fn len(&self) -> usize {
self.heap.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.heap.is_empty()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn entry(seq: u32, pts: u64, samples: usize, arrival: u64, playout: u64) -> JitterEntry {
JitterEntry::new(seq, pts, 48_000, vec![0.0; samples], arrival, playout)
}
#[test]
fn test_state_is_ok() {
assert!(JitterBufferState::Ok.is_ok());
assert!(!JitterBufferState::Filling.is_ok());
assert!(!JitterBufferState::Underrun.is_ok());
assert!(!JitterBufferState::Overrun.is_ok());
}
#[test]
fn test_state_labels() {
assert_eq!(JitterBufferState::Filling.label(), "Filling");
assert_eq!(JitterBufferState::Underrun.label(), "Underrun");
}
#[test]
fn test_entry_is_late_true() {
let e = entry(0, 0, 480, 100, 50); assert!(e.is_late());
}
#[test]
fn test_entry_is_late_false() {
let e = entry(0, 0, 480, 30, 50); assert!(!e.is_late());
}
#[test]
fn test_entry_latency_ms_positive() {
let e = entry(0, 0, 480, 100, 50);
assert_eq!(e.latency_ms(), 50);
}
#[test]
fn test_entry_latency_ms_negative() {
let e = entry(0, 0, 480, 20, 50);
assert_eq!(e.latency_ms(), -30);
}
#[test]
fn test_jitter_buffer_filling_state_initially() {
let jb = JitterBuffer::new(16, 4, 48_000);
assert_eq!(jb.state(), JitterBufferState::Filling);
}
#[test]
fn test_jitter_buffer_transitions_to_ok() {
let mut jb = JitterBuffer::new(16, 2, 48_000);
jb.insert(entry(0, 0, 480, 0, 10));
assert_eq!(jb.state(), JitterBufferState::Filling);
jb.insert(entry(1, 480, 480, 10, 20));
assert_eq!(jb.state(), JitterBufferState::Ok);
}
#[test]
fn test_jitter_buffer_insert_returns_false_when_full() {
let mut jb = JitterBuffer::new(2, 1, 48_000);
assert!(jb.insert(entry(0, 0, 480, 0, 10)));
assert!(jb.insert(entry(1, 480, 480, 0, 10)));
assert!(!jb.insert(entry(2, 960, 480, 0, 10)));
}
#[test]
fn test_jitter_buffer_drain_in_order() {
let mut jb = JitterBuffer::new(16, 1, 48_000);
jb.insert(entry(2, 960, 480, 0, 10));
jb.insert(entry(0, 0, 480, 0, 10));
jb.insert(entry(1, 480, 480, 0, 10));
let drained = jb.drain(1440); assert_eq!(drained.len(), 3);
assert_eq!(drained[0].pts_samples, 0);
assert_eq!(drained[1].pts_samples, 480);
assert_eq!(drained[2].pts_samples, 960);
}
#[test]
fn test_jitter_buffer_drain_partial() {
let mut jb = JitterBuffer::new(16, 1, 48_000);
jb.insert(entry(0, 0, 480, 0, 10));
jb.insert(entry(1, 480, 480, 0, 10));
let drained = jb.drain(0); assert_eq!(drained.len(), 1);
assert_eq!(jb.len(), 1);
}
#[test]
fn test_jitter_buffer_depth_ms() {
let mut jb = JitterBuffer::new(16, 1, 48_000);
jb.insert(entry(0, 0, 480, 0, 10)); jb.insert(entry(1, 480, 480, 0, 10)); let depth = jb.depth_ms();
assert!((depth - 20.0).abs() < 0.01);
}
#[test]
fn test_stats_late_pct_zero_when_no_inserts() {
let stats = JitterStats::default();
assert_eq!(stats.late_pct(), 0.0);
}
#[test]
fn test_stats_late_pct_calculation() {
let stats = JitterStats {
total_inserted: 10,
total_late: 3,
..Default::default()
};
assert!((stats.late_pct() - 30.0).abs() < f64::EPSILON);
}
#[test]
fn test_jitter_buffer_is_empty_initially() {
let jb = JitterBuffer::new(16, 4, 48_000);
assert!(jb.is_empty());
}
#[test]
fn test_jitter_buffer_underrun_after_drain_empty() {
let mut jb = JitterBuffer::new(16, 1, 48_000);
jb.insert(entry(0, 0, 480, 0, 10));
jb.drain(0); assert_eq!(jb.state(), JitterBufferState::Underrun);
}
}