Skip to main content

rtc_interceptor/cc/
estimator.rs

1//! The seam a congestion control algorithm plugs into.
2
3use crate::rtpfb::acknowledgement::PacketReport;
4use std::time::Instant;
5
6/// A congestion control algorithm: acknowledged packets in, a target bitrate out.
7///
8/// # Why this is not an `Interceptor`
9///
10/// An estimator does not transform packets, it observes them. Making it an interceptor would give
11/// it a position in the chain ordering it does not need, four bind methods it has no use for, and —
12/// fatally — bury it inside a `Box<dyn Interceptor>` the application cannot reach.
13/// `CongestionControlInterceptor` is the interceptor; this is the algorithm it drives.
14///
15/// # Why the seam is here rather than at the RTCP packet
16///
17/// Upstream's equivalent interface has six methods, of which one returns a writer, one takes raw
18/// RTCP, and one takes a callback. A custom estimator there must parse feedback, own a history of
19/// sent packets, own a pacer, and correctly wrap a writer — four responsibilities that have nothing
20/// to do with the algorithm, reimplemented per algorithm.
21///
22/// Here [`PacketReport`] is already resolved by [`History`](crate::History): departure joined with
23/// arrival, per packet, in send order. What is left is a function from acknowledgements to a number,
24/// which is what a congestion control algorithm actually is.
25///
26/// # Clocks
27///
28/// There are none. Every instant arrives as a parameter, so a test can pin an exact bitrate
29/// trajectory for a given feedback sequence rather than asserting that something eventually
30/// happens.
31pub trait BandwidthEstimator: Send + Sync {
32    /// Packets whose fate the remote has now reported, in send order.
33    ///
34    /// Each report carries the instant it *left* this endpoint — the pacer's release instant, not
35    /// the instant the application enqueued it — the instant it arrived on the receiver's clock,
36    /// its size, and whether it arrived at all. That is everything a delay-based or loss-based
37    /// estimator needs.
38    ///
39    /// May be called with an empty slice; an implementation should treat that as "no news".
40    fn on_reports(&mut self, now: Instant, reports: &[PacketReport]);
41
42    /// The current estimate, in bits per second.
43    ///
44    /// Read after every [`on_reports`](Self::on_reports) and after every
45    /// [`handle_timeout`](Self::handle_timeout); a change is what reaches the pacer.
46    fn target_bitrate(&self) -> f64;
47
48    /// Periodic work, for an estimator that has any. Most do not.
49    fn handle_timeout(&mut self, _now: Instant) {}
50
51    /// When this estimator next wants waking, or `None` if it does not.
52    ///
53    /// `None` when idle, and the instant must advance — a deadline at or before the `now` just
54    /// handed to [`handle_timeout`](Self::handle_timeout) is a busy-loop that wakes the whole chain.
55    fn poll_timeout(&self) -> Option<Instant> {
56        None
57    }
58
59    /// Whatever the algorithm wants to expose. Never load-bearing.
60    fn stats(&self) -> EstimatorStats {
61        EstimatorStats::default()
62    }
63}
64
65/// What an estimator is willing to say about itself.
66///
67/// Deliberately a struct rather than upstream's `map[string]any`: a stringly-typed bag cannot be
68/// read without knowing what the implementation happened to put in it. Fields are optional because
69/// an estimator that does not compute one should say so rather than report a zero that reads like a
70/// measurement.
71#[derive(Debug, Clone, Copy, Default, PartialEq)]
72#[non_exhaustive]
73pub struct EstimatorStats {
74    /// The delay-based half of the estimate, in bits per second.
75    pub delay_based_bitrate: Option<f64>,
76    /// The loss-based half of the estimate, in bits per second.
77    pub loss_based_bitrate: Option<f64>,
78    /// Fraction of packets the remote reported lost, over whatever window the estimator uses.
79    pub packet_loss: Option<f64>,
80    /// Round trip time implied by the most recent feedback.
81    pub round_trip_time: Option<std::time::Duration>,
82}
83
84/// An estimator that always says the same number.
85///
86/// Not a placeholder. It is the proof that [`BandwidthEstimator`] is usable with two methods, and
87/// it is what the interceptor's own tests drive so that the interceptor's behaviour — recording
88/// departures, resolving feedback, attaching the attribute — is separable from any algorithm's.
89///
90/// It is also a legitimate configuration: a fixed rate with a pacer in front of it is what an
91/// application wants when the path is known and it would rather not have an algorithm second-guess
92/// it.
93#[derive(Debug, Clone, Copy)]
94pub struct ConstantBitrate {
95    bits_per_second: f64,
96}
97
98impl ConstantBitrate {
99    /// An estimator fixed at `bits_per_second`.
100    pub fn new(bits_per_second: f64) -> Self {
101        Self { bits_per_second }
102    }
103}
104
105impl BandwidthEstimator for ConstantBitrate {
106    /// Nothing to learn from: the answer does not depend on the question.
107    fn on_reports(&mut self, _now: Instant, _reports: &[PacketReport]) {}
108
109    fn target_bitrate(&self) -> f64 {
110        self.bits_per_second
111    }
112}
113
114#[cfg(test)]
115mod tests {
116    use super::*;
117    use rtcp::transport_feedbacks::cc_feedback_report::Ecn;
118    use std::time::Duration;
119
120    fn report(id: u64, arrived: bool, departure: Instant) -> PacketReport {
121        PacketReport {
122            ssrc: 1,
123            id,
124            rtp_sequence_number: id as u16,
125            is_twcc: true,
126            twcc_sequence_number: id as u16,
127            size: 1200,
128            arrived,
129            departure,
130            arrival: arrived.then(|| Duration::from_millis(10)),
131            ecn: Ecn::default(),
132        }
133    }
134
135    /// The seam is two methods. If this ever needs a third to compile, the defaults have stopped
136    /// carrying their weight and the trait has grown.
137    #[test]
138    fn an_estimator_needs_only_two_methods() {
139        struct Minimal(f64);
140        impl BandwidthEstimator for Minimal {
141            fn on_reports(&mut self, _now: Instant, reports: &[PacketReport]) {
142                // Something an algorithm plausibly does, so this is not vacuously minimal.
143                let arrived = reports.iter().filter(|report| report.arrived).count();
144                self.0 = 100_000.0 * arrived as f64;
145            }
146            fn target_bitrate(&self) -> f64 {
147                self.0
148            }
149        }
150
151        let epoch = Instant::now();
152        let mut estimator = Minimal(0.0);
153        estimator.on_reports(epoch, &[report(1, true, epoch), report(2, false, epoch)]);
154
155        assert_eq!(100_000.0, estimator.target_bitrate());
156        assert_eq!(None, estimator.poll_timeout(), "the default is idle");
157        assert_eq!(EstimatorStats::default(), estimator.stats());
158        estimator.handle_timeout(epoch + Duration::from_secs(1));
159    }
160
161    #[test]
162    fn a_constant_estimator_ignores_what_it_is_told() {
163        let epoch = Instant::now();
164        let mut estimator = ConstantBitrate::new(750_000.0);
165
166        assert_eq!(750_000.0, estimator.target_bitrate());
167        estimator.on_reports(epoch, &[report(1, false, epoch)]);
168        estimator.handle_timeout(epoch + Duration::from_secs(10));
169        assert_eq!(
170            750_000.0,
171            estimator.target_bitrate(),
172            "loss and time must not move a rate the application fixed"
173        );
174    }
175
176    /// `Send + Sync` is a supertrait because the chain is, and a `Box<dyn BandwidthEstimator>`
177    /// has to be storable in one.
178    #[test]
179    fn an_estimator_is_send_and_sync() {
180        fn assert_send_sync<T: Send + Sync>() {}
181        assert_send_sync::<ConstantBitrate>();
182        assert_send_sync::<Box<dyn BandwidthEstimator>>();
183    }
184}