pub const PT_SENDER_REPORT: u8 = 200;
pub const PT_RECEIVER_REPORT: u8 = 201;
pub const PT_SDES: u8 = 202;
pub const PT_BYE: u8 = 203;
pub const PT_APP: u8 = 204;
pub const PT_RTPFB: u8 = 205;
pub const PT_PSFB: u8 = 206;
const FMT_PLI: u8 = 1; const FMT_FIR: u8 = 4;
pub fn is_rtcp(buf: &[u8]) -> bool {
buf.len() >= 2 && (PT_SENDER_REPORT..=PT_PSFB).contains(&buf[1])
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SenderReport {
pub ssrc: u32,
pub ntp_middle: u32,
}
impl SenderReport {
pub fn parse_first(buf: &[u8]) -> Option<SenderReport> {
let mut off = 0;
while off + 4 <= buf.len() {
let pt = buf[off + 1];
let len_words = u16::from_be_bytes([buf[off + 2], buf[off + 3]]) as usize;
let pkt_len = (len_words + 1) * 4;
if pkt_len == 0 || off + pkt_len > buf.len() {
break;
}
if pt == PT_SENDER_REPORT && pkt_len >= 16 {
let ssrc =
u32::from_be_bytes([buf[off + 4], buf[off + 5], buf[off + 6], buf[off + 7]]);
let ntp_msw =
u32::from_be_bytes([buf[off + 8], buf[off + 9], buf[off + 10], buf[off + 11]]);
let ntp_lsw = u32::from_be_bytes([
buf[off + 12],
buf[off + 13],
buf[off + 14],
buf[off + 15],
]);
let ntp_middle = (ntp_msw << 16) | (ntp_lsw >> 16);
return Some(SenderReport { ssrc, ntp_middle });
}
off += pkt_len;
}
None
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct ReportBlock {
pub source_ssrc: u32,
pub fraction_lost: u8,
pub cumulative_lost: u32,
pub highest_seq: u32,
pub jitter: u32,
pub lsr: u32,
pub dlsr: u32,
}
fn patch_length(out: &mut [u8], start: usize) {
let words = ((out.len() - start) / 4).saturating_sub(1) as u16;
out[start + 2..start + 4].copy_from_slice(&words.to_be_bytes());
}
pub fn build_receiver_report(our_ssrc: u32, blocks: &[ReportBlock]) -> Vec<u8> {
let rc = blocks.len().min(31);
let mut out = Vec::with_capacity(8 + rc * 24);
out.push(0x80 | rc as u8); out.push(PT_RECEIVER_REPORT);
out.extend_from_slice(&[0, 0]); out.extend_from_slice(&our_ssrc.to_be_bytes());
for b in blocks.iter().take(31) {
out.extend_from_slice(&b.source_ssrc.to_be_bytes());
out.push(b.fraction_lost);
out.extend_from_slice(&b.cumulative_lost.to_be_bytes()[1..]);
out.extend_from_slice(&b.highest_seq.to_be_bytes());
out.extend_from_slice(&b.jitter.to_be_bytes());
out.extend_from_slice(&b.lsr.to_be_bytes());
out.extend_from_slice(&b.dlsr.to_be_bytes());
}
patch_length(&mut out, 0);
out
}
pub fn build_sdes(our_ssrc: u32, cname: &str) -> Vec<u8> {
let mut out = Vec::new();
out.push(0x80 | 1); out.push(PT_SDES);
out.extend_from_slice(&[0, 0]); out.extend_from_slice(&our_ssrc.to_be_bytes());
let bytes = cname.as_bytes();
let len = bytes.len().min(255);
out.push(1);
out.push(len as u8);
out.extend_from_slice(&bytes[..len]);
out.push(0);
while out.len() % 4 != 0 {
out.push(0);
}
patch_length(&mut out, 0);
out
}
pub fn build_pli(our_ssrc: u32, media_ssrc: u32) -> Vec<u8> {
let mut out = Vec::with_capacity(12);
out.push(0x80 | FMT_PLI); out.push(PT_PSFB);
out.extend_from_slice(&[0, 0]); out.extend_from_slice(&our_ssrc.to_be_bytes());
out.extend_from_slice(&media_ssrc.to_be_bytes());
patch_length(&mut out, 0);
out
}
pub fn build_fir(our_ssrc: u32, media_ssrc: u32, seq_nr: u8) -> Vec<u8> {
let mut out = Vec::with_capacity(20);
out.push(0x80 | FMT_FIR); out.push(PT_PSFB);
out.extend_from_slice(&[0, 0]); out.extend_from_slice(&our_ssrc.to_be_bytes());
out.extend_from_slice(&0u32.to_be_bytes());
out.extend_from_slice(&media_ssrc.to_be_bytes());
out.push(seq_nr);
out.extend_from_slice(&[0, 0, 0]);
patch_length(&mut out, 0);
out
}
const APP_NAME_FRAME_ACK: [u8; 4] = [0x00, 0x00, 0x00, 0x05];
pub fn build_frame_ack(our_ssrc: u32, rtp_timestamp: u32) -> Vec<u8> {
let mut out = Vec::with_capacity(16);
out.push(0x80); out.push(PT_APP);
out.extend_from_slice(&[0, 0]); out.extend_from_slice(&our_ssrc.to_be_bytes());
out.extend_from_slice(&APP_NAME_FRAME_ACK);
out.extend_from_slice(&rtp_timestamp.to_be_bytes());
patch_length(&mut out, 0);
out
}
pub fn build_rctl(our_ssrc: u32, clock_ms: u16, frames: u16, highest_seq: u16) -> Vec<u8> {
let mut out = Vec::with_capacity(32);
out.push(0x80); out.push(PT_APP);
out.extend_from_slice(&[0, 0]); out.extend_from_slice(&our_ssrc.to_be_bytes());
out.extend_from_slice(b"RCTL");
out.extend_from_slice(&[0x85, 0x00, 0x00, 0x04]);
out.extend_from_slice(&frames.to_be_bytes());
out.extend_from_slice(&[0, 0, 0, 0, 0, 0]); out.extend_from_slice(&clock_ms.to_be_bytes());
out.extend_from_slice(&[0, 0]); out.extend_from_slice(&highest_seq.to_be_bytes());
out.extend_from_slice(&[0, 0]); patch_length(&mut out, 0);
out
}
pub fn build_liveness(our_ssrc: u32, cname: &str, blocks: &[ReportBlock]) -> Vec<u8> {
let mut p = build_receiver_report(our_ssrc, blocks);
p.extend_from_slice(&build_sdes(our_ssrc, cname));
p
}
pub fn build_keyframe_request(
our_ssrc: u32,
cname: &str,
media_ssrc: u32,
blocks: &[ReportBlock],
fir_seq: u8,
) -> Vec<u8> {
let mut p = build_liveness(our_ssrc, cname, blocks);
p.extend_from_slice(&build_pli(our_ssrc, media_ssrc));
p.extend_from_slice(&build_fir(our_ssrc, media_ssrc, fir_seq));
p
}
#[cfg(test)]
mod tests {
use super::*;
fn assert_well_formed(buf: &[u8]) {
assert_eq!(buf.len() % 4, 0, "RTCP packet not 32-bit aligned");
let mut off = 0;
while off < buf.len() {
assert!(off + 4 <= buf.len(), "truncated sub-packet header");
assert_eq!(buf[off] >> 6, 2, "version must be 2");
let len_words = u16::from_be_bytes([buf[off + 2], buf[off + 3]]) as usize;
let pkt_len = (len_words + 1) * 4;
assert!(
off + pkt_len <= buf.len(),
"sub-packet length overruns buffer"
);
off += pkt_len;
}
assert_eq!(off, buf.len(), "sub-packet lengths don't tile the buffer");
}
#[test]
fn is_rtcp_discriminates_from_rtp() {
assert!(!is_rtcp(&[0x80, 100, 0, 0]));
assert!(!is_rtcp(&[0x80, 0x80 | 100, 0, 0]));
assert!(is_rtcp(&[0x80, PT_SENDER_REPORT, 0, 0]));
assert!(is_rtcp(&[0x81, PT_RECEIVER_REPORT, 0, 0]));
assert!(is_rtcp(&[0x81, PT_PSFB, 0, 0]));
}
#[test]
fn receiver_report_is_well_formed() {
let blocks = [ReportBlock {
source_ssrc: 0xdead_beef,
fraction_lost: 12,
cumulative_lost: 0x01_2345,
highest_seq: 0x0001_8000,
jitter: 42,
lsr: 0xaabb_ccdd,
dlsr: 0x0000_1000,
}];
let rr = build_receiver_report(0x1234_5678, &blocks);
assert_well_formed(&rr);
assert_eq!(rr[0] & 0x1f, 1, "RC should be 1");
assert_eq!(rr[1], PT_RECEIVER_REPORT);
assert_eq!(&rr[8..12], &0xdead_beefu32.to_be_bytes());
assert_eq!(rr[12], 12, "fraction lost");
assert_eq!(&rr[13..16], &[0x01, 0x23, 0x45], "cumulative loss (24-bit)");
}
#[test]
fn liveness_and_keyframe_request_are_well_formed() {
let blocks = [ReportBlock {
source_ssrc: 1,
..Default::default()
}];
assert_well_formed(&build_liveness(7, "host@1.2.3.4", &blocks));
assert_well_formed(&build_keyframe_request(
7,
"host@1.2.3.4",
0xabcd,
&blocks,
3,
));
assert_well_formed(&build_liveness(7, "h", &[]));
}
#[test]
fn pli_and_fir_carry_the_right_ssrcs() {
let pli = build_pli(0x1111_1111, 0x2222_2222);
assert_well_formed(&pli);
assert_eq!(pli[0] & 0x1f, FMT_PLI);
assert_eq!(&pli[4..8], &0x1111_1111u32.to_be_bytes());
assert_eq!(&pli[8..12], &0x2222_2222u32.to_be_bytes());
let fir = build_fir(0x1111_1111, 0x2222_2222, 9);
assert_well_formed(&fir);
assert_eq!(fir[0] & 0x1f, FMT_FIR);
assert_eq!(&fir[4..8], &0x1111_1111u32.to_be_bytes());
assert_eq!(
&fir[8..12],
&0u32.to_be_bytes(),
"FIR media-source SSRC must be 0"
);
assert_eq!(
&fir[12..16],
&0x2222_2222u32.to_be_bytes(),
"FCI target SSRC"
);
assert_eq!(fir[16], 9, "FIR seq nr");
}
#[test]
fn rctl_is_well_formed_and_carries_fields() {
let r = build_rctl(0x00db_16eb, 12438, 6, 21);
assert_well_formed(&r);
assert_eq!(r.len(), 32);
assert_eq!(r[1], PT_APP);
assert_eq!(&r[8..12], b"RCTL");
assert_eq!(&r[12..16], &[0x85, 0x00, 0x00, 0x04]);
assert_eq!(&r[16..18], &6u16.to_be_bytes()); assert_eq!(&r[24..26], &12438u16.to_be_bytes()); assert_eq!(&r[28..30], &21u16.to_be_bytes()); }
#[test]
fn frame_ack_matches_capture() {
let ack = build_frame_ack(0x00db_16eb, 5600);
assert_well_formed(&ack);
assert_eq!(ack.len(), 16);
assert_eq!(
&ack,
&[
0x80, 0xcc, 0x00, 0x03, 0x00, 0xdb, 0x16, 0xeb, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00,
0x15, 0xe0, ]
);
}
#[test]
fn parses_first_sender_report() {
let mut sr = vec![0x80, PT_SENDER_REPORT, 0, 6];
sr.extend_from_slice(&0x0a0b_0c0du32.to_be_bytes()); sr.extend_from_slice(&0x1111_2222u32.to_be_bytes()); sr.extend_from_slice(&0x3333_4444u32.to_be_bytes()); sr.extend_from_slice(&0u32.to_be_bytes()); sr.extend_from_slice(&0u32.to_be_bytes()); sr.extend_from_slice(&0u32.to_be_bytes()); let mut compound = build_receiver_report(0xfeed, &[]);
compound.extend_from_slice(&sr);
let parsed = SenderReport::parse_first(&compound).expect("should find the SR");
assert_eq!(parsed.ssrc, 0x0a0b_0c0d);
assert_eq!(parsed.ntp_middle, 0x2222_3333);
}
}