use alloc::string::{String, ToString};
use alloc::vec::Vec;
use core::fmt;
use core::str::FromStr;
use core::time::Duration;
use crate::xml_parse::{XmlError, XmlEvent, XmlTokenizer, skip_element};
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum DashParseError {
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,
},
InvalidDuration {
value: String,
},
TimelineTooLong {
count_hint: u64,
},
MismatchedEndTag {
expected: &'static str,
found: String,
},
}
impl fmt::Display for DashParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
DashParseError::UnexpectedEof => {
write!(f, "unexpected end of input while parsing MPD XML")
}
DashParseError::UnterminatedTag { pos } => {
write!(
f,
"unterminated XML tag/comment/declaration at byte offset {pos}"
)
}
DashParseError::MalformedAttribute { pos } => {
write!(f, "malformed XML attribute near byte offset {pos}")
}
DashParseError::UnexpectedElement { expected, found } => {
if found.is_empty() {
write!(f, "expected element <{expected}>, found none")
} else {
write!(f, "expected element <{expected}>, found <{found}>")
}
}
DashParseError::MissingAttribute { element, attr } => {
write!(f, "<{element}> is missing required attribute @{attr}")
}
DashParseError::InvalidAttributeValue {
element,
attr,
value,
} => write!(f, "<{element}>@{attr} has invalid value {value:?}"),
DashParseError::InvalidDuration { value } => {
write!(f, "invalid xs:duration {value:?}")
}
DashParseError::TimelineTooLong { count_hint } => {
write!(
f,
"SegmentTimeline exceeded max segment count ({count_hint} > {})",
MAX_TIMELINE_SEGMENTS
)
}
DashParseError::MismatchedEndTag { expected, found } => {
if found.is_empty() {
write!(f, "expected closing tag </{expected}>, found none")
} else {
write!(f, "expected closing tag </{expected}>, found </{found}>")
}
}
}
}
}
#[cfg(feature = "std")]
impl std::error::Error for DashParseError {}
impl From<XmlError> for DashParseError {
fn from(err: XmlError) -> Self {
match err {
XmlError::UnexpectedEof => DashParseError::UnexpectedEof,
XmlError::UnterminatedTag { pos } => DashParseError::UnterminatedTag { pos },
XmlError::MalformedAttribute { pos } => DashParseError::MalformedAttribute { pos },
XmlError::MismatchedEndTag { expected, found } => {
DashParseError::MismatchedEndTag { expected, found }
}
}
}
}
type Result<T> = core::result::Result<T, DashParseError>;
const DEFAULT_TIMESCALE: u64 = 1;
const DEFAULT_START_NUMBER: u64 = 1;
const DEFAULT_PRESENTATION_TIME_OFFSET: u64 = 0;
const DEFAULT_REPEAT: i64 = 0;
pub const MAX_TIMELINE_SEGMENTS: usize = 100_000;
pub const MAX_FORMAT_WIDTH: usize = 20;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[non_exhaustive]
pub enum MpdType {
#[default]
Static,
Dynamic,
}
impl MpdType {
pub fn name(&self) -> &'static str {
match self {
MpdType::Static => "static",
MpdType::Dynamic => "dynamic",
}
}
}
broadcast_common::impl_spec_display!(MpdType);
#[derive(Debug, Clone, PartialEq)]
pub struct Mpd {
pub profiles: String,
pub mpd_type: MpdType,
pub media_presentation_duration: Option<Duration>,
pub minimum_update_period: Option<Duration>,
pub availability_start_time: Option<String>,
pub time_shift_buffer_depth: Option<Duration>,
pub periods: Vec<Period>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct Period {
pub id: Option<String>,
pub start: Option<Duration>,
pub duration: Option<Duration>,
pub adaptation_sets: Vec<AdaptationSet>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct AdaptationSet {
pub mime_type: Option<String>,
pub content_type: Option<String>,
pub segment_template: Option<SegmentTemplate>,
pub representations: Vec<Representation>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct Representation {
pub id: String,
pub bandwidth: u64,
pub codecs: Option<String>,
pub width: Option<u32>,
pub height: Option<u32>,
pub frame_rate: Option<String>,
pub audio_sampling_rate: Option<u32>,
pub mime_type: Option<String>,
pub segment_template: Option<SegmentTemplate>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct SegmentTemplate {
pub timescale: u64,
pub initialization: Option<String>,
pub media: Option<String>,
pub start_number: u64,
pub duration: Option<u64>,
pub presentation_time_offset: u64,
pub timeline: Option<SegmentTimeline>,
}
impl SegmentTemplate {
pub fn number_sequence(&self, count: usize) -> Vec<u64> {
(0..count as u64)
.map(|i| self.start_number.saturating_add(i))
.collect()
}
pub fn resolve(
template: &str,
representation_id: &str,
number: Option<u64>,
time: Option<u64>,
bandwidth: Option<u64>,
) -> String {
let mut out = String::with_capacity(template.len());
let mut rest = template;
while let Some(dollar) = rest.find('$') {
out.push_str(&rest[..dollar]);
let tail = &rest[dollar + 1..];
if let Some(after_escape) = tail.strip_prefix('$') {
out.push('$');
rest = after_escape;
continue;
}
match tail.find('$') {
Some(end) => {
let ident = &tail[..end];
let (name, width) = match ident.split_once('%') {
Some((n, fmt)) => (
n,
fmt.strip_suffix('d').and_then(|w| w.parse::<usize>().ok()),
),
None => (ident, None),
};
match name {
"RepresentationID" => out.push_str(representation_id),
"Number" => push_numeric(&mut out, number, width, ident),
"Time" => push_numeric(&mut out, time, width, ident),
"Bandwidth" => push_numeric(&mut out, bandwidth, width, ident),
_ => {
out.push('$');
out.push_str(ident);
out.push('$');
}
}
rest = &tail[end + 1..];
}
None => {
out.push('$');
rest = tail;
}
}
}
out.push_str(rest);
out
}
}
fn push_numeric(out: &mut String, value: Option<u64>, width: Option<usize>, ident: &str) {
match value {
Some(v) => match width {
Some(w) => out.push_str(&format_width(v, w)),
None => {
out.push_str(&v.to_string());
}
},
None => {
out.push('$');
out.push_str(ident);
out.push('$');
}
}
}
fn format_width(n: u64, width: usize) -> String {
let width = width.min(MAX_FORMAT_WIDTH);
let digits = n.to_string();
if digits.len() >= width {
digits
} else {
let mut out = String::with_capacity(width);
for _ in 0..(width - digits.len()) {
out.push('0');
}
out.push_str(&digits);
out
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct SegmentTimeline {
pub segments: Vec<S>,
}
impl SegmentTimeline {
pub fn enumerate(&self, start_number: u64) -> Result<Vec<(u64, u64)>> {
let mut out = Vec::new();
let mut number = start_number;
let mut time: u64 = 0;
let mut total_segments: u64 = 0;
for s in &self.segments {
if let Some(t) = s.t {
time = t;
}
let repeats: u64 = if s.r < 0 {
1
} else {
(s.r as u64).saturating_add(1)
};
if repeats > MAX_TIMELINE_SEGMENTS as u64 {
return Err(DashParseError::TimelineTooLong {
count_hint: repeats,
});
}
total_segments = total_segments.saturating_add(repeats);
if total_segments as usize > MAX_TIMELINE_SEGMENTS {
return Err(DashParseError::TimelineTooLong {
count_hint: total_segments,
});
}
for _ in 0..repeats {
out.push((number, time));
number = number.saturating_add(1);
time = time.saturating_add(s.d);
}
}
Ok(out)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct S {
pub t: Option<u64>,
pub d: u64,
pub r: i64,
}
const SECONDS_PER_DAY: u64 = 24 * 60 * 60;
const SECONDS_PER_HOUR: u64 = 60 * 60;
const SECONDS_PER_MINUTE: u64 = 60;
const NANOSECOND_DIGITS: usize = 9;
pub fn parse_iso8601_duration(s: &str) -> Result<Duration> {
let trimmed = s.trim();
let invalid = || DashParseError::InvalidDuration {
value: trimmed.to_string(),
};
let rest = trimmed.strip_prefix('P').ok_or_else(invalid)?;
let (date_part, time_part) = match rest.find('T') {
Some(idx) => (&rest[..idx], Some(&rest[idx + 1..])),
None => (rest, None),
};
if date_part.is_empty() && time_part.is_none() {
return Err(invalid()); }
let mut total_secs: u64 = 0;
if !date_part.is_empty() {
let days_str = date_part.strip_suffix('D').ok_or_else(invalid)?;
let days: u64 = days_str.parse().map_err(|_| invalid())?;
total_secs = total_secs.saturating_add(days.saturating_mul(SECONDS_PER_DAY));
}
let mut nanos: u32 = 0;
if let Some(time_part) = time_part {
if time_part.is_empty() {
return Err(invalid()); }
let mut remaining = time_part;
if let Some(idx) = remaining.find('H') {
let n: u64 = remaining[..idx].parse().map_err(|_| invalid())?;
total_secs = total_secs.saturating_add(n.saturating_mul(SECONDS_PER_HOUR));
remaining = &remaining[idx + 1..];
}
if let Some(idx) = remaining.find('M') {
let n: u64 = remaining[..idx].parse().map_err(|_| invalid())?;
total_secs = total_secs.saturating_add(n.saturating_mul(SECONDS_PER_MINUTE));
remaining = &remaining[idx + 1..];
}
if let Some(idx) = remaining.find('S') {
let secs_str = &remaining[..idx];
let (whole, frac) = match secs_str.split_once('.') {
Some((w, f)) => (w, Some(f)),
None => (secs_str, None),
};
let whole_secs: u64 = if whole.is_empty() {
0
} else {
whole.parse().map_err(|_| invalid())?
};
total_secs = total_secs.saturating_add(whole_secs);
if let Some(frac) = frac {
nanos = parse_fraction_nanos(frac).map_err(|_| invalid())?;
}
remaining = &remaining[idx + 1..];
}
if !remaining.is_empty() {
return Err(invalid()); }
}
Ok(Duration::new(total_secs, nanos))
}
fn parse_fraction_nanos(frac: &str) -> core::result::Result<u32, ()> {
if frac.is_empty() || !frac.bytes().all(|b| b.is_ascii_digit()) {
return Err(());
}
let mut digits = String::with_capacity(NANOSECOND_DIGITS);
digits.push_str(frac);
while digits.len() < NANOSECOND_DIGITS {
digits.push('0');
}
digits.truncate(NANOSECOND_DIGITS);
digits.parse::<u32>().map_err(|_| ())
}
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(DashParseError::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(|_| DashParseError::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(DashParseError::MissingAttribute { element, attr: key })?;
v.trim()
.parse::<T>()
.map_err(|_| DashParseError::InvalidAttributeValue {
element,
attr: key,
value: v.to_string(),
})
}
fn parse_duration_attr(attrs: &[(String, String)], key: &str) -> Result<Option<Duration>> {
match attr(attrs, key) {
Some(v) => Ok(Some(parse_iso8601_duration(v)?)),
None => Ok(None),
}
}
impl Mpd {
pub fn parse(xml: &str) -> Result<Mpd> {
const EL: &str = "MPD";
let mut tok = XmlTokenizer::new(xml);
let (mpd_attrs, mpd_self_closing) = match tok.next_event()? {
Some(XmlEvent::Start {
name: "MPD",
attrs,
self_closing,
}) => (attrs, self_closing),
Some(XmlEvent::Start { name, .. }) => {
return Err(DashParseError::UnexpectedElement {
expected: EL,
found: name.to_string(),
});
}
Some(XmlEvent::End { .. }) => {
return Err(DashParseError::UnexpectedElement {
expected: EL,
found: String::new(),
});
}
None => return Err(DashParseError::UnexpectedEof),
};
let profiles = required_attr_owned(&mpd_attrs, "profiles", EL)?;
let mpd_type = match attr(&mpd_attrs, "type") {
Some("dynamic") => MpdType::Dynamic,
_ => MpdType::Static,
};
let media_presentation_duration =
parse_duration_attr(&mpd_attrs, "mediaPresentationDuration")?;
let minimum_update_period = parse_duration_attr(&mpd_attrs, "minimumUpdatePeriod")?;
let availability_start_time = attr_owned(&mpd_attrs, "availabilityStartTime");
let time_shift_buffer_depth = parse_duration_attr(&mpd_attrs, "timeShiftBufferDepth")?;
let mut periods = Vec::new();
if !mpd_self_closing {
loop {
match tok.next_event()? {
Some(XmlEvent::Start {
name: "Period",
attrs,
self_closing,
}) => periods.push(parse_period(&mut tok, attrs, self_closing)?),
Some(XmlEvent::Start { self_closing, .. }) => {
if !self_closing {
skip_element(&mut tok)?;
}
}
Some(XmlEvent::End { name }) => {
if name != EL {
return Err(DashParseError::MismatchedEndTag {
expected: EL,
found: name.to_string(),
});
}
break;
}
None => return Err(DashParseError::UnexpectedEof),
}
}
}
Ok(Mpd {
profiles,
mpd_type,
media_presentation_duration,
minimum_update_period,
availability_start_time,
time_shift_buffer_depth,
periods,
})
}
}
fn parse_period(
tok: &mut XmlTokenizer<'_>,
attrs: Vec<(String, String)>,
self_closing: bool,
) -> Result<Period> {
const EL: &str = "Period";
let id = attr_owned(&attrs, "id");
let start = parse_duration_attr(&attrs, "start")?;
let duration = parse_duration_attr(&attrs, "duration")?;
let mut adaptation_sets = Vec::new();
if !self_closing {
loop {
match tok.next_event()? {
Some(XmlEvent::Start {
name: "AdaptationSet",
attrs,
self_closing,
}) => adaptation_sets.push(parse_adaptation_set(tok, attrs, self_closing)?),
Some(XmlEvent::Start { self_closing, .. }) => {
if !self_closing {
skip_element(tok)?;
}
}
Some(XmlEvent::End { name }) => {
if name != EL {
return Err(DashParseError::MismatchedEndTag {
expected: EL,
found: name.to_string(),
});
}
break;
}
None => return Err(DashParseError::UnexpectedEof),
}
}
}
Ok(Period {
id,
start,
duration,
adaptation_sets,
})
}
fn parse_adaptation_set(
tok: &mut XmlTokenizer<'_>,
attrs: Vec<(String, String)>,
self_closing: bool,
) -> Result<AdaptationSet> {
const EL: &str = "AdaptationSet";
let mime_type = attr_owned(&attrs, "mimeType");
let content_type = attr_owned(&attrs, "contentType");
let mut segment_template = None;
let mut representations = Vec::new();
if !self_closing {
loop {
match tok.next_event()? {
Some(XmlEvent::Start {
name: "SegmentTemplate",
attrs,
self_closing,
}) => segment_template = Some(parse_segment_template(tok, attrs, self_closing)?),
Some(XmlEvent::Start {
name: "Representation",
attrs,
self_closing,
}) => representations.push(parse_representation(tok, attrs, self_closing)?),
Some(XmlEvent::Start { self_closing, .. }) => {
if !self_closing {
skip_element(tok)?;
}
}
Some(XmlEvent::End { name }) => {
if name != EL {
return Err(DashParseError::MismatchedEndTag {
expected: EL,
found: name.to_string(),
});
}
break;
}
None => return Err(DashParseError::UnexpectedEof),
}
}
}
if let Some(inherited) = &segment_template {
for r in &mut representations {
if r.segment_template.is_none() {
r.segment_template = Some(inherited.clone());
}
}
}
Ok(AdaptationSet {
mime_type,
content_type,
segment_template,
representations,
})
}
fn parse_representation(
tok: &mut XmlTokenizer<'_>,
attrs: Vec<(String, String)>,
self_closing: bool,
) -> Result<Representation> {
const EL: &str = "Representation";
let id = required_attr_owned(&attrs, "id", EL)?;
let bandwidth: u64 = required_attr_parse(&attrs, "bandwidth", EL)?;
let codecs = attr_owned(&attrs, "codecs");
let mime_type = attr_owned(&attrs, "mimeType");
let width: Option<u32> = parse_attr(&attrs, "width", EL)?;
let height: Option<u32> = parse_attr(&attrs, "height", EL)?;
let frame_rate = attr_owned(&attrs, "frameRate");
let audio_sampling_rate: Option<u32> = parse_attr(&attrs, "audioSamplingRate", EL)?;
let mut segment_template = None;
if !self_closing {
loop {
match tok.next_event()? {
Some(XmlEvent::Start {
name: "SegmentTemplate",
attrs,
self_closing,
}) => segment_template = Some(parse_segment_template(tok, attrs, self_closing)?),
Some(XmlEvent::Start { self_closing, .. }) => {
if !self_closing {
skip_element(tok)?;
}
}
Some(XmlEvent::End { name }) => {
if name != EL {
return Err(DashParseError::MismatchedEndTag {
expected: EL,
found: name.to_string(),
});
}
break;
}
None => return Err(DashParseError::UnexpectedEof),
}
}
}
Ok(Representation {
id,
bandwidth,
codecs,
width,
height,
frame_rate,
audio_sampling_rate,
mime_type,
segment_template,
})
}
fn parse_segment_template(
tok: &mut XmlTokenizer<'_>,
attrs: Vec<(String, String)>,
self_closing: bool,
) -> Result<SegmentTemplate> {
const EL: &str = "SegmentTemplate";
let timescale: u64 = parse_attr(&attrs, "timescale", EL)?.unwrap_or(DEFAULT_TIMESCALE);
let initialization = attr_owned(&attrs, "initialization");
let media = attr_owned(&attrs, "media");
let start_number: u64 = parse_attr(&attrs, "startNumber", EL)?.unwrap_or(DEFAULT_START_NUMBER);
let duration: Option<u64> = parse_attr(&attrs, "duration", EL)?;
let presentation_time_offset: u64 = parse_attr(&attrs, "presentationTimeOffset", EL)?
.unwrap_or(DEFAULT_PRESENTATION_TIME_OFFSET);
let mut timeline = None;
if !self_closing {
loop {
match tok.next_event()? {
Some(XmlEvent::Start {
name: "SegmentTimeline",
self_closing,
..
}) => timeline = Some(parse_segment_timeline(tok, self_closing)?),
Some(XmlEvent::Start { self_closing, .. }) => {
if !self_closing {
skip_element(tok)?;
}
}
Some(XmlEvent::End { name }) => {
if name != EL {
return Err(DashParseError::MismatchedEndTag {
expected: EL,
found: name.to_string(),
});
}
break;
}
None => return Err(DashParseError::UnexpectedEof),
}
}
}
Ok(SegmentTemplate {
timescale,
initialization,
media,
start_number,
duration,
presentation_time_offset,
timeline,
})
}
fn parse_segment_timeline(
tok: &mut XmlTokenizer<'_>,
self_closing: bool,
) -> Result<SegmentTimeline> {
const EL: &str = "SegmentTimeline";
let mut segments = Vec::new();
if !self_closing {
loop {
match tok.next_event()? {
Some(XmlEvent::Start {
name: "S",
attrs,
self_closing,
}) => {
let t: Option<u64> = parse_attr(&attrs, "t", "S")?;
let d: u64 = required_attr_parse(&attrs, "d", "S")?;
let r: i64 = parse_attr(&attrs, "r", "S")?.unwrap_or(DEFAULT_REPEAT);
segments.push(S { t, d, r });
if !self_closing {
skip_element(tok)?;
}
}
Some(XmlEvent::Start { self_closing, .. }) => {
if !self_closing {
skip_element(tok)?;
}
}
Some(XmlEvent::End { name }) => {
if name != EL {
return Err(DashParseError::MismatchedEndTag {
expected: EL,
found: name.to_string(),
});
}
break;
}
None => return Err(DashParseError::UnexpectedEof),
}
}
}
Ok(SegmentTimeline { segments })
}
#[cfg(test)]
mod tests {
use super::*;
use alloc::vec;
const SMALL_MPD: &str = r#"<?xml version="1.0" encoding="utf-8"?>
<MPD xmlns="urn:mpeg:dash:schema:mpd:2011" profiles="urn:mpeg:dash:profile:isoff-live:2011" type="static" mediaPresentationDuration="PT3.0S">
<Period id="0" start="PT0.0S">
<AdaptationSet contentType="video">
<Representation id="v0" mimeType="video/mp4" codecs="avc1.4d400d" bandwidth="58141" width="320" height="240">
<SegmentTemplate timescale="90000" initialization="init-stream$RepresentationID$.m4s" media="chunk-stream$RepresentationID$-$Number%05d$.m4s" startNumber="1">
<SegmentTimeline>
<S t="2070" d="90000" r="2" />
</SegmentTimeline>
</SegmentTemplate>
</Representation>
</AdaptationSet>
</Period>
</MPD>"#;
#[test]
fn parses_small_mpd_structure() {
let mpd = Mpd::parse(SMALL_MPD).expect("parse");
assert_eq!(mpd.profiles, "urn:mpeg:dash:profile:isoff-live:2011");
assert_eq!(mpd.mpd_type, MpdType::Static);
assert_eq!(mpd.media_presentation_duration, Some(Duration::new(3, 0)));
assert_eq!(mpd.periods.len(), 1);
let period = &mpd.periods[0];
assert_eq!(period.id.as_deref(), Some("0"));
assert_eq!(period.start, Some(Duration::new(0, 0)));
assert_eq!(period.adaptation_sets.len(), 1);
let set = &period.adaptation_sets[0];
assert_eq!(set.content_type.as_deref(), Some("video"));
assert_eq!(set.representations.len(), 1);
let repr = &set.representations[0];
assert_eq!(repr.id, "v0");
assert_eq!(repr.bandwidth, 58141);
assert_eq!(repr.codecs.as_deref(), Some("avc1.4d400d"));
assert_eq!(repr.width, Some(320));
assert_eq!(repr.height, Some(240));
let st = repr.segment_template.as_ref().expect("segment template");
assert_eq!(st.timescale, 90000);
assert_eq!(
st.initialization.as_deref(),
Some("init-stream$RepresentationID$.m4s")
);
assert_eq!(
st.media.as_deref(),
Some("chunk-stream$RepresentationID$-$Number%05d$.m4s")
);
assert_eq!(st.start_number, 1);
let timeline = st.timeline.as_ref().expect("timeline");
assert_eq!(
timeline.segments,
vec![S {
t: Some(2070),
d: 90000,
r: 2
}]
);
}
#[test]
fn dynamic_type_and_defaults() {
let xml = r#"<MPD profiles="p" type="dynamic"><Period><AdaptationSet><Representation id="0" bandwidth="1"/></AdaptationSet></Period></MPD>"#;
let mpd = Mpd::parse(xml).expect("parse");
assert_eq!(mpd.mpd_type, MpdType::Dynamic);
assert_eq!(mpd.media_presentation_duration, None);
assert_eq!(mpd.periods[0].adaptation_sets[0].representations[0].id, "0");
assert_eq!(
mpd.periods[0].adaptation_sets[0].representations[0].bandwidth,
1
);
}
#[test]
fn missing_type_defaults_to_static() {
let xml = r#"<MPD profiles="p"><Period/></MPD>"#;
let mpd = Mpd::parse(xml).expect("parse");
assert_eq!(mpd.mpd_type, MpdType::Static);
assert_eq!(mpd.periods.len(), 1);
assert!(mpd.periods[0].adaptation_sets.is_empty());
}
#[test]
fn segment_template_inherited_from_adaptation_set() {
let xml = r#"<MPD profiles="p">
<Period>
<AdaptationSet contentType="video">
<SegmentTemplate timescale="1000" media="chunk-$Number$.m4s" startNumber="1"/>
<Representation id="0" bandwidth="1"/>
<Representation id="1" bandwidth="2">
<SegmentTemplate timescale="2000" media="own-$Number$.m4s" startNumber="5"/>
</Representation>
</AdaptationSet>
</Period>
</MPD>"#;
let mpd = Mpd::parse(xml).expect("parse");
let set = &mpd.periods[0].adaptation_sets[0];
assert_eq!(
set.segment_template.as_ref().unwrap().timescale,
1000,
"AdaptationSet-level template retained"
);
let r0 = &set.representations[0];
let r0_st = r0.segment_template.as_ref().expect("inherited template");
assert_eq!(r0_st.timescale, 1000, "inherited from AdaptationSet");
assert_eq!(r0_st.media.as_deref(), Some("chunk-$Number$.m4s"));
let r1 = &set.representations[1];
let r1_st = r1.segment_template.as_ref().expect("own template");
assert_eq!(r1_st.timescale, 2000, "own template wins over inherited");
assert_eq!(r1_st.start_number, 5);
}
#[test]
fn representation_without_any_template_is_none() {
let xml = r#"<MPD profiles="p"><Period><AdaptationSet><Representation id="0" bandwidth="1"/></AdaptationSet></Period></MPD>"#;
let mpd = Mpd::parse(xml).expect("parse");
assert!(
mpd.periods[0].adaptation_sets[0].representations[0]
.segment_template
.is_none()
);
}
#[test]
fn tolerates_unknown_elements() {
let xml = r#"<MPD profiles="p">
<ProgramInformation></ProgramInformation>
<ServiceDescription id="0"></ServiceDescription>
<Period>
<AdaptationSet>
<Role schemeIdUri="urn:mpeg:dash:role:2011" value="main"/>
<Representation id="0" bandwidth="1">
<AudioChannelConfiguration schemeIdUri="x" value="2"/>
</Representation>
</AdaptationSet>
</Period>
</MPD>"#;
let mpd = Mpd::parse(xml).expect("parse should tolerate unmodeled elements");
assert_eq!(mpd.periods.len(), 1);
assert_eq!(mpd.periods[0].adaptation_sets[0].representations[0].id, "0");
}
#[test]
fn entity_unescape_in_attribute_values() {
let xml = r#"<MPD profiles="a & b <x>"><Period/></MPD>"#;
let mpd = Mpd::parse(xml).expect("parse");
assert_eq!(mpd.profiles, "a & b <x>");
}
#[test]
fn unterminated_tag_is_error_not_panic() {
let err = Mpd::parse("<MPD profiles=\"p\"").unwrap_err();
assert!(matches!(
err,
DashParseError::UnterminatedTag { .. } | DashParseError::UnexpectedEof
));
}
#[test]
fn unclosed_attribute_quote_is_error_not_panic() {
let err = Mpd::parse(r#"<MPD profiles="p type="static"><Period/></MPD>"#).unwrap_err();
let _ = err;
}
#[test]
fn truncated_after_declaration_is_error() {
let err = Mpd::parse("<?xml version=\"1.0\"?>").unwrap_err();
assert_eq!(err, DashParseError::UnexpectedEof);
}
#[test]
fn wrong_root_element_is_error() {
let err = Mpd::parse("<NotAnMpd/>").unwrap_err();
assert!(matches!(err, DashParseError::UnexpectedElement { .. }));
}
#[test]
fn missing_required_attribute_is_error() {
let xml = r#"<MPD profiles="p"><Period><AdaptationSet><Representation id="0"/></AdaptationSet></Period></MPD>"#;
let err = Mpd::parse(xml).unwrap_err();
assert!(matches!(
err,
DashParseError::MissingAttribute {
element: "Representation",
attr: "bandwidth"
}
));
}
#[test]
fn empty_input_is_error() {
let err = Mpd::parse("").unwrap_err();
assert_eq!(err, DashParseError::UnexpectedEof);
}
#[test]
fn iso8601_duration_hours_minutes_fractional_seconds() {
assert_eq!(
parse_iso8601_duration("PT1H2M3.5S").unwrap(),
Duration::new(3723, 500_000_000)
);
}
#[test]
fn iso8601_duration_seconds_only() {
assert_eq!(parse_iso8601_duration("PT4S").unwrap(), Duration::new(4, 0));
}
#[test]
fn iso8601_duration_zero() {
assert_eq!(parse_iso8601_duration("PT0S").unwrap(), Duration::new(0, 0));
}
#[test]
fn iso8601_duration_days_and_hours() {
assert_eq!(
parse_iso8601_duration("P1DT2H").unwrap(),
Duration::new(SECONDS_PER_DAY + 2 * SECONDS_PER_HOUR, 0)
);
}
#[test]
fn iso8601_duration_writer_tenths_form() {
assert_eq!(
parse_iso8601_duration("PT2.0S").unwrap(),
Duration::new(2, 0)
);
}
#[test]
fn iso8601_duration_rejects_bare_p() {
assert!(parse_iso8601_duration("P").is_err());
}
#[test]
fn iso8601_duration_rejects_missing_prefix() {
assert!(parse_iso8601_duration("1H2M3S").is_err());
}
#[test]
fn iso8601_duration_rejects_calendar_months() {
assert!(parse_iso8601_duration("P1M").is_err());
}
#[test]
fn resolve_representation_id() {
assert_eq!(
SegmentTemplate::resolve("init-$RepresentationID$.m4s", "7", None, None, None),
"init-7.m4s"
);
}
#[test]
fn resolve_number_with_width() {
assert_eq!(
SegmentTemplate::resolve(
"chunk-$RepresentationID$-$Number%05d$.m4s",
"0",
Some(42),
None,
None
),
"chunk-0-00042.m4s"
);
}
#[test]
fn resolve_number_without_width() {
assert_eq!(
SegmentTemplate::resolve("chunk-$Number$.m4s", "0", Some(42), None, None),
"chunk-42.m4s"
);
}
#[test]
fn resolve_time() {
assert_eq!(
SegmentTemplate::resolve("chunk-$Time$.m4s", "0", None, Some(2070), None),
"chunk-2070.m4s"
);
}
#[test]
fn resolve_bandwidth_with_width() {
assert_eq!(
SegmentTemplate::resolve("seg-$Bandwidth%08d$.m4s", "0", None, None, Some(58141)),
"seg-00058141.m4s"
);
}
#[test]
fn resolve_dollar_escape() {
assert_eq!(
SegmentTemplate::resolve("literal-$$-$Number$", "0", Some(1), None, None),
"literal-$-1"
);
}
#[test]
fn resolve_missing_value_emitted_literally() {
assert_eq!(
SegmentTemplate::resolve("chunk-$Time$.m4s", "0", Some(1), None, None),
"chunk-$Time$.m4s"
);
}
#[test]
fn resolve_unknown_identifier_passthrough() {
assert_eq!(
SegmentTemplate::resolve("$Unknown$-x", "0", None, None, None),
"$Unknown$-x"
);
}
#[test]
fn number_sequence_from_start_number() {
let st = SegmentTemplate {
timescale: 1,
initialization: None,
media: None,
start_number: 5,
duration: Some(1000),
presentation_time_offset: 0,
timeline: None,
};
assert_eq!(st.number_sequence(3), vec![5, 6, 7]);
}
#[test]
fn enumerate_expands_repeats() {
let timeline = SegmentTimeline {
segments: vec![S {
t: Some(2070),
d: 90000,
r: 2,
}],
};
assert_eq!(
timeline.enumerate(1).expect("enumerate"),
vec![(1, 2070), (2, 92070), (3, 182070)]
);
}
#[test]
fn enumerate_multiple_s_entries_accumulate_time() {
let timeline = SegmentTimeline {
segments: vec![
S {
t: Some(0),
d: 41984,
r: 0,
},
S {
t: None,
d: 44032,
r: 0,
},
S {
t: None,
d: 45056,
r: 0,
},
S {
t: None,
d: 3072,
r: 0,
},
],
};
assert_eq!(
timeline.enumerate(1).expect("enumerate"),
vec![(1, 0), (2, 41984), (3, 86016), (4, 131072)]
);
}
#[test]
fn enumerate_negative_r_tolerated_as_single_segment() {
let timeline = SegmentTimeline {
segments: vec![S {
t: Some(0),
d: 1000,
r: -1,
}],
};
assert_eq!(timeline.enumerate(1).expect("enumerate"), vec![(1, 0)]);
}
#[test]
fn segment_template_resolve_clamps_format_width_instantly() {
let resolved =
SegmentTemplate::resolve("chunk-$Number%9999999999d$.m4s", "r0", Some(42), None, None);
assert!(
resolved.len() < 100,
"resolved string must be small (clamped width): {resolved}"
);
assert!(
resolved.contains("42"),
"number must appear in resolved string: {resolved}"
);
assert_eq!(
resolved, "chunk-00000000000000000042.m4s",
"exact padding to 20 digits (MAX_FORMAT_WIDTH)"
);
}
}