rtc_interceptor/jitterbuffer/receiver.rs
1//! Receive-side jitter buffer interceptor: a time-based playout policy over [`JitterBuffer`].
2//!
3//! # Why this does not follow upstream
4//!
5//! `pion/interceptor`'s `ReceiverInterceptor` buffers a *packet count* and is pull-driven: its
6//! `BindRemoteStream` reader pushes the packet it just read and immediately pops one back, so the
7//! buffer is a fixed-length delay line of 50 packets rather than a span of time. webrtc#846 asks
8//! for the other thing — a depth measured in milliseconds, drained on a timer — because what a
9//! jitter buffer has to absorb is variation in *arrival time*, and a packet count only stands in
10//! for that while the bitrate is constant.
11//!
12//! The differences that follow from that:
13//!
14//! | | upstream | here |
15//! |---|---|---|
16//! | depth | 50 packets | a [`Duration`] |
17//! | emission | inside the read path, one in one out | on `handle_timeout`, everything now due |
18//! | not ready yet | `ErrPopWhileBuffering` | `poll_read` simply yields nothing |
19//! | per stream | one buffer for all of them | one per SSRC |
20//! | unbind | clears every stream's packets | drops only that stream |
21
22use super::buffer::{JitterBuffer, Rejected, State};
23use super::sequence::TimestampExtender;
24use crate::stream_info::StreamInfo;
25use crate::{Interceptor, Packet, TaggedPacket, interceptor};
26use shared::error::Error;
27use std::collections::HashMap;
28use std::marker::PhantomData;
29use std::time::{Duration, Instant};
30
31/// Default playout depth: enough to absorb ordinary network jitter without adding audible delay.
32pub const DEFAULT_DEPTH: Duration = Duration::from_millis(120);
33
34/// Default cap on packets held per stream, so a stalled or hostile stream cannot grow without
35/// bound while its deadline is still in the future.
36pub const DEFAULT_CAPACITY: usize = 512;
37
38/// A timestamp jump this large is read as a stream restart rather than a gap to wait out.
39///
40/// Ten seconds of media: far beyond any real inter-packet spacing, and small enough that a genuine
41/// restart is noticed promptly.
42const DISCONTINUITY: Duration = Duration::from_secs(10);
43
44/// Builder for [`JitterBufferInterceptor`].
45///
46/// # Example
47///
48/// ```
49/// use rtc_interceptor::{JitterBufferBuilder, Registry};
50/// use std::time::Duration;
51///
52/// let chain = Registry::new()
53/// .with(JitterBufferBuilder::new().with_depth(Duration::from_millis(80)).build())
54/// .build();
55/// ```
56pub struct JitterBufferBuilder<P> {
57 depth: Duration,
58 capacity: usize,
59 _phantom: PhantomData<P>,
60}
61
62impl<P> Default for JitterBufferBuilder<P> {
63 fn default() -> Self {
64 Self {
65 depth: DEFAULT_DEPTH,
66 capacity: DEFAULT_CAPACITY,
67 _phantom: PhantomData,
68 }
69 }
70}
71
72impl<P> JitterBufferBuilder<P> {
73 /// Create a builder with the default depth and capacity.
74 pub fn new() -> Self {
75 Self::default()
76 }
77
78 /// How long a packet is held to absorb arrival-time variation.
79 ///
80 /// # Its relationship with NACK
81 ///
82 /// This is the delay the buffer adds, and it is also the window in which a retransmission is
83 /// still useful. A lost packet cannot come back before
84 ///
85 /// ```text
86 /// detection (up to one NACK interval) + round trip + the sender's response
87 /// ```
88 ///
89 /// has elapsed, and the buffer plays a position out one depth after that position is due. So
90 /// **a depth shallower than that sum means every retransmission arrives too late** — its slot
91 /// has already been played past, so it is dropped rather than emitted out of order, and the
92 /// NACK traffic was spent for nothing.
93 ///
94 /// The two are deliberately not coupled: a mechanism for the jitter buffer and the NACK
95 /// generator to negotiate would tie together interceptors that are otherwise independent, and
96 /// an application that configures both can honour the inequality itself.
97 /// `tests/jitter_buffer_nack_depth.rs` holds it to that — the same loss recovered under a
98 /// depth chosen to accommodate it and lost under one chosen not to.
99 pub fn with_depth(mut self, depth: Duration) -> Self {
100 self.depth = depth;
101 self
102 }
103
104 /// Maximum packets held per stream, independently of the time depth.
105 pub fn with_capacity(mut self, capacity: usize) -> Self {
106 self.capacity = capacity;
107 self
108 }
109
110 /// Build the interceptor factory function.
111 pub fn build(self) -> impl FnOnce(P) -> JitterBufferInterceptor<P> {
112 move |inner| JitterBufferInterceptor::new(inner, self.depth, self.capacity)
113 }
114}
115
116/// Where a stream's RTP timeline was pinned to the wall clock.
117///
118/// Playout instants are derived from this: a packet's deadline is the anchor's arrival instant
119/// plus the depth, plus however far the packet's RTP timestamp is beyond the anchor's. Anchoring
120/// on the first packet, rather than waiting for the buffered span to reach the full depth, is what
121/// lets a single-packet or paused stream start at all.
122#[derive(Debug, Clone, Copy)]
123struct Anchor {
124 arrived: Instant,
125 timestamp: u64,
126}
127
128/// Everything tracked for one remote stream.
129struct Stream {
130 buffer: JitterBuffer,
131 timestamps: TimestampExtender,
132 /// RTP timestamp units per second, from the negotiated codec.
133 clock_rate: u32,
134 anchor: Option<Anchor>,
135 /// Playout instant per held packet, keyed by extended sequence number.
136 deadlines: HashMap<u64, Instant>,
137}
138
139impl Stream {
140 fn new(clock_rate: u32, capacity: usize) -> Self {
141 Self {
142 buffer: JitterBuffer::new(capacity),
143 timestamps: TimestampExtender::new(),
144 // A zero clock rate would make every deadline a division by zero; treat an
145 // unnegotiated rate as the RTP video default rather than refusing to buffer.
146 clock_rate: if clock_rate == 0 { 90_000 } else { clock_rate },
147 anchor: None,
148 deadlines: HashMap::new(),
149 }
150 }
151
152 /// How far `timestamp` is beyond the anchor, in wall-clock terms.
153 fn offset_from_anchor(&self, anchor: &Anchor, timestamp: u64) -> Duration {
154 let ticks = timestamp.saturating_sub(anchor.timestamp);
155 Duration::from_secs_f64(ticks as f64 / f64::from(self.clock_rate))
156 }
157
158 /// Start the timeline again at this packet, dropping what was held *and* the ordering state.
159 ///
160 /// Only for a timestamp discontinuity: the old anchor no longer describes where this stream is
161 /// in time, and deriving deadlines from it would put every subsequent packet either
162 /// immediately overdue or hours away. Resetting the buffer also clears its extended-sequence
163 /// origin, which is why running dry must not come through here — a fresh origin measured
164 /// against a stale played-out watermark would reject everything.
165 fn restart(&mut self, arrived: Instant, timestamp: u64) {
166 self.buffer.reset();
167 self.deadlines.clear();
168 self.anchor = Some(Anchor { arrived, timestamp });
169 }
170}
171
172/// Holds each stream's packets for a fixed span of time, then releases them in order.
173///
174/// The `jitterbuffer::receiver` module documentation covers how this differs from upstream.
175#[derive(Interceptor)]
176pub struct JitterBufferInterceptor<P> {
177 #[next]
178 inner: P,
179 depth: Duration,
180 capacity: usize,
181 streams: HashMap<u32, Stream>,
182}
183
184impl<P> JitterBufferInterceptor<P> {
185 fn new(inner: P, depth: Duration, capacity: usize) -> Self {
186 Self {
187 inner,
188 depth,
189 capacity,
190 streams: HashMap::new(),
191 }
192 }
193
194 /// The playout instant of the packet nearest release on `stream`.
195 fn next_deadline(stream: &Stream) -> Option<Instant> {
196 let front = stream.buffer.front_sequence()?;
197 stream.deadlines.get(&front).copied()
198 }
199}
200
201#[interceptor]
202impl<P: Interceptor> JitterBufferInterceptor<P> {
203 #[overrides]
204 fn bind_remote_stream(&mut self, info: &StreamInfo) {
205 // Keyed by SSRC, unlike upstream, whose single buffer interleaves every remote stream
206 // into one sequence-number ordering.
207 self.streams
208 .entry(info.ssrc)
209 .or_insert_with(|| Stream::new(info.clock_rate, self.capacity));
210 self.inner.bind_remote_stream(info);
211 }
212
213 #[overrides]
214 fn unbind_remote_stream(&mut self, info: &StreamInfo) {
215 // Only this stream. Upstream's `UnbindRemoteStream` clears the shared buffer, discarding
216 // every other stream's packets along with it.
217 self.streams.remove(&info.ssrc);
218 self.inner.unbind_remote_stream(info);
219 }
220
221 #[overrides]
222 fn handle_read(&mut self, msg: TaggedPacket) -> Result<(), Self::Error> {
223 let Packet::Rtp(rtp) = &msg.message else {
224 // RTCP has no sequence number to order by and no playout deadline; it must not be
225 // delayed behind media either, since feedback is only useful while it is fresh.
226 return self.inner.handle_read(msg);
227 };
228
229 let ssrc = rtp.header.ssrc;
230 let timestamp = rtp.header.timestamp;
231 let arrived = msg.now;
232
233 let Some(stream) = self.streams.get_mut(&ssrc) else {
234 // Not a stream we were told about: pass it straight through rather than buffering
235 // packets nobody will ever come to collect.
236 return self.inner.handle_read(msg);
237 };
238
239 let extended_timestamp = stream.timestamps.extend(timestamp);
240
241 // Anchor the timeline on the first packet, or re-anchor across a discontinuity.
242 //
243 // Re-anchoring is not the same as restarting: a stream that merely ran dry keeps its
244 // ordering state, because `released_through` is what stops a straggler from the previous
245 // run being emitted behind packets that already left. Only a discontinuity — where the
246 // timeline genuinely no longer relates to the old one — wipes it.
247 match stream.anchor {
248 None => {
249 stream.anchor = Some(Anchor {
250 arrived,
251 timestamp: extended_timestamp,
252 })
253 }
254 Some(anchor) => {
255 let ticks = extended_timestamp.abs_diff(anchor.timestamp);
256 let elapsed = Duration::from_secs_f64(ticks as f64 / f64::from(stream.clock_rate));
257 if elapsed > DISCONTINUITY {
258 stream.restart(arrived, extended_timestamp);
259 }
260 }
261 }
262
263 let anchor = stream.anchor.expect("just anchored");
264 let deadline =
265 anchor.arrived + self.depth + stream.offset_from_anchor(&anchor, extended_timestamp);
266
267 match stream.buffer.push(msg) {
268 Ok(extended) => {
269 stream.deadlines.insert(extended, deadline);
270 Ok(())
271 }
272 // Dropped on purpose — a duplicate, a straggler past its position, a foreign SSRC, or
273 // the capacity cap. None of these may be forwarded: doing so would emit a packet twice
274 // or out of order, which is exactly what this interceptor exists to prevent.
275 Err(
276 Rejected::Duplicate | Rejected::Late | Rejected::Overflow | Rejected::ForeignSsrc,
277 ) => Ok(()),
278 }
279 }
280
281 #[overrides]
282 fn handle_timeout(&mut self, now: Self::Time) -> Result<(), Self::Error> {
283 // Collected first so the borrow of `self.streams` ends before `self.inner` is used.
284 let mut due: Vec<TaggedPacket> = Vec::new();
285
286 for stream in self.streams.values_mut() {
287 // The first packet's deadline is what starts playout; before that the stream is still
288 // filling, and `pop` yields nothing.
289 if stream.buffer.state() == State::Buffering
290 && Self::next_deadline(stream).is_some_and(|deadline| deadline <= now)
291 {
292 stream.buffer.begin_emitting();
293 }
294
295 while let Some(front) = stream.buffer.front_sequence() {
296 let Some(&deadline) = stream.deadlines.get(&front) else {
297 break;
298 };
299 if deadline > now {
300 break;
301 }
302 let Some(mut packet) = stream.buffer.pop() else {
303 break;
304 };
305 stream.deadlines.remove(&front);
306 // Rule 3 of the chain contract: the packet carries the instant it was released,
307 // not the instant it arrived, so nothing downstream counts this buffer's own
308 // holding time as network delay.
309 packet.now = now;
310 due.push(packet);
311 }
312
313 // Run dry: fill again before playout resumes, so the next packet is not emitted the
314 // moment it lands with no cushion behind it.
315 if stream.buffer.is_empty() && stream.buffer.state() == State::Emitting {
316 stream.buffer.begin_buffering();
317 // Timeline anchor only: ordering state survives, so a straggler from the run that
318 // just ended is still rejected rather than emitted out of order.
319 stream.anchor = None;
320 }
321 }
322
323 // Rule 2: re-injected through `inner`, not returned from a local queue, so a released
324 // packet traverses every downstream interceptor exactly as a live one would.
325 for packet in due {
326 self.inner.handle_read(packet)?;
327 }
328
329 self.inner.handle_timeout(now)
330 }
331
332 #[overrides]
333 fn poll_timeout(&mut self) -> Option<Self::Time> {
334 let mine = self.streams.values().filter_map(Self::next_deadline).min();
335
336 match (mine, self.inner.poll_timeout()) {
337 (Some(a), Some(b)) => Some(a.min(b)),
338 (a, b) => a.or(b),
339 }
340 }
341}