1use bytes::{BufMut, Bytes, BytesMut};
19
20use crate::packet::sequence_is_newer;
21
22pub const SENDER_REPORT: u8 = 200;
24pub const RECEIVER_REPORT: u8 = 201;
26pub const SOURCE_DESCRIPTION: u8 = 202;
28pub const GOODBYE: u8 = 203;
30
31#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
33#[non_exhaustive]
34pub enum RtcpError {
35 #[error("packet is {0} bytes; an RTCP header is 4")]
37 TooShort(usize),
38 #[error("RTCP version {0}; only version 2 exists")]
40 BadVersion(u8),
41 #[error("the length field claims more than the packet contains")]
43 Truncated,
44}
45
46#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
48pub struct ReportBlock {
49 pub ssrc: u32,
51 pub fraction_lost: u8,
53 pub cumulative_lost: i32,
56 pub extended_highest_sequence: u32,
58 pub jitter: u32,
60 pub last_sender_report: u32,
62 pub delay_since_last_sender_report: u32,
64}
65
66#[derive(Debug, Clone, PartialEq, Eq)]
68pub struct SenderReport {
69 pub ssrc: u32,
71 pub ntp_timestamp: u64,
73 pub rtp_timestamp: u32,
76 pub packet_count: u32,
78 pub octet_count: u32,
80 pub reports: Vec<ReportBlock>,
82}
83
84#[derive(Debug, Clone, PartialEq, Eq)]
86pub struct ReceiverReport {
87 pub ssrc: u32,
89 pub reports: Vec<ReportBlock>,
91}
92
93pub const SDES_CNAME: u8 = 1;
95
96#[derive(Debug, Clone, PartialEq, Eq)]
98pub struct SdesItem {
99 pub kind: u8,
101 pub value: Bytes,
103}
104
105#[derive(Debug, Clone, PartialEq, Eq)]
107pub struct SdesChunk {
108 pub ssrc: u32,
110 pub items: Vec<SdesItem>,
112}
113
114#[derive(Debug, Clone, PartialEq, Eq)]
116pub struct Sdes {
117 pub chunks: Vec<SdesChunk>,
119}
120
121impl Sdes {
122 #[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#[derive(Debug, Clone, PartialEq, Eq)]
143pub enum Rtcp {
144 Sender(SenderReport),
146 Receiver(ReceiverReport),
148 Sdes(Sdes),
150 Other {
155 packet_type: u8,
157 count: u8,
161 padding: bool,
164 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 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 let lost_raw = u32::from(*slice.get(5)?) << 16
195 | u32::from(*slice.get(6)?) << 8
196 | u32::from(*slice.get(7)?);
197 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 #[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 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 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 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 #[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 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 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 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#[derive(Debug)]
455pub struct StreamStats {
456 ssrc: u32,
457 base_sequence: Option<u16>,
459 cycles: u32,
461 highest_sequence: u16,
462 received: u64,
463 received_at_last_report: u64,
465 expected_at_last_report: u64,
466 jitter: f64,
468 last_transit: Option<u32>,
471}
472
473impl StreamStats {
474 #[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 pub fn set_ssrc(&mut self, ssrc: u32) {
496 self.ssrc = ssrc;
497 }
498
499 pub fn on_packet(&mut self, sequence: u16, rtp_timestamp: u32, arrival: u32) {
504 self.record(sequence);
505
506 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 let d = f64::from(difference);
517 self.jitter += (d - self.jitter) / 16.0;
518 }
519 self.last_transit = Some(transit);
520 }
521
522 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 #[must_use]
554 pub fn extended_highest_sequence(&self) -> u32 {
555 (self.cycles << 16) | u32::from(self.highest_sequence)
556 }
557
558 #[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 #[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 #[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 #[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 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 #[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 #[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 #[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 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 #[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 #[test]
768 fn a_forwarded_packet_keeps_its_count_and_padding_bit() {
769 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 #[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 #[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 #[test]
853 fn a_receiver_report_counts_the_loss_the_buffer_saw() {
854 let mut stats = StreamStats::new(99);
855
856 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 assert_eq!(block.fraction_lost, 51, "loss as a fraction in 256ths");
874 }
875
876 #[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 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 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 #[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 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 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 #[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 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 #[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 stats.on_packet(sequence, timestamp, timestamp + 100_000);
982 }
983 assert_eq!(stats.jitter(), 0, "even spacing is zero jitter");
984 }
985
986 #[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 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 #[test]
1016 fn untimed_packets_count_for_loss_but_not_for_jitter() {
1017 let mut stats = StreamStats::new(1);
1018 for sequence in 1u16..=5 {
1020 let timestamp = u32::from(sequence) * 160;
1021 stats.on_packet(sequence, timestamp, timestamp + 4000);
1022 }
1023 for sequence in 6u16..=10 {
1025 if sequence == 7 {
1026 continue;
1027 }
1028 stats.on_untimed_packet(sequence);
1029 }
1030 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 #[test]
1045 fn a_timestamp_wrap_does_not_register_as_jitter() {
1046 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 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 #[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}