Skip to main content

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}