Skip to main content

rtc_interceptor/
noop.rs

1//! Where the inbound RTCP path ends.
2
3use crate::Interceptor;
4use crate::{Attribute, Packet, StreamInfo, TaggedPacket};
5use sansio::Protocol;
6use shared::error::Error;
7use std::collections::VecDeque;
8use std::time::Instant;
9
10/// Decides what becomes of inbound RTCP once the interceptors have had it, and passes everything
11/// else through.
12///
13/// # Why this exists
14///
15/// Inbound RTCP is **for the interceptors**: a receiver report feeds the sender-side statistics, a
16/// NACK is answered by the responder, transport-wide feedback drives the bandwidth estimate.
17/// Handing it onward by default would give an application a stream of control traffic it did not
18/// ask for and cannot act on, mixed in with its media — so by default it stops here.
19///
20/// An application that *does* read RTCP puts an interceptor of its own in the chain and has it
21/// attach [`Attribute::DeliverToApplication`] to the packets it wants. That makes delivery a
22/// per-packet judgement, made by the one component that knows which packets matter: an SFU
23/// forwarding PLIs wants those and not the receiver reports its own chain is already acting on. A
24/// chain-wide switch could only offer "all of it or none of it", and every application that wanted
25/// one kind of packet had to take the lot and filter afterwards.
26///
27/// # Where this belongs in the chain
28///
29/// **Last**, so every interceptor that reads RTCP has already seen it by the time it is dropped.
30/// Anywhere else it would starve the ones beyond it — a NACK responder placed after this would
31/// never see a NACK. [`Registry::build`](crate::Registry::build) appends it for that reason,
32/// rather than leaving the position to each caller.
33///
34/// # Why marking is the mechanism, and not re-emitting a copy
35///
36/// Under the nested chain an application could add an interceptor that kept a copy of each RTCP
37/// packet and returned it from its own `poll_read` ahead of delegating inward. That worked because
38/// a local `poll_read` queue was *terminal*: the copy bypassed everything below and escaped this
39/// one.
40///
41/// On the belt it does not. What an interceptor emits from `poll_read` rejoins the belt **behind
42/// itself**, so it arrives here like any other packet and is dropped like any other packet — the
43/// original *and* the copy, twice over. Since this is always the last interceptor, no position
44/// exists from which to forward past it. Marking the packet works precisely because it does not try
45/// to get around this one: the packet travels the whole chain as normal, and this reads the mark
46/// when it arrives.
47///
48/// # Naming
49///
50/// It was a no-op in the nested chain, where its job was to terminate the recursion and hand
51/// packets back. The belt needs no terminator, so all that is left is the one decision it always
52/// quietly made.
53#[derive(Default)]
54pub struct NoopInterceptor {
55    read_queue: VecDeque<TaggedPacket>,
56    write_queue: VecDeque<TaggedPacket>,
57}
58
59impl NoopInterceptor {
60    /// A terminus
61    pub fn new() -> Self {
62        Self::default()
63    }
64}
65
66impl Protocol<TaggedPacket, TaggedPacket, ()> for NoopInterceptor {
67    type Rout = TaggedPacket;
68    type Wout = TaggedPacket;
69    type Eout = ();
70    type Error = Error;
71    type Time = Instant;
72
73    fn handle_read(&mut self, mut msg: TaggedPacket) -> Result<(), Self::Error> {
74        // RTP is media the application asked for; it always passes.
75        if matches!(msg.message.packet, Packet::Rtp(_)) {
76            self.read_queue.push_back(msg);
77            return Ok(());
78        }
79
80        // Inbound RTCP is for the interceptors. Two ways past this point:
81        //
82        // 1. an interceptor judged this particular packet worth forwarding, by attaching
83        //    `Attribute::DeliverToApplication` — the per-packet judgement, made by whichever
84        //    interceptor is qualified to make it;
85        // 2. it carries attributes an interceptor attached for something beyond the chain. The
86        //    *payload* still stops here — nothing asked for this packet, and handing it over
87        //    because an interceptor annotated it would be a surprise — but the packet carries on
88        //    as an empty-RTCP carrier so the attributes reach the crate boundary.
89        //
90        // Anything else ends here.
91        if msg.message.has(&Attribute::DeliverToApplication) {
92            self.read_queue.push_back(msg);
93        } else if !msg.message.attributes.is_empty() {
94            msg.message.packet = Packet::Rtcp(Vec::new());
95            self.read_queue.push_back(msg);
96        }
97        Ok(())
98    }
99
100    fn poll_read(&mut self) -> Option<Self::Rout> {
101        self.read_queue.pop_front()
102    }
103
104    fn handle_write(&mut self, msg: TaggedPacket) -> Result<(), Self::Error> {
105        self.write_queue.push_back(msg);
106        Ok(())
107    }
108
109    fn poll_write(&mut self) -> Option<Self::Wout> {
110        self.write_queue.pop_front()
111    }
112}
113
114impl Interceptor for NoopInterceptor {
115    fn bind_local_stream(&mut self, _info: &StreamInfo) {}
116    fn unbind_local_stream(&mut self, _info: &StreamInfo) {}
117    fn bind_remote_stream(&mut self, _info: &StreamInfo) {}
118    fn unbind_remote_stream(&mut self, _info: &StreamInfo) {}
119}
120
121#[cfg(test)]
122mod tests {
123    use super::*;
124    use crate::{AttributedPacket, Registry, Slot, StreamInfo};
125    use sansio::Protocol;
126    use shared::TransportContext;
127    use shared::error::Error;
128    use std::collections::VecDeque;
129    use std::time::Instant;
130
131    fn packet(message: Packet) -> TaggedPacket {
132        TaggedPacket {
133            now: Instant::now(),
134            transport: TransportContext::default(),
135            message: AttributedPacket::new(message),
136        }
137    }
138
139    #[test]
140    fn inbound_rtcp_does_not_reach_the_application() {
141        let mut chain = Registry::new().build();
142
143        chain.handle_read(packet(Packet::Rtcp(vec![]))).unwrap();
144        assert!(chain.poll_read().is_none());
145    }
146
147    #[test]
148    fn inbound_rtp_passes_through() {
149        let mut chain = Registry::new().build();
150
151        chain
152            .handle_read(packet(Packet::Rtp(rtp::Packet::default())))
153            .unwrap();
154        assert!(chain.poll_read().is_some());
155    }
156
157    /// Outbound RTCP is untouched: this ends the *inbound* path only.
158    #[test]
159    fn outbound_rtcp_is_not_affected() {
160        let mut chain = Registry::new().build();
161
162        chain.handle_write(packet(Packet::Rtcp(vec![]))).unwrap();
163        assert!(chain.poll_write().is_some());
164    }
165
166    /// An interceptor wire-ward of the terminus still sees inbound RTCP — that is the whole point of it
167    /// being application-most.
168    #[test]
169    fn stages_before_it_still_see_inbound_rtcp() {
170        #[derive(Default)]
171        struct Counter {
172            seen: std::sync::Arc<std::sync::atomic::AtomicUsize>,
173            read_queue: VecDeque<TaggedPacket>,
174            write_queue: VecDeque<TaggedPacket>,
175        }
176        impl Protocol<TaggedPacket, TaggedPacket, ()> for Counter {
177            type Rout = TaggedPacket;
178            type Wout = TaggedPacket;
179            type Eout = ();
180            type Error = Error;
181            type Time = Instant;
182
183            fn handle_read(&mut self, msg: TaggedPacket) -> Result<(), Self::Error> {
184                if matches!(msg.message.packet, Packet::Rtcp(_)) {
185                    self.seen.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
186                }
187                self.read_queue.push_back(msg);
188                Ok(())
189            }
190
191            fn poll_read(&mut self) -> Option<Self::Rout> {
192                self.read_queue.pop_front()
193            }
194
195            fn handle_write(&mut self, msg: TaggedPacket) -> Result<(), Self::Error> {
196                self.write_queue.push_back(msg);
197                Ok(())
198            }
199
200            fn poll_write(&mut self) -> Option<Self::Wout> {
201                self.write_queue.pop_front()
202            }
203
204            fn handle_timeout(&mut self, _now: Instant) -> Result<(), Self::Error> {
205                Ok(())
206            }
207
208            fn poll_timeout(&mut self) -> Option<Self::Time> {
209                None
210            }
211        }
212        impl Interceptor for Counter {
213            fn bind_local_stream(&mut self, _info: &StreamInfo) {}
214            fn unbind_local_stream(&mut self, _info: &StreamInfo) {}
215            fn bind_remote_stream(&mut self, _info: &StreamInfo) {}
216            fn unbind_remote_stream(&mut self, _info: &StreamInfo) {}
217        }
218        let counter = Counter::default();
219        let seen = counter.seen.clone();
220        let mut chain = Registry::new().with(Slot::NackGenerator, counter).build();
221
222        chain.handle_read(packet(Packet::Rtcp(vec![]))).unwrap();
223
224        assert_eq!(1, seen.load(std::sync::atomic::Ordering::Relaxed));
225        assert!(chain.poll_read().is_none(), "but it stops at the terminus");
226    }
227}
228
229#[cfg(test)]
230mod carrier_tests {
231    use super::*;
232    use crate::{Attribute, AttributedPacket, Registry};
233    use sansio::Protocol;
234    use shared::TransportContext;
235    use std::time::Instant;
236
237    fn annotated(attribute: Option<Attribute>) -> TaggedPacket {
238        let mut message = AttributedPacket::new(Packet::Rtcp(vec![Box::new(
239            rtcp::receiver_report::ReceiverReport::default(),
240        )]));
241        if let Some(attribute) = attribute {
242            message.add(attribute);
243        }
244        TaggedPacket {
245            now: Instant::now(),
246            transport: TransportContext::default(),
247            message,
248        }
249    }
250
251    /// An annotated report is stripped, not dropped: the payload stops here — the application did
252    /// not ask for RTCP — but the attributes carry on to the crate boundary, which is the only way
253    /// a bandwidth estimate reaches the application.
254    #[test]
255    fn an_annotated_report_passes_on_as_an_empty_carrier() {
256        let mut chain = Registry::new().build();
257        chain
258            .handle_read(annotated(Some(Attribute::TargetBitrateChanged {
259                bits_per_second: 750_000.0,
260            })))
261            .unwrap();
262
263        let carrier = chain.poll_read().expect("the attributes must get through");
264        assert!(
265            matches!(&carrier.message.packet, Packet::Rtcp(packets) if packets.is_empty()),
266            "the payload must be stripped: the application did not ask for RTCP"
267        );
268        assert!(
269            carrier.message.has(&Attribute::TargetBitrateChanged {
270                bits_per_second: 0.0
271            }),
272            "but the attribute must survive"
273        );
274    }
275
276    /// A report nobody annotated is still dropped, exactly as before.
277    #[test]
278    fn an_unannotated_report_still_stops_here() {
279        let mut chain = Registry::new().build();
280        chain.handle_read(annotated(None)).unwrap();
281        assert!(chain.poll_read().is_none());
282    }
283
284    /// `DeliverToApplication` is the per-packet judgement its documentation always described:
285    /// this one packet goes on **with its payload**, without turning RTCP on chain-wide.
286    #[test]
287    fn deliver_to_application_keeps_the_payload() {
288        let mut chain = Registry::new().build();
289        chain
290            .handle_read(annotated(Some(Attribute::DeliverToApplication)))
291            .unwrap();
292
293        let delivered = chain.poll_read().expect("forwarded");
294        assert!(
295            matches!(&delivered.message.packet, Packet::Rtcp(packets) if !packets.is_empty()),
296            "this packet was judged worth delivering, payload and all"
297        );
298    }
299}