use crate::TrackId;
use alloc::string::{String, ToString};
use alloc::vec::Vec;
use core::fmt;
use core::str::FromStr;
use crate::aac_asc::{ChannelConfiguration, SamplingFrequencyIndex};
use crate::annexb::iter_annexb_nals;
use crate::error::{Error as CrateError, Result as CrateResult};
use crate::nal::{NalCodec, nal_unit_type};
use crate::nalu_types::{AvcPps, AvcSps};
use crate::pipeline::{CodecConfig, SubtitleFormat, TrackSpec};
use crate::rtp_sdp::{aac_config_from_asc_bytes, avc_config_from_sps_pps};
use crate::smooth::{FOURCC_AACH, FOURCC_AACL, FOURCC_AVC1, FOURCC_H264, SMOOTH_TIMESCALE};
use crate::subtitle_entries::{WvttSampleEntry, XmlSubtitleSampleEntry};
use crate::xml_chars::{char_data, is_xml_char};
use quick_xml::Reader;
use quick_xml::XmlVersion;
use quick_xml::errors::IllFormedError;
use quick_xml::events::{BytesStart, Event};
use quick_xml::name::QName;
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum SmoothParseError {
UnexpectedEof,
UnterminatedTag {
pos: usize,
},
MalformedAttribute {
pos: usize,
},
UnexpectedElement {
expected: &'static str,
found: String,
},
MissingAttribute {
element: &'static str,
attr: &'static str,
},
InvalidAttributeValue {
element: &'static str,
attr: &'static str,
value: String,
},
ChunkRunTooLong {
count_hint: u64,
},
CodecPrivateDataTooLong {
len: usize,
},
InvalidHex {
value: String,
},
MismatchedEndTag {
expected: String,
found: String,
},
Xml {
pos: usize,
message: String,
},
}
impl fmt::Display for SmoothParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
SmoothParseError::UnexpectedEof => {
write!(
f,
"unexpected end of input while parsing Smooth manifest XML"
)
}
SmoothParseError::UnterminatedTag { pos } => {
write!(
f,
"unterminated XML tag/comment/declaration at byte offset {pos}"
)
}
SmoothParseError::MalformedAttribute { pos } => {
write!(f, "malformed XML attribute near byte offset {pos}")
}
SmoothParseError::UnexpectedElement { expected, found } => {
if found.is_empty() {
write!(f, "expected element <{expected}>, found none")
} else {
write!(f, "expected element <{expected}>, found <{found}>")
}
}
SmoothParseError::MissingAttribute { element, attr } => {
write!(f, "<{element}> is missing required attribute @{attr}")
}
SmoothParseError::InvalidAttributeValue {
element,
attr,
value,
} => write!(f, "<{element}>@{attr} has invalid value {value:?}"),
SmoothParseError::ChunkRunTooLong { count_hint } => {
write!(
f,
"c@r repeat run exceeded max chunk count ({count_hint} > {})",
MAX_CHUNK_RUN
)
}
SmoothParseError::CodecPrivateDataTooLong { len } => {
write!(
f,
"CodecPrivateData hex length {len} exceeds max {}",
MAX_CODEC_PRIVATE_DATA_HEX_LEN
)
}
SmoothParseError::InvalidHex { value } => {
write!(f, "invalid CodecPrivateData hex {value:?}")
}
SmoothParseError::MismatchedEndTag { expected, found } => {
if found.is_empty() {
write!(f, "expected closing tag </{expected}>, found none")
} else {
write!(f, "expected closing tag </{expected}>, found </{found}>")
}
}
SmoothParseError::Xml { pos, message } => {
write!(f, "XML error at byte offset {pos}: {message}")
}
}
}
}
#[cfg(feature = "std")]
impl std::error::Error for SmoothParseError {}
type Result<T> = core::result::Result<T, SmoothParseError>;
type XmlReader<'a> = Reader<&'a [u8]>;
enum Tag {
Open {
name: String,
attrs: Vec<(String, String)>,
self_closing: bool,
},
Close { name: String },
}
fn new_reader(xml: &str) -> XmlReader<'_> {
Reader::from_str(xml)
}
fn xml_error(reader: &XmlReader<'_>, err: &quick_xml::Error) -> SmoothParseError {
use quick_xml::Error as E;
let pos = usize::try_from(reader.buffer_position()).unwrap_or(usize::MAX);
match err {
E::Syntax(_) => SmoothParseError::UnterminatedTag { pos },
E::IllFormed(IllFormedError::MismatchedEndTag { expected, found }) => {
SmoothParseError::MismatchedEndTag {
expected: local_part(expected),
found: local_part(found),
}
}
_ => SmoothParseError::Xml {
pos,
message: err.to_string(),
},
}
}
fn xml_message(reader: &XmlReader<'_>, message: String) -> SmoothParseError {
SmoothParseError::Xml {
pos: usize::try_from(reader.buffer_position()).unwrap_or(usize::MAX),
message,
}
}
fn local_part(qname: &str) -> String {
QName(qname).local_name().into_inner().to_string()
}
fn read_event<'a>(reader: &mut XmlReader<'a>) -> Result<Event<'a>> {
match reader.read_event() {
Ok(event) => Ok(event),
Err(e) => Err(xml_error(reader, &e)),
}
}
fn read_attrs(reader: &XmlReader<'_>, e: &BytesStart<'_>) -> Result<Vec<(String, String)>> {
let pos = usize::try_from(reader.buffer_position()).unwrap_or(usize::MAX);
let mut attrs = Vec::new();
for attr in e.attributes() {
let attr = attr.map_err(|_| SmoothParseError::MalformedAttribute { pos })?;
let value = attr
.normalized_value(XmlVersion::Implicit1_0)
.map_err(|_| SmoothParseError::MalformedAttribute { pos })?;
if !value.chars().all(is_xml_char) {
return Err(SmoothParseError::MalformedAttribute { pos });
}
attrs.push((attr.key.as_ref().to_string(), value.into_owned()));
}
Ok(attrs)
}
fn next_tag(reader: &mut XmlReader<'_>) -> Result<Option<Tag>> {
loop {
let event = read_event(reader)?;
if char_data(&event)
.map_err(|message| xml_message(reader, message))?
.is_some()
{
continue;
}
match event {
Event::Start(e) => {
return Ok(Some(Tag::Open {
name: e.local_name().into_inner().to_string(),
attrs: read_attrs(reader, &e)?,
self_closing: false,
}));
}
Event::Empty(e) => {
return Ok(Some(Tag::Open {
name: e.local_name().into_inner().to_string(),
attrs: read_attrs(reader, &e)?,
self_closing: true,
}));
}
Event::End(e) => {
return Ok(Some(Tag::Close {
name: e.local_name().into_inner().to_string(),
}));
}
Event::Eof => return Ok(None),
_ => {}
}
}
}
fn skip_element(reader: &mut XmlReader<'_>) -> Result<()> {
let mut depth: usize = 1;
while depth > 0 {
match next_tag(reader)? {
Some(Tag::Open { self_closing, .. }) => {
if !self_closing {
depth += 1;
}
}
Some(Tag::Close { .. }) => depth -= 1,
None => return Err(SmoothParseError::UnexpectedEof),
}
}
Ok(())
}
pub const MAX_CHUNK_RUN: usize = 100_000;
pub const MAX_CODEC_PRIVATE_DATA_HEX_LEN: usize = 65_536;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum StreamType {
Video,
Audio,
Text,
}
impl StreamType {
pub fn name(&self) -> &'static str {
match self {
StreamType::Video => "video",
StreamType::Audio => "audio",
StreamType::Text => "text",
}
}
}
broadcast_common::impl_spec_display!(StreamType);
#[derive(Debug, Clone, PartialEq)]
pub struct SmoothManifest {
pub major_version: u32,
pub minor_version: u32,
pub timescale: u64,
pub duration: Option<u64>,
pub is_live: bool,
pub look_ahead_fragment_count: Option<u32>,
pub dvr_window_length: Option<u64>,
pub streams: Vec<StreamIndex>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct StreamIndex {
pub stream_type: StreamType,
pub name: Option<String>,
pub subtype: Option<String>,
pub chunks: Option<u32>,
pub timescale: Option<u64>,
pub url: String,
pub qualities: Vec<QualityLevel>,
pub chunks_list: Vec<C>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct QualityLevel {
pub index: u32,
pub bitrate: u64,
pub four_cc: String,
pub codec_private_data: Vec<u8>,
pub width: Option<u32>,
pub height: Option<u32>,
pub sampling_rate: Option<u32>,
pub channels: Option<u16>,
pub bits_per_sample: Option<u16>,
pub packet_size: Option<u32>,
pub audio_tag: Option<u32>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct C {
pub t: Option<u64>,
pub d: Option<u64>,
pub r: Option<u32>,
}
pub const TOKEN_BITRATE: &str = "{bitrate}";
pub const TOKEN_START_TIME: &str = "{start time}";
impl StreamIndex {
pub fn enumerate_chunks(&self) -> Result<Vec<(u64, u64)>> {
let mut out = Vec::new();
let mut t: u64 = 0;
let mut total: u64 = 0;
for c in &self.chunks_list {
if let Some(explicit_t) = c.t {
t = explicit_t;
}
let d = c.d.unwrap_or(0);
let repeats: u64 = c.r.map(|r| u64::from(r).saturating_add(1)).unwrap_or(1);
if repeats > MAX_CHUNK_RUN as u64 {
return Err(SmoothParseError::ChunkRunTooLong {
count_hint: repeats,
});
}
total = total.saturating_add(repeats);
if total as usize > MAX_CHUNK_RUN {
return Err(SmoothParseError::ChunkRunTooLong { count_hint: total });
}
for _ in 0..repeats {
out.push((t, d));
t = t.saturating_add(d);
}
}
Ok(out)
}
pub fn resolve_fragment_url(&self, bitrate: u64, start_time: u64) -> String {
self.url
.replace(TOKEN_BITRATE, &bitrate.to_string())
.replace(TOKEN_START_TIME, &start_time.to_string())
}
}
pub fn hex_decode(s: &str) -> Result<Vec<u8>> {
if s.len() > MAX_CODEC_PRIVATE_DATA_HEX_LEN {
return Err(SmoothParseError::CodecPrivateDataTooLong { len: s.len() });
}
::hex::decode(s).map_err(|_| SmoothParseError::InvalidHex {
value: s.to_string(),
})
}
fn attr<'a>(attrs: &'a [(String, String)], key: &str) -> Option<&'a str> {
attrs
.iter()
.find(|(k, _)| k == key)
.map(|(_, v)| v.as_str())
}
fn attr_owned(attrs: &[(String, String)], key: &str) -> Option<String> {
attr(attrs, key).map(String::from)
}
fn required_attr_owned(
attrs: &[(String, String)],
key: &'static str,
element: &'static str,
) -> Result<String> {
attr(attrs, key)
.map(String::from)
.ok_or(SmoothParseError::MissingAttribute { element, attr: key })
}
fn parse_attr<T: FromStr>(
attrs: &[(String, String)],
key: &'static str,
element: &'static str,
) -> Result<Option<T>> {
match attr(attrs, key) {
Some(v) => {
v.trim()
.parse::<T>()
.map(Some)
.map_err(|_| SmoothParseError::InvalidAttributeValue {
element,
attr: key,
value: v.to_string(),
})
}
None => Ok(None),
}
}
fn required_attr_parse<T: FromStr>(
attrs: &[(String, String)],
key: &'static str,
element: &'static str,
) -> Result<T> {
let v = attr(attrs, key).ok_or(SmoothParseError::MissingAttribute { element, attr: key })?;
v.trim()
.parse::<T>()
.map_err(|_| SmoothParseError::InvalidAttributeValue {
element,
attr: key,
value: v.to_string(),
})
}
impl SmoothManifest {
pub fn parse(xml: &str) -> Result<SmoothManifest> {
const EL: &str = "SmoothStreamingMedia";
let mut xml_reader = new_reader(xml);
let reader = &mut xml_reader;
let (attrs, self_closing) = match next_tag(reader)? {
Some(Tag::Open {
name,
attrs,
self_closing,
}) if name == "SmoothStreamingMedia" => (attrs, self_closing),
Some(Tag::Open { name, .. }) => {
return Err(SmoothParseError::UnexpectedElement {
expected: EL,
found: name.to_string(),
});
}
Some(Tag::Close { .. }) => {
return Err(SmoothParseError::UnexpectedElement {
expected: EL,
found: String::new(),
});
}
None => return Err(SmoothParseError::UnexpectedEof),
};
let major_version: u32 = parse_attr(&attrs, "MajorVersion", EL)?.unwrap_or(2);
let minor_version: u32 = parse_attr(&attrs, "MinorVersion", EL)?.unwrap_or(0);
let timescale: u64 = parse_attr(&attrs, "TimeScale", EL)?.unwrap_or(SMOOTH_TIMESCALE);
let duration: Option<u64> = parse_attr(&attrs, "Duration", EL)?;
let is_live = attr(&attrs, "IsLive").is_some_and(|v| v.eq_ignore_ascii_case("true"));
let look_ahead_fragment_count: Option<u32> =
parse_attr(&attrs, "LookAheadFragmentCount", EL)?;
let dvr_window_length: Option<u64> = parse_attr(&attrs, "DVRWindowLength", EL)?;
let mut streams = Vec::new();
if !self_closing {
loop {
match next_tag(reader)? {
Some(Tag::Open {
name,
attrs,
self_closing,
}) if name == "StreamIndex" => {
streams.push(parse_stream_index(reader, attrs, self_closing)?)
}
Some(Tag::Open { self_closing, .. }) => {
if !self_closing {
skip_element(reader)?;
}
}
Some(Tag::Close { name }) => {
if name != EL {
return Err(SmoothParseError::MismatchedEndTag {
expected: EL.to_string(),
found: name.to_string(),
});
}
break;
}
None => return Err(SmoothParseError::UnexpectedEof),
}
}
}
Ok(SmoothManifest {
major_version,
minor_version,
timescale,
duration,
is_live,
look_ahead_fragment_count,
dvr_window_length,
streams,
})
}
}
fn parse_stream_index(
reader: &mut XmlReader<'_>,
attrs: Vec<(String, String)>,
self_closing: bool,
) -> Result<StreamIndex> {
const EL: &str = "StreamIndex";
let type_str = required_attr_owned(&attrs, "Type", EL)?;
let stream_type = match type_str.as_str() {
"video" => StreamType::Video,
"audio" => StreamType::Audio,
"text" => StreamType::Text,
_ => {
return Err(SmoothParseError::InvalidAttributeValue {
element: EL,
attr: "Type",
value: type_str,
});
}
};
let name = attr_owned(&attrs, "Name");
let subtype = attr_owned(&attrs, "Subtype");
let chunks: Option<u32> = parse_attr(&attrs, "Chunks", EL)?;
let timescale: Option<u64> = parse_attr(&attrs, "TimeScale", EL)?;
let url = required_attr_owned(&attrs, "Url", EL)?;
let mut qualities = Vec::new();
let mut chunks_list = Vec::new();
if !self_closing {
loop {
match next_tag(reader)? {
Some(Tag::Open {
name,
attrs,
self_closing,
}) if name == "QualityLevel" => {
qualities.push(parse_quality_level(&attrs)?);
if !self_closing {
skip_element(reader)?;
}
}
Some(Tag::Open {
name,
attrs,
self_closing,
}) if name == "c" => {
let t: Option<u64> = parse_attr(&attrs, "t", "c")?;
let d: Option<u64> = parse_attr(&attrs, "d", "c")?;
let r: Option<u32> = parse_attr(&attrs, "r", "c")?;
chunks_list.push(C { t, d, r });
if !self_closing {
skip_element(reader)?;
}
}
Some(Tag::Open { self_closing, .. }) => {
if !self_closing {
skip_element(reader)?;
}
}
Some(Tag::Close { name }) => {
if name != EL {
return Err(SmoothParseError::MismatchedEndTag {
expected: EL.to_string(),
found: name.to_string(),
});
}
break;
}
None => return Err(SmoothParseError::UnexpectedEof),
}
}
}
Ok(StreamIndex {
stream_type,
name,
subtype,
chunks,
timescale,
url,
qualities,
chunks_list,
})
}
fn parse_quality_level(attrs: &[(String, String)]) -> Result<QualityLevel> {
const EL: &str = "QualityLevel";
let index: u32 = parse_attr(attrs, "Index", EL)?.unwrap_or(0);
let bitrate: u64 = required_attr_parse(attrs, "Bitrate", EL)?;
let four_cc = required_attr_owned(attrs, "FourCC", EL)?;
let codec_private_data = match attr(attrs, "CodecPrivateData") {
Some(hex) => hex_decode(hex)?,
None => Vec::new(),
};
let width: Option<u32> = parse_attr(attrs, "MaxWidth", EL)?;
let height: Option<u32> = parse_attr(attrs, "MaxHeight", EL)?;
let sampling_rate: Option<u32> = parse_attr(attrs, "SamplingRate", EL)?;
let channels: Option<u16> = parse_attr(attrs, "Channels", EL)?;
let bits_per_sample: Option<u16> = parse_attr(attrs, "BitsPerSample", EL)?;
let packet_size: Option<u32> = parse_attr(attrs, "PacketSize", EL)?;
let audio_tag: Option<u32> = parse_attr(attrs, "AudioTag", EL)?;
Ok(QualityLevel {
index,
bitrate,
four_cc,
codec_private_data,
width,
height,
sampling_rate,
channels,
bits_per_sample,
packet_size,
audio_tag,
})
}
const AVC_NAL_SPS: u8 = 7;
const TTML_NAMESPACE: &str = "http://www.w3.org/ns/ttml";
const WEBVTT_HEADER: &str = "WEBVTT";
const AVC_NAL_PPS: u8 = 8;
pub fn track_spec_from_quality_level(
track_id: TrackId,
timescale: u32,
stream_type: StreamType,
quality: &QualityLevel,
) -> CrateResult<TrackSpec> {
match stream_type {
StreamType::Video => {
if !is_fourcc(&quality.four_cc, FOURCC_H264)
&& !is_fourcc(&quality.four_cc, FOURCC_AVC1)
{
return Err(CrateError::UnsupportedCodec {
codec: "Smooth video FourCC (only H264/AVC1 is synthesised)",
});
}
let mut sps: Vec<AvcSps> = Vec::new();
let mut pps: Vec<AvcPps> = Vec::new();
for nal in iter_annexb_nals(&quality.codec_private_data) {
match nal_unit_type(NalCodec::Avc, nal) {
Some(AVC_NAL_SPS) => sps.push(AvcSps(nal.to_vec())),
Some(AVC_NAL_PPS) => pps.push(AvcPps(nal.to_vec())),
_ => {}
}
}
let config = avc_config_from_sps_pps(sps, pps)?;
let (width, height) = config
.config
.sps
.first()
.and_then(|s| s.decode().ok())
.map(|info| {
Ok::<_, CrateError>((
u16::try_from(info.width).map_err(|_| CrateError::InvalidValue {
field: "SPS sps_pic_width_max_in_luma_samples",
value: u64::from(info.width),
reason: "does not fit the IR's 16-bit dimension field",
})?,
u16::try_from(info.height).map_err(|_| CrateError::InvalidValue {
field: "SPS sps_pic_height_max_in_luma_samples",
value: u64::from(info.height),
reason: "does not fit the IR's 16-bit dimension field",
})?,
))
})
.transpose()?
.unwrap_or((
checked_u16(quality.width, "QualityLevel@MaxWidth")?,
checked_u16(quality.height, "QualityLevel@MaxHeight")?,
));
Ok(TrackSpec::new(
track_id,
timescale,
CodecConfig::Avc {
config,
width,
height,
},
))
}
StreamType::Audio => {
if !is_fourcc(&quality.four_cc, FOURCC_AACL)
&& !is_fourcc(&quality.four_cc, FOURCC_AACH)
{
return Err(CrateError::UnsupportedCodec {
codec: "Smooth audio FourCC (only AACL/AACH is synthesised)",
});
}
let asc = if quality.codec_private_data.is_empty() {
synthesise_asc(AOT_AAC_LC, quality)?
} else {
quality.codec_private_data.clone()
};
let config = aac_config_from_asc_bytes(asc)?;
Ok(TrackSpec::new(track_id, timescale, config))
}
StreamType::Text => {
use crate::pipeline::SubtitleSampleEntry;
use crate::smooth::{FOURCC_STPP, FOURCC_WVTT};
let (format, entry) = if is_fourcc(&quality.four_cc, FOURCC_STPP) {
(
SubtitleFormat::Ttml,
SubtitleSampleEntry::Ttml(XmlSubtitleSampleEntry::new(TTML_NAMESPACE)),
)
} else if is_fourcc(&quality.four_cc, FOURCC_WVTT) {
(
SubtitleFormat::WebVtt,
SubtitleSampleEntry::WebVtt(WvttSampleEntry::new(WEBVTT_HEADER)),
)
} else {
return Err(CrateError::UnsupportedCodec {
codec: "Smooth text FourCC (only stpp/wvtt is synthesised)",
});
};
Ok(TrackSpec::new(
track_id,
timescale,
CodecConfig::Subtitle {
format,
sample_entry: Some(entry),
},
))
}
}
}
fn is_fourcc(value: &str, expected: &str) -> bool {
value.eq_ignore_ascii_case(expected)
}
fn checked_u16(value: Option<u32>, field: &'static str) -> CrateResult<u16> {
match value {
Some(v) => u16::try_from(v).map_err(|_| CrateError::InvalidValue {
field,
value: u64::from(v),
reason: "does not fit the IR's 16-bit dimension field",
}),
None => Ok(0),
}
}
fn synthesise_asc(aot: u8, quality: &QualityLevel) -> CrateResult<Vec<u8>> {
let rate = quality.sampling_rate.ok_or(CrateError::UnsupportedCodec {
codec: "Smooth AAC QualityLevel with no SamplingRate and no CodecPrivateData",
})?;
let channels = quality.channels.ok_or(CrateError::UnsupportedCodec {
codec: "Smooth AAC QualityLevel with no Channels and no CodecPrivateData",
})?;
let sf_index = frequency_index_for(rate).ok_or(CrateError::UnsupportedCodec {
codec: "Smooth AAC SamplingRate is not an ISO/IEC 14496-3 Table 1.10 entry",
})?;
let channel_raw = channel_configuration_for(channels)?;
let mut out = alloc::vec![0u8; 2];
out[0] = (aot << 3) | (sf_index >> 1);
out[1] = ((sf_index & 0x01) << 7) | (channel_raw << 3);
Ok(out)
}
fn frequency_index_for(rate: u32) -> Option<u8> {
SAMPLING_FREQUENCY_INDICES
.iter()
.find(|c| c.table_hz() == Some(rate))
.map(|c| c.raw())
}
const AOT_AAC_LC: u8 = 2;
const SAMPLING_FREQUENCY_INDICES: [SamplingFrequencyIndex; 12] = [
SamplingFrequencyIndex::Fs96000,
SamplingFrequencyIndex::Fs88200,
SamplingFrequencyIndex::Fs64000,
SamplingFrequencyIndex::Fs48000,
SamplingFrequencyIndex::Fs44100,
SamplingFrequencyIndex::Fs32000,
SamplingFrequencyIndex::Fs24000,
SamplingFrequencyIndex::Fs22050,
SamplingFrequencyIndex::Fs16000,
SamplingFrequencyIndex::Fs12000,
SamplingFrequencyIndex::Fs8000,
SamplingFrequencyIndex::Fs7350,
];
fn channel_configuration_for(channels: u16) -> CrateResult<u8> {
match channels {
1 => Ok(ChannelConfiguration::Mono.raw()),
2 => Ok(ChannelConfiguration::Stereo.raw()),
3 => Ok(ChannelConfiguration::Ch3.raw()),
4 => Ok(ChannelConfiguration::Ch4.raw()),
5 => Ok(ChannelConfiguration::Ch5.raw()),
6 => Ok(ChannelConfiguration::Ch5_1.raw()),
8 => Ok(ChannelConfiguration::Ch7_1.raw()),
_ => Err(CrateError::UnsupportedCodec {
codec: "Smooth AAC channel count with no Table 1.19 configuration",
}),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn invalid_character_data_in_skipped_content_is_rejected() {
for bad in ["&nope;", "", "", "\u{1}", "<![CDATA[\u{1}]]>"] {
let skipped = format!(
"<SmoothStreamingMedia><Protection><ProtectionHeader>{bad}</ProtectionHeader></Protection></SmoothStreamingMedia>"
);
assert!(SmoothManifest::parse(&skipped).is_err(), "skipped {bad:?}");
let between = format!(
"<SmoothStreamingMedia>{bad}<StreamIndex Type=\"video\"/></SmoothStreamingMedia>"
);
assert!(SmoothManifest::parse(&between).is_err(), "between {bad:?}");
}
}
const SMALL_MANIFEST: &str = r#"<?xml version="1.0" encoding="utf-8"?>
<SmoothStreamingMedia MajorVersion="2" MinorVersion="0" Duration="30000000" TimeScale="10000000">
<StreamIndex Type="video" Subtype="" Chunks="2" QualityLevels="1" Url="QualityLevels({bitrate})/Fragments(video={start time})">
<QualityLevel Index="0" Bitrate="500000" FourCC="H264" MaxWidth="640" MaxHeight="360" CodecPrivateData="000000016742C01EAB0000000168CE3C80"/>
<c n="0" t="0" d="20000000"/>
<c n="1" d="10000000"/>
</StreamIndex>
<StreamIndex Type="audio" Subtype="" Chunks="1" QualityLevels="1" Url="QualityLevels({bitrate})/Fragments(audio={start time})">
<QualityLevel Index="0" Bitrate="128000" FourCC="AACL" SamplingRate="44100" Channels="2" BitsPerSample="16" AudioTag="255" CodecPrivateData="1210"/>
<c d="30000000"/>
</StreamIndex>
</SmoothStreamingMedia>"#;
#[test]
fn parses_small_manifest_structure() {
let m = SmoothManifest::parse(SMALL_MANIFEST).expect("parse");
assert_eq!(m.major_version, 2);
assert_eq!(m.minor_version, 0);
assert_eq!(m.timescale, 10_000_000);
assert_eq!(m.duration, Some(30_000_000));
assert!(!m.is_live);
assert_eq!(m.look_ahead_fragment_count, None);
assert_eq!(m.streams.len(), 2);
let video = &m.streams[0];
assert_eq!(video.stream_type, StreamType::Video);
assert_eq!(video.chunks, Some(2));
assert_eq!(
video.url,
"QualityLevels({bitrate})/Fragments(video={start time})"
);
assert_eq!(video.qualities.len(), 1);
let vq = &video.qualities[0];
assert_eq!(vq.four_cc, "H264");
assert_eq!(vq.bitrate, 500_000);
assert_eq!(vq.width, Some(640));
assert_eq!(vq.height, Some(360));
assert_eq!(
vq.codec_private_data,
alloc::vec![
0x00, 0x00, 0x00, 0x01, 0x67, 0x42, 0xC0, 0x1E, 0xAB, 0x00, 0x00, 0x00, 0x01, 0x68,
0xCE, 0x3C, 0x80
]
);
assert_eq!(video.chunks_list.len(), 2);
let audio = &m.streams[1];
assert_eq!(audio.stream_type, StreamType::Audio);
let aq = &audio.qualities[0];
assert_eq!(aq.four_cc, "AACL");
assert_eq!(aq.sampling_rate, Some(44100));
assert_eq!(aq.channels, Some(2));
assert_eq!(aq.audio_tag, Some(255));
assert_eq!(aq.codec_private_data, alloc::vec![0x12, 0x10]);
}
#[test]
fn live_manifest_shape_parses() {
let xml = r#"<SmoothStreamingMedia MajorVersion="2" MinorVersion="0" TimeScale="10000000" IsLive="TRUE" LookAheadFragmentCount="2" DVRWindowLength="600000000">
<StreamIndex Type="video" Url="QualityLevels({bitrate})/Fragments(video={start time})">
<QualityLevel Index="0" Bitrate="1" FourCC="H264"/>
</StreamIndex>
</SmoothStreamingMedia>"#;
let m = SmoothManifest::parse(xml).expect("parse live manifest");
assert!(m.is_live);
assert_eq!(m.look_ahead_fragment_count, Some(2));
assert_eq!(m.dvr_window_length, Some(600_000_000));
}
#[test]
fn missing_is_live_defaults_false() {
let xml = r#"<SmoothStreamingMedia MajorVersion="2" MinorVersion="0" TimeScale="10000000">
</SmoothStreamingMedia>"#;
let m = SmoothManifest::parse(xml).expect("parse");
assert!(!m.is_live);
}
#[test]
fn enumerate_chunks_accumulates_and_expands_r() {
let si = StreamIndex {
stream_type: StreamType::Video,
name: None,
subtype: None,
chunks: None,
timescale: None,
url: String::new(),
qualities: Vec::new(),
chunks_list: alloc::vec![
C {
t: Some(0),
d: Some(1000),
r: Some(2),
},
C {
t: None,
d: Some(500),
r: None,
},
],
};
let chunks = si.enumerate_chunks().expect("enumerate");
assert_eq!(
chunks,
alloc::vec![(0, 1000), (1000, 1000), (2000, 1000), (3000, 500)]
);
}
#[test]
fn enumerate_chunks_matches_fixture_timeline() {
let m = SmoothManifest::parse(SMALL_MANIFEST).unwrap();
let chunks = m.streams[0].enumerate_chunks().unwrap();
assert_eq!(
chunks,
alloc::vec![(0, 20_000_000), (20_000_000, 10_000_000)]
);
}
#[test]
fn resolve_fragment_url_substitutes_tokens() {
let m = SmoothManifest::parse(SMALL_MANIFEST).unwrap();
let video = &m.streams[0];
let resolved = video.resolve_fragment_url(500_000, 20_000_000);
assert_eq!(resolved, "QualityLevels(500000)/Fragments(video=20000000)");
}
#[test]
fn chunk_run_cap_bites_without_huge_alloc() {
let xml = r#"<SmoothStreamingMedia MajorVersion="2" MinorVersion="0" TimeScale="10000000">
<StreamIndex Type="video" Url="Fragments(video={start time})">
<QualityLevel Index="0" Bitrate="1" FourCC="H264"/>
<c d="1" r="4000000000"/>
</StreamIndex>
</SmoothStreamingMedia>"#;
let m = SmoothManifest::parse(xml).expect("XML shape itself parses");
let err = m.streams[0].enumerate_chunks().unwrap_err();
assert!(matches!(err, SmoothParseError::ChunkRunTooLong { .. }));
}
#[test]
fn codec_private_data_hex_cap_bites() {
let huge_hex: String = "AB".repeat(MAX_CODEC_PRIVATE_DATA_HEX_LEN / 2 + 1);
let err = hex_decode(&huge_hex).unwrap_err();
assert!(matches!(
err,
SmoothParseError::CodecPrivateDataTooLong { .. }
));
}
#[test]
fn codec_private_data_hex_cap_bites_via_manifest_parse() {
let huge_hex: String = "AB".repeat(MAX_CODEC_PRIVATE_DATA_HEX_LEN / 2 + 1);
let xml = alloc::format!(
r#"<SmoothStreamingMedia MajorVersion="2" MinorVersion="0" TimeScale="10000000">
<StreamIndex Type="video" Url="Fragments(video={{start time}})">
<QualityLevel Index="0" Bitrate="1" FourCC="H264" CodecPrivateData="{huge_hex}"/>
</StreamIndex>
</SmoothStreamingMedia>"#
);
let err = SmoothManifest::parse(&xml).unwrap_err();
assert!(matches!(
err,
SmoothParseError::CodecPrivateDataTooLong { .. }
));
}
#[test]
fn hex_decode_round_trips() {
assert_eq!(
hex_decode("0001ABff").unwrap(),
alloc::vec![0x00, 0x01, 0xAB, 0xFF]
);
}
#[test]
fn hex_decode_rejects_odd_length() {
assert!(hex_decode("ABC").is_err());
}
#[test]
fn hex_decode_rejects_non_hex() {
assert!(hex_decode("ZZ").is_err());
}
#[test]
fn hex_decode_empty_is_empty() {
assert_eq!(hex_decode("").unwrap(), Vec::<u8>::new());
}
#[test]
fn unterminated_tag_is_error_not_panic() {
let err = SmoothManifest::parse("<SmoothStreamingMedia MajorVersion=\"2\"").unwrap_err();
assert!(matches!(
err,
SmoothParseError::UnterminatedTag { .. } | SmoothParseError::UnexpectedEof
));
}
#[test]
fn wrong_root_element_is_error() {
let err = SmoothManifest::parse("<NotSmooth/>").unwrap_err();
assert!(matches!(err, SmoothParseError::UnexpectedElement { .. }));
}
#[test]
fn empty_input_is_error() {
assert_eq!(
SmoothManifest::parse("").unwrap_err(),
SmoothParseError::UnexpectedEof
);
}
#[test]
fn missing_required_attribute_is_error() {
let xml = r#"<SmoothStreamingMedia MajorVersion="2" MinorVersion="0" TimeScale="10000000">
<StreamIndex Type="video" Url="x">
<QualityLevel Index="0" FourCC="H264"/>
</StreamIndex>
</SmoothStreamingMedia>"#;
let err = SmoothManifest::parse(xml).unwrap_err();
assert!(matches!(
err,
SmoothParseError::MissingAttribute {
element: "QualityLevel",
attr: "Bitrate"
}
));
}
#[test]
fn unknown_stream_type_is_error() {
let xml = r#"<SmoothStreamingMedia MajorVersion="2" MinorVersion="0" TimeScale="10000000">
<StreamIndex Type="bogus" Url="x"/>
</SmoothStreamingMedia>"#;
let err = SmoothManifest::parse(xml).unwrap_err();
assert!(matches!(
err,
SmoothParseError::InvalidAttributeValue {
element: "StreamIndex",
attr: "Type",
..
}
));
}
#[test]
fn truncated_garbage_shapes_never_panic() {
let inputs = [
"<SmoothStreamingMedia",
"<SmoothStreamingMedia MajorVersion=",
"<SmoothStreamingMedia><StreamIndex",
"<SmoothStreamingMedia><StreamIndex Type=\"video\"><QualityLevel",
"<SmoothStreamingMedia></OtherTag>",
"not xml at all",
"<<<<<<<",
];
for input in inputs {
let _ = SmoothManifest::parse(input);
}
}
#[test]
fn track_spec_from_quality_level_video_avc() {
let cpd = alloc::vec![
0x00, 0x00, 0x00, 0x01, 0x67, 0x42, 0xC0, 0x1E, 0xAB, 0x00, 0x00, 0x00, 0x01, 0x68,
0xCE, 0x3C, 0x80,
];
let quality = QualityLevel {
index: 0,
bitrate: 500_000,
four_cc: "H264".to_string(),
codec_private_data: cpd,
width: Some(640),
height: Some(360),
sampling_rate: None,
channels: None,
bits_per_sample: None,
packet_size: None,
audio_tag: None,
};
let spec =
track_spec_from_quality_level(TrackId::new(1), 90_000, StreamType::Video, &quality)
.expect("build video TrackSpec");
match spec.config {
CodecConfig::Avc { config, .. } => {
assert_eq!(config.config.sps.len(), 1);
assert_eq!(config.config.pps.len(), 1);
assert_eq!(
config.config.sps[0].0,
alloc::vec![0x67, 0x42, 0xC0, 0x1E, 0xAB]
);
assert_eq!(config.config.pps[0].0, alloc::vec![0x68, 0xCE, 0x3C, 0x80]);
}
_ => panic!("expected CodecConfig::Avc"),
}
}
#[test]
fn track_spec_from_quality_level_audio_aac() {
let quality = QualityLevel {
index: 0,
bitrate: 128_000,
four_cc: "AACL".to_string(),
codec_private_data: alloc::vec![0x12, 0x10],
width: None,
height: None,
sampling_rate: Some(44100),
channels: Some(2),
bits_per_sample: Some(16),
packet_size: None,
audio_tag: Some(255),
};
let spec =
track_spec_from_quality_level(TrackId::new(2), 44_100, StreamType::Audio, &quality)
.expect("build audio TrackSpec");
match spec.config {
CodecConfig::Aac {
sample_rate,
channel_count,
..
} => {
assert_eq!(sample_rate, 44100);
assert_eq!(channel_count, 2);
}
_ => panic!("expected CodecConfig::Aac"),
}
}
#[test]
fn track_spec_from_quality_level_audio_dolby_fourcc_rejected() {
let quality = QualityLevel {
index: 0,
bitrate: 128_000,
four_cc: "EC-3".to_string(),
codec_private_data: alloc::vec![0x10, 0x3D],
width: None,
height: None,
sampling_rate: Some(48_000),
channels: Some(6),
bits_per_sample: Some(16),
packet_size: None,
audio_tag: None,
};
match track_spec_from_quality_level(TrackId::new(2), 48_000, StreamType::Audio, &quality) {
Err(CrateError::UnsupportedCodec { codec }) => {
assert_eq!(codec, "Smooth audio FourCC (only AACL/AACH is synthesised)");
}
other => panic!("expected UnsupportedCodec for EC-3, got {other:?}"),
}
let quality = QualityLevel {
four_cc: "AC-3".to_string(),
..quality
};
assert!(matches!(
track_spec_from_quality_level(TrackId::new(2), 48_000, StreamType::Audio, &quality),
Err(CrateError::UnsupportedCodec { .. })
));
}
#[test]
fn track_spec_from_quality_level_video_non_h264_fourcc_rejected() {
let quality = QualityLevel {
index: 0,
bitrate: 1_000_000,
four_cc: "H265".to_string(),
codec_private_data: alloc::vec![0x00, 0x00, 0x00, 0x01, 0x67, 0x42, 0xC0, 0x1E, 0xAB],
width: Some(640),
height: Some(360),
sampling_rate: None,
channels: None,
bits_per_sample: None,
packet_size: None,
audio_tag: None,
};
match track_spec_from_quality_level(TrackId::new(1), 90_000, StreamType::Video, &quality) {
Err(CrateError::UnsupportedCodec { codec }) => {
assert_eq!(codec, "Smooth video FourCC (only H264/AVC1 is synthesised)");
}
other => panic!("expected UnsupportedCodec for H265, got {other:?}"),
}
}
#[test]
fn four_cc_matching_is_case_insensitive() {
for cc in ["H264", "h264", "AVC1", "avc1"] {
let quality = QualityLevel {
index: 0,
bitrate: 500_000,
four_cc: cc.to_string(),
codec_private_data: alloc::vec![
0x00, 0x00, 0x00, 0x01, 0x67, 0x42, 0xC0, 0x1E, 0xAB, 0x00, 0x00, 0x00, 0x01,
0x68, 0xCE, 0x3C, 0x80,
],
width: Some(640),
height: Some(360),
sampling_rate: None,
channels: None,
bits_per_sample: None,
packet_size: None,
audio_tag: None,
};
assert!(
track_spec_from_quality_level(TrackId::new(1), 90_000, StreamType::Video, &quality)
.is_ok(),
"video FourCC {cc} must be accepted"
);
}
for cc in ["AACL", "aacl", "AACH", "aach"] {
let quality = QualityLevel {
index: 0,
bitrate: 128_000,
four_cc: cc.to_string(),
codec_private_data: alloc::vec![0x12, 0x10],
width: None,
height: None,
sampling_rate: Some(44_100),
channels: Some(2),
bits_per_sample: Some(16),
packet_size: None,
audio_tag: Some(255),
};
assert!(
track_spec_from_quality_level(TrackId::new(2), 44_100, StreamType::Audio, &quality)
.is_ok(),
"audio FourCC {cc} must be accepted"
);
}
}
#[test]
fn empty_codec_private_data_synthesises_aac_lc_asc() {
let quality = QualityLevel {
index: 0,
bitrate: 128_000,
four_cc: "AACL".to_string(),
codec_private_data: Vec::new(),
width: None,
height: None,
sampling_rate: Some(44_100),
channels: Some(2),
bits_per_sample: Some(16),
packet_size: None,
audio_tag: Some(255),
};
assert_eq!(
synthesise_asc(AOT_AAC_LC, &quality).unwrap(),
alloc::vec![0x12, 0x10]
);
let asc =
<crate::aac_asc::AudioSpecificConfig as broadcast_common::Parse>::parse(&[0x12, 0x10])
.unwrap();
assert_eq!(asc.sampling_frequency_index.raw(), 4);
assert_eq!(asc.channel_configuration.raw(), 2);
let spec =
track_spec_from_quality_level(TrackId::new(2), 44_100, StreamType::Audio, &quality)
.expect("empty CodecPrivateData must synthesise an ASC");
let CodecConfig::Aac {
sample_rate,
channel_count,
esds,
..
} = spec.config
else {
panic!("expected CodecConfig::Aac");
};
assert_eq!((sample_rate, channel_count), (44_100, 2));
let dsi = esds
.es_descriptor
.decoder_config
.as_ref()
.and_then(|d| d.decoder_specific_info.as_ref())
.expect("esds carries a DecoderSpecificInfo");
assert_eq!(
dsi.data.as_ref(),
[0x12u8, 0x10],
"the esds DecoderSpecificInfo must be the literal AAC-LC ASC 12 10"
);
}
#[test]
fn empty_codec_private_data_synthesises_core_asc_for_aach() {
let quality = QualityLevel {
index: 0,
bitrate: 64_000,
four_cc: "AACH".to_string(),
codec_private_data: Vec::new(),
width: None,
height: None,
sampling_rate: Some(44_100),
channels: Some(2),
bits_per_sample: Some(16),
packet_size: None,
audio_tag: Some(255),
};
let spec =
track_spec_from_quality_level(TrackId::new(3), 44_100, StreamType::Audio, &quality)
.expect("AACH must synthesise a core ASC");
let CodecConfig::Aac {
sample_rate,
channel_count,
esds,
..
} = spec.config
else {
panic!("expected CodecConfig::Aac");
};
assert_eq!((sample_rate, channel_count), (44_100, 2));
let dsi = esds
.es_descriptor
.decoder_config
.as_ref()
.and_then(|d| d.decoder_specific_info.as_ref())
.expect("esds carries a DecoderSpecificInfo");
assert_eq!(
dsi.data.as_ref(),
[0x12u8, 0x10],
"the esds DecoderSpecificInfo must be the core AAC-LC ASC"
);
let asc =
<crate::aac_asc::AudioSpecificConfig as broadcast_common::Parse>::parse(&dsi.data)
.unwrap();
assert_eq!(asc.audio_object_type.raw(), AOT_AAC_LC);
let he = asc.heaac_signaling();
assert!(!he.sbr_present, "SBR is implicit, not explicitly signalled");
assert!(!he.ps_present);
}
#[test]
fn channel_configurations_synthesise_literal_asc_bytes() {
let base = QualityLevel {
index: 0,
bitrate: 128_000,
four_cc: "AACL".to_string(),
codec_private_data: Vec::new(),
width: None,
height: None,
sampling_rate: Some(48_000),
channels: Some(2),
bits_per_sample: Some(16),
packet_size: None,
audio_tag: Some(255),
};
for (channels, expected) in [
(1u16, [0x11u8, 0x88]),
(2, [0x11, 0x90]),
(6, [0x11, 0xB0]),
(8, [0x11, 0xB8]),
] {
let q = QualityLevel {
channels: Some(channels),
..base.clone()
};
assert_eq!(
synthesise_asc(AOT_AAC_LC, &q).unwrap(),
expected,
"channels={channels}"
);
let asc =
<crate::aac_asc::AudioSpecificConfig as broadcast_common::Parse>::parse(&expected)
.unwrap();
assert_eq!(asc.channel_configuration.channel_count(), Some(channels));
}
let seven = QualityLevel {
channels: Some(7),
..base
};
assert!(matches!(
synthesise_asc(AOT_AAC_LC, &seven),
Err(CrateError::UnsupportedCodec { .. })
));
}
#[test]
fn unsupported_rate_or_channels_are_rejected_not_guessed() {
let base = QualityLevel {
index: 0,
bitrate: 128_000,
four_cc: "AACL".to_string(),
codec_private_data: Vec::new(),
width: None,
height: None,
sampling_rate: Some(44_100),
channels: Some(2),
bits_per_sample: Some(16),
packet_size: None,
audio_tag: Some(255),
};
let odd_rate = QualityLevel {
sampling_rate: Some(44_000),
..base.clone()
};
assert!(matches!(
synthesise_asc(AOT_AAC_LC, &odd_rate),
Err(CrateError::UnsupportedCodec { .. })
));
let odd_channels = QualityLevel {
channels: Some(9),
..base.clone()
};
assert!(matches!(
synthesise_asc(AOT_AAC_LC, &odd_channels),
Err(CrateError::UnsupportedCodec { .. })
));
let no_rate = QualityLevel {
sampling_rate: None,
..base.clone()
};
assert!(synthesise_asc(AOT_AAC_LC, &no_rate).is_err());
let no_channels = QualityLevel {
channels: None,
..base
};
assert!(synthesise_asc(AOT_AAC_LC, &no_channels).is_err());
}
#[test]
fn oversized_sps_dimensions_are_invalid_value() {
let quality = QualityLevel {
index: 0,
bitrate: 1,
four_cc: "H264".to_string(),
codec_private_data: alloc::vec![
0x00, 0x00, 0x00, 0x01, 0x67, 0x42, 0x00, 0x1F, 0xF4, 0x00, 0x08, 0x00, 0x38, 0x80,
],
width: None,
height: None,
sampling_rate: None,
channels: None,
bits_per_sample: None,
packet_size: None,
audio_tag: None,
};
let decoded = crate::sps::decode_avc_sps(&quality.codec_private_data[4..])
.expect("the oversize SPS decodes");
assert_eq!(decoded.width, 65_536);
match track_spec_from_quality_level(TrackId::new(1), 90_000, StreamType::Video, &quality) {
Err(CrateError::InvalidValue {
field: "SPS sps_pic_width_max_in_luma_samples",
value: 65_536,
..
}) => {}
other => panic!("expected an SPS width overflow InvalidValue, got {other:?}"),
}
}
#[test]
fn oversized_quality_level_dimensions_are_rejected() {
let quality = QualityLevel {
index: 0,
bitrate: 1,
four_cc: "H264".to_string(),
codec_private_data: alloc::vec![0x00, 0x00, 0x00, 0x01, 0x67, 0xFF, 0xFF, 0xFF, 0xFF,],
width: Some(70_000),
height: Some(360),
sampling_rate: None,
channels: None,
bits_per_sample: None,
packet_size: None,
audio_tag: None,
};
match track_spec_from_quality_level(TrackId::new(1), 90_000, StreamType::Video, &quality) {
Err(CrateError::InvalidValue {
field: "QualityLevel@MaxWidth",
value: 70_000,
..
}) => {}
other => panic!("expected a MaxWidth overflow error, got {other:?}"),
}
}
#[test]
fn track_spec_from_quality_level_text_unsupported() {
let quality = QualityLevel {
index: 0,
bitrate: 1,
four_cc: "TTML".to_string(),
codec_private_data: Vec::new(),
width: None,
height: None,
sampling_rate: None,
channels: None,
bits_per_sample: None,
packet_size: None,
audio_tag: None,
};
assert!(
track_spec_from_quality_level(TrackId::new(3), 1000, StreamType::Text, &quality)
.is_err()
);
}
#[test]
fn track_spec_from_quality_level_video_no_sps_errors_not_panics() {
let quality = QualityLevel {
index: 0,
bitrate: 1,
four_cc: "H264".to_string(),
codec_private_data: Vec::new(),
width: None,
height: None,
sampling_rate: None,
channels: None,
bits_per_sample: None,
packet_size: None,
audio_tag: None,
};
assert!(
track_spec_from_quality_level(TrackId::new(1), 90_000, StreamType::Video, &quality)
.is_err()
);
}
}