use std::time::Duration;
use crate::rtp_::{Bitrate, DataSize};
#[derive(Debug, Clone)]
pub struct IntervalBudget {
target_rate: Bitrate,
max_bytes_in_budget: DataSize,
bytes_remaining: DataSize,
can_build_up_underuse: bool,
}
impl IntervalBudget {
const WINDOW: Duration = Duration::from_millis(500);
pub fn new(target_rate: Bitrate, can_build_up_underuse: bool) -> Self {
let max_bytes_in_budget = Self::calculate_max_bytes(target_rate);
Self {
target_rate,
max_bytes_in_budget,
bytes_remaining: DataSize::ZERO,
can_build_up_underuse,
}
}
pub fn set_target_rate(&mut self, target_rate: Bitrate) {
self.target_rate = target_rate;
self.max_bytes_in_budget = Self::calculate_max_bytes(target_rate);
let max = self.max_bytes_in_budget;
let neg_max = max * -1i64;
self.bytes_remaining = DataSize::bytes(
self.bytes_remaining
.as_bytes_i64()
.clamp(neg_max, max.as_bytes_i64()),
);
}
pub fn increase_budget(&mut self, delta_time: Duration) {
let bytes = self.target_rate * delta_time;
let max = self.max_bytes_in_budget;
if self.bytes_remaining.as_bytes_i64() < 0 || self.can_build_up_underuse {
self.bytes_remaining = DataSize::bytes(
(self.bytes_remaining + bytes)
.as_bytes_i64()
.min(max.as_bytes_i64()),
);
} else {
self.bytes_remaining = DataSize::bytes(bytes.as_bytes_i64().min(max.as_bytes_i64()));
}
}
pub fn use_budget(&mut self, bytes: DataSize) {
let max = self.max_bytes_in_budget;
let neg_max = max * -1i64;
self.bytes_remaining =
DataSize::bytes((self.bytes_remaining - bytes).as_bytes_i64().max(neg_max));
}
pub fn budget_ratio(&self) -> f64 {
let max = self.max_bytes_in_budget.as_bytes_i64();
if max == 0 {
return 0.0;
}
self.bytes_remaining.as_bytes_i64() as f64 / max as f64
}
fn calculate_max_bytes(target_rate: Bitrate) -> DataSize {
target_rate * Self::WINDOW
}
#[cfg(test)]
pub fn target_rate(&self) -> Bitrate {
self.target_rate
}
#[cfg(test)]
pub fn bytes_remaining(&self) -> i64 {
self.bytes_remaining.as_bytes_i64()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_initial_budget_is_zero() {
let budget = IntervalBudget::new(Bitrate::kbps(300), true);
assert_eq!(budget.bytes_remaining(), 0);
assert_eq!(budget.budget_ratio(), 0.0);
}
#[test]
fn test_increase_budget_accumulates() {
let mut budget = IntervalBudget::new(Bitrate::kbps(300), true);
budget.increase_budget(Duration::from_millis(100));
assert_eq!(budget.bytes_remaining(), 3750);
assert!(budget.budget_ratio() > 0.0);
}
#[test]
fn test_use_budget_creates_debt() {
let mut budget = IntervalBudget::new(Bitrate::kbps(300), true);
budget.use_budget(DataSize::bytes(5000));
assert_eq!(budget.bytes_remaining(), -5000);
assert!(budget.budget_ratio() < 0.0);
}
#[test]
fn test_budget_ratio_clamped() {
let mut budget = IntervalBudget::new(Bitrate::kbps(300), true);
let max_bytes = (Bitrate::kbps(300) * Duration::from_millis(500)).as_bytes_i64();
budget.increase_budget(Duration::from_millis(500));
assert_eq!(budget.budget_ratio(), 1.0);
budget.increase_budget(Duration::from_millis(100));
assert_eq!(budget.budget_ratio(), 1.0);
budget.use_budget(DataSize::bytes(max_bytes * 2));
assert_eq!(budget.budget_ratio(), -1.0);
}
#[test]
fn test_can_build_up_underuse_false() {
let mut budget = IntervalBudget::new(Bitrate::kbps(300), false);
budget.increase_budget(Duration::from_millis(100));
let after_first = budget.bytes_remaining();
assert!(after_first > 0);
budget.increase_budget(Duration::from_millis(100));
assert_eq!(budget.bytes_remaining(), 3750);
assert_ne!(budget.bytes_remaining(), after_first + 3750);
}
#[test]
fn test_can_build_up_underuse_true() {
let mut budget = IntervalBudget::new(Bitrate::kbps(300), true);
budget.increase_budget(Duration::from_millis(100));
let after_first = budget.bytes_remaining();
budget.increase_budget(Duration::from_millis(100));
assert_eq!(budget.bytes_remaining(), after_first + 3750);
}
#[test]
fn test_debt_recovery() {
let mut budget = IntervalBudget::new(Bitrate::kbps(300), true);
budget.use_budget(DataSize::bytes(5000));
assert_eq!(budget.bytes_remaining(), -5000);
budget.increase_budget(Duration::from_millis(200));
let expected_recovery = 3750 * 2; assert_eq!(budget.bytes_remaining(), -5000 + expected_recovery);
}
#[test]
fn test_set_target_rate_clamps_budget() {
let mut budget = IntervalBudget::new(Bitrate::kbps(300), true);
budget.increase_budget(Duration::from_millis(500));
let old_max = (Bitrate::kbps(300) * Duration::from_millis(500)).as_bytes_i64();
assert_eq!(budget.bytes_remaining(), old_max);
budget.set_target_rate(Bitrate::kbps(150));
let new_max = (Bitrate::kbps(150) * Duration::from_millis(500)).as_bytes_i64();
assert_eq!(budget.bytes_remaining(), new_max);
assert!(new_max < old_max);
}
#[test]
fn test_alr_hysteresis_thresholds() {
let mut budget = IntervalBudget::new(Bitrate::kbps(300), true);
let target_bytes =
(0.8 * (Bitrate::kbps(300) * Duration::from_millis(500)).as_bytes_i64() as f64) as i64;
while budget.bytes_remaining() < target_bytes {
budget.increase_budget(Duration::from_millis(50));
}
assert!(budget.budget_ratio() >= 0.80);
let use_amount =
(0.30 * (Bitrate::kbps(300) * Duration::from_millis(500)).as_bytes_i64() as f64) as i64;
budget.use_budget(DataSize::bytes(use_amount));
assert!(budget.budget_ratio() < 0.80);
assert!(budget.budget_ratio() > 0.40); }
#[test]
fn test_target_rate_getter() {
let budget = IntervalBudget::new(Bitrate::kbps(500), true);
assert_eq!(budget.target_rate(), Bitrate::kbps(500));
let mut budget = IntervalBudget::new(Bitrate::kbps(300), true);
budget.set_target_rate(Bitrate::kbps(600));
assert_eq!(budget.target_rate(), Bitrate::kbps(600));
}
}