Skip to main content

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}