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}