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//! ## Congestion control
31//!
32//! | Interceptor | Description |
33//! |-------------|-------------|
34//! | [`PacerInterceptor`] | Releases outgoing packets at a target rate rather than in bursts |
35//! | [`Rfc8888Interceptor`] | Reports per-packet arrival times back to the sender (RFC 8888) |
36//!
37//! ## Utility
38//!
39//! | Interceptor | Description |
40//! |-------------|-------------|
41//! | [`NoopInterceptor`] | Ends the inbound RTCP path; the last interceptor in a chain |
42//!
43//! # Design
44//!
45//! A chain is a flat list of interceptors driven over a shared belt. Each one can:
46//! - transform a packet passing through, or swallow it to drop or delay it
47//! - emit packets it generated or was holding, which rejoin the belt and carry on
48//! - act on timeouts, for periodic work like report generation
49//! - track stream statistics and state
50//!
51//! All interceptors work with [`TaggedPacket`] — an RTP or RTCP packet with transport metadata,
52//! carrying [`Attribute`]s that say what happened to it on the way. No interceptor holds a
53//! reference to another; [`Registry`] assembles the list and walks it. [`Registry::build`] appends
54//! [`NoopInterceptor`] last, so inbound RTCP stops before the application — control traffic the
55//! interceptors act on is not media the caller asked for.
56//!
57//! # Direction
58//!
59//! A chain is a flat list ordered by **distance from the wire**: the first interceptor is closest to the
60//! network, the last closest to the application. Direction is a property of the walk, not of the
61//! structure:
62//!
63//! ```text
64//! read   (network → application)   forward:  first → … → last
65//! write  (application → network)   reverse:  last  → … → first
66//! ```
67//!
68//! Each interceptor is fed from a shared belt and its output is collected back onto it, so **what a
69//! interceptor emits is seen by every interceptor still ahead of it in the walk**. A retransmission emitted
70//! mid-chain still gets paced, numbered and recorded, because there is no way out of the chain
71//! except through the interceptors that follow.
72//!
73//! One list serves both directions, so "closest to the wire" means one thing rather than opposite
74//! things per direction — which is why the send history and the FEC decoder sit next to each
75//! other, one being the last thing on the way out and the other the first on the way in.
76//!
77//! # Quick Start
78//!
79//! ```
80//! use rtc_interceptor::{
81//!     NackGeneratorBuilder, NackResponderBuilder, ReceiverReportBuilder,
82//!     Registry, SenderReportBuilder, TwccReceiverBuilder, TwccSenderBuilder,
83//! };
84//! use std::time::Duration;
85//!
86//! // Listed wire-to-application, which is the order they run in on the read path and the
87//! // reverse of the order they run in on the write path.
88//! let chain = Registry::new()
89//!     .with(TwccSenderBuilder::new().build())
90//!     .with(NackResponderBuilder::new().build())
91//!     .with(NackGeneratorBuilder::new().build())
92//!     .with(TwccReceiverBuilder::new().build())
93//!     .with(ReceiverReportBuilder::new().build())
94//!     .with(SenderReportBuilder::new().with_interval(Duration::from_secs(1)).build())
95//!     .build();
96//!
97//! // `build` appends [`NoopInterceptor`] last, so inbound RTCP — control traffic the interceptors
98//! // above act on — stops there rather than arriving mixed in with the application's media.
99//! # let _ = chain;
100//! ```
101//!
102//! # One chain type
103//!
104//! [`Registry::build`] returns a single concrete type whatever it was built from, so a struct can
105//! hold one without a type parameter and two connections with different chains share a collection:
106//!
107//! ```
108//! use rtc_interceptor::{NackGeneratorBuilder, Registry, SenderReportBuilder};
109//!
110//! # let nack_enabled = true; // e.g. from configuration, negotiated SDP, …
111//! let chain = if nack_enabled {
112//!     Registry::new().with(NackGeneratorBuilder::new().build()).build()
113//! } else {
114//!     Registry::new().with(SenderReportBuilder::new().build()).build()
115//! };
116//! ```
117//!
118//! The cost is one virtual call per interceptor per packet, which is nothing beside SRTP.
119//!
120//! # Stream Binding
121//!
122//! Before interceptors can process packets for a stream, the stream must be bound:
123//!
124//! ```
125//! use rtc_interceptor::{Interceptor, RTCPFeedback, RTPHeaderExtension, Registry, StreamInfo};
126//!
127//! let mut chain = Registry::new().build();
128//!
129//! // Create stream info with NACK and TWCC support
130//! let stream_info = StreamInfo {
131//!     ssrc: 0x12345678,
132//!     clock_rate: 90000,
133//!     mime_type: "video/VP8".to_string(),
134//!     payload_type: 96,
135//!     rtcp_feedback: vec![RTCPFeedback {
136//!         typ: "nack".to_string(),
137//!         parameter: String::new(),
138//!     }],
139//!     rtp_header_extensions: vec![RTPHeaderExtension {
140//!         uri: "http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01".to_string(),
141//!         id: 5,
142//!     }],
143//!     ..Default::default()
144//! };
145//!
146//! // Bind for outgoing streams (sender side)
147//! chain.bind_local_stream(&stream_info);
148//!
149//! // Bind for incoming streams (receiver side)
150//! chain.bind_remote_stream(&stream_info);
151//! ```
152//!
153//! # Writing your own
154//!
155//! Implement [`sansio::Protocol`] and [`Interceptor`], then add it wherever it belongs in the
156//! list. What `handle_*` takes in, `poll_*` gives back — so even a pass-through needs a queue,
157//! because the queue is what the next interceptor is fed from:
158//!
159//! ```
160//! use rtc_interceptor::{Interceptor, Registry, StreamInfo, TaggedPacket};
161//! use sansio::Protocol;
162//! use std::collections::VecDeque;
163//! use std::time::Instant;
164//!
165//! /// Counts packets on their way out.
166//! #[derive(Default)]
167//! struct Counter {
168//!     sent: u64,
169//!     read_queue: VecDeque<TaggedPacket>,
170//!     write_queue: VecDeque<TaggedPacket>,
171//! }
172//!
173//! impl Protocol<TaggedPacket, TaggedPacket, ()> for Counter {
174//!     type Rout = TaggedPacket;
175//!     type Wout = TaggedPacket;
176//!     type Eout = ();
177//!     type Error = shared::error::Error;
178//!     type Time = Instant;
179//!
180//!     fn handle_read(&mut self, msg: TaggedPacket) -> Result<(), Self::Error> {
181//!         self.read_queue.push_back(msg);
182//!         Ok(())
183//!     }
184//!
185//!     fn poll_read(&mut self) -> Option<Self::Rout> {
186//!         self.read_queue.pop_front()
187//!     }
188//!
189//!     fn handle_write(&mut self, msg: TaggedPacket) -> Result<(), Self::Error> {
190//!         self.sent += 1;
191//!         self.write_queue.push_back(msg); // queueing nothing would swallow it
192//!         Ok(())
193//!     }
194//!
195//!     fn poll_write(&mut self) -> Option<Self::Wout> {
196//!         self.write_queue.pop_front()
197//!     }
198//! }
199//!
200//! impl Interceptor for Counter {
201//!     fn bind_local_stream(&mut self, _info: &StreamInfo) {}
202//!     fn unbind_local_stream(&mut self, _info: &StreamInfo) {}
203//!     fn bind_remote_stream(&mut self, _info: &StreamInfo) {}
204//!     fn unbind_remote_stream(&mut self, _info: &StreamInfo) {}
205//! }
206//!
207//! let chain = Registry::new().with(Counter::default()).build();
208//! # let _ = chain;
209//! ```
210//!
211//! Queue nothing to drop or delay a packet, and queue delayed or generated ones whenever they are
212//! ready — from [`handle_timeout`](sansio::Protocol::handle_timeout), say. They leave through
213//! `poll_*` and continue through every interceptor ahead.
214
215#![warn(rust_2018_idioms)]
216#![warn(missing_docs)]
217#![allow(dead_code)]
218
219use std::time::Instant;
220
221pub(crate) mod chain;
222pub(crate) mod noop;
223pub(crate) mod registry;
224
225pub(crate) mod flexfec;
226pub(crate) mod intervalpli;
227pub(crate) mod jitterbuffer;
228pub(crate) mod nack;
229pub(crate) mod pacing;
230pub(crate) mod packet;
231pub(crate) mod report;
232pub(crate) mod rfc8888;
233pub(crate) mod rtpfb;
234pub(crate) mod stream_info;
235pub(crate) mod twcc;
236
237pub use flexfec::bit_array::BitArray;
238pub use flexfec::coverage::{MAX_FEC_PACKETS, MAX_MEDIA_PACKETS, ProtectionCoverage};
239pub use flexfec::draft03::decoder::{FlexFec03Decoder, ParseError as FlexFecParseError};
240pub use flexfec::draft03::encoder::FlexFec03Encoder;
241pub use flexfec::draft03::receiver::{FlexFec03ReceiveBuilder, FlexFec03ReceiveInterceptor};
242pub use flexfec::draft03::sender::{
243    DEFAULT_NUM_FEC_PACKETS, DEFAULT_NUM_MEDIA_PACKETS, FlexFec03SendBuilder,
244    FlexFec03SendInterceptor,
245};
246pub use intervalpli::generator::{
247    DEFAULT_INTERVAL as INTERVAL_PLI_DEFAULT_INTERVAL, IntervalPliInterceptor,
248};
249pub use jitterbuffer::buffer::{
250    JitterBuffer, JitterBufferStats, Rejected, State as JitterBufferState,
251};
252pub use jitterbuffer::receiver::{
253    DEFAULT_CAPACITY as JITTER_BUFFER_DEFAULT_CAPACITY,
254    DEFAULT_DEPTH as JITTER_BUFFER_DEFAULT_DEPTH, JitterBufferBuilder, JitterBufferInterceptor,
255};
256pub use nack::generator::{NackGeneratorBuilder, NackGeneratorInterceptor};
257pub use nack::responder::{NackResponderBuilder, NackResponderInterceptor};
258pub use noop::NoopInterceptor;
259pub use pacing::pacer::{MIN_BURST_BITS as PACER_MIN_BURST_BITS, Pacer};
260pub use pacing::sender::{
261    DEFAULT_BITRATE as PACER_DEFAULT_BITRATE, DEFAULT_QUEUE_LIMIT as PACER_DEFAULT_QUEUE_LIMIT,
262    PacerBuilder, PacerInterceptor,
263};
264pub use packet::{Attribute, AttributedPacket, Packet, TaggedPacket};
265pub use registry::Registry;
266pub use report::receiver::{ReceiverReportBuilder, ReceiverReportInterceptor};
267pub use report::sender::{SenderReportBuilder, SenderReportInterceptor};
268pub use rfc8888::recorder::CcFeedbackRecorder;
269pub use rfc8888::sender::{
270    DEFAULT_INTERVAL as RFC8888_DEFAULT_INTERVAL,
271    DEFAULT_MAX_REPORT_SIZE as RFC8888_DEFAULT_MAX_REPORT_SIZE, Rfc8888Builder, Rfc8888Interceptor,
272};
273pub use rtpfb::acknowledgement::{Acknowledgement, PacketReport, Report};
274pub use rtpfb::convert::{convert_ccfb, convert_twcc};
275pub use rtpfb::history::History;
276pub use stream_info::{RTCPFeedback, RTPHeaderExtension, StreamInfo};
277pub use twcc::receiver::{TwccReceiverBuilder, TwccReceiverInterceptor};
278pub use twcc::sender::{TwccSenderBuilder, TwccSenderInterceptor};
279
280/// One interceptor of packet processing.
281///
282/// An interceptor is a [`sansio::Protocol`] like everything else in this stack: packets arrive
283/// through `handle_read`/`handle_write` and leave through `poll_read`/`poll_write`. What is
284/// different is that nothing is wired to anything — an interceptor does not know what is on either
285/// side of it. [`Registry`] builds a flat list and the chain it returns moves packets along it.
286///
287/// # The contract
288///
289/// **What `handle_*` takes in, `poll_*` gives back.** The chain hands you a packet, then asks what
290/// you have ready; whatever you return is what the next interceptor receives. So an interceptor
291/// that passes packets through still needs a queue — take the packet in `handle_read`, hand it
292/// back from `poll_read`.
293///
294/// | To | Do |
295/// |---|---|
296/// | pass a packet through | queue it in `handle_*`, return it from `poll_*` |
297/// | transform it | queue the modified packet |
298/// | drop or delay it | queue nothing; a delayed one is queued later, from `handle_timeout` |
299/// | generate one | queue it whenever you like; it joins the walk from `poll_*` |
300/// | act on a timer | `handle_timeout`, and report the deadline from `poll_timeout` |
301///
302/// # What you emit continues
303///
304/// A packet returned from `poll_write` is handed to the next interceptor in the walk and passes
305/// through every one still ahead of it. Nothing can bypass an interceptor by being generated past
306/// it — which is the class of bug the previous, nested design allowed, and why a retransmission
307/// used to escape the pacer, the transport-wide numbering and the send history.
308///
309/// The same is true in reverse: it also means **nothing reaches the wire or the application except
310/// by passing through the interceptors that follow it**. An interceptor that keeps a packet to
311/// itself keeps it from everything downstream, deliberately.
312///
313/// # Direction
314///
315/// Read walks the list forwards, write walks it in reverse, so one ordering serves both: the first
316/// interceptor is closest to the network in both directions.
317///
318pub trait Interceptor:
319    sansio::Protocol<
320        TaggedPacket,
321        TaggedPacket,
322        (),
323        Rout = TaggedPacket,
324        Wout = TaggedPacket,
325        Eout = (),
326        Time = Instant,
327        Error = shared::error::Error,
328    > + Send
329    + Sync
330{
331    /// bind_local_stream lets you modify any outgoing RTP packets. It is called once for per LocalStream. The returned method
332    /// will be called once per rtp packet.
333    fn bind_local_stream(&mut self, info: &StreamInfo);
334
335    /// unbind_local_stream is called when the Stream is removed. It can be used to clean up any data related to that track.
336    fn unbind_local_stream(&mut self, info: &StreamInfo);
337
338    /// bind_remote_stream lets you modify any incoming RTP packets. It is called once for per RemoteStream. The returned method
339    /// will be called once per rtp packet.
340    fn bind_remote_stream(&mut self, info: &StreamInfo);
341
342    /// unbind_remote_stream is called when the Stream is removed. It can be used to clean up any data related to that track.
343    fn unbind_remote_stream(&mut self, info: &StreamInfo);
344}
345
346/// An interceptor whose concrete type has been erased.
347///
348/// `Interceptor` is object safe, which is what lets a chain be a flat list of these rather than a
349/// tower of nested types. Name it when an application chooses an interceptor at runtime and hands
350/// the result to [`Registry::with_boxed`].
351pub type BoxedInterceptor = Box<dyn Interceptor>;
352
353impl<P: Interceptor + ?Sized> Interceptor for Box<P> {
354    fn bind_local_stream(&mut self, info: &StreamInfo) {
355        (**self).bind_local_stream(info)
356    }
357
358    fn unbind_local_stream(&mut self, info: &StreamInfo) {
359        (**self).unbind_local_stream(info)
360    }
361
362    fn bind_remote_stream(&mut self, info: &StreamInfo) {
363        (**self).bind_remote_stream(info)
364    }
365
366    fn unbind_remote_stream(&mut self, info: &StreamInfo) {
367        (**self).unbind_remote_stream(info)
368    }
369}
370
371/// Blanket implementation for mutable references.
372///
373/// This lets a borrowed chain satisfy an `Interceptor` bound, so a function taking
374/// `I: Interceptor` by value can be called with `&mut chain` and leave ownership with the
375/// caller. It mirrors [`sansio::Protocol`]'s own `&mut P` implementation, and the same idiom
376/// in `std` (`impl Read for &mut R`, `impl Iterator for &mut I`).
377///
378/// This is only expressible because [`Interceptor`] does not require `'static`: `&'a mut P`
379/// outlives only `'a`. See [`Registry::boxed`], which carries that bound locally instead.
380impl<P: Interceptor + ?Sized> Interceptor for &mut P {
381    fn bind_local_stream(&mut self, info: &StreamInfo) {
382        (**self).bind_local_stream(info)
383    }
384
385    fn unbind_local_stream(&mut self, info: &StreamInfo) {
386        (**self).unbind_local_stream(info)
387    }
388
389    fn bind_remote_stream(&mut self, info: &StreamInfo) {
390        (**self).bind_remote_stream(info)
391    }
392
393    fn unbind_remote_stream(&mut self, info: &StreamInfo) {
394        (**self).unbind_remote_stream(info)
395    }
396}