Skip to main content

rtc_interceptor/
lib.rs

1//! RTC Interceptor - Sans-IO interceptor framework for RTP/RTCP processing.
2//!
3//! This crate provides a composable interceptor framework built on top of the
4//! [`sansio::Protocol`] trait. Interceptors can process, modify, or generate
5//! RTP/RTCP packets as they flow through the pipeline.
6//!
7//! # Available Interceptors
8//!
9//! ## RTCP Reports
10//!
11//! | Interceptor | Description |
12//! |-------------|-------------|
13//! | [`SenderReportInterceptor`] | Generates RTCP Sender Reports (SR) for local streams and filters hop-by-hop RTCP feedback |
14//! | [`ReceiverReportInterceptor`] | Generates RTCP Receiver Reports (RR) based on incoming RTP statistics |
15//!
16//! ## NACK (Negative Acknowledgement)
17//!
18//! | Interceptor | Description |
19//! |-------------|-------------|
20//! | [`NackGeneratorInterceptor`] | Detects missing RTP packets and generates NACK requests (RFC 4585) |
21//! | [`NackResponderInterceptor`] | Buffers sent packets and retransmits on NACK, with optional RTX support (RFC 4588) |
22//!
23//! ## TWCC (Transport Wide Congestion Control)
24//!
25//! | Interceptor | Description |
26//! |-------------|-------------|
27//! | [`TwccSenderInterceptor`] | Adds transport-wide sequence numbers to outgoing RTP packets |
28//! | [`TwccReceiverInterceptor`] | Tracks incoming packets and generates TransportLayerCC feedback |
29//!
30//! ## Utility
31//!
32//! | Interceptor | Description |
33//! |-------------|-------------|
34//! | [`NoopInterceptor`] | Pass-through terminal for interceptor chains |
35//!
36//! # Design
37//!
38//! Each interceptor wraps an inner `Interceptor` and can:
39//! - Process incoming/outgoing RTP/RTCP packets
40//! - Modify packet contents (headers, payloads)
41//! - Generate new packets (e.g., RTCP Sender/Receiver Reports)
42//! - Handle timeouts for periodic tasks (e.g., report generation)
43//! - Track stream statistics and state
44//!
45//! All interceptors work with [`TaggedPacket`] (RTP or RTCP packets with transport metadata).
46//! The innermost interceptor is typically [`NoopInterceptor`], which serves as the terminal.
47//!
48//! # No Direction Concept
49//!
50//! **Important:** Unlike PeerConnection's pipeline where `read` and `write` have
51//! opposite processing direction orders, interceptors have **no direction concept**.
52//!
53//! In PeerConnection's pipeline:
54//! ```text
55//! Read:  Network → HandlerA → HandlerB → HandlerC → Application
56//! Write: Application → HandlerC → HandlerB → HandlerA → Network
57//!        (reversed order)
58//! ```
59//!
60//! In Interceptor chains, all operations flow in the **same direction**:
61//! ```text
62//! handle_read:    Outer → Inner (A.handle_read calls B.handle_read calls C.handle_read)
63//! handle_write:   Outer → Inner (A.handle_write calls B.handle_write calls C.handle_write)
64//! handle_event:   Outer → Inner (A.handle_event calls B.handle_event calls C.handle_event)
65//! handle_timeout: Outer → Inner (A.handle_timeout calls B.handle_timeout calls C.handle_timeout)
66//!
67//! poll_read:    Outer → Inner (A.poll_read calls B.poll_read calls C.poll_read)
68//! poll_write:   Outer → Inner (A.poll_write calls B.poll_write calls C.poll_write)
69//! poll_event:   Outer → Inner (A.poll_event calls B.poll_event calls C.poll_event)
70//! poll_timeout: Outer → Inner (A.poll_timeout calls B.poll_timeout calls C.poll_timeout)
71//! ```
72//!
73//! This means interceptors are symmetric - they process `read`, `write`, and `event`
74//! in the same structural order. The distinction between "inbound" and "outbound"
75//! is semantic (based on message content), not structural (based on call order).
76//!
77//! # Quick Start
78//!
79//! ```
80//! use rtc_interceptor::{
81//!     Registry, SenderReportBuilder, ReceiverReportBuilder,
82//!     NackGeneratorBuilder, NackResponderBuilder,
83//!     TwccSenderBuilder, TwccReceiverBuilder,
84//! };
85//! use std::time::Duration;
86//!
87//! // Build a full-featured interceptor chain
88//! let chain = Registry::new()
89//!     // RTCP reports
90//!     .with(SenderReportBuilder::new()
91//!         .with_interval(Duration::from_secs(1))
92//!         .build())
93//!     .with(ReceiverReportBuilder::new()
94//!         .with_interval(Duration::from_secs(1))
95//!         .build())
96//!     // NACK for packet loss recovery
97//!     .with(NackGeneratorBuilder::new()
98//!         .with_size(512)
99//!         .with_interval(Duration::from_millis(100))
100//!         .build())
101//!     .with(NackResponderBuilder::new()
102//!         .with_size(1024)
103//!         .build())
104//!     // TWCC for congestion control
105//!     .with(TwccSenderBuilder::new().build())
106//!     .with(TwccReceiverBuilder::new()
107//!         .with_interval(Duration::from_millis(100))
108//!         .build())
109//!     .build();
110//! ```
111//!
112//! # Type-Erasing a Chain
113//!
114//! A chain's type spells out its whole composition
115//! (`TwccReceiverInterceptor<SenderReportInterceptor<…>>`), and it propagates into every type
116//! that holds the peer connection built from it. That is fine when the chain is fixed at compile
117//! time, and a problem when it is chosen at runtime or has to live in your own structs.
118//!
119//! [`Interceptor`] is object safe, so [`Registry::boxed`] can erase the chain to
120//! [`BoxedInterceptor`] — one concrete type, whatever it was built from:
121//!
122//! ```
123//! use rtc_interceptor::{BoxedInterceptor, NackGeneratorBuilder, Registry, SenderReportBuilder};
124//!
125//! // Two different chain types, unified by `.boxed()`.
126//! let chain: BoxedInterceptor = if cfg!(feature = "unstable") {
127//!     Registry::new()
128//!         .with(SenderReportBuilder::new().build())
129//!         .with(NackGeneratorBuilder::new().build())
130//!         .boxed()
131//!         .build()
132//! } else {
133//!     Registry::new().with(SenderReportBuilder::new().build()).boxed().build()
134//! };
135//! ```
136//!
137//! The cost is one virtual call per chain entry point (`handle_read`, `poll_write`,
138//! `handle_timeout`, …); the layers inside still call each other through static dispatch and
139//! inline as before. `Box<P>` and `&mut P` both implement [`Interceptor`], so a boxed or borrowed
140//! chain satisfies an `I: Interceptor` bound like any other.
141//!
142//! # Stream Binding
143//!
144//! Before interceptors can process packets for a stream, the stream must be bound:
145//!
146//! ```
147//! use rtc_interceptor::{Interceptor, RTCPFeedback, RTPHeaderExtension, Registry, StreamInfo};
148//!
149//! let mut chain = Registry::new().build();
150//!
151//! // Create stream info with NACK and TWCC support
152//! let stream_info = StreamInfo {
153//!     ssrc: 0x12345678,
154//!     clock_rate: 90000,
155//!     mime_type: "video/VP8".to_string(),
156//!     payload_type: 96,
157//!     rtcp_feedback: vec![RTCPFeedback {
158//!         typ: "nack".to_string(),
159//!         parameter: String::new(),
160//!     }],
161//!     rtp_header_extensions: vec![RTPHeaderExtension {
162//!         uri: "http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01".to_string(),
163//!         id: 5,
164//!     }],
165//!     ..Default::default()
166//! };
167//!
168//! // Bind for outgoing streams (sender side)
169//! chain.bind_local_stream(&stream_info);
170//!
171//! // Bind for incoming streams (receiver side)
172//! chain.bind_remote_stream(&stream_info);
173//! ```
174//!
175//! # Creating Custom Interceptors
176//!
177//! Use the derive macros to easily create custom interceptors:
178//!
179//! ```
180//! use rtc_interceptor::{Interceptor, StreamInfo, TaggedPacket, interceptor};
181//! use sansio::Protocol;
182//! use shared::error::Error; // the generated `Protocol` impl names it
183//! use std::collections::VecDeque;
184//!
185//! #[derive(Interceptor)]
186//! pub struct MyInterceptor<P: Interceptor> {
187//!     #[next]
188//!     next: P,  // The next interceptor in the chain (can use any field name)
189//!     buffer: VecDeque<TaggedPacket>,
190//! }
191//!
192//! #[interceptor]
193//! impl<P: Interceptor> MyInterceptor<P> {
194//!     #[overrides]
195//!     fn handle_read(&mut self, msg: TaggedPacket) -> Result<(), Self::Error> {
196//!         // Custom logic here
197//!         self.next.handle_read(msg)
198//!     }
199//! }
200//! ```
201//!
202//! - `#[derive(Interceptor)]` - Marks a struct as an interceptor, requires `#[next]` field
203//! - `#[interceptor]` - Generates `Protocol` and `Interceptor` trait implementations
204//! - `#[overrides]` - Marks methods with custom implementations (non-marked methods delegate to next)
205//!
206//! See the [`Interceptor`] trait documentation for more details.
207
208#![warn(rust_2018_idioms)]
209#![warn(missing_docs)]
210#![allow(dead_code)]
211
212use shared::TransportMessage;
213use std::time::Instant;
214
215mod noop;
216mod registry;
217
218pub(crate) mod nack;
219pub(crate) mod report;
220pub(crate) mod stream_info;
221pub(crate) mod twcc;
222
223pub use nack::{
224    generator::{NackGeneratorBuilder, NackGeneratorInterceptor},
225    responder::{NackResponderBuilder, NackResponderInterceptor},
226};
227pub use noop::NoopInterceptor;
228pub use registry::Registry;
229pub use report::{
230    receiver::{ReceiverReportBuilder, ReceiverReportInterceptor},
231    sender::{SenderReportBuilder, SenderReportInterceptor},
232};
233pub use stream_info::{RTCPFeedback, RTPHeaderExtension, StreamInfo};
234pub use twcc::{
235    receiver::{TwccReceiverBuilder, TwccReceiverInterceptor},
236    sender::{TwccSenderBuilder, TwccSenderInterceptor},
237};
238
239// Re-export derive macros for creating custom interceptors
240// - `Interceptor` derive macro: marks a struct as an interceptor with #[next] field
241// - `interceptor` attribute macro: generates Protocol and Interceptor trait implementations
242pub use interceptor_derive::{Interceptor, interceptor};
243
244/// RTP/RTCP Packet
245///
246/// An enum representing either an RTP or RTCP packet that can be processed
247/// by interceptors in the chain.
248#[derive(Debug, Clone, PartialEq)]
249pub enum Packet {
250    /// RTP (Real-time Transport Protocol) packet containing media data
251    Rtp(rtp::Packet),
252    /// RTCP (RTP Control Protocol) packets for feedback and statistics
253    Rtcp(Vec<Box<dyn rtcp::Packet>>),
254}
255
256/// Tagged packet with transport metadata.
257///
258/// A [`TransportMessage`] wrapping a [`Packet`], which includes transport-level
259/// context such as source/destination addresses and protocol information.
260/// This is the primary message type passed through interceptor chains.
261pub type TaggedPacket = TransportMessage<Packet>;
262
263/// Trait for RTP/RTCP interceptors with fixed Protocol type parameters.
264///
265/// `Interceptor` is a marker trait that requires implementors to also implement
266/// [`sansio::Protocol`] with specific fixed type parameters for RTP/RTCP processing:
267/// - `Rin`, `Win`, `Rout`, `Wout` = [`TaggedPacket`]
268/// - `Ein`, `Eout` = `()`
269/// - `Time` = [`Instant`]
270/// - `Error` = [`shared::error::Error`]
271///
272/// This trait adds stream binding methods and provides a [`with()`](Interceptor::with)
273/// method for composable chaining of interceptors.
274///
275/// # Creating Custom Interceptors
276///
277/// ## Using Derive Macros (Recommended)
278///
279/// The easiest way to create a custom interceptor is using the derive macros:
280///
281/// ```
282/// use rtc_interceptor::{Interceptor, StreamInfo, TaggedPacket, interceptor};
283/// use sansio::Protocol;
284/// use shared::error::Error; // the generated `Protocol` impl names it
285/// use std::collections::VecDeque;
286///
287/// #[derive(Interceptor)]
288/// pub struct MyInterceptor<P: Interceptor> {
289///     #[next]
290///     next: P,  // The next interceptor in the chain
291///     buffer: VecDeque<TaggedPacket>,
292/// }
293///
294/// #[interceptor]
295/// impl<P: Interceptor> MyInterceptor<P> {
296///     #[overrides]
297///     fn handle_read(&mut self, msg: TaggedPacket) -> Result<(), Self::Error> {
298///         // Custom logic here
299///         self.next.handle_read(msg)
300///     }
301/// }
302/// ```
303///
304/// The `#[derive(Interceptor)]` macro requires a `#[next]` field that contains the
305/// next interceptor in the chain. The `#[interceptor]` attribute on the impl block
306/// generates the `Protocol` and `Interceptor` trait implementations, delegating
307/// non-overridden methods to the next interceptor.
308///
309/// Use `#[overrides]` to mark methods with custom implementations.
310///
311/// ## Manual Implementation
312///
313/// For more control, you can implement the traits manually. The sketch below omits the
314/// `Protocol` method bodies, so it is not compiled — see [`NoopInterceptor`] for a complete
315/// hand-written implementation:
316///
317/// ```ignore
318/// pub struct MyInterceptor<P> {
319///     inner: P,
320/// }
321///
322/// impl<P: Interceptor> Protocol<TaggedPacket, TaggedPacket, ()> for MyInterceptor<P> {
323///     type Rout = TaggedPacket;
324///     type Wout = TaggedPacket;
325///     type Eout = ();
326///     type Time = Instant;
327///     type Error = shared::error::Error;
328///     // ... implement Protocol methods
329/// }
330///
331/// impl<P: Interceptor> Interceptor for MyInterceptor<P> {
332///     fn bind_local_stream(&mut self, _info: &StreamInfo) {}
333///     fn unbind_local_stream(&mut self, _info: &StreamInfo) {}
334///     fn bind_remote_stream(&mut self, _info: &StreamInfo) {}
335///     fn unbind_remote_stream(&mut self, _info: &StreamInfo) {}
336/// }
337/// ```
338///
339/// # Using with Registry
340///
341/// A builder is just a closure from the next layer to the wrapping one, so a custom
342/// interceptor can be added the same way as a built-in:
343///
344/// ```
345/// use rtc_interceptor::{Registry, SenderReportBuilder};
346///
347/// let registry = Registry::new().with(SenderReportBuilder::new().build());
348/// // ...or with a closure: `.with(|inner| MyInterceptor { next: inner, .. })`
349/// ```
350pub trait Interceptor:
351    sansio::Protocol<
352        TaggedPacket,
353        TaggedPacket,
354        (),
355        Rout = TaggedPacket,
356        Wout = TaggedPacket,
357        Eout = (),
358        Time = Instant,
359        Error = shared::error::Error,
360    > + Send
361    + Sync
362{
363    /// Wrap this interceptor with another layer.
364    ///
365    /// The wrapper function receives `self` and returns a new interceptor
366    /// that wraps it.
367    ///
368    /// # Example
369    ///
370    /// ```
371    /// use rtc_interceptor::{Interceptor, NoopInterceptor, SenderReportBuilder};
372    /// use std::time::Duration;
373    ///
374    /// // `Interceptor` must be in scope for `with` to resolve.
375    /// let chain = NoopInterceptor::new()
376    ///     .with(SenderReportBuilder::new().with_interval(Duration::from_secs(1)).build());
377    /// ```
378    fn with<O, F>(self, f: F) -> O
379    where
380        Self: Sized,
381        F: FnOnce(Self) -> O,
382        O: Interceptor,
383    {
384        f(self)
385    }
386
387    /// bind_local_stream lets you modify any outgoing RTP packets. It is called once for per LocalStream. The returned method
388    /// will be called once per rtp packet.
389    fn bind_local_stream(&mut self, info: &StreamInfo);
390
391    /// unbind_local_stream is called when the Stream is removed. It can be used to clean up any data related to that track.
392    fn unbind_local_stream(&mut self, info: &StreamInfo);
393
394    /// bind_remote_stream lets you modify any incoming RTP packets. It is called once for per RemoteStream. The returned method
395    /// will be called once per rtp packet.
396    fn bind_remote_stream(&mut self, info: &StreamInfo);
397
398    /// unbind_remote_stream is called when the Stream is removed. It can be used to clean up any data related to that track.
399    fn unbind_remote_stream(&mut self, info: &StreamInfo);
400}
401
402/// A type-erased interceptor chain.
403///
404/// `Interceptor` is object safe, so a chain built at runtime can be erased into this one
405/// concrete type. That lets an application store a `RTCPeerConnection<BoxedInterceptor>`
406/// (see [`Registry::boxed`]) instead of being generic over the chain's type.
407pub type BoxedInterceptor = Box<dyn Interceptor>;
408
409impl<P: Interceptor + ?Sized> Interceptor for Box<P> {
410    fn bind_local_stream(&mut self, info: &StreamInfo) {
411        (**self).bind_local_stream(info)
412    }
413
414    fn unbind_local_stream(&mut self, info: &StreamInfo) {
415        (**self).unbind_local_stream(info)
416    }
417
418    fn bind_remote_stream(&mut self, info: &StreamInfo) {
419        (**self).bind_remote_stream(info)
420    }
421
422    fn unbind_remote_stream(&mut self, info: &StreamInfo) {
423        (**self).unbind_remote_stream(info)
424    }
425}
426
427/// Blanket implementation for mutable references.
428///
429/// This lets a borrowed chain satisfy an `Interceptor` bound, so a function taking
430/// `I: Interceptor` by value can be called with `&mut chain` and leave ownership with the
431/// caller. It mirrors [`sansio::Protocol`]'s own `&mut P` implementation, and the same idiom
432/// in `std` (`impl Read for &mut R`, `impl Iterator for &mut I`).
433///
434/// This is only expressible because [`Interceptor`] does not require `'static`: `&'a mut P`
435/// outlives only `'a`. See [`Registry::boxed`], which carries that bound locally instead.
436impl<P: Interceptor + ?Sized> Interceptor for &mut P {
437    fn bind_local_stream(&mut self, info: &StreamInfo) {
438        (**self).bind_local_stream(info)
439    }
440
441    fn unbind_local_stream(&mut self, info: &StreamInfo) {
442        (**self).unbind_local_stream(info)
443    }
444
445    fn bind_remote_stream(&mut self, info: &StreamInfo) {
446        (**self).bind_remote_stream(info)
447    }
448
449    fn unbind_remote_stream(&mut self, info: &StreamInfo) {
450        (**self).unbind_remote_stream(info)
451    }
452}
453
454#[cfg(test)]
455mod derive_tests {
456    use super::*;
457    #[allow(unused_imports)]
458    use shared::error::Error;
459
460    /// Test interceptor that uses the derive macro.
461    /// It should automatically delegate all Protocol and Interceptor methods to inner.
462    #[derive(Interceptor)]
463    pub struct SimplePassthrough<P: Interceptor> {
464        #[next]
465        inner: P,
466    }
467
468    // Empty impl block - #[interceptor] generates all delegations
469    #[interceptor]
470    impl<P: Interceptor> SimplePassthrough<P> {}
471
472    impl<P: Interceptor> SimplePassthrough<P> {
473        fn new(inner: P) -> Self {
474            Self { inner }
475        }
476    }
477
478    #[test]
479    fn test_derive_interceptor_basic() {
480        // Build a chain with the derived interceptor
481        let mut chain = SimplePassthrough::new(NoopInterceptor::new());
482
483        // Test that delegation works
484        let pkt = TaggedPacket {
485            now: std::time::Instant::now(),
486            transport: Default::default(),
487            message: Packet::Rtp(rtp::Packet::default()),
488        };
489
490        // handle_write should delegate to inner
491        sansio::Protocol::handle_write(&mut chain, pkt).unwrap();
492
493        // poll_write should return the packet from inner
494        let result = sansio::Protocol::poll_write(&mut chain);
495        assert!(result.is_some());
496    }
497
498    #[test]
499    fn test_derive_interceptor_close() {
500        let mut chain = SimplePassthrough::new(NoopInterceptor::new());
501
502        // close should delegate to inner without error
503        sansio::Protocol::close(&mut chain).unwrap();
504    }
505
506    #[test]
507    fn test_derive_interceptor_stream_binding() {
508        let mut chain = SimplePassthrough::new(NoopInterceptor::new());
509
510        let info = StreamInfo {
511            ssrc: 12345,
512            ..Default::default()
513        };
514
515        // These should delegate to inner without panic
516        chain.bind_local_stream(&info);
517        chain.unbind_local_stream(&info);
518        chain.bind_remote_stream(&info);
519        chain.unbind_remote_stream(&info);
520    }
521
522    /// Consumes an interceptor by value, as the `Registry`/`with` builders do.
523    fn takes_by_value<I: Interceptor>(mut interceptor: I, info: &StreamInfo) {
524        interceptor.bind_local_stream(info);
525        interceptor.unbind_local_stream(info);
526    }
527
528    #[test]
529    fn test_borrowed_chain_satisfies_interceptor_bound() {
530        let mut chain = SimplePassthrough::new(NoopInterceptor::new());
531        let info = StreamInfo {
532            ssrc: 12345,
533            ..Default::default()
534        };
535
536        // `&mut chain` satisfies a by-value `I: Interceptor` bound thanks to the blanket impl.
537        takes_by_value(&mut chain, &info);
538
539        // Ownership stayed with us, so the chain is still usable afterwards.
540        takes_by_value(&mut chain, &info);
541        chain.bind_remote_stream(&info);
542
543        // The borrow also still drives the Protocol side.
544        let pkt = TaggedPacket {
545            now: std::time::Instant::now(),
546            transport: Default::default(),
547            message: Packet::Rtp(rtp::Packet::default()),
548        };
549        sansio::Protocol::handle_write(&mut chain, pkt).unwrap();
550        assert!(sansio::Protocol::poll_write(&mut chain).is_some());
551    }
552
553    #[test]
554    fn test_boxed_chain_still_satisfies_interceptor_bound() {
555        // The `Box<P>` impl coexists with the new `&mut P` impl.
556        let chain: BoxedInterceptor = Box::new(SimplePassthrough::new(NoopInterceptor::new()));
557        let info = StreamInfo {
558            ssrc: 999,
559            ..Default::default()
560        };
561        takes_by_value(chain, &info);
562    }
563}