use std::time::Instant; use super::budget::IntervalBudget; use crate::rtp_::{Bitrate, DataSize}; /// Application Limited Region detector. /// /// Detects when we're sending significantly less than network capacity, /// using IntervalBudget to handle bursty encoder traffic over 500ms windows. /// /// This is critical for real-world usage where encoders produce bursty traffic /// (keyframes, scene changes, temporal layers, etc.) The IntervalBudget smooths /// these bursts over a 500ms window to detect sustained application-limited state. /// /// ## ALR State Transitions /// /// - **Enter ALR**: When budget ratio > 0.80 (sustained low usage for 500ms) /// - **Exit ALR**: When budget ratio < 0.50 (increased usage) /// - The 30% hysteresis gap prevents rapid state flapping #[derive(Debug)] pub struct AlrDetector { /// Budget tracker with 500ms window budget: IntervalBudget, /// Current ALR state state: AlrState, /// Last time we sent bytes last_send_time: Option, // Configuration matching libWebRTC defaults /// Target send rate as fraction of estimate (0.65 = 65%) bandwidth_usage_ratio: f64, /// Budget ratio threshold to enter ALR (0.80 = 80%) start_budget_level_ratio: f64, /// Budget ratio threshold to exit ALR (0.50 = 50%) stop_budget_level_ratio: f64, } /// ALR state for the detector. #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum AlrState { /// Not currently in Application Limited Region NotInAlr, /// In Application Limited Region since the given instant InAlr(Instant), } impl AlrState { fn alr_start_time(&self) -> Option { match self { AlrState::InAlr(start) => Some(*start), AlrState::NotInAlr => None, } } } impl AlrDetector { /// Create a new ALR detector with libWebRTC default configuration. pub fn new() -> Self { Self { // Start with zero bitrate, will be updated via set_estimated_bitrate() // Use can_build_up_underuse=true for ALR detection budget: IntervalBudget::new(Bitrate::ZERO, true), state: AlrState::NotInAlr, last_send_time: None, // libWebRTC defaults from alr_detector.cc:30-49 bandwidth_usage_ratio: 0.65, start_budget_level_ratio: 0.80, stop_budget_level_ratio: 0.50, } } /// Update with bytes sent. /// /// Should be called for every media packet sent (excluding padding and probes). /// This updates the IntervalBudget and checks for ALR state transitions. pub fn on_bytes_sent(&mut self, bytes: DataSize, now: Instant) { // First call - just record time let Some(last) = self.last_send_time else { self.last_send_time = Some(now); return; }; // saturating_duration_since protects against time going backwards let delta = now.saturating_duration_since(last); self.last_send_time = Some(now); // Update budget based on time and usage self.budget.use_budget(bytes); self.budget.increase_budget(delta); // Check for state transitions with hysteresis let ratio = self.budget.budget_ratio(); match self.state { AlrState::NotInAlr => { if ratio > self.start_budget_level_ratio { // Enter ALR: budget accumulated because we're not sending much 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 { // Exit ALR: budget depleted because we're sending more self.state = AlrState::NotInAlr; trace!( "ALR: Exited ALR state (ratio={:.3} < threshold={:.3})", ratio, self.stop_budget_level_ratio ); } } } } /// Update the BWE estimate. /// /// This adjusts the IntervalBudget's target rate to 65% of the estimate. /// Should be called whenever the bandwidth estimate changes. pub fn set_estimated_bitrate(&mut self, estimate: Bitrate) { let target = estimate * self.bandwidth_usage_ratio; self.budget.set_target_rate(target); } /// Get the time when ALR started, if currently in ALR. /// /// Returns `Some(Instant)` if in ALR, `None` otherwise. /// Used by ProbeControl to determine if periodic ALR probes should be sent. pub fn alr_start_time(&self) -> Option { 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(); // Set estimate to 1 Mbps (target will be 650 kbps) alr.set_estimated_bitrate(Bitrate::mbps(1)); // Send very little data (way below target) // Over 500ms, we should accumulate budget for i in 0..50 { let t = now + Duration::from_millis(i * 10); // Send 100 bytes every 10ms = 80 kbps (way below 650 kbps target) alr.on_bytes_sent(DataSize::bytes(100), t); } // Should enter ALR 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)); // Enter ALR with low usage 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()); // Now send at high rate to exit ALR let start_exit = now + Duration::from_millis(500); for i in 0..50 { let t = start_exit + Duration::from_millis(i * 10); // Send 10,000 bytes every 10ms = 8 Mbps (way above 650 kbps target) alr.on_bytes_sent(DataSize::bytes(10_000), t); } // Should exit ALR 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)); // Send at exactly 65% (the target) - should not enter ALR for i in 0..100 { let t = now + Duration::from_millis(i * 10); // 65% of 1 Mbps = 650 kbps = 812.5 bytes per 10ms alr.on_bytes_sent(DataSize::bytes(812), t); } // Budget ratio should be near 0, not trigger ALR entry (needs > 0.80) assert!(alr.alr_start_time().is_none()); let ratio = alr.budget_ratio(); assert!(ratio < 0.80, "ratio={}", ratio); assert!(ratio > -0.20, "ratio={}", ratio); // Some tolerance } #[test] fn test_alr_handles_bursts() { let mut alr = AlrDetector::new(); let now = Instant::now(); alr.set_estimated_bitrate(Bitrate::mbps(1)); // Simulate bursty encoder: alternate between large and small frames let mut time = now; for i in 0..100 { time = time + Duration::from_millis(33); // ~30fps if i % 10 == 0 { // Keyframe: 10x normal size alr.on_bytes_sent(DataSize::bytes(50_000), time); } else { // Normal frame: small alr.on_bytes_sent(DataSize::bytes(5_000), time); } } // Average is about 9.5 KB per frame = 285 KB/s = 2.28 Mbps // This is above the 650 kbps target, so should NOT be in ALR // The IntervalBudget's 500ms window should smooth out the bursts assert!(alr.alr_start_time().is_none()); } #[test] fn test_alr_estimate_change() { let mut alr = AlrDetector::new(); let now = Instant::now(); // Start with 1 Mbps estimate (target = 650 kbps) alr.set_estimated_bitrate(Bitrate::mbps(1)); // Send at 200 kbps (way below 650 kbps target) to accumulate budget quickly for i in 0..100 { let t = now + Duration::from_millis(i * 10); alr.on_bytes_sent(DataSize::bytes(250), t); // 200 kbps } // Should enter ALR after sending well below target assert!(alr.alr_start_time().is_some()); // Now BWE drops to 300 kbps (target becomes 195 kbps) alr.set_estimated_bitrate(Bitrate::kbps(300)); // Continue sending at 250 kbps - now above target (195 kbps) 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); // 250 kbps } // Should exit ALR (now sending above target) assert!(alr.alr_start_time().is_none()); } #[test] fn test_alr_should_not_trigger_when_sending_at_target_rate() { // This test verifies that ALR doesn't incorrectly trigger when sending // at the target rate, even with small time advances between packets. let mut alr = AlrDetector::new(); let now = Instant::now(); // Set estimate to 8.25 Mbps (target will be 5.36 Mbps at 65%) let estimate = Bitrate::kbps(8250); alr.set_estimated_bitrate(estimate); // Send at exactly 8.25 Mbps with realistic packet timing // Packet: 1150 bytes // Interval: 1150 bytes × 8 bits / 8,250,000 bps = 1.115ms let packet_size = DataSize::bytes(1150); let packet_interval = Duration::from_micros(1115); // 1.115ms // Send 100 packets over ~111ms for i in 0..100 { let t = now + packet_interval * i; alr.on_bytes_sent(packet_size, t); } // Should NOT be in ALR - we're sending at 8.25 Mbps, which is above // the target threshold of 5.36 Mbps (65% of estimate) 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() { // This test verifies that ALR works correctly even when time advances // in very small increments (like the test harness's forced 0.2ms advances). let mut alr = AlrDetector::new(); let now = Instant::now(); // Set estimate to 8.25 Mbps let estimate = Bitrate::kbps(8250); alr.set_estimated_bitrate(estimate); // Simulate sending 1150 byte packets at 8.25 Mbps // But with spurious 0.2ms time advances between on_bytes_sent calls let packet_size = DataSize::bytes(1150); let spurious_advance = Duration::from_micros(200); // 0.2ms let mut time = now; // Send packets - but accumulate proper time even if called frequently for _ in 0..100 { alr.on_bytes_sent(packet_size, time); time += spurious_advance; // Small spurious advance } // With 100 packets over 20ms (100 × 0.2ms), we've sent: // 115,000 bytes in 20ms = 46 Mbps // This is WAY above target, so ALR should NOT trigger assert!( alr.alr_start_time().is_none(), "ALR should not trigger with spurious small advances when sending fast. Budget ratio: {:.3}", alr.budget_ratio() ); } }