use std::time::Instant;
use super::budget::IntervalBudget;
use crate::rtp_::{Bitrate, DataSize};
#[derive(Debug)]
pub struct AlrDetector {
budget: IntervalBudget,
state: AlrState,
last_send_time: Option<Instant>,
bandwidth_usage_ratio: f64,
start_budget_level_ratio: f64,
stop_budget_level_ratio: f64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum AlrState {
NotInAlr,
InAlr(Instant),
}
impl AlrState {
fn alr_start_time(&self) -> Option<Instant> {
match self {
AlrState::InAlr(start) => Some(*start),
AlrState::NotInAlr => None,
}
}
}
impl AlrDetector {
pub fn new() -> Self {
Self {
budget: IntervalBudget::new(Bitrate::ZERO, true),
state: AlrState::NotInAlr,
last_send_time: None,
bandwidth_usage_ratio: 0.65,
start_budget_level_ratio: 0.80,
stop_budget_level_ratio: 0.50,
}
}
pub fn on_bytes_sent(&mut self, bytes: DataSize, now: Instant) {
let Some(last) = self.last_send_time else {
self.last_send_time = Some(now);
return;
};
let delta = now.saturating_duration_since(last);
self.last_send_time = Some(now);
self.budget.use_budget(bytes);
self.budget.increase_budget(delta);
let ratio = self.budget.budget_ratio();
match self.state {
AlrState::NotInAlr => {
if ratio > self.start_budget_level_ratio {
self.state = AlrState::InAlr(now);
trace!(
"ALR: Entered ALR state (ratio={:.3} > threshold={:.3})",
ratio, self.start_budget_level_ratio
);
}
}
AlrState::InAlr(_) => {
if ratio < self.stop_budget_level_ratio {
self.state = AlrState::NotInAlr;
trace!(
"ALR: Exited ALR state (ratio={:.3} < threshold={:.3})",
ratio, self.stop_budget_level_ratio
);
}
}
}
}
pub fn set_estimated_bitrate(&mut self, estimate: Bitrate) {
let target = estimate * self.bandwidth_usage_ratio;
self.budget.set_target_rate(target);
}
pub fn alr_start_time(&self) -> Option<Instant> {
self.state.alr_start_time()
}
#[cfg(test)]
pub fn budget_ratio(&self) -> f64 {
self.budget.budget_ratio()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::Duration;
#[test]
fn test_alr_not_detected_initially() {
let alr = AlrDetector::new();
assert!(alr.alr_start_time().is_none());
}
#[test]
fn test_alr_enter_with_low_usage() {
let mut alr = AlrDetector::new();
let now = Instant::now();
alr.set_estimated_bitrate(Bitrate::mbps(1));
for i in 0..50 {
let t = now + Duration::from_millis(i * 10);
alr.on_bytes_sent(DataSize::bytes(100), t);
}
assert!(alr.alr_start_time().is_some());
assert!(alr.budget_ratio() > 0.80);
}
#[test]
fn test_alr_exit_with_high_usage() {
let mut alr = AlrDetector::new();
let now = Instant::now();
alr.set_estimated_bitrate(Bitrate::mbps(1));
for i in 0..50 {
let t = now + Duration::from_millis(i * 10);
alr.on_bytes_sent(DataSize::bytes(100), t);
}
assert!(alr.alr_start_time().is_some());
let start_exit = now + Duration::from_millis(500);
for i in 0..50 {
let t = start_exit + Duration::from_millis(i * 10);
alr.on_bytes_sent(DataSize::bytes(10_000), t);
}
assert!(alr.alr_start_time().is_none());
assert!(alr.budget_ratio() < 0.50);
}
#[test]
fn test_alr_hysteresis() {
let mut alr = AlrDetector::new();
let now = Instant::now();
alr.set_estimated_bitrate(Bitrate::mbps(1));
for i in 0..100 {
let t = now + Duration::from_millis(i * 10);
alr.on_bytes_sent(DataSize::bytes(812), t);
}
assert!(alr.alr_start_time().is_none());
let ratio = alr.budget_ratio();
assert!(ratio < 0.80, "ratio={}", ratio);
assert!(ratio > -0.20, "ratio={}", ratio); }
#[test]
fn test_alr_handles_bursts() {
let mut alr = AlrDetector::new();
let now = Instant::now();
alr.set_estimated_bitrate(Bitrate::mbps(1));
let mut time = now;
for i in 0..100 {
time = time + Duration::from_millis(33);
if i % 10 == 0 {
alr.on_bytes_sent(DataSize::bytes(50_000), time);
} else {
alr.on_bytes_sent(DataSize::bytes(5_000), time);
}
}
assert!(alr.alr_start_time().is_none());
}
#[test]
fn test_alr_estimate_change() {
let mut alr = AlrDetector::new();
let now = Instant::now();
alr.set_estimated_bitrate(Bitrate::mbps(1));
for i in 0..100 {
let t = now + Duration::from_millis(i * 10);
alr.on_bytes_sent(DataSize::bytes(250), t); }
assert!(alr.alr_start_time().is_some());
alr.set_estimated_bitrate(Bitrate::kbps(300));
let continue_time = now + Duration::from_millis(1000);
for i in 0..100 {
let t = continue_time + Duration::from_millis(i * 10);
alr.on_bytes_sent(DataSize::bytes(312), t); }
assert!(alr.alr_start_time().is_none());
}
#[test]
fn test_alr_should_not_trigger_when_sending_at_target_rate() {
let mut alr = AlrDetector::new();
let now = Instant::now();
let estimate = Bitrate::kbps(8250);
alr.set_estimated_bitrate(estimate);
let packet_size = DataSize::bytes(1150);
let packet_interval = Duration::from_micros(1115);
for i in 0..100 {
let t = now + packet_interval * i;
alr.on_bytes_sent(packet_size, t);
}
assert!(
alr.alr_start_time().is_none(),
"ALR should not trigger when sending at target rate. Budget ratio: {:.3}",
alr.budget_ratio()
);
}
#[test]
fn test_alr_handles_spurious_small_time_advances() {
let mut alr = AlrDetector::new();
let now = Instant::now();
let estimate = Bitrate::kbps(8250);
alr.set_estimated_bitrate(estimate);
let packet_size = DataSize::bytes(1150);
let spurious_advance = Duration::from_micros(200);
let mut time = now;
for _ in 0..100 {
alr.on_bytes_sent(packet_size, time);
time += spurious_advance; }
assert!(
alr.alr_start_time().is_none(),
"ALR should not trigger with spurious small advances when sending fast. Budget ratio: {:.3}",
alr.budget_ratio()
);
}
}