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