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