Skip to main content

flowcat_core/sip/
rtp.rs

1// SPDX-License-Identifier: Apache-2.0
2//
3//! Hand-rolled RTP (RFC 3550) for G.711 telephony — packetize, depacketize,
4//! and a small fixed jitter buffer.
5//!
6//! RTP is deliberately hand-rolled rather than pulled from a crate: for fixed
7//! 20 ms G.711 it is *simple and fully deterministic*, and avoids a risky media
8//! dependency (see SIP-DESIGN.md §2). The signaling stack (`rsipstack`) does not
9//! touch any of this — RTP rides a plain `tokio::net::UdpSocket` owned by
10//! [`SipTransport`](crate::sip::transport::SipTransport).
11//!
12//! ## Wire format (the 12-byte fixed header we emit/parse)
13//!
14//! ```text
15//!  0                   1                   2                   3
16//!  0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
17//! +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
18//! |V=2|P|X|  CC   |M|     PT      |       sequence number         |
19//! +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
20//! |                           timestamp                           |
21//! +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
22//! |           synchronization source (SSRC) identifier            |
23//! +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
24//! ```
25//!
26//! We always emit V=2, P=0, X=0, CC=0, M=0 (no CSRCs, no extensions, no marker
27//! bit — telephony G.711 has no use for them). On receive we parse the same
28//! fields, **skip** any CSRC list (CC>0) and a header extension (X=1) if a peer
29//! sends them, and hand back the payload. Sequence increments by 1 per packet;
30//! timestamp increments by the sample count (160 for a 20 ms G.711 frame, since
31//! the G.711 clock rate is 8000 Hz and one byte == one sample).
32
33/// RTP version 2 (the only version RFC 3550 defines / we speak).
34const RTP_VERSION: u8 = 2;
35
36/// The fixed RTP header length in bytes (no CSRCs, no extension).
37pub const RTP_HEADER_LEN: usize = 12;
38
39/// G.711 payload type for μ-law (PCMU) — static RTP/AVP assignment (RFC 3551).
40pub const PT_PCMU: u8 = 0;
41/// G.711 payload type for A-law (PCMA) — static RTP/AVP assignment (RFC 3551).
42pub const PT_PCMA: u8 = 8;
43
44/// A parsed inbound RTP packet (the fields [`SipTransport`] cares about).
45///
46/// [`crate::sip::transport::SipTransport`] uses `seq` for the jitter buffer,
47/// `payload_type` to choose μ-law vs A-law decode, and `payload` as the G.711
48/// bytes. `timestamp`/`ssrc`/`marker` are parsed for completeness (and tests).
49#[derive(Debug, Clone, PartialEq, Eq)]
50pub struct RtpPacket {
51    /// Payload type (0 = PCMU, 8 = PCMA for our G.711-only path).
52    pub payload_type: u8,
53    /// 16-bit sequence number (wraps; used to reorder/dedupe in the jitter buffer).
54    pub seq: u16,
55    /// 32-bit media timestamp (G.711: +160 per 20 ms frame).
56    pub timestamp: u32,
57    /// Synchronization source identifier.
58    pub ssrc: u32,
59    /// The RTP marker bit (M). Unused for G.711 but parsed for completeness.
60    pub marker: bool,
61    /// The codec payload bytes (G.711 μ-law / A-law samples, one byte each).
62    pub payload: Vec<u8>,
63}
64
65/// Errors from parsing a raw datagram as RTP.
66#[derive(Debug, Clone, PartialEq, Eq)]
67pub enum RtpParseError {
68    /// The datagram is shorter than the 12-byte fixed header.
69    TooShort,
70    /// The version field was not 2.
71    BadVersion(u8),
72    /// The header (incl. CSRCs / extension) claimed more bytes than the datagram has.
73    Truncated,
74}
75
76impl std::fmt::Display for RtpParseError {
77    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
78        match self {
79            RtpParseError::TooShort => write!(f, "RTP datagram shorter than 12-byte header"),
80            RtpParseError::BadVersion(v) => write!(f, "RTP version {v} != 2"),
81            RtpParseError::Truncated => write!(f, "RTP header claims more bytes than present"),
82        }
83    }
84}
85
86/// Monotonic RTP sender state: holds the SSRC and the running sequence /
87/// timestamp, advancing them per outbound packet.
88///
89/// Seeded with a random SSRC and random initial seq/ts as RFC 3550 §5.1
90/// recommends. One per outbound media leg.
91#[derive(Debug, Clone)]
92pub struct RtpSender {
93    ssrc: u32,
94    payload_type: u8,
95    seq: u16,
96    timestamp: u32,
97}
98
99impl RtpSender {
100    /// New sender for `payload_type` (0/8) with a random SSRC + random initial
101    /// seq/ts (RFC 3550 §5.1).
102    pub fn new(payload_type: u8) -> Self {
103        Self::with_seeds(
104            payload_type,
105            rand::random::<u32>(),
106            rand::random::<u16>(),
107            rand::random::<u32>(),
108        )
109    }
110
111    /// New sender with explicit seeds — used by tests for deterministic output.
112    pub fn with_seeds(payload_type: u8, ssrc: u32, seq: u16, timestamp: u32) -> Self {
113        Self {
114            ssrc,
115            payload_type,
116            seq,
117            timestamp,
118        }
119    }
120
121    /// The SSRC this sender stamps on every packet.
122    pub fn ssrc(&self) -> u32 {
123        self.ssrc
124    }
125
126    /// Packetize one G.711 frame: build the 12-byte header over `payload`, then
127    /// advance seq (+1) and timestamp (+payload length, i.e. one tick/sample).
128    ///
129    /// Returns the full datagram (header + payload) ready for `UdpSocket::send`.
130    pub fn packetize(&mut self, payload: &[u8]) -> Vec<u8> {
131        let mut pkt = Vec::with_capacity(RTP_HEADER_LEN + payload.len());
132        // Byte 0: V(2) P(1) X(1) CC(4) — V=2, everything else 0.
133        pkt.push(RTP_VERSION << 6);
134        // Byte 1: M(1) PT(7) — marker 0, payload type in low 7 bits.
135        pkt.push(self.payload_type & 0x7f);
136        // Bytes 2..4: sequence number (big-endian).
137        pkt.extend_from_slice(&self.seq.to_be_bytes());
138        // Bytes 4..8: timestamp (big-endian).
139        pkt.extend_from_slice(&self.timestamp.to_be_bytes());
140        // Bytes 8..12: SSRC (big-endian).
141        pkt.extend_from_slice(&self.ssrc.to_be_bytes());
142        pkt.extend_from_slice(payload);
143
144        // Advance for the next packet. G.711 clock == sample rate, and one G.711
145        // byte == one sample, so the timestamp step is exactly the payload len.
146        self.seq = self.seq.wrapping_add(1);
147        self.timestamp = self.timestamp.wrapping_add(payload.len() as u32);
148        pkt
149    }
150}
151
152/// Parse a raw datagram as an RTP packet, validating V=2 and skipping any
153/// CSRC list / header extension so we always return the real payload.
154pub fn depacketize(buf: &[u8]) -> Result<RtpPacket, RtpParseError> {
155    if buf.len() < RTP_HEADER_LEN {
156        return Err(RtpParseError::TooShort);
157    }
158    let b0 = buf[0];
159    let version = b0 >> 6;
160    if version != RTP_VERSION {
161        return Err(RtpParseError::BadVersion(version));
162    }
163    let has_extension = (b0 & 0x10) != 0;
164    let csrc_count = (b0 & 0x0f) as usize;
165
166    let b1 = buf[1];
167    let marker = (b1 & 0x80) != 0;
168    let payload_type = b1 & 0x7f;
169    let seq = u16::from_be_bytes([buf[2], buf[3]]);
170    let timestamp = u32::from_be_bytes([buf[4], buf[5], buf[6], buf[7]]);
171    let ssrc = u32::from_be_bytes([buf[8], buf[9], buf[10], buf[11]]);
172
173    // Skip the CSRC list (4 bytes each).
174    let mut offset = RTP_HEADER_LEN + csrc_count * 4;
175    if buf.len() < offset {
176        return Err(RtpParseError::Truncated);
177    }
178
179    // Skip a header extension if present: 4-byte (profile + length-in-32bit-words)
180    // prefix, then `length` words of extension data.
181    if has_extension {
182        if buf.len() < offset + 4 {
183            return Err(RtpParseError::Truncated);
184        }
185        let ext_words = u16::from_be_bytes([buf[offset + 2], buf[offset + 3]]) as usize;
186        offset += 4 + ext_words * 4;
187        if buf.len() < offset {
188            return Err(RtpParseError::Truncated);
189        }
190    }
191
192    Ok(RtpPacket {
193        payload_type,
194        seq,
195        timestamp,
196        ssrc,
197        marker,
198        payload: buf[offset..].to_vec(),
199    })
200}
201
202/// A tiny fixed-depth playout **jitter buffer** for inbound RTP.
203///
204/// Telephony G.711 is a constant 20 ms / 160-byte cadence, which makes reordering
205/// trivial: we hold up to [`JITTER_DEPTH`] packets sorted by sequence number, and
206/// only release a packet once the buffer is full (so a packet that arrived up to
207/// `JITTER_DEPTH - 1` slots out of order gets a chance to be reordered ahead of
208/// release).
209///
210/// Behaviour:
211/// - **In order** → released in order, one out per one in once primed.
212/// - **Reordered** within the window → re-sorted, released in sequence order.
213/// - **Duplicate** seq (already buffered, or already released this far) → dropped.
214/// - **Late** (seq below the last released, accounting for 16-bit wrap) → dropped.
215///
216/// ## Depth
217///
218/// [`JITTER_DEPTH`] = **2** frames (= 40 ms at 20 ms/frame). Tuned down from 4
219/// (80 ms) to shave mouth-to-ear latency for the real-time voice agent: a single
220/// SIP hop (agent ↔ carrier) almost never reorders by more than one packet, so a
221/// 2-deep window still corrects the common adjacent swap while halving the fixed
222/// playout delay. The buffer never grows past this; it is a bounded reorder
223/// window, not an elastic backlog. Bump back toward 4 if a lossier/multi-hop
224/// path shows audible reorder-drops.
225pub const JITTER_DEPTH: usize = 2;
226
227/// Bounded reorder buffer over RTP sequence numbers (see [`JITTER_DEPTH`]).
228#[derive(Debug)]
229pub struct JitterBuffer {
230    /// Buffered packets, kept sorted ascending by 16-bit sequence number.
231    buf: Vec<RtpPacket>,
232    /// Max packets held before we start releasing (the reorder window).
233    depth: usize,
234    /// Sequence number of the last packet released, if any. Used to drop late
235    /// and duplicate packets. `None` until the first release.
236    last_released: Option<u16>,
237    /// Whether `last_released` has ever been set (distinguishes "nothing released
238    /// yet" so the very first packets aren't treated as late).
239    primed: bool,
240}
241
242impl JitterBuffer {
243    /// A jitter buffer with the default [`JITTER_DEPTH`].
244    pub fn new() -> Self {
245        Self::with_depth(JITTER_DEPTH)
246    }
247
248    /// A jitter buffer with an explicit reorder depth (>= 1).
249    pub fn with_depth(depth: usize) -> Self {
250        Self {
251            buf: Vec::with_capacity(depth + 1),
252            depth: depth.max(1),
253            last_released: None,
254            primed: false,
255        }
256    }
257
258    /// Clear all buffered packets and playout state. Used when the RTP **SSRC
259    /// changes mid-call** — the new stream's sequence numbers are unrelated to the
260    /// old one, so without a reset the new (low) seqs would be dropped as "late"
261    /// against the stale `last_released`, causing an audible dropout. Call
262    /// [`drain`](Self::drain) first if the old stream's trailing audio must be kept.
263    pub fn reset(&mut self) {
264        self.buf.clear();
265        self.last_released = None;
266        self.primed = false;
267    }
268
269    /// Whether `seq` is "older" than `reference` on the 16-bit sequence circle.
270    ///
271    /// RFC 1982 serial-number arithmetic: treat the half-range (32768) as the
272    /// split between "before" and "after" so wrap-around (…65535 → 0) is handled.
273    fn seq_lt(seq: u16, reference: u16) -> bool {
274        // (seq - reference) interpreted as a signed 16-bit delta < 0 ⇒ seq is behind.
275        seq != reference && seq.wrapping_sub(reference) > 0x8000
276    }
277
278    /// Push one inbound packet. Returns any packets ready for playout (in
279    /// sequence order); usually 0 or 1, but a burst that overfills the window
280    /// can release more than one.
281    ///
282    /// Drops duplicates and late packets (see the type docs).
283    pub fn push(&mut self, pkt: RtpPacket) -> Vec<RtpPacket> {
284        // Drop late packets: at/below the last released sequence (incl. dup of it).
285        if self.primed {
286            let last = self.last_released.unwrap();
287            if pkt.seq == last || Self::seq_lt(pkt.seq, last) {
288                return Vec::new();
289            }
290        }
291        // Drop duplicates already sitting in the buffer.
292        if self.buf.iter().any(|p| p.seq == pkt.seq) {
293            return Vec::new();
294        }
295
296        // Insert keeping the buffer sorted ascending by seq (relative to the
297        // release point, so wrapped sequences sort correctly).
298        let pos = self
299            .buf
300            .iter()
301            .position(|p| Self::seq_lt(pkt.seq, p.seq))
302            .unwrap_or(self.buf.len());
303        self.buf.insert(pos, pkt);
304
305        // Release from the front while we're over the reorder window.
306        let mut out = Vec::new();
307        while self.buf.len() > self.depth {
308            let p = self.buf.remove(0);
309            self.last_released = Some(p.seq);
310            self.primed = true;
311            out.push(p);
312        }
313        out
314    }
315
316    /// Drain everything still buffered, in sequence order (end-of-call flush).
317    pub fn drain(&mut self) -> Vec<RtpPacket> {
318        let out: Vec<RtpPacket> = self.buf.drain(..).collect();
319        if let Some(last) = out.last() {
320            self.last_released = Some(last.seq);
321            self.primed = true;
322        }
323        out
324    }
325}
326
327impl Default for JitterBuffer {
328    fn default() -> Self {
329        Self::new()
330    }
331}
332
333#[cfg(test)]
334mod tests {
335    use super::*;
336
337    fn pkt(seq: u16, payload: &[u8]) -> RtpPacket {
338        RtpPacket {
339            payload_type: PT_PCMU,
340            seq,
341            timestamp: seq as u32 * 160,
342            ssrc: 0xDEAD_BEEF,
343            marker: false,
344            payload: payload.to_vec(),
345        }
346    }
347
348    // ---- packetize / depacketize round-trip -------------------------------
349
350    #[test]
351    fn packetize_writes_a_well_formed_12_byte_header() {
352        let mut s = RtpSender::with_seeds(PT_PCMU, 0x1122_3344, 0x0010, 0x0000_0100);
353        let payload: Vec<u8> = (0..160u16).map(|i| i as u8).collect();
354        let dgram = s.packetize(&payload);
355
356        assert_eq!(dgram.len(), RTP_HEADER_LEN + 160);
357        // V=2, P=0, X=0, CC=0.
358        assert_eq!(dgram[0], 0x80);
359        // M=0, PT=0 (PCMU).
360        assert_eq!(dgram[1], 0x00);
361        // seq, ts, ssrc big-endian.
362        assert_eq!(u16::from_be_bytes([dgram[2], dgram[3]]), 0x0010);
363        assert_eq!(
364            u32::from_be_bytes([dgram[4], dgram[5], dgram[6], dgram[7]]),
365            0x0000_0100
366        );
367        assert_eq!(
368            u32::from_be_bytes([dgram[8], dgram[9], dgram[10], dgram[11]]),
369            0x1122_3344
370        );
371        assert_eq!(&dgram[RTP_HEADER_LEN..], &payload[..]);
372    }
373
374    #[test]
375    fn packetize_advances_seq_by_one_and_ts_by_sample_count() {
376        let mut s = RtpSender::with_seeds(PT_PCMA, 7, 100, 1000);
377        let a = depacketize(&s.packetize(&[0u8; 160])).unwrap();
378        let b = depacketize(&s.packetize(&[0u8; 160])).unwrap();
379        let c = depacketize(&s.packetize(&[0u8; 80])).unwrap(); // a short (10ms) frame
380        assert_eq!((a.seq, a.timestamp), (100, 1000));
381        assert_eq!((b.seq, b.timestamp), (101, 1160)); // +1 seq, +160 ts
382        assert_eq!((c.seq, c.timestamp), (102, 1320)); // +1 seq, +160 ts
383                                                       // PT + SSRC carried through unchanged.
384        assert_eq!(a.payload_type, PT_PCMA);
385        assert_eq!(a.ssrc, 7);
386    }
387
388    #[test]
389    fn packetize_then_depacketize_preserves_all_fields_and_payload() {
390        let mut s = RtpSender::with_seeds(PT_PCMU, 0xCAFE_BABE, 65530, 4_000_000_000);
391        let payload: Vec<u8> = (0..160u32).map(|i| (i * 7) as u8).collect();
392        let dgram = s.packetize(&payload);
393        let p = depacketize(&dgram).unwrap();
394        assert_eq!(p.payload_type, PT_PCMU);
395        assert_eq!(p.seq, 65530);
396        assert_eq!(p.timestamp, 4_000_000_000);
397        assert_eq!(p.ssrc, 0xCAFE_BABE);
398        assert!(!p.marker);
399        assert_eq!(p.payload, payload);
400    }
401
402    #[test]
403    fn seq_and_timestamp_wrap_around() {
404        // seq starts at u16::MAX, ts near u32::MAX → both must wrap, not panic.
405        let mut s = RtpSender::with_seeds(PT_PCMU, 1, u16::MAX, u32::MAX - 100);
406        let a = depacketize(&s.packetize(&[0u8; 160])).unwrap();
407        let b = depacketize(&s.packetize(&[0u8; 160])).unwrap();
408        assert_eq!(a.seq, u16::MAX);
409        assert_eq!(b.seq, 0); // wrapped
410        assert_eq!(a.timestamp, u32::MAX - 100);
411        // (MAX-100) + 160 = MAX + 60; wrapping past 2^32 (= MAX+1) lands on 59.
412        assert_eq!(b.timestamp, 59);
413    }
414
415    #[test]
416    fn depacketize_rejects_short_and_bad_version() {
417        assert_eq!(depacketize(&[0u8; 11]), Err(RtpParseError::TooShort));
418        // Version 1 in the top two bits (0x40).
419        let mut bad = vec![0x40u8];
420        bad.extend_from_slice(&[0u8; 11]);
421        assert_eq!(depacketize(&bad), Err(RtpParseError::BadVersion(1)));
422    }
423
424    #[test]
425    fn depacketize_skips_csrc_list() {
426        // V=2, CC=2 → two 4-byte CSRCs before the payload.
427        let mut dgram = vec![0x82u8, PT_PCMU];
428        dgram.extend_from_slice(&5u16.to_be_bytes()); // seq
429        dgram.extend_from_slice(&0u32.to_be_bytes()); // ts
430        dgram.extend_from_slice(&0u32.to_be_bytes()); // ssrc
431        dgram.extend_from_slice(&[0u8; 8]); // 2 CSRCs
432        dgram.extend_from_slice(&[0xAA, 0xBB]); // payload
433        let p = depacketize(&dgram).unwrap();
434        assert_eq!(p.seq, 5);
435        assert_eq!(p.payload, vec![0xAA, 0xBB]);
436    }
437
438    #[test]
439    fn depacketize_skips_header_extension() {
440        // V=2, X=1 (0x90). One extension word after the 4-byte ext prefix.
441        let mut dgram = vec![0x90u8, PT_PCMU];
442        dgram.extend_from_slice(&9u16.to_be_bytes()); // seq
443        dgram.extend_from_slice(&0u32.to_be_bytes()); // ts
444        dgram.extend_from_slice(&0u32.to_be_bytes()); // ssrc
445        dgram.extend_from_slice(&0xBEEFu16.to_be_bytes()); // ext profile
446        dgram.extend_from_slice(&1u16.to_be_bytes()); // ext length = 1 word
447        dgram.extend_from_slice(&[1, 2, 3, 4]); // the one ext word
448        dgram.extend_from_slice(&[0x77]); // payload
449        let p = depacketize(&dgram).unwrap();
450        assert_eq!(p.seq, 9);
451        assert_eq!(p.payload, vec![0x77]);
452    }
453
454    // ---- jitter buffer ----------------------------------------------------
455
456    /// Helper: feed a sequence of packets, collect everything released in order.
457    fn run(jb: &mut JitterBuffer, seqs: &[u16]) -> Vec<u16> {
458        let mut out = Vec::new();
459        for &s in seqs {
460            for p in jb.push(pkt(s, &[s as u8])) {
461                out.push(p.seq);
462            }
463        }
464        out
465    }
466
467    // ── depacketize robustness (no panic on hostile/short input) ────────────────
468    #[test]
469    fn depacketize_exactly_12_bytes_is_empty_payload_no_panic() {
470        let mut d = vec![0x80u8, PT_PCMU];
471        d.extend_from_slice(&7u16.to_be_bytes());
472        d.extend_from_slice(&0u32.to_be_bytes());
473        d.extend_from_slice(&0u32.to_be_bytes());
474        let p = depacketize(&d).unwrap();
475        assert_eq!(p.seq, 7);
476        assert!(p.payload.is_empty());
477    }
478
479    #[test]
480    fn depacketize_rejects_truncated_csrc_list() {
481        // CC=2 claims 8 CSRC bytes but the datagram stops after the 12-byte header.
482        let mut d = vec![0x82u8, PT_PCMU];
483        d.extend_from_slice(&1u16.to_be_bytes());
484        d.extend_from_slice(&0u32.to_be_bytes());
485        d.extend_from_slice(&0u32.to_be_bytes());
486        assert_eq!(depacketize(&d), Err(RtpParseError::Truncated));
487    }
488
489    #[test]
490    fn depacketize_rejects_truncated_header_extension() {
491        // X=1, ext length claims 4 words but none follow.
492        let mut d = vec![0x90u8, PT_PCMU];
493        d.extend_from_slice(&2u16.to_be_bytes());
494        d.extend_from_slice(&0u32.to_be_bytes());
495        d.extend_from_slice(&0u32.to_be_bytes());
496        d.extend_from_slice(&0xBEEFu16.to_be_bytes());
497        d.extend_from_slice(&4u16.to_be_bytes());
498        assert_eq!(depacketize(&d), Err(RtpParseError::Truncated));
499    }
500
501    #[test]
502    fn depacketize_parses_marker_bit_set() {
503        let mut d = vec![0x80u8, 0x80 | PT_PCMU];
504        d.extend_from_slice(&3u16.to_be_bytes());
505        d.extend_from_slice(&0u32.to_be_bytes());
506        d.extend_from_slice(&0u32.to_be_bytes());
507        d.push(0xAA);
508        let p = depacketize(&d).unwrap();
509        assert!(p.marker, "marker bit should parse as true");
510        assert_eq!(p.payload_type, PT_PCMU);
511        assert_eq!(p.payload, vec![0xAA]);
512    }
513
514    #[test]
515    fn depacketize_does_not_panic_on_arbitrary_short_buffers() {
516        for n in 0..RTP_HEADER_LEN {
517            assert_eq!(depacketize(&vec![0xFFu8; n]), Err(RtpParseError::TooShort));
518        }
519    }
520
521    // ── sender seq (u16) + timestamp (u32) wrap independently ───────────────────
522    #[test]
523    fn sender_seq_and_ts_wrap_independently() {
524        let mut s = RtpSender::with_seeds(PT_PCMU, 1, u16::MAX - 1, 0);
525        let seqs: Vec<u16> = (0..4)
526            .map(|_| depacketize(&s.packetize(&[0u8; 160])).unwrap().seq)
527            .collect();
528        assert_eq!(seqs, vec![u16::MAX - 1, u16::MAX, 0, 1]);
529
530        let mut s2 = RtpSender::with_seeds(PT_PCMU, 1, 0, u32::MAX - 160);
531        assert_eq!(
532            depacketize(&s2.packetize(&[0u8; 160])).unwrap().timestamp,
533            u32::MAX - 160
534        );
535        assert_eq!(
536            depacketize(&s2.packetize(&[0u8; 160])).unwrap().timestamp,
537            u32::MAX
538        );
539        assert_eq!(
540            depacketize(&s2.packetize(&[0u8; 160])).unwrap().timestamp,
541            159
542        ); // wrapped
543    }
544
545    // ── jitter: depth edges + lost-packet handling ──────────────────────────────
546    #[test]
547    fn jitter_depth_one_releases_immediately_in_order() {
548        let mut jb = JitterBuffer::with_depth(1);
549        assert_eq!(run(&mut jb, &[1, 2, 3, 4]), vec![1, 2, 3]);
550        let tail: Vec<u16> = jb.drain().into_iter().map(|p| p.seq).collect();
551        assert_eq!(tail, vec![4]);
552    }
553
554    #[test]
555    fn jitter_with_depth_zero_clamps_to_one() {
556        let mut jb = JitterBuffer::with_depth(0);
557        assert_eq!(run(&mut jb, &[5, 6]), vec![5]);
558    }
559
560    #[test]
561    fn jitter_lost_packet_does_not_stall_and_late_arrival_is_dropped() {
562        // 3 is lost: 1,2,4,5,6 arrive. Window drains in order; a late 3 is dropped.
563        let mut jb = JitterBuffer::with_depth(2);
564        assert_eq!(run(&mut jb, &[1, 2, 4, 5, 6]), vec![1, 2, 4]);
565        assert_eq!(jb.push(pkt(3, &[3])), vec![], "late lost packet dropped");
566    }
567
568    // ── SSRC-change fix: reset() clears state so a fresh stream starts clean ────
569    #[test]
570    fn jitter_reset_clears_state_for_a_fresh_stream() {
571        let mut jb = JitterBuffer::with_depth(2);
572        let _ = run(&mut jb, &[40000, 40001, 40002, 40003]); // primes + leaves 40002/40003 buffered
573        jb.reset(); // what rx_loop does on an SSRC change
574                    // Buffered old-stream packets are gone and last_released is cleared, so a
575                    // brand-new stream (new SSRC) primes from scratch and releases in order —
576                    // releasing exactly the new seqs proves the old 40002/40003 didn't leak.
577        assert_eq!(run(&mut jb, &[100, 101, 102, 103]), vec![100, 101]);
578    }
579
580    #[test]
581    fn jitter_in_order_releases_in_order_after_priming() {
582        let mut jb = JitterBuffer::with_depth(4);
583        // 8 in-order packets through a depth-4 window: first 4 stay buffered,
584        // then one out per one in → seqs 1..=4 released.
585        let released = run(&mut jb, &[1, 2, 3, 4, 5, 6, 7, 8]);
586        assert_eq!(released, vec![1, 2, 3, 4]);
587        // Draining the window yields the held tail in order.
588        let tail: Vec<u16> = jb.drain().into_iter().map(|p| p.seq).collect();
589        assert_eq!(tail, vec![5, 6, 7, 8]);
590    }
591
592    #[test]
593    fn jitter_reorders_within_window() {
594        let mut jb = JitterBuffer::with_depth(4);
595        // 3 and 4 swapped on the wire; window re-sorts them before release.
596        let released = run(&mut jb, &[1, 2, 4, 3, 5, 6, 7]);
597        assert_eq!(released, vec![1, 2, 3]); // 3 emitted before 4 despite arriving later
598        let tail: Vec<u16> = jb.drain().into_iter().map(|p| p.seq).collect();
599        assert_eq!(tail, vec![4, 5, 6, 7]);
600    }
601
602    #[test]
603    fn jitter_drops_duplicates() {
604        let mut jb = JitterBuffer::with_depth(4);
605        // Duplicate 2 (still in the window) is dropped; output has no repeat.
606        let released = run(&mut jb, &[1, 2, 2, 3, 4, 5, 6]);
607        assert_eq!(released, vec![1, 2]);
608        let tail: Vec<u16> = jb.drain().into_iter().map(|p| p.seq).collect();
609        assert_eq!(tail, vec![3, 4, 5, 6]);
610    }
611
612    #[test]
613    fn jitter_drops_late_packet_below_release_point() {
614        let mut jb = JitterBuffer::with_depth(2);
615        // Prime + release up through some seqs, then a stale low seq arrives late.
616        let released = run(&mut jb, &[10, 11, 12, 13, 14]);
617        assert_eq!(released, vec![10, 11, 12]); // depth 2 → release once over 2
618                                                // 11 is now below last_released (12) → dropped, releases nothing.
619        assert_eq!(jb.push(pkt(11, &[0])), vec![]);
620        // A fresh in-window late-ish dup of an already-released seq also drops.
621        assert_eq!(jb.push(pkt(12, &[0])), vec![]);
622    }
623
624    #[test]
625    fn jitter_handles_seq_wraparound() {
626        let mut jb = JitterBuffer::with_depth(2);
627        // Sequence wraps 65534, 65535, 0, 1 — must stay in order across the wrap.
628        let released = run(&mut jb, &[65534, 65535, 0, 1, 2]);
629        assert_eq!(released, vec![65534, 65535, 0]);
630        let tail: Vec<u16> = jb.drain().into_iter().map(|p| p.seq).collect();
631        assert_eq!(tail, vec![1, 2]);
632    }
633
634    #[test]
635    fn jitter_reordered_across_wrap_is_sorted() {
636        let mut jb = JitterBuffer::with_depth(3);
637        // 0 arrives before 65535 (reordered across the wrap); window sorts them.
638        let released = run(&mut jb, &[65534, 0, 65535, 1, 2, 3]);
639        assert_eq!(released, vec![65534, 65535, 0]);
640        let tail: Vec<u16> = jb.drain().into_iter().map(|p| p.seq).collect();
641        assert_eq!(tail, vec![1, 2, 3]);
642    }
643}