Skip to main content

sipx_rtp/
rtcp.rs

1//! RTCP sender and receiver reports (RFC 3550 §6.4).
2//!
3//! RTP carries the media; RTCP carries what happened to it. Without reports a stack cannot say
4//! why a call sounded bad, and cannot answer a peer that asks.
5//!
6//! The field that repays attention is **interarrival jitter**. It is not a variance and not a
7//! standard deviation — it is the smoothed mean deviation of packet spacing, updated per
8//! packet by the RFC's own recurrence:
9//!
10//! ```text
11//! J += (|D(i-1, i)| - J) / 16
12//! ```
13//!
14//! An implementation that computes a variance instead produces numbers that look plausible,
15//! move in the right direction, and are wrong by a factor that depends on the traffic — which
16//! is worse than reporting nothing, because someone will tune a jitter buffer with them.
17
18use bytes::{BufMut, Bytes, BytesMut};
19
20use crate::packet::sequence_is_newer;
21
22/// RTCP packet types (RFC 3550 §12.1).
23pub const SENDER_REPORT: u8 = 200;
24/// A receiver report.
25pub const RECEIVER_REPORT: u8 = 201;
26/// A source description.
27pub const SOURCE_DESCRIPTION: u8 = 202;
28/// Goodbye.
29pub const GOODBYE: u8 = 203;
30
31/// What can go wrong reading RTCP.
32#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
33#[non_exhaustive]
34pub enum RtcpError {
35    /// Fewer bytes than a header.
36    #[error("packet is {0} bytes; an RTCP header is 4")]
37    TooShort(usize),
38    /// A version other than 2.
39    #[error("RTCP version {0}; only version 2 exists")]
40    BadVersion(u8),
41    /// The length field claims more than the packet holds.
42    #[error("the length field claims more than the packet contains")]
43    Truncated,
44}
45
46/// One report block: what one source's stream looked like from here.
47#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
48pub struct ReportBlock {
49    /// Whose stream this describes.
50    pub ssrc: u32,
51    /// Loss since the last report, as a fraction in 256ths.
52    pub fraction_lost: u8,
53    /// Packets lost since the stream began. 24-bit and signed, because duplicates can make it
54    /// go down.
55    pub cumulative_lost: i32,
56    /// The highest sequence number seen, with the wrap count in the high 16 bits.
57    pub extended_highest_sequence: u32,
58    /// Interarrival jitter, in timestamp units.
59    pub jitter: u32,
60    /// The middle 32 bits of the last sender report's NTP timestamp.
61    pub last_sender_report: u32,
62    /// How long since that report arrived, in 1/65536 second units.
63    pub delay_since_last_sender_report: u32,
64}
65
66/// A sender report: what we have sent, plus what we have received from others.
67#[derive(Debug, Clone, PartialEq, Eq)]
68pub struct SenderReport {
69    /// Our synchronisation source.
70    pub ssrc: u32,
71    /// Wallclock time, as an NTP timestamp.
72    pub ntp_timestamp: u64,
73    /// The RTP timestamp corresponding to it, which is what lets a receiver relate the two
74    /// clocks and synchronise streams.
75    pub rtp_timestamp: u32,
76    /// Packets sent since the stream began.
77    pub packet_count: u32,
78    /// Payload bytes sent, headers excluded.
79    pub octet_count: u32,
80    /// What we have received from other sources.
81    pub reports: Vec<ReportBlock>,
82}
83
84/// A receiver report: what we have received, from a participant that sends nothing.
85#[derive(Debug, Clone, PartialEq, Eq)]
86pub struct ReceiverReport {
87    /// Our synchronisation source.
88    pub ssrc: u32,
89    /// What we have received.
90    pub reports: Vec<ReportBlock>,
91}
92
93/// The SDES item type of a canonical name (RFC 3550 §6.5.1).
94pub const SDES_CNAME: u8 = 1;
95
96/// One source description item.
97#[derive(Debug, Clone, PartialEq, Eq)]
98pub struct SdesItem {
99    /// Which attribute this is; 1 is CNAME (RFC 3550 §6.5).
100    pub kind: u8,
101    /// The text, carried verbatim. UTF-8 by the RFC, but never trusted to be.
102    pub value: Bytes,
103}
104
105/// What one source says about itself.
106#[derive(Debug, Clone, PartialEq, Eq)]
107pub struct SdesChunk {
108    /// Who is being described.
109    pub ssrc: u32,
110    /// The attributes, in wire order.
111    pub items: Vec<SdesItem>,
112}
113
114/// A source description packet.
115#[derive(Debug, Clone, PartialEq, Eq)]
116pub struct Sdes {
117    /// One chunk per source.
118    pub chunks: Vec<SdesChunk>,
119}
120
121impl Sdes {
122    /// A description carrying only a canonical name.
123    ///
124    /// The one every sender needs: RFC 3550 §6.1 requires an SDES CNAME in each compound
125    /// packet, because the CNAME is what ties streams to one participant across an SSRC
126    /// change.
127    #[must_use]
128    pub fn cname(ssrc: u32, cname: &str) -> Self {
129        Self {
130            chunks: vec![SdesChunk {
131                ssrc,
132                items: vec![SdesItem {
133                    kind: SDES_CNAME,
134                    value: Bytes::copy_from_slice(cname.as_bytes()),
135                }],
136            }],
137        }
138    }
139}
140
141/// An RTCP packet.
142#[derive(Debug, Clone, PartialEq, Eq)]
143pub enum Rtcp {
144    /// A sender report.
145    Sender(SenderReport),
146    /// A receiver report.
147    Receiver(ReceiverReport),
148    /// A source description.
149    Sdes(Sdes),
150    /// Something else — goodbyes, application data.
151    ///
152    /// Kept rather than dropped: RTCP arrives as compound packets, and an element that
153    /// discards the types it does not model cannot forward one intact.
154    Other {
155        /// The packet type.
156        packet_type: u8,
157        /// The header's five-bit count, verbatim. It is load-bearing for types this crate
158        /// does not model — §6.5 reads it as the SDES chunk count, §6.6 as the BYE source
159        /// count — so re-encoding it as zero empties the packet.
160        count: u8,
161        /// Whether the padding bit was set. It decides where the payload ends, so it has
162        /// to travel with the bytes it describes.
163        padding: bool,
164        /// Its body, verbatim.
165        payload: Bytes,
166    },
167}
168
169fn put_header(out: &mut BytesMut, count: u8, packet_type: u8, body_words: u16, padding: bool) {
170    out.put_u8(0b1000_0000 | (u8::from(padding) << 5) | (count & 0x1F));
171    out.put_u8(packet_type);
172    // The length is in 32-bit words *minus one*, counting the header. Off-by-one here shifts
173    // every later packet in a compound, which is why it is written out rather than inlined.
174    out.put_u16(body_words);
175}
176
177fn put_block(out: &mut BytesMut, block: &ReportBlock) {
178    out.put_u32(block.ssrc);
179    out.put_u8(block.fraction_lost);
180    let lost = block.cumulative_lost.clamp(-0x0080_0000, 0x007F_FFFF);
181    let lost = u32::from_ne_bytes(lost.to_ne_bytes()) & 0x00FF_FFFF;
182    out.put_u8(u8::try_from((lost >> 16) & 0xFF).unwrap_or(0));
183    out.put_u16(u16::try_from(lost & 0xFFFF).unwrap_or(0));
184    out.put_u32(block.extended_highest_sequence);
185    out.put_u32(block.jitter);
186    out.put_u32(block.last_sender_report);
187    out.put_u32(block.delay_since_last_sender_report);
188}
189
190fn read_block(bytes: &[u8], at: usize) -> Option<ReportBlock> {
191    let slice = bytes.get(at..at + 24)?;
192    // Byte 4 is the fraction; the cumulative count is bytes 5 to 7. Reading from 4 folds the
193    // fraction into the high byte of the loss, which turns 42 lost into 1.7 million.
194    let lost_raw = u32::from(*slice.get(5)?) << 16
195        | u32::from(*slice.get(6)?) << 8
196        | u32::from(*slice.get(7)?);
197    // 24-bit two's complement, sign-extended.
198    let cumulative_lost = if lost_raw & 0x0080_0000 == 0 {
199        i32::try_from(lost_raw).unwrap_or(0)
200    } else {
201        i32::try_from(lost_raw).unwrap_or(0) - 0x0100_0000
202    };
203    Some(ReportBlock {
204        ssrc: u32::from_be_bytes(slice.get(0..4)?.try_into().ok()?),
205        fraction_lost: *slice.get(4)?,
206        cumulative_lost,
207        extended_highest_sequence: u32::from_be_bytes(slice.get(8..12)?.try_into().ok()?),
208        jitter: u32::from_be_bytes(slice.get(12..16)?.try_into().ok()?),
209        last_sender_report: u32::from_be_bytes(slice.get(16..20)?.try_into().ok()?),
210        delay_since_last_sender_report: u32::from_be_bytes(slice.get(20..24)?.try_into().ok()?),
211    })
212}
213
214impl Rtcp {
215    /// Serialize one packet.
216    #[must_use]
217    pub fn encode(&self) -> Bytes {
218        let mut out = BytesMut::with_capacity(64);
219        match self {
220            Self::Sender(report) => {
221                let count = u8::try_from(report.reports.len().min(31)).unwrap_or(0);
222                let words = 6 + u16::from(count) * 6;
223                put_header(&mut out, count, SENDER_REPORT, words, false);
224                out.put_u32(report.ssrc);
225                out.put_u64(report.ntp_timestamp);
226                out.put_u32(report.rtp_timestamp);
227                out.put_u32(report.packet_count);
228                out.put_u32(report.octet_count);
229                for block in report.reports.iter().take(31) {
230                    put_block(&mut out, block);
231                }
232            }
233            Self::Receiver(report) => {
234                let count = u8::try_from(report.reports.len().min(31)).unwrap_or(0);
235                let words = 1 + u16::from(count) * 6;
236                put_header(&mut out, count, RECEIVER_REPORT, words, false);
237                out.put_u32(report.ssrc);
238                for block in report.reports.iter().take(31) {
239                    put_block(&mut out, block);
240                }
241            }
242            Self::Sdes(sdes) => {
243                let count = u8::try_from(sdes.chunks.len().min(31)).unwrap_or(0);
244                let mut body = BytesMut::with_capacity(32);
245                for chunk in sdes.chunks.iter().take(31) {
246                    body.put_u32(chunk.ssrc);
247                    for item in &chunk.items {
248                        // The item's length field is one byte, so longer text cannot be
249                        // represented; truncating beats letting it swallow the next item.
250                        let text = item.value.get(..item.value.len().min(255)).unwrap_or(&[]);
251                        body.put_u8(item.kind);
252                        body.put_u8(u8::try_from(text.len()).unwrap_or(255));
253                        body.put_slice(text);
254                    }
255                    // The item list ends with a null octet, and the chunk is padded to the
256                    // next 32-bit boundary with more of them (RFC 3550 §6.5).
257                    body.put_u8(0);
258                    while !body.len().is_multiple_of(4) {
259                        body.put_u8(0);
260                    }
261                }
262                let words = u16::try_from(body.len() / 4).unwrap_or(0);
263                put_header(&mut out, count, SOURCE_DESCRIPTION, words, false);
264                out.put_slice(&body);
265            }
266            Self::Other {
267                packet_type,
268                count,
269                padding,
270                payload,
271            } => {
272                // The length field counts whole 32-bit words, so an unaligned payload is
273                // zero-filled up to the boundary — a length word that disagrees with the
274                // bytes written desynchronises every later packet in the compound.
275                let padded = payload.len().div_ceil(4) * 4;
276                let words = u16::try_from(padded / 4).unwrap_or(0);
277                put_header(&mut out, *count, *packet_type, words, *padding);
278                out.put_slice(payload);
279                out.put_bytes(0, padded - payload.len());
280            }
281        }
282        out.freeze()
283    }
284
285    /// Serialize several packets as one datagram.
286    ///
287    /// RFC 3550 §6.1: every RTCP packet is sent in a compound of at least two, led by a
288    /// report and carrying an SDES CNAME. The parts are plain concatenation — this exists
289    /// so a caller sends one datagram rather than one per part.
290    #[must_use]
291    pub fn encode_compound(packets: &[Self]) -> Bytes {
292        let mut out = BytesMut::with_capacity(128);
293        for packet in packets {
294            out.put_slice(&packet.encode());
295        }
296        out.freeze()
297    }
298
299    /// Parse a compound packet into its parts.
300    ///
301    /// RTCP is sent compound — a report followed by a source description, usually — and a
302    /// parser that reads only the first packet sees a fraction of what arrived.
303    pub fn decode_compound(bytes: &Bytes) -> Result<Vec<Self>, RtcpError> {
304        let mut packets = Vec::new();
305        let mut offset = 0usize;
306
307        while offset + 4 <= bytes.len() {
308            let first = bytes.get(offset).copied().unwrap_or(0);
309            let version = first >> 6;
310            if version != 2 {
311                return Err(RtcpError::BadVersion(version));
312            }
313            let count = usize::from(first & 0x1F);
314            let padding = first & 0b0010_0000 != 0;
315            let packet_type = bytes.get(offset + 1).copied().unwrap_or(0);
316            let words = usize::from(u16::from_be_bytes([
317                bytes.get(offset + 2).copied().ok_or(RtcpError::Truncated)?,
318                bytes.get(offset + 3).copied().ok_or(RtcpError::Truncated)?,
319            ]));
320            // Length is words-minus-one and excludes the first word, so the whole packet is
321            // (words + 1) * 4 bytes.
322            let total = (words + 1) * 4;
323            let body = bytes
324                .get(offset + 4..offset + total)
325                .ok_or(RtcpError::Truncated)?;
326
327            packets.push(Self::decode_one(
328                packet_type,
329                count,
330                padding,
331                body,
332                bytes,
333                offset,
334            )?);
335            offset += total;
336        }
337
338        if packets.is_empty() {
339            return Err(RtcpError::TooShort(bytes.len()));
340        }
341        Ok(packets)
342    }
343
344    fn decode_one(
345        packet_type: u8,
346        count: usize,
347        padding: bool,
348        body: &[u8],
349        whole: &Bytes,
350        offset: usize,
351    ) -> Result<Self, RtcpError> {
352        match packet_type {
353            SENDER_REPORT => {
354                let ssrc = u32::from_be_bytes(
355                    body.get(0..4)
356                        .and_then(|s| s.try_into().ok())
357                        .ok_or(RtcpError::Truncated)?,
358                );
359                let ntp = u64::from_be_bytes(
360                    body.get(4..12)
361                        .and_then(|s| s.try_into().ok())
362                        .ok_or(RtcpError::Truncated)?,
363                );
364                let rtp = u32::from_be_bytes(
365                    body.get(12..16)
366                        .and_then(|s| s.try_into().ok())
367                        .ok_or(RtcpError::Truncated)?,
368                );
369                let packets = u32::from_be_bytes(
370                    body.get(16..20)
371                        .and_then(|s| s.try_into().ok())
372                        .ok_or(RtcpError::Truncated)?,
373                );
374                let octets = u32::from_be_bytes(
375                    body.get(20..24)
376                        .and_then(|s| s.try_into().ok())
377                        .ok_or(RtcpError::Truncated)?,
378                );
379                let mut reports = Vec::with_capacity(count);
380                for index in 0..count {
381                    reports.push(read_block(body, 24 + index * 24).ok_or(RtcpError::Truncated)?);
382                }
383                Ok(Self::Sender(SenderReport {
384                    ssrc,
385                    ntp_timestamp: ntp,
386                    rtp_timestamp: rtp,
387                    packet_count: packets,
388                    octet_count: octets,
389                    reports,
390                }))
391            }
392            RECEIVER_REPORT => {
393                let ssrc = u32::from_be_bytes(
394                    body.get(0..4)
395                        .and_then(|s| s.try_into().ok())
396                        .ok_or(RtcpError::Truncated)?,
397                );
398                let mut reports = Vec::with_capacity(count);
399                for index in 0..count {
400                    reports.push(read_block(body, 4 + index * 24).ok_or(RtcpError::Truncated)?);
401                }
402                Ok(Self::Receiver(ReceiverReport { ssrc, reports }))
403            }
404            SOURCE_DESCRIPTION => {
405                let mut chunks = Vec::with_capacity(count);
406                let mut at = 0usize;
407                for _ in 0..count {
408                    let ssrc = u32::from_be_bytes(
409                        body.get(at..at + 4)
410                            .and_then(|s| s.try_into().ok())
411                            .ok_or(RtcpError::Truncated)?,
412                    );
413                    at += 4;
414                    let mut items = Vec::new();
415                    loop {
416                        let kind = *body.get(at).ok_or(RtcpError::Truncated)?;
417                        at += 1;
418                        if kind == 0 {
419                            // The terminator is followed by padding to the next 32-bit
420                            // boundary, which belongs to this chunk — reading it as the
421                            // next chunk's SSRC shreds every source after the first.
422                            at = at.next_multiple_of(4);
423                            break;
424                        }
425                        let length = usize::from(*body.get(at).ok_or(RtcpError::Truncated)?);
426                        at += 1;
427                        let value = body.get(at..at + length).ok_or(RtcpError::Truncated)?;
428                        items.push(SdesItem {
429                            kind,
430                            value: Bytes::copy_from_slice(value),
431                        });
432                        at += length;
433                    }
434                    chunks.push(SdesChunk { ssrc, items });
435                }
436                Ok(Self::Sdes(Sdes { chunks }))
437            }
438            other => Ok(Self::Other {
439                packet_type: other,
440                count: u8::try_from(count).unwrap_or(0),
441                padding,
442                payload: whole.slice(offset + 4..offset + 4 + body.len()),
443            }),
444        }
445    }
446}
447
448/// Tracks what a stream looked like from here, so a report can be produced.
449///
450/// The counters are the interesting part. Loss is *inferred* — nothing announces a lost packet
451/// — from the difference between how many sequence numbers went by and how many packets
452/// arrived. That is why duplicates can make cumulative loss negative, and why the field is
453/// signed.
454#[derive(Debug)]
455pub struct StreamStats {
456    ssrc: u32,
457    /// The first sequence number seen, to count from.
458    base_sequence: Option<u16>,
459    /// Wrap count in the high 16 bits.
460    cycles: u32,
461    highest_sequence: u16,
462    received: u64,
463    /// What `received` was at the last report, so a fraction can be computed.
464    received_at_last_report: u64,
465    expected_at_last_report: u64,
466    /// The smoothed interarrival jitter estimate.
467    jitter: f64,
468    /// The last packet's transit time, for the difference the estimate is built from.
469    /// Modular 32-bit, like the clocks it is derived from.
470    last_transit: Option<u32>,
471}
472
473impl StreamStats {
474    /// Statistics for one source.
475    #[must_use]
476    pub fn new(ssrc: u32) -> Self {
477        Self {
478            ssrc,
479            base_sequence: None,
480            cycles: 0,
481            highest_sequence: 0,
482            received: 0,
483            received_at_last_report: 0,
484            expected_at_last_report: 0,
485            jitter: 0.0,
486            last_transit: None,
487        }
488    }
489
490    /// Name the source these statistics describe.
491    ///
492    /// The far end chooses its synchronisation source at random (RFC 3550 §8) and announces
493    /// it only in its first packet, so the statistics can exist before the name does. The
494    /// name goes into every report block: a block that says SSRC 0 describes nobody.
495    pub fn set_ssrc(&mut self, ssrc: u32) {
496        self.ssrc = ssrc;
497    }
498
499    /// Record an arrival.
500    ///
501    /// `arrival` is the local clock in the same units as the RTP timestamp — for G.711, 8000
502    /// per second. Mixing units here is the other way to make jitter meaningless.
503    pub fn on_packet(&mut self, sequence: u16, rtp_timestamp: u32, arrival: u32) {
504        self.record(sequence);
505
506        // RFC 3550 §A.8. The transit time is the difference between the local clock and the
507        // sender's; its *change* between packets is what jitter measures, so a constant offset
508        // between the two clocks cancels and never appears in the estimate. Both differences
509        // stay in 32-bit modular arithmetic: the timestamp starts at a random value (§5.1),
510        // so either clock wrapping mid-call is ordinary, and widening the subtraction turns
511        // each wrap into 2^32/16 of phantom jitter.
512        let transit = arrival.wrapping_sub(rtp_timestamp);
513        if let Some(previous) = self.last_transit {
514            let difference = transit.wrapping_sub(previous).cast_signed().unsigned_abs();
515            // The RFC's recurrence, not a variance: J += (|D| - J) / 16.
516            let d = f64::from(difference);
517            self.jitter += (d - self.jitter) / 16.0;
518        }
519        self.last_transit = Some(transit);
520    }
521
522    /// Record an arrival whose timestamp does not track its sampling instant.
523    ///
524    /// Telephone events are the case in hand: RFC 4733 §2.5.1.2 gives every packet of one
525    /// event the timestamp of the event's *start* while the packets go out one interval
526    /// apart, so their transit grows per packet by design. RFC 3550 §6.4.1 defines jitter
527    /// over packets whose timestamps track sampling instants — these still count for loss
528    /// and sequence continuity, but must not feed the jitter estimate.
529    pub fn on_untimed_packet(&mut self, sequence: u16) {
530        self.record(sequence);
531    }
532
533    fn record(&mut self, sequence: u16) {
534        self.received += 1;
535
536        match self.base_sequence {
537            None => {
538                self.base_sequence = Some(sequence);
539                self.highest_sequence = sequence;
540            }
541            Some(_) => {
542                if sequence_is_newer(sequence, self.highest_sequence) {
543                    if sequence < self.highest_sequence {
544                        self.cycles = self.cycles.wrapping_add(1);
545                    }
546                    self.highest_sequence = sequence;
547                }
548            }
549        }
550    }
551
552    /// The highest sequence number seen, with its wrap count.
553    #[must_use]
554    pub fn extended_highest_sequence(&self) -> u32 {
555        (self.cycles << 16) | u32::from(self.highest_sequence)
556    }
557
558    /// How many packets should have arrived.
559    #[must_use]
560    pub fn expected(&self) -> u64 {
561        let Some(base) = self.base_sequence else {
562            return 0;
563        };
564        u64::from(self.extended_highest_sequence()).saturating_sub(u64::from(base)) + 1
565    }
566
567    /// How many never did. Signed, because duplicates can make it negative.
568    #[must_use]
569    pub fn cumulative_lost(&self) -> i64 {
570        i64::try_from(self.expected()).unwrap_or(0) - i64::try_from(self.received).unwrap_or(0)
571    }
572
573    /// The current jitter estimate, in timestamp units.
574    #[must_use]
575    pub fn jitter(&self) -> u32 {
576        #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
577        let jitter = self.jitter as u32;
578        jitter
579    }
580
581    /// The block a report would carry right now, leaving the interval open.
582    ///
583    /// This is the one to read to *look* at the numbers. The interval belongs to the reports
584    /// actually sent — RFC 3550 §6.4.1 defines `fraction_lost` as loss since the previous SR or
585    /// RR *packet*, not since somebody last enquired — so closing it is [`Self::report_block`]'s
586    /// job and reading is free. `M-33`: while these were the same function, an application
587    /// polling for a live display closed windows nobody was told about, and the next real report
588    /// described only what had arrived since the display was drawn.
589    #[must_use]
590    pub fn pending_report_block(&self) -> ReportBlock {
591        let expected_interval = self.expected().saturating_sub(self.expected_at_last_report);
592        let received_interval = self.received.saturating_sub(self.received_at_last_report);
593        let lost_interval = i64::try_from(expected_interval).unwrap_or(0)
594            - i64::try_from(received_interval).unwrap_or(0);
595
596        let fraction = if expected_interval == 0 || lost_interval <= 0 {
597            0
598        } else {
599            let scaled = (lost_interval * 256) / i64::try_from(expected_interval).unwrap_or(1);
600            u8::try_from(scaled.clamp(0, 255)).unwrap_or(0)
601        };
602
603        ReportBlock {
604            ssrc: self.ssrc,
605            fraction_lost: fraction,
606            cumulative_lost: i32::try_from(self.cumulative_lost()).unwrap_or(0),
607            extended_highest_sequence: self.extended_highest_sequence(),
608            jitter: self.jitter(),
609            last_sender_report: 0,
610            delay_since_last_sender_report: 0,
611        }
612    }
613
614    /// Produce a report block **to send**, closing the interval and starting the next.
615    ///
616    /// The fraction is loss *since the last report*, not since the stream began — a call that
617    /// lost heavily at the start and is now clean must report clean, or nobody can see it
618    /// recover. That is what the `&mut` is: only the path that puts the block on the wire may
619    /// call this, because a caller that only wants to read the numbers and closes an interval
620    /// anyway hides that interval's loss from the far end for good. Read with
621    /// [`Self::pending_report_block`] instead.
622    pub fn report_block(&mut self) -> ReportBlock {
623        let block = self.pending_report_block();
624        self.expected_at_last_report = self.expected();
625        self.received_at_last_report = self.received;
626        block
627    }
628}
629
630#[cfg(test)]
631#[allow(
632    clippy::unwrap_used,
633    clippy::expect_used,
634    clippy::panic,
635    clippy::indexing_slicing
636)]
637mod tests {
638    use super::*;
639
640    fn block() -> ReportBlock {
641        ReportBlock {
642            ssrc: 0x1234_5678,
643            fraction_lost: 26,
644            cumulative_lost: 42,
645            extended_highest_sequence: 0x0001_0064,
646            jitter: 17,
647            last_sender_report: 0xAABB_CCDD,
648            delay_since_last_sender_report: 65_536,
649        }
650    }
651
652    #[test]
653    fn a_receiver_report_round_trips() {
654        let report = Rtcp::Receiver(ReceiverReport {
655            ssrc: 0xDEAD_BEEF,
656            reports: vec![block()],
657        });
658        let decoded = Rtcp::decode_compound(&report.encode()).expect("decodes");
659        assert_eq!(decoded, vec![report]);
660    }
661
662    #[test]
663    fn a_sender_report_round_trips() {
664        let report = Rtcp::Sender(SenderReport {
665            ssrc: 0xCAFE_BABE,
666            ntp_timestamp: 0x0123_4567_89AB_CDEF,
667            rtp_timestamp: 160_000,
668            packet_count: 500,
669            octet_count: 80_000,
670            reports: vec![block(), block()],
671        });
672        let decoded = Rtcp::decode_compound(&report.encode()).expect("decodes");
673        assert_eq!(decoded, vec![report]);
674    }
675
676    /// Cumulative loss is 24-bit and signed, because duplicates can make it negative. A parser
677    /// that reads it unsigned turns "we received three extra" into eight million lost.
678    #[test]
679    fn negative_cumulative_loss_survives_the_round_trip() {
680        let report = Rtcp::Receiver(ReceiverReport {
681            ssrc: 1,
682            reports: vec![ReportBlock {
683                cumulative_lost: -3,
684                ..block()
685            }],
686        });
687        let decoded = Rtcp::decode_compound(&report.encode()).expect("decodes");
688        match &decoded[0] {
689            Rtcp::Receiver(receiver) => assert_eq!(receiver.reports[0].cumulative_lost, -3),
690            other => panic!("expected a receiver report, got {other:?}"),
691        }
692    }
693
694    /// RTCP arrives compound. A parser that reads only the first packet sees a fraction of
695    /// what arrived.
696    #[test]
697    fn a_compound_packet_is_read_as_a_whole() {
698        let mut bytes = BytesMut::new();
699        bytes.put_slice(
700            &Rtcp::Sender(SenderReport {
701                ssrc: 1,
702                ntp_timestamp: 0,
703                rtp_timestamp: 0,
704                packet_count: 1,
705                octet_count: 160,
706                reports: vec![],
707            })
708            .encode(),
709        );
710        bytes.put_slice(&Rtcp::Sdes(Sdes::cname(1, "user@host")).encode());
711
712        let decoded = Rtcp::decode_compound(&bytes.freeze()).expect("decodes");
713        assert_eq!(decoded.len(), 2);
714        assert!(matches!(decoded[0], Rtcp::Sender(_)));
715        assert!(matches!(decoded[1], Rtcp::Sdes(_)));
716    }
717
718    /// RFC 3550 §6.5: a chunk is the SSRC, its items, a null terminator, and padding to
719    /// the next 32-bit boundary. The padding belongs to the chunk — reading it as the next
720    /// chunk's SSRC shreds every source after the first.
721    #[test]
722    fn a_source_description_round_trips_with_padding() {
723        let sdes = Rtcp::Sdes(Sdes {
724            chunks: vec![
725                SdesChunk {
726                    ssrc: 0x1111_2222,
727                    // Three bytes of text on a two-byte item header: the chunk needs two
728                    // padding octets beyond the terminator.
729                    items: vec![SdesItem {
730                        kind: SDES_CNAME,
731                        value: Bytes::from_static(b"a@b"),
732                    }],
733                },
734                SdesChunk {
735                    ssrc: 0x3333_4444,
736                    items: vec![SdesItem {
737                        kind: SDES_CNAME,
738                        value: Bytes::from_static(b"user@host.example"),
739                    }],
740                },
741            ],
742        });
743        let encoded = sdes.encode();
744        assert_eq!(encoded.len() % 4, 0, "RTCP packets are whole words");
745        let decoded = Rtcp::decode_compound(&encoded).expect("decodes");
746        assert_eq!(decoded, vec![sdes]);
747    }
748
749    /// The compound helper is plain concatenation, so what it emits parses back to its
750    /// parts in order.
751    #[test]
752    fn an_encoded_compound_decodes_to_its_parts() {
753        let report = Rtcp::Receiver(ReceiverReport {
754            ssrc: 9,
755            reports: vec![block()],
756        });
757        let sdes = Rtcp::Sdes(Sdes::cname(9, "token@203.0.113.7"));
758        let datagram = Rtcp::encode_compound(&[report.clone(), sdes.clone()]);
759        let decoded = Rtcp::decode_compound(&datagram).expect("decodes");
760        assert_eq!(decoded, vec![report, sdes]);
761    }
762
763    /// The count field is load-bearing — §6.5 reads it as the SDES chunk count and §6.6 as
764    /// the BYE source count — and the padding bit changes where the payload ends. The
765    /// variant promises a packet it does not model can be forwarded intact, so both must
766    /// survive the round trip.
767    #[test]
768    fn a_forwarded_packet_keeps_its_count_and_padding_bit() {
769        // A goodbye naming one source, with the padding bit set and four bytes of padding.
770        let mut raw = BytesMut::new();
771        raw.put_u8(0b1010_0001);
772        raw.put_u8(GOODBYE);
773        raw.put_u16(2);
774        raw.put_u32(0xDEAD_BEEF);
775        raw.put_slice(&[0, 0, 0, 4]);
776        let raw = raw.freeze();
777
778        let decoded = Rtcp::decode_compound(&raw).expect("decodes");
779        assert_eq!(decoded.len(), 1);
780        assert_eq!(decoded[0].encode(), raw, "forwarded byte for byte");
781    }
782
783    /// The length field counts whole 32-bit words. A hand-built payload that is not a
784    /// multiple of four must not produce a length word that disagrees with the bytes
785    /// written, or every packet after it in the compound is misread.
786    #[test]
787    fn an_unaligned_forwarded_payload_cannot_desynchronise_a_compound() {
788        let odd = Rtcp::Other {
789            packet_type: 204,
790            count: 0,
791            padding: false,
792            payload: Bytes::from_static(&[1, 2, 3]),
793        };
794        let mut compound = BytesMut::from(&odd.encode()[..]);
795        compound.put_slice(
796            &Rtcp::Receiver(ReceiverReport {
797                ssrc: 7,
798                reports: vec![],
799            })
800            .encode(),
801        );
802
803        let decoded = Rtcp::decode_compound(&compound.freeze()).expect("decodes");
804        assert_eq!(decoded.len(), 2);
805        assert!(
806            matches!(&decoded[1], Rtcp::Receiver(report) if report.ssrc == 7),
807            "the packet after the odd one still parses"
808        );
809    }
810
811    /// A receiver report arriving alone is legal and must be accepted.
812    #[test]
813    fn a_lone_receiver_report_is_accepted() {
814        let report = Rtcp::Receiver(ReceiverReport {
815            ssrc: 7,
816            reports: vec![],
817        });
818        assert_eq!(
819            Rtcp::decode_compound(&report.encode())
820                .expect("decodes")
821                .len(),
822            1
823        );
824    }
825
826    #[test]
827    fn a_wrong_version_is_rejected() {
828        let mut bytes = BytesMut::from(&[0u8; 8][..]);
829        bytes[0] = 0b0100_0000;
830        assert!(matches!(
831            Rtcp::decode_compound(&bytes.freeze()),
832            Err(RtcpError::BadVersion(1))
833        ));
834    }
835
836    #[test]
837    fn a_truncated_packet_is_rejected() {
838        let full = Rtcp::Receiver(ReceiverReport {
839            ssrc: 1,
840            reports: vec![block()],
841        })
842        .encode();
843        let truncated = full.slice(..full.len() - 8);
844        assert!(matches!(
845            Rtcp::decode_compound(&truncated),
846            Err(RtcpError::Truncated)
847        ));
848    }
849
850    /// The acceptance test for M-6: the numbers a report carries are the ones the receive path
851    /// actually saw.
852    #[test]
853    fn a_receiver_report_counts_the_loss_the_buffer_saw() {
854        let mut stats = StreamStats::new(99);
855
856        // Ten packets, of which 3 and 7 never arrive.
857        let mut arrival = 0u32;
858        for sequence in 1u16..=10 {
859            arrival += 160;
860            if sequence == 3 || sequence == 7 {
861                continue;
862            }
863            stats.on_packet(sequence, u32::from(sequence) * 160, arrival);
864        }
865
866        assert_eq!(stats.expected(), 10, "sequence 1 through 10");
867        assert_eq!(stats.cumulative_lost(), 2, "two never arrived");
868
869        let block = stats.report_block();
870        assert_eq!(block.cumulative_lost, 2);
871        assert_eq!(block.ssrc, 99);
872        // Two lost out of ten expected is a fifth, which in 256ths is 51.
873        assert_eq!(block.fraction_lost, 51, "loss as a fraction in 256ths");
874    }
875
876    /// The fraction is loss *since the last report*. A call that lost heavily at the start and
877    /// is now clean must report clean, or nobody can see it recover.
878    #[test]
879    fn the_fraction_covers_the_interval_not_the_whole_call() {
880        let mut stats = StreamStats::new(1);
881        let mut arrival = 0u32;
882
883        // A bad first interval: half the packets lost. It ends on a packet that *did* arrive,
884        // so the interval boundary is not itself a gap — a loss straddling the boundary is
885        // attributed to the interval in which it became known, which is correct and would
886        // muddy what this test is about.
887        for sequence in 1u16..=9 {
888            arrival += 160;
889            if sequence % 2 == 0 {
890                continue;
891            }
892            stats.on_packet(sequence, u32::from(sequence) * 160, arrival);
893        }
894        let first = stats.report_block();
895        assert!(
896            first.fraction_lost > 100,
897            "half lost: {}",
898            first.fraction_lost
899        );
900
901        // A clean second interval.
902        for sequence in 10u16..=20 {
903            arrival += 160;
904            stats.on_packet(sequence, u32::from(sequence) * 160, arrival);
905        }
906        let second = stats.report_block();
907        assert_eq!(second.fraction_lost, 0, "the interval was clean");
908        assert_eq!(
909            second.cumulative_lost, 4,
910            "but the cumulative count still remembers the four lost earlier"
911        );
912    }
913
914    /// Which of the two readers closes the interval, asserted rather than described (`M-33`).
915    ///
916    /// The interval belongs to the reports that were sent: `pending_report_block` may be called
917    /// any number of times and describe the same window each time, and only `report_block` moves
918    /// the boundary. A single function that did both let a caller reading the numbers empty the
919    /// window the next report was going to describe.
920    #[test]
921    fn only_sending_a_report_closes_the_interval() {
922        let mut stats = StreamStats::new(7);
923        let mut arrival = 0u32;
924        for sequence in 1u16..=10 {
925            arrival += 160;
926            if sequence == 4 || sequence == 8 {
927                continue;
928            }
929            stats.on_packet(sequence, u32::from(sequence) * 160, arrival);
930        }
931
932        // Two of the ten expected, in 256ths — and the same answer however often it is asked.
933        let first = stats.pending_report_block();
934        assert_eq!(first.fraction_lost, 51, "{first:?}");
935        assert_eq!(
936            stats.pending_report_block(),
937            first,
938            "reading is not a side effect"
939        );
940        assert_eq!(stats.report_block(), first, "and describes what is sent");
941
942        // Now the interval has moved: nothing has arrived since, so there is nothing to report.
943        let after = stats.pending_report_block();
944        assert_eq!(
945            after.fraction_lost, 0,
946            "a closed interval is empty: {after:?}"
947        );
948        assert_eq!(
949            after.cumulative_lost, 2,
950            "while the cumulative count spans the stream: {after:?}"
951        );
952    }
953
954    /// Duplicates can make cumulative loss negative, which is why the field is signed.
955    #[test]
956    fn duplicates_can_drive_cumulative_loss_negative() {
957        let mut stats = StreamStats::new(1);
958        let mut arrival = 0u32;
959        for sequence in 1u16..=5 {
960            arrival += 160;
961            stats.on_packet(sequence, u32::from(sequence) * 160, arrival);
962            // Every packet arrives twice.
963            stats.on_packet(sequence, u32::from(sequence) * 160, arrival);
964        }
965        assert_eq!(stats.expected(), 5);
966        assert_eq!(
967            stats.cumulative_lost(),
968            -5,
969            "ten arrived where five were due"
970        );
971    }
972
973    /// Evenly spaced packets have no jitter at all — a constant offset between the two clocks
974    /// cancels, which is the property that makes the estimate meaningful.
975    #[test]
976    fn perfectly_spaced_packets_report_no_jitter() {
977        let mut stats = StreamStats::new(1);
978        for sequence in 1u16..=50 {
979            let timestamp = u32::from(sequence) * 160;
980            // Arrival offset by a large constant: it must not appear in the estimate.
981            stats.on_packet(sequence, timestamp, timestamp + 100_000);
982        }
983        assert_eq!(stats.jitter(), 0, "even spacing is zero jitter");
984    }
985
986    /// And uneven spacing produces a positive estimate that grows with the unevenness.
987    #[test]
988    fn uneven_arrival_produces_jitter() {
989        let mut jittery = StreamStats::new(1);
990        let mut arrival = 0u32;
991        for sequence in 1u16..=50 {
992            // Alternating early and late by 80 timestamp units.
993            arrival += if sequence % 2 == 0 { 240 } else { 80 };
994            jittery.on_packet(sequence, u32::from(sequence) * 160, arrival);
995        }
996        assert!(jittery.jitter() > 0, "uneven arrival must show as jitter");
997
998        let mut worse = StreamStats::new(1);
999        let mut arrival = 0u32;
1000        for sequence in 1u16..=50 {
1001            arrival += if sequence % 2 == 0 { 480 } else { 20 };
1002            worse.on_packet(sequence, u32::from(sequence) * 160, arrival);
1003        }
1004        assert!(
1005            worse.jitter() > jittery.jitter(),
1006            "more unevenness, more jitter: {} vs {}",
1007            worse.jitter(),
1008            jittery.jitter()
1009        );
1010    }
1011
1012    /// Telephone events share one timestamp across a run of packets (RFC 4733 §2.5.1.2),
1013    /// so their transit grows per packet by design and must not feed the estimate — while
1014    /// their sequence numbers still count for loss.
1015    #[test]
1016    fn untimed_packets_count_for_loss_but_not_for_jitter() {
1017        let mut stats = StreamStats::new(1);
1018        // Clean, evenly spaced audio...
1019        for sequence in 1u16..=5 {
1020            let timestamp = u32::from(sequence) * 160;
1021            stats.on_packet(sequence, timestamp, timestamp + 4000);
1022        }
1023        // ...then a keypress, with one of its packets lost in flight...
1024        for sequence in 6u16..=10 {
1025            if sequence == 7 {
1026                continue;
1027            }
1028            stats.on_untimed_packet(sequence);
1029        }
1030        // ...and the audio resumes, still evenly spaced.
1031        for sequence in 11u16..=15 {
1032            let timestamp = u32::from(sequence) * 160;
1033            stats.on_packet(sequence, timestamp, timestamp + 4000);
1034        }
1035
1036        assert_eq!(stats.jitter(), 0, "the keypress fabricated no jitter");
1037        assert_eq!(stats.expected(), 15);
1038        assert_eq!(stats.cumulative_lost(), 1, "its lost packet still counts");
1039    }
1040
1041    /// The transit is computed modulo 2^32, so a wrap of either clock cancels. Senders
1042    /// start the timestamp at a random value (RFC 3550 §5.1), so a mid-call wrap is an
1043    /// ordinary event — and non-modular arithmetic turns it into 2^32/16 of phantom jitter.
1044    #[test]
1045    fn a_timestamp_wrap_does_not_register_as_jitter() {
1046        // The sender's clock wraps one packet in; arrival stays low.
1047        let mut stats = StreamStats::new(1);
1048        let mut timestamp = 0xFFFF_FF60u32;
1049        let mut arrival = 1000u32;
1050        for sequence in 1u16..=10 {
1051            stats.on_packet(sequence, timestamp, arrival);
1052            timestamp = timestamp.wrapping_add(160);
1053            arrival = arrival.wrapping_add(160);
1054        }
1055        assert_eq!(stats.jitter(), 0, "even spacing across the sender's wrap");
1056
1057        // And the receiver's clock wraps while the sender's stays low.
1058        let mut stats = StreamStats::new(1);
1059        let mut timestamp = 1000u32;
1060        let mut arrival = 0xFFFF_FF60u32;
1061        for sequence in 1u16..=10 {
1062            stats.on_packet(sequence, timestamp, arrival);
1063            timestamp = timestamp.wrapping_add(160);
1064            arrival = arrival.wrapping_add(160);
1065        }
1066        assert_eq!(
1067            stats.jitter(),
1068            0,
1069            "even spacing across the arrival clock's wrap"
1070        );
1071    }
1072
1073    /// The sequence wrap must be counted, or the extended number goes backwards and the loss
1074    /// calculation reports eight million packets lost.
1075    #[test]
1076    fn the_extended_sequence_number_counts_wraps() {
1077        let mut stats = StreamStats::new(1);
1078        let mut arrival = 0u32;
1079        for sequence in [65_534u16, 65_535, 0, 1, 2] {
1080            arrival += 160;
1081            stats.on_packet(sequence, arrival, arrival);
1082        }
1083        assert_eq!(
1084            stats.extended_highest_sequence(),
1085            0x0001_0002,
1086            "one wrap, then sequence 2"
1087        );
1088        assert_eq!(
1089            stats.cumulative_lost(),
1090            0,
1091            "nothing was lost across the wrap"
1092        );
1093    }
1094}