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//! ```ignore
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//! # Stream Binding
113//!
114//! Before interceptors can process packets for a stream, the stream must be bound:
115//!
116//! ```ignore
117//! use rtc_interceptor::{StreamInfo, RTCPFeedback, RTPHeaderExtension};
118//!
119//! // Create stream info with NACK and TWCC support
120//! let stream_info = StreamInfo {
121//!     ssrc: 0x12345678,
122//!     clock_rate: 90000,
123//!     mime_type: "video/VP8".to_string(),
124//!     payload_type: 96,
125//!     rtcp_feedback: vec![RTCPFeedback {
126//!         typ: "nack".to_string(),
127//!         parameter: String::new(),
128//!     }],
129//!     rtp_header_extensions: vec![RTPHeaderExtension {
130//!         uri: "http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01".to_string(),
131//!         id: 5,
132//!     }],
133//!     ..Default::default()
134//! };
135//!
136//! // Bind for outgoing streams (sender side)
137//! chain.bind_local_stream(&stream_info);
138//!
139//! // Bind for incoming streams (receiver side)
140//! chain.bind_remote_stream(&stream_info);
141//! ```
142//!
143//! # Creating Custom Interceptors
144//!
145//! Use the derive macros to easily create custom interceptors:
146//!
147//! ```ignore
148//! use rtc_interceptor::{Interceptor, interceptor, TaggedPacket, StreamInfo};
149//! use std::collections::VecDeque;
150//!
151//! #[derive(Interceptor)]
152//! pub struct MyInterceptor<P: Interceptor> {
153//!     #[next]
154//!     next: P,  // The next interceptor in the chain (can use any field name)
155//!     buffer: VecDeque<TaggedPacket>,
156//! }
157//!
158//! #[interceptor]
159//! impl<P: Interceptor> MyInterceptor<P> {
160//!     #[overrides]
161//!     fn handle_read(&mut self, msg: TaggedPacket) -> Result<(), Self::Error> {
162//!         // Custom logic here
163//!         self.next.handle_read(msg)
164//!     }
165//! }
166//! ```
167//!
168//! - `#[derive(Interceptor)]` - Marks a struct as an interceptor, requires `#[next]` field
169//! - `#[interceptor]` - Generates `Protocol` and `Interceptor` trait implementations
170//! - `#[overrides]` - Marks methods with custom implementations (non-marked methods delegate to next)
171//!
172//! See the [`Interceptor`] trait documentation for more details.
173
174#![warn(rust_2018_idioms)]
175#![allow(dead_code)]
176
177use shared::TransportMessage;
178use std::time::Instant;
179
180mod noop;
181mod registry;
182
183pub(crate) mod nack;
184pub(crate) mod report;
185pub(crate) mod stream_info;
186pub(crate) mod twcc;
187
188pub use nack::{
189    generator::{NackGeneratorBuilder, NackGeneratorInterceptor},
190    responder::{NackResponderBuilder, NackResponderInterceptor},
191};
192pub use noop::NoopInterceptor;
193pub use registry::Registry;
194pub use report::{
195    receiver::{ReceiverReportBuilder, ReceiverReportInterceptor},
196    sender::{SenderReportBuilder, SenderReportInterceptor},
197};
198pub use stream_info::{RTCPFeedback, RTPHeaderExtension, StreamInfo};
199pub use twcc::{
200    receiver::{TwccReceiverBuilder, TwccReceiverInterceptor},
201    sender::{TwccSenderBuilder, TwccSenderInterceptor},
202};
203
204// Re-export derive macros for creating custom interceptors
205// - `Interceptor` derive macro: marks a struct as an interceptor with #[next] field
206// - `interceptor` attribute macro: generates Protocol and Interceptor trait implementations
207pub use interceptor_derive::{Interceptor, interceptor};
208
209/// RTP/RTCP Packet
210///
211/// An enum representing either an RTP or RTCP packet that can be processed
212/// by interceptors in the chain.
213#[derive(Debug, Clone, PartialEq)]
214pub enum Packet {
215    /// RTP (Real-time Transport Protocol) packet containing media data
216    Rtp(rtp::Packet),
217    /// RTCP (RTP Control Protocol) packets for feedback and statistics
218    Rtcp(Vec<Box<dyn rtcp::Packet>>),
219}
220
221/// Tagged packet with transport metadata.
222///
223/// A [`TransportMessage`] wrapping a [`Packet`], which includes transport-level
224/// context such as source/destination addresses and protocol information.
225/// This is the primary message type passed through interceptor chains.
226pub type TaggedPacket = TransportMessage<Packet>;
227
228/// Trait for RTP/RTCP interceptors with fixed Protocol type parameters.
229///
230/// `Interceptor` is a marker trait that requires implementors to also implement
231/// [`sansio::Protocol`] with specific fixed type parameters for RTP/RTCP processing:
232/// - `Rin`, `Win`, `Rout`, `Wout` = [`TaggedPacket`]
233/// - `Ein`, `Eout` = `()`
234/// - `Time` = [`Instant`]
235/// - `Error` = [`shared::error::Error`]
236///
237/// This trait adds stream binding methods and provides a [`with()`](Interceptor::with)
238/// method for composable chaining of interceptors.
239///
240/// # Creating Custom Interceptors
241///
242/// ## Using Derive Macros (Recommended)
243///
244/// The easiest way to create a custom interceptor is using the derive macros:
245///
246/// ```ignore
247/// use rtc_interceptor::{Interceptor, interceptor, TaggedPacket, Packet, StreamInfo};
248/// use std::collections::VecDeque;
249///
250/// #[derive(Interceptor)]
251/// pub struct MyInterceptor<P: Interceptor> {
252///     #[next]
253///     next: P,  // The next interceptor in the chain
254///     buffer: VecDeque<TaggedPacket>,
255/// }
256///
257/// #[interceptor]
258/// impl<P: Interceptor> MyInterceptor<P> {
259///     #[overrides]
260///     fn handle_read(&mut self, msg: TaggedPacket) -> Result<(), Self::Error> {
261///         // Custom logic here
262///         self.next.handle_read(msg)
263///     }
264/// }
265/// ```
266///
267/// The `#[derive(Interceptor)]` macro requires a `#[next]` field that contains the
268/// next interceptor in the chain. The `#[interceptor]` attribute on the impl block
269/// generates the `Protocol` and `Interceptor` trait implementations, delegating
270/// non-overridden methods to the next interceptor.
271///
272/// Use `#[overrides]` to mark methods with custom implementations.
273///
274/// ## Manual Implementation
275///
276/// For more control, you can implement the traits manually:
277///
278/// ```ignore
279/// pub struct MyInterceptor<P> {
280///     inner: P,
281/// }
282///
283/// impl<P: Interceptor> Protocol<TaggedPacket, TaggedPacket, ()> for MyInterceptor<P> {
284///     type Rout = TaggedPacket;
285///     type Wout = TaggedPacket;
286///     type Eout = ();
287///     type Time = Instant;
288///     type Error = shared::error::Error;
289///     // ... implement Protocol methods
290/// }
291///
292/// impl<P: Interceptor> Interceptor for MyInterceptor<P> {
293///     fn bind_local_stream(&mut self, _info: &StreamInfo) {}
294///     fn unbind_local_stream(&mut self, _info: &StreamInfo) {}
295///     fn bind_remote_stream(&mut self, _info: &StreamInfo) {}
296///     fn unbind_remote_stream(&mut self, _info: &StreamInfo) {}
297/// }
298/// ```
299///
300/// # Using with Registry
301///
302/// ```ignore
303/// let chain = Registry::new()
304///     .with(|inner| MyInterceptor { next: inner, buffer: VecDeque::new() });
305/// ```
306pub trait Interceptor:
307    sansio::Protocol<
308        TaggedPacket,
309        TaggedPacket,
310        (),
311        Rout = TaggedPacket,
312        Wout = TaggedPacket,
313        Eout = (),
314        Time = Instant,
315        Error = shared::error::Error,
316    > + Sized
317    + Send
318    + Sync
319    + 'static
320{
321    /// Wrap this interceptor with another layer.
322    ///
323    /// The wrapper function receives `self` and returns a new interceptor
324    /// that wraps it.
325    ///
326    /// # Example
327    ///
328    /// ```ignore
329    /// use std::time::Duration;
330    /// use rtc_interceptor::{NoopInterceptor, SenderReportBuilder};
331    ///
332    /// // Using the builder pattern (recommended)
333    /// let chain = NoopInterceptor::new()
334    ///     .with(SenderReportBuilder::new().with_interval(Duration::from_secs(1)).build());
335    /// ```
336    fn with<O, F>(self, f: F) -> O
337    where
338        F: FnOnce(Self) -> O,
339        O: Interceptor,
340    {
341        f(self)
342    }
343
344    /// bind_local_stream lets you modify any outgoing RTP packets. It is called once for per LocalStream. The returned method
345    /// will be called once per rtp packet.
346    fn bind_local_stream(&mut self, info: &StreamInfo);
347
348    /// unbind_local_stream is called when the Stream is removed. It can be used to clean up any data related to that track.
349    fn unbind_local_stream(&mut self, info: &StreamInfo);
350
351    /// bind_remote_stream lets you modify any incoming RTP packets. It is called once for per RemoteStream. The returned method
352    /// will be called once per rtp packet.
353    fn bind_remote_stream(&mut self, info: &StreamInfo);
354
355    /// unbind_remote_stream is called when the Stream is removed. It can be used to clean up any data related to that track.
356    fn unbind_remote_stream(&mut self, info: &StreamInfo);
357}
358
359#[cfg(test)]
360mod derive_tests {
361    use super::*;
362    #[allow(unused_imports)]
363    use shared::error::Error;
364
365    /// Test interceptor that uses the derive macro.
366    /// It should automatically delegate all Protocol and Interceptor methods to inner.
367    #[derive(Interceptor)]
368    pub struct SimplePassthrough<P: Interceptor> {
369        #[next]
370        inner: P,
371    }
372
373    // Empty impl block - #[interceptor] generates all delegations
374    #[interceptor]
375    impl<P: Interceptor> SimplePassthrough<P> {}
376
377    impl<P: Interceptor> SimplePassthrough<P> {
378        fn new(inner: P) -> Self {
379            Self { inner }
380        }
381    }
382
383    #[test]
384    fn test_derive_interceptor_basic() {
385        // Build a chain with the derived interceptor
386        let mut chain = SimplePassthrough::new(NoopInterceptor::new());
387
388        // Test that delegation works
389        let pkt = TaggedPacket {
390            now: std::time::Instant::now(),
391            transport: Default::default(),
392            message: Packet::Rtp(rtp::Packet::default()),
393        };
394
395        // handle_write should delegate to inner
396        sansio::Protocol::handle_write(&mut chain, pkt).unwrap();
397
398        // poll_write should return the packet from inner
399        let result = sansio::Protocol::poll_write(&mut chain);
400        assert!(result.is_some());
401    }
402
403    #[test]
404    fn test_derive_interceptor_close() {
405        let mut chain = SimplePassthrough::new(NoopInterceptor::new());
406
407        // close should delegate to inner without error
408        sansio::Protocol::close(&mut chain).unwrap();
409    }
410
411    #[test]
412    fn test_derive_interceptor_stream_binding() {
413        let mut chain = SimplePassthrough::new(NoopInterceptor::new());
414
415        let info = StreamInfo {
416            ssrc: 12345,
417            ..Default::default()
418        };
419
420        // These should delegate to inner without panic
421        chain.bind_local_stream(&info);
422        chain.unbind_local_stream(&info);
423        chain.bind_remote_stream(&info);
424        chain.unbind_remote_stream(&info);
425    }
426}