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) = ®istry.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}