pub const PT_SR: u8 = 200;
pub const PT_RR: u8 = 201;
pub const PT_SDES: u8 = 202;
pub const PT_BYE: u8 = 203;
pub const PT_APP: u8 = 204;
pub const APP_NAME_LEN: usize = 4;
pub const APP_SUBTYPE_MAX: u8 = 31;
pub const MAX_SOURCE_COUNT: usize = 31;
pub const MAX_TEXT_LEN: usize = 255;
pub const RTCP_HEADER_LEN: usize = 8;
pub const RTCP_HEADER_FIXED_LEN: usize = 4;
pub const SENDER_INFO_LEN: usize = 20;
pub const REPORT_BLOCK_LEN: usize = 24;
pub const MAX_REPORT_BLOCKS: usize = 31;
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum RtcpError {
TooManyReportBlocks { count: usize },
ShortHeader,
BadVersion { value: u8 },
UnexpectedPacketType { value: u8 },
Truncated,
LengthMismatch {
stated_words: u16,
actual_words: u16,
},
TooManySources { count: usize },
TextTooLong { len: usize },
PrivTooLong { len: usize },
AppNameWrongLength { len: usize },
AppDataNotAligned { len: usize },
AppSubtypeOutOfRange { value: u8 },
}
impl core::fmt::Display for RtcpError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
RtcpError::TooManyReportBlocks { count } => {
write!(f, "rtcp: {count} report blocks exceeds the 31-block limit")
}
RtcpError::ShortHeader => write!(f, "rtcp: buffer < 8-byte report header"),
RtcpError::BadVersion { value } => write!(f, "rtcp: version {value} != 2"),
RtcpError::UnexpectedPacketType { value } => {
write!(f, "rtcp: PT {value} is neither SR (200) nor RR (201)")
}
RtcpError::Truncated => write!(f, "rtcp: buffer shorter than length/RC implies"),
RtcpError::LengthMismatch {
stated_words,
actual_words,
} => write!(
f,
"rtcp: length field says {stated_words} words, body is {actual_words}"
),
RtcpError::TooManySources { count } => {
write!(f, "rtcp: {count} sources exceeds the 31-source SC limit")
}
RtcpError::TextTooLong { len } => {
write!(f, "rtcp: text/reason {len} octets exceeds the 255 limit")
}
RtcpError::PrivTooLong { len } => {
write!(f, "rtcp: PRIV item {len} octets exceeds the 255 limit")
}
RtcpError::AppNameWrongLength { len } => {
write!(f, "rtcp: APP name is {len} octets, must be exactly 4")
}
RtcpError::AppDataNotAligned { len } => {
write!(f, "rtcp: APP data is {len} octets, must be a multiple of 4")
}
RtcpError::AppSubtypeOutOfRange { value } => {
write!(f, "rtcp: APP subtype {value} exceeds the 5-bit max of 31")
}
}
}
}
impl std::error::Error for RtcpError {}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub struct SenderInfo {
pub ntp_timestamp: u64,
pub rtp_timestamp: u32,
pub packet_count: u32,
pub octet_count: u32,
}
impl SenderInfo {
pub fn ntp_from_parts(seconds: u32, fraction: u32) -> u64 {
((seconds as u64) << 32) | (fraction as u64)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub struct ReceptionReportBlock {
pub ssrc: u32,
pub fraction_lost: u8,
pub cumulative_lost: i32,
pub extended_highest_seq: u32,
pub jitter: u32,
pub last_sr: u32,
pub delay_since_last_sr: u32,
}
impl ReceptionReportBlock {
fn write(&self, buf: &mut Vec<u8>) {
buf.extend_from_slice(&self.ssrc.to_be_bytes());
let cum = (self.cumulative_lost as u32) & 0x00FF_FFFF;
let word = ((self.fraction_lost as u32) << 24) | cum;
buf.extend_from_slice(&word.to_be_bytes());
buf.extend_from_slice(&self.extended_highest_seq.to_be_bytes());
buf.extend_from_slice(&self.jitter.to_be_bytes());
buf.extend_from_slice(&self.last_sr.to_be_bytes());
buf.extend_from_slice(&self.delay_since_last_sr.to_be_bytes());
}
fn read(buf: &[u8]) -> Self {
let ssrc = u32::from_be_bytes([buf[0], buf[1], buf[2], buf[3]]);
let word = u32::from_be_bytes([buf[4], buf[5], buf[6], buf[7]]);
let fraction_lost = (word >> 24) as u8;
let cum24 = word & 0x00FF_FFFF;
let cumulative_lost = if cum24 & 0x0080_0000 != 0 {
(cum24 | 0xFF00_0000) as i32
} else {
cum24 as i32
};
let extended_highest_seq = u32::from_be_bytes([buf[8], buf[9], buf[10], buf[11]]);
let jitter = u32::from_be_bytes([buf[12], buf[13], buf[14], buf[15]]);
let last_sr = u32::from_be_bytes([buf[16], buf[17], buf[18], buf[19]]);
let delay_since_last_sr = u32::from_be_bytes([buf[20], buf[21], buf[22], buf[23]]);
Self {
ssrc,
fraction_lost,
cumulative_lost,
extended_highest_seq,
jitter,
last_sr,
delay_since_last_sr,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RtcpReport {
pub packet_type: u8,
pub ssrc: u32,
pub sender_info: Option<SenderInfo>,
pub report_blocks: Vec<ReceptionReportBlock>,
}
fn write_header(buf: &mut Vec<u8>, rc: u8, pt: u8, ssrc: u32, total_len_bytes: usize) {
debug_assert!(rc <= 31);
debug_assert_eq!(total_len_bytes % 4, 0);
buf.push(0x80 | (rc & 0x1F));
buf.push(pt);
let words = (total_len_bytes / 4) as u16;
let length_field = words.saturating_sub(1);
buf.extend_from_slice(&length_field.to_be_bytes());
buf.extend_from_slice(&ssrc.to_be_bytes());
}
fn write_count_header(buf: &mut Vec<u8>, cnt: u8, pt: u8, total_len_bytes: usize) {
debug_assert!(cnt <= 31);
debug_assert_eq!(total_len_bytes % 4, 0);
buf.push(0x80 | (cnt & 0x1F)); buf.push(pt);
let words = (total_len_bytes / 4) as u16;
buf.extend_from_slice(&words.saturating_sub(1).to_be_bytes());
}
pub fn build_sender_report(
ssrc: u32,
info: &SenderInfo,
blocks: &[ReceptionReportBlock],
) -> Result<Vec<u8>, RtcpError> {
if blocks.len() > MAX_REPORT_BLOCKS {
return Err(RtcpError::TooManyReportBlocks {
count: blocks.len(),
});
}
let total = RTCP_HEADER_LEN + SENDER_INFO_LEN + blocks.len() * REPORT_BLOCK_LEN;
let mut buf = Vec::with_capacity(total);
write_header(&mut buf, blocks.len() as u8, PT_SR, ssrc, total);
buf.extend_from_slice(&info.ntp_timestamp.to_be_bytes());
buf.extend_from_slice(&info.rtp_timestamp.to_be_bytes());
buf.extend_from_slice(&info.packet_count.to_be_bytes());
buf.extend_from_slice(&info.octet_count.to_be_bytes());
for b in blocks {
b.write(&mut buf);
}
debug_assert_eq!(buf.len(), total);
Ok(buf)
}
pub fn build_receiver_report(
ssrc: u32,
blocks: &[ReceptionReportBlock],
) -> Result<Vec<u8>, RtcpError> {
if blocks.len() > MAX_REPORT_BLOCKS {
return Err(RtcpError::TooManyReportBlocks {
count: blocks.len(),
});
}
let total = RTCP_HEADER_LEN + blocks.len() * REPORT_BLOCK_LEN;
let mut buf = Vec::with_capacity(total);
write_header(&mut buf, blocks.len() as u8, PT_RR, ssrc, total);
for b in blocks {
b.write(&mut buf);
}
debug_assert_eq!(buf.len(), total);
Ok(buf)
}
pub fn parse_report(buf: &[u8]) -> Result<RtcpReport, RtcpError> {
if buf.len() < RTCP_HEADER_LEN {
return Err(RtcpError::ShortHeader);
}
let b0 = buf[0];
let version = (b0 >> 6) & 0x3;
if version != 2 {
return Err(RtcpError::BadVersion { value: version });
}
let rc = (b0 & 0x1F) as usize;
let pt = buf[1];
if pt != PT_SR && pt != PT_RR {
return Err(RtcpError::UnexpectedPacketType { value: pt });
}
let stated_words = u16::from_be_bytes([buf[2], buf[3]]);
let ssrc = u32::from_be_bytes([buf[4], buf[5], buf[6], buf[7]]);
let mut off = RTCP_HEADER_LEN;
let sender_info = if pt == PT_SR {
if buf.len() < off + SENDER_INFO_LEN {
return Err(RtcpError::Truncated);
}
let ntp_timestamp = u64::from_be_bytes([
buf[off],
buf[off + 1],
buf[off + 2],
buf[off + 3],
buf[off + 4],
buf[off + 5],
buf[off + 6],
buf[off + 7],
]);
let rtp_timestamp =
u32::from_be_bytes([buf[off + 8], buf[off + 9], buf[off + 10], buf[off + 11]]);
let packet_count =
u32::from_be_bytes([buf[off + 12], buf[off + 13], buf[off + 14], buf[off + 15]]);
let octet_count =
u32::from_be_bytes([buf[off + 16], buf[off + 17], buf[off + 18], buf[off + 19]]);
off += SENDER_INFO_LEN;
Some(SenderInfo {
ntp_timestamp,
rtp_timestamp,
packet_count,
octet_count,
})
} else {
None
};
let need_blocks = rc * REPORT_BLOCK_LEN;
if buf.len() < off + need_blocks {
return Err(RtcpError::Truncated);
}
let mut report_blocks = Vec::with_capacity(rc);
for i in 0..rc {
let start = off + i * REPORT_BLOCK_LEN;
report_blocks.push(ReceptionReportBlock::read(&buf[start..]));
}
let body_end = off + need_blocks;
let actual_words = (body_end / 4) as u16;
let stated_total_words = stated_words.saturating_add(1);
if stated_total_words < actual_words {
return Err(RtcpError::LengthMismatch {
stated_words: stated_total_words,
actual_words,
});
}
Ok(RtcpReport {
packet_type: pt,
ssrc,
sender_info,
report_blocks,
})
}
pub mod sdes_type {
pub const END: u8 = 0;
pub const CNAME: u8 = 1;
pub const NAME: u8 = 2;
pub const EMAIL: u8 = 3;
pub const PHONE: u8 = 4;
pub const LOC: u8 = 5;
pub const TOOL: u8 = 6;
pub const NOTE: u8 = 7;
pub const PRIV: u8 = 8;
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum SdesItem {
Cname(String),
Name(String),
Email(String),
Phone(String),
Loc(String),
Tool(String),
Note(String),
Priv { prefix: String, value: String },
}
impl SdesItem {
pub fn type_code(&self) -> u8 {
match self {
SdesItem::Cname(_) => sdes_type::CNAME,
SdesItem::Name(_) => sdes_type::NAME,
SdesItem::Email(_) => sdes_type::EMAIL,
SdesItem::Phone(_) => sdes_type::PHONE,
SdesItem::Loc(_) => sdes_type::LOC,
SdesItem::Tool(_) => sdes_type::TOOL,
SdesItem::Note(_) => sdes_type::NOTE,
SdesItem::Priv { .. } => sdes_type::PRIV,
}
}
fn wire_len(&self) -> usize {
2 + self.text_len()
}
fn text_len(&self) -> usize {
match self {
SdesItem::Cname(s)
| SdesItem::Name(s)
| SdesItem::Email(s)
| SdesItem::Phone(s)
| SdesItem::Loc(s)
| SdesItem::Tool(s)
| SdesItem::Note(s) => s.len(),
SdesItem::Priv { prefix, value } => 1 + prefix.len() + value.len(),
}
}
fn validate(&self) -> Result<(), RtcpError> {
match self {
SdesItem::Priv { .. } => {
let len = self.text_len();
if len > MAX_TEXT_LEN {
return Err(RtcpError::PrivTooLong { len });
}
}
_ => {
let len = self.text_len();
if len > MAX_TEXT_LEN {
return Err(RtcpError::TextTooLong { len });
}
}
}
Ok(())
}
fn write(&self, buf: &mut Vec<u8>) {
buf.push(self.type_code());
match self {
SdesItem::Priv { prefix, value } => {
buf.push((1 + prefix.len() + value.len()) as u8);
buf.push(prefix.len() as u8);
buf.extend_from_slice(prefix.as_bytes());
buf.extend_from_slice(value.as_bytes());
}
SdesItem::Cname(s)
| SdesItem::Name(s)
| SdesItem::Email(s)
| SdesItem::Phone(s)
| SdesItem::Loc(s)
| SdesItem::Tool(s)
| SdesItem::Note(s) => {
buf.push(s.len() as u8);
buf.extend_from_slice(s.as_bytes());
}
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SdesChunk {
pub ssrc: u32,
pub items: Vec<SdesItem>,
}
impl SdesChunk {
fn padded_len(&self) -> usize {
let raw = 4 + self.items.iter().map(SdesItem::wire_len).sum::<usize>() + 1; raw.div_ceil(4) * 4
}
fn write(&self, buf: &mut Vec<u8>) {
let start = buf.len();
buf.extend_from_slice(&self.ssrc.to_be_bytes());
for item in &self.items {
item.write(buf);
}
buf.push(sdes_type::END);
while (buf.len() - start) % 4 != 0 {
buf.push(0);
}
}
}
pub fn build_sdes(chunks: &[SdesChunk]) -> Result<Vec<u8>, RtcpError> {
if chunks.len() > MAX_SOURCE_COUNT {
return Err(RtcpError::TooManySources {
count: chunks.len(),
});
}
for chunk in chunks {
for item in &chunk.items {
item.validate()?;
}
}
let body: usize = chunks.iter().map(SdesChunk::padded_len).sum();
let total = RTCP_HEADER_FIXED_LEN + body;
let mut buf = Vec::with_capacity(total);
write_count_header(&mut buf, chunks.len() as u8, PT_SDES, total);
for chunk in chunks {
chunk.write(&mut buf);
}
debug_assert_eq!(buf.len(), total);
debug_assert_eq!(buf.len() % 4, 0);
Ok(buf)
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SdesPacket {
pub chunks: Vec<SdesChunk>,
}
pub fn parse_sdes(buf: &[u8]) -> Result<SdesPacket, RtcpError> {
if buf.len() < RTCP_HEADER_FIXED_LEN {
return Err(RtcpError::ShortHeader);
}
let b0 = buf[0];
if (b0 >> 6) & 0x3 != 2 {
return Err(RtcpError::BadVersion {
value: (b0 >> 6) & 0x3,
});
}
if buf[1] != PT_SDES {
return Err(RtcpError::UnexpectedPacketType { value: buf[1] });
}
let sc = (b0 & 0x1F) as usize;
let stated_words = u16::from_be_bytes([buf[2], buf[3]]);
let stated_total = (stated_words as usize + 1) * 4;
let body_end = stated_total.min(buf.len());
let mut off = RTCP_HEADER_FIXED_LEN;
let mut chunks = Vec::with_capacity(sc);
for _ in 0..sc {
if off + 4 > body_end {
return Err(RtcpError::Truncated);
}
let ssrc = u32::from_be_bytes([buf[off], buf[off + 1], buf[off + 2], buf[off + 3]]);
off += 4;
let mut items = Vec::new();
loop {
if off >= body_end {
return Err(RtcpError::Truncated);
}
let ty = buf[off];
off += 1;
if ty == sdes_type::END {
break;
}
if off >= body_end {
return Err(RtcpError::Truncated);
}
let len = buf[off] as usize;
off += 1;
if off + len > body_end {
return Err(RtcpError::Truncated);
}
let text = &buf[off..off + len];
off += len;
match ty {
sdes_type::CNAME => items.push(SdesItem::Cname(utf8_lossy(text))),
sdes_type::NAME => items.push(SdesItem::Name(utf8_lossy(text))),
sdes_type::EMAIL => items.push(SdesItem::Email(utf8_lossy(text))),
sdes_type::PHONE => items.push(SdesItem::Phone(utf8_lossy(text))),
sdes_type::LOC => items.push(SdesItem::Loc(utf8_lossy(text))),
sdes_type::TOOL => items.push(SdesItem::Tool(utf8_lossy(text))),
sdes_type::NOTE => items.push(SdesItem::Note(utf8_lossy(text))),
sdes_type::PRIV if len >= 1 => {
let plen = (text[0] as usize).min(len - 1);
let prefix = utf8_lossy(&text[1..1 + plen]);
let value = utf8_lossy(&text[1 + plen..]);
items.push(SdesItem::Priv { prefix, value });
}
_ => { }
}
}
while (off - RTCP_HEADER_FIXED_LEN) % 4 != 0 {
if off >= body_end {
break;
}
off += 1;
}
chunks.push(SdesChunk { ssrc, items });
}
Ok(SdesPacket { chunks })
}
pub fn build_cname_sdes(ssrc: u32, cname: &str) -> Result<Vec<u8>, RtcpError> {
build_sdes(&[SdesChunk {
ssrc,
items: vec![SdesItem::Cname(cname.to_string())],
}])
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ByePacket {
pub sources: Vec<u32>,
pub reason: Option<String>,
}
pub fn build_bye(sources: &[u32], reason: Option<&str>) -> Result<Vec<u8>, RtcpError> {
if sources.len() > MAX_SOURCE_COUNT {
return Err(RtcpError::TooManySources {
count: sources.len(),
});
}
let reason_bytes = reason.map(str::as_bytes);
if let Some(r) = reason_bytes {
if r.len() > MAX_TEXT_LEN {
return Err(RtcpError::TextTooLong { len: r.len() });
}
}
let mut body = sources.len() * 4;
if let Some(r) = reason_bytes {
body += 1 + r.len();
}
let unpadded = RTCP_HEADER_FIXED_LEN + body;
let total = unpadded.div_ceil(4) * 4;
let mut buf = Vec::with_capacity(total);
write_count_header(&mut buf, sources.len() as u8, PT_BYE, total);
for &s in sources {
buf.extend_from_slice(&s.to_be_bytes());
}
if let Some(r) = reason_bytes {
buf.push(r.len() as u8);
buf.extend_from_slice(r);
}
while buf.len() < total {
buf.push(0);
}
debug_assert_eq!(buf.len(), total);
Ok(buf)
}
pub fn parse_bye(buf: &[u8]) -> Result<ByePacket, RtcpError> {
if buf.len() < RTCP_HEADER_FIXED_LEN {
return Err(RtcpError::ShortHeader);
}
let b0 = buf[0];
if (b0 >> 6) & 0x3 != 2 {
return Err(RtcpError::BadVersion {
value: (b0 >> 6) & 0x3,
});
}
if buf[1] != PT_BYE {
return Err(RtcpError::UnexpectedPacketType { value: buf[1] });
}
let sc = (b0 & 0x1F) as usize;
let stated_words = u16::from_be_bytes([buf[2], buf[3]]);
let stated_total = (stated_words as usize + 1) * 4;
let body_end = stated_total.min(buf.len());
let mut off = RTCP_HEADER_FIXED_LEN;
if off + sc * 4 > body_end {
return Err(RtcpError::Truncated);
}
let mut sources = Vec::with_capacity(sc);
for _ in 0..sc {
sources.push(u32::from_be_bytes([
buf[off],
buf[off + 1],
buf[off + 2],
buf[off + 3],
]));
off += 4;
}
let reason = if off < body_end {
let len = buf[off] as usize;
off += 1;
if off + len > body_end {
return Err(RtcpError::Truncated);
}
Some(utf8_lossy(&buf[off..off + len]))
} else {
None
};
Ok(ByePacket { sources, reason })
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct AppPacket {
pub subtype: u8,
pub ssrc: u32,
pub name: [u8; APP_NAME_LEN],
pub data: Vec<u8>,
}
pub fn build_app(subtype: u8, ssrc: u32, name: &[u8], data: &[u8]) -> Result<Vec<u8>, RtcpError> {
if subtype > APP_SUBTYPE_MAX {
return Err(RtcpError::AppSubtypeOutOfRange { value: subtype });
}
if name.len() != APP_NAME_LEN {
return Err(RtcpError::AppNameWrongLength { len: name.len() });
}
if data.len() % 4 != 0 {
return Err(RtcpError::AppDataNotAligned { len: data.len() });
}
let total = RTCP_HEADER_FIXED_LEN + 4 + APP_NAME_LEN + data.len();
debug_assert_eq!(total % 4, 0);
let mut buf = Vec::with_capacity(total);
write_count_header(&mut buf, subtype, PT_APP, total);
buf.extend_from_slice(&ssrc.to_be_bytes());
buf.extend_from_slice(name);
buf.extend_from_slice(data);
debug_assert_eq!(buf.len(), total);
Ok(buf)
}
pub fn parse_app(buf: &[u8]) -> Result<AppPacket, RtcpError> {
const MIN_LEN: usize = RTCP_HEADER_FIXED_LEN + 4 + APP_NAME_LEN;
if buf.len() < MIN_LEN {
return Err(RtcpError::ShortHeader);
}
let b0 = buf[0];
if (b0 >> 6) & 0x3 != 2 {
return Err(RtcpError::BadVersion {
value: (b0 >> 6) & 0x3,
});
}
if buf[1] != PT_APP {
return Err(RtcpError::UnexpectedPacketType { value: buf[1] });
}
let subtype = b0 & 0x1F;
let stated_words = u16::from_be_bytes([buf[2], buf[3]]);
let stated_total = (stated_words as usize + 1) * 4;
if stated_total < MIN_LEN {
return Err(RtcpError::LengthMismatch {
stated_words: stated_total.div_ceil(4) as u16,
actual_words: MIN_LEN.div_ceil(4) as u16,
});
}
if buf.len() < stated_total {
return Err(RtcpError::Truncated);
}
let ssrc = u32::from_be_bytes([buf[4], buf[5], buf[6], buf[7]]);
let mut name = [0u8; APP_NAME_LEN];
name.copy_from_slice(&buf[8..8 + APP_NAME_LEN]);
let data_start = RTCP_HEADER_FIXED_LEN + 4 + APP_NAME_LEN;
let data = buf[data_start..stated_total].to_vec();
Ok(AppPacket {
subtype,
ssrc,
name,
data,
})
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum RtcpPacket {
Report(RtcpReport),
Sdes(SdesPacket),
Bye(ByePacket),
App(AppPacket),
Other { packet_type: u8, bytes: Vec<u8> },
}
pub fn compound(packets: &[&[u8]]) -> Vec<u8> {
let total: usize = packets.iter().map(|p| p.len()).sum();
let mut buf = Vec::with_capacity(total);
for p in packets {
buf.extend_from_slice(p);
}
buf
}
pub fn parse_compound(buf: &[u8]) -> Result<Vec<RtcpPacket>, RtcpError> {
let mut out = Vec::new();
let mut off = 0;
while off < buf.len() {
if buf.len() - off < RTCP_HEADER_FIXED_LEN {
return Err(RtcpError::ShortHeader);
}
let pt = buf[off + 1];
let words = u16::from_be_bytes([buf[off + 2], buf[off + 3]]) as usize;
let sub_len = (words + 1) * 4;
if off + sub_len > buf.len() {
return Err(RtcpError::Truncated);
}
let sub = &buf[off..off + sub_len];
let parsed = match pt {
PT_SR | PT_RR => RtcpPacket::Report(parse_report(sub)?),
PT_SDES => RtcpPacket::Sdes(parse_sdes(sub)?),
PT_BYE => RtcpPacket::Bye(parse_bye(sub)?),
PT_APP => RtcpPacket::App(parse_app(sub)?),
other => RtcpPacket::Other {
packet_type: other,
bytes: sub.to_vec(),
},
};
out.push(parsed);
off += sub_len;
}
Ok(out)
}
fn utf8_lossy(bytes: &[u8]) -> String {
String::from_utf8_lossy(bytes).into_owned()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn sr_header_fields_and_length() {
let info = SenderInfo {
ntp_timestamp: 0xB44D_B705_2000_0000,
rtp_timestamp: 90_000,
packet_count: 7,
octet_count: 4242,
};
let bytes = build_sender_report(0xDEAD_BEEF, &info, &[]).unwrap();
assert_eq!(bytes.len(), RTCP_HEADER_LEN + SENDER_INFO_LEN);
assert_eq!(bytes[0], 0x80);
assert_eq!(bytes[1], PT_SR);
assert_eq!(u16::from_be_bytes([bytes[2], bytes[3]]), 6);
assert_eq!(
u32::from_be_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]),
0xDEAD_BEEF
);
assert_eq!(
u64::from_be_bytes(bytes[8..16].try_into().unwrap()),
0xB44D_B705_2000_0000
);
assert_eq!(
u32::from_be_bytes(bytes[16..20].try_into().unwrap()),
90_000
);
assert_eq!(u32::from_be_bytes(bytes[20..24].try_into().unwrap()), 7);
assert_eq!(u32::from_be_bytes(bytes[24..28].try_into().unwrap()), 4242);
}
#[test]
fn rr_empty_is_canonical() {
let bytes = build_receiver_report(0x1234_5678, &[]).unwrap();
assert_eq!(bytes.len(), RTCP_HEADER_LEN);
assert_eq!(bytes[0], 0x80); assert_eq!(bytes[1], PT_RR);
assert_eq!(u16::from_be_bytes([bytes[2], bytes[3]]), 1);
assert_eq!(
u32::from_be_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]),
0x1234_5678
);
}
#[test]
fn sr_round_trip_with_blocks() {
let info = SenderInfo {
ntp_timestamp: SenderInfo::ntp_from_parts(0xB705_2000 >> 16, 0x2000_0000),
rtp_timestamp: 12345,
packet_count: 100,
octet_count: 200_000,
};
let blocks = vec![
ReceptionReportBlock {
ssrc: 0xAAAA_AAAA,
fraction_lost: 13,
cumulative_lost: 4096,
extended_highest_seq: 0x0001_2345,
jitter: 88,
last_sr: 0xB705_2000,
delay_since_last_sr: 0x0005_4000,
},
ReceptionReportBlock {
ssrc: 0xBBBB_BBBB,
fraction_lost: 0,
cumulative_lost: -3, extended_highest_seq: 0x0002_0000,
jitter: 0,
last_sr: 0,
delay_since_last_sr: 0,
},
];
let bytes = build_sender_report(0xCAFE_F00D, &info, &blocks).unwrap();
assert_eq!(
bytes.len(),
RTCP_HEADER_LEN + SENDER_INFO_LEN + 2 * REPORT_BLOCK_LEN
);
assert_eq!(bytes[0] & 0x1F, 2);
let parsed = parse_report(&bytes).unwrap();
assert_eq!(parsed.packet_type, PT_SR);
assert_eq!(parsed.ssrc, 0xCAFE_F00D);
assert_eq!(parsed.sender_info, Some(info));
assert_eq!(parsed.report_blocks, blocks);
assert_eq!(parsed.report_blocks[1].cumulative_lost, -3);
}
#[test]
fn rr_round_trip_with_blocks() {
let blocks = vec![ReceptionReportBlock {
ssrc: 0x0102_0304,
fraction_lost: 255,
cumulative_lost: 0x007F_FFFF, extended_highest_seq: 0xFFFF_FFFF,
jitter: 0xDEAD_BEEF,
last_sr: 0x1122_3344,
delay_since_last_sr: 0x5566_7788,
}];
let bytes = build_receiver_report(0x9999_9999, &blocks).unwrap();
let parsed = parse_report(&bytes).unwrap();
assert_eq!(parsed.packet_type, PT_RR);
assert_eq!(parsed.ssrc, 0x9999_9999);
assert_eq!(parsed.sender_info, None);
assert_eq!(parsed.report_blocks, blocks);
assert_eq!(parsed.report_blocks[0].cumulative_lost, 0x007F_FFFF);
}
#[test]
fn cumulative_lost_min_negative_round_trips() {
let blocks = vec![ReceptionReportBlock {
ssrc: 1,
cumulative_lost: -8_388_608,
..Default::default()
}];
let bytes = build_receiver_report(2, &blocks).unwrap();
let parsed = parse_report(&bytes).unwrap();
assert_eq!(parsed.report_blocks[0].cumulative_lost, -8_388_608);
}
#[test]
fn build_rejects_more_than_31_blocks() {
let blocks = vec![ReceptionReportBlock::default(); 32];
assert_eq!(
build_receiver_report(0, &blocks),
Err(RtcpError::TooManyReportBlocks { count: 32 })
);
let info = SenderInfo::default();
assert_eq!(
build_sender_report(0, &info, &blocks),
Err(RtcpError::TooManyReportBlocks { count: 32 })
);
}
#[test]
fn exactly_31_blocks_is_allowed() {
let blocks = vec![ReceptionReportBlock::default(); MAX_REPORT_BLOCKS];
let bytes = build_receiver_report(0, &blocks).unwrap();
assert_eq!(bytes[0] & 0x1F, 31);
let parsed = parse_report(&bytes).unwrap();
assert_eq!(parsed.report_blocks.len(), 31);
}
#[test]
fn parse_rejects_short_header() {
for n in 0..RTCP_HEADER_LEN {
assert_eq!(parse_report(&vec![0x80u8; n]), Err(RtcpError::ShortHeader));
}
}
#[test]
fn parse_rejects_bad_version() {
let mut bytes = build_receiver_report(0, &[]).unwrap();
bytes[0] = 0x40; assert_eq!(
parse_report(&bytes),
Err(RtcpError::BadVersion { value: 1 })
);
}
#[test]
fn parse_rejects_unknown_pt() {
let mut bytes = build_receiver_report(0, &[]).unwrap();
bytes[1] = 202; assert_eq!(
parse_report(&bytes),
Err(RtcpError::UnexpectedPacketType { value: 202 })
);
}
#[test]
fn parse_rejects_truncated_sender_info() {
let info = SenderInfo::default();
let mut bytes = build_sender_report(0, &info, &[]).unwrap();
bytes.truncate(RTCP_HEADER_LEN + 4); assert_eq!(parse_report(&bytes), Err(RtcpError::Truncated));
}
#[test]
fn parse_rejects_truncated_report_block() {
let blocks = vec![ReceptionReportBlock::default(); 2];
let mut bytes = build_receiver_report(0, &blocks).unwrap();
bytes.truncate(bytes.len() - 4);
assert_eq!(parse_report(&bytes), Err(RtcpError::Truncated));
}
#[test]
fn parse_tolerates_trailing_extension() {
let info = SenderInfo::default();
let mut bytes = build_sender_report(0x55, &info, &[]).unwrap();
bytes.extend_from_slice(&[0xAB, 0xCD, 0xEF, 0x01]); let words = (bytes.len() / 4) as u16;
bytes[2..4].copy_from_slice(&(words - 1).to_be_bytes());
let parsed = parse_report(&bytes).unwrap();
assert_eq!(parsed.ssrc, 0x55);
assert!(parsed.report_blocks.is_empty());
}
#[test]
fn parse_flags_length_too_small() {
let blocks = vec![ReceptionReportBlock::default(); 1];
let mut bytes = build_receiver_report(0, &blocks).unwrap();
bytes[2..4].copy_from_slice(&0u16.to_be_bytes()); match parse_report(&bytes) {
Err(RtcpError::LengthMismatch { .. }) => {}
other => panic!("expected LengthMismatch, got {other:?}"),
}
}
#[test]
fn ntp_from_parts_packs_high_low() {
assert_eq!(
SenderInfo::ntp_from_parts(0xB44D_B705, 0x2000_0000),
0xB44D_B705_2000_0000
);
}
#[test]
fn sdes_cname_header_and_alignment() {
let bytes = build_cname_sdes(0xDEAD_BEEF, "alice@example.com").unwrap();
assert_eq!(bytes[0], 0x81);
assert_eq!(bytes[1], PT_SDES);
assert_eq!(bytes.len(), 28);
assert_eq!(bytes.len() % 4, 0);
assert_eq!(u16::from_be_bytes([bytes[2], bytes[3]]), 6);
assert_eq!(
u32::from_be_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]),
0xDEAD_BEEF
);
assert_eq!(bytes[8], sdes_type::CNAME);
assert_eq!(bytes[9], 17);
}
#[test]
fn sdes_round_trip_all_item_types() {
let chunk = SdesChunk {
ssrc: 0xCAFE_F00D,
items: vec![
SdesItem::Cname("doe@sleepy.example.com".to_string()),
SdesItem::Name("John Doe".to_string()),
SdesItem::Email("John.Doe@example.com".to_string()),
SdesItem::Phone("+1 908 555 1212".to_string()),
SdesItem::Loc("Murray Hill, New Jersey".to_string()),
SdesItem::Tool("oxideav-h261".to_string()),
SdesItem::Note("on the phone".to_string()),
SdesItem::Priv {
prefix: "x-oxideav".to_string(),
value: "round101".to_string(),
},
],
};
let bytes = build_sdes(std::slice::from_ref(&chunk)).unwrap();
assert_eq!(bytes.len() % 4, 0);
let parsed = parse_sdes(&bytes).unwrap();
assert_eq!(parsed.chunks.len(), 1);
assert_eq!(parsed.chunks[0], chunk);
}
#[test]
fn sdes_multiple_chunks_each_aligned() {
let chunks = vec![
SdesChunk {
ssrc: 0x1111_1111,
items: vec![SdesItem::Cname("a@h".to_string())],
},
SdesChunk {
ssrc: 0x2222_2222,
items: vec![SdesItem::Cname("bb@hh".to_string())],
},
];
let bytes = build_sdes(&chunks).unwrap();
assert_eq!(bytes[0] & 0x1F, 2); assert_eq!(bytes.len() % 4, 0);
let parsed = parse_sdes(&bytes).unwrap();
assert_eq!(parsed.chunks, chunks);
}
#[test]
fn sdes_empty_chunk_is_four_null_octets() {
let chunks = vec![SdesChunk {
ssrc: 0x0102_0304,
items: vec![],
}];
let bytes = build_sdes(&chunks).unwrap();
assert_eq!(bytes.len(), 12);
let parsed = parse_sdes(&bytes).unwrap();
assert_eq!(parsed.chunks[0].ssrc, 0x0102_0304);
assert!(parsed.chunks[0].items.is_empty());
}
#[test]
fn sdes_rejects_more_than_31_chunks() {
let chunks = vec![
SdesChunk {
ssrc: 0,
items: vec![]
};
32
];
assert_eq!(
build_sdes(&chunks),
Err(RtcpError::TooManySources { count: 32 })
);
}
#[test]
fn sdes_rejects_text_over_255() {
let chunks = vec![SdesChunk {
ssrc: 1,
items: vec![SdesItem::Cname("x".repeat(256))],
}];
assert_eq!(
build_sdes(&chunks),
Err(RtcpError::TextTooLong { len: 256 })
);
}
#[test]
fn sdes_priv_rejects_over_255() {
let chunks = vec![SdesChunk {
ssrc: 1,
items: vec![SdesItem::Priv {
prefix: "p".repeat(200),
value: "v".repeat(60),
}],
}];
assert_eq!(
build_sdes(&chunks),
Err(RtcpError::PrivTooLong { len: 261 })
);
}
#[test]
fn sdes_max_length_cname_round_trips() {
let chunks = vec![SdesChunk {
ssrc: 7,
items: vec![SdesItem::Cname("z".repeat(255))],
}];
let bytes = build_sdes(&chunks).unwrap();
let parsed = parse_sdes(&bytes).unwrap();
assert_eq!(parsed.chunks[0].items[0], SdesItem::Cname("z".repeat(255)));
}
#[test]
fn sdes_parse_skips_unknown_item_type() {
let mut buf = Vec::new();
write_count_header(&mut buf, 1, PT_SDES, 0); let chunk_start = buf.len();
buf.extend_from_slice(&0xABCD_1234u32.to_be_bytes());
buf.push(sdes_type::CNAME);
buf.push(2);
buf.extend_from_slice(b"hi");
buf.push(99);
buf.push(3);
buf.extend_from_slice(b"xyz");
buf.push(sdes_type::END);
while (buf.len() - chunk_start) % 4 != 0 {
buf.push(0);
}
let words = (buf.len() / 4) as u16;
buf[2..4].copy_from_slice(&(words - 1).to_be_bytes());
let parsed = parse_sdes(&buf).unwrap();
assert_eq!(
parsed.chunks[0].items,
vec![SdesItem::Cname("hi".to_string())]
);
}
#[test]
fn sdes_parse_rejects_wrong_pt() {
let mut bytes = build_cname_sdes(1, "a@b").unwrap();
bytes[1] = PT_BYE;
assert_eq!(
parse_sdes(&bytes),
Err(RtcpError::UnexpectedPacketType { value: PT_BYE })
);
}
#[test]
fn bye_no_reason_header_and_length() {
let bytes = build_bye(&[0xDEAD_BEEF, 0xCAFE_F00D], None).unwrap();
assert_eq!(bytes[0], 0x82);
assert_eq!(bytes[1], PT_BYE);
assert_eq!(bytes.len(), 12);
assert_eq!(u16::from_be_bytes([bytes[2], bytes[3]]), 2);
let parsed = parse_bye(&bytes).unwrap();
assert_eq!(parsed.sources, vec![0xDEAD_BEEF, 0xCAFE_F00D]);
assert_eq!(parsed.reason, None);
}
#[test]
fn bye_with_reason_round_trips_and_pads() {
let bytes = build_bye(&[0x1234_5678], Some("camera malfunction")).unwrap();
assert_eq!(bytes.len(), 28);
assert_eq!(bytes.len() % 4, 0);
let parsed = parse_bye(&bytes).unwrap();
assert_eq!(parsed.sources, vec![0x1234_5678]);
assert_eq!(parsed.reason.as_deref(), Some("camera malfunction"));
}
#[test]
fn bye_empty_reason_is_distinguished_from_none() {
let bytes = build_bye(&[1], Some("")).unwrap();
let parsed = parse_bye(&bytes).unwrap();
assert_eq!(parsed.reason.as_deref(), Some(""));
}
#[test]
fn bye_rejects_more_than_31_sources() {
let sources = vec![0u32; 32];
assert_eq!(
build_bye(&sources, None),
Err(RtcpError::TooManySources { count: 32 })
);
}
#[test]
fn bye_rejects_reason_over_255() {
assert_eq!(
build_bye(&[1], Some(&"r".repeat(256))),
Err(RtcpError::TextTooLong { len: 256 })
);
}
#[test]
fn bye_parse_rejects_truncated_sources() {
let mut bytes = build_bye(&[1, 2], None).unwrap();
bytes.truncate(RTCP_HEADER_FIXED_LEN + 4); assert_eq!(parse_bye(&bytes), Err(RtcpError::Truncated));
}
#[test]
fn app_header_layout_empty_data() {
let bytes = build_app(0, 0xDEAD_BEEF, b"TEST", &[]).unwrap();
assert_eq!(bytes.len(), 12);
assert_eq!(bytes[0], 0x80); assert_eq!(bytes[1], PT_APP);
assert_eq!(u16::from_be_bytes([bytes[2], bytes[3]]), 2);
assert_eq!(
u32::from_be_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]),
0xDEAD_BEEF
);
assert_eq!(&bytes[8..12], b"TEST");
}
#[test]
fn app_round_trip_with_data() {
let data = [0xAA, 0xBB, 0xCC, 0xDD, 0x01, 0x02, 0x03, 0x04];
let bytes = build_app(7, 0x1234_5678, b"oxAV", &data).unwrap();
assert_eq!(bytes.len(), 20);
assert_eq!(bytes[0] & 0x1F, 7);
let parsed = parse_app(&bytes).unwrap();
assert_eq!(parsed.subtype, 7);
assert_eq!(parsed.ssrc, 0x1234_5678);
assert_eq!(&parsed.name, b"oxAV");
assert_eq!(parsed.data, data);
}
#[test]
fn app_subtype_31_max_is_allowed() {
let bytes = build_app(31, 0, b"NAME", &[]).unwrap();
assert_eq!(bytes[0] & 0x1F, 31);
let parsed = parse_app(&bytes).unwrap();
assert_eq!(parsed.subtype, 31);
}
#[test]
fn app_rejects_subtype_over_31() {
assert_eq!(
build_app(32, 0, b"NAME", &[]),
Err(RtcpError::AppSubtypeOutOfRange { value: 32 })
);
assert_eq!(
build_app(255, 0, b"NAME", &[]),
Err(RtcpError::AppSubtypeOutOfRange { value: 255 })
);
}
#[test]
fn app_rejects_name_not_4_octets() {
for bad in [
b"".as_ref(),
b"x".as_ref(),
b"xyz".as_ref(),
b"toolong".as_ref(),
] {
assert_eq!(
build_app(0, 0, bad, &[]),
Err(RtcpError::AppNameWrongLength { len: bad.len() })
);
}
}
#[test]
fn app_rejects_data_not_32_bit_aligned() {
for bad_len in [1, 2, 3, 5, 6, 7, 9] {
let data = vec![0u8; bad_len];
assert_eq!(
build_app(0, 0, b"NAME", &data),
Err(RtcpError::AppDataNotAligned { len: bad_len })
);
}
}
#[test]
fn app_round_trip_large_data() {
let mut data = Vec::with_capacity(1024);
for i in 0..1024 {
data.push((i & 0xFF) as u8);
}
let bytes = build_app(3, 0xCAFE_F00D, b"BIG!", &data).unwrap();
let parsed = parse_app(&bytes).unwrap();
assert_eq!(parsed.subtype, 3);
assert_eq!(parsed.ssrc, 0xCAFE_F00D);
assert_eq!(&parsed.name, b"BIG!");
assert_eq!(parsed.data, data);
}
#[test]
fn app_name_is_byte_exact_not_case_folded() {
let a = build_app(0, 0, b"NaMe", &[]).unwrap();
let b = build_app(0, 0, b"name", &[]).unwrap();
assert_ne!(a, b);
let pa = parse_app(&a).unwrap();
let pb = parse_app(&b).unwrap();
assert_ne!(pa.name, pb.name);
}
#[test]
fn app_parse_rejects_short_header() {
let bytes = [0x80, PT_APP, 0x00, 0x02, 0, 0, 0, 0, b'X', b'X', b'X'];
assert_eq!(parse_app(&bytes), Err(RtcpError::ShortHeader));
}
#[test]
fn app_parse_rejects_bad_version() {
let mut bytes = build_app(0, 0, b"NAME", &[]).unwrap();
bytes[0] &= 0x3F;
assert_eq!(parse_app(&bytes), Err(RtcpError::BadVersion { value: 0 }));
}
#[test]
fn app_parse_rejects_wrong_pt() {
let mut bytes = build_app(0, 0, b"NAME", &[]).unwrap();
bytes[1] = PT_BYE;
assert_eq!(
parse_app(&bytes),
Err(RtcpError::UnexpectedPacketType { value: PT_BYE })
);
}
#[test]
fn app_parse_rejects_truncated_when_length_exceeds_buffer() {
let mut bytes = build_app(0, 0, b"NAME", &[0u8; 8]).unwrap();
bytes[2..4].copy_from_slice(&6u16.to_be_bytes());
assert_eq!(parse_app(&bytes), Err(RtcpError::Truncated));
}
#[test]
fn app_parse_ignores_trailing_bytes_past_stated_length() {
let mut bytes = build_app(2, 0xAA, b"app1", &[1, 2, 3, 4]).unwrap();
let original_len = bytes.len();
bytes.extend_from_slice(&[0xFF; 8]);
let parsed = parse_app(&bytes).unwrap();
assert_eq!(parsed.subtype, 2);
assert_eq!(parsed.data, vec![1, 2, 3, 4]);
assert_eq!(original_len, RTCP_HEADER_FIXED_LEN + 4 + APP_NAME_LEN + 4);
}
#[test]
fn compound_rr_sdes_bye_round_trips() {
let rr = build_receiver_report(0xAAAA_AAAA, &[]).unwrap();
let sdes = build_cname_sdes(0xAAAA_AAAA, "me@host").unwrap();
let bye = build_bye(&[0xAAAA_AAAA], Some("leaving")).unwrap();
let datagram = compound(&[&rr, &sdes, &bye]);
assert_eq!(datagram.len(), rr.len() + sdes.len() + bye.len());
let parsed = parse_compound(&datagram).unwrap();
assert_eq!(parsed.len(), 3);
match &parsed[0] {
RtcpPacket::Report(r) => {
assert_eq!(r.packet_type, PT_RR);
assert_eq!(r.ssrc, 0xAAAA_AAAA);
}
other => panic!("expected RR, got {other:?}"),
}
match &parsed[1] {
RtcpPacket::Sdes(s) => {
assert_eq!(
s.chunks[0].items,
vec![SdesItem::Cname("me@host".to_string())]
);
}
other => panic!("expected SDES, got {other:?}"),
}
match &parsed[2] {
RtcpPacket::Bye(b) => {
assert_eq!(b.sources, vec![0xAAAA_AAAA]);
assert_eq!(b.reason.as_deref(), Some("leaving"));
}
other => panic!("expected BYE, got {other:?}"),
}
}
#[test]
fn compound_sr_with_block_then_sdes() {
let info = SenderInfo {
ntp_timestamp: 0xB44D_B705_2000_0000,
rtp_timestamp: 90_000,
packet_count: 3,
octet_count: 1000,
};
let block = ReceptionReportBlock {
ssrc: 0xBBBB_BBBB,
fraction_lost: 5,
cumulative_lost: 2,
..Default::default()
};
let sr = build_sender_report(0x1357_9BDF, &info, &[block]).unwrap();
let sdes = build_cname_sdes(0x1357_9BDF, "cam@studio").unwrap();
let datagram = compound(&[&sr, &sdes]);
let parsed = parse_compound(&datagram).unwrap();
assert_eq!(parsed.len(), 2);
match &parsed[0] {
RtcpPacket::Report(r) => {
assert_eq!(r.packet_type, PT_SR);
assert_eq!(r.sender_info, Some(info));
assert_eq!(r.report_blocks, vec![block]);
}
other => panic!("expected SR, got {other:?}"),
}
}
#[test]
fn compound_rr_sdes_app_round_trips() {
let rr = build_receiver_report(0xBBBB_BBBB, &[]).unwrap();
let sdes = build_cname_sdes(0xBBBB_BBBB, "x@y").unwrap();
let app = build_app(15, 0xBBBB_BBBB, b"OXAV", &[0xDE, 0xAD, 0xBE, 0xEF]).unwrap();
let datagram = compound(&[&rr, &sdes, &app]);
let parsed = parse_compound(&datagram).unwrap();
assert_eq!(parsed.len(), 3);
match &parsed[2] {
RtcpPacket::App(a) => {
assert_eq!(a.subtype, 15);
assert_eq!(a.ssrc, 0xBBBB_BBBB);
assert_eq!(&a.name, b"OXAV");
assert_eq!(a.data, vec![0xDE, 0xAD, 0xBE, 0xEF]);
}
other => panic!("expected App, got {other:?}"),
}
}
#[test]
fn compound_preserves_unknown_packet_type() {
let rr = build_receiver_report(1, &[]).unwrap();
let fb = vec![0x80, 205, 0x00, 0x01, 0, 0, 0, 9];
let datagram = compound(&[&rr, &fb]);
let parsed = parse_compound(&datagram).unwrap();
assert_eq!(parsed.len(), 2);
match &parsed[1] {
RtcpPacket::Other { packet_type, bytes } => {
assert_eq!(*packet_type, 205);
assert_eq!(bytes, &fb);
}
other => panic!("expected Other, got {other:?}"),
}
}
#[test]
fn compound_parse_rejects_truncated_subpacket() {
let rr = build_receiver_report(1, &[]).unwrap();
let mut datagram = compound(&[&rr]);
datagram[2..4].copy_from_slice(&5u16.to_be_bytes());
assert_eq!(parse_compound(&datagram), Err(RtcpError::Truncated));
}
}