RTC Interceptor - Sans-IO interceptor framework for RTP/RTCP processing.
This crate provides a composable interceptor framework built on top of the
[sansio::Protocol] trait. Interceptors can process, modify, or generate
RTP/RTCP packets as they flow through the pipeline.
Available Interceptors
RTCP Reports
| Interceptor | Description |
|---|---|
[SenderReportInterceptor] |
Generates RTCP Sender Reports (SR) for local streams and filters hop-by-hop RTCP feedback |
[ReceiverReportInterceptor] |
Generates RTCP Receiver Reports (RR) based on incoming RTP statistics |
NACK (Negative Acknowledgement)
| Interceptor | Description |
|---|---|
[NackGeneratorInterceptor] |
Detects missing RTP packets and generates NACK requests (RFC 4585) |
[NackResponderInterceptor] |
Buffers sent packets and retransmits on NACK, with optional RTX support (RFC 4588) |
TWCC (Transport Wide Congestion Control)
| Interceptor | Description |
|---|---|
[TwccSenderInterceptor] |
Adds transport-wide sequence numbers to outgoing RTP packets |
[TwccReceiverInterceptor] |
Tracks incoming packets and generates TransportLayerCC feedback |
Utility
| Interceptor | Description |
|---|---|
[NoopInterceptor] |
Pass-through terminal for interceptor chains |
Design
Each interceptor wraps an inner Interceptor and can:
- Process incoming/outgoing RTP/RTCP packets
- Modify packet contents (headers, payloads)
- Generate new packets (e.g., RTCP Sender/Receiver Reports)
- Handle timeouts for periodic tasks (e.g., report generation)
- Track stream statistics and state
All interceptors work with [TaggedPacket] (RTP or RTCP packets with transport metadata).
The innermost interceptor is typically [NoopInterceptor], which serves as the terminal.
No Direction Concept
Important: Unlike PeerConnection's pipeline where read and write have
opposite processing direction orders, interceptors have no direction concept.
In PeerConnection's pipeline:
Read: Network → HandlerA → HandlerB → HandlerC → Application
Write: Application → HandlerC → HandlerB → HandlerA → Network
(reversed order)
In Interceptor chains, all operations flow in the same direction:
handle_read: Outer → Inner (A.handle_read calls B.handle_read calls C.handle_read)
handle_write: Outer → Inner (A.handle_write calls B.handle_write calls C.handle_write)
handle_event: Outer → Inner (A.handle_event calls B.handle_event calls C.handle_event)
handle_timeout: Outer → Inner (A.handle_timeout calls B.handle_timeout calls C.handle_timeout)
poll_read: Outer → Inner (A.poll_read calls B.poll_read calls C.poll_read)
poll_write: Outer → Inner (A.poll_write calls B.poll_write calls C.poll_write)
poll_event: Outer → Inner (A.poll_event calls B.poll_event calls C.poll_event)
poll_timeout: Outer → Inner (A.poll_timeout calls B.poll_timeout calls C.poll_timeout)
This means interceptors are symmetric - they process read, write, and event
in the same structural order. The distinction between "inbound" and "outbound"
is semantic (based on message content), not structural (based on call order).
Quick Start
use ;
use Duration;
// Build a full-featured interceptor chain
let chain = new
// RTCP reports
.with
.with
// NACK for packet loss recovery
.with
.with
// TWCC for congestion control
.with
.with
.build;
Type-Erasing a Chain
A chain's type spells out its whole composition
(TwccReceiverInterceptor<SenderReportInterceptor<…>>), and it propagates into every type
that holds the peer connection built from it. That is fine when the chain is fixed at compile
time, and a problem when it is chosen at runtime or has to live in your own structs.
[Interceptor] is object safe, so [Registry::boxed] can erase the chain to
[BoxedInterceptor] — one concrete type, whatever it was built from:
use ;
// Two different chain types, unified by `.boxed()`.
let chain: BoxedInterceptor = if cfg! else ;
The cost is one virtual call per chain entry point (handle_read, poll_write,
handle_timeout, …); the layers inside still call each other through static dispatch and
inline as before. Box<P> and &mut P both implement [Interceptor], so a boxed or borrowed
chain satisfies an I: Interceptor bound like any other.
Stream Binding
Before interceptors can process packets for a stream, the stream must be bound:
use ;
let mut chain = new.build;
// Create stream info with NACK and TWCC support
let stream_info = StreamInfo ;
// Bind for outgoing streams (sender side)
chain.bind_local_stream;
// Bind for incoming streams (receiver side)
chain.bind_remote_stream;
Creating Custom Interceptors
Use the derive macros to easily create custom interceptors:
use ;
use Protocol;
use Error; // the generated `Protocol` impl names it
use VecDeque;
#[derive(Interceptor)]- Marks a struct as an interceptor, requires#[next]field#[interceptor]- GeneratesProtocolandInterceptortrait implementations#[overrides]- Marks methods with custom implementations (non-marked methods delegate to next)
See the [Interceptor] trait documentation for more details.