rtc_interceptor/registry.rs
1//! Interceptor Registry - Type-safe builder for constructing interceptor chains.
2//!
3//! The [`Registry`] provides a fluent API for composing interceptor chains. Each call
4//! to [`with()`](Registry::with) wraps the current chain with a new interceptor layer.
5//!
6//! # Chain Construction
7//!
8//! Interceptors are added from innermost to outermost. The first interceptor added
9//! becomes the innermost (closest to [`NoopInterceptor`](crate::NoopInterceptor)),
10//! and the last becomes the outermost (processes packets first).
11//!
12//! ```text
13//! Registry::new()
14//! .with(InterceptorA) // Innermost
15//! .with(InterceptorB) // Middle
16//! .with(InterceptorC) // Outermost
17//! .build()
18//!
19//! Results in: C wraps B wraps A wraps NoopInterceptor
20//! ```
21//!
22//! # The chain contract
23//!
24//! Interceptors that only transform the packet in front of them compose in any order. The ones
25//! that **delay** a packet (a jitter buffer, a pacer) or **generate** one (an RTCP report, a FEC
26//! repair packet, a recovered packet) do not: for those, where the packet re-enters the chain is
27//! a correctness property, not a preference. This section is the contract they are written
28//! against. It is verified by `tests/chain_order.rs`, which is where to look for what each rule
29//! means in practice.
30//!
31//! ## Rule 1 — a local `poll_*` queue is terminal
32//!
33//! `handle_*` and `poll_*` both walk **outer → inner**. An interceptor that returns a packet from
34//! its own queue in `poll_read`/`poll_write` returns it *instead of* delegating inward, so that
35//! packet **never passes through `inner`**:
36//!
37//! ```text
38//! poll_write: C ──▶ B ──▶ A ──▶ Noop
39//! └── B returns its own queued packet here;
40//! A never sees it.
41//! ```
42//!
43//! That is the right shape for a packet whose processing is complete — an RTCP Sender Report is
44//! generated fully formed and nothing further downstream needs to touch it. It is the wrong shape
45//! for a packet that still needs the rest of the chain.
46//!
47//! ## Rule 2 — re-inject through `inner` when processing must continue
48//!
49//! A packet released later but still needing downstream work is handed to
50//! `inner.handle_read(pkt)` / `inner.handle_write(pkt)` and collected afterwards from the
51//! delegating `poll_*`. It then traverses every layer below exactly once, in the same order a
52//! live packet would.
53//!
54//! | Situation | Route | Why |
55//! |---|---|---|
56//! | RTCP report generated on a timer | local queue (terminal) | complete when built; nothing below acts on it |
57//! | Retransmission answering a NACK | local queue (terminal) | already went through the chain when first sent |
58//! | Packet released by a jitter buffer | `inner.handle_read` | downstream receive processing has not run yet |
59//! | Packet recovered by FEC | `inner.handle_read` | it is a media packet the rest of the chain has never seen |
60//! | FEC repair packet being sent | `inner.handle_write` | still needs the outbound layers below |
61//! | Packet released by the pacer | `inner.handle_write` | must pick up send-time state at the release instant |
62//!
63//! **Exactly once** is the part worth testing. Re-injecting and *also* queueing locally delivers
64//! the packet twice; queueing locally when downstream work was required silently skips layers.
65//! Neither shows up as a compile error.
66//!
67//! ## Rule 3 — record departure at release, not at enqueue
68//!
69//! [`TaggedPacket::now`](shared::transport::TransportMessage::now) is a timestamp on the packet,
70//! and a delaying interceptor must **replace** it with the instant it actually releases the
71//! packet before handing it inward. Anything below that records departure — a send history
72//! feeding congestion control — otherwise attributes the interceptor's own buffering delay to
73//! the network, which is exactly the measurement congestion control must not get wrong.
74//!
75//! The same applies on the read side: a packet released by a jitter buffer carries the instant it
76//! was released, not the instant it arrived, once it re-enters the chain.
77//!
78//! ## Rule 4 — default relative order
79//!
80//! Listed outermost first, which is the order each packet meets them. `Registry::with` adds
81//! **innermost first**, so a registry builds this list bottom-up.
82//!
83//! ```text
84//! write (application → network) read (network → application)
85//! ──────────────────────────── ───────────────────────────
86//! outermost pacing FEC decode / recover
87//! FEC encode (repair packets) NACK generator
88//! RTCP reports (SR) jitter buffer
89//! NACK responder (retransmit buffer) RTCP reports (RR)
90//! TWCC sender (tags seq numbers) TWCC receiver (records arrivals)
91//! innermost rtpfb / send history rtpfb / feedback ingest
92//! ```
93//!
94//! The constraints that fix these positions, as opposed to the ones that are conventional:
95//!
96//! - **Pacing is outermost on write.** Everything below it must observe the release instant
97//! (rule 3), so nothing that timestamps or records a packet may sit above it.
98//! - **Send history is innermost on write.** It records what actually left, after every layer
99//! that may rewrite the packet — including the TWCC sequence number it is keyed by.
100//! - **FEC decode is outermost on read.** A recovered packet has to look to every other
101//! interceptor exactly like a packet that arrived normally, so recovery happens before anything
102//! below inspects sequence numbers.
103//! - **The NACK generator sits above the jitter buffer.** Loss has to be detected from *arrivals*.
104//! A generator below the buffer sees a packet only once it is released, so it cannot notice a
105//! gap until a whole depth after the packet went missing, and every NACK it sends is late by
106//! that much. (This corrects the order first written here, which had them the other way round;
107//! `tests/jitter_buffer_nack_depth.rs` measures the cost.)
108//! - **The jitter buffer sits below FEC.** Recovery gets its chance before the buffer has to
109//! decide whether to wait for a gap, and a recovered packet is then indistinguishable from one
110//! that arrived normally.
111//! - **The buffer's depth bounds how long a retransmission stays useful.** A depth shallower than
112//! NACK detection plus the round trip means every retransmission arrives after its position has
113//! been played past. The two are deliberately *not* coupled — see
114//! [`JitterBufferBuilder::with_depth`](crate::JitterBufferBuilder::with_depth).
115//! - **TWCC receiver is innermost on read**, recording arrival after the packet set is final —
116//! including packets FEC recovered.
117//!
118//! Interceptors outside these groups compose freely. When adding one that delays or generates,
119//! state its required position and which rule fixes it.
120
121use crate::noop::NoopInterceptor;
122use crate::{BoxedInterceptor, Interceptor};
123
124/// Registry for constructing interceptor chains.
125///
126/// `Registry` wraps an interceptor chain and allows adding more interceptors
127/// via the [`with`](Registry::with) method. The chain can be extracted with [`build`](Registry::build).
128///
129/// # Example
130///
131/// ```
132/// use rtc_interceptor::{ReceiverReportBuilder, Registry, SenderReportBuilder};
133///
134/// // Each `with` changes the registry's type, so rebind rather than reassign.
135/// let registry = Registry::new()
136/// .with(SenderReportBuilder::new().build())
137/// .with(ReceiverReportBuilder::new().build());
138///
139/// // Build the final chain
140/// let chain = registry.build();
141/// ```
142///
143/// # Helper Function Pattern
144///
145/// ```
146/// use rtc_interceptor::{Interceptor, ReceiverReportBuilder, Registry, SenderReportBuilder};
147///
148/// fn register_default_interceptors<P: Interceptor>(
149/// registry: Registry<P>,
150/// ) -> Registry<impl Interceptor + use<P>> {
151/// registry
152/// .with(SenderReportBuilder::new().build())
153/// .with(ReceiverReportBuilder::new().build())
154/// }
155///
156/// let registry = Registry::new();
157/// let registry = register_default_interceptors(registry);
158/// let chain = registry.build();
159/// ```
160#[derive(Clone)]
161pub struct Registry<P> {
162 inner: P,
163}
164
165impl Registry<NoopInterceptor> {
166 /// Create a new empty registry.
167 ///
168 /// This creates a `NoopInterceptor` as the innermost layer.
169 ///
170 /// # Example
171 ///
172 /// ```
173 /// use rtc_interceptor::Registry;
174 ///
175 /// let registry = Registry::new();
176 /// ```
177 pub fn new() -> Self {
178 Registry {
179 inner: NoopInterceptor::new(),
180 }
181 }
182}
183
184impl Default for Registry<NoopInterceptor> {
185 fn default() -> Self {
186 Self::new()
187 }
188}
189
190impl<P: Interceptor> Registry<P> {
191 /// Create a registry from an existing interceptor.
192 ///
193 /// # Example
194 ///
195 /// ```
196 /// use rtc_interceptor::{NoopInterceptor, Registry};
197 ///
198 /// let custom = NoopInterceptor::new();
199 /// let registry = Registry::from(custom);
200 /// ```
201 pub fn from(inner: P) -> Self {
202 Registry { inner }
203 }
204
205 /// Wrap the current chain with another interceptor.
206 ///
207 /// Returns a new `Registry` with the updated chain type.
208 ///
209 /// # Example
210 ///
211 /// ```
212 /// use rtc_interceptor::{ReceiverReportBuilder, Registry, SenderReportBuilder};
213 ///
214 /// let registry = Registry::new()
215 /// .with(SenderReportBuilder::new().build())
216 /// .with(ReceiverReportBuilder::new().build());
217 /// ```
218 pub fn with<O, F>(self, f: F) -> Registry<O>
219 where
220 F: FnOnce(P) -> O,
221 O: Interceptor,
222 {
223 Registry {
224 inner: f(self.inner),
225 }
226 }
227
228 /// Build and return the interceptor chain.
229 ///
230 /// Consumes the registry and returns the inner interceptor chain.
231 ///
232 /// # Example
233 ///
234 /// ```
235 /// use rtc_interceptor::{Registry, SenderReportBuilder};
236 ///
237 /// let registry = Registry::new().with(SenderReportBuilder::new().build());
238 /// let chain = registry.build();
239 /// ```
240 pub fn build(self) -> P {
241 self.inner
242 }
243
244 /// Erase the chain's type, turning this into a `Registry<BoxedInterceptor>`.
245 ///
246 /// The chain an application assembles at runtime is a deep nest of generic types
247 /// (`TwccSender<NackResponder<...<NoopInterceptor>>>`), which otherwise leaks into every
248 /// type that holds the peer connection. Boxing it collapses that to one concrete type, so
249 /// a struct can store an `RTCPeerConnection<BoxedInterceptor>` field directly.
250 ///
251 /// # Example
252 ///
253 /// ```
254 /// use rtc_interceptor::{BoxedInterceptor, Registry, SenderReportBuilder};
255 ///
256 /// // Whatever the chain was composed of, the result has one concrete type.
257 /// let chain: BoxedInterceptor = Registry::new()
258 /// .with(SenderReportBuilder::new().build())
259 /// .boxed()
260 /// .build();
261 /// ```
262 ///
263 /// The `rtc` crate accepts the erased registry directly, so a peer connection can be stored
264 /// as `RTCPeerConnection<BoxedInterceptor>`:
265 ///
266 /// ```ignore
267 /// let registry = register_default_interceptors(Registry::new(), &mut media_engine)?;
268 /// let pc = RTCPeerConnectionBuilder::new()
269 /// .with_interceptor_registry(registry.boxed())
270 /// .build()?;
271 /// ```
272 ///
273 /// `P: 'static` is required because [`BoxedInterceptor`] is `Box<dyn Interceptor + 'static>`,
274 /// so the chain must not borrow anything shorter-lived. This is the only operation that needs
275 /// the bound, which is why [`Interceptor`] itself does not require `'static`.
276 pub fn boxed(self) -> Registry<BoxedInterceptor>
277 where
278 P: 'static,
279 {
280 Registry {
281 inner: Box::new(self.inner),
282 }
283 }
284}
285
286#[cfg(test)]
287mod tests {
288 use super::*;
289 use crate::TaggedPacket;
290 use sansio::Protocol;
291 use shared::error::Error;
292 use std::time::Instant;
293
294 fn dummy_rtp_packet() -> TaggedPacket {
295 TaggedPacket {
296 now: Instant::now(),
297 transport: Default::default(),
298 message: crate::Packet::Rtp(rtp::Packet::default()),
299 }
300 }
301
302 // A simple test interceptor that wraps an inner protocol
303 struct TestInterceptor<P> {
304 inner: P,
305 name: &'static str,
306 }
307
308 impl<P> TestInterceptor<P> {
309 fn new(inner: P) -> Self {
310 Self {
311 inner,
312 name: "test",
313 }
314 }
315
316 fn with_name(name: &'static str) -> impl FnOnce(P) -> Self {
317 move |inner| Self { inner, name }
318 }
319 }
320
321 impl<P: Interceptor> Protocol<TaggedPacket, TaggedPacket, ()> for TestInterceptor<P> {
322 type Rout = TaggedPacket;
323 type Wout = TaggedPacket;
324 type Eout = ();
325 type Error = Error;
326 type Time = Instant;
327
328 fn handle_read(&mut self, msg: TaggedPacket) -> Result<(), Self::Error> {
329 self.inner.handle_read(msg)
330 }
331
332 fn poll_read(&mut self) -> Option<Self::Rout> {
333 self.inner.poll_read()
334 }
335
336 fn handle_write(&mut self, msg: TaggedPacket) -> Result<(), Self::Error> {
337 self.inner.handle_write(msg)
338 }
339
340 fn poll_write(&mut self) -> Option<Self::Wout> {
341 self.inner.poll_write()
342 }
343 }
344
345 impl<P: Interceptor> Interceptor for TestInterceptor<P> {
346 fn bind_local_stream(&mut self, info: &crate::StreamInfo) {
347 self.inner.bind_local_stream(info);
348 }
349 fn unbind_local_stream(&mut self, info: &crate::StreamInfo) {
350 self.inner.unbind_local_stream(info);
351 }
352 fn bind_remote_stream(&mut self, info: &crate::StreamInfo) {
353 self.inner.bind_remote_stream(info);
354 }
355 fn unbind_remote_stream(&mut self, info: &crate::StreamInfo) {
356 self.inner.unbind_remote_stream(info);
357 }
358 }
359
360 #[test]
361 fn test_registry_new() {
362 let registry = Registry::new();
363 let mut chain = registry.build();
364 let pkt = dummy_rtp_packet();
365 chain.handle_read(pkt).unwrap();
366 assert!(chain.poll_read().is_some());
367 }
368
369 #[test]
370 fn test_registry_with_single_interceptor() {
371 let registry = Registry::new().with(TestInterceptor::new);
372 let mut chain = registry.build();
373
374 let pkt = dummy_rtp_packet();
375 chain.handle_read(pkt).unwrap();
376 assert!(chain.poll_read().is_some());
377 assert_eq!(chain.name, "test");
378 }
379
380 #[test]
381 fn test_registry_with_multiple_interceptors() {
382 let registry = Registry::new()
383 .with(TestInterceptor::with_name("inner"))
384 .with(TestInterceptor::with_name("outer"));
385 let mut chain = registry.build();
386
387 let pkt = dummy_rtp_packet();
388 chain.handle_read(pkt).unwrap();
389 assert!(chain.poll_read().is_some());
390 assert_eq!(chain.name, "outer");
391 assert_eq!(chain.inner.name, "inner");
392 }
393
394 #[test]
395 fn test_registry_from_inner() {
396 let custom = NoopInterceptor::new();
397 let registry = Registry::from(custom).with(TestInterceptor::new);
398 let mut chain = registry.build();
399
400 let pkt = dummy_rtp_packet();
401 let pkt_message = pkt.message.clone();
402 chain.handle_write(pkt).unwrap();
403 assert_eq!(chain.poll_write().unwrap().message, pkt_message);
404 }
405
406 // Test the helper function pattern
407 fn register_test_interceptors<P: Interceptor>(
408 registry: Registry<P>,
409 ) -> Registry<TestInterceptor<TestInterceptor<P>>> {
410 registry
411 .with(TestInterceptor::with_name("first"))
412 .with(TestInterceptor::with_name("second"))
413 }
414
415 #[test]
416 fn test_helper_function_pattern() {
417 let registry = Registry::new();
418 let registry = register_test_interceptors(registry);
419 let mut chain = registry.build();
420
421 let pkt = dummy_rtp_packet();
422 chain.handle_read(pkt).unwrap();
423 assert!(chain.poll_read().is_some());
424 assert_eq!(chain.name, "second");
425 assert_eq!(chain.inner.name, "first");
426 }
427}