use std::time::Duration;
use super::FRAME_MS;
const ALPHA: f64 = 0.125;
const MIN_HOLD: Duration = Duration::from_millis(super::FRAME_MS);
const MAX_HOLD: Duration = Duration::from_millis(super::FRAME_MS * 5);
const FRAME_US: u64 = FRAME_MS * 1_000;
const INITIAL_HOLD: Duration = Duration::from_millis(FRAME_MS * 2);
#[derive(Debug, Clone)]
pub struct Jitter {
variance_us: f64,
samples: u32,
}
impl Jitter {
pub fn new() -> Self {
Self {
variance_us: 0.0,
samples: 0,
}
}
pub fn observe_gap_us(&mut self, gap_us: u64) {
let deviation = gap_us.abs_diff(FRAME_US) as f64;
self.variance_us = if self.samples == 0 {
deviation
} else {
(1.0 - ALPHA) * self.variance_us + ALPHA * deviation
};
self.samples = self.samples.saturating_add(1);
}
pub fn hold_time(&self) -> Duration {
if self.samples == 0 {
return INITIAL_HOLD;
}
let hold_us = (2.0 * self.variance_us).round() as u64;
Duration::from_micros(hold_us).clamp(MIN_HOLD, MAX_HOLD)
}
pub fn max_concealed_frames(&self) -> u32 {
(self.hold_time().as_micros() / FRAME_US as u128).max(1) as u32
}
}
impl Default for Jitter {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn stable_arrivals_hold_near_one_frame() {
let mut j = Jitter::new();
for _ in 0..20 {
j.observe_gap_us(FRAME_US);
}
assert_eq!(j.hold_time(), MIN_HOLD);
assert_eq!(j.max_concealed_frames(), 1);
}
#[test]
fn jittery_arrivals_grow_the_hold_but_stay_bounded() {
let mut j = Jitter::new();
for _ in 0..40 {
j.observe_gap_us(5_000);
j.observe_gap_us(60_000);
}
let hold = j.hold_time();
assert!(hold > MIN_HOLD, "variance must grow the hold, got {hold:?}");
assert!(hold <= MAX_HOLD, "the hold must stay bounded, got {hold:?}");
}
#[test]
fn a_route_change_settles_rather_than_sticking() {
let mut j = Jitter::new();
for _ in 0..40 {
j.observe_gap_us(60_000);
}
let wild = j.hold_time();
for _ in 0..60 {
j.observe_gap_us(FRAME_US);
}
assert!(
j.hold_time() < wild,
"steady arrivals must shrink the hold again"
);
}
#[test]
fn no_samples_means_a_sane_default() {
let j = Jitter::new();
assert_eq!(j.hold_time(), INITIAL_HOLD);
}
}