Skip to content
File

Blob: firmware/vendor/str0m/tests/dlrr.rs

rust152 lines
1use std::net::Ipv4Addr;
2use std::time::{Duration, Instant};
3 
4use str0m::media::{Direction, MediaKind};
5use str0m::rtp::RawPacket;
6use str0m::rtp::rtcp::{ReportBlock, Rtcp};
7use str0m::{Rtc, RtcError};
8use tracing::info_span;
9 
10mod common;
11use common::{TestRtc, init_crypto_default, init_log, negotiate, progress};
12 
13/// Verify that a StreamTx responds to an incoming RRTR with a DLRR.
14///
15/// L sends audio to R. R's StreamRx generates RRTR (Receiver Reference Time Report,
16/// RFC 3611 §4.4) alongside its Receiver Reports. L's StreamTx should respond with a
17/// DLRR (Delay since Last Receiver Report, RFC 3611 §4.5) in its next Sender Report,
18/// allowing R to compute RTT.
19#[test]
20pub fn dlrr_response_to_rrtr() -> Result<(), RtcError> {
21 init_log();
22 init_crypto_default();
23 
24 let now = Instant::now();
25 let l_rtc = Rtc::builder().enable_raw_packets(true).build(now);
26 let r_rtc = Rtc::builder().enable_raw_packets(true).build(now);
27 
28 let mut l = TestRtc::new_with_rtc(info_span!("L"), l_rtc);
29 let mut r = TestRtc::new_with_rtc(info_span!("R"), r_rtc);
30 
31 l.add_host_candidate((Ipv4Addr::new(1, 1, 1, 1), 1000).into());
32 r.add_host_candidate((Ipv4Addr::new(2, 2, 2, 2), 2000).into());
33 
34 // L sends audio to R: L gets a StreamTx, R gets a StreamRx.
35 let mid = negotiate(&mut l, &mut r, |change| {
36 change.add_media(MediaKind::Audio, Direction::SendOnly, None, None, None)
37 });
38 
39 loop {
40 if l.is_connected() || r.is_connected() {
41 break;
42 }
43 progress(&mut l, &mut r)?;
44 }
45 
46 // Sync clocks.
47 let max = l.last.max(r.last);
48 l.last = max;
49 r.last = max;
50 
51 let params = l.params_opus();
52 let pt = params.pt();
53 let data = [1_u8; 80];
54 
55 // Run ~15s of synthetic time. R will send RRTR with its Receiver Reports
56 // (~every 5s), and L should reply with DLRR in its next Sender Report.
57 loop {
58 let wallclock = l.start + l.duration();
59 let time = l.duration().into();
60 l.writer(mid).unwrap().write(pt, wallclock, time, data)?;
61 progress(&mut l, &mut r)?;
62 if l.duration() > Duration::from_secs(15) {
63 break;
64 }
65 }
66 
67 let media_ssrc = l
68 .events
69 .iter()
70 .find_map(|(_, e)| match e.as_raw_packet() {
71 Some(RawPacket::RtpTx(header, _)) => Some(header.ssrc),
72 _ => None,
73 })
74 .expect("L should have sent RTP");
75 
76 // R should have sent at least one RRTR, using its local RTCP SSRC rather than
77 // the remote media SSRC it is reporting on.
78 let rrtr_ssrc = r
79 .events
80 .iter()
81 .find_map(|(_, e)| {
82 let Some(RawPacket::RtcpTx(Rtcp::ExtendedReport(xr))) = e.as_raw_packet() else {
83 return None;
84 };
85 xr.blocks
86 .iter()
87 .any(|b| matches!(b, ReportBlock::Rrtr(_)))
88 .then_some(xr.ssrc)
89 })
90 .expect("R should have sent at least one RRTR");
91 assert_ne!(rrtr_ssrc, media_ssrc);
92 
93 // L should have sent at least one DLRR in response.
94 let dlrr_reports: Vec<_> = l
95 .events
96 .iter()
97 .filter_map(|(_, e)| {
98 if let Some(RawPacket::RtcpTx(Rtcp::ExtendedReport(xr))) = e.as_raw_packet() {
99 if xr.blocks.iter().any(|b| matches!(b, ReportBlock::Dlrr(_))) {
100 return Some(xr);
101 }
102 }
103 None
104 })
105 .collect();
106 assert!(
107 !dlrr_reports.is_empty(),
108 "L should have sent DLRR in response to RRTR"
109 );
110 
111 // Verify the DLRR contains valid data.
112 let dlrr = dlrr_reports[0]
113 .blocks
114 .iter()
115 .find_map(|b| match b {
116 ReportBlock::Dlrr(d) => Some(d),
117 _ => None,
118 })
119 .unwrap();
120 assert!(!dlrr.items.is_empty(), "DLRR should have at least one item");
121 let item = &dlrr.items[0];
122 assert_eq!(item.ssrc, rrtr_ssrc);
123 
124 // The last_rr_time field must be the middle 32 bits of the RRTR's NTP timestamp.
125 // Verify it's non-zero and that R received the same value back (round-trip integrity).
126 assert!(
127 item.last_rr_time != 0,
128 "DLRR last_rr_time should be non-zero"
129 );
130 
131 // Verify R actually received the DLRR that L sent.
132 let r_received_dlrr = r.events.iter().any(|(_, e)| {
133 if let Some(RawPacket::RtcpRx(Rtcp::ExtendedReport(xr))) = e.as_raw_packet() {
134 xr.blocks.iter().any(|b| {
135 if let ReportBlock::Dlrr(d) = b {
136 d.items.iter().any(|i| i.last_rr_time == item.last_rr_time)
137 } else {
138 false
139 }
140 })
141 } else {
142 false
143 }
144 });
145 assert!(
146 r_received_dlrr,
147 "R should have received the DLRR with matching last_rr_time"
148 );
149 
150 Ok(())
151}