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::stream_info::StreamInfo;
5use crate::{Interceptor, Packet, TaggedPacket, interceptor};
6use shared::TransportContext;
7use shared::error::Error;
8use std::collections::{BTreeSet, VecDeque};
9use std::marker::PhantomData;
10use std::time::{Duration, Instant};
11
12/// How often a keyframe is requested when no interval is configured.
13pub const DEFAULT_INTERVAL: Duration = Duration::from_secs(3);
14
15/// Builder for [`IntervalPliInterceptor`].
16///
17/// # Example
18///
19/// ```
20/// use rtc_interceptor::{IntervalPliBuilder, Registry};
21/// use std::time::Duration;
22///
23/// let chain = Registry::new()
24///     .with(IntervalPliBuilder::new().with_interval(Duration::from_secs(1)).build())
25///     .build();
26/// ```
27pub struct IntervalPliBuilder<P> {
28    interval: Duration,
29    _phantom: PhantomData<P>,
30}
31
32impl<P> Default for IntervalPliBuilder<P> {
33    fn default() -> Self {
34        Self {
35            interval: DEFAULT_INTERVAL,
36            _phantom: PhantomData,
37        }
38    }
39}
40
41impl<P> IntervalPliBuilder<P> {
42    /// Create a builder with the default interval.
43    pub fn new() -> Self {
44        Self::default()
45    }
46
47    /// How often each bound stream is asked for a keyframe.
48    ///
49    /// A zero interval disables periodic requests, leaving only
50    /// [`force_pli`](IntervalPliInterceptor::force_pli) — matching upstream, which creates no
51    /// ticker when its interval is not positive.
52    pub fn with_interval(mut self, interval: Duration) -> Self {
53        self.interval = interval;
54        self
55    }
56
57    /// Build the interceptor factory function.
58    pub fn build(self) -> impl FnOnce(P) -> IntervalPliInterceptor<P> {
59        move |inner| IntervalPliInterceptor::new(inner, self.interval)
60    }
61}
62
63/// Requests a keyframe from every bound remote stream on a fixed interval.
64///
65/// - Sans-I/O has no clock of its own: the interval is measured from the first `Instant`
66///   the interceptor is handed, whether that arrives via `handle_read` or `handle_timeout`.
67#[derive(Interceptor)]
68pub struct IntervalPliInterceptor<P> {
69    #[next]
70    inner: P,
71    interval: Duration,
72    /// Bound remote streams that negotiated PLI. Ordered so a tick emits deterministically.
73    streams: BTreeSet<u32>,
74    /// Streams bound but not yet sent their first request.
75    ///
76    /// Upstream asks immediately on bind, which needs an instant this interceptor does not have
77    /// until one is handed to it; these are flushed at the first opportunity.
78    pending_immediate: BTreeSet<u32>,
79    next_timeout: Option<Instant>,
80    write_queue: VecDeque<TaggedPacket>,
81}
82
83impl<P> IntervalPliInterceptor<P> {
84    fn new(inner: P, interval: Duration) -> Self {
85        Self {
86            inner,
87            interval,
88            streams: BTreeSet::new(),
89            pending_immediate: BTreeSet::new(),
90            next_timeout: None,
91            write_queue: VecDeque::new(),
92        }
93    }
94
95    /// Request a keyframe from every bound stream, now.
96    ///
97    /// # Why this takes an instant, and why it is not on the trait
98    ///
99    /// `Ein` is `()` for every interceptor, so there is no typed event to carry an out-of-band
100    /// request through a chain — and widening the trait for one interceptor would break every
101    /// other. So this is an inherent method, reachable while the concrete type is still in hand
102    /// (before [`Registry::boxed`](crate::Registry::boxed) erases it). The instant is a parameter
103    /// because a sans-I/O interceptor has no clock to ask.
104    pub fn force_pli(&mut self, now: Instant) {
105        let ssrcs: Vec<u32> = self.streams.iter().copied().collect();
106        self.queue_plis(now, &ssrcs);
107    }
108
109    /// Request a keyframe from specific streams, now.
110    ///
111    /// SSRCs that are not bound are ignored: a PLI for a stream nobody is receiving has no
112    /// destination.
113    pub fn force_pli_for(&mut self, now: Instant, ssrcs: &[u32]) {
114        let bound: Vec<u32> = ssrcs
115            .iter()
116            .copied()
117            .filter(|ssrc| self.streams.contains(ssrc))
118            .collect();
119        self.queue_plis(now, &bound);
120    }
121
122    /// The streams currently being asked for keyframes.
123    pub fn bound_streams(&self) -> impl Iterator<Item = u32> + '_ {
124        self.streams.iter().copied()
125    }
126
127    /// Queue one RTCP packet carrying a PLI per SSRC.
128    ///
129    /// One compound packet rather than one datagram each, as upstream does: they are all going to
130    /// the same peer at the same instant.
131    fn queue_plis(&mut self, now: Instant, ssrcs: &[u32]) {
132        if ssrcs.is_empty() {
133            return;
134        }
135
136        let plis: Vec<Box<dyn rtcp::Packet>> = ssrcs
137            .iter()
138            .map(|&ssrc| {
139                Box::new(
140                    rtcp::payload_feedbacks::picture_loss_indication::PictureLossIndication {
141                        sender_ssrc: 0,
142                        media_ssrc: ssrc,
143                    },
144                ) as Box<dyn rtcp::Packet>
145            })
146            .collect();
147
148        self.write_queue.push_back(TaggedPacket {
149            now,
150            transport: TransportContext::default(),
151            message: Packet::Rtcp(plis),
152        });
153    }
154
155    /// Arm the interval from the first instant this interceptor is handed, and send the first
156    /// request for any stream bound since.
157    fn observe(&mut self, now: Instant) {
158        if !self.pending_immediate.is_empty() {
159            let ssrcs: Vec<u32> = self.pending_immediate.iter().copied().collect();
160            self.pending_immediate.clear();
161            self.queue_plis(now, &ssrcs);
162        }
163
164        if self.next_timeout.is_none() && self.is_periodic() && !self.streams.is_empty() {
165            self.next_timeout = Some(now + self.interval);
166        }
167    }
168
169    fn is_periodic(&self) -> bool {
170        !self.interval.is_zero()
171    }
172}
173
174#[interceptor]
175impl<P: Interceptor> IntervalPliInterceptor<P> {
176    #[overrides]
177    fn bind_remote_stream(&mut self, info: &StreamInfo) {
178        if stream_supports_pli(info) {
179            self.streams.insert(info.ssrc);
180            self.pending_immediate.insert(info.ssrc);
181        }
182        self.inner.bind_remote_stream(info);
183    }
184
185    #[overrides]
186    fn unbind_remote_stream(&mut self, info: &StreamInfo) {
187        self.streams.remove(&info.ssrc);
188        self.pending_immediate.remove(&info.ssrc);
189        if self.streams.is_empty() {
190            // Nothing left to ask: stop asking to be woken (delivery rule 3).
191            self.next_timeout = None;
192        }
193        self.inner.unbind_remote_stream(info);
194    }
195
196    #[overrides]
197    fn handle_read(&mut self, msg: TaggedPacket) -> Result<(), Self::Error> {
198        self.observe(msg.now);
199        self.inner.handle_read(msg)
200    }
201
202    #[overrides]
203    fn handle_timeout(&mut self, now: Self::Time) -> Result<(), Self::Error> {
204        self.observe(now);
205
206        if let Some(next_timeout) = self.next_timeout
207            && now >= next_timeout
208        {
209            self.next_timeout = Some(now + self.interval);
210            let ssrcs: Vec<u32> = self.streams.iter().copied().collect();
211            self.queue_plis(now, &ssrcs);
212        }
213
214        self.inner.handle_timeout(now)
215    }
216
217    #[overrides]
218    fn poll_timeout(&mut self) -> Option<Self::Time> {
219        match (self.next_timeout, self.inner.poll_timeout()) {
220            (Some(mine), Some(theirs)) => Some(mine.min(theirs)),
221            (mine, theirs) => mine.or(theirs),
222        }
223    }
224
225    #[overrides]
226    fn poll_write(&mut self) -> Option<Self::Wout> {
227        // A generated PLI is terminal (chain contract rule 1): it is complete when built, and
228        // nothing below needs to transform it.
229        if let Some(packet) = self.write_queue.pop_front() {
230            return Some(packet);
231        }
232        self.inner.poll_write()
233    }
234}