#![allow(dead_code)]
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct RtpBandwidthSample {
pub bytes: u64,
pub duration_ms: u64,
}
impl RtpBandwidthSample {
#[must_use]
pub const fn new(bytes: u64, duration_ms: u64) -> Self {
Self { bytes, duration_ms }
}
#[allow(clippy::cast_precision_loss)]
#[must_use]
pub fn kbps(&self) -> f64 {
if self.duration_ms == 0 {
return 0.0;
}
(self.bytes as f64 * 8.0) / (self.duration_ms as f64)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BandwidthTrend {
Stable,
Rising,
Falling,
}
#[derive(Debug)]
pub struct RtpBandwidthEstimator {
samples: Vec<RtpBandwidthSample>,
capacity: usize,
}
impl RtpBandwidthEstimator {
#[must_use]
pub fn new(capacity: usize) -> Self {
assert!(capacity > 0, "capacity must be > 0");
Self {
samples: Vec::with_capacity(capacity),
capacity,
}
}
pub fn add_sample(&mut self, sample: RtpBandwidthSample) {
if self.samples.len() == self.capacity {
self.samples.remove(0);
}
self.samples.push(sample);
}
#[must_use]
pub fn len(&self) -> usize {
self.samples.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.samples.is_empty()
}
#[must_use]
pub fn estimate_kbps(&self) -> f64 {
if self.samples.is_empty() {
return 0.0;
}
let total: f64 = self.samples.iter().map(RtpBandwidthSample::kbps).sum();
total / self.samples.len() as f64
}
#[must_use]
pub fn trend(&self) -> BandwidthTrend {
if self.samples.len() < 2 {
return BandwidthTrend::Stable;
}
let mid = self.samples.len() / 2;
let first_half = &self.samples[..mid];
let second_half = &self.samples[mid..];
let avg = |slice: &[RtpBandwidthSample]| -> f64 {
slice.iter().map(RtpBandwidthSample::kbps).sum::<f64>() / slice.len() as f64
};
let old_avg = avg(first_half);
let new_avg = avg(second_half);
if old_avg == 0.0 {
return BandwidthTrend::Stable;
}
let ratio = (new_avg - old_avg) / old_avg;
if ratio > 0.10 {
BandwidthTrend::Rising
} else if ratio < -0.10 {
BandwidthTrend::Falling
} else {
BandwidthTrend::Stable
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct BandwidthAlert {
pub current_kbps: f64,
pub threshold_kbps: f64,
pub message: String,
}
impl BandwidthAlert {
#[must_use]
pub fn new(current_kbps: f64, threshold_kbps: f64, message: impl Into<String>) -> Self {
Self {
current_kbps,
threshold_kbps,
message: message.into(),
}
}
#[must_use]
pub fn is_critical(&self) -> bool {
self.current_kbps < self.threshold_kbps
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_sample_kbps_basic() {
let s = RtpBandwidthSample::new(1000, 1000);
assert!((s.kbps() - 8.0).abs() < 1e-9);
}
#[test]
fn test_sample_kbps_zero_duration() {
let s = RtpBandwidthSample::new(500, 0);
assert_eq!(s.kbps(), 0.0);
}
#[test]
fn test_sample_kbps_zero_bytes() {
let s = RtpBandwidthSample::new(0, 1000);
assert_eq!(s.kbps(), 0.0);
}
#[test]
fn test_estimator_starts_empty() {
let est = RtpBandwidthEstimator::new(5);
assert!(est.is_empty());
assert_eq!(est.estimate_kbps(), 0.0);
}
#[test]
fn test_estimator_add_sample() {
let mut est = RtpBandwidthEstimator::new(5);
est.add_sample(RtpBandwidthSample::new(1000, 1000));
assert_eq!(est.len(), 1);
}
#[test]
fn test_estimator_eviction() {
let mut est = RtpBandwidthEstimator::new(3);
for i in 0u64..5 {
est.add_sample(RtpBandwidthSample::new(i * 100, 1000));
}
assert_eq!(est.len(), 3);
}
#[test]
fn test_estimate_kbps_mean() {
let mut est = RtpBandwidthEstimator::new(2);
est.add_sample(RtpBandwidthSample::new(1000, 1000)); est.add_sample(RtpBandwidthSample::new(2000, 1000)); assert!((est.estimate_kbps() - 12.0).abs() < 1e-6);
}
#[test]
fn test_trend_stable() {
let mut est = RtpBandwidthEstimator::new(4);
for _ in 0..4 {
est.add_sample(RtpBandwidthSample::new(1000, 1000));
}
assert_eq!(est.trend(), BandwidthTrend::Stable);
}
#[test]
fn test_trend_rising() {
let mut est = RtpBandwidthEstimator::new(4);
est.add_sample(RtpBandwidthSample::new(1000, 1000));
est.add_sample(RtpBandwidthSample::new(1000, 1000));
est.add_sample(RtpBandwidthSample::new(3000, 1000));
est.add_sample(RtpBandwidthSample::new(3000, 1000));
assert_eq!(est.trend(), BandwidthTrend::Rising);
}
#[test]
fn test_trend_falling() {
let mut est = RtpBandwidthEstimator::new(4);
est.add_sample(RtpBandwidthSample::new(3000, 1000));
est.add_sample(RtpBandwidthSample::new(3000, 1000));
est.add_sample(RtpBandwidthSample::new(1000, 1000));
est.add_sample(RtpBandwidthSample::new(1000, 1000));
assert_eq!(est.trend(), BandwidthTrend::Falling);
}
#[test]
fn test_alert_is_critical() {
let alert = BandwidthAlert::new(50.0, 100.0, "low bandwidth");
assert!(alert.is_critical());
}
#[test]
fn test_alert_not_critical() {
let alert = BandwidthAlert::new(200.0, 100.0, "ok");
assert!(!alert.is_critical());
}
#[test]
fn test_trend_single_sample() {
let mut est = RtpBandwidthEstimator::new(5);
est.add_sample(RtpBandwidthSample::new(1000, 1000));
assert_eq!(est.trend(), BandwidthTrend::Stable);
}
}