Skip to main content

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}