use crate::aac_asc::AudioSpecificConfig;
use crate::avc_config::{AVCConfigurationBox, AVCDecoderConfigurationRecord};
use crate::error::{Error, Result};
use crate::mp4esds::{
DecoderConfigDescriptor, DecoderSpecificInfo, ESDescriptor, EsdsBox, SLConfigDescriptor,
};
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 AAC_SAMPLE_SIZE_BITS: u16 = 16;
fn asc_sample_rate(asc: &AudioSpecificConfig) -> Result<u32> {
if let Some(freq) = asc.sampling_frequency {
return Ok(freq);
}
asc.sampling_frequency_index
.table_hz()
.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)?;
aac_config_from_asc_bytes(asc_bytes)
}
pub fn aac_config_from_asc_bytes(asc_bytes: Vec<u8>) -> Result<CodecConfig> {
let asc = AudioSpecificConfig::parse(&asc_bytes)?;
let sample_rate = asc_sample_rate(&asc)?;
let channel_count = crate::flv::aac_channel_count(&asc);
let esds = EsdsBox::new(ESDescriptor::new(
0,
0,
Some(DecoderConfigDescriptor::new(
OTI_AUDIO_ISO14496_3,
STREAM_TYPE_AUDIO,
false,
0,
0,
0,
Some(DecoderSpecificInfo::new(asc_bytes)),
)),
Some(SLConfigDescriptor::predefined_two()),
));
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")),
}
}
avc_config_from_sps_pps(sps, pps)
}
pub fn avc_config_from_sps_pps(sps: Vec<AvcSps>, pps: Vec<AvcPps>) -> Result<AVCConfigurationBox> {
let first_sps = sps
.first()
.ok_or(Error::InvalidInput("no SPS NAL unit supplied"))?;
if first_sps.0.len() < 4 {
return Err(Error::BufferTooShort {
need: 4,
have: first_sps.0.len(),
what: "SPS profile/level bytes",
});
}
let profile_indication = first_sps.0[1];
let ext = if crate::avc_config::AVCDecoderConfigurationRecord::has_high_profile_ext(
profile_indication,
) {
Some(crate::sps::decode_avc_sps(&first_sps.0)?)
} else {
None
};
let record = AVCDecoderConfigurationRecord {
configuration_version: 1,
profile_indication,
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: ext.as_ref().map(|i| i.chroma_format_idc),
bit_depth_luma_minus8: ext.as_ref().map(|i| i.bit_depth_luma.saturating_sub(8)),
bit_depth_chroma_minus8: ext.as_ref().map(|i| i.bit_depth_chroma.saturating_sub(8)),
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;
use broadcast_common::Serialize;
const HIGH_SDP_FMTP: &str = "packetization-mode=1; sprop-parameter-sets=Z2QADazZQUH7ARAAAAMAEAAAAwMg8UKZYA==,aOvjyyLA; profile-level-id=64000D";
#[test]
fn high_profile_sprop_gets_the_chroma_bit_depth_trailer() {
let config = avc_config_from_fmtp(HIGH_SDP_FMTP).expect("real ffmpeg High SDP");
let r = &config.config;
assert_eq!(r.profile_indication, 0x64, "High profile, per the SDP");
assert_eq!(r.level_indication, 0x0D, "level 1.3, per the SDP");
assert_eq!(
r.chroma_format,
Some(1),
"4:2:0 — the profile_idc-100 trailer must be present"
);
assert_eq!(r.bit_depth_luma_minus8, Some(0), "8-bit luma");
assert_eq!(r.bit_depth_chroma_minus8, Some(0), "8-bit chroma");
let mut out = alloc::vec![0u8; r.serialized_len()];
r.serialize_into(&mut out).expect("serialize avcC");
let sps_len = r.sps[0].0.len();
let pps_len = r.pps[0].0.len();
let trailer_start = out.len() - 4;
assert_eq!(
&out[trailer_start..],
&[0xFD, 0xF8, 0xF8, 0x00],
"the serialized trailer must match ffmpeg's own avcC bytes (chroma 1, depths 0, no SPS-ext); sps/pps lens {sps_len}/{pps_len}"
);
}
#[test]
fn non_high_profile_sprop_has_no_trailer() {
const BASELINE: &str = "packetization-mode=1; sprop-parameter-sets=Z0LADdkBQfsBEAAAAwAQAAADAyDxQqSA,aMuDyyA=; profile-level-id=42C00D";
let config = avc_config_from_fmtp(BASELINE).expect("real ffmpeg Baseline SDP");
let r = &config.config;
assert_eq!(r.profile_indication, 0x42, "Baseline, per the SDP");
assert_eq!(
r.chroma_format, None,
"no trailer for a non-High profile_idc"
);
assert_eq!(r.bit_depth_luma_minus8, None);
assert_eq!(r.bit_depth_chroma_minus8, None);
}
#[test]
fn aac_channel_count_uses_table_1_19() {
let cfg = aac_config_from_asc_hex("11B856E500").expect("real 7.1 ASC");
match cfg {
CodecConfig::Aac {
channel_count,
sample_rate,
..
} => {
assert_eq!(sample_rate, 48_000, "sfi 3 = 48000 Hz");
assert_eq!(channel_count, 8, "Table 1.19: configuration 7 = 8 ch");
}
other => panic!("expected AAC, got {other:?}"),
}
for asc_hex in ["1180", "11E0"] {
let cfg = aac_config_from_asc_hex(asc_hex).expect("parseable ASC");
match cfg {
CodecConfig::Aac { channel_count, .. } => assert_eq!(
channel_count,
crate::flv::AAC_CHANNEL_COUNT_UNKNOWN,
"ASC {asc_hex}: an undetermined channel count is marked, not fabricated"
),
other => panic!("expected AAC, got {other:?}"),
}
}
}
#[test]
fn high_profile_sps_that_cannot_be_decoded_is_an_error() {
let sps = alloc::vec![0x67u8, 100, 0x00, 0x1E];
let pps = alloc::vec![0x68u8, 0xCE, 0x3C, 0x80];
let err = avc_config_from_sps_pps(alloc::vec![AvcSps(sps)], alloc::vec![AvcPps(pps)])
.expect_err("a High profile SPS with no decodable body must not yield a record");
assert!(
matches!(
err,
Error::BufferTooShort { .. } | Error::InvalidValue { .. }
),
"must be a structured decode error, got {err:?}"
);
}
#[test]
fn trailer_gate_is_the_avcc_profile_set_not_the_sps_parse_set() {
use crate::avc_config::AVCDecoderConfigurationRecord as Record;
for idc in [100u8, 110, 122, 244] {
assert!(
Record::has_high_profile_ext(idc),
"profile {idc} carries the trailer (ISO/IEC 14496-15 §5.3.3.1.2)"
);
}
for idc in [66u8, 77, 88, 44, 83, 86, 118, 128, 138, 139, 134, 135] {
assert!(
!Record::has_high_profile_ext(idc),
"profile {idc} is outside the record's trailer set"
);
}
assert!(
crate::sps::is_high_profile(44) && !Record::has_high_profile_ext(44),
"the parse gate and the trailer gate must be different sets"
);
}
#[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 avc_config_from_sps_pps_matches_sprop_path() {
let sps = alloc::vec![0x67u8, 0x42, 0xC0, 0x1E, 0xAB];
let pps = alloc::vec![0x68u8, 0xCE, 0x3C, 0x80];
let via_sprop = avc_config_from_sprop(&alloc::format!(
"{},{}",
base64_encode(&sps),
base64_encode(&pps)
))
.unwrap();
let via_bytes =
avc_config_from_sps_pps(alloc::vec![AvcSps(sps)], alloc::vec![AvcPps(pps)]).unwrap();
assert_eq!(via_sprop.config.sps, via_bytes.config.sps);
assert_eq!(via_sprop.config.pps, via_bytes.config.pps);
assert_eq!(
via_sprop.config.profile_indication,
via_bytes.config.profile_indication
);
}
#[test]
fn avc_config_from_sps_pps_rejects_no_sps() {
assert!(avc_config_from_sps_pps(Vec::new(), Vec::new()).is_err());
}
#[test]
fn aac_config_from_asc_bytes_matches_hex_path() {
let asc_bytes = alloc::vec![0x12u8, 0x10];
let via_hex = aac_config_from_asc_hex("1210").unwrap();
let via_bytes = aac_config_from_asc_bytes(asc_bytes).unwrap();
match (via_hex, via_bytes) {
(
crate::pipeline::CodecConfig::Aac {
sample_rate: sr1,
channel_count: ch1,
..
},
crate::pipeline::CodecConfig::Aac {
sample_rate: sr2,
channel_count: ch2,
..
},
) => {
assert_eq!(sr1, sr2);
assert_eq!(ch1, ch2);
}
_ => panic!("expected CodecConfig::Aac from both paths"),
}
}
#[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);
}
}