use alloc::string::{String, ToString};
use alloc::vec::Vec;
use core::fmt;
use core::str::FromStr;
use core::time::Duration;
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 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,
},
TimelineNotIncreasing {
t: u64,
next_t: u64,
},
MismatchedEndTag {
expected: String,
found: String,
},
Xml {
pos: usize,
message: 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::TimelineNotIncreasing { t, next_t } => write!(
f,
"SegmentTimeline <S t={t} r=\"-1\"> is followed by an <S> starting at {next_t}, \
which is not later"
),
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}>")
}
}
DashParseError::Xml { pos, message } => {
write!(f, "XML error at byte offset {pos}: {message}")
}
}
}
}
#[cfg(feature = "std")]
impl std::error::Error for DashParseError {}
type Result<T> = core::result::Result<T, DashParseError>;
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) -> DashParseError {
use quick_xml::Error as E;
let pos = usize::try_from(reader.buffer_position()).unwrap_or(usize::MAX);
match err {
E::Syntax(_) => DashParseError::UnterminatedTag { pos },
E::IllFormed(IllFormedError::MismatchedEndTag { expected, found }) => {
DashParseError::MismatchedEndTag {
expected: local_part(expected),
found: local_part(found),
}
}
_ => DashParseError::Xml {
pos,
message: err.to_string(),
},
}
}
fn xml_message(reader: &XmlReader<'_>, message: String) -> DashParseError {
DashParseError::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(|_| DashParseError::MalformedAttribute { pos })?;
let value = attr
.normalized_value(XmlVersion::Implicit1_0)
.map_err(|_| DashParseError::MalformedAttribute { pos })?;
if !value.chars().all(is_xml_char) {
return Err(DashParseError::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(DashParseError::UnexpectedEof),
}
}
Ok(())
}
fn text_content(
reader: &mut XmlReader<'_>,
name: &'static str,
self_closing: bool,
) -> Result<Option<String>> {
if self_closing {
return Ok(None);
}
let mut text = String::new();
let mut nested = false;
loop {
let event = read_event(reader)?;
if let Some(piece) = char_data(&event).map_err(|message| xml_message(reader, message))? {
text.push_str(&piece);
continue;
}
match event {
Event::Start(_) => {
nested = true;
skip_element(reader)?;
}
Event::Empty(_) => nested = true,
Event::End(e) => {
let found = e.local_name().into_inner();
if found != name {
return Err(DashParseError::MismatchedEndTag {
expected: name.to_string(),
found: found.to_string(),
});
}
return Ok(if nested { None } else { Some(text) });
}
Event::Eof => return Err(DashParseError::UnexpectedEof),
_ => {}
}
}
}
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 base_url: Option<String>,
pub periods: Vec<Period>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct Period {
pub id: Option<String>,
pub start: Option<Duration>,
pub duration: Option<Duration>,
pub base_url: Option<String>,
pub adaptation_sets: Vec<AdaptationSet>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct AdaptationSet {
pub mime_type: Option<String>,
pub content_type: Option<String>,
pub base_url: 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 base_url: 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 (tokens, _) = tokenize_template(template);
let mut out = String::with_capacity(template.len());
for token in tokens {
match token {
TemplateToken::Literal(s) => out.push_str(&s),
TemplateToken::Escape => out.push('$'),
TemplateToken::Identifier { kind, width, raw } => match kind {
TemplateIdentifier::RepresentationId => out.push_str(representation_id),
TemplateIdentifier::Number => push_numeric(&mut out, number, width, &raw),
TemplateIdentifier::Time => push_numeric(&mut out, time, width, &raw),
TemplateIdentifier::Bandwidth => push_numeric(&mut out, bandwidth, width, &raw),
},
}
}
out
}
pub fn match_url(
template: &str,
candidate: &str,
known_representation_ids: &[&str],
) -> Option<TemplateMatch> {
let (tokens, valid) = tokenize_template(template);
if !valid {
return None;
}
let mut ids: Vec<&str> = known_representation_ids.to_vec();
ids.sort_by(|a, b| b.len().cmp(&a.len()).then_with(|| a.cmp(b)));
ids.dedup();
let initial = TemplateMatch {
representation_id: String::new(),
number: None,
time: None,
bandwidth: None,
};
let mut solutions: Vec<TemplateMatch> = Vec::new();
match_template_tokens(&tokens, &ids, candidate, 0, 0, initial, &mut solutions);
if solutions.len() == 1 {
solutions.pop()
} else {
None
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct TemplateMatch {
pub representation_id: String,
pub number: Option<u64>,
pub time: Option<u64>,
pub bandwidth: Option<u64>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum TemplateIdentifier {
RepresentationId,
Number,
Time,
Bandwidth,
}
#[derive(Debug, Clone)]
enum TemplateToken {
Literal(String),
Escape,
Identifier {
kind: TemplateIdentifier,
width: Option<usize>,
raw: String,
},
}
fn tokenize_template(template: &str) -> (Vec<TemplateToken>, bool) {
let mut tokens: Vec<TemplateToken> = Vec::new();
let mut rest = template;
let mut valid = true;
while let Some(dollar) = rest.find('$') {
if dollar > 0 {
tokens.push(TemplateToken::Literal(rest[..dollar].to_string()));
}
let tail = &rest[dollar + 1..];
if let Some(after_escape) = tail.strip_prefix('$') {
tokens.push(TemplateToken::Escape);
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" => tokens.push(TemplateToken::Identifier {
kind: TemplateIdentifier::RepresentationId,
width,
raw: ident.to_string(),
}),
"Number" => tokens.push(TemplateToken::Identifier {
kind: TemplateIdentifier::Number,
width,
raw: ident.to_string(),
}),
"Time" => tokens.push(TemplateToken::Identifier {
kind: TemplateIdentifier::Time,
width,
raw: ident.to_string(),
}),
"Bandwidth" => tokens.push(TemplateToken::Identifier {
kind: TemplateIdentifier::Bandwidth,
width,
raw: ident.to_string(),
}),
_ => tokens.push(TemplateToken::Literal(format!("${}$", ident))),
}
rest = &tail[end + 1..];
}
None => {
tokens.push(TemplateToken::Literal("$".to_string()));
valid = false;
rest = tail;
}
}
}
if !rest.is_empty() {
tokens.push(TemplateToken::Literal(rest.to_string()));
}
(tokens, valid)
}
#[allow(clippy::too_many_arguments)]
fn match_template_tokens(
tokens: &[TemplateToken],
ids: &[&str],
candidate: &str,
ti: usize,
pos: usize,
current: TemplateMatch,
solutions: &mut Vec<TemplateMatch>,
) {
if solutions.len() >= 2 {
return;
}
if ti == tokens.len() {
if pos == candidate.len() {
solutions.push(current);
}
return;
}
match &tokens[ti] {
TemplateToken::Literal(s) => {
if candidate[pos..].starts_with(s.as_str()) {
match_template_tokens(
tokens,
ids,
candidate,
ti + 1,
pos + s.len(),
current,
solutions,
);
}
}
TemplateToken::Escape => {
if candidate[pos..].starts_with('$') {
match_template_tokens(tokens, ids, candidate, ti + 1, pos + 1, current, solutions);
}
}
TemplateToken::Identifier {
kind,
width,
raw: _,
} => match kind {
TemplateIdentifier::RepresentationId => {
for id in ids {
if candidate[pos..].starts_with(id)
&& (current.representation_id.is_empty()
|| current.representation_id == *id)
{
let mut next = current.clone();
if next.representation_id.is_empty() {
next.representation_id = (*id).to_string();
}
match_template_tokens(
tokens,
ids,
candidate,
ti + 1,
pos + id.len(),
next,
solutions,
);
if solutions.len() >= 2 {
return;
}
}
}
}
TemplateIdentifier::Number => {
try_numeric_token(
tokens,
ids,
candidate,
ti,
pos,
width,
current,
solutions,
|tm, v| {
if tm.number.is_none() || tm.number == Some(v) {
let mut n = tm.clone();
if n.number.is_none() {
n.number = Some(v);
}
Some(n)
} else {
None
}
},
);
}
TemplateIdentifier::Time => {
try_numeric_token(
tokens,
ids,
candidate,
ti,
pos,
width,
current,
solutions,
|tm, v| {
if tm.time.is_none() || tm.time == Some(v) {
let mut n = tm.clone();
if n.time.is_none() {
n.time = Some(v);
}
Some(n)
} else {
None
}
},
);
}
TemplateIdentifier::Bandwidth => {
try_numeric_token(
tokens,
ids,
candidate,
ti,
pos,
width,
current,
solutions,
|tm, v| {
if tm.bandwidth.is_none() || tm.bandwidth == Some(v) {
let mut n = tm.clone();
if n.bandwidth.is_none() {
n.bandwidth = Some(v);
}
Some(n)
} else {
None
}
},
);
}
},
}
}
#[allow(clippy::too_many_arguments)]
fn try_numeric_token<F>(
tokens: &[TemplateToken],
ids: &[&str],
candidate: &str,
ti: usize,
pos: usize,
width: &Option<usize>,
current: TemplateMatch,
solutions: &mut Vec<TemplateMatch>,
apply: F,
) where
F: Fn(&TemplateMatch, u64) -> Option<TemplateMatch>,
{
let rest = &candidate[pos..];
let digit_end = rest
.find(|c: char| !c.is_ascii_digit())
.unwrap_or(rest.len());
let max_len = digit_end;
let w = width.unwrap_or(1).clamp(1, MAX_FORMAT_WIDTH);
let min_len = w.min(max_len);
let min_len = min_len.max(1).min(max_len);
if max_len == 0 {
return;
}
for l in (min_len..=max_len).rev() {
let sub = &candidate[pos..pos + l];
let val: u64 = match sub.parse() {
Ok(v) => v,
Err(_) => continue,
};
let rendered = format_width(val, width.unwrap_or(1));
if rendered != sub {
continue;
}
if let Some(next) = apply(¤t, val) {
match_template_tokens(tokens, ids, candidate, ti + 1, pos + l, next, solutions);
if solutions.len() >= 2 {
return;
}
}
}
}
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)>> {
Ok(self
.resolve(start_number, None)?
.into_iter()
.map(|seg| (seg.number, seg.time))
.collect())
}
pub fn resolve_in_period(
&self,
start_number: u64,
presentation_time_offset: u64,
period_duration_ticks: Option<u64>,
) -> Result<Vec<TimelineSegment>> {
self.resolve(
start_number,
period_duration_ticks.map(|d| d.saturating_add(presentation_time_offset)),
)
}
pub fn resolve(
&self,
start_number: u64,
period_end_ticks: Option<u64>,
) -> Result<Vec<TimelineSegment>> {
let mut out = Vec::new();
let mut number = start_number;
let mut time: u64 = 0;
let mut total_segments: u64 = 0;
for (idx, s) in self.segments.iter().enumerate() {
if let Some(t) = s.t {
time = t;
}
let repeats: u64 = if s.r >= 0 {
(s.r as u64).saturating_add(1)
} else {
let next_t = self.segments.get(idx + 1).and_then(|n| n.t);
if let Some(next_t) = next_t
&& next_t <= time
{
return Err(DashParseError::TimelineNotIncreasing { t: time, next_t });
}
match (next_t.or(period_end_ticks), s.d) {
(Some(end), d) if d > 0 => end.saturating_sub(time).div_ceil(d),
_ => 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(TimelineSegment {
number,
time,
duration: s.d,
});
number = number.saturating_add(1);
time = time.saturating_add(s.d);
}
}
Ok(out)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TimelineSegment {
pub number: u64,
pub time: u64,
pub duration: u64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct S {
pub t: Option<u64>,
pub d: u64,
pub r: i64,
}
#[cfg(test)]
const SECONDS_PER_DAY: u64 = 24 * 60 * 60;
#[cfg(test)]
const SECONDS_PER_HOUR: u64 = 60 * 60;
pub fn parse_iso8601_duration(s: &str) -> Result<Duration> {
let trimmed = s.trim();
let invalid = || DashParseError::InvalidDuration {
value: trimmed.to_string(),
};
let normalised = normalise_xs_duration(trimmed).ok_or_else(invalid)?;
let span = jiff::fmt::temporal::SpanParser::new()
.parse_span(normalised.as_str())
.map_err(|_| invalid())?;
let signed = span
.to_duration(jiff::SpanRelativeTo::days_are_24_hours())
.map_err(|_| invalid())?;
Duration::try_from(signed).map_err(|_| invalid())
}
fn normalise_xs_duration(s: &str) -> Option<String> {
let rest = s.strip_prefix('P')?;
let (date_part, time_part) = match rest.split_once('T') {
Some((d, t)) => (d, Some(t)),
None => (rest, None),
};
let mut components = 0usize;
let mut out = String::from("P");
let mut remaining = date_part;
for designator in ['Y', 'M', 'D'] {
if let Some((digits, tail)) = remaining.split_once(designator) {
if digits.is_empty() || !digits.bytes().all(|b| b.is_ascii_digit()) {
return None;
}
out.push_str(digits);
out.push(designator);
remaining = tail;
components += 1;
}
}
if !remaining.is_empty() {
return None;
}
if let Some(time) = time_part {
out.push('T');
let mut remaining = time;
let mut time_components = 0usize;
for designator in ['H', 'M'] {
if let Some((digits, tail)) = remaining.split_once(designator) {
if digits.is_empty() || !digits.bytes().all(|b| b.is_ascii_digit()) {
return None;
}
out.push_str(digits);
out.push(designator);
remaining = tail;
time_components += 1;
}
}
if let Some((number, tail)) = remaining.split_once('S') {
if !tail.is_empty() {
return None;
}
let (whole, frac) = match number.split_once('.') {
Some((w, f)) => (w, Some(f)),
None => (number, None),
};
if whole.is_empty() || !whole.bytes().all(|b| b.is_ascii_digit()) {
return None;
}
out.push_str(whole);
if let Some(frac) = frac {
if frac.is_empty() || !frac.bytes().all(|b| b.is_ascii_digit()) {
return None;
}
out.push('.');
out.push_str(&frac[..frac.len().min(9)]);
}
out.push('S');
time_components += 1;
} else if !remaining.is_empty() {
return None;
}
if time_components == 0 {
return None;
}
components += time_components;
}
(components > 0).then_some(out)
}
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 base_url_chain(
&self,
period: &Period,
adaptation_set: &AdaptationSet,
representation: &Representation,
) -> Vec<String> {
[
&self.base_url,
&period.base_url,
&adaptation_set.base_url,
&representation.base_url,
]
.into_iter()
.flatten()
.filter(|value| !value.is_empty())
.cloned()
.collect()
}
pub fn resolve_segment_url(
&self,
mpd_url: Option<&url::Url>,
period: &Period,
adaptation_set: &AdaptationSet,
representation: &Representation,
reference: &str,
) -> Option<String> {
crate::base_url::resolve_chain(
mpd_url,
&self.base_url_chain(period, adaptation_set, representation),
reference,
)
}
pub fn parse(xml: &str) -> Result<Mpd> {
const EL: &str = "MPD";
let mut xml_reader = new_reader(xml);
let reader = &mut xml_reader;
let (mpd_attrs, mpd_self_closing) = match next_tag(reader)? {
Some(Tag::Open {
name,
attrs,
self_closing,
}) if name == "MPD" => (attrs, self_closing),
Some(Tag::Open { name, .. }) => {
return Err(DashParseError::UnexpectedElement {
expected: EL,
found: name.to_string(),
});
}
Some(Tag::Close { .. }) => {
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 base_url = None;
let mut periods = Vec::new();
if !mpd_self_closing {
loop {
match next_tag(reader)? {
Some(Tag::Open {
name,
attrs,
self_closing,
}) if name == "BaseURL" => {
let found = parse_base_url(reader, &attrs, self_closing)?;
keep_first_base_url(&mut base_url, found);
}
Some(Tag::Open {
name,
attrs,
self_closing,
}) if name == "Period" => {
periods.push(parse_period(reader, attrs, self_closing)?)
}
Some(Tag::Open { self_closing, .. }) => {
if !self_closing {
skip_element(reader)?;
}
}
Some(Tag::Close { name }) => {
if name != EL {
return Err(DashParseError::MismatchedEndTag {
expected: EL.to_string(),
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,
base_url,
periods,
})
}
}
#[derive(Debug, Clone)]
struct TemplateLayer {
attrs: LayerTemplate,
timeline: Option<SegmentTimeline>,
}
fn parse_period<'a>(
reader: &mut XmlReader<'a>,
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 base_url = None;
let mut template: Option<TemplateLayer> = None;
let mut adaptation_sets = Vec::new();
if !self_closing {
loop {
match next_tag(reader)? {
Some(Tag::Open {
name,
attrs,
self_closing,
}) if name == "BaseURL" => {
let found = parse_base_url(reader, &attrs, self_closing)?;
keep_first_base_url(&mut base_url, found);
}
Some(Tag::Open {
name,
attrs,
self_closing,
}) if name == "SegmentTemplate" => {
template = Some(parse_segment_template_layer(reader, attrs, self_closing)?)
}
Some(Tag::Open {
name,
attrs,
self_closing,
}) if name == "AdaptationSet" => {
let parent = template
.as_ref()
.map(|t| merge_templates(None, t.attrs.clone(), t.timeline.clone()));
adaptation_sets.push(parse_adaptation_set(
reader,
attrs,
self_closing,
parent.as_ref(),
)?);
}
Some(Tag::Open { self_closing, .. }) => {
if !self_closing {
skip_element(reader)?;
}
}
Some(Tag::Close { name }) => {
if name != EL {
return Err(DashParseError::MismatchedEndTag {
expected: EL.to_string(),
found: name.to_string(),
});
}
break;
}
None => return Err(DashParseError::UnexpectedEof),
}
}
}
Ok(Period {
id,
start,
duration,
base_url,
adaptation_sets,
})
}
fn parse_base_url<'a>(
reader: &mut XmlReader<'a>,
attrs: &[(String, String)],
self_closing: bool,
) -> Result<Option<String>> {
let _ = attrs;
let text = text_content(reader, "BaseURL", self_closing)?;
Ok(text.map(|t| t.trim().to_string()).filter(|t| !t.is_empty()))
}
fn keep_first_base_url(slot: &mut Option<String>, found: Option<String>) {
if slot.is_none() {
*slot = found;
}
}
fn parse_adaptation_set<'a>(
reader: &mut XmlReader<'a>,
attrs: Vec<(String, String)>,
self_closing: bool,
parent: Option<&SegmentTemplate>,
) -> Result<AdaptationSet> {
const EL: &str = "AdaptationSet";
let mime_type = attr_owned(&attrs, "mimeType");
let content_type = attr_owned(&attrs, "contentType");
let mut base_url = None;
let mut own_layer: Option<TemplateLayer> = None;
let mut parsed: Vec<(Representation, Option<TemplateLayer>)> = Vec::new();
if !self_closing {
loop {
match next_tag(reader)? {
Some(Tag::Open {
name,
attrs,
self_closing,
}) if name == "BaseURL" => {
let found = parse_base_url(reader, &attrs, self_closing)?;
keep_first_base_url(&mut base_url, found);
}
Some(Tag::Open {
name,
attrs,
self_closing,
}) if name == "SegmentTemplate" => {
own_layer = Some(parse_segment_template_layer(reader, attrs, self_closing)?)
}
Some(Tag::Open {
name,
attrs,
self_closing,
}) if name == "Representation" => {
parsed.push(parse_representation(reader, attrs, self_closing)?)
}
Some(Tag::Open { self_closing, .. }) => {
if !self_closing {
skip_element(reader)?;
}
}
Some(Tag::Close { name }) => {
if name != EL {
return Err(DashParseError::MismatchedEndTag {
expected: EL.to_string(),
found: name.to_string(),
});
}
break;
}
None => return Err(DashParseError::UnexpectedEof),
}
}
}
let effective = match own_layer {
Some(layer) => Some(merge_templates(parent, layer.attrs, layer.timeline)),
None => parent.cloned(),
};
let mut representations = Vec::with_capacity(parsed.len());
for (mut repr, layer) in parsed {
repr.segment_template = match (effective.as_ref(), layer) {
(Some(parent), Some(child)) => {
Some(merge_templates(Some(parent), child.attrs, child.timeline))
}
(Some(parent), None) => Some(parent.clone()),
(None, Some(child)) => Some(to_template(child)),
(None, None) => None,
};
representations.push(repr);
}
let segment_template = effective;
Ok(AdaptationSet {
mime_type,
content_type,
base_url,
segment_template,
representations,
})
}
fn parse_representation<'a>(
reader: &mut XmlReader<'a>,
attrs: Vec<(String, String)>,
self_closing: bool,
) -> Result<(Representation, Option<TemplateLayer>)> {
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 base_url = None;
let mut pending: Option<TemplateLayer> = None;
if !self_closing {
loop {
match next_tag(reader)? {
Some(Tag::Open {
name,
attrs,
self_closing,
}) if name == "BaseURL" => {
let found = parse_base_url(reader, &attrs, self_closing)?;
keep_first_base_url(&mut base_url, found);
}
Some(Tag::Open {
name,
attrs,
self_closing,
}) if name == "SegmentTemplate" => {
pending = Some(parse_segment_template_layer(reader, attrs, self_closing)?)
}
Some(Tag::Open { self_closing, .. }) => {
if !self_closing {
skip_element(reader)?;
}
}
Some(Tag::Close { name }) => {
if name != EL {
return Err(DashParseError::MismatchedEndTag {
expected: EL.to_string(),
found: name.to_string(),
});
}
break;
}
None => return Err(DashParseError::UnexpectedEof),
}
}
}
Ok((
Representation {
id,
bandwidth,
codecs,
width,
height,
frame_rate,
audio_sampling_rate,
mime_type,
base_url,
segment_template: None,
},
pending,
))
}
fn to_template(layer: TemplateLayer) -> SegmentTemplate {
merge_templates(None, layer.attrs, layer.timeline)
}
fn split_template_attrs(attrs: &[(String, String)]) -> Result<LayerTemplate> {
const EL: &str = "SegmentTemplate";
Ok(LayerTemplate {
timescale: parse_attr(attrs, "timescale", EL)?,
initialization: attr_owned(attrs, "initialization"),
media: attr_owned(attrs, "media"),
start_number: parse_attr(attrs, "startNumber", EL)?,
duration: parse_attr(attrs, "duration", EL)?,
presentation_time_offset: parse_attr(attrs, "presentationTimeOffset", EL)?,
})
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
struct LayerTemplate {
timescale: Option<u64>,
initialization: Option<String>,
media: Option<String>,
start_number: Option<u64>,
duration: Option<u64>,
presentation_time_offset: Option<u64>,
}
fn merge_templates(
parent: Option<&SegmentTemplate>,
own: LayerTemplate,
own_timeline: Option<SegmentTimeline>,
) -> SegmentTemplate {
let inherit_u64 = |own: Option<u64>, get: fn(&SegmentTemplate) -> u64, default: u64| {
own.or_else(|| parent.map(get)).unwrap_or(default)
};
let own_has_timeline = own_timeline.is_some();
let timeline = if own.duration.is_some() {
own_timeline
} else {
own_timeline.or_else(|| parent.and_then(|p| p.timeline.clone()))
};
let duration = if own_has_timeline {
None
} else {
own.duration.or_else(|| parent.and_then(|p| p.duration))
};
SegmentTemplate {
timescale: inherit_u64(own.timescale, |p| p.timescale, DEFAULT_TIMESCALE),
initialization: own
.initialization
.or_else(|| parent.and_then(|p| p.initialization.clone())),
media: own.media.or_else(|| parent.and_then(|p| p.media.clone())),
start_number: inherit_u64(own.start_number, |p| p.start_number, DEFAULT_START_NUMBER),
duration,
presentation_time_offset: inherit_u64(
own.presentation_time_offset,
|p| p.presentation_time_offset,
DEFAULT_PRESENTATION_TIME_OFFSET,
),
timeline,
}
}
fn parse_segment_template_layer(
reader: &mut XmlReader<'_>,
attrs: Vec<(String, String)>,
self_closing: bool,
) -> Result<TemplateLayer> {
let attrs = split_template_attrs(&attrs)?;
let timeline = parse_segment_template_body(reader, self_closing)?;
Ok(TemplateLayer { attrs, timeline })
}
fn parse_segment_template_body(
reader: &mut XmlReader<'_>,
self_closing: bool,
) -> Result<Option<SegmentTimeline>> {
const EL: &str = "SegmentTemplate";
let mut timeline = None;
if !self_closing {
loop {
match next_tag(reader)? {
Some(Tag::Open {
name, self_closing, ..
}) if name == "SegmentTimeline" => {
timeline = Some(parse_segment_timeline(reader, self_closing)?)
}
Some(Tag::Open { self_closing, .. }) => {
if !self_closing {
skip_element(reader)?;
}
}
Some(Tag::Close { name }) => {
if name != EL {
return Err(DashParseError::MismatchedEndTag {
expected: EL.to_string(),
found: name.to_string(),
});
}
break;
}
None => return Err(DashParseError::UnexpectedEof),
}
}
}
Ok(timeline)
}
fn parse_segment_timeline(
reader: &mut XmlReader<'_>,
self_closing: bool,
) -> Result<SegmentTimeline> {
const EL: &str = "SegmentTimeline";
let mut segments = Vec::new();
if !self_closing {
loop {
match next_tag(reader)? {
Some(Tag::Open {
name,
attrs,
self_closing,
}) if name == "S" => {
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(reader)?;
}
}
Some(Tag::Open { self_closing, .. }) => {
if !self_closing {
skip_element(reader)?;
}
}
Some(Tag::Close { name }) => {
if name != EL {
return Err(DashParseError::MismatchedEndTag {
expected: EL.to_string(),
found: name.to_string(),
});
}
break;
}
None => return Err(DashParseError::UnexpectedEof),
}
}
}
Ok(SegmentTimeline { segments })
}
#[cfg(test)]
mod tests {
use super::*;
use alloc::vec;
const SPECIALS: &str = "a&b<c>d\"e'f";
fn first_open_attrs(xml: &str) -> Vec<(String, String)> {
let mut reader = new_reader(xml);
loop {
match next_tag(&mut reader).expect("tokenize") {
Some(Tag::Open { attrs, .. }) => return attrs,
Some(_) => continue,
None => panic!("no start tag"),
}
}
}
#[test]
fn greater_than_inside_attribute_value_is_not_a_tag_end() {
let attrs = first_open_attrs(r#"<SegmentTemplate media="a?x=1&y=2>3" id="v"/>"#);
assert_eq!(
attrs,
vec![
("media".to_string(), "a?x=1&y=2>3".to_string()),
("id".to_string(), "v".to_string()),
]
);
let attrs = first_open_attrs("<X a='>' b='z'/>");
assert_eq!(attrs[0].1, ">");
}
#[test]
fn character_and_named_references_in_attribute_values() {
let attrs = first_open_attrs(r#"<X media="a?x=1&n=$Number$" hex="&"/>"#);
assert_eq!(attrs[0].1, "a?x=1&n=$Number$");
assert_eq!(attrs[1].1, "&");
let attrs = first_open_attrs(r#"<X a="<>&"'"/>"#);
assert_eq!(attrs[0].1, "<>&\"'");
}
#[test]
fn undefined_entity_in_attribute_is_a_structured_error() {
let mut reader = new_reader(r#"<X b="&nope;"/>"#);
assert!(matches!(
next_tag(&mut reader),
Err(DashParseError::MalformedAttribute { .. })
));
}
#[test]
fn unterminated_constructs_are_errors_not_panics() {
for bad in [
"<X",
r#"<X a="unterminated"#,
"<X a='1'",
"<!-- never closed",
"<![CDATA[ never",
"<?pi",
] {
let mut reader = new_reader(bad);
let mut outcome = Ok(Some(()));
for _ in 0..4 {
outcome = next_tag(&mut reader).map(|t| t.map(|_| ()));
if !matches!(outcome, Ok(Some(()))) {
break;
}
}
assert!(
matches!(outcome, Err(_) | Ok(None)),
"{bad:?} must not panic"
);
}
let mut reader = new_reader("<X");
assert!(matches!(
next_tag(&mut reader),
Err(DashParseError::UnterminatedTag { .. })
));
}
#[test]
fn mismatched_end_tag_names_both_elements() {
let mut reader = new_reader("<A><B></A>");
let mut err = None;
for _ in 0..4 {
match next_tag(&mut reader) {
Ok(Some(_)) => {}
Ok(None) => break,
Err(e) => {
err = Some(e);
break;
}
}
}
assert!(matches!(
err,
Some(DashParseError::MismatchedEndTag { ref expected, ref found })
if expected == "B" && found == "A"
));
}
#[test]
fn text_content_resolves_references_and_is_linear() {
let xml = format!("<BaseURL>{}</BaseURL>", "&".repeat(20_000));
let mut reader = new_reader(&xml);
assert!(matches!(next_tag(&mut reader), Ok(Some(Tag::Open { .. }))));
let text = text_content(&mut reader, "BaseURL", false)
.unwrap()
.unwrap();
assert_eq!(text, "&".repeat(20_000));
let mut reader = new_reader("<B>aAB<<![CDATA[<&>]]></B>");
next_tag(&mut reader).unwrap();
assert_eq!(
text_content(&mut reader, "B", false).unwrap().as_deref(),
Some("aAB<<&>")
);
}
#[test]
fn bad_references_and_nested_markup_in_text() {
for bad in [
"<B>&nope;</B>",
"<B>�</B>",
"<B>�</B>",
"<B>&</B>",
] {
let mut reader = new_reader(bad);
next_tag(&mut reader).unwrap();
assert!(text_content(&mut reader, "B", false).is_err(), "{bad}");
}
let mut reader = new_reader("<B>x<c>y</c></B><Next/>");
next_tag(&mut reader).unwrap();
assert_eq!(text_content(&mut reader, "B", false).unwrap(), None);
assert!(matches!(
next_tag(&mut reader),
Ok(Some(Tag::Open { ref name, .. })) if name == "Next"
));
}
#[test]
fn control_characters_are_rejected() {
for bad in [
"<B></B>",
"<B></B>",
"<B>\u{1}</B>",
"<B>�</B>",
] {
let mut reader = new_reader(bad);
next_tag(&mut reader).unwrap();
assert!(text_content(&mut reader, "B", false).is_err(), "{bad:?}");
}
for bad in [r#"<X a=""/>"#, "<X a=\"\u{1}\"/>"] {
let mut reader = new_reader(bad);
assert!(
matches!(
next_tag(&mut reader),
Err(DashParseError::MalformedAttribute { .. })
),
"{bad:?}"
);
}
let mut reader = new_reader("<B>a	 b</B>");
next_tag(&mut reader).unwrap();
assert_eq!(
text_content(&mut reader, "B", false).unwrap().as_deref(),
Some("a\t\nb")
);
}
#[test]
fn invalid_character_data_in_skipped_content_is_rejected() {
for bad in ["&nope;", "", "", "\u{1}", "<![CDATA[\u{1}]]>"] {
let skipped =
format!(r#"<MPD profiles="p"><Foo><Deep>{bad}</Deep></Foo><Period/></MPD>"#);
assert!(Mpd::parse(&skipped).is_err(), "skipped subtree {bad:?}");
let between = format!(r#"<MPD profiles="p">{bad}<Period/></MPD>"#);
assert!(Mpd::parse(&between).is_err(), "between elements {bad:?}");
let base = format!(r#"<MPD profiles="p"><BaseURL>a<x>{bad}</x></BaseURL></MPD>"#);
assert!(Mpd::parse(&base).is_err(), "nested in BaseURL {bad:?}");
}
}
#[test]
fn specials_round_trip_through_attribute_escaping() {
let xml = format!("<X a=\"{}\"/>", quick_xml::escape::escape(SPECIALS));
assert_eq!(first_open_attrs(&xml)[0].1, SPECIALS);
}
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 segment_template_merges_attribute_by_attribute_across_three_levels() {
let xml = r#"<MPD profiles="p">
<Period>
<SegmentTemplate timescale="90000" media="p-$Number$.m4s">
<SegmentTimeline><S t="0" d="90000" r="2"/></SegmentTimeline>
</SegmentTemplate>
<AdaptationSet contentType="video">
<SegmentTemplate startNumber="7"/>
<Representation id="0" bandwidth="1">
<SegmentTemplate media="$RepresentationID$-$Time$.m4s"/>
</Representation>
<Representation id="1" bandwidth="2"/>
</AdaptationSet>
</Period>
</MPD>"#;
let mpd = Mpd::parse(xml).expect("parse");
let set = &mpd.periods[0].adaptation_sets[0];
let set_st = set.segment_template.as_ref().expect("set template");
assert_eq!(set_st.timescale, 90000, "inherited from Period");
assert_eq!(set_st.start_number, 7, "declared by the AdaptationSet");
assert_eq!(set_st.media.as_deref(), Some("p-$Number$.m4s"));
assert!(set_st.timeline.is_some(), "timeline inherited from Period");
let r0 = set.representations[0]
.segment_template
.as_ref()
.expect("effective template");
assert_eq!(r0.timescale, 90000, "inherited up the whole chain");
assert_eq!(r0.start_number, 7);
assert_eq!(
r0.media.as_deref(),
Some("$RepresentationID$-$Time$.m4s"),
"the Representation's own @media wins"
);
assert!(
r0.timeline.is_some(),
"the Representation inherits the Period's timeline"
);
let r1 = set.representations[1]
.segment_template
.as_ref()
.expect("effective template");
assert_eq!(r1.media.as_deref(), Some("p-$Number$.m4s"));
assert_eq!(r1.start_number, 7);
assert_eq!(r1.timescale, 90000);
}
#[test]
fn base_url_is_reported_per_level() {
let xml = r#"<MPD profiles="p">
<Period>
<BaseURL>
https://cdn.example.com/vod/
</BaseURL>
<AdaptationSet contentType="video">
<Representation id="0" bandwidth="1">
<BaseURL>rep/</BaseURL>
</Representation>
</AdaptationSet>
</Period>
</MPD>"#;
let mpd = Mpd::parse(xml).expect("parse");
let period = &mpd.periods[0];
assert_eq!(
period.base_url.as_deref(),
Some("https://cdn.example.com/vod/"),
"surrounding whitespace is trimmed"
);
assert!(period.adaptation_sets[0].base_url.is_none());
assert_eq!(
period.adaptation_sets[0].representations[0]
.base_url
.as_deref(),
Some("rep/")
);
}
#[test]
fn first_non_empty_base_url_wins_per_level() {
let xml = r#"<MPD profiles="p">
<BaseURL>https://mpd.example.com/</BaseURL>
<Period>
<BaseURL></BaseURL>
<BaseURL>period-a/</BaseURL>
<BaseURL>period-b/</BaseURL>
<AdaptationSet contentType="video">
<Representation id="0" bandwidth="1"/>
</AdaptationSet>
</Period>
</MPD>"#;
let mpd = Mpd::parse(xml).expect("parse");
assert_eq!(
mpd.base_url.as_deref(),
Some("https://mpd.example.com/"),
"the MPD-level BaseURL is read"
);
assert_eq!(
mpd.periods[0].base_url.as_deref(),
Some("period-a/"),
"the first non-empty sibling wins; an empty one is skipped and a later one does not override"
);
}
#[test]
fn child_duration_over_parent_timeline_is_also_exclusive() {
let xml = r#"<MPD profiles="p">
<Period>
<AdaptationSet contentType="video">
<SegmentTemplate media="t-$Time$.m4s" timescale="90000">
<SegmentTimeline><S t="0" d="90000" r="2"/></SegmentTimeline>
</SegmentTemplate>
<Representation id="0" bandwidth="1">
<SegmentTemplate media="n-$Number$.m4s" duration="45000"/>
</Representation>
</AdaptationSet>
</Period>
</MPD>"#;
let mpd = Mpd::parse(xml).expect("parse");
let set = &mpd.periods[0].adaptation_sets[0];
let set_st = set.segment_template.as_ref().expect("set template");
assert!(set_st.timeline.is_some(), "the set addresses by $Time$");
assert_eq!(set_st.duration, None);
let r = set.representations[0]
.segment_template
.as_ref()
.expect("effective template");
assert_eq!(
r.duration,
Some(45000),
"the Representation's own @duration is kept"
);
assert!(
r.timeline.is_none(),
"a template carrying @duration must not also inherit the parent's SegmentTimeline; the two addressing modes are exclusive"
);
assert_eq!(r.timescale, 90000);
assert_eq!(r.media.as_deref(), Some("n-$Number$.m4s"));
}
#[test]
fn segment_timeline_and_duration_are_mutually_exclusive() {
let xml = r#"<MPD profiles="p">
<Period>
<AdaptationSet contentType="video">
<SegmentTemplate media="c-$Number$.m4s" duration="90000" timescale="90000"/>
<Representation id="0" bandwidth="1">
<SegmentTemplate media="t-$Time$.m4s">
<SegmentTimeline><S t="0" d="90000" r="2"/></SegmentTimeline>
</SegmentTemplate>
</Representation>
</AdaptationSet>
</Period>
</MPD>"#;
let mpd = Mpd::parse(xml).expect("parse");
let set = &mpd.periods[0].adaptation_sets[0];
let set_st = set.segment_template.as_ref().expect("set template");
assert_eq!(set_st.duration, Some(90000), "the set's own @duration");
assert!(set_st.timeline.is_none());
let r = set.representations[0].segment_template.as_ref().unwrap();
assert!(r.timeline.is_some(), "the child's timeline");
assert_eq!(
r.duration, None,
"a template carrying a SegmentTimeline must not also carry the parent's @duration; the two addressing modes are exclusive"
);
assert_eq!(r.timescale, 90000);
}
#[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)]
);
}
fn s(t: Option<u64>, d: u64, r: i64) -> S {
S { t, d, r }
}
#[test]
fn resolve_first_s_absolute_t_and_explicit_t_gap() {
let tl = SegmentTimeline {
segments: vec![s(Some(1_000_000), 100, 1), s(Some(1_000_500), 100, 0)],
};
let got: Vec<(u64, u64)> = tl
.resolve(7, None)
.expect("resolve")
.into_iter()
.map(|x| (x.number, x.time))
.collect();
assert_eq!(got, vec![(7, 1_000_000), (8, 1_000_100), (9, 1_000_500)]);
}
#[test]
fn resolve_negative_r_runs_to_next_explicit_t() {
let tl = SegmentTimeline {
segments: vec![s(Some(0), 300, -1), s(Some(1000), 300, 0)],
};
let got = tl.resolve(1, None).expect("resolve");
let times: Vec<u64> = got.iter().map(|x| x.time).collect();
assert_eq!(times, vec![0, 300, 600, 900, 1000]);
}
#[test]
fn resolve_negative_r_last_s_runs_to_period_end() {
let tl = SegmentTimeline {
segments: vec![s(Some(500), 200, -1)],
};
let times: Vec<u64> = tl
.resolve(1, Some(1100))
.expect("resolve")
.iter()
.map(|x| x.time)
.collect();
assert_eq!(times, vec![500, 700, 900]);
assert_eq!(tl.resolve(1, None).expect("resolve").len(), 1);
}
#[test]
fn resolve_negative_r_hostile_count_is_capped_not_allocated() {
let tl = SegmentTimeline {
segments: vec![s(Some(0), 1, -1)],
};
assert!(tl.resolve(1, Some(u64::MAX)).is_err());
}
#[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)"
);
}
}