use crate::TrackId;
use alloc::collections::VecDeque;
#[cfg(any(feature = "std", test))]
use alloc::format;
use alloc::string::String;
use alloc::vec::Vec;
use broadcast_common::{Package, Parse, Serialize, Unpackage};
use bytes::Bytes;
use rtp_packet::RtpPacket as RtpPacketWire;
use crate::annexb::NAL_LENGTH_SIZE;
use crate::error::{Error, Result};
use crate::ir::InputDegradation;
use crate::media::Media;
use crate::pipeline::CodecConfig;
const RTP_HEADER_LEN: usize = rtp_packet::FIXED_HEADER_LEN;
const RTP_PT_MASK: u8 = 0x7F;
pub const DEFAULT_VIDEO_PT: u8 = 96;
pub const DEFAULT_AUDIO_PT: u8 = 97;
pub const DEFAULT_MTU: usize = 1400;
pub const LOCAL_CONNECTION_ADDRESS: core::net::IpAddr =
core::net::IpAddr::V4(core::net::Ipv4Addr::LOCALHOST);
const DYNAMIC_PT_MIN: u8 = 96;
const DYNAMIC_PT_MAX: u8 = 127;
pub const VIDEO_CLOCK_RATE: u32 = 90_000;
pub const DEFAULT_AAC_CLOCK_RATE: u32 = 48_000;
pub const DEFAULT_KLV_PT: u8 = 98;
pub const KLV_ENCODING_NAME: &str = "smpte336m";
const NAL_TYPE_MASK: u8 = 0x1F;
const NAL_FNRI_MASK: u8 = 0xE0;
const NAL_TYPE_STAP_A: u8 = 24;
const NAL_TYPE_FU_A: u8 = 28;
const FU_START_MASK: u8 = 0x80;
const FU_END_MASK: u8 = 0x40;
const STAP_A_SIZE_LEN: usize = 2;
pub const NAL_TYPE_IDR: u8 = 5;
const AAC_SIZE_LENGTH: u32 = 13;
const AAC_INDEX_LENGTH: u32 = 3;
#[cfg(feature = "std")]
const AAC_INDEX_DELTA_LENGTH: u32 = 3;
const AAC_AU_HEADER_LEN: usize = 2;
const AAC_AU_HEADERS_LENGTH_LEN: usize = 2;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[non_exhaustive]
pub enum RtpMediaKind {
H264,
Aac,
}
impl RtpMediaKind {
pub fn name(&self) -> &'static str {
match self {
RtpMediaKind::H264 => "video",
RtpMediaKind::Aac => "audio",
}
}
}
broadcast_common::impl_spec_display!(RtpMediaKind);
#[derive(Debug, Clone)]
pub struct RtpPacket {
pub header: Bytes,
pub payload: Bytes,
}
impl RtpPacket {
pub fn as_contiguous(&self) -> Bytes {
use bytes::BytesMut;
let mut buf = BytesMut::with_capacity(self.header.len() + self.payload.len());
buf.extend_from_slice(&self.header);
buf.extend_from_slice(&self.payload);
buf.freeze()
}
}
#[derive(Debug, Clone)]
pub struct RtpStream {
pub pt: u8,
pub kind: RtpMediaKind,
pub packets: Vec<RtpPacket>,
}
#[derive(Debug, Clone)]
pub struct RtpOutput {
pub streams: Vec<RtpStream>,
#[cfg(feature = "std")]
pub sdp: String,
}
#[derive(Debug, Clone)]
pub struct RtpPacketiser {
pub mtu: usize,
pub video_pt: u8,
pub audio_pt: u8,
pub ssrc: u32,
pub stap_a_parameter_sets: bool,
}
impl Default for RtpPacketiser {
fn default() -> Self {
Self {
mtu: DEFAULT_MTU,
video_pt: DEFAULT_VIDEO_PT,
audio_pt: DEFAULT_AUDIO_PT,
ssrc: 0x1234_5678,
stap_a_parameter_sets: true,
}
}
}
impl RtpPacketiser {
pub fn new() -> Self {
Self::default()
}
}
struct PtAllocator {
next: u8,
}
impl PtAllocator {
fn new(base: u8) -> Self {
let next = if (DYNAMIC_PT_MIN..=DYNAMIC_PT_MAX).contains(&base) {
base
} else {
DYNAMIC_PT_MIN
};
Self { next }
}
fn reserve_through(&mut self, base: u8) {
self.next = self.next.max(base.saturating_add(1));
}
fn allocate(&mut self) -> Option<u8> {
loop {
if self.next > DYNAMIC_PT_MAX {
return None;
}
let pt = self.next;
self.next = self.next.checked_add(1)?;
if pt != DEFAULT_KLV_PT {
return Some(pt);
}
}
}
}
struct SeqCounter(u16);
impl SeqCounter {
fn new(start: u16) -> Self {
Self(start)
}
fn next(&mut self) -> u16 {
let v = self.0;
self.0 = self.0.wrapping_add(1);
v
}
}
fn rtp_header(pt: u8, marker: bool, seq: u16, timestamp: u32, ssrc: u32) -> Bytes {
let pkt = RtpPacketWire {
marker,
payload_type: pt & RTP_PT_MASK,
sequence_number: seq,
timestamp,
ssrc,
csrc: Vec::new(),
extension: None,
padding: None,
payload: &[],
};
let len = pkt.serialized_len();
let mut buf = bytes::BytesMut::with_capacity(len);
buf.resize(len, 0);
pkt.serialize_into(&mut buf)
.expect("simple V=2 P=0 X=0 CC=0 header always serializes");
buf.freeze()
}
impl Package for RtpPacketiser {
type Media = Media;
type Output = RtpOutput;
type Error = Error;
fn package(&mut self, media: &Media) -> Result<RtpOutput> {
if media.tracks.is_empty() {
return Err(Error::InvalidInput(
"cannot packetise a Media with no tracks",
));
}
let mut streams = Vec::new();
#[cfg(feature = "std")]
let mut sdp_media: Vec<sdp_types::Media> = Vec::new();
let mut pts = PtAllocator::new(self.video_pt.min(self.audio_pt));
let mut next_video_pt = Some(self.video_pt);
let mut next_audio_pt = Some(self.audio_pt);
pts.reserve_through(self.video_pt);
pts.reserve_through(self.audio_pt);
for track in &media.tracks {
match &track.spec.config {
CodecConfig::Avc { config, .. } => {
let pt = match next_video_pt.take() {
Some(base) => {
pts.reserve_through(base);
base
}
None => pts.allocate().ok_or(Error::InvalidInput(
"no dynamic RTP payload type left (RFC 3551 §6's range is 96-127): too many streams share this session",
))?,
};
let packets = self.packetise_video(track, pt)?;
streams.push(RtpStream {
pt,
kind: RtpMediaKind::H264,
packets,
});
#[cfg(feature = "std")]
sdp_media.push(sdp_video(pt, &config.config));
#[cfg(not(feature = "std"))]
let _ = config;
}
CodecConfig::Aac {
esds,
channel_count,
sample_rate,
..
} => {
let pt = match next_audio_pt.take() {
Some(base) => {
pts.reserve_through(base);
base
}
None => pts.allocate().ok_or(Error::InvalidInput(
"no dynamic RTP payload type left (RFC 3551 §6's range is 96-127): too many streams share this session",
))?,
};
let clock = if track.spec.timescale != 0 {
track.spec.timescale
} else {
*sample_rate
};
let packets = self.packetise_audio(track, pt, clock)?;
streams.push(RtpStream {
pt,
kind: RtpMediaKind::Aac,
packets,
});
let asc = asc_bytes(esds)?;
#[cfg(feature = "std")]
sdp_media.push(sdp_audio(pt, clock, *channel_count, asc));
#[cfg(not(feature = "std"))]
let _ = (asc, channel_count);
}
_ => {
return Err(Error::InvalidInput(
"RTP packetiser supports only AVC video and AAC audio tracks",
));
}
}
}
if streams.is_empty() {
return Err(Error::InvalidInput(
"no AVC/AAC tracks to packetise into RTP",
));
}
Ok(RtpOutput {
streams,
#[cfg(feature = "std")]
sdp: build_sdp(sdp_media),
})
}
}
impl RtpPacketiser {
pub fn packetise_video(&self, track: &crate::media::Track, pt: u8) -> Result<Vec<RtpPacket>> {
let timescale = if track.spec.timescale != 0 {
track.spec.timescale
} else {
VIDEO_CLOCK_RATE
};
let mut packets = Vec::new();
let mut seq = SeqCounter::new(0);
let mut timestamp: u32 = 0;
if self.stap_a_parameter_sets
&& let CodecConfig::Avc { config, .. } = &track.spec.config
{
let mut param_nals: Vec<Vec<u8>> = Vec::new();
for sps in &config.config.sps {
param_nals.push(sps.0.clone());
}
for pps in &config.config.pps {
param_nals.push(pps.0.clone());
}
if !param_nals.is_empty() {
let pkt = build_stap_a(pt, ¶m_nals, &mut seq, timestamp, self.ssrc)?;
packets.push(pkt);
}
}
for (i, sample) in track.samples.iter().enumerate() {
timestamp = rescale_ts(sample_pts(track, i), timescale, VIDEO_CLOCK_RATE);
let nals = split_length_prefixed(&sample.data)?;
if nals.is_empty() {
continue;
}
let last_nal = nals.len() - 1;
for (n, nal) in nals.iter().enumerate() {
let is_last_nal = n == last_nal;
if nal.len() + RTP_HEADER_LEN <= self.mtu {
let marker = is_last_nal;
let header = rtp_header(pt, marker, seq.next(), timestamp, self.ssrc);
let nal_offset = nal.as_ptr() as usize - sample.data.as_ptr() as usize;
let payload = sample.data.slice(nal_offset..nal_offset + nal.len());
packets.push(RtpPacket { header, payload });
} else {
fragment_fu_a(
nal,
&sample.data,
pt,
is_last_nal,
self.mtu,
&mut seq,
timestamp,
self.ssrc,
&mut packets,
)?;
}
}
}
Ok(packets)
}
fn packetise_audio(
&self,
track: &crate::media::Track,
pt: u8,
clock: u32,
) -> Result<Vec<RtpPacket>> {
let mut packets = Vec::with_capacity(track.samples.len());
let mut seq = SeqCounter::new(0);
let timescale = if track.spec.timescale != 0 {
track.spec.timescale
} else {
clock
};
let per_packet = self
.mtu
.checked_sub(RTP_HEADER_LEN + AAC_AU_HEADERS_LENGTH_LEN + AAC_AU_HEADER_LEN)
.filter(|&b| b > 0)
.ok_or(Error::InvalidInput(
"MTU too small for an AAC-hbr packet header",
))?;
for (i, sample) in track.samples.iter().enumerate() {
let au = &sample.data;
if au.len() >= (1usize << AAC_SIZE_LENGTH) {
return Err(Error::InvalidValue {
field: "aac_au_size",
value: au.len() as u64,
reason: "exceeds 13-bit AAC-hbr AU-size field",
});
}
let timestamp = rescale_ts(sample_pts(track, i), timescale, clock);
let au_headers_len_bits = (AAC_AU_HEADER_LEN * 8) as u16;
let hdr = (au.len() as u16) << AAC_INDEX_LENGTH;
let num_frags = au.len().div_ceil(per_packet).max(1);
for f in 0..num_frags {
let start = f * per_packet;
let end = (start + per_packet).min(au.len());
let is_last = f == num_frags - 1;
let rtp_hdr = rtp_header(pt, is_last, seq.next(), timestamp, self.ssrc);
let frag = &au[start..end];
let mut buf = bytes::BytesMut::with_capacity(
rtp_hdr.len() + AAC_AU_HEADERS_LENGTH_LEN + AAC_AU_HEADER_LEN + frag.len(),
);
buf.extend_from_slice(&rtp_hdr);
buf.extend_from_slice(&au_headers_len_bits.to_be_bytes());
buf.extend_from_slice(&hdr.to_be_bytes());
buf.extend_from_slice(frag);
let full = buf.freeze();
let header_len = rtp_hdr.len() + AAC_AU_HEADERS_LENGTH_LEN + AAC_AU_HEADER_LEN;
packets.push(RtpPacket {
header: full.slice(0..header_len),
payload: full.slice(header_len..),
});
}
}
Ok(packets)
}
}
fn sample_pts(track: &crate::media::Track, i: usize) -> u64 {
if let (Some(first), Some(cur)) = (
track.samples.first().and_then(|s| s.pts),
track.samples.get(i).and_then(|s| s.pts),
) {
return u64::try_from((cur - first).max(0)).unwrap_or(0);
}
track.samples[..i]
.iter()
.map(|s| u64::from(s.duration.unwrap_or(0)))
.sum()
}
fn rescale_ts(ticks: u64, from: u32, to: u32) -> u32 {
if from == 0 || from == to {
return ticks as u32;
}
((ticks * to as u64 + from as u64 / 2) / from as u64) as u32
}
fn split_length_prefixed(data: &[u8]) -> Result<Vec<&[u8]>> {
crate::annexb::iter_length_prefixed_nals(data)
}
fn build_stap_a(
pt: u8,
nals: &[Vec<u8>],
seq: &mut SeqCounter,
timestamp: u32,
ssrc: u32,
) -> Result<RtpPacket> {
let mut max_nri = 0u8;
let mut forbidden = 0u8;
for nal in nals {
if let Some(&octet) = nal.first() {
max_nri = max_nri.max(octet & 0x60);
forbidden |= octet & 0x80;
}
}
let stap_hdr = forbidden | max_nri | NAL_TYPE_STAP_A;
let total_nal_bytes: usize = nals.iter().map(|n| n.len() + STAP_A_SIZE_LEN).sum();
let rtp_hdr = rtp_header(pt, false, seq.next(), timestamp, ssrc);
let total = rtp_hdr.len() + 1 + total_nal_bytes;
let mut buf = bytes::BytesMut::with_capacity(total);
buf.extend_from_slice(&rtp_hdr);
buf.extend_from_slice(&[stap_hdr]);
for nal in nals {
if nal.len() > u16::MAX as usize {
return Err(Error::InvalidValue {
field: "stap_a_nal_size",
value: nal.len() as u64,
reason: "exceeds 16-bit STAP-A size prefix",
});
}
buf.extend_from_slice(&(nal.len() as u16).to_be_bytes());
buf.extend_from_slice(nal);
}
let full = buf.freeze();
let header = full.slice(0..rtp_hdr.len());
let payload = full.slice(rtp_hdr.len()..);
Ok(RtpPacket { header, payload })
}
#[allow(clippy::too_many_arguments)]
fn fragment_fu_a(
nal: &[u8],
sample_data: &Bytes,
pt: u8,
au_is_last_nal: bool,
mtu: usize,
seq: &mut SeqCounter,
timestamp: u32,
ssrc: u32,
out: &mut Vec<RtpPacket>,
) -> Result<()> {
if nal.is_empty() {
return Err(Error::InvalidInput("cannot FU-A fragment an empty NAL"));
}
let nal_octet = nal[0];
let fnri = nal_octet & NAL_FNRI_MASK;
let nal_type = nal_octet & NAL_TYPE_MASK;
let fu_indicator = fnri | NAL_TYPE_FU_A;
let payload = &nal[1..];
let per_packet = mtu
.checked_sub(RTP_HEADER_LEN + 2)
.filter(|&b| b > 0)
.ok_or(Error::InvalidInput("MTU too small for FU-A fragmentation"))?;
let base_offset = nal.as_ptr() as usize - sample_data.as_ptr() as usize + 1;
let total = payload.len();
let num_frags = total.div_ceil(per_packet).max(1);
for f in 0..num_frags {
let start = f * per_packet;
let end = (start + per_packet).min(total);
let is_start = f == 0;
let is_end = f == num_frags - 1;
let mut fu_header = nal_type;
if is_start {
fu_header |= FU_START_MASK;
}
if is_end {
fu_header |= FU_END_MASK;
}
let marker = is_end && au_is_last_nal;
let rtp_hdr = rtp_header(pt, marker, seq.next(), timestamp, ssrc);
let mut header_buf = bytes::BytesMut::with_capacity(rtp_hdr.len() + 2);
header_buf.extend_from_slice(&rtp_hdr);
header_buf.extend_from_slice(&[fu_indicator, fu_header]);
let header = header_buf.freeze();
let slice_start = base_offset + start;
let slice_end = base_offset + end;
let payload_slice = sample_data.slice(slice_start..slice_end);
out.push(RtpPacket {
header,
payload: payload_slice,
});
}
Ok(())
}
fn asc_bytes(esds: &crate::mp4esds::EsdsBox) -> Result<&[u8]> {
esds.es_descriptor
.decoder_config
.as_ref()
.and_then(|dc| dc.decoder_specific_info.as_ref())
.map(|dsi| dsi.data.as_slice())
.ok_or(Error::InvalidInput(
"AAC esds has no DecoderSpecificInfo (AudioSpecificConfig)",
))
}
#[cfg(feature = "std")]
fn build_sdp(medias: Vec<sdp_types::Media>) -> String {
build_sdp_with_connection(LOCAL_CONNECTION_ADDRESS, medias)
}
#[cfg(feature = "std")]
pub fn build_sdp_with_connection(
connection_address: core::net::IpAddr,
medias: Vec<sdp_types::Media>,
) -> String {
let mut session = sdp_types::Session::new(
sdp_types::Origin::with_ip_addr(0, 0, core::net::Ipv4Addr::LOCALHOST),
"transmux RTP",
);
session.connection = Some(sdp_types::Connection::from_ip_addr(connection_address));
session.medias = medias;
let mut out = Vec::new();
let _ = session.write(&mut out);
String::from_utf8_lossy(&out).into_owned()
}
#[cfg(feature = "std")]
fn sdp_video(
pt: u8,
config: &crate::avc_config::AVCDecoderConfigurationRecord,
) -> sdp_types::Media {
let profile_level_id = format!(
"{:02X}{:02X}{:02X}",
config.profile_indication, config.profile_compatibility, config.level_indication
);
let sprop = config
.sps
.iter()
.map(|n| base64_encode(&n.0))
.chain(config.pps.iter().map(|n| base64_encode(&n.0)))
.collect::<Vec<_>>()
.join(",");
let mut media = sdp_types::Media::new(
sdp_types::MediaType::Video,
0,
sdp_types::TransportProto::RtpAvp,
pt,
);
media.add_attribute(sdp_types::RtpMap::new(pt, "H264", VIDEO_CLOCK_RATE));
media.add_attribute_with_value(
"fmtp",
format!(
"{pt} packetization-mode=1; profile-level-id={profile_level_id}; sprop-parameter-sets={sprop}"
),
);
media
}
#[cfg(feature = "std")]
fn sdp_audio(pt: u8, clock: u32, channels: u16, asc: &[u8]) -> sdp_types::Media {
let config = hex_encode(asc);
let mut media = sdp_types::Media::new(
sdp_types::MediaType::Audio,
0,
sdp_types::TransportProto::RtpAvp,
pt,
);
media.add_attribute(sdp_types::RtpMap::with_encoding_params(
pt,
"mpeg4-generic",
clock,
channels,
));
media.add_attribute_with_value(
"fmtp",
format!(
"{pt} streamtype=5; profile-level-id=1; mode=AAC-hbr; config={config}; \
sizeLength={AAC_SIZE_LENGTH}; indexLength={AAC_INDEX_LENGTH}; \
indexDeltaLength={AAC_INDEX_DELTA_LENGTH}"
),
);
media
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct RtpInputStream {
pub kind: RtpMediaKind,
pub clock_rate: u32,
pub config: Option<CodecConfig>,
clock_rate_explicit: bool,
pub packets: Vec<Vec<u8>>,
}
impl RtpInputStream {
pub fn new(kind: RtpMediaKind, packets: Vec<Vec<u8>>) -> Self {
Self {
kind,
clock_rate: match kind {
RtpMediaKind::H264 => VIDEO_CLOCK_RATE,
RtpMediaKind::Aac => DEFAULT_AAC_CLOCK_RATE,
},
config: None,
clock_rate_explicit: false,
packets,
}
}
pub fn with_config(mut self, config: CodecConfig) -> Self {
if let CodecConfig::Aac { sample_rate, .. } = &config {
if !self.clock_rate_explicit {
self.clock_rate = *sample_rate;
}
}
self.config = Some(config);
self
}
pub fn with_clock_rate(mut self, clock_rate: u32) -> Self {
self.clock_rate = clock_rate;
self.clock_rate_explicit = true;
self
}
}
impl RtpInputStream {
fn validated_clock_rate(&self) -> Result<u32> {
match &self.config {
Some(CodecConfig::Aac { sample_rate, .. }) => {
if self.kind != RtpMediaKind::Aac {
return Err(Error::InvalidInput(
"an RTP stream carrying an AAC config must have kind Aac: the \
payload format is what the packets are parsed as",
));
}
if self.clock_rate != *sample_rate {
return Err(Error::InvalidValue {
field: "rtp_clock_rate",
value: u64::from(self.clock_rate),
reason: "an MPEG-4 audio stream's RTP clock rate is its \
sampling rate (RFC 3640 §3.1), so it must equal the \
AudioSpecificConfig's sample rate",
});
}
Ok(*sample_rate)
}
Some(CodecConfig::Avc { .. }) => {
if self.kind != RtpMediaKind::H264 {
return Err(Error::InvalidInput(
"an RTP stream carrying an AVC config must have kind H264: the \
payload format is what the packets are parsed as",
));
}
if self.clock_rate != VIDEO_CLOCK_RATE {
return Err(Error::InvalidValue {
field: "rtp_clock_rate",
value: u64::from(self.clock_rate),
reason: "RFC 6184 §8.1 fixes the H.264 RTP clock rate at 90 kHz",
});
}
Ok(self.clock_rate)
}
Some(_) | None => {
if self.clock_rate == 0 {
return Err(Error::InvalidInput(
"RTP stream has no clock rate: an RTP timestamp's unit is the \
stream's own clock (RFC 3550 §5.1), so a track cannot be timed without it",
));
}
Ok(self.clock_rate)
}
}
}
}
#[derive(Debug, Clone)]
pub struct RtpInput {
pub streams: Vec<RtpInputStream>,
}
#[derive(Debug, Default, Clone)]
pub struct RtpDepacketiser {
timing_warnings: VecDeque<RtpTimingWarning>,
dropped_timing_warnings: u64,
}
impl RtpDepacketiser {
pub fn new() -> Self {
Self::default()
}
pub fn poll_timing_warning(&mut self) -> Option<RtpTimingWarning> {
self.timing_warnings.pop_front()
}
pub fn dropped_timing_warnings(&self) -> u64 {
self.dropped_timing_warnings
}
}
pub const MAX_TIMING_WARNINGS: usize = 1024;
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RtpTimingWarning {
ReorderedPresentationTimestamps {
stream_index: usize,
previous_pts: u64,
pts: u64,
},
}
impl Unpackage for RtpDepacketiser {
type Input = RtpInput;
type Media = Media;
type Error = Error;
fn unpackage(&mut self, input: RtpInput) -> Result<Media> {
self.timing_warnings.clear();
self.dropped_timing_warnings = 0;
let mut tracks = Vec::new();
let mut degraded: Vec<InputDegradation> = Vec::new();
for (idx, stream) in input.streams.iter().enumerate() {
let clock_rate = stream.validated_clock_rate()?;
let (samples, bad) = match stream.kind {
RtpMediaKind::H264 => reassemble_video_lenient(&stream.packets),
RtpMediaKind::Aac => reassemble_audio_lenient(&stream.packets),
};
degraded.extend(
bad.into_iter()
.map(|i| InputDegradation::MalformedUnit { position: i as u64 }),
);
let mut prev: Option<i64> = None;
let mut wrap_check = RtpWrapState::default();
for au in &samples {
let pts = wrap_check.push(au.timestamp);
if let Some(previous_pts) = prev
&& pts < previous_pts
{
if self.timing_warnings.len() == MAX_TIMING_WARNINGS {
self.timing_warnings.pop_front();
self.dropped_timing_warnings += 1;
}
self.timing_warnings.push_back(
RtpTimingWarning::ReorderedPresentationTimestamps {
stream_index: idx,
previous_pts: u64::try_from(previous_pts).unwrap_or(0),
pts: u64::try_from(pts).unwrap_or(0),
},
);
}
prev = Some(pts);
}
tracks.push(RtpTrack {
id: TrackId::new(u32::try_from(idx).unwrap_or(u32::MAX).saturating_add(1)),
kind: stream.kind,
clock_rate,
config: stream.config.clone(),
samples,
});
}
let mut media = rtp_tracks_to_media(tracks)?;
media.degraded = degraded;
Ok(media)
}
}
struct RtpTrack {
id: TrackId,
kind: RtpMediaKind,
clock_rate: u32,
config: Option<CodecConfig>,
samples: Vec<ReassembledAu>,
}
const RTP_TS_WRAP: i64 = 1 << 32;
const RTP_TS_WRAP_HALF: i64 = RTP_TS_WRAP / 2;
#[derive(Default)]
struct RtpWrapState {
initialized: bool,
prev_raw: u32,
prev_uw: i64,
}
impl RtpWrapState {
fn push(&mut self, raw: u32) -> i64 {
if !self.initialized {
self.initialized = true;
self.prev_raw = raw;
self.prev_uw = raw as i64;
return self.prev_uw;
}
let mut delta = raw as i64 - self.prev_raw as i64;
if delta > RTP_TS_WRAP_HALF {
delta -= RTP_TS_WRAP; } else if delta < -RTP_TS_WRAP_HALF {
delta += RTP_TS_WRAP; }
let uw = self.prev_uw + delta;
self.prev_raw = raw;
self.prev_uw = uw;
uw
}
}
fn decode_timeline(pts: &[i64]) -> (Vec<i64>, Vec<i64>) {
if pts.windows(2).all(|w| w[1] >= w[0]) {
return (pts.to_vec(), pts.to_vec());
}
let mut sorted: Vec<i64> = pts.to_vec();
sorted.sort_unstable();
let latch = sorted
.iter()
.zip(pts.iter())
.map(|(s, p)| s.saturating_sub(*p))
.max()
.unwrap_or(0)
.max(0);
let mut dts: Vec<i64> = Vec::with_capacity(pts.len());
let mut shifted_pts: Vec<i64> = Vec::with_capacity(pts.len());
for (s, p) in sorted.iter().zip(pts.iter()) {
let d = *s - latch;
dts.push(d);
shifted_pts.push((*p).max(d));
}
(dts, shifted_pts)
}
fn duration_from(delta: i64) -> u32 {
u32::try_from(delta.max(0)).unwrap_or(u32::MAX)
}
fn rtp_tracks_to_media(tracks: Vec<RtpTrack>) -> Result<Media> {
use crate::pipeline::{Sample, TrackSpec};
let mut prepared: Vec<(RtpTrack, TrackSpec, Vec<i64>, Vec<i64>)> =
Vec::with_capacity(tracks.len());
for t in tracks {
let spec = track_spec(&t)?;
let mut wrap = RtpWrapState::default();
let pts_series: Vec<i64> = t.samples.iter().map(|au| wrap.push(au.timestamp)).collect();
let (dts_series, pts_series) = decode_timeline(&pts_series);
prepared.push((t, spec, dts_series, pts_series));
}
let origin = prepared
.iter()
.flat_map(|(_, _, dts, _)| dts.iter().copied())
.min()
.unwrap_or(0)
.min(0);
let ir_tracks = prepared
.into_iter()
.map(
|(t, spec, dts_series, pts_series)| -> Result<crate::media::Track> {
let dts_series: Vec<i64> =
dts_series.iter().map(|d| (*d - origin).max(0)).collect();
let pts_series: Vec<i64> =
pts_series.iter().map(|p| (*p - origin).max(0)).collect();
let n = dts_series.len();
let mut previous_nonzero_duration: Option<i64> =
dts_series.windows(2).map(|w| w[1] - w[0]).find(|d| *d > 0);
let samples: Vec<Sample> = t
.samples
.iter()
.enumerate()
.map(|(i, au)| {
let pts = pts_series[i];
let dts = dts_series[i];
let delta = if i + 1 < n {
let d = dts_series[i + 1] - dts;
if d > 0 {
Some(d)
} else {
previous_nonzero_duration
}
} else if n >= 2 {
let d = dts - dts_series[i - 1];
if d > 0 {
Some(d)
} else {
previous_nonzero_duration
}
} else {
Some(1)
};
if let Some(d) = delta
&& d > 0
{
previous_nonzero_duration = Some(d);
}
let duration = delta.map(|d| duration_from(d.max(1)));
Sample {
data: au.data.clone().into(),
dts: Some(dts),
pts: Some(pts),
duration,
flags: crate::ir::SampleFlags::new(au.is_sync),
provenance: None,
display: None,
}
})
.collect();
let anchor = samples
.first()
.and_then(|s| s.dts)
.map(|d| u64::try_from(d.max(0)).unwrap_or(0))
.unwrap_or(0);
Ok(crate::media::Track::new_at(spec, samples, anchor))
},
)
.collect::<Result<Vec<_>>>()?;
Ok(Media::new(ir_tracks, VIDEO_CLOCK_RATE))
}
fn track_spec(t: &RtpTrack) -> Result<crate::pipeline::TrackSpec> {
use crate::pipeline::TrackSpec;
let config = match (&t.config, t.kind) {
(Some(c), _) => c.clone(),
(None, RtpMediaKind::H264) => placeholder_avc_config(),
(None, RtpMediaKind::Aac) => {
return Err(Error::InvalidInput(
"an AAC RTP stream needs its AudioSpecificConfig: RTP carries no codec config, so it must come from the session's SDP `config=` parameter (RFC 3640 §4.1) - a fabricated one would name a sample rate and channel count the stream may not have",
));
}
};
Ok(TrackSpec::new(t.id, t.clock_rate, config))
}
fn placeholder_avc_config() -> CodecConfig {
use crate::avc_config::{AVCConfigurationBox, AVCDecoderConfigurationRecord};
let record = AVCDecoderConfigurationRecord {
configuration_version: 1,
profile_indication: 0,
profile_compatibility: 0,
level_indication: 0,
length_size_minus_one: (NAL_LENGTH_SIZE - 1) as u8,
sps: Vec::new(),
pps: Vec::new(),
chroma_format: None,
bit_depth_luma_minus8: None,
bit_depth_chroma_minus8: None,
sps_ext: Vec::new(),
};
CodecConfig::Avc {
config: AVCConfigurationBox::new(record),
width: 0,
height: 0,
}
}
pub(crate) struct ReassembledAu {
pub timestamp: u32,
pub is_sync: bool,
pub data: Vec<u8>,
}
pub(crate) fn reassemble_video(packets: &[Vec<u8>]) -> Result<Vec<ReassembledAu>> {
reassemble_video_impl(packets, None)
}
pub(crate) fn reassemble_video_lenient(packets: &[Vec<u8>]) -> (Vec<ReassembledAu>, Vec<usize>) {
let mut bad = Vec::new();
let aus = reassemble_video_impl(packets, Some(&mut bad)).unwrap_or_default();
(aus, bad)
}
fn reassemble_video_impl(
packets: &[Vec<u8>],
mut lenient: Option<&mut Vec<usize>>,
) -> Result<Vec<ReassembledAu>> {
let mut aus: Vec<ReassembledAu> = Vec::new();
let mut cur_nals: Vec<Vec<u8>> = Vec::new();
let mut cur_ts: Option<u32> = None;
let mut fu_buf: Vec<u8> = Vec::new();
let mut fu_active = false;
let mut last_seq: Option<u16> = None;
fn flush_au(aus: &mut Vec<ReassembledAu>, nals: &mut Vec<Vec<u8>>, ts: u32) {
if nals.is_empty() {
return;
}
let is_sync = nals
.iter()
.any(|n| !n.is_empty() && (n[0] & NAL_TYPE_MASK) == NAL_TYPE_IDR);
aus.push(ReassembledAu {
timestamp: ts,
is_sync,
data: length_prefix_nals(nals),
});
nals.clear();
}
'pkts: for (pkt_idx, pkt) in packets.iter().enumerate() {
macro_rules! bad {
($e:expr) => {{
match lenient.as_deref_mut() {
Some(skipped) => {
skipped.push(pkt_idx);
continue 'pkts;
}
None => return Err($e),
}
}};
}
let hdr = match parse_rtp_header(pkt) {
Ok(h) => h,
Err(e) => bad!(e),
};
let payload = hdr.payload;
if payload.is_empty() {
continue;
}
let gap = last_seq.is_some_and(|previous| previous.wrapping_add(1) != hdr.sequence);
last_seq = Some(hdr.sequence);
if gap {
cur_nals.clear();
fu_buf.clear();
fu_active = false;
}
if let Some(ts) = cur_ts
&& ts != hdr.timestamp
&& !cur_nals.is_empty()
{
flush_au(&mut aus, &mut cur_nals, ts);
}
cur_ts = Some(hdr.timestamp);
let nal_type = payload[0] & NAL_TYPE_MASK;
match nal_type {
NAL_TYPE_STAP_A => {
let mut off = 1usize;
let mut stap_nals: Vec<Vec<u8>> = Vec::new();
while off < payload.len() {
if off + STAP_A_SIZE_LEN > payload.len() {
bad!(Error::BufferTooShort {
need: off + STAP_A_SIZE_LEN,
have: payload.len(),
what: "STAP-A size prefix",
});
}
let size = u16::from_be_bytes([payload[off], payload[off + 1]]) as usize;
off += STAP_A_SIZE_LEN;
let end = off + size;
if end > payload.len() {
bad!(Error::BufferTooShort {
need: end,
have: payload.len(),
what: "STAP-A NAL",
});
}
stap_nals.push(payload[off..end].to_vec());
off = end;
}
cur_nals.extend(stap_nals);
}
NAL_TYPE_FU_A => {
if payload.len() < 2 {
bad!(Error::BufferTooShort {
need: 2,
have: payload.len(),
what: "FU-A header",
});
}
let fu_indicator = payload[0];
let fu_header = payload[1];
let is_start = fu_header & FU_START_MASK != 0;
let is_end = fu_header & FU_END_MASK != 0;
let orig_type = fu_header & NAL_TYPE_MASK;
let fnri = fu_indicator & NAL_FNRI_MASK;
if is_start {
fu_buf.clear();
fu_buf.push(fnri | orig_type);
fu_active = true;
}
if !fu_active {
continue;
}
fu_buf.extend_from_slice(&payload[2..]);
if is_end {
cur_nals.push(core::mem::take(&mut fu_buf));
fu_active = false;
}
}
_ => cur_nals.push(payload.to_vec()),
}
if hdr.marker && !cur_nals.is_empty() && !fu_active {
let ts = hdr.timestamp;
flush_au(&mut aus, &mut cur_nals, ts);
cur_ts = None;
}
}
if let Some(ts) = cur_ts {
flush_au(&mut aus, &mut cur_nals, ts);
}
Ok(aus)
}
pub(crate) fn reassemble_audio(packets: &[Vec<u8>]) -> Result<Vec<ReassembledAu>> {
reassemble_audio_impl(packets, None)
}
pub(crate) fn reassemble_audio_lenient(packets: &[Vec<u8>]) -> (Vec<ReassembledAu>, Vec<usize>) {
let mut bad = Vec::new();
let aus = reassemble_audio_impl(packets, Some(&mut bad)).unwrap_or_default();
(aus, bad)
}
fn reassemble_audio_impl(
packets: &[Vec<u8>],
mut lenient: Option<&mut Vec<usize>>,
) -> Result<Vec<ReassembledAu>> {
let mut aus = Vec::new();
struct PendingAu {
timestamp: u32,
size: usize,
seq: u16,
gap: bool,
data: Vec<u8>,
}
let mut pending: Option<PendingAu> = None;
fn complete_if_exact(aus: &mut Vec<ReassembledAu>, pending: &mut Option<PendingAu>) {
if pending
.as_ref()
.is_some_and(|p| !p.gap && !p.data.is_empty() && p.data.len() == p.size)
&& let Some(p) = pending.take()
{
aus.push(ReassembledAu {
timestamp: p.timestamp,
is_sync: true,
data: p.data,
});
}
}
fn discard(pending: &mut Option<PendingAu>) {
pending.take();
}
'pkts: for (pkt_idx, pkt) in packets.iter().enumerate() {
macro_rules! bad {
($e:expr) => {{
match lenient.as_deref_mut() {
Some(skipped) => {
skipped.push(pkt_idx);
continue 'pkts;
}
None => return Err($e),
}
}};
}
let hdr = match parse_rtp_header(pkt) {
Ok(h) => h,
Err(e) => bad!(e),
};
let payload = hdr.payload;
if payload.len() < AAC_AU_HEADERS_LENGTH_LEN {
bad!(Error::BufferTooShort {
need: AAC_AU_HEADERS_LENGTH_LEN,
have: payload.len(),
what: "AAC AU-headers-length",
});
}
let au_headers_len_bits = u16::from_be_bytes([payload[0], payload[1]]) as usize;
let header_bytes = au_headers_len_bits.div_ceil(8);
let num_headers = au_headers_len_bits / (AAC_AU_HEADER_LEN * 8);
let mut off = AAC_AU_HEADERS_LENGTH_LEN;
if off + header_bytes > payload.len() {
bad!(Error::BufferTooShort {
need: off + header_bytes,
have: payload.len(),
what: "AAC AU headers",
});
}
let mut sizes = Vec::with_capacity(num_headers);
for h in 0..num_headers {
let hoff = off + h * AAC_AU_HEADER_LEN;
let ah = u16::from_be_bytes([payload[hoff], payload[hoff + 1]]);
sizes.push((ah >> AAC_INDEX_LENGTH) as usize);
}
off += header_bytes;
if pending
.as_ref()
.is_some_and(|p| p.timestamp != hdr.timestamp)
{
complete_if_exact(&mut aus, &mut pending);
discard(&mut pending);
}
for size in sizes {
let available = payload.len().saturating_sub(off);
let take = size.min(available);
match pending.as_mut() {
Some(p) if p.size == size => {
if p.seq.wrapping_add(1) == hdr.sequence {
p.data.extend_from_slice(&payload[off..off + take]);
p.seq = hdr.sequence;
} else {
complete_if_exact(&mut aus, &mut pending);
discard(&mut pending);
pending = Some(PendingAu {
timestamp: hdr.timestamp,
size,
seq: hdr.sequence,
gap: true,
data: payload[off..off + take].to_vec(),
});
}
}
Some(_) => {
complete_if_exact(&mut aus, &mut pending);
discard(&mut pending);
pending = Some(PendingAu {
timestamp: hdr.timestamp,
size,
seq: hdr.sequence,
gap: false,
data: payload[off..off + take].to_vec(),
});
}
None => {
pending = Some(PendingAu {
timestamp: hdr.timestamp,
size,
seq: hdr.sequence,
gap: false,
data: payload[off..off + take].to_vec(),
});
}
}
off += take;
complete_if_exact(&mut aus, &mut pending);
}
if hdr.marker {
complete_if_exact(&mut aus, &mut pending);
discard(&mut pending);
}
}
complete_if_exact(&mut aus, &mut pending);
Ok(aus)
}
fn length_prefix_nals(nals: &[Vec<u8>]) -> Vec<u8> {
let total: usize = nals.iter().map(|n| NAL_LENGTH_SIZE + n.len()).sum();
let mut out = Vec::with_capacity(total);
for nal in nals {
out.extend_from_slice(&(nal.len() as u32).to_be_bytes());
out.extend_from_slice(nal);
}
out
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct RtpHeader<'a> {
pub(crate) marker: bool,
#[allow(dead_code)]
payload_type: u8,
pub(crate) sequence: u16,
pub(crate) timestamp: u32,
pub(crate) ssrc: u32,
payload: &'a [u8],
}
pub(crate) fn parse_rtp_header(pkt: &[u8]) -> Result<RtpHeader<'_>> {
let parsed = RtpPacketWire::parse(pkt).map_err(map_rtp_error)?;
Ok(RtpHeader {
marker: parsed.marker,
payload_type: parsed.payload_type,
sequence: parsed.sequence_number,
timestamp: parsed.timestamp,
ssrc: parsed.ssrc,
payload: parsed.payload,
})
}
fn map_rtp_error(e: rtp_packet::Error) -> Error {
match e {
rtp_packet::Error::BufferTooShort { need, have, what } => {
Error::BufferTooShort { need, have, what }
}
rtp_packet::Error::InvalidVersion(v) => Error::InvalidValue {
field: "rtp_version",
value: u64::from(v),
reason: "must be 2",
},
rtp_packet::Error::InvalidValue {
field,
value,
reason,
} => Error::InvalidValue {
field,
value,
reason,
},
rtp_packet::Error::InvalidPadding { count, reason } => Error::InvalidValue {
field: "rtp_padding",
value: u64::from(count),
reason,
},
rtp_packet::Error::ExtensionNotWordAligned { data_len } => Error::InvalidValue {
field: "rtp_extension_length",
value: data_len as u64,
reason: "extension data length is not a multiple of 4 bytes",
},
_ => Error::InvalidInput("invalid RTP header"),
}
}
pub fn packetise_klv(
klv_unit: &Bytes,
pt: u8,
seq_start: u16,
timestamp: u32,
ssrc: u32,
mtu: usize,
) -> Result<Vec<RtpPacket>> {
if klv_unit.is_empty() {
return Err(Error::InvalidInput("cannot packetise an empty KLV unit"));
}
let per_packet = mtu
.checked_sub(RTP_HEADER_LEN)
.filter(|&b| b > 0)
.ok_or(Error::InvalidInput("MTU too small for KLV-over-RTP"))?;
let total = klv_unit.len();
let num_frags = total.div_ceil(per_packet).max(1);
let mut seq = SeqCounter::new(seq_start);
let mut packets = Vec::with_capacity(num_frags);
for f in 0..num_frags {
let start = f * per_packet;
let end = (start + per_packet).min(total);
let is_last = f == num_frags - 1;
let header = rtp_header(pt, is_last, seq.next(), timestamp, ssrc);
let payload = klv_unit.slice(start..end);
packets.push(RtpPacket { header, payload });
}
Ok(packets)
}
pub fn depacketise_klv(packets: &[Vec<u8>]) -> Result<Vec<Vec<u8>>> {
let mut units: Vec<Vec<u8>> = Vec::new();
let mut cur: Vec<u8> = Vec::new();
let mut cur_ts: Option<u32> = None;
for pkt in packets {
let hdr = parse_rtp_header(pkt)?;
if let Some(ts) = cur_ts
&& ts != hdr.timestamp
&& !cur.is_empty()
{
units.push(core::mem::take(&mut cur));
}
cur_ts = Some(hdr.timestamp);
cur.extend_from_slice(hdr.payload);
if hdr.marker {
units.push(core::mem::take(&mut cur));
cur_ts = None;
}
}
if !cur.is_empty() {
units.push(cur);
}
Ok(units)
}
use base64::Engine as _;
use base64::engine::general_purpose::STANDARD;
use base64::engine::{DecodePaddingMode, GeneralPurpose, GeneralPurposeConfig};
const LENIENT: GeneralPurpose = GeneralPurpose::new(
&base64::alphabet::STANDARD,
GeneralPurposeConfig::new()
.with_decode_padding_mode(DecodePaddingMode::Indifferent)
.with_decode_allow_trailing_bits(true),
);
pub fn base64_encode(data: &[u8]) -> String {
STANDARD.encode(data)
}
pub fn base64_decode(s: &str) -> Result<Vec<u8>> {
LENIENT.decode(s).map_err(|e| match e {
base64::DecodeError::InvalidByte(_, byte) => Error::InvalidValue {
field: "base64",
value: u64::from(byte),
reason: "not a base64 character",
},
_ => Error::InvalidValue {
field: "base64",
value: s.len() as u64,
reason: "invalid base64 length",
},
})
}
#[cfg(any(feature = "std", test))]
use broadcast_common::hex::hex_encode;
pub fn hex_decode(s: &str) -> Result<Vec<u8>> {
::hex::decode(s).map_err(|e| match e {
::hex::FromHexError::OddLength => Error::InvalidValue {
field: "hex",
value: s.len() as u64,
reason: "odd-length hex string",
},
::hex::FromHexError::InvalidHexCharacter { c, .. } => Error::InvalidValue {
field: "hex",
value: u64::from(c),
reason: "not a hex digit",
},
::hex::FromHexError::InvalidStringLength => Error::InvalidValue {
field: "hex",
value: s.len() as u64,
reason: "invalid hex string length",
},
})
}
#[cfg(test)]
mod tests {
use super::*;
fn pseudo_random_pts(n: usize, seed: u64) -> alloc::vec::Vec<i64> {
let mut state = seed | 1;
let mut next = move || {
state ^= state << 13;
state ^= state >> 7;
state ^= state << 17;
state
};
let mut out = alloc::vec::Vec::with_capacity(n);
let mut t = 0i64;
for _ in 0..n {
t += 1000 * (1 + (next() % 4) as i64);
out.push(t);
}
for i in (1..out.len()).rev() {
let j = (next() % (i as u64 + 1)) as usize;
out.swap(i, j);
}
out
}
#[test]
fn equal_timestamp_aus_never_get_a_zero_duration() {
let packets: alloc::vec::Vec<alloc::vec::Vec<u8>> = [0u32, 3000, 3000, 6000]
.iter()
.enumerate()
.map(|(i, ts)| {
let mut p = alloc::vec![0x80u8, 0x80 | 96];
p.extend_from_slice(&(i as u16).to_be_bytes());
p.extend_from_slice(&ts.to_be_bytes());
p.extend_from_slice(&[0, 0, 0, 0]);
p.extend_from_slice(&[0x41, 0xAA]);
p
})
.collect();
let ir = RtpDepacketiser::new()
.unpackage(crate::rtp::RtpInput {
streams: alloc::vec![crate::rtp::RtpInputStream::new(RtpMediaKind::H264, packets,)],
})
.expect("depacketise");
let samples = &ir.tracks[0].samples;
assert_eq!(samples.len(), 4);
for (i, s) in samples.iter().enumerate() {
let d = s.duration.expect("a multi-sample track times every sample");
if i + 1 < samples.len() {
assert!(
d > 0,
"non-final sample {i} must not have a zero duration: {:?}",
samples
.iter()
.map(|s| s.duration)
.collect::<alloc::vec::Vec<_>>()
);
}
}
assert_eq!(samples[1].duration, Some(3000));
assert_eq!(
samples[2].duration,
Some(3000),
"the duplicate instant must reuse the previous non-zero duration"
);
}
#[test]
fn decode_timeline_matches_a_brute_force_reference() {
let pts = pseudo_random_pts(2000, 0x9E37_79B9_7F4A_7C15);
let (dts, shifted) = decode_timeline(&pts);
let reference = brute_force_decode_timeline(&pts);
assert_eq!(dts, reference.0, "dts must match the reference");
assert_eq!(shifted, reference.1, "pts must match the reference");
}
fn brute_force_decode_timeline(pts: &[i64]) -> (alloc::vec::Vec<i64>, alloc::vec::Vec<i64>) {
let mut sorted = pts.to_vec();
sorted.sort_unstable();
let mut latch = 0i64;
for (s, p) in sorted.iter().zip(pts.iter()) {
if s - p > latch {
latch = s - p;
}
}
let mut dts = alloc::vec::Vec::new();
let mut out_pts = alloc::vec::Vec::new();
for (s, p) in sorted.iter().zip(pts.iter()) {
let d = *s - latch;
dts.push(d);
out_pts.push((*p).max(d));
}
(dts, out_pts)
}
#[test]
fn decode_timeline_is_not_quadratic() {
let pts = pseudo_random_pts(200_000, 0x1234_5678_9ABC_DEF0);
let (dts, shifted) = decode_timeline(&pts);
assert_eq!(dts.len(), 200_000);
assert_eq!(shifted.len(), 200_000);
assert!(
dts.windows(2).all(|w| w[1] >= w[0]),
"dts must be non-decreasing at scale"
);
assert!(
dts.iter().zip(&shifted).all(|(d, p)| d <= p),
"dts must stay at or behind presentation at scale"
);
let first = dts[0];
let durations: i64 = dts.windows(2).map(|w| w[1] - w[0]).sum();
let span = pts.iter().max().copied().unwrap_or(0) - pts.iter().min().copied().unwrap_or(0);
assert_eq!(
durations, span,
"the decode timeline must span the wire's own instants exactly"
);
let max_instant = pts.iter().copied().max().unwrap_or(0);
assert!(
first >= pts[0] - max_instant - span,
"the latch must be bounded by the sequence's own instants"
);
}
#[test]
fn decode_timeline_handles_23_976_fps_tick_jitter() {
let times: alloc::vec::Vec<i64> = (0..24i64)
.map(|i| (i * 90_000 * 1001 + 12_000) / 24_000)
.collect();
let order: [usize; 24] = [
0, 3, 1, 2, 6, 4, 5, 9, 7, 8, 12, 10, 11, 15, 13, 14, 18, 16, 17, 21, 19, 20, 23, 22,
];
let pts: alloc::vec::Vec<i64> = order.iter().map(|i| times[*i]).collect();
let (dts, shifted) = decode_timeline(&pts);
assert!(
!pts.windows(2).all(|w| w[1] >= w[0]),
"the fixture must actually be reordered"
);
assert!(
dts.windows(2).all(|w| w[1] >= w[0]),
"dts must be non-decreasing: {dts:?}"
);
assert!(
dts.iter().zip(&shifted).all(|(d, p)| d <= p),
"dts must never run ahead of presentation"
);
let durations: alloc::vec::Vec<i64> = dts.windows(2).map(|w| w[1] - w[0]).collect();
assert!(
durations.iter().all(|d| (3753..=3754).contains(d)),
"every duration must be one jittered frame period, got {durations:?}"
);
assert!(
durations.contains(&3753) && durations.contains(&3754),
"the jitter must be preserved, not flattened: {durations:?}"
);
}
#[test]
fn decode_timeline_is_pts_when_presentation_order_is_wire_order() {
let pts = [0i64, 3000, 9000];
let (dts, shifted) = decode_timeline(&pts);
assert_eq!(
(dts.as_slice(), shifted.as_slice()),
(&[0i64, 3000, 9000][..], &[0i64, 3000, 9000][..]),
"a non-decreasing (VFR) series must leave dts == pts"
);
assert_eq!(dts[1] - dts[0], 3000);
assert_eq!(dts[2] - dts[1], 6000);
}
#[test]
fn decode_timeline_recovers_the_standard_decode_order() {
let pts = [0i64, 3000, 1000, 2000];
let (dts, shifted) = decode_timeline(&pts);
assert_eq!(dts, alloc::vec![-1000, 0, 1000, 2000]);
assert_eq!(shifted, alloc::vec![0, 3000, 1000, 2000]);
let pts = [0i64, 4000, 2000, 1000, 3000, 8000, 6000, 5000, 7000];
let (dts, _) = decode_timeline(&pts);
assert_eq!(
dts,
alloc::vec![-2000, -1000, 0, 1000, 2000, 3000, 4000, 5000, 6000],
"a pyramid group must decode one period apart, in wire order"
);
}
#[test]
fn decode_timeline_properties_hold_for_reordered_series() {
for pts in [
alloc::vec![0i64, 3000, 1000, 2000],
alloc::vec![0i64, 4000, 2000, 1000, 3000],
alloc::vec![0i64, 4000, 2000, 1000, 3000, 8000, 6000, 5000, 7000],
alloc::vec![100i64, 700, 400, 500],
] {
let (dts, shifted) = decode_timeline(&pts);
assert_eq!(dts.len(), pts.len());
assert!(
dts[0] >= pts[0] - 10_000,
"the timeline stays on the wire's own epoch: {dts:?}"
);
assert!(
dts.windows(2).all(|w| w[1] >= w[0]),
"dts must be non-decreasing: {dts:?}"
);
assert!(
dts.iter().zip(&shifted).all(|(d, p)| d <= p),
"dts must never run ahead of presentation: {dts:?} vs {shifted:?}"
);
assert!(
shifted.iter().all(|p| *p >= 0),
"presentation instants must stay non-negative: {shifted:?}"
);
}
}
#[test]
fn decode_timeline_never_advances_a_hostile_reorder() {
let pts = [1000i64, 0, 500];
let (dts, shifted) = decode_timeline(&pts);
assert!(
dts.windows(2).all(|w| w[1] >= w[0]),
"must stay non-decreasing: {dts:?}"
);
assert!(
dts.iter().zip(&shifted).all(|(d, p)| d <= p),
"must stay at or behind presentation: {dts:?} vs {shifted:?}"
);
}
#[test]
fn decode_timeline_edge_cases_are_total() {
let (dts, pts) = decode_timeline(&[]);
assert!(dts.is_empty() && pts.is_empty());
assert_eq!(
decode_timeline(&[1234]),
(alloc::vec![1234], alloc::vec![1234])
);
let (dts, pts) = decode_timeline(&[7, 7, 7]);
assert_eq!(dts, alloc::vec![7, 7, 7]);
assert_eq!(pts, alloc::vec![7, 7, 7]);
}
#[test]
fn reassemble_video_reports_timestamp_and_sync() {
fn pkt(seq: u16, ts: u32, marker: bool, nal: &[u8]) -> Vec<u8> {
let mut p = alloc::vec![0x80u8, if marker { 0x80 | 96 } else { 96 }];
p.extend_from_slice(&seq.to_be_bytes());
p.extend_from_slice(&ts.to_be_bytes());
p.extend_from_slice(&[0, 0, 0, 0]); p.extend_from_slice(nal);
p
}
let idr = [0x65u8, 0xAA]; let non = [0x41u8, 0xBB]; let packets = alloc::vec![pkt(1, 1000, true, &idr), pkt(2, 4000, true, &non)];
let aus = reassemble_video(&packets).unwrap();
assert_eq!(aus.len(), 2);
assert_eq!(aus[0].timestamp, 1000);
assert!(aus[0].is_sync, "IDR AU must be sync");
assert_eq!(aus[1].timestamp, 4000);
assert!(!aus[1].is_sync, "non-IDR AU must not be sync");
assert_eq!(&aus[0].data[..4], &[0, 0, 0, 2]);
assert_eq!(&aus[0].data[4..], &idr);
}
#[test]
fn base64_round_trip() {
let data = b"\x67\x42\xc0\x1e\xd9";
let enc = base64_encode(data);
assert_eq!(base64_decode(&enc).unwrap(), data);
}
#[test]
fn base64_known_vector() {
assert_eq!(base64_encode(b"foobar"), "Zm9vYmFy");
assert_eq!(base64_encode(b"fo"), "Zm8=");
assert_eq!(base64_decode("Zm9vYmFy").unwrap(), b"foobar");
}
#[test]
fn hex_round_trip() {
let data = b"\x12\x08\x56\xe5\x00";
let enc = hex_encode(data);
assert_eq!(enc, "12085 6e500".replace(' ', ""));
assert_eq!(hex_decode(&enc).unwrap(), data);
}
#[test]
fn rtp_header_layout() {
let h = rtp_header(96, true, 7, 0x0001_0000, 0xDEAD_BEEF);
assert_eq!(h.len(), RTP_HEADER_LEN);
assert_eq!(h[0], 0x80); assert_eq!(h[1], 0x80 | 96); assert_eq!(u16::from_be_bytes([h[2], h[3]]), 7);
assert_eq!(u32::from_be_bytes([h[4], h[5], h[6], h[7]]), 0x0001_0000);
let parsed = parse_rtp_header(&h).unwrap();
assert!(parsed.marker);
assert_eq!(parsed.payload_type, 96);
}
}