Skip to main content

rtc_interceptor/intervalpli/
generator.rs

1//! Periodic Picture Loss Indication for bound remote streams.
2
3use super::stream_supports_pli;
4use crate::Interceptor;
5use crate::stream_info::StreamInfo;
6use crate::{Attribute, AttributedPacket, Packet, TaggedPacket};
7use sansio::Protocol;
8use shared::TransportContext;
9use shared::error::Error;
10use std::collections::{BTreeSet, VecDeque};
11use std::time::{Duration, Instant};
12
13/// How often a keyframe is requested when no interval is configured.
14pub const DEFAULT_INTERVAL: Duration = Duration::from_secs(3);
15
16/// Requests a keyframe from every bound remote stream on a fixed interval.
17///
18/// # Where this belongs in the chain
19///
20/// It only generates, and what it generates is RTCP, so its position is fixed by nothing except
21/// being on the application side of the pacer — as everything that generates is, so that what it
22/// produces is still metered on its way out.
23///
24/// # Forcing a keyframe
25///
26/// Attach [`Attribute::ForcePli`] to a packet handed to `handle_read`. Under the nested design
27/// this was an inherent method, `force_pli`, which became unreachable the moment anything wrapped
28/// this interceptor — it was dead code in practice. An attribute rides on a packet through the
29/// walk and reaches this interceptor wherever it sits, and is not consumed, so anything further
30/// on sees both the request and the PLI it produced.
31///
32/// Sans-I/O has no clock of its own, so the interval is measured from the first `Instant` the
33/// interceptor is handed, whether that arrives via `handle_read` or `handle_timeout`.
34pub struct IntervalPliInterceptor {
35    interval: Duration,
36    /// Bound remote streams that negotiated PLI. Ordered so a tick emits deterministically.
37    streams: BTreeSet<u32>,
38    /// Streams bound but not yet sent their first request.
39    ///
40    /// Upstream asks immediately on bind, which needs an instant this interceptor does not have until
41    /// one is handed to it; these are flushed at the first opportunity.
42    pending_immediate: BTreeSet<u32>,
43    next_timeout: Option<Instant>,
44    write_queue: VecDeque<TaggedPacket>,
45    /// Inbound packets ready for the next interceptor.
46    read_queue: VecDeque<TaggedPacket>,
47}
48
49impl Default for IntervalPliInterceptor {
50    fn default() -> Self {
51        Self::new(DEFAULT_INTERVAL)
52    }
53}
54
55impl IntervalPliInterceptor {
56    /// A generator asking every bound stream for a keyframe every `interval`.
57    ///
58    /// A zero interval disables periodic requests, leaving only [`Attribute::ForcePli`] —
59    /// matching upstream, which creates no ticker when its interval is not positive.
60    pub fn new(interval: Duration) -> Self {
61        Self {
62            read_queue: VecDeque::new(),
63            interval,
64            streams: BTreeSet::new(),
65            pending_immediate: BTreeSet::new(),
66            next_timeout: None,
67            write_queue: VecDeque::new(),
68        }
69    }
70
71    /// The streams currently being asked for keyframes.
72    pub fn bound_streams(&self) -> impl Iterator<Item = u32> + '_ {
73        self.streams.iter().copied()
74    }
75
76    /// Queue one RTCP packet carrying a PLI per SSRC.
77    ///
78    /// One compound packet rather than one datagram each, as upstream does: they are all going to
79    /// the same peer at the same instant.
80    fn queue_plis(&mut self, now: Instant, ssrcs: &[u32]) {
81        if ssrcs.is_empty() {
82            return;
83        }
84
85        // Asking now satisfies the ask-on-bind these streams were still waiting for, so a forced
86        // request does not arrive alongside a duplicate of itself.
87        for ssrc in ssrcs {
88            self.pending_immediate.remove(ssrc);
89        }
90
91        let plis: Vec<Box<dyn rtcp::Packet>> = ssrcs
92            .iter()
93            .map(|&ssrc| {
94                Box::new(
95                    rtcp::payload_feedbacks::picture_loss_indication::PictureLossIndication {
96                        sender_ssrc: 0,
97                        media_ssrc: ssrc,
98                    },
99                ) as Box<dyn rtcp::Packet>
100            })
101            .collect();
102
103        self.write_queue.push_back(TaggedPacket {
104            now,
105            transport: TransportContext::default(),
106            message: AttributedPacket::new(Packet::Rtcp(plis)),
107        });
108    }
109
110    /// Arm the interval from the first instant this interceptor is handed, and send the first request
111    /// for any stream bound since.
112    fn observe(&mut self, now: Instant) {
113        if !self.pending_immediate.is_empty() {
114            let ssrcs: Vec<u32> = self.pending_immediate.iter().copied().collect();
115            self.queue_plis(now, &ssrcs);
116        }
117        self.arm(now);
118    }
119
120    /// Start the interval running, if it is not already and there is anything to ask.
121    fn arm(&mut self, now: Instant) {
122        if self.next_timeout.is_none() && self.is_periodic() && !self.streams.is_empty() {
123            self.next_timeout = Some(now + self.interval);
124        }
125    }
126
127    fn is_periodic(&self) -> bool {
128        !self.interval.is_zero()
129    }
130
131    /// SSRCs that are bound, from a request naming some or all of them.
132    ///
133    /// A PLI for a stream nobody is receiving has no destination, so unbound SSRCs are dropped.
134    fn targets(&self, requested: Option<&Vec<u32>>) -> Vec<u32> {
135        match requested {
136            None => self.streams.iter().copied().collect(),
137            Some(ssrcs) => ssrcs
138                .iter()
139                .copied()
140                .filter(|ssrc| self.streams.contains(ssrc))
141                .collect(),
142        }
143    }
144}
145
146impl Protocol<TaggedPacket, TaggedPacket, ()> for IntervalPliInterceptor {
147    type Rout = TaggedPacket;
148    type Wout = TaggedPacket;
149    type Eout = ();
150    type Error = Error;
151    type Time = Instant;
152
153    fn handle_read(&mut self, msg: TaggedPacket) -> Result<(), Self::Error> {
154        self.observe(msg.now);
155
156        // A keyframe request arrives as an attribute on a packet rather than out of band: with no
157        // event channel, that is how one interceptor tells another something.
158        if let Some(Attribute::ForcePli { ssrcs }) =
159            msg.message.get(&Attribute::ForcePli { ssrcs: None })
160        {
161            let targets = self.targets(ssrcs.as_ref());
162            self.queue_plis(msg.now, &targets);
163            self.arm(msg.now);
164        }
165
166        self.read_queue.push_back(msg);
167        Ok(())
168    }
169
170    fn poll_read(&mut self) -> Option<Self::Rout> {
171        self.read_queue.pop_front()
172    }
173
174    fn handle_write(&mut self, msg: TaggedPacket) -> Result<(), Self::Error> {
175        self.write_queue.push_back(msg);
176        Ok(())
177    }
178
179    fn poll_write(&mut self) -> Option<TaggedPacket> {
180        self.write_queue.pop_front()
181    }
182
183    fn handle_timeout(&mut self, now: Instant) -> Result<(), Error> {
184        self.observe(now);
185
186        if let Some(next_timeout) = self.next_timeout
187            && now >= next_timeout
188        {
189            self.next_timeout = Some(now + self.interval);
190            let ssrcs: Vec<u32> = self.streams.iter().copied().collect();
191            self.queue_plis(now, &ssrcs);
192        }
193        Ok(())
194    }
195
196    fn poll_timeout(&mut self) -> Option<Instant> {
197        self.next_timeout
198    }
199}
200
201impl Interceptor for IntervalPliInterceptor {
202    fn bind_remote_stream(&mut self, info: &StreamInfo) {
203        if stream_supports_pli(info) {
204            self.streams.insert(info.ssrc);
205            self.pending_immediate.insert(info.ssrc);
206        }
207    }
208
209    fn unbind_remote_stream(&mut self, info: &StreamInfo) {
210        self.streams.remove(&info.ssrc);
211        self.pending_immediate.remove(&info.ssrc);
212        if self.streams.is_empty() {
213            // Nothing left to ask: stop asking to be woken.
214            self.next_timeout = None;
215        }
216    }
217
218    fn bind_local_stream(&mut self, _info: &StreamInfo) {}
219
220    fn unbind_local_stream(&mut self, _info: &StreamInfo) {}
221}
222
223#[cfg(test)]
224mod tests {
225    use super::*;
226    use crate::chain::InterceptorChain;
227    use crate::stream_info::RTCPFeedback;
228    use sansio::Protocol;
229
230    fn stream_info(ssrc: u32) -> StreamInfo {
231        StreamInfo {
232            ssrc,
233            rtcp_feedback: vec![RTCPFeedback {
234                typ: "nack".to_owned(),
235                parameter: "pli".to_owned(),
236            }],
237            ..Default::default()
238        }
239    }
240
241    /// A packet carrying a keyframe request. With no event channel, an attribute on a packet is
242    /// how one interceptor asks another for something.
243    fn force_pli(now: Instant, ssrcs: Option<Vec<u32>>) -> TaggedPacket {
244        let mut msg = TaggedPacket {
245            now,
246            transport: Default::default(),
247            message: AttributedPacket::new(Packet::Rtp(rtp::Packet::default())),
248        };
249        msg.message.add(Attribute::ForcePli { ssrcs });
250        msg
251    }
252
253    fn plis(chain: &mut InterceptorChain) -> Vec<u32> {
254        let mut out = Vec::new();
255        while let Some(pkt) = chain.poll_write() {
256            if let Packet::Rtcp(packets) = &pkt.message.packet {
257                for p in packets {
258                    if let Some(pli) = p
259                        .as_any()
260                        .downcast_ref::<rtcp::payload_feedbacks::picture_loss_indication::PictureLossIndication>(
261                        ) {
262                        out.push(pli.media_ssrc);
263                    }
264                }
265            }
266        }
267        out
268    }
269
270    fn chain(interval: Duration) -> InterceptorChain {
271        InterceptorChain::new(vec![Box::new(IntervalPliInterceptor::new(interval))])
272    }
273
274    #[test]
275    fn a_bound_stream_is_asked_immediately() {
276        let now = Instant::now();
277        let mut chain = chain(Duration::from_secs(3));
278        chain.bind_remote_stream(&stream_info(7));
279
280        chain.handle_timeout(now).unwrap();
281        assert_eq!(vec![7], plis(&mut chain));
282    }
283
284    #[test]
285    fn requests_repeat_on_the_interval() {
286        let now = Instant::now();
287        let mut chain = chain(Duration::from_secs(1));
288        chain.bind_remote_stream(&stream_info(7));
289
290        chain.handle_timeout(now).unwrap();
291        assert_eq!(vec![7], plis(&mut chain), "the immediate one");
292
293        chain
294            .handle_timeout(now + Duration::from_millis(999))
295            .unwrap();
296        assert!(plis(&mut chain).is_empty(), "not due yet");
297
298        chain.handle_timeout(now + Duration::from_secs(1)).unwrap();
299        assert_eq!(vec![7], plis(&mut chain), "due");
300    }
301
302    #[test]
303    fn an_unbound_stream_stops_being_asked() {
304        let now = Instant::now();
305        let mut chain = chain(Duration::from_secs(1));
306        chain.bind_remote_stream(&stream_info(7));
307        chain.handle_timeout(now).unwrap();
308        let _ = plis(&mut chain);
309
310        chain.unbind_remote_stream(&stream_info(7));
311        chain.handle_timeout(now + Duration::from_secs(5)).unwrap();
312
313        assert!(plis(&mut chain).is_empty());
314        assert_eq!(None, chain.poll_timeout(), "and stops asking to be woken");
315    }
316
317    // -----------------------------------------------------------------------------------
318    // ForcePli — the capability that replaces an unreachable inherent method
319    // -----------------------------------------------------------------------------------
320
321    /// The whole point of the event: this reaches the generator **through the chain**, which the
322    /// inherent `force_pli` could not do once anything wrapped the interceptor.
323    #[test]
324    fn force_pli_reaches_the_generator_through_the_chain() {
325        let now = Instant::now();
326        // Deliberately not the only interceptor, and not the one the application holds: under nesting
327        // this arrangement is exactly what made `force_pli` unreachable.
328        let mut chain = InterceptorChain::new(vec![
329            Box::new(IntervalPliInterceptor::new(Duration::ZERO)),
330            Box::new(crate::TwccSenderBuilder::new().build()),
331        ]);
332        chain.bind_remote_stream(&stream_info(7));
333        // A newly bound stream is asked for a keyframe the first time the interceptor is handed a
334        // clock, and a carrier packet supplies one — so clear that out first, leaving only what
335        // the request itself produces.
336        chain.handle_timeout(now).unwrap();
337        plis(&mut chain);
338
339        chain
340            .handle_read(force_pli(now, None))
341            .expect("handle_read");
342
343        assert_eq!(vec![7], plis(&mut chain));
344    }
345
346    #[test]
347    fn force_pli_can_name_specific_streams() {
348        let now = Instant::now();
349        let mut chain = chain(Duration::ZERO);
350        chain.bind_remote_stream(&stream_info(7));
351        chain.bind_remote_stream(&stream_info(8));
352        // A newly bound stream is asked for a keyframe the first time the interceptor is handed a
353        // clock, and a carrier packet supplies one — so clear that out first, leaving only what
354        // the request itself produces.
355        chain.handle_timeout(now).unwrap();
356        plis(&mut chain);
357
358        chain
359            .handle_read(force_pli(now, Some(vec![8])))
360            .expect("handle_read");
361
362        assert_eq!(vec![8], plis(&mut chain), "only the one named");
363    }
364
365    #[test]
366    fn force_pli_ignores_streams_that_are_not_bound() {
367        let now = Instant::now();
368        let mut chain = chain(Duration::ZERO);
369        chain.bind_remote_stream(&stream_info(7));
370        // A newly bound stream is asked for a keyframe the first time the interceptor is handed a
371        // clock, and a carrier packet supplies one — so clear that out first, leaving only what
372        // the request itself produces.
373        chain.handle_timeout(now).unwrap();
374        plis(&mut chain);
375
376        chain
377            .handle_read(force_pli(now, Some(vec![999])))
378            .expect("handle_read");
379
380        assert!(
381            plis(&mut chain).is_empty(),
382            "a PLI for a stream nobody receives has no destination"
383        );
384    }
385
386    /// A zero interval means no periodic requests, but forcing still works.
387    #[test]
388    fn a_zero_interval_disables_only_the_periodic_requests() {
389        let now = Instant::now();
390        let mut chain = chain(Duration::ZERO);
391        chain.bind_remote_stream(&stream_info(7));
392
393        chain.handle_timeout(now).unwrap();
394        assert_eq!(vec![7], plis(&mut chain), "the immediate one still happens");
395        assert_eq!(None, chain.poll_timeout(), "but no interval is armed");
396
397        chain.handle_timeout(now + Duration::from_secs(60)).unwrap();
398        assert!(plis(&mut chain).is_empty());
399
400        chain
401            .handle_read(force_pli(now, None))
402            .expect("handle_read");
403        assert_eq!(vec![7], plis(&mut chain), "forcing still works");
404    }
405
406    /// A request rides in on a packet and the packet carries on, so anything after this
407    /// interceptor sees both the request and the PLI it produced.
408    #[test]
409    fn a_force_pli_attribute_is_not_consumed() {
410        let now = Instant::now();
411        let mut chain =
412            InterceptorChain::new(vec![Box::new(IntervalPliInterceptor::new(Duration::ZERO))]);
413        chain.bind_remote_stream(&stream_info(7));
414        // A newly bound stream is asked for a keyframe the first time the interceptor is handed a
415        // clock, and a carrier packet supplies one — so clear that out first, leaving only what
416        // the request itself produces.
417        chain.handle_timeout(now).unwrap();
418        plis(&mut chain);
419
420        let mut carrier = TaggedPacket {
421            now,
422            transport: Default::default(),
423            message: AttributedPacket::new(Packet::Rtp(rtp::Packet::default())),
424        };
425        carrier.message.add(Attribute::ForcePli { ssrcs: None });
426        chain.handle_read(carrier).expect("handle_read");
427
428        assert_eq!(vec![7], plis(&mut chain), "the request was acted on");
429        assert!(
430            chain.poll_read().is_some(),
431            "and the packet that carried it carried on"
432        );
433    }
434}