Skip to main content

rtc_interceptor/gcc/
estimator.rs

1//! `Gcc`: the delay half and the loss half, combined.
2
3use super::loss::LossController;
4use super::overuse::OveruseDetector;
5use super::rate_calc::RateCalculator;
6use super::rate_control::RateController;
7use super::slope::SlopeEstimator;
8use crate::cc::estimator::{BandwidthEstimator, EstimatorStats};
9use crate::rtpfb::acknowledgement::PacketReport;
10use std::time::Instant;
11
12/// Rate to start from when nothing is known yet.
13pub const DEFAULT_INITIAL_BITRATE: f64 = 300_000.0;
14/// Floor. Below this a video call is not worth having.
15pub const DEFAULT_MIN_BITRATE: f64 = 100_000.0;
16/// Ceiling.
17pub const DEFAULT_MAX_BITRATE: f64 = 100_000_000.0;
18
19/// Google Congestion Control.
20///
21/// # The two halves
22///
23/// ```text
24/// reports ─┬─▶ SlopeEstimator ─▶ OveruseDetector ─▶ RateController ─┐
25///          │   group, filter      usage + adaptive   AIMD           ├─▶ min ─▶ target
26///          │                      threshold                         │
27///          ├─▶ RateCalculator ────────────────────────────────────▶─┘
28///          │   what is actually arriving
29///          └─▶ LossController ────────────────────────────────────▶─┘
30/// ```
31///
32/// The two are combined by **`min`**: whichever signal is more pessimistic wins. A path can be
33/// congested in either way independently — a deep buffer queues without losing, a shallow or lossy
34/// one loses without queueing — so neither half alone is sufficient and taking the lower of the two
35/// is the only combination that responds to both.
36///
37/// # Deliberate divergences from upstream
38///
39/// - **D3 — one clamp.** Applied once, here, from configuration. Upstream clamps inside the loss
40///   controller to a hard-coded 100 kb/s–100 Mb/s *and* again in the rate controller to the
41///   configured range, and the two disagree.
42/// - **D4 — loss may move the target alone.** See [`LossController`].
43/// - **No hidden headroom.** [`target_bitrate`](Self::target_bitrate) is what the pacer is asked
44///   for. Upstream's pacer silently multiplies by 1.5, so its reported target and its wire rate
45///   differ by half again.
46/// - **No clock.** Every instant is a parameter, which is what makes a bitrate trajectory
47///   reproducible.
48pub struct Gcc {
49    slope: SlopeEstimator,
50    detector: OveruseDetector,
51    rate: RateCalculator,
52    delay_control: RateController,
53    loss_control: LossController,
54    min: f64,
55    max: f64,
56    target: f64,
57}
58
59impl Default for Gcc {
60    fn default() -> Self {
61        Self::new(
62            DEFAULT_INITIAL_BITRATE,
63            DEFAULT_MIN_BITRATE,
64            DEFAULT_MAX_BITRATE,
65        )
66    }
67}
68
69impl Gcc {
70    /// An estimator starting at `initial`, held within `min..=max`.
71    pub fn new(initial: f64, min: f64, max: f64) -> Self {
72        let initial = initial.clamp(min, max);
73        Self {
74            slope: SlopeEstimator::new(),
75            detector: OveruseDetector::new(),
76            rate: RateCalculator::default(),
77            delay_control: RateController::new(initial, min, max),
78            loss_control: LossController::new(initial, min, max),
79            min,
80            max,
81            target: initial,
82        }
83    }
84
85    /// The delay-based half's current target, in bits per second.
86    pub fn delay_based_bitrate(&self) -> f64 {
87        self.delay_control.target()
88    }
89
90    /// The loss-based half's current target, in bits per second.
91    pub fn loss_based_bitrate(&self) -> f64 {
92        self.loss_control.target()
93    }
94}
95
96impl BandwidthEstimator for Gcc {
97    fn on_reports(&mut self, now: Instant, reports: &[PacketReport]) {
98        if reports.is_empty() {
99            return;
100        }
101
102        // Delay: group, filter, detect, control.
103        let mut usage = self.detector.usage();
104        for report in reports {
105            if report.arrived {
106                self.rate.add(now, report.size);
107            }
108            if let Some(trend) = self.slope.accumulate(report) {
109                usage = self.detector.update(trend.at, trend.estimate_ms);
110            }
111        }
112
113        let received = self.rate.rate_bits_per_second(now);
114        let delay_target = self.delay_control.update(now, usage, received);
115
116        // Loss: a fraction over this batch.
117        let lost = reports.iter().filter(|report| !report.arrived).count();
118        let loss_target = self.loss_control.update(now, lost, reports.len());
119
120        // Whichever half is more pessimistic wins, clamped once (D3).
121        self.target = delay_target.min(loss_target).clamp(self.min, self.max);
122    }
123
124    fn target_bitrate(&self) -> f64 {
125        self.target
126    }
127
128    fn stats(&self) -> EstimatorStats {
129        EstimatorStats {
130            delay_based_bitrate: Some(self.delay_control.target()),
131            loss_based_bitrate: Some(self.loss_control.target()),
132            packet_loss: self.loss_control.average_loss(),
133            round_trip_time: None,
134        }
135    }
136}
137
138#[cfg(test)]
139mod tests {
140    use super::*;
141    use rtcp::transport_feedbacks::cc_feedback_report::Ecn;
142    use std::time::Duration;
143
144    fn report(id: u64, departure: Instant, arrival_ms: Option<u64>) -> PacketReport {
145        PacketReport {
146            ssrc: 1,
147            id,
148            rtp_sequence_number: id as u16,
149            is_twcc: true,
150            twcc_sequence_number: id as u16,
151            size: 1500,
152            arrived: arrival_ms.is_some(),
153            departure,
154            arrival: arrival_ms.map(Duration::from_millis),
155            ecn: Ecn::default(),
156        }
157    }
158
159    /// Empty feedback says nothing and must not move anything.
160    #[test]
161    fn empty_feedback_changes_nothing() {
162        let epoch = Instant::now();
163        let mut gcc = Gcc::default();
164        let before = gcc.target_bitrate();
165
166        gcc.on_reports(epoch, &[]);
167
168        assert_eq!(before, gcc.target_bitrate());
169    }
170
171    /// The more pessimistic half wins. Here loss is catastrophic while delay is quiet, so the
172    /// combined target must follow loss — which is the whole point of the `min`.
173    #[test]
174    fn the_more_pessimistic_half_wins() {
175        let epoch = Instant::now();
176        let mut gcc = Gcc::default();
177
178        let mut at;
179        for batch in 0..20u64 {
180            at = epoch + Duration::from_millis(batch * 200);
181            // Half the packets vanish; the ones that arrive do so promptly.
182            let reports: Vec<PacketReport> = (0..10)
183                .map(|index| {
184                    let id = batch * 10 + index;
185                    let departure = at + Duration::from_millis(index * 10);
186                    let arrival = (index % 2 == 0).then(|| batch * 200 + index * 10 + 20);
187                    report(id, departure, arrival)
188                })
189                .collect();
190            gcc.on_reports(at, &reports);
191        }
192
193        assert!(
194            gcc.target_bitrate() <= gcc.loss_based_bitrate() + 1.0,
195            "the combined target must not exceed the loss half: {} vs {}",
196            gcc.target_bitrate(),
197            gcc.loss_based_bitrate()
198        );
199        assert!(
200            gcc.target_bitrate() < DEFAULT_INITIAL_BITRATE,
201            "50% loss must bring the target down, got {}",
202            gcc.target_bitrate()
203        );
204    }
205
206    /// One clamp, applied once at the combination point (D3).
207    #[test]
208    fn the_target_stays_within_configured_bounds() {
209        let epoch = Instant::now();
210        let mut gcc = Gcc::new(200_000.0, 150_000.0, 250_000.0);
211
212        let mut at;
213        for batch in 0..50u64 {
214            at = epoch + Duration::from_millis(batch * 200);
215            let reports: Vec<PacketReport> = (0..10)
216                .map(|index| {
217                    let id = batch * 10 + index;
218                    let departure = at + Duration::from_millis(index * 10);
219                    report(id, departure, Some(batch * 200 + index * 10 + 20))
220                })
221                .collect();
222            gcc.on_reports(at, &reports);
223
224            assert!(
225                (150_000.0..=250_000.0).contains(&gcc.target_bitrate()),
226                "target left its bounds: {}",
227                gcc.target_bitrate()
228            );
229        }
230    }
231
232    /// The stats expose both halves, so an application can see *why* the target is where it is
233    /// rather than only what it is.
234    #[test]
235    fn stats_report_both_halves() {
236        let epoch = Instant::now();
237        let mut gcc = Gcc::default();
238        gcc.on_reports(epoch, &[report(0, epoch, Some(20)), report(1, epoch, None)]);
239
240        let stats = gcc.stats();
241        assert!(stats.delay_based_bitrate.is_some());
242        assert!(stats.loss_based_bitrate.is_some());
243        assert!(stats.packet_loss.is_some());
244    }
245}