Skip to main content

rtc_interceptor/
registry.rs

1//! Building a chain.
2
3use crate::chain::InterceptorChain;
4use crate::noop::NoopInterceptor;
5use crate::{BoxedInterceptor, Interceptor};
6
7/// Collects interceptors and assembles them into an [`InterceptorChain`].
8///
9/// # Order
10///
11/// Interceptors run in the order they are added, measured by **distance from the wire**: the first is
12/// closest to the network, the last closest to the application. Read walks that order, write walks
13/// it in reverse, so one list serves both directions and "closest to the wire" means one thing
14/// rather than opposite things per direction.
15///
16/// ```text
17/// Registry::new()
18///     .with(a)   // closest to the wire
19///     .with(b)
20///     .with(c)   // closest to the application
21///     .build()
22///
23/// read:   a → b → c → application
24/// write:  application → c → b → a → wire
25/// ```
26///
27/// A registry reads the way the chain runs, so getting the order right is a matter of reading it
28/// top to bottom. The nested registry it replaces added *innermost* first, which meant the list
29/// ran application-to-network on read and the composed order was the reverse of what the file
30/// looked like — `register_default_interceptors` ended up assembling TWCC receiver → RTCP reports
31/// → NACK generator, the opposite of what the chain contract documented, with nothing to catch it.
32///
33/// # Example
34///
35/// ```
36/// use rtc_interceptor::{NackGeneratorBuilder, Registry, TwccSenderBuilder};
37///
38/// let chain = Registry::new()
39///     .with(TwccSenderBuilder::new().build())     // closest to the wire
40///     .with(NackGeneratorBuilder::new().build())  // sees arrivals after it
41///     .build();                                   // the terminus is appended here
42/// # let _ = chain;
43/// ```
44#[derive(Default)]
45pub struct Registry {
46    interceptors: Vec<Box<dyn Interceptor>>,
47    rtcp_readable: bool,
48}
49
50impl Registry {
51    /// An empty registry.
52    pub fn new() -> Self {
53        Self::default()
54    }
55
56    /// Add an interceptor on the application side of everything added so far.
57    pub fn with(mut self, interceptor: impl Interceptor + 'static) -> Self {
58        self.interceptors.push(Box::new(interceptor));
59        self
60    }
61
62    /// Add an interceptor that is already boxed, for a caller assembling a chain dynamically.
63    pub fn with_boxed(mut self, boxed_interceptor: BoxedInterceptor) -> Self {
64        self.interceptors.push(boxed_interceptor);
65        self
66    }
67
68    /// Make inbound RTCP readable by the application — it arrives from
69    /// [`poll_read`](sansio::Protocol::poll_read) like media does — as well as acted on by the
70    /// interceptors.
71    ///
72    /// Off by default. RTCP is control traffic the interceptors act on: a receiver report feeds
73    /// the sender statistics, a NACK is answered by the responder, transport-wide feedback drives
74    /// the bandwidth estimate. An application that did not ask for it would find a stream of
75    /// packets it cannot use interleaved with its media. Turn it on for an SFU relaying feedback,
76    /// or a tool inspecting a session.
77    ///
78    /// Outbound RTCP is unaffected; this is only about what arrives.
79    ///
80    /// It has to be asked for here rather than arranged by an interceptor of your own. One that
81    /// captured an RTCP packet and re-emitted it from `poll_read` would put the copy back on the
82    /// belt *behind* itself, where [`NoopInterceptor`] is still ahead of it and drops it — the
83    /// original and the copy both. The nested chain allowed that trick because a local `poll_read`
84    /// queue was terminal and bypassed everything below it, which is precisely the bypass this
85    /// design removes.
86    pub fn with_rtcp_readable(mut self) -> Self {
87        self.rtcp_readable = true;
88        self
89    }
90
91    /// Assemble the interceptor chain.
92    ///
93    /// [`NoopInterceptor`] is appended last, so every chain decides what becomes of inbound RTCP.
94    /// That is a property of a chain rather than something a caller opts into: left out, an
95    /// application would get a stream of control traffic it never asked for, and the omission
96    /// would look like working code. [`with_rtcp_readable`] is how a chain asks for it deliberately.
97    ///
98    /// [`with_rtcp_readable`]: Registry::with_rtcp_readable
99    pub fn build(mut self) -> impl Interceptor {
100        self.interceptors
101            .push(Box::new(NoopInterceptor::new(self.rtcp_readable)));
102        InterceptorChain::new(self.interceptors)
103    }
104}
105
106#[cfg(test)]
107mod tests {
108    use super::*;
109    use crate::StreamInfo;
110    use crate::{AttributedPacket, Packet, TaggedPacket};
111    use sansio::Protocol;
112    use shared::TransportContext;
113    use shared::error::Error;
114    use std::collections::VecDeque;
115    use std::sync::{Arc, Mutex};
116    use std::time::Instant;
117
118    #[derive(Clone, Default)]
119    struct Log(Arc<Mutex<Vec<&'static str>>>);
120
121    struct Marker {
122        name: &'static str,
123        log: Log,
124        read_queue: VecDeque<TaggedPacket>,
125        write_queue: VecDeque<TaggedPacket>,
126    }
127
128    impl Marker {
129        fn new(name: &'static str, log: Log) -> Self {
130            Self {
131                name,
132                log,
133                read_queue: VecDeque::new(),
134                write_queue: VecDeque::new(),
135            }
136        }
137    }
138
139    impl Protocol<TaggedPacket, TaggedPacket, ()> for Marker {
140        type Rout = TaggedPacket;
141        type Wout = TaggedPacket;
142        type Eout = ();
143        type Error = Error;
144        type Time = Instant;
145
146        fn handle_read(&mut self, msg: TaggedPacket) -> Result<(), Self::Error> {
147            self.log.0.lock().unwrap().push(self.name);
148            self.read_queue.push_back(msg);
149            Ok(())
150        }
151
152        fn poll_read(&mut self) -> Option<Self::Rout> {
153            self.read_queue.pop_front()
154        }
155
156        fn handle_write(&mut self, msg: TaggedPacket) -> Result<(), Self::Error> {
157            self.log.0.lock().unwrap().push(self.name);
158            self.write_queue.push_back(msg);
159            Ok(())
160        }
161
162        fn poll_write(&mut self) -> Option<Self::Wout> {
163            self.write_queue.pop_front()
164        }
165
166        fn handle_timeout(&mut self, _now: Instant) -> Result<(), Self::Error> {
167            Ok(())
168        }
169
170        fn poll_timeout(&mut self) -> Option<Self::Time> {
171            None
172        }
173    }
174
175    impl Interceptor for Marker {
176        fn bind_local_stream(&mut self, _info: &StreamInfo) {}
177        fn unbind_local_stream(&mut self, _info: &StreamInfo) {}
178        fn bind_remote_stream(&mut self, _info: &StreamInfo) {}
179        fn unbind_remote_stream(&mut self, _info: &StreamInfo) {}
180    }
181
182    fn packet() -> TaggedPacket {
183        TaggedPacket {
184            now: Instant::now(),
185            transport: TransportContext::default(),
186            message: AttributedPacket::new(Packet::Rtp(rtp::Packet::default())),
187        }
188    }
189
190    fn chain(log: &Log) -> impl Interceptor {
191        Registry::new()
192            .with(Marker::new("wire", log.clone()))
193            .with(Marker::new("middle", log.clone()))
194            .with(Marker::new("app", log.clone()))
195            .build()
196    }
197
198    /// Read runs the list forwards: the first interceptor added is closest to the wire.
199    #[test]
200    fn read_runs_in_the_order_stages_were_added() {
201        let log = Log::default();
202        let mut chain = chain(&log);
203
204        chain.handle_read(packet()).unwrap();
205        while chain.poll_read().is_some() {}
206
207        assert_eq!(vec!["wire", "middle", "app"], *log.0.lock().unwrap());
208    }
209
210    /// Write runs it backwards, so the same list describes both directions.
211    #[test]
212    fn write_runs_in_reverse() {
213        let log = Log::default();
214        let mut chain = chain(&log);
215
216        chain.handle_write(packet()).unwrap();
217        while chain.poll_write().is_some() {}
218
219        assert_eq!(vec!["app", "middle", "wire"], *log.0.lock().unwrap());
220    }
221
222    /// Ending the inbound RTCP path is a property of every chain, not something a caller adds.
223    #[test]
224    fn a_registry_with_nothing_added_still_has_the_terminus() {
225        let mut chain = Registry::new().build();
226
227        chain
228            .handle_read(TaggedPacket {
229                now: Instant::now(),
230                transport: TransportContext::default(),
231                message: AttributedPacket::new(Packet::Rtcp(vec![])),
232            })
233            .unwrap();
234        assert!(
235            chain.poll_read().is_none(),
236            "inbound RTCP stops before the application"
237        );
238    }
239
240    /// The terminus goes last, so every interceptor sees inbound RTCP before it is dropped.
241    #[test]
242    fn the_terminus_is_application_most() {
243        let log = Log::default();
244        let mut chain = Registry::new()
245            .with(Marker::new("wire", log.clone()))
246            .build();
247
248        chain
249            .handle_read(TaggedPacket {
250                now: Instant::now(),
251                transport: TransportContext::default(),
252                message: AttributedPacket::new(Packet::Rtcp(vec![])),
253            })
254            .unwrap();
255
256        assert_eq!(
257            vec!["wire"],
258            *log.0.lock().unwrap(),
259            "the stage saw the RTCP packet; the terminus dropped it afterwards"
260        );
261        assert!(chain.poll_read().is_none());
262    }
263}