use alloc::vec::Vec;
use core::time::Duration;
use std::time::Instant;
#[derive(Debug, Clone, Copy)]
struct ByteSample {
at: Instant,
bytes: u64,
}
#[derive(Debug, Clone)]
pub(crate) struct VideoBitrateWindow {
window: Duration,
samples: Vec<ByteSample>,
}
impl VideoBitrateWindow {
#[must_use]
pub(crate) const fn new(window: Duration) -> Self {
Self {
window,
samples: Vec::new(),
}
}
pub(crate) fn record(&mut self, bytes: u64, now: Instant) {
self.evict(now);
self.samples.push(ByteSample { at: now, bytes });
}
pub(crate) fn evict(&mut self, now: Instant) {
let Some(cutoff) = now.checked_sub(self.window) else {
return;
};
self.samples.retain(|sample| sample.at >= cutoff);
}
#[must_use]
pub(crate) fn bitrate_bps(&self, now: Instant) -> u64 {
let window_secs = self.window.as_secs_f64();
if window_secs <= f64::EPSILON {
return 0;
}
let total_bytes: u64 = match now.checked_sub(self.window) {
Some(cutoff) => self
.samples
.iter()
.filter(|sample| sample.at >= cutoff)
.map(|sample| sample.bytes)
.sum(),
None => self.samples.iter().map(|sample| sample.bytes).sum(),
};
((total_bytes as f64) * 8.0 / window_secs) as u64
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn early_burst_does_not_spike_bitrate() {
let window = Duration::from_secs(1);
let mut tracker = VideoBitrateWindow::new(window);
let t0 = Instant::now();
tracker.record(1_500_000, t0);
let bps = tracker.bitrate_bps(t0 + Duration::from_millis(1));
assert_eq!(bps, 12_000_000);
}
#[test]
fn evicts_samples_outside_window() {
let window = Duration::from_millis(100);
let mut tracker = VideoBitrateWindow::new(window);
let t0 = Instant::now();
tracker.record(10_000, t0);
let mid = t0 + Duration::from_millis(150);
tracker.evict(mid);
assert_eq!(tracker.bitrate_bps(mid), 0);
}
}