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//! # let nack_enabled = true; // e.g. from configuration, negotiated SDP, …
126//! // Two different chain types, unified by `.boxed()`.
127//! let chain: BoxedInterceptor = if nack_enabled {
128//! Registry::new()
129//! .with(SenderReportBuilder::new().build())
130//! .with(NackGeneratorBuilder::new().build())
131//! .boxed()
132//! .build()
133//! } else {
134//! Registry::new().with(SenderReportBuilder::new().build()).boxed().build()
135//! };
136//! ```
137//!
138//! The cost is one virtual call per chain entry point (`handle_read`, `poll_write`,
139//! `handle_timeout`, …); the layers inside still call each other through static dispatch and
140//! inline as before. `Box<P>` and `&mut P` both implement [`Interceptor`], so a boxed or borrowed
141//! chain satisfies an `I: Interceptor` bound like any other.
142//!
143//! # Stream Binding
144//!
145//! Before interceptors can process packets for a stream, the stream must be bound:
146//!
147//! ```
148//! use rtc_interceptor::{Interceptor, RTCPFeedback, RTPHeaderExtension, Registry, StreamInfo};
149//!
150//! let mut chain = Registry::new().build();
151//!
152//! // Create stream info with NACK and TWCC support
153//! let stream_info = StreamInfo {
154//! ssrc: 0x12345678,
155//! clock_rate: 90000,
156//! mime_type: "video/VP8".to_string(),
157//! payload_type: 96,
158//! rtcp_feedback: vec![RTCPFeedback {
159//! typ: "nack".to_string(),
160//! parameter: String::new(),
161//! }],
162//! rtp_header_extensions: vec![RTPHeaderExtension {
163//! uri: "http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01".to_string(),
164//! id: 5,
165//! }],
166//! ..Default::default()
167//! };
168//!
169//! // Bind for outgoing streams (sender side)
170//! chain.bind_local_stream(&stream_info);
171//!
172//! // Bind for incoming streams (receiver side)
173//! chain.bind_remote_stream(&stream_info);
174//! ```
175//!
176//! # Creating Custom Interceptors
177//!
178//! Use the derive macros to easily create custom interceptors:
179//!
180//! ```
181//! use rtc_interceptor::{Interceptor, StreamInfo, TaggedPacket, interceptor};
182//! use sansio::Protocol;
183//! use shared::error::Error; // the generated `Protocol` impl names it
184//! use std::collections::VecDeque;
185//!
186//! #[derive(Interceptor)]
187//! pub struct MyInterceptor<P: Interceptor> {
188//! #[next]
189//! next: P, // The next interceptor in the chain (can use any field name)
190//! buffer: VecDeque<TaggedPacket>,
191//! }
192//!
193//! #[interceptor]
194//! impl<P: Interceptor> MyInterceptor<P> {
195//! #[overrides]
196//! fn handle_read(&mut self, msg: TaggedPacket) -> Result<(), Self::Error> {
197//! // Custom logic here
198//! self.next.handle_read(msg)
199//! }
200//! }
201//! ```
202//!
203//! - `#[derive(Interceptor)]` - Marks a struct as an interceptor, requires `#[next]` field
204//! - `#[interceptor]` - Generates `Protocol` and `Interceptor` trait implementations
205//! - `#[overrides]` - Marks methods with custom implementations (non-marked methods delegate to next)
206//!
207//! See the [`Interceptor`] trait documentation for more details.
208
209#![warn(rust_2018_idioms)]
210#![warn(missing_docs)]
211#![allow(dead_code)]
212
213use shared::TransportMessage;
214use std::time::Instant;
215
216mod noop;
217mod registry;
218
219pub(crate) mod flexfec;
220pub(crate) mod intervalpli;
221pub(crate) mod jitterbuffer;
222pub(crate) mod nack;
223pub(crate) mod report;
224pub(crate) mod rfc8888;
225pub(crate) mod rtpfb;
226pub(crate) mod stream_info;
227pub(crate) mod twcc;
228
229pub use flexfec::bit_array::BitArray;
230pub use flexfec::coverage::{MAX_FEC_PACKETS, MAX_MEDIA_PACKETS, ProtectionCoverage};
231pub use flexfec::draft03::decoder::{FlexFec03Decoder, ParseError as FlexFecParseError};
232pub use flexfec::draft03::encoder::FlexFec03Encoder;
233pub use flexfec::draft03::receiver::{FlexFec03ReceiveBuilder, FlexFec03ReceiveInterceptor};
234pub use flexfec::draft03::sender::{
235 DEFAULT_NUM_FEC_PACKETS, DEFAULT_NUM_MEDIA_PACKETS, FlexFec03SendBuilder,
236 FlexFec03SendInterceptor,
237};
238pub use intervalpli::generator::{
239 DEFAULT_INTERVAL as INTERVAL_PLI_DEFAULT_INTERVAL, IntervalPliBuilder, IntervalPliInterceptor,
240};
241pub use jitterbuffer::buffer::{
242 JitterBuffer, JitterBufferStats, Rejected, State as JitterBufferState,
243};
244pub use jitterbuffer::receiver::{
245 DEFAULT_CAPACITY as JITTER_BUFFER_DEFAULT_CAPACITY,
246 DEFAULT_DEPTH as JITTER_BUFFER_DEFAULT_DEPTH, JitterBufferBuilder, JitterBufferInterceptor,
247};
248pub use nack::{
249 generator::{NackGeneratorBuilder, NackGeneratorInterceptor},
250 responder::{NackResponderBuilder, NackResponderInterceptor},
251};
252pub use noop::NoopInterceptor;
253pub use registry::Registry;
254pub use report::{
255 receiver::{ReceiverReportBuilder, ReceiverReportInterceptor},
256 sender::{SenderReportBuilder, SenderReportInterceptor},
257};
258pub use rfc8888::recorder::CcFeedbackRecorder;
259pub use rfc8888::sender::{
260 DEFAULT_INTERVAL as RFC8888_DEFAULT_INTERVAL,
261 DEFAULT_MAX_REPORT_SIZE as RFC8888_DEFAULT_MAX_REPORT_SIZE, Rfc8888Builder, Rfc8888Interceptor,
262};
263pub use rtpfb::acknowledgement::{Acknowledgement, PacketReport, Report};
264pub use rtpfb::convert::{convert_ccfb, convert_twcc};
265pub use rtpfb::history::History;
266pub use stream_info::{RTCPFeedback, RTPHeaderExtension, StreamInfo};
267pub use twcc::{
268 receiver::{TwccReceiverBuilder, TwccReceiverInterceptor},
269 sender::{TwccSenderBuilder, TwccSenderInterceptor},
270};
271
272// Re-export derive macros for creating custom interceptors
273// - `Interceptor` derive macro: marks a struct as an interceptor with #[next] field
274// - `interceptor` attribute macro: generates Protocol and Interceptor trait implementations
275pub use interceptor_derive::{Interceptor, interceptor};
276
277/// RTP/RTCP Packet
278///
279/// An enum representing either an RTP or RTCP packet that can be processed
280/// by interceptors in the chain.
281#[derive(Debug, Clone, PartialEq)]
282#[non_exhaustive]
283pub enum Packet {
284 /// RTP (Real-time Transport Protocol) packet containing media data
285 Rtp(rtp::Packet),
286 /// RTCP (RTP Control Protocol) packets for feedback and statistics
287 Rtcp(Vec<Box<dyn rtcp::Packet>>),
288}
289
290/// Tagged packet with transport metadata.
291///
292/// A [`TransportMessage`] wrapping a [`Packet`], which includes transport-level
293/// context such as source/destination addresses and protocol information.
294/// This is the primary message type passed through interceptor chains.
295pub type TaggedPacket = TransportMessage<Packet>;
296
297/// Trait for RTP/RTCP interceptors with fixed Protocol type parameters.
298///
299/// `Interceptor` is a marker trait that requires implementors to also implement
300/// [`sansio::Protocol`] with specific fixed type parameters for RTP/RTCP processing:
301/// - `Rin`, `Win`, `Rout`, `Wout` = [`TaggedPacket`]
302/// - `Ein`, `Eout` = `()`
303/// - `Time` = [`Instant`]
304/// - `Error` = [`shared::error::Error`]
305///
306/// This trait adds stream binding methods and provides a [`with()`](Interceptor::with)
307/// method for composable chaining of interceptors.
308///
309/// # Creating Custom Interceptors
310///
311/// ## Using Derive Macros (Recommended)
312///
313/// The easiest way to create a custom interceptor is using the derive macros:
314///
315/// ```
316/// use rtc_interceptor::{Interceptor, StreamInfo, TaggedPacket, interceptor};
317/// use sansio::Protocol;
318/// use shared::error::Error; // the generated `Protocol` impl names it
319/// use std::collections::VecDeque;
320///
321/// #[derive(Interceptor)]
322/// pub struct MyInterceptor<P: Interceptor> {
323/// #[next]
324/// next: P, // The next interceptor in the chain
325/// buffer: VecDeque<TaggedPacket>,
326/// }
327///
328/// #[interceptor]
329/// impl<P: Interceptor> MyInterceptor<P> {
330/// #[overrides]
331/// fn handle_read(&mut self, msg: TaggedPacket) -> Result<(), Self::Error> {
332/// // Custom logic here
333/// self.next.handle_read(msg)
334/// }
335/// }
336/// ```
337///
338/// The `#[derive(Interceptor)]` macro requires a `#[next]` field that contains the
339/// next interceptor in the chain. The `#[interceptor]` attribute on the impl block
340/// generates the `Protocol` and `Interceptor` trait implementations, delegating
341/// non-overridden methods to the next interceptor.
342///
343/// Use `#[overrides]` to mark methods with custom implementations.
344///
345/// ## Manual Implementation
346///
347/// For more control, you can implement the traits manually. The sketch below omits the
348/// `Protocol` method bodies, so it is not compiled — see [`NoopInterceptor`] for a complete
349/// hand-written implementation:
350///
351/// ```ignore
352/// pub struct MyInterceptor<P> {
353/// inner: P,
354/// }
355///
356/// impl<P: Interceptor> Protocol<TaggedPacket, TaggedPacket, ()> for MyInterceptor<P> {
357/// type Rout = TaggedPacket;
358/// type Wout = TaggedPacket;
359/// type Eout = ();
360/// type Time = Instant;
361/// type Error = shared::error::Error;
362/// // ... implement Protocol methods
363/// }
364///
365/// impl<P: Interceptor> Interceptor for MyInterceptor<P> {
366/// fn bind_local_stream(&mut self, _info: &StreamInfo) {}
367/// fn unbind_local_stream(&mut self, _info: &StreamInfo) {}
368/// fn bind_remote_stream(&mut self, _info: &StreamInfo) {}
369/// fn unbind_remote_stream(&mut self, _info: &StreamInfo) {}
370/// }
371/// ```
372///
373/// # Using with Registry
374///
375/// A builder is just a closure from the next layer to the wrapping one, so a custom
376/// interceptor can be added the same way as a built-in:
377///
378/// ```
379/// use rtc_interceptor::{Registry, SenderReportBuilder};
380///
381/// let registry = Registry::new().with(SenderReportBuilder::new().build());
382/// // ...or with a closure: `.with(|inner| MyInterceptor { next: inner, .. })`
383/// ```
384pub trait Interceptor:
385 sansio::Protocol<
386 TaggedPacket,
387 TaggedPacket,
388 (),
389 Rout = TaggedPacket,
390 Wout = TaggedPacket,
391 Eout = (),
392 Time = Instant,
393 Error = shared::error::Error,
394 > + Send
395 + Sync
396{
397 /// Wrap this interceptor with another layer.
398 ///
399 /// The wrapper function receives `self` and returns a new interceptor
400 /// that wraps it.
401 ///
402 /// # Example
403 ///
404 /// ```
405 /// use rtc_interceptor::{Interceptor, NoopInterceptor, SenderReportBuilder};
406 /// use std::time::Duration;
407 ///
408 /// // `Interceptor` must be in scope for `with` to resolve.
409 /// let chain = NoopInterceptor::new()
410 /// .with(SenderReportBuilder::new().with_interval(Duration::from_secs(1)).build());
411 /// ```
412 fn with<O, F>(self, f: F) -> O
413 where
414 Self: Sized,
415 F: FnOnce(Self) -> O,
416 O: Interceptor,
417 {
418 f(self)
419 }
420
421 /// bind_local_stream lets you modify any outgoing RTP packets. It is called once for per LocalStream. The returned method
422 /// will be called once per rtp packet.
423 fn bind_local_stream(&mut self, info: &StreamInfo);
424
425 /// unbind_local_stream is called when the Stream is removed. It can be used to clean up any data related to that track.
426 fn unbind_local_stream(&mut self, info: &StreamInfo);
427
428 /// bind_remote_stream lets you modify any incoming RTP packets. It is called once for per RemoteStream. The returned method
429 /// will be called once per rtp packet.
430 fn bind_remote_stream(&mut self, info: &StreamInfo);
431
432 /// unbind_remote_stream is called when the Stream is removed. It can be used to clean up any data related to that track.
433 fn unbind_remote_stream(&mut self, info: &StreamInfo);
434}
435
436/// A type-erased interceptor chain.
437///
438/// `Interceptor` is object safe, so a chain built at runtime can be erased into this one
439/// concrete type. That lets an application store a `RTCPeerConnection<BoxedInterceptor>`
440/// (see [`Registry::boxed`]) instead of being generic over the chain's type.
441pub type BoxedInterceptor = Box<dyn Interceptor>;
442
443impl<P: Interceptor + ?Sized> Interceptor for Box<P> {
444 fn bind_local_stream(&mut self, info: &StreamInfo) {
445 (**self).bind_local_stream(info)
446 }
447
448 fn unbind_local_stream(&mut self, info: &StreamInfo) {
449 (**self).unbind_local_stream(info)
450 }
451
452 fn bind_remote_stream(&mut self, info: &StreamInfo) {
453 (**self).bind_remote_stream(info)
454 }
455
456 fn unbind_remote_stream(&mut self, info: &StreamInfo) {
457 (**self).unbind_remote_stream(info)
458 }
459}
460
461/// Blanket implementation for mutable references.
462///
463/// This lets a borrowed chain satisfy an `Interceptor` bound, so a function taking
464/// `I: Interceptor` by value can be called with `&mut chain` and leave ownership with the
465/// caller. It mirrors [`sansio::Protocol`]'s own `&mut P` implementation, and the same idiom
466/// in `std` (`impl Read for &mut R`, `impl Iterator for &mut I`).
467///
468/// This is only expressible because [`Interceptor`] does not require `'static`: `&'a mut P`
469/// outlives only `'a`. See [`Registry::boxed`], which carries that bound locally instead.
470impl<P: Interceptor + ?Sized> Interceptor for &mut P {
471 fn bind_local_stream(&mut self, info: &StreamInfo) {
472 (**self).bind_local_stream(info)
473 }
474
475 fn unbind_local_stream(&mut self, info: &StreamInfo) {
476 (**self).unbind_local_stream(info)
477 }
478
479 fn bind_remote_stream(&mut self, info: &StreamInfo) {
480 (**self).bind_remote_stream(info)
481 }
482
483 fn unbind_remote_stream(&mut self, info: &StreamInfo) {
484 (**self).unbind_remote_stream(info)
485 }
486}
487
488#[cfg(test)]
489mod derive_test {
490 use super::*;
491 #[allow(unused_imports)]
492 use shared::error::Error;
493
494 /// Test interceptor that uses the derive macro.
495 /// It should automatically delegate all Protocol and Interceptor methods to inner.
496 #[derive(Interceptor)]
497 pub struct SimplePassthrough<P: Interceptor> {
498 #[next]
499 inner: P,
500 }
501
502 // Empty impl block - #[interceptor] generates all delegations
503 #[interceptor]
504 impl<P: Interceptor> SimplePassthrough<P> {}
505
506 impl<P: Interceptor> SimplePassthrough<P> {
507 fn new(inner: P) -> Self {
508 Self { inner }
509 }
510 }
511
512 #[test]
513 fn test_derive_interceptor_basic() {
514 // Build a chain with the derived interceptor
515 let mut chain = SimplePassthrough::new(NoopInterceptor::new());
516
517 // Test that delegation works
518 let pkt = TaggedPacket {
519 now: std::time::Instant::now(),
520 transport: Default::default(),
521 message: Packet::Rtp(rtp::Packet::default()),
522 };
523
524 // handle_write should delegate to inner
525 sansio::Protocol::handle_write(&mut chain, pkt).unwrap();
526
527 // poll_write should return the packet from inner
528 let result = sansio::Protocol::poll_write(&mut chain);
529 assert!(result.is_some());
530 }
531
532 #[test]
533 fn test_derive_interceptor_close() {
534 let mut chain = SimplePassthrough::new(NoopInterceptor::new());
535
536 // close should delegate to inner without error
537 sansio::Protocol::close(&mut chain).unwrap();
538 }
539
540 #[test]
541 fn test_derive_interceptor_stream_binding() {
542 let mut chain = SimplePassthrough::new(NoopInterceptor::new());
543
544 let info = StreamInfo {
545 ssrc: 12345,
546 ..Default::default()
547 };
548
549 // These should delegate to inner without panic
550 chain.bind_local_stream(&info);
551 chain.unbind_local_stream(&info);
552 chain.bind_remote_stream(&info);
553 chain.unbind_remote_stream(&info);
554 }
555
556 /// Consumes an interceptor by value, as the `Registry`/`with` builders do.
557 fn takes_by_value<I: Interceptor>(mut interceptor: I, info: &StreamInfo) {
558 interceptor.bind_local_stream(info);
559 interceptor.unbind_local_stream(info);
560 }
561
562 #[test]
563 fn test_borrowed_chain_satisfies_interceptor_bound() {
564 let mut chain = SimplePassthrough::new(NoopInterceptor::new());
565 let info = StreamInfo {
566 ssrc: 12345,
567 ..Default::default()
568 };
569
570 // `&mut chain` satisfies a by-value `I: Interceptor` bound thanks to the blanket impl.
571 takes_by_value(&mut chain, &info);
572
573 // Ownership stayed with us, so the chain is still usable afterwards.
574 takes_by_value(&mut chain, &info);
575 chain.bind_remote_stream(&info);
576
577 // The borrow also still drives the Protocol side.
578 let pkt = TaggedPacket {
579 now: std::time::Instant::now(),
580 transport: Default::default(),
581 message: Packet::Rtp(rtp::Packet::default()),
582 };
583 sansio::Protocol::handle_write(&mut chain, pkt).unwrap();
584 assert!(sansio::Protocol::poll_write(&mut chain).is_some());
585 }
586
587 #[test]
588 fn test_boxed_chain_still_satisfies_interceptor_bound() {
589 // The `Box<P>` impl coexists with the new `&mut P` impl.
590 let chain: BoxedInterceptor = Box::new(SimplePassthrough::new(NoopInterceptor::new()));
591 let info = StreamInfo {
592 ssrc: 999,
593 ..Default::default()
594 };
595 takes_by_value(chain, &info);
596 }
597}