Skip to main content

rtc_interceptor/
registry.rs

1//! Building a chain.
2
3use crate::chain::Chain;
4use crate::noop::NoopInterceptor;
5use crate::{BoxedInterceptor, Interceptor};
6use log::warn;
7use std::collections::BTreeMap;
8
9/// Where an interceptor belongs in the chain, measured by **distance from the wire**.
10///
11/// This is the chain contract's ordering table expressed as data, so that one place decides it and
12/// a test can check a builder against it. The doc comments carry that table's indices; the gaps are
13/// slots nothing fills yet.
14///
15/// Read walks the list forwards and write walks it in reverse, so a smaller slot is closer to the
16/// network in both directions.
17#[derive(Copy, Clone, Debug)]
18#[non_exhaustive]
19#[repr(usize)]
20pub enum Slot {
21    /// Congestion control: send history and feedback ingest.
22    ///
23    /// The only position that sees every byte that leaves, because nothing exits the chain except
24    /// through the interceptors ahead of it. Named here so an estimator of your own has a
25    /// landmark.
26    CongestionControl = 1_000,
27    /// `TwccSenderInterceptor` — assigns the transport-wide sequence number the send history keys on.
28    TwccSender = 2_000,
29    /// `PacerInterceptor` — gates departures. Everything that generates a packet sits above it, so
30    /// retransmissions, FEC repair and generated RTCP are all metered.
31    Pacer = 3_000,
32    /// `NackResponderInterceptor` — buffers sent RTP; its retransmissions still reach 3_000, 2_000, 1_000.
33    NackResponder = 4_000,
34    /// `FlexFec03SendInterceptor` — its repair packets still reach everything below.
35    FecEncoder = 5_000,
36    /// `FlexFec03ReceiveInterceptor` — recovery before anything inspects sequence numbers.
37    FecDecoder = 6_000,
38    /// `NackGeneratorInterceptor` — loss detected from arrivals, not from released packets.
39    NackGenerator = 7_000,
40    /// `TwccReceiverInterceptor` — records arrivals and reports them to the remote sender's
41    /// congestion controller. An arrival recorder, so it precedes the jitter buffer.
42    TwccReceiver = 8_000,
43    /// `Rfc8888Interceptor` — the same job as [`Slot::TwccReceiver`] in a different format, and
44    /// registering both double-counts, so a chain carries one or the other.
45    Rfc8888 = 9_000,
46    /// `ReceiverReportInterceptor` — RFC 3550 reception quality, not congestion-control feedback.
47    /// Still an arrival recorder, so it precedes the jitter buffer.
48    ReceiverReport = 10_000,
49    /// `SenderReportInterceptor` — a generator with no read-side ordering constraint.
50    SenderReport = 11_000,
51    /// `IntervalPliInterceptor` — a generator with no read-side ordering constraint.
52    IntervalPli = 12_000,
53    /// `JitterBufferInterceptor` — delays and re-stamps, so every arrival recorder precedes it.
54    ///
55    /// A recorder below this would report local playout instants to the remote as arrival times,
56    /// and the remote's congestion controller would read this endpoint's buffering depth as network
57    /// delay variation.
58    JitterBuffer = 13_000,
59    /// Anywhere else, for an interceptor this crate knows nothing about.
60    ///
61    /// The named slots are spaced a thousand apart so that one of your own fits between any two of
62    /// them without renumbering anything: `Slot::from(6_500)` sits after the FEC decoder and before
63    /// the NACK generator. Reach it through [`From<usize>`](#impl-From<usize>-for-Slot) rather than
64    /// by naming the variant, so the spelling stays the same if this gains a richer representation.
65    Custom(usize),
66}
67
68impl Slot {
69    /// Where this sits, as a distance from the wire.
70    ///
71    /// The named slots are the thousands; a custom one is whatever it was built from.
72    pub const fn slot(self) -> usize {
73        match self {
74            Self::CongestionControl => 1_000,
75            Self::TwccSender => 2_000,
76            Self::Pacer => 3_000,
77            Self::NackResponder => 4_000,
78            Self::FecEncoder => 5_000,
79            Self::FecDecoder => 6_000,
80            Self::NackGenerator => 7_000,
81            Self::TwccReceiver => 8_000,
82            Self::Rfc8888 => 9_000,
83            Self::ReceiverReport => 10_000,
84            Self::SenderReport => 11_000,
85            Self::IntervalPli => 12_000,
86            Self::JitterBuffer => 13_000,
87            Self::Custom(position) => position,
88        }
89    }
90}
91
92/// A position of your own. See [`Slot::Custom`].
93impl From<usize> for Slot {
94    fn from(position: usize) -> Self {
95        Self::Custom(position)
96    }
97}
98
99impl From<Slot> for usize {
100    fn from(slot: Slot) -> Self {
101        slot.slot()
102    }
103}
104
105// Equality and ordering are both by position, and they are written out rather than derived because
106// deriving them would disagree with each other. A derived `PartialEq` compares variants, so
107// `Slot::from(2_000) != Slot::TwccSender` even though both name the same distance from the wire; a
108// derived `Ord` compares *declaration* order, so `Slot::Custom(1_500)` would sort after
109// `JitterBuffer` rather than between `CongestionControl` and `TwccSender` — which is the whole
110// point of allowing a custom one. Two values that compare `Equal` must also be `==`, and a sort by
111// slot must put a custom position where its number says, so both come from [`Slot::slot`].
112impl PartialEq for Slot {
113    fn eq(&self, other: &Self) -> bool {
114        self.slot() == other.slot()
115    }
116}
117
118impl Eq for Slot {}
119
120impl PartialOrd for Slot {
121    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
122        Some(self.cmp(other))
123    }
124}
125
126impl Ord for Slot {
127    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
128        self.slot().cmp(&other.slot())
129    }
130}
131
132impl std::hash::Hash for Slot {
133    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
134        self.slot().hash(state);
135    }
136}
137
138/// Collects interceptors and assembles them into a [`Chain`].
139///
140/// # Order
141///
142/// Every interceptor is added at a [`Slot`], and [`build`](Self::build) sorts by it. A slot is a
143/// **distance from the wire**: the smallest is closest to the network, the largest closest to the
144/// application. Read walks that order, write walks it in reverse, so one list serves both
145/// directions and "closest to the wire" means one thing rather than opposite things per direction.
146///
147/// ```text
148/// Registry::new()
149///     .with(Slot::TwccSender, a)     // 2_000: closest to the wire
150///     .with(Slot::NackGenerator, b)  // 7_000
151///     .with(Slot::JitterBuffer, c)   // 13_000: closest to the application
152///     .build()
153///
154/// read:   a → b → c → application
155/// write:  application → c → b → a → wire
156/// ```
157///
158/// Declaring the position rather than relying on call order is what makes the helpers in
159/// `rtc` composable: `configure_twcc` places interceptors at 2_000 and 8_000, `configure_nack` at
160/// 4_000 and 7_000, and the two interleave correctly however the caller sequences them. With order taken
161/// from insertion, calling them in either sequence produced a chain that was wrong in a different
162/// way each time, and nothing caught it — the nested registry that preceded this added *innermost*
163/// first, so `register_default_interceptors` assembled TWCC receiver → RTCP reports → NACK
164/// generator, the reverse of what the chain contract documented.
165///
166/// A slot holds one interceptor. Two of your own go at two custom positions — the named slots are
167/// spaced a thousand apart so there is room between any two of them.
168///
169/// # Example
170///
171/// ```
172/// use rtc_interceptor::{NackGeneratorBuilder, Registry, Slot, TwccSenderBuilder};
173///
174/// let chain = Registry::new()
175///     .with(Slot::TwccSender, TwccSenderBuilder::new().build())        // closest to the wire
176///     .with(Slot::NackGenerator, NackGeneratorBuilder::new().build())  // sees arrivals after it
177///     .build();                                                        // terminus appended here
178/// # let _ = chain;
179/// ```
180#[derive(Default)]
181pub struct Registry {
182    interceptors: BTreeMap<Slot, BoxedInterceptor>,
183    /// What each interceptor is called, keyed the same way as `interceptors`.
184    ///
185    /// Kept beside the chain rather than asked of it: `Interceptor` is a trait object by the time
186    /// it is stored, and a trait object cannot say what it used to be. Recording the name at the
187    /// one moment the concrete type is still in hand is the only way to have it later, which is
188    /// also why [`with`](Self::with) takes a concrete interceptor rather than a boxed one.
189    names: BTreeMap<Slot, String>,
190}
191
192/// A type's name without its module paths — `TwccSenderInterceptor`, not
193/// `rtc_interceptor::twcc::sender::TwccSenderInterceptor`.
194///
195/// Every path is shortened, not just the outermost one, so a congestion controller reads as
196/// `CongestionControlInterceptor<Gcc>`. Splitting the whole string on its last `::` would be
197/// simpler and wrong: on a generic type that separator sits inside the *argument*, and
198/// `CongestionControlInterceptor<rtc_interceptor::cc::estimator::ConstantBitrate>` comes back as
199/// `ConstantBitrate>` — the interceptor's own name gone, and a stray bracket left behind.
200///
201/// The generic argument is kept because it is often the only thing telling two interceptors apart:
202/// which estimator a congestion controller carries is the interesting half of its name.
203fn short_type_name<T: ?Sized>() -> String {
204    let full = std::any::type_name::<T>();
205    let mut out = String::with_capacity(full.len());
206    let mut segment = String::new();
207
208    let flush = |segment: &mut String, out: &mut String| {
209        out.push_str(segment.rsplit("::").next().unwrap_or(segment));
210        segment.clear();
211    };
212
213    for ch in full.chars() {
214        // A path segment runs until punctuation that cannot appear in one: `<`, `>`, `,`, a space.
215        if ch.is_alphanumeric() || ch == '_' || ch == ':' {
216            segment.push(ch);
217        } else {
218            flush(&mut segment, &mut out);
219            out.push(ch);
220        }
221    }
222    flush(&mut segment, &mut out);
223
224    out
225}
226
227impl Registry {
228    /// An empty registry.
229    pub fn new() -> Self {
230        Self::default()
231    }
232
233    /// Add an interceptor at `slot`.
234    ///
235    /// Call order does not matter: the slot decides the position. A slot holds one interceptor, so
236    /// adding a second at the same position replaces the first and says so in the log.
237    pub fn with<T: Interceptor + 'static>(mut self, slot: Slot, interceptor: T) -> Self {
238        let name = short_type_name::<T>();
239
240        // One interceptor per slot: the map key is the position. Replacing rather than stacking is
241        // what a map gives, and it is announced rather than done quietly — an interceptor that
242        // vanished because something else claimed its slot is the kind of fault that shows up much
243        // later as "the chain does not do what I configured".
244        if let Some(displaced) = self.names.insert(slot, name.clone()) {
245            warn!("{slot:?} already held {displaced}; {name} replaced it");
246        }
247        self.interceptors.insert(slot, Box::new(interceptor));
248        self
249    }
250
251    /// What this registry holds, wire-to-application: each interceptor's slot and its type name,
252    /// in the order [`build`](Self::build) will compose them.
253    ///
254    /// Present so a caller assembling a chain from several helpers can assert what it got. Each
255    /// helper places interceptors at its own landmarks and none of them sees the whole, so the
256    /// composition is precisely the thing no single helper can check.
257    pub fn slots(&self) -> Vec<(Slot, String)> {
258        // Already wire-to-application: a `BTreeMap` iterates in key order, and `Slot` orders by
259        // position. This is the order `build` will compose them in, for the same reason.
260        self.names
261            .iter()
262            .map(|(slot, name)| (*slot, name.clone()))
263            .collect()
264    }
265
266    /// Assemble the interceptor chain.
267    ///
268    /// [`NoopInterceptor`] is appended last, so every chain ends the inbound RTCP path. That is a
269    /// property of a chain rather than something a caller opts into: left out, an application would
270    /// get a stream of control traffic it never asked for, and the omission would look like working
271    /// code.
272    ///
273    /// What gets past it is decided per packet, by an interceptor attaching
274    /// [`Attribute::DeliverToApplication`](crate::Attribute::DeliverToApplication) to the ones it
275    /// vouches for — the component that knows which packets an application can act on is the one
276    /// that makes the call, rather than a switch here that could only say "all of it or none".
277    pub fn build(self) -> impl Interceptor {
278        // No sort: a `BTreeMap` is already in key order, and `Slot` orders by distance from the
279        // wire, which is the order the chain runs in.
280        let mut interceptors: Vec<BoxedInterceptor> = self.interceptors.into_values().collect();
281
282        interceptors.push(Box::new(NoopInterceptor::new()));
283
284        Chain::new(interceptors)
285    }
286}
287
288#[cfg(test)]
289mod tests {
290    use super::*;
291    use crate::StreamInfo;
292    use crate::{AttributedPacket, Packet, TaggedPacket};
293    use sansio::Protocol;
294    use shared::TransportContext;
295    use shared::error::Error;
296    use std::collections::VecDeque;
297    use std::sync::{Arc, Mutex};
298    use std::time::Instant;
299
300    #[derive(Clone, Default)]
301    struct Log(Arc<Mutex<Vec<&'static str>>>);
302
303    struct Marker {
304        name: &'static str,
305        log: Log,
306        read_queue: VecDeque<TaggedPacket>,
307        write_queue: VecDeque<TaggedPacket>,
308    }
309
310    impl Marker {
311        fn new(name: &'static str, log: Log) -> Self {
312            Self {
313                name,
314                log,
315                read_queue: VecDeque::new(),
316                write_queue: VecDeque::new(),
317            }
318        }
319    }
320
321    impl Protocol<TaggedPacket, TaggedPacket, ()> for Marker {
322        type Rout = TaggedPacket;
323        type Wout = TaggedPacket;
324        type Eout = ();
325        type Error = Error;
326        type Time = Instant;
327
328        fn handle_read(&mut self, msg: TaggedPacket) -> Result<(), Self::Error> {
329            self.log.0.lock().unwrap().push(self.name);
330            self.read_queue.push_back(msg);
331            Ok(())
332        }
333
334        fn poll_read(&mut self) -> Option<Self::Rout> {
335            self.read_queue.pop_front()
336        }
337
338        fn handle_write(&mut self, msg: TaggedPacket) -> Result<(), Self::Error> {
339            self.log.0.lock().unwrap().push(self.name);
340            self.write_queue.push_back(msg);
341            Ok(())
342        }
343
344        fn poll_write(&mut self) -> Option<Self::Wout> {
345            self.write_queue.pop_front()
346        }
347
348        fn handle_timeout(&mut self, _now: Instant) -> Result<(), Self::Error> {
349            Ok(())
350        }
351
352        fn poll_timeout(&mut self) -> Option<Self::Time> {
353            None
354        }
355    }
356
357    impl Interceptor for Marker {
358        fn bind_local_stream(&mut self, _info: &StreamInfo) {}
359        fn unbind_local_stream(&mut self, _info: &StreamInfo) {}
360        fn bind_remote_stream(&mut self, _info: &StreamInfo) {}
361        fn unbind_remote_stream(&mut self, _info: &StreamInfo) {}
362    }
363
364    fn packet() -> TaggedPacket {
365        TaggedPacket {
366            now: Instant::now(),
367            transport: TransportContext::default(),
368            message: AttributedPacket::new(Packet::Rtp(rtp::Packet::default())),
369        }
370    }
371
372    fn chain(log: &Log) -> impl Interceptor {
373        Registry::new()
374            .with(Slot::TwccSender, Marker::new("wire", log.clone()))
375            .with(Slot::NackGenerator, Marker::new("middle", log.clone()))
376            .with(Slot::JitterBuffer, Marker::new("app", log.clone()))
377            .build()
378    }
379
380    /// Slots decide the order, not the sequence of calls. Adding application-most first must
381    /// compose the same chain as adding wire-most first — the property the helpers in `rtc` rely
382    /// on to be callable in any sequence.
383    #[test]
384    fn call_order_does_not_decide_chain_order() {
385        let log = Log::default();
386        let mut chain = Registry::new()
387            .with(Slot::JitterBuffer, Marker::new("app", log.clone()))
388            .with(Slot::TwccSender, Marker::new("wire", log.clone()))
389            .with(Slot::NackGenerator, Marker::new("middle", log.clone()))
390            .build();
391
392        chain.handle_read(packet()).unwrap();
393        while chain.poll_read().is_some() {}
394
395        assert_eq!(vec!["wire", "middle", "app"], *log.0.lock().unwrap());
396    }
397
398    /// A slot holds one interceptor: adding a second at the same position replaces the first
399    /// rather than stacking with it. Two of your own go at two custom positions, which is what the
400    /// thousand-apart spacing leaves room for.
401    #[test]
402    fn a_slot_holds_one_interceptor() {
403        let log = Log::default();
404        let mut chain = Registry::new()
405            .with(Slot::NackGenerator, Marker::new("first", log.clone()))
406            .with(Slot::NackGenerator, Marker::new("second", log.clone()))
407            .build();
408
409        chain.handle_read(packet()).unwrap();
410        while chain.poll_read().is_some() {}
411
412        assert_eq!(
413            vec!["second"],
414            *log.0.lock().unwrap(),
415            "the later one claimed the slot; the earlier one is not in the chain"
416        );
417    }
418
419    /// Read runs the list forwards: the first interceptor added is closest to the wire.
420    #[test]
421    fn read_runs_in_the_order_stages_were_added() {
422        let log = Log::default();
423        let mut chain = chain(&log);
424
425        chain.handle_read(packet()).unwrap();
426        while chain.poll_read().is_some() {}
427
428        assert_eq!(vec!["wire", "middle", "app"], *log.0.lock().unwrap());
429    }
430
431    /// Write runs it backwards, so the same list describes both directions.
432    #[test]
433    fn write_runs_in_reverse() {
434        let log = Log::default();
435        let mut chain = chain(&log);
436
437        chain.handle_write(packet()).unwrap();
438        while chain.poll_write().is_some() {}
439
440        assert_eq!(vec!["app", "middle", "wire"], *log.0.lock().unwrap());
441    }
442
443    /// Ending the inbound RTCP path is a property of every chain, not something a caller adds.
444    #[test]
445    fn a_registry_with_nothing_added_still_has_the_terminus() {
446        let mut chain = Registry::new().build();
447
448        chain
449            .handle_read(TaggedPacket {
450                now: Instant::now(),
451                transport: TransportContext::default(),
452                message: AttributedPacket::new(Packet::Rtcp(vec![])),
453            })
454            .unwrap();
455        assert!(
456            chain.poll_read().is_none(),
457            "inbound RTCP stops before the application"
458        );
459    }
460
461    /// The terminus goes last, so every interceptor sees inbound RTCP before it is dropped.
462    #[test]
463    fn the_terminus_is_application_most() {
464        let log = Log::default();
465        let mut chain = Registry::new()
466            .with(Slot::TwccSender, Marker::new("wire", log.clone()))
467            .build();
468
469        chain
470            .handle_read(TaggedPacket {
471                now: Instant::now(),
472                transport: TransportContext::default(),
473                message: AttributedPacket::new(Packet::Rtcp(vec![])),
474            })
475            .unwrap();
476
477        assert_eq!(
478            vec!["wire"],
479            *log.0.lock().unwrap(),
480            "the stage saw the RTCP packet; the terminus dropped it afterwards"
481        );
482        assert!(chain.poll_read().is_none());
483    }
484
485    /// An application's own interceptor goes between two named slots, which is what the spacing is
486    /// for: nothing has to be renumbered to make room.
487    #[test]
488    fn a_custom_slot_sits_where_its_number_says() {
489        let log = Log::default();
490        let mut chain = Registry::new()
491            .with(Slot::FecDecoder, Marker::new("fec", log.clone()))
492            .with(Slot::NackGenerator, Marker::new("nack", log.clone()))
493            .with(Slot::from(6_500), Marker::new("mine", log.clone()))
494            .build();
495
496        chain.handle_read(packet()).unwrap();
497        while chain.poll_read().is_some() {}
498
499        assert_eq!(
500            vec!["fec", "mine", "nack"],
501            *log.0.lock().unwrap(),
502            "6_500 belongs after the FEC decoder at 6_000 and before the NACK generator at 7_000"
503        );
504    }
505
506    /// A custom slot naming a named slot's position *is* that slot. Equality and ordering both come
507    /// from the position, and they have to agree: a pair that compares `Equal` must also be `==`,
508    /// or a sort or a `BTreeMap` keyed on this behaves differently depending on which spelling the
509    /// caller reached for.
510    #[test]
511    fn equality_and_ordering_both_follow_the_position() {
512        assert_eq!(Slot::TwccSender, Slot::from(2_000));
513        assert_eq!(
514            std::cmp::Ordering::Equal,
515            Slot::TwccSender.cmp(&Slot::from(2_000))
516        );
517        assert!(Slot::from(1_500) > Slot::CongestionControl);
518        assert!(Slot::from(1_500) < Slot::TwccSender);
519        assert!(
520            Slot::from(20_000) > Slot::JitterBuffer,
521            "a position past every named slot sorts past them, not by declaration order"
522        );
523    }
524
525    /// The named slots keep the spacing the doc promises, so `Slot::from` has room to aim at.
526    #[test]
527    fn the_named_slots_are_spaced_by_a_thousand() {
528        let named = [
529            Slot::CongestionControl,
530            Slot::TwccSender,
531            Slot::Pacer,
532            Slot::NackResponder,
533            Slot::FecEncoder,
534            Slot::FecDecoder,
535            Slot::NackGenerator,
536            Slot::TwccReceiver,
537            Slot::Rfc8888,
538            Slot::ReceiverReport,
539            Slot::SenderReport,
540            Slot::IntervalPli,
541            Slot::JitterBuffer,
542        ];
543
544        for pair in named.windows(2) {
545            assert_eq!(
546                1_000,
547                pair[1].slot() - pair[0].slot(),
548                "{:?} and {:?} must stay a thousand apart",
549                pair[0],
550                pair[1]
551            );
552        }
553    }
554
555    /// A registry records what each interceptor is called, which a chain of trait objects could not
556    /// tell you afterwards. It is what makes a composed chain inspectable — several helpers each
557    /// place interceptors at their own landmarks, and this is the only view of the result.
558    #[test]
559    fn slots_carry_the_interceptor_names() {
560        let log = Log::default();
561        let registry = Registry::new()
562            .with(Slot::JitterBuffer, Marker::new("app", log.clone()))
563            .with(Slot::TwccSender, crate::TwccSenderBuilder::new().build());
564
565        assert_eq!(
566            vec![
567                (Slot::TwccSender, "TwccSenderInterceptor".to_owned()),
568                (Slot::JitterBuffer, "Marker".to_owned()),
569            ],
570            registry.slots(),
571            "names come back with their slots, sorted wire-to-application"
572        );
573    }
574
575    /// The module path is dropped: a name is for reading, and the full path is mostly the crate's
576    /// own directory layout.
577    #[test]
578    fn names_are_stripped_of_their_module_path() {
579        let registry = Registry::new().with(
580            Slot::CongestionControl,
581            crate::CongestionControlBuilder::new(crate::ConstantBitrate::new(1_000_000.0)).build(),
582        );
583
584        let (_, name) = &registry.slots()[0];
585        assert!(
586            !name.contains("::"),
587            "a module path leaked into the name: {name}"
588        );
589        assert_eq!(
590            "CongestionControlInterceptor<ConstantBitrate>", name,
591            "the generic argument is shortened too, and kept — it is what tells two \
592             congestion controllers apart"
593        );
594    }
595}