File
Blob: firmware/vendor/str0m/src/bwe/mod.rs
| 1 | //! Google Congestion Control (GoogCC) Bandwidth Estimation based on TWCC feedback. |
| 2 | //! |
| 3 | //! This implementation is ported from libWebRTC's GoogCC and goes beyond the simplified |
| 4 | //! IETF draft (<https://datatracker.ietf.org/doc/html/draft-ietf-rmcat-gcc-02>) to include |
| 5 | //! WebRTC's production features: |
| 6 | //! |
| 7 | //! - Delay-based control (trendline estimator with AIMD rate control) |
| 8 | //! - Loss-based control (with inherent loss rate estimation) |
| 9 | //! - Probe controller with state machine and multi-stage probing strategy |
| 10 | //! - ALR (Application Limited Region) detection and periodic probing |
| 11 | //! - Link capacity estimation from ALR probes |
| 12 | //! |
| 13 | //! The probe controller in particular closely matches WebRTC's `ProbeController` behavior |
| 14 | //! and default constants, enabling compatible bandwidth discovery with WebRTC endpoints. |
| 15 | |
| 16 | use std::cmp::Ordering; |
| 17 | use std::fmt; |
| 18 | use std::time::{Duration, Instant}; |
| 19 | |
| 20 | use crate::Reason; |
| 21 | use crate::rtp_::{Bitrate, DataSize, TwccClusterId, TwccSendRecord, TwccSeq}; |
| 22 | use crate::util::Soonest; |
| 23 | |
| 24 | mod acked_bitrate_estimator; |
| 25 | mod alr_detector; |
| 26 | pub(crate) mod api; |
| 27 | mod delay; |
| 28 | mod link_capacity_estimator; |
| 29 | mod loss_controller; |
| 30 | mod macros; |
| 31 | mod probe; |
| 32 | mod smoother; |
| 33 | mod time; |
| 34 | |
| 35 | use acked_bitrate_estimator::AckedBitrateEstimator; |
| 36 | use alr_detector::AlrDetector; |
| 37 | use delay::DelayController; |
| 38 | use link_capacity_estimator::LinkCapacityEstimator; |
| 39 | use loss_controller::{LossController, LossControllerState}; |
| 40 | use macros::log_loss; |
| 41 | use smoother::EstimateSmoother; |
| 42 | |
| 43 | pub(crate) use macros::{log_pacer_media_debt, log_pacer_padding_debt}; |
| 44 | pub(crate) use probe::{BandwidthLimitedCause, ProbeEstimator}; |
| 45 | pub(crate) use probe::{ProbeClusterState, ProbeControl}; |
| 46 | |
| 47 | #[cfg(feature = "_internal_test_exports")] |
| 48 | pub use probe::ProbeClusterConfig; |
| 49 | #[cfg(not(feature = "_internal_test_exports"))] |
| 50 | pub(crate) use probe::ProbeClusterConfig; |
| 51 | |
| 52 | const INITIAL_BITRATE_WINDOW: Duration = Duration::from_millis(500); |
| 53 | const BITRATE_WINDOW: Duration = Duration::from_millis(150); |
| 54 | const STARTUP_PHASE: Duration = Duration::from_secs(2); |
| 55 | |
| 56 | pub struct Bwe { |
| 57 | bwe: SendSideBandwidthEstimator, |
| 58 | desired_bitrate: Bitrate, |
| 59 | smoother: EstimateSmoother, |
| 60 | } |
| 61 | |
| 62 | impl Bwe { |
| 63 | pub fn new(initial: Bitrate) -> Self { |
| 64 | let send_side_bwe = SendSideBandwidthEstimator::new(initial); |
| 65 | Bwe { |
| 66 | bwe: send_side_bwe, |
| 67 | desired_bitrate: Bitrate::ZERO, |
| 68 | smoother: EstimateSmoother::new(), |
| 69 | } |
| 70 | } |
| 71 | |
| 72 | pub fn handle_timeout(&mut self, now: Instant, do_probe: bool) -> Option<ProbeClusterConfig> { |
| 73 | let result = self.bwe.handle_timeout(self.desired_bitrate, do_probe, now); |
| 74 | if let Some(estimate) = self.bwe.last_estimate() { |
| 75 | self.smoother.record(now, estimate); |
| 76 | } |
| 77 | result |
| 78 | } |
| 79 | |
| 80 | pub fn start_probe(&mut self, config: ProbeClusterConfig, now: Instant) -> bool { |
| 81 | self.bwe.start_probe(config, now) |
| 82 | } |
| 83 | |
| 84 | pub fn end_probe(&mut self, now: Instant, cluster_id: TwccClusterId) { |
| 85 | self.bwe.end_probe(now, cluster_id); |
| 86 | } |
| 87 | |
| 88 | pub fn reset(&mut self, init_bitrate: Bitrate) { |
| 89 | self.bwe.reset(init_bitrate); |
| 90 | } |
| 91 | |
| 92 | pub fn update<'t>( |
| 93 | &mut self, |
| 94 | records: impl Iterator<Item = &'t crate::rtp_::TwccSendRecord>, |
| 95 | now: Instant, |
| 96 | ) { |
| 97 | self.bwe.update(records, now); |
| 98 | } |
| 99 | |
| 100 | pub fn poll_estimate(&mut self) -> Option<Bitrate> { |
| 101 | self.smoother.poll() |
| 102 | } |
| 103 | |
| 104 | pub fn poll_timeout(&self) -> (Option<Instant>, Reason) { |
| 105 | self.bwe.poll_timeout() |
| 106 | } |
| 107 | |
| 108 | pub fn last_estimate(&self) -> Option<Bitrate> { |
| 109 | self.bwe.last_estimate() |
| 110 | } |
| 111 | |
| 112 | pub fn on_media_sent(&mut self, payload_size: DataSize, is_padding: bool, now: Instant) { |
| 113 | if !is_padding { |
| 114 | // Update ALR detector with media bytes sent |
| 115 | self.bwe.on_media_sent(payload_size, now); |
| 116 | } |
| 117 | } |
| 118 | |
| 119 | pub fn is_overusing(&self) -> bool { |
| 120 | self.bwe.is_overusing() |
| 121 | } |
| 122 | |
| 123 | pub fn set_desired_bitrate(&mut self, v: Bitrate) { |
| 124 | self.desired_bitrate = v; |
| 125 | } |
| 126 | } |
| 127 | |
| 128 | struct SendSideBandwidthEstimator { |
| 129 | delay_controller: DelayController, |
| 130 | loss_controller: LossController, |
| 131 | acked_bitrate_estimator: AckedBitrateEstimator, |
| 132 | probe_control: ProbeControl, |
| 133 | probe_estimator: ProbeEstimator, |
| 134 | started_at: Option<Instant>, |
| 135 | alr_detector: AlrDetector, |
| 136 | link_capacity_estimator: LinkCapacityEstimator, |
| 137 | last_updated_estimate: Option<Bitrate>, |
| 138 | } |
| 139 | |
| 140 | impl SendSideBandwidthEstimator { |
| 141 | pub fn new(initial_bitrate: Bitrate) -> Self { |
| 142 | let mut alr_detector = AlrDetector::new(); |
| 143 | alr_detector.set_estimated_bitrate(initial_bitrate); |
| 144 | |
| 145 | let mut loss_controller = LossController::new(); |
| 146 | loss_controller.set_bandwidth_estimate(initial_bitrate); |
| 147 | |
| 148 | Self { |
| 149 | delay_controller: DelayController::new(initial_bitrate), |
| 150 | loss_controller, |
| 151 | acked_bitrate_estimator: AckedBitrateEstimator::new( |
| 152 | INITIAL_BITRATE_WINDOW, |
| 153 | BITRATE_WINDOW, |
| 154 | ), |
| 155 | probe_control: ProbeControl::new(), |
| 156 | probe_estimator: ProbeEstimator::new(), |
| 157 | started_at: None, |
| 158 | alr_detector, |
| 159 | link_capacity_estimator: LinkCapacityEstimator::new(), |
| 160 | last_updated_estimate: None, |
| 161 | } |
| 162 | } |
| 163 | |
| 164 | /// Whether the delay-based detector currently signals overuse. |
| 165 | /// |
| 166 | /// This is useful for gating behaviors (like padding/probing) that would otherwise |
| 167 | /// re-excite the system while we're already congested. |
| 168 | pub fn is_overusing(&self) -> bool { |
| 169 | self.delay_controller.is_overusing() |
| 170 | } |
| 171 | |
| 172 | /// Update ALR detector with actual bytes sent. |
| 173 | /// |
| 174 | /// Should be called for media packets (not padding/probes). |
| 175 | /// This is typically called from the session's packet sending logic. |
| 176 | pub fn on_media_sent(&mut self, bytes: DataSize, now: Instant) { |
| 177 | self.alr_detector.on_bytes_sent(bytes, now); |
| 178 | } |
| 179 | |
| 180 | /// Record a packet from a TWCC report. |
| 181 | pub fn update<'t>(&mut self, records: impl Iterator<Item = &'t TwccSendRecord>, now: Instant) { |
| 182 | let _ = self.started_at.get_or_insert(now); |
| 183 | |
| 184 | let send_records: Vec<_> = records.collect(); |
| 185 | |
| 186 | // Feed records to probe estimator for analysis and process any new probe results |
| 187 | let mut latest_probe_result = None; |
| 188 | for (config, bitrate) in self.probe_estimator.update(send_records.iter().copied()) { |
| 189 | latest_probe_result = Some(bitrate); |
| 190 | |
| 191 | // Update link capacity estimator for every successful ALR probe, not just the latest. |
| 192 | // The estimator internally takes the max of all probe results, building up knowledge |
| 193 | // of proven link capacity. This differs from the delay controller, which only receives |
| 194 | // the latest probe result (matching WebRTC's FetchAndResetLastEstimatedBitrate behavior). |
| 195 | if config.is_alr_probe() { |
| 196 | self.link_capacity_estimator.update_from_probe(bitrate, now); |
| 197 | } |
| 198 | } |
| 199 | |
| 200 | let mut acked_packets = vec![]; |
| 201 | |
| 202 | let mut max_rtt = None; |
| 203 | let mut count = 0; |
| 204 | let mut lost = 0; |
| 205 | for record in send_records.iter() { |
| 206 | count += 1; |
| 207 | let Ok(acked_packet) = (*record).try_into() else { |
| 208 | lost += 1; |
| 209 | continue; |
| 210 | }; |
| 211 | acked_packets.push(acked_packet); |
| 212 | max_rtt = max_rtt.max(record.rtt()); |
| 213 | } |
| 214 | acked_packets.sort_by(AckedPacket::order_by_receive_time); |
| 215 | |
| 216 | for acked_packet in acked_packets.iter() { |
| 217 | self.acked_bitrate_estimator |
| 218 | .update(acked_packet.remote_recv_time, acked_packet.size); |
| 219 | } |
| 220 | |
| 221 | let acked_bitrate = self.acked_bitrate_estimator.current_estimate(); |
| 222 | |
| 223 | // Use the latest probe result from this update, if any |
| 224 | let probe_result = latest_probe_result; |
| 225 | |
| 226 | let is_probe_result = probe_result.is_some(); |
| 227 | |
| 228 | // Update delay controller with the latest probe result |
| 229 | let maybe_estimate = |
| 230 | self.delay_controller |
| 231 | .update(&acked_packets, acked_bitrate, probe_result, now); |
| 232 | |
| 233 | let Some(delay_estimate) = maybe_estimate else { |
| 234 | return; |
| 235 | }; |
| 236 | |
| 237 | let loss = if count == 0 { |
| 238 | 0.0 |
| 239 | } else { |
| 240 | lost as f64 / count as f64 |
| 241 | }; |
| 242 | log_loss!(loss); |
| 243 | |
| 244 | // During startup with no loss, use delay-based estimate directly |
| 245 | if in_startup_phase(self.started_at, now) && loss <= 0.001 { |
| 246 | self.loss_controller.set_bandwidth_estimate(delay_estimate); |
| 247 | return; |
| 248 | } |
| 249 | |
| 250 | // When probe succeeds, set bandwidth directly |
| 251 | if is_probe_result { |
| 252 | self.loss_controller.set_bandwidth_estimate(delay_estimate); |
| 253 | } |
| 254 | |
| 255 | if let Some(acked_bitrate) = acked_bitrate { |
| 256 | self.loss_controller.set_acknowledged_bitrate(acked_bitrate); |
| 257 | } |
| 258 | |
| 259 | // This corresponds to UpdateLossBasedEstimator + UpdateEstimate |
| 260 | self.loss_controller |
| 261 | .update_bandwidth_estimate(&send_records, delay_estimate); |
| 262 | |
| 263 | // Loss-based result is capped by delay_based_limit |
| 264 | let loss_result = self.loss_controller.loss_based_result(); |
| 265 | if let Some(loss_estimate) = loss_result.bandwidth_estimate { |
| 266 | if loss_estimate > delay_estimate { |
| 267 | // Loss controller produced higher estimate than delay controller |
| 268 | // Cap it at delay estimate (delay controller is the upper limit) |
| 269 | self.loss_controller.set_bandwidth_estimate(delay_estimate); |
| 270 | } |
| 271 | } |
| 272 | |
| 273 | // Feed the (possibly combined) estimate into subcomponents wanting it. |
| 274 | self.propagate_estimate(); |
| 275 | } |
| 276 | |
| 277 | pub fn poll_timeout(&self) -> (Option<Instant>, Reason) { |
| 278 | let delay_timeout = Some(self.delay_controller.poll_timeout()); |
| 279 | let probe_timeout = Some(self.probe_control.poll_timeout()); |
| 280 | let probe_estimator_timeout = Some(self.probe_estimator.poll_timeout()); |
| 281 | (delay_timeout, Reason::BweDelayControl) |
| 282 | .soonest((probe_timeout, Reason::BweProbeControl)) |
| 283 | .soonest((probe_estimator_timeout, Reason::BweProbeEstimator)) |
| 284 | } |
| 285 | |
| 286 | /// Handle periodic timeout for BWE components. |
| 287 | pub fn handle_timeout( |
| 288 | &mut self, |
| 289 | desired_bitrate: Bitrate, |
| 290 | do_probe: bool, |
| 291 | now: Instant, |
| 292 | ) -> Option<ProbeClusterConfig> { |
| 293 | self.delay_controller |
| 294 | .handle_timeout(self.acked_bitrate_estimator.current_estimate(), now); |
| 295 | |
| 296 | // Update probe control with desired bitrate. |
| 297 | self.probe_control.set_desired_bitrate(desired_bitrate); |
| 298 | |
| 299 | // Get ALR state and forward to both probe control and loss controller |
| 300 | let alr_start_time = self.alr_detector.alr_start_time(); |
| 301 | if let Some(t) = alr_start_time { |
| 302 | self.probe_control.set_alr_start_time(t); |
| 303 | } else { |
| 304 | self.probe_control.set_alr_stop_time(now); |
| 305 | } |
| 306 | |
| 307 | self.loss_controller.set_alr_start_time(alr_start_time); |
| 308 | |
| 309 | // Get link capacity estimate and forward to loss controller. |
| 310 | let link_capacity = self.link_capacity_estimator.capacity_estimate(now); |
| 311 | self.loss_controller |
| 312 | .set_link_capacity_estimate(link_capacity); |
| 313 | |
| 314 | // Clean up expired probe cluster state |
| 315 | self.probe_estimator.handle_timeout(now); |
| 316 | |
| 317 | // Feed the current estimate into subcontrollers, if it changed. |
| 318 | self.propagate_estimate(); |
| 319 | |
| 320 | // If we can't probe, clear any pending/active probes |
| 321 | if !do_probe { |
| 322 | self.probe_estimator.clear_probes(); |
| 323 | } |
| 324 | |
| 325 | self.probe_control.enable(do_probe); |
| 326 | |
| 327 | // Timer-driven probe logic (WebRTC `Process()` equivalent). |
| 328 | self.probe_control.handle_timeout(now) |
| 329 | } |
| 330 | |
| 331 | fn propagate_estimate(&mut self) { |
| 332 | // Do we have a value? |
| 333 | let Some(estimate) = self.last_estimate() else { |
| 334 | return; |
| 335 | }; |
| 336 | // Did it change? |
| 337 | if self.last_updated_estimate == Some(estimate) { |
| 338 | return; |
| 339 | } |
| 340 | |
| 341 | let cause = self.bandwidth_limited_cause(); |
| 342 | |
| 343 | self.probe_control.set_estimated_bitrate(estimate, cause); |
| 344 | self.alr_detector.set_estimated_bitrate(estimate); |
| 345 | |
| 346 | // Don't update until this changes. |
| 347 | self.last_updated_estimate = Some(estimate); |
| 348 | } |
| 349 | |
| 350 | fn bandwidth_limited_cause(&self) -> BandwidthLimitedCause { |
| 351 | if self.delay_controller.is_overusing() { |
| 352 | return BandwidthLimitedCause::DelayBasedLimitedDelayIncreased; |
| 353 | } |
| 354 | |
| 355 | match self.loss_controller.loss_based_result().state { |
| 356 | LossControllerState::DelayBased => BandwidthLimitedCause::DelayBasedLimited, |
| 357 | LossControllerState::Increasing => BandwidthLimitedCause::LossLimitedBweIncreasing, |
| 358 | LossControllerState::Decreasing => BandwidthLimitedCause::LossLimitedBwe, |
| 359 | } |
| 360 | } |
| 361 | |
| 362 | /// Get the latest estimate. |
| 363 | pub fn last_estimate(&self) -> Option<Bitrate> { |
| 364 | let delay_estimate = self.delay_controller.last_estimate(); |
| 365 | |
| 366 | let loss_result = self.loss_controller.loss_based_result(); |
| 367 | |
| 368 | // Only apply loss-based limiting when actively in a loss-limiting state |
| 369 | match loss_result.state { |
| 370 | LossControllerState::DelayBased => { |
| 371 | // Loss controller defers to delay-based estimate |
| 372 | delay_estimate |
| 373 | } |
| 374 | LossControllerState::Decreasing | LossControllerState::Increasing => { |
| 375 | // Loss controller is actively limiting or recovering |
| 376 | match (delay_estimate, loss_result.bandwidth_estimate) { |
| 377 | (Some(de), Some(le)) => Some(de.min(le)), |
| 378 | (None, le @ Some(_)) => le, |
| 379 | (de @ Some(_), None) => de, |
| 380 | (None, None) => None, |
| 381 | } |
| 382 | } |
| 383 | } |
| 384 | } |
| 385 | |
| 386 | /// Start analyzing a probe cluster. |
| 387 | /// |
| 388 | /// This should be called when the pacer starts sending a probe cluster, |
| 389 | /// to tell the estimator which cluster to watch for in TWCC feedback. |
| 390 | /// Returns `true` if the probe was started, `false` if rejected. |
| 391 | pub fn start_probe(&mut self, config: ProbeClusterConfig, now: Instant) -> bool { |
| 392 | self.probe_estimator.probe_start(config, now) |
| 393 | } |
| 394 | |
| 395 | /// End a probe cluster and mark it for cleanup. |
| 396 | /// |
| 397 | /// This should be called when the pacer finishes sending a probe cluster. |
| 398 | /// The estimator will continue collecting feedback for a cluster history period |
| 399 | /// to allow late-arriving TWCC reports to refine the estimate. |
| 400 | pub fn end_probe(&mut self, now: Instant, cluster_id: TwccClusterId) { |
| 401 | self.probe_estimator.end_probe(now, cluster_id); |
| 402 | } |
| 403 | |
| 404 | pub fn reset(&mut self, init_bitrate: Bitrate) { |
| 405 | *self = Self::new(init_bitrate); |
| 406 | } |
| 407 | } |
| 408 | |
| 409 | /// A RTP packet that has been sent and acknowledged by the receiver in a TWCC report. |
| 410 | #[derive(Debug, Copy, Clone)] |
| 411 | pub struct AckedPacket { |
| 412 | /// The TWCC sequence number |
| 413 | seq_no: TwccSeq, |
| 414 | /// The size of the packets in bytes. |
| 415 | size: DataSize, |
| 416 | /// When we sent the packet |
| 417 | local_send_time: Instant, |
| 418 | /// When the packet was received at the remote, note this Instant is only usable with other |
| 419 | /// instants of the same type i.e. those that represent a TWCC reported receive time for this |
| 420 | /// session. |
| 421 | remote_recv_time: Instant, |
| 422 | /// The local time when received confirmation that the other side received the seq i.e. when we |
| 423 | /// received the TWCC report for this packet. |
| 424 | local_recv_time: Instant, |
| 425 | } |
| 426 | |
| 427 | impl AckedPacket { |
| 428 | fn rtt(&self) -> Duration { |
| 429 | self.local_recv_time - self.local_send_time |
| 430 | } |
| 431 | |
| 432 | fn order_by_receive_time(lhs: &Self, rhs: &Self) -> Ordering { |
| 433 | if lhs.remote_recv_time != rhs.remote_recv_time { |
| 434 | lhs.remote_recv_time.cmp(&rhs.remote_recv_time) |
| 435 | } else if lhs.local_send_time != rhs.local_send_time { |
| 436 | lhs.local_send_time.cmp(&rhs.local_send_time) |
| 437 | } else { |
| 438 | lhs.seq_no.cmp(&rhs.seq_no) |
| 439 | } |
| 440 | } |
| 441 | } |
| 442 | |
| 443 | // NB: Extracted for lifetime reasons |
| 444 | fn in_startup_phase(started_at: Option<Instant>, now: Instant) -> bool { |
| 445 | started_at |
| 446 | .map(|s| now.duration_since(s) <= STARTUP_PHASE) |
| 447 | .unwrap_or(false) |
| 448 | } |
| 449 | |
| 450 | impl TryFrom<&TwccSendRecord> for AckedPacket { |
| 451 | type Error = (); |
| 452 | |
| 453 | fn try_from(value: &TwccSendRecord) -> Result<Self, Self::Error> { |
| 454 | let Some(remote_recv_time) = value.remote_recv_time() else { |
| 455 | return Err(()); |
| 456 | }; |
| 457 | let Some(local_recv_time) = value.local_recv_time() else { |
| 458 | return Err(()); |
| 459 | }; |
| 460 | |
| 461 | Ok(Self { |
| 462 | seq_no: value.seq(), |
| 463 | size: value.size().into(), |
| 464 | local_send_time: value.local_send_time(), |
| 465 | remote_recv_time, |
| 466 | local_recv_time, |
| 467 | }) |
| 468 | } |
| 469 | } |
| 470 | |
| 471 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] |
| 472 | enum BandwidthUsage { |
| 473 | Overuse, |
| 474 | Normal, |
| 475 | Underuse, |
| 476 | } |
| 477 | |
| 478 | impl fmt::Display for BandwidthUsage { |
| 479 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
| 480 | match self { |
| 481 | BandwidthUsage::Overuse => write!(f, "overuse"), |
| 482 | BandwidthUsage::Normal => write!(f, "normal"), |
| 483 | BandwidthUsage::Underuse => write!(f, "underuse"), |
| 484 | } |
| 485 | } |
| 486 | } |