Skip to main content

rtc_interceptor/rfc8888/
sender.rs

1//! Emits RFC 8888 congestion control feedback for the streams being received.
2
3use super::recorder::CcFeedbackRecorder;
4use crate::stream_info::StreamInfo;
5use crate::{Interceptor, Packet, TaggedPacket, interceptor};
6use rtcp::transport_feedbacks::cc_feedback_report::Ecn;
7use shared::TransportContext;
8use shared::error::Error;
9use shared::time::SystemInstant;
10use std::collections::HashSet;
11use std::collections::VecDeque;
12use std::marker::PhantomData;
13use std::time::{Duration, Instant};
14
15/// How often feedback is sent when no interval is configured.
16pub const DEFAULT_INTERVAL: Duration = Duration::from_millis(100);
17
18/// Byte budget for one report, chosen to sit inside a conservative path MTU.
19pub const DEFAULT_MAX_REPORT_SIZE: usize = 1200;
20
21/// Builder for [`Rfc8888Interceptor`].
22///
23/// # Example
24///
25/// ```
26/// use rtc_interceptor::{Registry, Rfc8888Builder};
27/// use std::time::Duration;
28///
29/// let chain = Registry::new()
30///     .with(Rfc8888Builder::new().with_interval(Duration::from_millis(50)).build())
31///     .build();
32/// ```
33pub struct Rfc8888Builder<P> {
34    interval: Duration,
35    max_report_size: usize,
36    sender_ssrc: u32,
37    _phantom: PhantomData<P>,
38}
39
40impl<P> Default for Rfc8888Builder<P> {
41    fn default() -> Self {
42        Self {
43            interval: DEFAULT_INTERVAL,
44            max_report_size: DEFAULT_MAX_REPORT_SIZE,
45            sender_ssrc: 0,
46            _phantom: PhantomData,
47        }
48    }
49}
50
51impl<P> Rfc8888Builder<P> {
52    /// Create a builder with the default interval and report size.
53    pub fn new() -> Self {
54        Self::default()
55    }
56
57    /// How often feedback is sent.
58    ///
59    /// Congestion control wants this often — the estimate is only as fresh as the feedback
60    /// driving it — traded against the RTCP bandwidth it costs.
61    pub fn with_interval(mut self, interval: Duration) -> Self {
62        self.interval = interval;
63        self
64    }
65
66    /// Byte budget for one report.
67    ///
68    /// A report larger than the path MTU is fragmented or dropped, and feedback that does not
69    /// arrive is worth nothing, so a report describes fewer packets rather than growing.
70    pub fn with_max_report_size(mut self, max_report_size: usize) -> Self {
71        self.max_report_size = max_report_size;
72        self
73    }
74
75    /// The SSRC these reports are sent from.
76    pub fn with_sender_ssrc(mut self, sender_ssrc: u32) -> Self {
77        self.sender_ssrc = sender_ssrc;
78        self
79    }
80
81    /// Build the interceptor factory function.
82    pub fn build(self) -> impl FnOnce(P) -> Rfc8888Interceptor<P> {
83        move |inner| {
84            Rfc8888Interceptor::new(inner, self.interval, self.max_report_size, self.sender_ssrc)
85        }
86    }
87}
88
89/// Reports when each packet of each bound remote stream arrived ([RFC 8888]).
90///
91/// # Differences from upstream
92///
93/// `pion/interceptor` injects a `SenderTicker` and a `SenderNow` so its tests can control time.
94/// Sans-I/O needs neither: every instant arrives as a parameter, so the tests drive the clock by
95/// passing one. Both are dropped.
96///
97/// Upstream's `Recorder` also never sets its sender SSRC, so every report it builds claims to come
98/// from SSRC 0. Here it is configured.
99///
100/// [RFC 8888]: https://www.rfc-editor.org/rfc/rfc8888
101#[derive(Interceptor)]
102pub struct Rfc8888Interceptor<P> {
103    #[next]
104    inner: P,
105    interval: Duration,
106    max_report_size: usize,
107    sender_ssrc: u32,
108    recorder: CcFeedbackRecorder,
109    /// Remote streams being reported on.
110    streams: HashSet<u32>,
111    next_timeout: Option<Instant>,
112    /// Wall-clock reference, captured from the first instant handed over, so a monotonic `Instant`
113    /// can be turned into the NTP timestamp a report carries.
114    epoch: Option<SystemInstant>,
115    write_queue: VecDeque<TaggedPacket>,
116}
117
118impl<P> Rfc8888Interceptor<P> {
119    fn new(inner: P, interval: Duration, max_report_size: usize, sender_ssrc: u32) -> Self {
120        Self {
121            inner,
122            interval,
123            max_report_size,
124            sender_ssrc,
125            recorder: CcFeedbackRecorder::new(),
126            streams: HashSet::new(),
127            next_timeout: None,
128            epoch: None,
129            write_queue: VecDeque::new(),
130        }
131    }
132
133    /// The middle 32 bits of the NTP timestamp for `now`, which is what a report carries.
134    fn report_timestamp(&mut self, now: Instant) -> u32 {
135        let epoch = self.epoch.get_or_insert_with(|| SystemInstant::now(now));
136        (epoch.ntp(now) >> 16) as u32
137    }
138
139    /// Arm the interval from the first instant this interceptor is given.
140    fn arm(&mut self, now: Instant) {
141        if self.next_timeout.is_none() && !self.streams.is_empty() && !self.interval.is_zero() {
142            self.next_timeout = Some(now + self.interval);
143        }
144    }
145}
146
147#[interceptor]
148impl<P: Interceptor> Rfc8888Interceptor<P> {
149    #[overrides]
150    fn bind_remote_stream(&mut self, info: &StreamInfo) {
151        self.streams.insert(info.ssrc);
152        self.inner.bind_remote_stream(info);
153    }
154
155    #[overrides]
156    fn unbind_remote_stream(&mut self, info: &StreamInfo) {
157        self.streams.remove(&info.ssrc);
158        self.recorder.remove_stream(info.ssrc);
159        if self.streams.is_empty() {
160            self.next_timeout = None;
161        }
162        self.inner.unbind_remote_stream(info);
163    }
164
165    #[overrides]
166    fn handle_read(&mut self, msg: TaggedPacket) -> Result<(), Self::Error> {
167        if let Packet::Rtp(rtp_packet) = &msg.message
168            && self.streams.contains(&rtp_packet.header.ssrc)
169        {
170            // ECN lives in the IP header, which a sans-I/O interceptor never sees. Until the
171            // transport surfaces it, every packet is reported as Not-ECT — which is what an
172            // endpoint without ECN support would report anyway.
173            self.recorder.add_packet(
174                msg.now,
175                rtp_packet.header.ssrc,
176                rtp_packet.header.sequence_number,
177                Ecn::NotEct,
178            );
179            self.arm(msg.now);
180        }
181        self.inner.handle_read(msg)
182    }
183
184    #[overrides]
185    fn handle_timeout(&mut self, now: Self::Time) -> Result<(), Self::Error> {
186        self.arm(now);
187
188        if let Some(next_timeout) = self.next_timeout
189            && now >= next_timeout
190        {
191            self.next_timeout = Some(now + self.interval);
192
193            if !self.recorder.is_empty() {
194                let report_timestamp = self.report_timestamp(now);
195                let report = self.recorder.build_report(
196                    now,
197                    self.sender_ssrc,
198                    report_timestamp,
199                    self.max_report_size,
200                );
201                if !report.report_blocks.is_empty() {
202                    self.write_queue.push_back(TaggedPacket {
203                        now,
204                        transport: TransportContext::default(),
205                        message: Packet::Rtcp(vec![Box::new(report)]),
206                    });
207                }
208            }
209        }
210
211        self.inner.handle_timeout(now)
212    }
213
214    #[overrides]
215    fn poll_timeout(&mut self) -> Option<Self::Time> {
216        match (self.next_timeout, self.inner.poll_timeout()) {
217            (Some(mine), Some(theirs)) => Some(mine.min(theirs)),
218            (mine, theirs) => mine.or(theirs),
219        }
220    }
221
222    #[overrides]
223    fn poll_write(&mut self) -> Option<Self::Wout> {
224        // A generated report is terminal (chain contract rule 1): complete when built, with
225        // nothing below to transform it.
226        if let Some(packet) = self.write_queue.pop_front() {
227            return Some(packet);
228        }
229        self.inner.poll_write()
230    }
231}