Skip to main content

rtc_interceptor/cc/
interceptor.rs

1//! Records what left, ingests what the remote said about it, and drives an estimator.
2
3use super::estimator::BandwidthEstimator;
4use crate::Interceptor;
5use crate::rtpfb::convert::{convert_ccfb, convert_twcc};
6use crate::rtpfb::history::History;
7use crate::stream_info::StreamInfo;
8use crate::twcc::stream_supports_twcc;
9use crate::{Attribute, Packet, TaggedPacket};
10use sansio::Protocol;
11use shared::error::Error;
12use shared::marshal::{MarshalSize, Unmarshal};
13use std::collections::{HashMap, VecDeque};
14use std::time::{Duration, Instant};
15
16/// How long an unacknowledged packet is kept before it is written off.
17///
18/// Bounds the send history on a path that has stopped reporting. Too short and late feedback names
19/// packets there is no record of; too long and memory grows on a dead path. Two seconds is several
20/// round trips on any path worth estimating for, and one report interval is measured in tens of
21/// milliseconds.
22pub const DEFAULT_PRUNE_HORIZON: Duration = Duration::from_secs(2);
23
24/// Per-stream state: the header-extension id the transport-wide sequence number is written under.
25struct LocalStream {
26    hdr_ext_id: u8,
27}
28
29/// Builder for [`CongestionControlInterceptor`].
30///
31/// # Example
32///
33/// ```
34/// use rtc_interceptor::{Slot, CongestionControlBuilder, ConstantBitrate, Registry};
35///
36/// let chain = Registry::new()
37///     .with(Slot::CongestionControl, CongestionControlBuilder::new(ConstantBitrate::new(1_000_000.0)).build())
38///     .build();
39/// # let _ = chain;
40/// ```
41pub struct CongestionControlBuilder<E: BandwidthEstimator> {
42    estimator: E,
43    prune_horizon: Duration,
44}
45
46impl<E: BandwidthEstimator> CongestionControlBuilder<E> {
47    /// A builder driving `estimator`.
48    pub fn new(estimator: E) -> Self {
49        Self {
50            estimator,
51            prune_horizon: DEFAULT_PRUNE_HORIZON,
52        }
53    }
54
55    /// How long an unacknowledged packet is kept before it is written off.
56    pub fn with_prune_horizon(mut self, prune_horizon: Duration) -> Self {
57        self.prune_horizon = prune_horizon;
58        self
59    }
60
61    /// Build the interceptor.
62    pub fn build(self) -> CongestionControlInterceptor<E> {
63        CongestionControlInterceptor {
64            last_target: self.estimator.target_bitrate(),
65            estimator: self.estimator,
66            prune_horizon: self.prune_horizon,
67            history: History::new(),
68            streams: HashMap::new(),
69            read_queue: VecDeque::new(),
70            write_queue: VecDeque::new(),
71        }
72    }
73}
74
75/// Records every departing packet, resolves the remote's feedback against it, and drives a
76/// [`BandwidthEstimator`].
77///
78/// # Where this belongs in the chain
79///
80/// **Wire-most.** It is the only position that sees every byte that leaves: nothing exits the chain
81/// except through the interceptors ahead of it in the walk, so a retransmission emitted by the NACK
82/// responder and a repair packet emitted by the FEC encoder both arrive here, already paced and
83/// already numbered. An estimator reading a history that omits them sees fewer bytes than are on
84/// the wire, infers headroom, and raises the target during loss — a positive feedback loop that is
85/// hard to spot, because the estimator's own accounting stays internally consistent throughout.
86///
87/// It also has to be **below the pacer**, so `packet.now` is the instant the packet was *released*
88/// rather than the instant the application enqueued it. A pacer can hold a packet for tens of
89/// milliseconds; counting that as network delay is exactly the error that makes a delay-based
90/// estimate collapse.
91///
92/// # How the estimate gets out
93///
94/// On the read leg, attached to the feedback packet that produced it, as
95/// [`Attribute::TargetBitrateChanged`]. The pacer sits application-ward of this interceptor, so it
96/// sees that packet *after* this one does and reads the attribute on its way past. That is the only
97/// leg the estimate can cross on: on the write leg this interceptor is last, and anything it
98/// attached would already have gone by everything that cares.
99pub struct CongestionControlInterceptor<E: BandwidthEstimator> {
100    estimator: E,
101    history: History,
102    streams: HashMap<u32, LocalStream>,
103    prune_horizon: Duration,
104    /// The last target handed onward, so an unchanged estimate does not re-announce itself.
105    last_target: f64,
106    read_queue: VecDeque<TaggedPacket>,
107    write_queue: VecDeque<TaggedPacket>,
108}
109
110impl<E: BandwidthEstimator> CongestionControlInterceptor<E> {
111    /// The estimator, for reading its stats.
112    pub fn estimator(&self) -> &E {
113        &self.estimator
114    }
115
116    /// How many sent packets are still awaiting a verdict.
117    pub fn outstanding(&self) -> usize {
118        self.history.len()
119    }
120
121    /// The transport-wide sequence number the TWCC sender wrote, if this stream carries one.
122    ///
123    /// The sender sits between the pacer and here, so by the time a packet arrives the number is
124    /// already in its header — which is the whole reason this interceptor is wire-most rather than
125    /// the sender being.
126    fn twcc_sequence_number(&self, rtp_packet: &rtp::Packet) -> Option<u16> {
127        let stream = self.streams.get(&rtp_packet.header.ssrc)?;
128        let mut extension = rtp_packet.header.get_extension(stream.hdr_ext_id)?;
129        rtp::extension::transport_cc_extension::TransportCcExtension::unmarshal(&mut extension)
130            .ok()
131            .map(|extension| extension.transport_sequence)
132    }
133
134    /// Feed one inbound RTCP packet to the history. Returns whether it said anything.
135    #[allow(clippy::borrowed_box)]
136    fn ingest(&mut self, now: Instant, rtcp_packet: &Box<dyn rtcp::Packet>) -> bool {
137        let payload = rtcp_packet.as_any();
138
139        if let Some(feedback) = payload
140            .downcast_ref::<rtcp::transport_feedbacks::transport_layer_cc::TransportLayerCc>(
141        ) {
142            for acknowledgement in convert_twcc(feedback) {
143                self.history.on_twcc_feedback(now, acknowledgement);
144            }
145            return true;
146        }
147
148        if let Some(feedback) = payload
149            .downcast_ref::<rtcp::transport_feedbacks::cc_feedback_report::CcFeedbackReport>(
150        ) {
151            let (_report_delay, per_stream) = convert_ccfb(feedback);
152            for (ssrc, acknowledgements) in per_stream {
153                for acknowledgement in acknowledgements {
154                    self.history.on_ccfb_feedback(now, ssrc, acknowledgement);
155                }
156            }
157            return true;
158        }
159
160        false
161    }
162}
163
164impl<E: BandwidthEstimator> Protocol<TaggedPacket, TaggedPacket, ()>
165    for CongestionControlInterceptor<E>
166{
167    type Rout = TaggedPacket;
168    type Wout = TaggedPacket;
169    type Eout = ();
170    type Error = Error;
171    type Time = Instant;
172
173    fn handle_read(&mut self, mut msg: TaggedPacket) -> Result<(), Self::Error> {
174        let mut reported = false;
175        if let Packet::Rtcp(ref rtcp_packets) = msg.message.packet {
176            // `for` over a borrow of `msg` while `self` is borrowed mutably: collect first.
177            let feedback: Vec<_> = rtcp_packets.to_vec();
178            for rtcp_packet in &feedback {
179                reported |= self.ingest(msg.now, rtcp_packet);
180            }
181        }
182
183        if reported {
184            let reports = self.history.take_reports();
185            self.estimator.on_reports(msg.now, &reports);
186
187            let target = self.estimator.target_bitrate();
188            if target != self.last_target {
189                self.last_target = target;
190                // Onto *this* packet: the pacer is application-ward of here, so it sees this
191                // packet after this interceptor does and reads the attribute on its way past.
192                msg.message.add(Attribute::TargetBitrateChanged {
193                    bits_per_second: target,
194                });
195            }
196        }
197
198        self.read_queue.push_back(msg);
199        Ok(())
200    }
201
202    fn poll_read(&mut self) -> Option<Self::Rout> {
203        self.read_queue.pop_front()
204    }
205
206    fn handle_write(&mut self, msg: TaggedPacket) -> Result<(), Self::Error> {
207        if let Packet::Rtp(ref rtp_packet) = msg.message.packet {
208            let twcc_sequence_number = self.twcc_sequence_number(rtp_packet);
209            // Only a stream this endpoint is sending and that negotiated transport-wide CC is
210            // tracked; anything else has no sequence space the remote will report against.
211            if self.streams.contains_key(&rtp_packet.header.ssrc) {
212                self.history.add_outgoing(
213                    rtp_packet.header.ssrc,
214                    rtp_packet.header.sequence_number,
215                    twcc_sequence_number.is_some(),
216                    twcc_sequence_number.unwrap_or_default(),
217                    rtp_packet.marshal_size(),
218                    // The release instant. The pacer has already run on this leg — recording the
219                    // enqueue instant instead would charge its queueing delay to the network.
220                    msg.now,
221                );
222            }
223        }
224
225        self.write_queue.push_back(msg);
226        Ok(())
227    }
228
229    fn poll_write(&mut self) -> Option<Self::Wout> {
230        self.write_queue.pop_front()
231    }
232
233    fn handle_timeout(&mut self, now: Instant) -> Result<(), Self::Error> {
234        self.history
235            .prune_before(now.checked_sub(self.prune_horizon).unwrap_or(now));
236        self.estimator.handle_timeout(now);
237        Ok(())
238    }
239
240    /// Whatever the estimator wants, and `None` when it wants nothing.
241    ///
242    /// This interceptor has no timer of its own: pruning is bounded work that can ride any wake-up,
243    /// and asking for one on its own account would wake the whole chain on an idle connection.
244    fn poll_timeout(&mut self) -> Option<Self::Time> {
245        self.estimator.poll_timeout()
246    }
247}
248
249impl<E: BandwidthEstimator> Interceptor for CongestionControlInterceptor<E> {
250    fn bind_local_stream(&mut self, info: &StreamInfo) {
251        // Tracked whether or not it negotiated transport-wide CC: RFC 8888 reports against the RTP
252        // sequence number and needs no extension, so a stream without one is still worth recording.
253        let hdr_ext_id = stream_supports_twcc(info).unwrap_or_default();
254        self.streams.insert(info.ssrc, LocalStream { hdr_ext_id });
255    }
256
257    fn unbind_local_stream(&mut self, info: &StreamInfo) {
258        self.streams.remove(&info.ssrc);
259    }
260
261    fn bind_remote_stream(&mut self, _info: &StreamInfo) {}
262    fn unbind_remote_stream(&mut self, _info: &StreamInfo) {}
263}