use crate::aac_asc::{AudioSpecificConfig, SamplingFrequencyIndex};
use crate::avc_config::{AVCConfigurationBox, AVCDecoderConfigurationRecord};
use crate::error::{Error, Result};
use crate::mp4esds::{
DecoderConfigDescriptor, DecoderSpecificInfo, ESDescriptor, EsdsBox, ObjectTypeIndication,
SLConfigDescriptor, StreamType,
};
use crate::nal::{NalCodec, nal_unit_type};
use crate::nalu_types::{AvcPps, AvcSps};
use crate::pipeline::CodecConfig;
use crate::rtp::{base64_decode, hex_decode};
use alloc::vec::Vec;
use broadcast_common::Parse;
const NAL_LENGTH_SIZE_MINUS_ONE: u8 = 3;
const AVC_NAL_SPS: u8 = 7;
const AVC_NAL_PPS: u8 = 8;
const OTI_AUDIO_ISO14496_3: u8 = 0x40;
const STREAM_TYPE_AUDIO: u8 = 5;
const SL_CONFIG_PREDEFINED_MP4: u8 = 2;
const AAC_SAMPLE_SIZE_BITS: u16 = 16;
const SAMPLING_FREQUENCY_TABLE_HZ: [u32; 13] = [
96000, 88200, 64000, 48000, 44100, 32000, 24000, 22050, 16000, 12000, 11025, 8000, 7350,
];
fn sampling_frequency_table_hz(index: &SamplingFrequencyIndex) -> Option<u32> {
let raw = index.raw() as usize;
SAMPLING_FREQUENCY_TABLE_HZ.get(raw).copied()
}
fn asc_sample_rate(asc: &AudioSpecificConfig) -> Result<u32> {
if let Some(freq) = asc.sampling_frequency {
return Ok(freq);
}
sampling_frequency_table_hz(&asc.sampling_frequency_index).ok_or(Error::InvalidValue {
field: "sampling_frequency_index",
value: u64::from(asc.sampling_frequency_index.raw()),
reason: "no frequency for index",
})
}
pub fn aac_config_from_asc_hex(config_hex: &str) -> Result<CodecConfig> {
let asc_bytes = hex_decode(config_hex)?;
let asc = AudioSpecificConfig::parse(&asc_bytes)?;
let sample_rate = asc_sample_rate(&asc)?;
let channel_count = u16::from(asc.channel_configuration.raw());
let esds = EsdsBox::new(ESDescriptor {
es_id: 0,
stream_dependence_flag: false,
url_flag: false,
ocr_stream_flag: false,
stream_priority: 0,
depends_on_es_id: None,
url: None,
ocr_es_id: None,
decoder_config: Some(DecoderConfigDescriptor {
object_type_indication: ObjectTypeIndication(OTI_AUDIO_ISO14496_3),
stream_type: StreamType(STREAM_TYPE_AUDIO),
up_stream: false,
buffer_size_db: 0,
max_bitrate: 0,
avg_bitrate: 0,
decoder_specific_info: Some(DecoderSpecificInfo { data: asc_bytes }),
}),
sl_config: Some(SLConfigDescriptor {
body: alloc::vec![SL_CONFIG_PREDEFINED_MP4],
}),
});
Ok(CodecConfig::Aac {
esds,
channel_count,
sample_rate,
sample_size: AAC_SAMPLE_SIZE_BITS,
})
}
pub fn avc_config_from_sprop(sprop_parameter_sets: &str) -> Result<AVCConfigurationBox> {
let mut sps: Vec<AvcSps> = Vec::new();
let mut pps: Vec<AvcPps> = Vec::new();
for token in sprop_parameter_sets.split(',') {
let token = token.trim();
if token.is_empty() {
continue;
}
let nal = base64_decode(token)?;
if nal.is_empty() {
return Err(Error::InvalidInput("empty sprop parameter set"));
}
match nal_unit_type(NalCodec::Avc, &nal) {
Some(AVC_NAL_SPS) => sps.push(AvcSps(nal)),
Some(AVC_NAL_PPS) => pps.push(AvcPps(nal)),
_ => return Err(Error::InvalidInput("sprop NAL is neither SPS nor PPS")),
}
}
let first_sps = sps
.first()
.ok_or(Error::InvalidInput("sprop-parameter-sets contained no SPS"))?;
if first_sps.0.len() < 4 {
return Err(Error::BufferTooShort {
need: 4,
have: first_sps.0.len(),
what: "SPS profile/level bytes",
});
}
let record = AVCDecoderConfigurationRecord {
configuration_version: 1,
profile_indication: first_sps.0[1],
profile_compatibility: first_sps.0[2],
level_indication: first_sps.0[3],
length_size_minus_one: NAL_LENGTH_SIZE_MINUS_ONE,
sps,
pps,
chroma_format: None,
bit_depth_luma_minus8: None,
bit_depth_chroma_minus8: None,
sps_ext: Vec::new(),
};
Ok(AVCConfigurationBox::new(record))
}
fn strip_leading_pt_token(value: &str) -> &str {
let trimmed = value.trim_start();
match trimmed.split_once(char::is_whitespace) {
Some((pt, rest)) if !pt.is_empty() && pt.bytes().all(|b| b.is_ascii_digit()) => {
rest.trim_start()
}
_ => trimmed,
}
}
pub fn fmtp_param<'a>(fmtp: &'a str, key: &str) -> Option<&'a str> {
let params = strip_leading_pt_token(fmtp);
for pair in params.split(';') {
let pair = pair.trim();
if pair.is_empty() {
continue;
}
let Some((k, v)) = pair.split_once('=') else {
continue;
};
if k.trim() == key {
let v = v.trim();
if !v.is_empty() {
return Some(v);
}
}
}
None
}
pub fn avc_config_from_fmtp(fmtp: &str) -> Result<AVCConfigurationBox> {
let sprop = fmtp_param(fmtp, "sprop-parameter-sets").ok_or(Error::InvalidInput(
"fmtp has no sprop-parameter-sets parameter",
))?;
avc_config_from_sprop(sprop)
}
pub fn aac_config_from_fmtp(fmtp: &str) -> Result<CodecConfig> {
let config_hex =
fmtp_param(fmtp, "config").ok_or(Error::InvalidInput("fmtp has no config parameter"))?;
aac_config_from_asc_hex(config_hex)
}
pub fn rtpmap_clock_rate(rtpmap: &str) -> Option<u32> {
let encoding = strip_leading_pt_token(rtpmap);
let mut fields = encoding.split('/');
let _name = fields.next()?;
let clock_str = fields.next()?;
clock_str.trim().parse::<u32>().ok()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::rtp::base64_encode;
#[test]
fn sprop_round_trips_sps_pps_and_profile() {
let sps = alloc::vec![0x67u8, 0x42, 0xC0, 0x1E, 0xAB]; let pps = alloc::vec![0x68u8, 0xCE, 0x3C, 0x80];
let sprop = alloc::format!("{},{}", base64_encode(&sps), base64_encode(&pps));
let boxed = avc_config_from_sprop(&sprop).unwrap();
let r = &boxed.config;
assert_eq!(r.sps.len(), 1);
assert_eq!(r.pps.len(), 1);
assert_eq!(r.sps[0].0, sps);
assert_eq!(r.pps[0].0, pps);
assert_eq!(r.profile_indication, 0x42);
assert_eq!(r.profile_compatibility, 0xC0);
assert_eq!(r.level_indication, 0x1E);
assert_eq!(r.length_size_minus_one, 3);
}
#[test]
fn sprop_rejects_when_no_sps() {
let pps = alloc::vec![0x68u8, 0xCE];
let sprop = base64_encode(&pps);
assert!(avc_config_from_sprop(&sprop).is_err());
}
#[test]
fn sprop_rejects_invalid_base64() {
let sprop = "not-valid-base64!!!";
assert!(avc_config_from_sprop(sprop).is_err());
}
#[test]
fn sprop_rejects_sps_too_short() {
let sps = alloc::vec![0x67u8, 0x42]; let sprop = base64_encode(&sps);
let result = avc_config_from_sprop(&sprop);
assert!(result.is_err());
if let Err(Error::BufferTooShort { need, have, what }) = result {
assert_eq!(need, 4);
assert_eq!(have, 2);
assert!(what.contains("SPS"));
}
}
#[test]
fn sprop_rejects_non_sps_non_pps_nal() {
let sei = alloc::vec![0x06u8, 0x00]; let sprop = base64_encode(&sei);
let result = avc_config_from_sprop(&sprop);
assert!(result.is_err());
}
#[test]
fn aac_asc_hex_recovers_rate_channels_and_asc() {
let config_hex = "1210";
let cfg = aac_config_from_asc_hex(config_hex).unwrap();
match cfg {
crate::pipeline::CodecConfig::Aac {
sample_rate,
channel_count,
esds,
..
} => {
assert_eq!(sample_rate, 44100);
assert_eq!(channel_count, 2);
let dsi = esds
.es_descriptor
.decoder_config
.as_ref()
.unwrap()
.decoder_specific_info
.as_ref()
.unwrap();
assert_eq!(dsi.data, alloc::vec![0x12u8, 0x10]);
}
_ => panic!("expected CodecConfig::Aac"),
}
}
#[test]
fn fmtp_param_matches_key_anchored() {
let fmtp =
"96 packetization-mode=1; sprop-parameter-sets=Zm9v,YmFy; profile-level-id=42e01e";
assert_eq!(fmtp_param(fmtp, "sprop-parameter-sets"), Some("Zm9v,YmFy"));
assert_eq!(fmtp_param(fmtp, "profile-level-id"), Some("42e01e"));
assert_eq!(fmtp_param(fmtp, "mode"), None);
}
#[test]
fn fmtp_param_trims_whitespace() {
let fmtp = "97 streamtype = 5 ; config =1210 ;sizeLength=13";
assert_eq!(fmtp_param(fmtp, "config"), Some("1210"));
assert_eq!(fmtp_param(fmtp, "streamtype"), Some("5"));
assert_eq!(fmtp_param(fmtp, "sizeLength"), Some("13"));
}
#[test]
fn fmtp_param_skips_pair_without_equals() {
let fmtp = "96 bareflag; config=1210";
assert_eq!(fmtp_param(fmtp, "config"), Some("1210"));
}
#[test]
fn fmtp_param_preserves_base64_padding() {
let fmtp = "96 sprop-parameter-sets=Zm9v,YmFy==;x=1";
assert_eq!(
fmtp_param(fmtp, "sprop-parameter-sets"),
Some("Zm9v,YmFy==")
);
}
#[test]
fn fmtp_param_empty_value_is_none() {
assert_eq!(fmtp_param("96 config=;x=1", "config"), None);
}
#[test]
fn fmtp_param_charset_preserves_multibyte() {
let fmtp = "96 sprop-description=caf\u{e9}; other=1";
assert_eq!(fmtp_param(fmtp, "sprop-description"), Some("caf\u{e9}"));
}
#[test]
fn avc_config_from_fmtp_extracts_sprop() {
let fmtp =
"96 packetization-mode=1; sprop-parameter-sets=Z0IAKeKQFAe2AtwEBAaQeJEV,aM48gA==";
let boxed = avc_config_from_fmtp(fmtp).unwrap();
assert!(!boxed.config.sps.is_empty());
assert!(!boxed.config.pps.is_empty());
}
#[test]
fn aac_config_from_fmtp_extracts_config() {
let fmtp = "97 streamtype=5; mode=AAC-hbr; config=1210; sizeLength=13";
let cfg = aac_config_from_fmtp(fmtp).unwrap();
match cfg {
crate::pipeline::CodecConfig::Aac {
sample_rate,
channel_count,
..
} => {
assert_eq!(sample_rate, 44100);
assert_eq!(channel_count, 2);
}
_ => panic!("expected CodecConfig::Aac"),
}
}
#[test]
fn aac_config_from_fmtp_missing_config_errors() {
let fmtp = "97 streamtype=5; mode=AAC-hbr";
assert!(aac_config_from_fmtp(fmtp).is_err());
}
#[test]
fn avc_config_from_fmtp_missing_sprop_errors() {
let fmtp = "96 packetization-mode=1";
assert!(avc_config_from_fmtp(fmtp).is_err());
}
#[test]
fn rtpmap_clock_rate_parses() {
assert_eq!(rtpmap_clock_rate("96 H264/90000"), Some(90000));
assert_eq!(rtpmap_clock_rate("97 mpeg4-generic/48000/2"), Some(48000));
assert_eq!(rtpmap_clock_rate("H264/90000"), Some(90000));
assert_eq!(rtpmap_clock_rate("malformed"), None);
assert_eq!(rtpmap_clock_rate("96 H264/notanumber"), None);
assert_eq!(rtpmap_clock_rate(""), None);
}
}