use crate::boxes::*;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SrppBox {
pub version: u8,
pub encryption_algorithm_rtp: u32,
pub encryption_algorithm_rtcp: u32,
pub integrity_algorithm_rtp: u32,
pub integrity_algorithm_rtcp: u32,
pub scheme_bytes: Vec<u8>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RtpHintSampleEntry {
pub format: [u8; 4],
pub data_reference_index: u16,
pub hint_track_version: u16,
pub highest_compatible_version: u16,
pub max_packet_size: u32,
pub timescale: Option<u32>,
pub time_offset: Option<i32>,
pub sequence_offset: Option<i32>,
pub srpp: Option<SrppBox>,
}
fn rd_u16(buf: &[u8], pos: &mut usize) -> Option<u16> {
if *pos + 2 > buf.len() {
return None;
}
let v = u16::from_be_bytes([buf[*pos], buf[*pos + 1]]);
*pos += 2;
Some(v)
}
fn rd_u32(buf: &[u8], pos: &mut usize) -> Option<u32> {
if *pos + 4 > buf.len() {
return None;
}
let v = u32::from_be_bytes([buf[*pos], buf[*pos + 1], buf[*pos + 2], buf[*pos + 3]]);
*pos += 4;
Some(v)
}
fn wrap(fourcc: &[u8; 4], body: &[u8]) -> Vec<u8> {
let total = (8 + body.len()) as u32;
let mut v = Vec::with_capacity(8 + body.len());
v.extend_from_slice(&total.to_be_bytes());
v.extend_from_slice(fourcc);
v.extend_from_slice(body);
v
}
fn each_child<F: FnMut([u8; 4], &[u8])>(body: &[u8], mut f: F) {
let mut pos = 0usize;
while pos + 8 <= body.len() {
let size = u32::from_be_bytes([body[pos], body[pos + 1], body[pos + 2], body[pos + 3]]);
let mut fourcc = [0u8; 4];
fourcc.copy_from_slice(&body[pos + 4..pos + 8]);
let total = if size == 0 {
body.len() - pos
} else {
size as usize
};
let end = match pos.checked_add(total) {
Some(e) if total >= 8 && e <= body.len() => e,
_ => break,
};
f(fourcc, &body[pos + 8..end]);
pos = end;
}
}
pub fn parse_srpp_box(body: &[u8]) -> Option<SrppBox> {
let mut p = 0usize;
if body.is_empty() {
return None;
}
let version = body[0];
p += 4; let encryption_algorithm_rtp = rd_u32(body, &mut p)?;
let encryption_algorithm_rtcp = rd_u32(body, &mut p)?;
let integrity_algorithm_rtp = rd_u32(body, &mut p)?;
let integrity_algorithm_rtcp = rd_u32(body, &mut p)?;
Some(SrppBox {
version,
encryption_algorithm_rtp,
encryption_algorithm_rtcp,
integrity_algorithm_rtp,
integrity_algorithm_rtcp,
scheme_bytes: body[p..].to_vec(),
})
}
pub fn build_srpp_box(b: &SrppBox) -> Vec<u8> {
let mut body = Vec::new();
body.push(b.version);
body.extend_from_slice(&[0, 0, 0]); body.extend_from_slice(&b.encryption_algorithm_rtp.to_be_bytes());
body.extend_from_slice(&b.encryption_algorithm_rtcp.to_be_bytes());
body.extend_from_slice(&b.integrity_algorithm_rtp.to_be_bytes());
body.extend_from_slice(&b.integrity_algorithm_rtcp.to_be_bytes());
body.extend_from_slice(&b.scheme_bytes);
wrap(&SRPP, &body)
}
pub fn parse_rtp_hint_sample_entry(format: [u8; 4], entry: &[u8]) -> Option<RtpHintSampleEntry> {
let mut p = 0usize;
if entry.len() < 8 {
return None;
}
p += 6;
let data_reference_index = rd_u16(entry, &mut p)?;
let hint_track_version = rd_u16(entry, &mut p)?;
let highest_compatible_version = rd_u16(entry, &mut p)?;
let max_packet_size = rd_u32(entry, &mut p)?;
let mut out = RtpHintSampleEntry {
format,
data_reference_index,
hint_track_version,
highest_compatible_version,
max_packet_size,
timescale: None,
time_offset: None,
sequence_offset: None,
srpp: None,
};
each_child(&entry[p..], |fourcc, child| match fourcc {
TIMS if child.len() >= 4 => {
out.timescale = Some(u32::from_be_bytes([child[0], child[1], child[2], child[3]]));
}
TSRO if child.len() >= 4 => {
out.time_offset = Some(i32::from_be_bytes([child[0], child[1], child[2], child[3]]));
}
SNRO if child.len() >= 4 => {
out.sequence_offset =
Some(i32::from_be_bytes([child[0], child[1], child[2], child[3]]));
}
SRPP => out.srpp = parse_srpp_box(child),
_ => {}
});
Some(out)
}
pub fn build_rtp_hint_sample_entry(e: &RtpHintSampleEntry) -> Vec<u8> {
let mut body = Vec::new();
body.extend_from_slice(&[0u8; 6]); body.extend_from_slice(&e.data_reference_index.to_be_bytes());
body.extend_from_slice(&e.hint_track_version.to_be_bytes());
body.extend_from_slice(&e.highest_compatible_version.to_be_bytes());
body.extend_from_slice(&e.max_packet_size.to_be_bytes());
if let Some(ts) = e.timescale {
body.extend_from_slice(&wrap(&TIMS, &ts.to_be_bytes()));
}
if let Some(off) = e.time_offset {
body.extend_from_slice(&wrap(&TSRO, &off.to_be_bytes()));
}
if let Some(off) = e.sequence_offset {
body.extend_from_slice(&wrap(&SNRO, &off.to_be_bytes()));
}
if let Some(srpp) = &e.srpp {
body.extend_from_slice(&build_srpp_box(srpp));
}
wrap(&e.format, &body)
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Mpeg2TsHintSampleEntry {
pub format: [u8; 4],
pub data_reference_index: u16,
pub hint_track_version: u16,
pub highest_compatible_version: u16,
pub preceding_bytes_len: u8,
pub trailing_bytes_len: u8,
pub precomputed_only: bool,
pub additional_data: Vec<u8>,
}
pub fn parse_mpeg2ts_hint_sample_entry(
format: [u8; 4],
entry: &[u8],
) -> Option<Mpeg2TsHintSampleEntry> {
let mut p = 0usize;
if entry.len() < 8 {
return None;
}
p += 6;
let data_reference_index = rd_u16(entry, &mut p)?;
let hint_track_version = rd_u16(entry, &mut p)?;
let highest_compatible_version = rd_u16(entry, &mut p)?;
if p + 3 > entry.len() {
return None;
}
let preceding_bytes_len = entry[p];
let trailing_bytes_len = entry[p + 1];
let precomputed_only = entry[p + 2] & 0x80 != 0;
p += 3;
Some(Mpeg2TsHintSampleEntry {
format,
data_reference_index,
hint_track_version,
highest_compatible_version,
preceding_bytes_len,
trailing_bytes_len,
precomputed_only,
additional_data: entry[p..].to_vec(),
})
}
pub fn build_mpeg2ts_hint_sample_entry(e: &Mpeg2TsHintSampleEntry) -> Vec<u8> {
let mut body = Vec::new();
body.extend_from_slice(&[0u8; 6]); body.extend_from_slice(&e.data_reference_index.to_be_bytes());
body.extend_from_slice(&e.hint_track_version.to_be_bytes());
body.extend_from_slice(&e.highest_compatible_version.to_be_bytes());
body.push(e.preceding_bytes_len);
body.push(e.trailing_bytes_len);
body.push(if e.precomputed_only { 0x80 } else { 0x00 });
body.extend_from_slice(&e.additional_data);
wrap(&e.format, &body)
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct MaxRate {
pub period: u32,
pub bytes: u32,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PayloadId {
pub payload_id: u32,
pub rtpmap: String,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct HintStatistics {
pub bytes_sent_with_rtp_64: Option<u64>,
pub packets_sent_64: Option<u64>,
pub bytes_sent_no_rtp_64: Option<u64>,
pub bytes_sent_with_rtp_32: Option<u32>,
pub packets_sent_32: Option<u32>,
pub bytes_sent_no_rtp_32: Option<u32>,
pub max_rates: Vec<MaxRate>,
pub media_bytes_sent: Option<u64>,
pub immediate_bytes_sent: Option<u64>,
pub repeated_bytes_sent: Option<u64>,
pub min_relative_time: Option<i32>,
pub max_relative_time: Option<i32>,
pub largest_packet: Option<u32>,
pub longest_packet: Option<u32>,
pub payload_ids: Vec<PayloadId>,
}
impl HintStatistics {
pub fn is_empty(&self) -> bool {
*self == HintStatistics::default()
}
}
fn rd_u64(buf: &[u8], pos: &mut usize) -> Option<u64> {
if *pos + 8 > buf.len() {
return None;
}
let mut a = [0u8; 8];
a.copy_from_slice(&buf[*pos..*pos + 8]);
*pos += 8;
Some(u64::from_be_bytes(a))
}
pub fn parse_hinf_box(body: &[u8]) -> HintStatistics {
let mut out = HintStatistics::default();
each_child(body, |fourcc, child| {
let mut p = 0usize;
match &fourcc {
b"trpy" => out.bytes_sent_with_rtp_64 = rd_u64(child, &mut p),
b"nump" => out.packets_sent_64 = rd_u64(child, &mut p),
b"tpyl" => out.bytes_sent_no_rtp_64 = rd_u64(child, &mut p),
b"totl" => out.bytes_sent_with_rtp_32 = rd_u32(child, &mut p),
b"npck" => out.packets_sent_32 = rd_u32(child, &mut p),
b"tpay" => out.bytes_sent_no_rtp_32 = rd_u32(child, &mut p),
b"maxr" => {
if let (Some(period), Some(bytes)) = (rd_u32(child, &mut p), rd_u32(child, &mut p))
{
out.max_rates.push(MaxRate { period, bytes });
}
}
b"dmed" => out.media_bytes_sent = rd_u64(child, &mut p),
b"dimm" => out.immediate_bytes_sent = rd_u64(child, &mut p),
b"drep" => out.repeated_bytes_sent = rd_u64(child, &mut p),
b"tmin" => out.min_relative_time = rd_u32(child, &mut p).map(|v| v as i32),
b"tmax" => out.max_relative_time = rd_u32(child, &mut p).map(|v| v as i32),
b"pmax" => out.largest_packet = rd_u32(child, &mut p),
b"dmax" => out.longest_packet = rd_u32(child, &mut p),
b"payt" => {
if let Some(payload_id) = rd_u32(child, &mut p) {
if p < child.len() {
let count = child[p] as usize;
p += 1;
let end = (p + count).min(child.len());
let rtpmap = String::from_utf8_lossy(&child[p..end]).into_owned();
out.payload_ids.push(PayloadId { payload_id, rtpmap });
}
}
}
_ => {}
}
});
out
}
pub fn build_hinf_box(s: &HintStatistics) -> Vec<u8> {
fn emit_u64(body: &mut Vec<u8>, fourcc: &[u8; 4], v: Option<u64>) {
if let Some(v) = v {
body.extend_from_slice(&wrap(fourcc, &v.to_be_bytes()));
}
}
fn emit_u32(body: &mut Vec<u8>, fourcc: &[u8; 4], v: Option<u32>) {
if let Some(v) = v {
body.extend_from_slice(&wrap(fourcc, &v.to_be_bytes()));
}
}
let mut body = Vec::new();
emit_u64(&mut body, b"trpy", s.bytes_sent_with_rtp_64);
emit_u64(&mut body, b"nump", s.packets_sent_64);
emit_u64(&mut body, b"tpyl", s.bytes_sent_no_rtp_64);
emit_u32(&mut body, b"totl", s.bytes_sent_with_rtp_32);
emit_u32(&mut body, b"npck", s.packets_sent_32);
emit_u32(&mut body, b"tpay", s.bytes_sent_no_rtp_32);
for m in &s.max_rates {
let mut b = Vec::with_capacity(8);
b.extend_from_slice(&m.period.to_be_bytes());
b.extend_from_slice(&m.bytes.to_be_bytes());
body.extend_from_slice(&wrap(b"maxr", &b));
}
emit_u64(&mut body, b"dmed", s.media_bytes_sent);
emit_u64(&mut body, b"dimm", s.immediate_bytes_sent);
emit_u64(&mut body, b"drep", s.repeated_bytes_sent);
emit_u32(&mut body, b"tmin", s.min_relative_time.map(|v| v as u32));
emit_u32(&mut body, b"tmax", s.max_relative_time.map(|v| v as u32));
emit_u32(&mut body, b"pmax", s.largest_packet);
emit_u32(&mut body, b"dmax", s.longest_packet);
for pid in &s.payload_ids {
let mut b = Vec::new();
b.extend_from_slice(&pid.payload_id.to_be_bytes());
b.push(pid.rtpmap.len() as u8);
b.extend_from_slice(pid.rtpmap.as_bytes());
body.extend_from_slice(&wrap(b"payt", &b));
}
wrap(&HINF, &body)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn each_child_rejects_oversize_child() {
let mut body = Vec::new();
body.extend_from_slice(&0xFFFF_FFFFu32.to_be_bytes());
body.extend_from_slice(b"junk");
let mut seen = 0usize;
each_child(&body, |_, _| seen += 1);
assert_eq!(seen, 0, "oversize child must be dropped, not walked");
}
fn unwrap_box<'a>(bytes: &'a [u8], fourcc: &[u8; 4]) -> &'a [u8] {
let total = u32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]) as usize;
assert_eq!(total, bytes.len());
assert_eq!(&bytes[4..8], fourcc);
&bytes[8..]
}
#[test]
fn rtp_entry_with_tims_only_round_trips() {
let e = RtpHintSampleEntry {
format: *b"rtp ",
data_reference_index: 1,
hint_track_version: 1,
highest_compatible_version: 1,
max_packet_size: 1450,
timescale: Some(90_000),
time_offset: None,
sequence_offset: None,
srpp: None,
};
let bytes = build_rtp_hint_sample_entry(&e);
let body = unwrap_box(&bytes, b"rtp ");
assert_eq!(parse_rtp_hint_sample_entry(*b"rtp ", body).unwrap(), e);
}
#[test]
fn rtp_entry_with_all_offsets_round_trips() {
let e = RtpHintSampleEntry {
format: *b"rrtp",
data_reference_index: 1,
hint_track_version: 1,
highest_compatible_version: 1,
max_packet_size: 1200,
timescale: Some(48_000),
time_offset: Some(-12345),
sequence_offset: Some(7777),
srpp: None,
};
let bytes = build_rtp_hint_sample_entry(&e);
let body = unwrap_box(&bytes, b"rrtp");
let parsed = parse_rtp_hint_sample_entry(*b"rrtp", body).unwrap();
assert_eq!(parsed, e);
assert_eq!(parsed.time_offset, Some(-12345));
assert_eq!(parsed.sequence_offset, Some(7777));
}
#[test]
fn srtp_entry_round_trips_with_srpp() {
let srpp = SrppBox {
version: 0,
encryption_algorithm_rtp: u32::from_be_bytes(*b"ACM1"),
encryption_algorithm_rtcp: u32::from_be_bytes(*b" "),
integrity_algorithm_rtp: u32::from_be_bytes(*b"SHM1"),
integrity_algorithm_rtcp: u32::from_be_bytes(*b" "),
scheme_bytes: {
let mut schm = vec![0u8, 0, 0, 0]; schm.extend_from_slice(b"srtp"); schm.extend_from_slice(&1u32.to_be_bytes()); let total = (8 + schm.len()) as u32;
let mut v = total.to_be_bytes().to_vec();
v.extend_from_slice(b"schm");
v.extend_from_slice(&schm);
v
},
};
let e = RtpHintSampleEntry {
format: *b"srtp",
data_reference_index: 1,
hint_track_version: 1,
highest_compatible_version: 1,
max_packet_size: 1400,
timescale: Some(90_000),
time_offset: Some(0),
sequence_offset: None,
srpp: Some(srpp.clone()),
};
let bytes = build_rtp_hint_sample_entry(&e);
let body = unwrap_box(&bytes, b"srtp");
let parsed = parse_rtp_hint_sample_entry(*b"srtp", body).unwrap();
assert_eq!(parsed, e);
assert_eq!(parsed.srpp.as_ref().unwrap(), &srpp);
}
#[test]
fn srpp_round_trips_empty_scheme_tail() {
let b = SrppBox {
version: 0,
encryption_algorithm_rtp: 0x2020_2020,
encryption_algorithm_rtcp: 0x2020_2020,
integrity_algorithm_rtp: 0x2020_2020,
integrity_algorithm_rtcp: 0x2020_2020,
scheme_bytes: vec![],
};
let bytes = build_srpp_box(&b);
let body = unwrap_box(&bytes, b"srpp");
assert_eq!(body.len(), 20);
assert_eq!(parse_srpp_box(body).unwrap(), b);
}
#[test]
fn entry_truncated_preamble_rejected() {
let short = [0u8; 8];
assert!(parse_rtp_hint_sample_entry(*b"rtp ", &short).is_none());
}
#[test]
fn rtcp_entry_uses_rtp_body() {
let e = RtpHintSampleEntry {
format: *b"rtcp",
data_reference_index: 1,
hint_track_version: 1,
highest_compatible_version: 1,
max_packet_size: 1500,
timescale: Some(90_000),
time_offset: None,
sequence_offset: None,
srpp: None,
};
let bytes = build_rtp_hint_sample_entry(&e);
let body = unwrap_box(&bytes, b"rtcp");
assert_eq!(parse_rtp_hint_sample_entry(*b"rtcp", body).unwrap(), e);
}
#[test]
fn mpeg2ts_server_entry_round_trips() {
let e = Mpeg2TsHintSampleEntry {
format: *b"sm2t",
data_reference_index: 1,
hint_track_version: 1,
highest_compatible_version: 1,
preceding_bytes_len: 4,
trailing_bytes_len: 16,
precomputed_only: true,
additional_data: vec![],
};
let bytes = build_mpeg2ts_hint_sample_entry(&e);
let body = unwrap_box(&bytes, b"sm2t");
assert_eq!(parse_mpeg2ts_hint_sample_entry(*b"sm2t", body).unwrap(), e);
}
#[test]
fn mpeg2ts_reception_entry_round_trips_with_additional_data() {
let extra = wrap(b"abcd", &[1, 2, 3]);
let e = Mpeg2TsHintSampleEntry {
format: *b"rm2t",
data_reference_index: 1,
hint_track_version: 1,
highest_compatible_version: 1,
preceding_bytes_len: 0,
trailing_bytes_len: 0,
precomputed_only: false,
additional_data: extra.clone(),
};
let bytes = build_mpeg2ts_hint_sample_entry(&e);
let body = unwrap_box(&bytes, b"rm2t");
let parsed = parse_mpeg2ts_hint_sample_entry(*b"rm2t", body).unwrap();
assert_eq!(parsed, e);
assert!(!parsed.precomputed_only);
assert_eq!(parsed.additional_data, extra);
}
#[test]
fn mpeg2ts_entry_truncated_rejected() {
let mut short = vec![0u8; 6];
short.extend_from_slice(&1u16.to_be_bytes()); short.extend_from_slice(&1u16.to_be_bytes()); short.extend_from_slice(&1u16.to_be_bytes()); assert!(parse_mpeg2ts_hint_sample_entry(*b"sm2t", &short).is_none());
}
#[test]
fn hinf_full_round_trips() {
let s = HintStatistics {
bytes_sent_with_rtp_64: Some(1_000_000),
packets_sent_64: Some(5000),
bytes_sent_no_rtp_64: Some(940_000),
bytes_sent_with_rtp_32: Some(123),
packets_sent_32: Some(7),
bytes_sent_no_rtp_32: Some(99),
max_rates: vec![
MaxRate {
period: 1000,
bytes: 64_000,
},
MaxRate {
period: 2000,
bytes: 120_000,
},
],
media_bytes_sent: Some(800_000),
immediate_bytes_sent: Some(50_000),
repeated_bytes_sent: Some(10_000),
min_relative_time: Some(-50),
max_relative_time: Some(50),
largest_packet: Some(1452),
longest_packet: Some(33),
payload_ids: vec![PayloadId {
payload_id: 96,
rtpmap: "H264/90000".to_string(),
}],
};
let bytes = build_hinf_box(&s);
let body = unwrap_box(&bytes, b"hinf");
assert_eq!(parse_hinf_box(body), s);
assert!(!s.is_empty());
}
#[test]
fn hinf_partial_round_trips() {
let s = HintStatistics {
bytes_sent_with_rtp_64: Some(42),
packets_sent_64: Some(1),
..HintStatistics::default()
};
let bytes = build_hinf_box(&s);
let body = unwrap_box(&bytes, b"hinf");
let parsed = parse_hinf_box(body);
assert_eq!(parsed, s);
assert_eq!(parsed.bytes_sent_with_rtp_64, Some(42));
assert!(parsed.bytes_sent_no_rtp_64.is_none());
assert!(parsed.max_rates.is_empty());
}
#[test]
fn hinf_empty_body_is_empty() {
assert!(parse_hinf_box(&[]).is_empty());
}
#[test]
fn hinf_multiple_maxr_preserved() {
let s = HintStatistics {
max_rates: vec![
MaxRate {
period: 100,
bytes: 10,
},
MaxRate {
period: 200,
bytes: 20,
},
MaxRate {
period: 300,
bytes: 30,
},
],
..HintStatistics::default()
};
let bytes = build_hinf_box(&s);
let body = unwrap_box(&bytes, b"hinf");
let parsed = parse_hinf_box(body);
assert_eq!(parsed.max_rates.len(), 3);
assert_eq!(parsed.max_rates[2].period, 300);
}
#[test]
fn unknown_additional_data_box_ignored() {
let e = RtpHintSampleEntry {
format: *b"rtp ",
data_reference_index: 1,
hint_track_version: 1,
highest_compatible_version: 1,
max_packet_size: 1000,
timescale: Some(8000),
time_offset: None,
sequence_offset: None,
srpp: None,
};
let mut bytes = build_rtp_hint_sample_entry(&e);
let extra = wrap(b"abcd", &[1, 2, 3, 4]);
let new_total = (bytes.len() + extra.len()) as u32;
bytes[0..4].copy_from_slice(&new_total.to_be_bytes());
bytes.extend_from_slice(&extra);
let body = unwrap_box(&bytes, b"rtp ");
let parsed = parse_rtp_hint_sample_entry(*b"rtp ", body).unwrap();
assert_eq!(parsed.timescale, Some(8000));
assert!(parsed.srpp.is_none());
}
}