Skip to main content

dash_runtime/mpd/
parse.rs

1//! MPD element tree to typed model (ISO/IEC 23009-1 clause 5.3, Annex B).
2
3use std::collections::BTreeSet;
4use std::time::Duration;
5
6use transmux::parse_iso8601_duration;
7
8use super::descriptors::XLINK_RESOLVE_TO_ZERO;
9use super::model::*;
10use super::xml::{Elem, Ns, read_tree};
11use crate::error::{Limit, MpdError, Unsupported};
12use crate::limits::Limits;
13use crate::timing::datetime::parse_xs_datetime;
14use crate::timing::utc_timing::UtcScheme;
15
16const PROFILE_SEPARATOR: char = ',';
17const INFINITY_TOKEN: &str = "INF";
18const NEGATIVE_INFINITY_TOKEN: &str = "-INF";
19const MAX_START_WITH_SAP: u8 = 6;
20
21fn invalid(what: &'static str, detail: impl Into<String>) -> MpdError {
22    MpdError::Invalid {
23        what,
24        detail: detail.into(),
25    }
26}
27
28fn opt_str(e: &Elem, name: &str) -> Option<String> {
29    e.attr(name).map(str::to_owned)
30}
31
32fn opt_parse<T: core::str::FromStr>(e: &Elem, name: &'static str) -> Result<Option<T>, MpdError> {
33    match e.attr(name) {
34        None => Ok(None),
35        Some(v) => v
36            .trim()
37            .parse::<T>()
38            .map(Some)
39            .map_err(|_| invalid(name, v)),
40    }
41}
42
43fn opt_u64(e: &Elem, name: &'static str) -> Result<Option<u64>, MpdError> {
44    opt_parse(e, name)
45}
46
47fn opt_u32(e: &Elem, name: &'static str) -> Result<Option<u32>, MpdError> {
48    opt_parse(e, name)
49}
50
51fn opt_i64(e: &Elem, name: &'static str) -> Result<Option<i64>, MpdError> {
52    opt_parse(e, name)
53}
54
55fn positive_u64(e: &Elem, name: &'static str) -> Result<Option<u64>, MpdError> {
56    match opt_u64(e, name)? {
57        Some(0) => Err(invalid(name, "must be greater than zero")),
58        other => Ok(other),
59    }
60}
61
62fn parse_bool(name: &'static str, v: &str) -> Result<bool, MpdError> {
63    match v.trim() {
64        "true" | "1" => Ok(true),
65        "false" | "0" => Ok(false),
66        other => Err(invalid(name, other)),
67    }
68}
69
70fn opt_bool(e: &Elem, name: &'static str) -> Result<Option<bool>, MpdError> {
71    e.attr(name).map(|v| parse_bool(name, v)).transpose()
72}
73
74fn opt_f64(e: &Elem, name: &'static str) -> Result<Option<f64>, MpdError> {
75    let Some(v) = e.attr(name) else {
76        return Ok(None);
77    };
78    let v = v.trim();
79    if v == INFINITY_TOKEN {
80        return Ok(Some(f64::INFINITY));
81    }
82    if v == NEGATIVE_INFINITY_TOKEN {
83        return Ok(Some(f64::NEG_INFINITY));
84    }
85    match v.parse::<f64>() {
86        Ok(x) if x.is_finite() => Ok(Some(x)),
87        _ => Err(invalid(name, v)),
88    }
89}
90
91fn opt_duration(e: &Elem, name: &'static str) -> Result<Option<Duration>, MpdError> {
92    e.attr(name)
93        .map(|v| parse_iso8601_duration(v).map_err(|_| invalid(name, v)))
94        .transpose()
95}
96
97fn opt_time(e: &Elem, name: &'static str) -> Result<Option<i64>, MpdError> {
98    e.attr(name)
99        .map(|v| {
100            parse_xs_datetime(v)
101                .map(|t| t.unix_us)
102                .map_err(|_| invalid(name, v))
103        })
104        .transpose()
105}
106
107fn parse_range(name: &'static str, v: &str) -> Result<ByteRange, MpdError> {
108    let v = v.trim();
109    let (first, last) = v.split_once('-').ok_or_else(|| invalid(name, v))?;
110    let first = first.parse::<u64>().map_err(|_| invalid(name, v))?;
111    let last = if last.is_empty() {
112        None
113    } else {
114        Some(last.parse::<u64>().map_err(|_| invalid(name, v))?)
115    };
116    if let Some(l) = last
117        && l < first
118    {
119        return Err(invalid(name, v));
120    }
121    Ok(ByteRange { first, last })
122}
123
124fn opt_range(e: &Elem, name: &'static str) -> Result<Option<ByteRange>, MpdError> {
125    e.attr(name).map(|v| parse_range(name, v)).transpose()
126}
127
128fn xlink(e: &Elem) -> Result<Option<Xlink>, MpdError> {
129    let Some(href) = e.attr_in(Ns::Xlink, "href") else {
130        return Ok(None);
131    };
132    let actuate = match e.attr_in(Ns::Xlink, "actuate") {
133        None | Some("onRequest") => Actuate::OnRequest,
134        Some("onLoad") => Actuate::OnLoad,
135        Some(other) => return Err(invalid("xlink:actuate", other)),
136    };
137    Ok(Some(Xlink {
138        href: href.to_owned(),
139        actuate,
140    }))
141}
142
143fn is_resolve_to_zero(x: &Option<Xlink>) -> bool {
144    x.as_ref().is_some_and(|x| x.href == XLINK_RESOLVE_TO_ZERO)
145}
146
147struct Ctx<'a> {
148    limits: &'a Limits,
149    features: MpdFeatures,
150}
151
152fn check_count(count: usize, max: usize, limit: Limit) -> Result<(), MpdError> {
153    if count > max {
154        Err(MpdError::TooLarge { limit, max })
155    } else {
156        Ok(())
157    }
158}
159
160fn descriptor(e: &Elem) -> Result<Descriptor, MpdError> {
161    let scheme_id_uri = e
162        .attr("schemeIdUri")
163        .ok_or_else(|| invalid("schemeIdUri", format!("missing on {}", e.name)))?
164        .to_owned();
165    let url_query = e
166        .children
167        .iter()
168        .find(|c| c.ns == Ns::UrlParam && c.name == "UrlQueryInfo")
169        .map(|q| -> Result<UrlQueryInfo, MpdError> {
170            Ok(UrlQueryInfo {
171                use_mpd_url_query: opt_bool(q, "useMPDUrlQuery")?.unwrap_or(false),
172                query_string: opt_str(q, "queryString"),
173                query_template: opt_str(q, "queryTemplate"),
174            })
175        })
176        .transpose()?;
177    Ok(Descriptor {
178        scheme_id_uri,
179        value: opt_str(e, "value"),
180        id: opt_str(e, "id"),
181        default_kid: e.attr_in(Ns::Cenc, "default_KID").map(str::to_owned),
182        url_query,
183    })
184}
185
186fn descriptors(e: &Elem, name: &str) -> Result<Vec<Descriptor>, MpdError> {
187    e.children_named(name).map(descriptor).collect()
188}
189
190fn base_urls(e: &Elem, ctx: &mut Ctx<'_>) -> Result<Vec<BaseUrl>, MpdError> {
191    let mut out = Vec::new();
192    for b in e.children_named("BaseURL") {
193        check_count(out.len() + 1, ctx.limits.max_base_urls, Limit::BaseUrls)?;
194        let ato = opt_f64(b, "availabilityTimeOffset")?;
195        let complete = opt_bool(b, "availabilityTimeComplete")?;
196        if ato.is_some() || complete == Some(false) {
197            ctx.features.low_latency = true;
198        }
199        out.push(BaseUrl {
200            url: b.text.trim().to_owned(),
201            service_location: opt_str(b, "serviceLocation"),
202            priority: dvb_u32(b, "priority")?,
203            weight: dvb_u32(b, "weight")?,
204            availability_time_offset: ato,
205            availability_time_complete: complete,
206            time_shift_buffer_depth: opt_duration(b, "timeShiftBufferDepth")?,
207        });
208    }
209    Ok(out)
210}
211
212fn dvb_u32(e: &Elem, name: &'static str) -> Result<Option<u32>, MpdError> {
213    e.attr_in(Ns::Dvb, name)
214        .map(|v| v.trim().parse::<u32>().map_err(|_| invalid(name, v)))
215        .transpose()
216}
217
218fn url_type(e: &Elem) -> Result<UrlType, MpdError> {
219    Ok(UrlType {
220        source_url: opt_str(e, "sourceURL"),
221        range: opt_range(e, "range")?,
222    })
223}
224
225fn base_info(e: &Elem, ctx: &mut Ctx<'_>) -> Result<BaseInfo, MpdError> {
226    let ato = opt_f64(e, "availabilityTimeOffset")?;
227    let complete = opt_bool(e, "availabilityTimeComplete")?;
228    if ato.is_some() || complete == Some(false) {
229        ctx.features.low_latency = true;
230    }
231    Ok(BaseInfo {
232        timescale: positive_u64(e, "timescale")?,
233        presentation_time_offset: opt_u64(e, "presentationTimeOffset")?,
234        ept_delta: opt_i64(e, "eptDelta")?,
235        pd_delta: opt_i64(e, "pdDelta")?,
236        presentation_duration: opt_u64(e, "presentationDuration")?,
237        time_shift_buffer_depth: opt_duration(e, "timeShiftBufferDepth")?,
238        index_range: opt_range(e, "indexRange")?,
239        index_range_exact: opt_bool(e, "indexRangeExact")?,
240        availability_time_offset: ato,
241        availability_time_complete: complete,
242        initialization: e.child("Initialization").map(url_type).transpose()?,
243        representation_index: e.child("RepresentationIndex").map(url_type).transpose()?,
244    })
245}
246
247fn timeline(e: &Elem, ctx: &Ctx<'_>) -> Result<SegmentTimeline, MpdError> {
248    let mut out = SegmentTimeline::default();
249    if e.has_child("Pattern") {
250        out.unsupported = Some(Unsupported::SegmentSequence);
251    }
252    for s in e.children_named("S") {
253        check_count(
254            out.entries.len() + 1,
255            ctx.limits.max_timeline_entries,
256            Limit::TimelineEntries,
257        )?;
258        let d = opt_u64(s, "d")?.ok_or_else(|| invalid("S@d", "missing"))?;
259        if d == 0 {
260            return Err(invalid("S@d", "must be greater than zero"));
261        }
262        let k = opt_u64(s, "k")?;
263        if k.is_some_and(|k| k > 1) {
264            out.unsupported = Some(Unsupported::SegmentSequence);
265        }
266        out.entries.push(TimelineEntry {
267            t: opt_u64(s, "t")?,
268            n: opt_u64(s, "n")?,
269            d,
270            r: opt_i64(s, "r")?.unwrap_or(0),
271            k,
272        });
273    }
274    Ok(out)
275}
276
277fn multi_info(e: &Elem, ctx: &mut Ctx<'_>) -> Result<MultiInfo, MpdError> {
278    Ok(MultiInfo {
279        base: base_info(e, ctx)?,
280        duration: positive_u64(e, "duration")?,
281        start_number: opt_u64(e, "startNumber")?,
282        end_number: opt_u64(e, "endNumber")?,
283        timeline: e
284            .child("SegmentTimeline")
285            .map(|t| timeline(t, ctx))
286            .transpose()?,
287        bitstream_switching: e.child("BitstreamSwitching").map(url_type).transpose()?,
288    })
289}
290
291fn segment_layer(e: &Elem, ctx: &mut Ctx<'_>) -> Result<SegmentLayer, MpdError> {
292    let mut layer = SegmentLayer::default();
293    let count = ["SegmentBase", "SegmentList", "SegmentTemplate"]
294        .iter()
295        .map(|n| e.children_named(n).count())
296        .sum::<usize>();
297    if count > 1 {
298        return Err(invalid(
299            "Segment information",
300            format!(
301                "more than one of SegmentBase/SegmentList/SegmentTemplate in {}",
302                e.name
303            ),
304        ));
305    }
306    if let Some(b) = e.child("SegmentBase") {
307        layer.base = Some(SegmentBase {
308            info: base_info(b, ctx)?,
309        });
310    }
311    if let Some(l) = e.child("SegmentList") {
312        let mut urls = Vec::new();
313        for u in l.children_named("SegmentURL") {
314            check_count(
315                urls.len() + 1,
316                ctx.limits.max_segment_urls,
317                Limit::SegmentUrls,
318            )?;
319            urls.push(SegmentUrl {
320                media: opt_str(u, "media"),
321                media_range: opt_range(u, "mediaRange")?,
322                index: opt_str(u, "index"),
323                index_range: opt_range(u, "indexRange")?,
324            });
325        }
326        layer.list = Some(SegmentList {
327            multi: multi_info(l, ctx)?,
328            xlink: xlink(l)?,
329            urls,
330        });
331    }
332    if let Some(t) = e.child("SegmentTemplate") {
333        layer.template = Some(SegmentTemplate {
334            multi: multi_info(t, ctx)?,
335            media: opt_str(t, "media"),
336            index: opt_str(t, "index"),
337            initialization: opt_str(t, "initialization"),
338            bitstream_switching: opt_str(t, "bitstreamSwitching"),
339        });
340    }
341    Ok(layer)
342}
343
344fn common(e: &Elem) -> Result<CommonAttributes, MpdError> {
345    Ok(CommonAttributes {
346        profiles: opt_str(e, "profiles"),
347        width: opt_u32(e, "width")?,
348        height: opt_u32(e, "height")?,
349        sar: opt_str(e, "sar"),
350        frame_rate: opt_str(e, "frameRate"),
351        audio_sampling_rate: opt_str(e, "audioSamplingRate"),
352        mime_type: opt_str(e, "mimeType"),
353        codecs: opt_str(e, "codecs"),
354        max_playout_rate: opt_f64(e, "maxPlayoutRate")?,
355    })
356}
357
358fn representation(e: &Elem, ctx: &mut Ctx<'_>) -> Result<Representation, MpdError> {
359    let id = e
360        .attr("id")
361        .ok_or_else(|| invalid("Representation@id", "missing"))?
362        .to_owned();
363    let bandwidth = opt_u64(e, "bandwidth")?
364        .ok_or_else(|| invalid("Representation@bandwidth", format!("missing on {id}")))?;
365    Ok(Representation {
366        id,
367        bandwidth,
368        quality_ranking: opt_u32(e, "qualityRanking")?,
369        dependency_ids: e
370            .attr("dependencyId")
371            .map(|v| v.split_ascii_whitespace().map(str::to_owned).collect())
372            .unwrap_or_default(),
373        common: common(e)?,
374        base_urls: base_urls(e, ctx)?,
375        segment: segment_layer(e, ctx)?,
376        content_protections: descriptors(e, "ContentProtection")?,
377        essential_properties: descriptors(e, "EssentialProperty")?,
378        supplemental_properties: descriptors(e, "SupplementalProperty")?,
379        audio_channel_configurations: descriptors(e, "AudioChannelConfiguration")?,
380    })
381}
382
383fn conditional(name: &'static str, e: &Elem) -> Result<bool, MpdError> {
384    match e.attr(name) {
385        None => Ok(false),
386        Some(v) => match v.trim() {
387            "true" => Ok(true),
388            "false" => Ok(false),
389            digits => digits
390                .parse::<u64>()
391                .map(|n| n != 0)
392                .map_err(|_| invalid(name, digits)),
393        },
394    }
395}
396
397fn adaptation_set(e: &Elem, ctx: &mut Ctx<'_>) -> Result<Option<AdaptationSet>, MpdError> {
398    let link = xlink(e)?;
399    if is_resolve_to_zero(&link) {
400        ctx.features.xlink_resolve_to_zero = true;
401        return Ok(None);
402    }
403    let start_with_sap = opt_u32(e, "startWithSAP")?
404        .map(|v| u8::try_from(v).ok().filter(|v| *v <= MAX_START_WITH_SAP))
405        .map(|v| v.ok_or_else(|| invalid("startWithSAP", "out of range")))
406        .transpose()?;
407    let mut representations = Vec::new();
408    for r in e.children_named("Representation") {
409        check_count(
410            representations.len() + 1,
411            ctx.limits.max_representations,
412            Limit::Representations,
413        )?;
414        representations.push(representation(r, ctx)?);
415    }
416    Ok(Some(AdaptationSet {
417        id: opt_u64(e, "id")?,
418        group: opt_u64(e, "group")?,
419        lang: opt_str(e, "lang"),
420        content_type: opt_str(e, "contentType"),
421        par: opt_str(e, "par"),
422        segment_alignment: conditional("segmentAlignment", e)?,
423        start_with_sap,
424        bitstream_switching: opt_bool(e, "bitstreamSwitching")?,
425        common: common(e)?,
426        xlink: link,
427        roles: descriptors(e, "Role")?,
428        accessibilities: descriptors(e, "Accessibility")?,
429        labels: e
430            .children_named("Label")
431            .map(|l| l.text.trim().to_owned())
432            .collect(),
433        base_urls: base_urls(e, ctx)?,
434        segment: segment_layer(e, ctx)?,
435        content_protections: descriptors(e, "ContentProtection")?,
436        essential_properties: descriptors(e, "EssentialProperty")?,
437        supplemental_properties: descriptors(e, "SupplementalProperty")?,
438        audio_channel_configurations: descriptors(e, "AudioChannelConfiguration")?,
439        representations,
440    }))
441}
442
443fn period(e: &Elem, ctx: &mut Ctx<'_>) -> Result<Option<Period>, MpdError> {
444    let link = xlink(e)?;
445    if is_resolve_to_zero(&link) {
446        ctx.features.xlink_resolve_to_zero = true;
447        return Ok(None);
448    }
449    let mut sets = Vec::new();
450    for a in e.children_named("AdaptationSet") {
451        check_count(
452            sets.len() + 1,
453            ctx.limits.max_adaptation_sets,
454            Limit::AdaptationSets,
455        )?;
456        if let Some(set) = adaptation_set(a, ctx)? {
457            sets.push(set);
458        }
459    }
460    Ok(Some(Period {
461        id: opt_str(e, "id"),
462        start: opt_duration(e, "start")?,
463        duration: opt_duration(e, "duration")?,
464        bitstream_switching: opt_bool(e, "bitstreamSwitching")?.unwrap_or(false),
465        xlink: link,
466        base_urls: base_urls(e, ctx)?,
467        segment: segment_layer(e, ctx)?,
468        asset_identifier: e.child("AssetIdentifier").map(descriptor).transpose()?,
469        essential_properties: descriptors(e, "EssentialProperty")?,
470        supplemental_properties: descriptors(e, "SupplementalProperty")?,
471        adaptation_sets: sets,
472    }))
473}
474
475fn scan_features(e: &Elem, f: &mut MpdFeatures) {
476    if e.ns == Ns::Mpd {
477        match e.name.as_str() {
478            "PatchLocation" => f.patch_location = true,
479            "EventStream" => f.event_stream = true,
480            "InbandEventStream" => f.inband_event_stream = true,
481            "Preselection" => f.preselection = true,
482            "ServiceDescription" => f.service_description = true,
483            "ProducerReferenceTime" => f.producer_reference_time = true,
484            "ContentSteering" => f.content_steering = true,
485            "Resync" => f.resync = true,
486            _ => {}
487        }
488    }
489    for c in &e.children {
490        scan_features(c, f);
491    }
492}
493
494impl Mpd {
495    /// Parse an MPD document with the default [`Limits`].
496    ///
497    /// # Errors
498    /// [`MpdError`] for malformed XML, a non-MPD root, a schema violation the
499    /// core relies on, an exceeded bound, or an out-of-scope presentation type.
500    pub fn parse(bytes: &[u8]) -> Result<Self, MpdError> {
501        Self::parse_with(bytes, &Limits::default())
502    }
503
504    /// Parse an MPD document under explicit [`Limits`].
505    ///
506    /// # Errors
507    /// See [`Mpd::parse`].
508    pub fn parse_with(bytes: &[u8], limits: &Limits) -> Result<Self, MpdError> {
509        let root = read_tree(bytes, limits)?;
510        let mut ctx = Ctx {
511            limits,
512            features: MpdFeatures::default(),
513        };
514        let kind = match root.attr("type") {
515            None | Some("static") => PresentationType::Static,
516            Some("dynamic") => PresentationType::Dynamic,
517            Some("list") => return Err(MpdError::Unsupported(Unsupported::PresentationTypeList)),
518            Some(other) => return Err(invalid("MPD@type", other)),
519        };
520        let mut periods = Vec::new();
521        for p in root.children_named("Period") {
522            check_count(periods.len() + 1, limits.max_periods, Limit::Periods)?;
523            if let Some(period) = period(p, &mut ctx)? {
524                periods.push(period);
525            }
526        }
527        let mut period_ids = BTreeSet::new();
528        for id in periods.iter().filter_map(|p| p.id.as_deref()) {
529            if !period_ids.insert(id) {
530                return Err(invalid(
531                    "Period@id",
532                    format!("`{id}` names more than one Period"),
533                ));
534            }
535        }
536        let mut utc_timings = Vec::new();
537        for u in root.children_named("UTCTiming") {
538            let scheme_id_uri = u
539                .attr("schemeIdUri")
540                .ok_or_else(|| invalid("schemeIdUri", "missing on UTCTiming"))?
541                .to_owned();
542            utc_timings.push(UtcTiming {
543                scheme: UtcScheme::from_scheme_id_uri(&scheme_id_uri),
544                scheme_id_uri,
545                value: u.attr("value").unwrap_or_default().to_owned(),
546                id: opt_str(u, "id"),
547            });
548        }
549        let base = base_urls(&root, &mut ctx)?;
550        let mut features = ctx.features;
551        scan_features(&root, &mut features);
552        Ok(Self {
553            id: opt_str(&root, "id"),
554            profiles: root
555                .attr("profiles")
556                .map(|p| {
557                    p.split(PROFILE_SEPARATOR)
558                        .map(|s| s.trim().to_owned())
559                        .filter(|s| !s.is_empty())
560                        .collect()
561                })
562                .unwrap_or_default(),
563            kind,
564            availability_start_time_us: opt_time(&root, "availabilityStartTime")?,
565            availability_end_time_us: opt_time(&root, "availabilityEndTime")?,
566            publish_time_us: opt_time(&root, "publishTime")?,
567            media_presentation_duration: opt_duration(&root, "mediaPresentationDuration")?,
568            minimum_update_period: opt_duration(&root, "minimumUpdatePeriod")?,
569            min_buffer_time: opt_duration(&root, "minBufferTime")?,
570            time_shift_buffer_depth: opt_duration(&root, "timeShiftBufferDepth")?,
571            suggested_presentation_delay: opt_duration(&root, "suggestedPresentationDelay")?,
572            max_segment_duration: opt_duration(&root, "maxSegmentDuration")?,
573            max_subsegment_duration: opt_duration(&root, "maxSubsegmentDuration")?,
574            base_urls: base,
575            locations: root
576                .children_named("Location")
577                .map(|l| l.text.trim().to_owned())
578                .collect(),
579            utc_timings,
580            essential_properties: descriptors(&root, "EssentialProperty")?,
581            supplemental_properties: descriptors(&root, "SupplementalProperty")?,
582            periods,
583            features,
584        })
585    }
586}