use std::collections::BTreeMap;
use std::time::Instant;
pub const FRAME_MS: u32 = 20;
const MIN_DEPTH_MS: u32 = 40;
const MAX_DEPTH_MS: u32 = 200;
const BASE_DEPTH_MS: u32 = 40;
const JITTER_K: f64 = 3.0;
const MAX_FRAMES: usize = 100;
#[derive(Debug, Clone, PartialEq)]
pub enum Pull {
Play(Vec<u8>),
Conceal,
Silence,
}
#[derive(Debug, Default, Clone, Copy)]
pub struct Stats {
pub played: u64,
pub concealed: u64,
pub late: u64,
pub overflow: u64,
pub target_ms: u32,
pub buffered: usize,
}
pub struct JitterBuffer {
frames: BTreeMap<u32, Vec<u8>>,
next_ts: u32,
playing: bool,
target_ms: u32,
jitter: f64, last_arrival: Option<(u32, Instant)>, played: u64,
concealed: u64,
late: u64,
overflow: u64,
}
impl Default for JitterBuffer {
fn default() -> Self {
Self::new()
}
}
impl JitterBuffer {
pub fn new() -> Self {
JitterBuffer {
frames: BTreeMap::new(),
next_ts: 0,
playing: false,
target_ms: MIN_DEPTH_MS,
jitter: 0.0,
last_arrival: None,
played: 0,
concealed: 0,
late: 0,
overflow: 0,
}
}
pub fn reset(&mut self) {
self.frames.clear();
self.playing = false;
self.last_arrival = None;
}
pub fn push(&mut self, ts: u32, payload: Vec<u8>, now: Instant) {
if let Some((prev_ts, prev_inst)) = self.last_arrival {
if ts > prev_ts {
let arrival_delta = now.saturating_duration_since(prev_inst).as_millis() as f64;
let ts_delta = (ts - prev_ts) as f64;
let d = arrival_delta - ts_delta;
self.jitter += (d.abs() - self.jitter) / 16.0;
self.recompute_target();
}
}
if ts > self.last_arrival.map(|(t, _)| t).unwrap_or(0) || self.last_arrival.is_none() {
self.last_arrival = Some((ts, now));
}
if self.playing && ts < self.next_ts {
self.late += 1;
return;
}
self.frames.insert(ts, payload);
while self.frames.len() > MAX_FRAMES {
if let Some((&old, _)) = self.frames.iter().next() {
self.frames.remove(&old);
self.overflow += 1;
} else {
break;
}
}
}
pub fn pull(&mut self) -> Pull {
if !self.playing {
if self.buffered_span_ms() >= self.target_ms {
if let Some((&first, _)) = self.frames.iter().next() {
self.next_ts = first;
self.playing = true;
}
}
if !self.playing {
return Pull::Silence;
}
}
if let Some(payload) = self.frames.remove(&self.next_ts) {
self.next_ts = self.next_ts.wrapping_add(FRAME_MS);
self.played += 1;
Pull::Play(payload)
} else if !self.frames.is_empty() {
self.next_ts = self.next_ts.wrapping_add(FRAME_MS);
self.concealed += 1;
Pull::Conceal
} else {
self.playing = false;
Pull::Silence
}
}
pub fn stats(&self) -> Stats {
Stats {
played: self.played,
concealed: self.concealed,
late: self.late,
overflow: self.overflow,
target_ms: self.target_ms,
buffered: self.frames.len(),
}
}
fn recompute_target(&mut self) {
let t = BASE_DEPTH_MS as f64 + JITTER_K * self.jitter;
self.target_ms = (t as u32).clamp(MIN_DEPTH_MS, MAX_DEPTH_MS);
}
fn buffered_span_ms(&self) -> u32 {
match (self.frames.keys().next(), self.frames.keys().next_back()) {
(Some(&lo), Some(&hi)) => hi.wrapping_sub(lo).wrapping_add(FRAME_MS),
_ => 0,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::Duration;
fn pcm(tag: u8) -> Vec<u8> {
vec![tag; 160]
}
#[test]
fn in_order_playout() {
let t0 = Instant::now();
let mut jb = JitterBuffer::new();
for i in 0..4u32 {
jb.push(i * FRAME_MS, pcm(i as u8), t0 + Duration::from_millis((i * 20) as u64));
}
let mut out = Vec::new();
for _ in 0..4 {
if let Pull::Play(p) = jb.pull() {
out.push(p[0]);
}
}
assert_eq!(out, vec![0, 1, 2, 3], "riproduzione in ordine");
}
#[test]
fn reorders_by_timestamp() {
let t0 = Instant::now();
let mut jb = JitterBuffer::new();
for (n, i) in [2u32, 0, 3, 1].into_iter().enumerate() {
jb.push(i * FRAME_MS, pcm(i as u8), t0 + Duration::from_millis((n as u64) * 20));
}
let mut out = Vec::new();
for _ in 0..4 {
if let Pull::Play(p) = jb.pull() {
out.push(p[0]);
}
}
assert_eq!(out, vec![0, 1, 2, 3], "riallineati per ts nonostante l'ordine d'arrivo");
}
#[test]
fn gap_yields_conceal() {
let t0 = Instant::now();
let mut jb = JitterBuffer::new();
for i in [0u32, 20, 60, 80] {
jb.push(i, pcm((i / 20) as u8), t0 + Duration::from_millis(i as u64));
}
let mut seq = Vec::new();
for _ in 0..5 {
seq.push(jb.pull());
}
assert_eq!(seq[0], Pull::Play(pcm(0)));
assert_eq!(seq[1], Pull::Play(pcm(1)));
assert_eq!(seq[2], Pull::Conceal, "il buco produce PLC");
assert_eq!(seq[3], Pull::Play(pcm(3)));
assert!(jb.stats().concealed >= 1);
}
#[test]
fn late_frame_is_dropped() {
let t0 = Instant::now();
let mut jb = JitterBuffer::new();
for i in [0u32, 20, 40, 60] {
jb.push(i, pcm((i / 20) as u8), t0 + Duration::from_millis(i as u64));
}
let _ = jb.pull();
let _ = jb.pull();
jb.push(20, pcm(99), t0 + Duration::from_millis(100));
assert_eq!(jb.stats().late, 1, "il frame in ritardo e' contato e scartato");
}
#[test]
fn target_depth_grows_with_jitter() {
let t0 = Instant::now();
let mut jb = JitterBuffer::new();
assert_eq!(jb.stats().target_ms, MIN_DEPTH_MS);
let arrivals = [0u64, 5, 70, 80, 200, 210, 215, 400];
for (n, a) in arrivals.into_iter().enumerate() {
jb.push(n as u32 * FRAME_MS, pcm(n as u8), t0 + Duration::from_millis(a));
}
assert!(jb.stats().target_ms > MIN_DEPTH_MS, "il target si adatta al jitter");
}
}