use base64::prelude::{BASE64_STANDARD, Engine as _};
use super::Packetizer;
use super::rtp::RtpHeader;
const DEFAULT_MTU: usize = 1400;
#[derive(Debug)]
pub struct H264Packetizer {
header: RtpHeader,
mtu: usize,
sps: Option<Vec<u8>>,
pps: Option<Vec<u8>>,
}
impl H264Packetizer {
pub fn new(pt: u8, ssrc: u32) -> Self {
Self {
header: RtpHeader::new(pt, ssrc),
mtu: DEFAULT_MTU,
sps: None,
pps: None,
}
}
pub fn with_random_ssrc(pt: u8) -> Self {
Self {
header: RtpHeader::with_random_ssrc(pt),
mtu: DEFAULT_MTU,
sps: None,
pps: None,
}
}
fn get_profile_level_id(&self) -> Result<String, String> {
let sps = self.sps.as_deref().ok_or("SPS not set")?;
if sps.len() < 4 {
return Err("SPS too short for profile-level-id".into());
}
Ok(format!("{:02x}{:02x}{:02x}", sps[1], sps[2], sps[3]))
}
fn get_sprop_parameter_sets(&self) -> Result<String, String> {
let sps = self.sps.as_deref().ok_or("SPS not set")?;
let pps = self.pps.as_deref().ok_or("PPS not set")?;
Ok(format!(
"{},{}",
BASE64_STANDARD.encode(sps),
BASE64_STANDARD.encode(pps)
))
}
fn packetize_nal(&mut self, nal_unit: &[u8], is_last_nal: bool) -> Vec<Vec<u8>> {
let mut packets = Vec::new();
if nal_unit.is_empty() {
return packets;
}
if nal_unit.len() <= self.mtu {
let hdr = self.header.write(is_last_nal);
let mut packet = Vec::with_capacity(12 + nal_unit.len());
packet.extend_from_slice(&hdr);
packet.extend_from_slice(nal_unit);
packets.push(packet);
} else {
let nal_header = nal_unit[0];
let nal_type = nal_header & 0x1f;
let nri = nal_header & 0x60;
let fu_indicator = nri | 28;
let payload = &nal_unit[1..];
let max_fragment = self.mtu - 2; let mut offset = 0usize;
let mut first = true;
while offset < payload.len() {
let remaining = payload.len() - offset;
let last_fragment = remaining <= max_fragment;
let chunk_size = std::cmp::min(max_fragment, remaining);
let chunk = &payload[offset..offset + chunk_size];
let start_bit = if first { 0x80 } else { 0x00 };
let end_bit = if last_fragment { 0x40 } else { 0x00 };
let fu_header = start_bit | end_bit | nal_type;
let marker = is_last_nal && last_fragment;
let hdr = self.header.write(marker);
let mut packet = Vec::with_capacity(12 + 2 + chunk.len());
packet.extend_from_slice(&hdr);
packet.push(fu_indicator);
packet.push(fu_header);
packet.extend_from_slice(chunk);
packets.push(packet);
offset += chunk_size;
first = false;
}
tracing::trace!(
nal_type,
nal_size = nal_unit.len(),
fragments = packets.len(),
"FU-A fragmented NAL unit"
);
}
packets
}
pub fn extract_nal_units(data: &[u8]) -> Vec<Vec<u8>> {
let mut nal_units = Vec::new();
let mut i = 0usize;
let mut start_entries: Vec<(usize, usize)> = Vec::new();
while i < data.len() {
if i + 3 < data.len() && data[i..i + 4] == [0, 0, 0, 1] {
start_entries.push((i + 4, 4));
i += 4;
} else if i + 2 < data.len() && data[i..i + 3] == [0, 0, 1] {
start_entries.push((i + 3, 3));
i += 3;
} else {
i += 1;
}
}
for (idx, &(start, _)) in start_entries.iter().enumerate() {
let end = if idx + 1 < start_entries.len() {
let (next_start, next_sc_len) = start_entries[idx + 1];
next_start - next_sc_len
} else {
data.len()
};
if start < end {
nal_units.push(data[start..end].to_vec());
}
}
nal_units
}
}
impl Packetizer for H264Packetizer {
fn packetize(&mut self, encoded_data: &[u8], timestamp_increment: u32) -> Vec<Vec<u8>> {
let nal_units = Self::extract_nal_units(encoded_data);
let mut packets = Vec::new();
if self.sps.is_none() || self.pps.is_none() {
for nal in &nal_units {
if nal.is_empty() {
continue;
}
let nal_type = nal[0] & 0x1f;
if nal_type == 7 && self.sps.is_none() {
self.sps = Some(nal.clone());
tracing::debug!("H.264 SPS captured from bitstream ({} bytes)", nal.len());
} else if nal_type == 8 && self.pps.is_none() {
self.pps = Some(nal.clone());
tracing::debug!("H.264 PPS captured from bitstream ({} bytes)", nal.len());
}
}
}
for (i, nal) in nal_units.iter().enumerate() {
let is_last = i == nal_units.len() - 1;
packets.append(&mut self.packetize_nal(nal, is_last));
}
self.header.advance_timestamp(timestamp_increment);
tracing::trace!(
nal_count = nal_units.len(),
rtp_packets = packets.len(),
frame_bytes = encoded_data.len(),
seq = self.header.sequence(),
ts = self.header.timestamp(),
"frame packetized"
);
packets
}
fn codec_name(&self) -> &'static str {
"H264"
}
fn clock_rate(&self) -> u32 {
90000
}
fn payload_type(&self) -> u8 {
self.header.pt
}
fn sdp_attributes(&self) -> Vec<String> {
let mut fmtp = format!("a=fmtp:{} packetization-mode=1", self.header.pt);
if let Ok(pl) = self.get_profile_level_id() {
fmtp.push_str(&format!(";profile-level-id={}", pl));
}
if let Ok(sprop) = self.get_sprop_parameter_sets() {
fmtp.push_str(&format!(";sprop-parameter-sets={}", sprop));
}
vec![
format!(
"a=rtpmap:{} {}/{}",
self.payload_type(),
self.codec_name(),
self.clock_rate()
),
fmtp,
"a=control:track1".to_string(),
]
}
fn next_sequence(&self) -> u16 {
self.header.sequence()
}
fn next_rtp_timestamp(&self) -> u32 {
self.header.timestamp() as u32
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_packetizer() -> H264Packetizer {
H264Packetizer::new(96, 0xAABBCCDD)
}
#[test]
fn extract_single_nal_4byte_sc() {
let data = [0, 0, 0, 1, 0x65, 0xAA, 0xBB];
let nals = H264Packetizer::extract_nal_units(&data);
assert_eq!(nals.len(), 1);
assert_eq!(nals[0], vec![0x65, 0xAA, 0xBB]);
}
#[test]
fn extract_single_nal_3byte_sc() {
let data = [0, 0, 1, 0x67, 0x42, 0x00];
let nals = H264Packetizer::extract_nal_units(&data);
assert_eq!(nals.len(), 1);
assert_eq!(nals[0], vec![0x67, 0x42, 0x00]);
}
#[test]
fn extract_two_nals_4byte_sc() {
let mut data = vec![0, 0, 0, 1, 0x67, 0x42];
data.extend_from_slice(&[0, 0, 0, 1, 0x68, 0xCE]);
let nals = H264Packetizer::extract_nal_units(&data);
assert_eq!(nals.len(), 2);
assert_eq!(nals[0], vec![0x67, 0x42]);
assert_eq!(nals[1], vec![0x68, 0xCE]);
}
#[test]
fn extract_mixed_start_codes() {
let mut data = vec![0, 0, 0, 1, 0x67, 0x42];
data.extend_from_slice(&[0, 0, 1, 0x68, 0xCE]);
let nals = H264Packetizer::extract_nal_units(&data);
assert_eq!(nals.len(), 2);
assert_eq!(nals[0], vec![0x67, 0x42]);
assert_eq!(nals[1], vec![0x68, 0xCE]);
}
#[test]
fn extract_empty_data() {
assert!(H264Packetizer::extract_nal_units(&[]).is_empty());
}
#[test]
fn extract_no_start_code() {
assert!(H264Packetizer::extract_nal_units(&[0xFF, 0xFE]).is_empty());
}
#[test]
fn small_nal_single_packet() {
let mut p = make_packetizer();
let nal = vec![0x65, 0xAA, 0xBB, 0xCC];
let packets = p.packetize_nal(&nal, true);
assert_eq!(packets.len(), 1);
assert_eq!(packets[0].len(), 12 + 4);
assert_eq!(packets[0][1] & 0x80, 0x80); }
#[test]
fn large_nal_fragmented() {
let mut p = H264Packetizer::new(96, 0x11223344);
let mut nal = vec![0x65]; nal.extend(vec![0xAA; DEFAULT_MTU + 500]);
let packets = p.packetize_nal(&nal, true);
assert!(packets.len() > 1);
assert_eq!(packets[0][12] & 0x1f, 28); assert_eq!(packets[0][13] & 0x80, 0x80);
let last = packets.last().unwrap();
assert_eq!(last[13] & 0x40, 0x40); assert_eq!(last[1] & 0x80, 0x80); }
#[test]
fn empty_nal_no_packets() {
let mut p = make_packetizer();
assert!(p.packetize_nal(&[], true).is_empty());
}
#[test]
fn packetize_trait_advances_timestamp() {
let mut p = make_packetizer();
let frame = [0, 0, 0, 1, 0x65, 0xAA, 0xBB];
p.packetize(&frame, 3000);
p.packetize(&frame, 3000);
let packets = p.packetize(&frame, 3000);
assert!(!packets.is_empty());
}
#[test]
fn sdp_attributes_include_packetization_mode() {
let p = make_packetizer();
let attrs = p.sdp_attributes();
assert!(attrs.len() >= 1, "must include at least fmtp");
assert!(
attrs.iter().any(|a| a.contains("packetization-mode=1")),
"must include packetization-mode=1"
);
}
#[test]
fn codec_metadata() {
let p = make_packetizer();
assert_eq!(p.codec_name(), "H264");
assert_eq!(p.clock_rate(), 90000);
assert_eq!(p.payload_type(), 96);
}
#[test]
fn auto_capture_sps_pps_from_first_frame() {
let mut p = H264Packetizer::new(96, 0xAABBCCDD);
let sps_nal = vec![0x67, 0x42, 0x00, 0x1e]; let pps_nal = vec![0x68, 0xce, 0x38, 0x80]; let frame = [
&[0u8, 0, 0, 1][..],
sps_nal.as_slice(),
&[0, 0, 0, 1][..],
pps_nal.as_slice(),
&[0, 0, 0, 1, 0x65, 0x88, 0x00][..], ]
.concat();
p.packetize(&frame, 3000);
let attrs = p.sdp_attributes();
let fmtp = attrs
.iter()
.find(|a| a.starts_with("a=fmtp:"))
.expect("fmtp line");
assert!(
fmtp.contains("profile-level-id="),
"SPS auto-captured, profile-level-id in SDP"
);
assert!(
fmtp.contains("sprop-parameter-sets="),
"SPS/PPS auto-captured, sprop-parameter-sets in SDP"
);
}
}