str0m 0.23.0

WebRTC library in Sans-IO style
Documentation
use std::net::Ipv4Addr;
use std::time::{Duration, Instant};

use str0m::media::{Direction, MediaKind};
use str0m::rtp::RawPacket;
use str0m::rtp::rtcp::{ReportBlock, Rtcp};
use str0m::{Rtc, RtcError};
use tracing::info_span;

mod common;
use common::{TestRtc, init_crypto_default, init_log, negotiate, progress};

/// Verify that a StreamTx responds to an incoming RRTR with a DLRR.
///
/// L sends audio to R. R's StreamRx generates RRTR (Receiver Reference Time Report,
/// RFC 3611 §4.4) alongside its Receiver Reports. L's StreamTx should respond with a
/// DLRR (Delay since Last Receiver Report, RFC 3611 §4.5) in its next Sender Report,
/// allowing R to compute RTT.
#[test]
pub fn dlrr_response_to_rrtr() -> Result<(), RtcError> {
    init_log();
    init_crypto_default();

    let now = Instant::now();
    let l_rtc = Rtc::builder().enable_raw_packets(true).build(now);
    let r_rtc = Rtc::builder().enable_raw_packets(true).build(now);

    let mut l = TestRtc::new_with_rtc(info_span!("L"), l_rtc);
    let mut r = TestRtc::new_with_rtc(info_span!("R"), r_rtc);

    l.add_host_candidate((Ipv4Addr::new(1, 1, 1, 1), 1000).into());
    r.add_host_candidate((Ipv4Addr::new(2, 2, 2, 2), 2000).into());

    // L sends audio to R: L gets a StreamTx, R gets a StreamRx.
    let mid = negotiate(&mut l, &mut r, |change| {
        change.add_media(MediaKind::Audio, Direction::SendOnly, None, None, None)
    });

    loop {
        if l.is_connected() || r.is_connected() {
            break;
        }
        progress(&mut l, &mut r)?;
    }

    // Sync clocks.
    let max = l.last.max(r.last);
    l.last = max;
    r.last = max;

    let params = l.params_opus();
    let pt = params.pt();
    let data = [1_u8; 80];

    // Run ~15s of synthetic time. R will send RRTR with its Receiver Reports
    // (~every 5s), and L should reply with DLRR in its next Sender Report.
    loop {
        let wallclock = l.start + l.duration();
        let time = l.duration().into();
        l.writer(mid).unwrap().write(pt, wallclock, time, data)?;
        progress(&mut l, &mut r)?;
        if l.duration() > Duration::from_secs(15) {
            break;
        }
    }

    let media_ssrc = l
        .events
        .iter()
        .find_map(|(_, e)| match e.as_raw_packet() {
            Some(RawPacket::RtpTx(header, _)) => Some(header.ssrc),
            _ => None,
        })
        .expect("L should have sent RTP");

    // R should have sent at least one RRTR, using its local RTCP SSRC rather than
    // the remote media SSRC it is reporting on.
    let rrtr_ssrc = r
        .events
        .iter()
        .find_map(|(_, e)| {
            let Some(RawPacket::RtcpTx(Rtcp::ExtendedReport(xr))) = e.as_raw_packet() else {
                return None;
            };
            xr.blocks
                .iter()
                .any(|b| matches!(b, ReportBlock::Rrtr(_)))
                .then_some(xr.ssrc)
        })
        .expect("R should have sent at least one RRTR");
    assert_ne!(rrtr_ssrc, media_ssrc);

    // L should have sent at least one DLRR in response.
    let dlrr_reports: Vec<_> = l
        .events
        .iter()
        .filter_map(|(_, e)| {
            if let Some(RawPacket::RtcpTx(Rtcp::ExtendedReport(xr))) = e.as_raw_packet() {
                if xr.blocks.iter().any(|b| matches!(b, ReportBlock::Dlrr(_))) {
                    return Some(xr);
                }
            }
            None
        })
        .collect();
    assert!(
        !dlrr_reports.is_empty(),
        "L should have sent DLRR in response to RRTR"
    );

    // Verify the DLRR contains valid data.
    let dlrr = dlrr_reports[0]
        .blocks
        .iter()
        .find_map(|b| match b {
            ReportBlock::Dlrr(d) => Some(d),
            _ => None,
        })
        .unwrap();
    assert!(!dlrr.items.is_empty(), "DLRR should have at least one item");
    let item = &dlrr.items[0];
    assert_eq!(item.ssrc, rrtr_ssrc);

    // The last_rr_time field must be the middle 32 bits of the RRTR's NTP timestamp.
    // Verify it's non-zero and that R received the same value back (round-trip integrity).
    assert!(
        item.last_rr_time != 0,
        "DLRR last_rr_time should be non-zero"
    );

    // Verify R actually received the DLRR that L sent.
    let r_received_dlrr = r.events.iter().any(|(_, e)| {
        if let Some(RawPacket::RtcpRx(Rtcp::ExtendedReport(xr))) = e.as_raw_packet() {
            xr.blocks.iter().any(|b| {
                if let ReportBlock::Dlrr(d) = b {
                    d.items.iter().any(|i| i.last_rr_time == item.last_rr_time)
                } else {
                    false
                }
            })
        } else {
            false
        }
    });
    assert!(
        r_received_dlrr,
        "R should have received the DLRR with matching last_rr_time"
    );

    Ok(())
}