Skip to main content

dash_mpd/
lib.rs

1//! A Rust library for parsing, serializing and downloading media content from a DASH MPD manifest,
2//! as used for on-demand replay of TV content and video streaming services. Allows both parsing of
3//! a DASH manifest (XML format) to Rust structs (deserialization) and programmatic generation of an
4//! MPD manifest (serialization). The library also allows you to download media content from a
5//! streaming server.
6
7//! [DASH](https://en.wikipedia.org/wiki/Dynamic_Adaptive_Streaming_over_HTTP) (dynamic adaptive
8//! streaming over HTTP), also called MPEG-DASH, is a technology used for media streaming over the
9//! web, commonly used for video on demand (VOD) services. The Media Presentation Description (MPD)
10//! is a description of the resources (manifest or “playlist”) forming a streaming service, that a
11//! DASH client uses to determine which assets to request in order to perform adaptive streaming of
12//! the content. DASH MPD manifests can be used both with content encoded as MPEG and as WebM.
13//!
14
15//! This library provides a serde-based parser (deserializer) and serializer for the DASH MPD
16//! format, as formally defined in ISO/IEC standard 23009-1:2022. This version of the standard is
17//! [available for free online](https://standards.iso.org/ittf/PubliclyAvailableStandards/c083314_ISO_IEC%2023009-1_2022(en).zip). XML schema files are [available for no cost from
18//! ISO](https://standards.iso.org/ittf/PubliclyAvailableStandards/MPEG-DASH_schema_files/). When
19//! MPD files in practical use diverge from the formal standard, this library prefers to
20//! interoperate with existing practice.
21//!
22//! The library does not yet provide full coverage of the fifth edition of the specification. All
23//! elements and attributes in common use are supported, however.
24//!
25//! The library also provides experimental support for downloading content (audio or video)
26//! described by an MPD manifest. This involves selecting the alternative with the most appropriate
27//! encoding (in terms of bitrate, codec, etc.), fetching segments of the content using HTTP or
28//! HTTPS requests (this functionality depends on the `reqwest` crate) and muxing audio and video
29//! segments together (using ffmpeg via the `ac_ffmpeg` crate).
30//!
31//!
32//! ## DASH features supported
33//!
34//! - VOD (static) stream manifests
35//! - Multi-period content
36//! - XLink elements (only with actuate=onLoad semantics, resolve-to-zero supported)
37//! - All forms of segment index info: SegmentBase@indexRange, SegmentTimeline,
38//!   SegmentTemplate@duration, SegmentTemplate@index, SegmentList
39//! - Media containers of types supported by mkvmerge, ffmpeg, VLC and MP4Box (this includes
40//!   Matroska, ISO-BMFF / CMAF / MP4, WebM, MPEG-2 TS)
41//! - Subtitles: preliminary support for WebVTT and TTML streams
42//!
43//!
44//! ## Limitations / unsupported features
45//!
46//! - Dynamic MPD manifests, that are used for live streaming/OTT TV
47//! - XLink with actuate=onRequest semantics
48//! - Application of MPD patches
49//
50//
51//
52// Reference libdash library: https://github.com/bitmovin/libdash
53//   https://github.com/bitmovin/libdash/blob/master/libdash/libdash/source/xml/Node.cpp
54// Reference dash.js library: https://github.com/Dash-Industry-Forum/dash.js
55// Google Shaka player: https://github.com/google/shaka-player
56// The DASH code in VLC: https://code.videolan.org/videolan/vlc/-/tree/master/modules/demux/dash
57// Streamlink source code: https://github.com/streamlink/streamlink/blob/master/src/streamlink/stream/dash_manifest.py
58
59// TODO: handle dynamic MPD as per https://livesim.dashif.org/livesim/mup_30/testpic_2s/Manifest.mpd
60// TODO: handle indexRange attribute, as per https://dash.akamaized.net/dash264/TestCasesMCA/dolby/2/1/ChID_voices_71_768_ddp.mpd
61// TODO: implement MPD Patch support when downloading, with test cases from https://github.com/ab2022/mpddiffs/tree/main
62
63
64#![allow(non_snake_case)]
65
66/// If library feature `libav` is enabled, muxing support (combining audio and video streams, which
67/// are often separated out in DASH streams) is provided by ffmpeg's libav library, via the
68/// `ac_ffmpeg` crate. Otherwise, muxing is implemented by calling `mkvmerge`, `ffmpeg` or `vlc` as
69/// a subprocess. The muxing support is only compiled when the fetch feature is enabled.
70#[cfg(feature = "fetch")]
71pub mod media;
72#[cfg(all(feature = "fetch", feature = "libav"))]
73mod libav;
74#[cfg(all(feature = "fetch", not(feature = "libav")))]
75pub mod ffmpeg;
76#[cfg(feature = "fetch")]
77pub mod sidx;
78#[cfg(feature = "fetch")]
79pub mod fetch;
80#[cfg(feature = "fetch")]
81pub mod decryption;
82#[cfg(feature = "fetch")]
83pub mod stpp;
84#[cfg(feature = "fetch")]
85pub mod vtt;
86// Support for the SCTE-35 standard for insertion of alternate content
87#[cfg(feature = "scte35")]
88pub mod scte35;
89#[cfg(feature = "scte35")]
90use crate::scte35::{Signal, SpliceInfoSection};
91
92#[cfg(all(feature = "fetch", feature = "libav"))]
93use crate::libav::{mux_audio_video, copy_video_to_container, copy_audio_to_container};
94#[cfg(all(feature = "fetch", not(feature = "libav")))]
95use crate::ffmpeg::{mux_audio_video, copy_video_to_container, copy_audio_to_container};
96
97#[cfg(all(feature = "sandbox", feature = "fetch", target_os = "linux"))]
98pub mod sandbox;
99
100use serde::{Serialize, Serializer, Deserialize};
101use serde::de;
102use serde_with::skip_serializing_none;
103use regex::Regex;
104use std::sync::LazyLock;
105use std::time::Duration;
106use chrono::DateTime;
107use url::Url;
108#[allow(unused_imports)]
109use tracing::warn;
110
111// used to parse duration when de-serializing to MPD
112static XS_DURATION_REGEX: LazyLock<Regex> = LazyLock::new(||
113    Regex::new(concat!(r"^(?P<sign>[+-])?P",
114        r"(?:(?P<years>\d+)Y)?",
115        r"(?:(?P<months>\d+)M)?",
116        r"(?:(?P<weeks>\d+)W)?",
117        r"(?:(?P<days>\d+)D)?",
118        r"(?:(?P<hastime>T)", // time part must begin with a T
119        r"(?:(?P<hours>\d+)H)?",
120        r"(?:(?P<minutes>\d+)M)?",
121        r"(?:(?P<seconds>\d+)(?:(?P<nanoseconds>[.,]\d+)?)S)?",
122        r")?")).unwrap()
123);
124
125/// Type representing an xs:dateTime, as per <https://www.w3.org/TR/xmlschema-2/#dateTime>
126// Something like 2021-06-03T13:00:00Z or 2022-12-06T22:27:53
127pub type XsDatetime = DateTime<chrono::offset::Utc>;
128
129#[derive(thiserror::Error, Debug)]
130#[non_exhaustive]
131pub enum DashMpdError {
132    #[error("parse error {0:?}")]
133    Parsing(String),
134    #[error("invalid Duration: {0:?}")]
135    InvalidDuration(String),
136    #[error("invalid DateTime: {0:?}")]
137    InvalidDateTime(String),
138    #[error("invalid media stream: {0:?}")]
139    UnhandledMediaStream(String),
140    #[error("I/O error {1} ({0:?})")]
141    Io(#[source] std::io::Error, String),
142    #[error("network error {0:?}")]
143    Network(String),
144    #[error("network timeout: {0:?}")]
145    NetworkTimeout(String),
146    #[error("network connection: {0:?}")]
147    NetworkConnect(String),
148    #[error("muxing error {0:?}")]
149    Muxing(String),
150    #[error("decryption error {0:?}")]
151    Decrypting(String),
152    #[error("{0:?}")]
153    Other(String),
154}
155
156
157// Serialize an xsd:double parameter. We can't use the default serde serialization for f64 due to
158// the difference in handling INF, -INF and NaN values.
159//
160// Reference: http://www.datypic.com/sc/xsd/t-xsd_double.html
161fn serialize_xsd_double<S>(xsd: &f64, serializer: S) -> Result<S::Ok, S::Error>
162where
163    S: Serializer,
164{
165    let formatted = if xsd.is_nan() {
166        String::from("NaN")
167    } else if xsd.is_infinite() {
168        if xsd.is_sign_positive() {
169            // Here serde returns "inf", which doesn't match the XML Schema definition.
170            String::from("INF")
171        } else {
172            String::from("-INF")
173        }
174    } else {
175        xsd.to_string()
176    };
177    serializer.serialize_str(&formatted)
178}
179
180// Serialize an Option<f64> as an xsd:double.
181#[allow(clippy::ref_option)]
182fn serialize_opt_xsd_double<S>(oxsd: &Option<f64>, serializer: S) -> Result<S::Ok, S::Error>
183where
184    S: Serializer,
185{
186    if let Some(xsd) = oxsd {
187        serialize_xsd_double(xsd, serializer)
188    } else {
189        // in fact this won't be called because of the #[skip_serializing_none] annotation
190        serializer.serialize_none()
191    }
192}
193
194
195/// Parse an XML duration string, as per <https://www.w3.org/TR/xmlschema-2/#duration>
196///
197/// The lexical representation for duration is the ISO 8601 extended format PnYn MnDTnH nMnS, where
198/// nY represents the number of years, nM the number of months, nD the number of days, 'T' is the
199/// date/time separator, nH the number of hours, nM the number of minutes and nS the number of
200/// seconds. The number of seconds can include decimal digits to arbitrary precision.
201///
202/// Examples: "PT0H0M30.030S", "PT1.2S", PT1004199059S, PT130S
203/// P2Y6M5DT12H35M30S  => 2 years, 6 months, 5 days, 12 hours, 35 minutes, 30 seconds
204/// P1DT2H => 1 day, 2 hours
205/// P0Y20M0D => 20 months (0 is permitted as a number, but is not required)
206/// PT1M30.5S => 1 minute, 30.5 seconds
207///
208/// Limitations:
209///   - this function can't represent negative durations (leading "-" character) due to the choice of a
210///     std::time::Duration.
211///
212///   - this function only accepts fractional parts of seconds, and rejects for example "P0.5Y" and "PT2.3H"
213///
214///   - months are approximated as 30 days and years as 365 days, as std::time::Duration cannot
215///     represent calendar-relative durations. This means that values involving months or years are
216///     not perfectly round-trippable.
217fn parse_xs_duration(s: &str) -> Result<Duration, DashMpdError> {
218    use std::cmp::min;
219
220    match XS_DURATION_REGEX.captures(s) {
221        Some(m) => {
222            if m.name("hastime").is_none() &&
223               m.name("years").is_none() &&
224               m.name("months").is_none() &&
225               m.name("weeks").is_none() &&
226               m.name("days").is_none() {
227                  return Err(DashMpdError::InvalidDuration("empty".to_string()));
228            }
229            let mut secs: u64 = 0;
230            let mut nsecs: u32 = 0;
231            if let Some(nano) = m.name("nanoseconds") {
232                // We drop the initial "." and limit precision in the fractional seconds to 9 digits
233                // (nanosecond precision)
234                let lim = min(nano.as_str().len(), 9 + ".".len());
235                if let Some(ss) = &nano.as_str().get(1..lim) {
236                    let padded = format!("{ss:0<9}");
237                    nsecs = padded.parse::<u32>()
238                        .map_err(|_| DashMpdError::InvalidDuration(String::from(s)))?;
239                }
240            }
241            if let Some(mseconds) = m.name("seconds") {
242                let seconds = mseconds.as_str().parse::<u64>()
243                    .map_err(|_| DashMpdError::InvalidDuration(String::from(s)))?;
244                secs += seconds;
245            }
246            if let Some(mminutes) = m.name("minutes") {
247                let minutes = mminutes.as_str().parse::<u64>()
248                    .map_err(|_| DashMpdError::InvalidDuration(String::from(s)))?;
249                secs += minutes * 60;
250            }
251            if let Some(mhours) = m.name("hours") {
252                let hours = mhours.as_str().parse::<u64>()
253                    .map_err(|_| DashMpdError::InvalidDuration(String::from(s)))?;
254                secs += hours * 60 * 60;
255            }
256            if let Some(mdays) = m.name("days") {
257                let days = mdays.as_str().parse::<u64>()
258                    .map_err(|_| DashMpdError::InvalidDuration(String::from(s)))?;
259                secs += days * 60 * 60 * 24;
260            }
261            if let Some(mweeks) = m.name("weeks") {
262                let weeks = mweeks.as_str().parse::<u64>()
263                    .map_err(|_| DashMpdError::InvalidDuration(String::from(s)))?;
264                secs += weeks * 60 * 60 * 24 * 7;
265            }
266            if let Some(mmonths) = m.name("months") {
267                let months = mmonths.as_str().parse::<u64>()
268                    .map_err(|_| DashMpdError::InvalidDuration(String::from(s)))?;
269                secs += months * 60 * 60 * 24 * 30;
270            }
271            if let Some(myears) = m.name("years") {
272                let years = myears.as_str().parse::<u64>()
273                    .map_err(|_| DashMpdError::InvalidDuration(String::from(s)))?;
274                secs += years * 60 * 60 * 24 * 365;
275            }
276            if let Some(msign) = m.name("sign") {
277                if msign.as_str() == "-" {
278                    return Err(DashMpdError::InvalidDuration("can't represent negative durations".to_string()));
279                }
280            }
281            Ok(Duration::new(secs, nsecs))
282        },
283        None => Err(DashMpdError::InvalidDuration(String::from("couldn't parse XS duration"))),
284    }
285}
286
287
288// Note bug in current version of the iso8601 crate which incorrectly parses
289// strings like "PT344S" (seen in a real MPD) as a zero duration. However, ISO 8601 standard as
290// adopted by Indian Bureau of Standards includes p29 an example "PT72H", as do various MPD
291// manifests in the wild. https://archive.org/details/gov.in.is.7900.2007/
292// fn parse_xs_duration_buggy(s: &str) -> Result<Duration> {
293//     match iso8601::duration(s) {
294//         Ok(iso_duration) => {
295//             match iso_duration {
296//                 iso8601::Duration::Weeks(w) => Ok(Duration::new(w as u64*60 * 60 * 24 * 7, 0)),
297//                 iso8601::Duration::YMDHMS {year, month, day, hour, minute, second, millisecond } => {
298//                     // note that if year and month are specified, we are not going to do a very
299//                     // good conversion here
300//                     let mut secs: u64 = second.into();
301//                     secs += minute as u64 * 60;
302//                     secs += hour   as u64 * 60 * 60;
303//                     secs += day    as u64 * 60 * 60 * 24;
304//                     secs += month  as u64 * 60 * 60 * 24 * 31;
305//                     secs += year   as u64 * 60 * 60 * 24 * 31 * 365;
306//                     Ok(Duration::new(secs, millisecond * 1000_000))
307//                 },
308//             }
309//         },
310//         Err(e) => Err(anyhow!("Couldn't parse XS duration {}: {:?}", s, e)),
311//     }
312// }
313
314// The iso8601_duration crate can't handle durations with fractional seconds
315// fn parse_xs_duration_buggy(s: &str) -> Result<Duration> {
316//     match iso8601_duration::Duration::parse(s) {
317//         Ok(d) => {
318//             let nanos: u32 = 1000_000 * d.second.fract() as u32;
319//             let mut secs: u64 = d.second.trunc() as u64;
320//             secs += d.minute as u64 * 60;
321//             secs += d.hour   as u64 * 60 * 60;
322//             secs += d.day    as u64 * 60 * 60 * 24;
323//             secs += d.month  as u64 * 60 * 60 * 24 * 31;
324//             secs += d.year   as u64 * 60 * 60 * 24 * 31 * 365;
325//             Ok(Duration::new(secs, nanos))
326//         },
327//         Err(e) => Err(anyhow!("Couldn't parse XS duration {}: {:?}", s, e)),
328//     }
329// }
330
331
332
333// Deserialize an optional XML duration string to an Option<Duration>. This is a little trickier
334// than deserializing a required field with serde.
335fn deserialize_xs_duration<'de, D>(deserializer: D) -> Result<Option<Duration>, D::Error>
336where
337    D: de::Deserializer<'de>,
338{
339    match <Option<String>>::deserialize(deserializer) {
340        Ok(optstring) => match optstring {
341            Some(xs) => match parse_xs_duration(&xs) {
342                Ok(d) => Ok(Some(d)),
343                Err(e) => Err(de::Error::custom(e)),
344            },
345            None => Ok(None),
346        },
347        // the field isn't present, return an Ok(None)
348        Err(_) => Ok(None),
349    }
350}
351
352// There are many possible correct ways of serializing a Duration in xs:duration (ISO 8601) format.
353// We choose to serialize to a perhaps-canonical xs:duration format including hours and minutes
354// (instead of representing them as a large number of seconds). Hour and minute count are not
355// included when the duration is less than a minute. Trailing zeros are omitted. Fractional seconds
356// are included to a nanosecond precision.
357//
358// Example: Duration::new(3600, 40_000_000) => "PT1H0M0.04S"
359#[allow(clippy::ref_option)]
360fn serialize_xs_duration<S>(oxs: &Option<Duration>, serializer: S) -> Result<S::Ok, S::Error>
361where
362    S: Serializer,
363{
364    if let Some(xs) = oxs {
365        let total_secs = xs.as_secs();
366        let nanos = xs.subsec_nanos();
367        let hours = total_secs / 3600;
368        let mins = (total_secs % 3600) / 60;
369        let secs = total_secs % 60;
370        let frac = if nanos > 0 {
371            format!(".{nanos:09}").trim_end_matches('0').to_string()
372        } else {
373            String::new()
374        };
375        let s = match (hours, mins, secs, nanos) {
376            (h, 0, 0, 0) if h > 0 => format!("PT{h}H"),
377            (h, m, 0, 0) if h > 0 => format!("PT{h}H{m}M"),
378            (0, m, 0, 0) if m > 0 => format!("PT{m}M"),
379            (h, m, s, _) if h > 0 => format!("PT{h}H{m}M{s}{frac}S"),
380            (0, m, s, _) if m > 0 => format!("PT{m}M{s}{frac}S"),
381            _ => format!("PT{secs}{frac}S"),
382        };
383        serializer.serialize_str(&s)
384    } else {
385        // in fact this won't be called because of the #[skip_serializing_none] annotation
386        serializer.serialize_none()
387    }
388}
389
390
391// We can't use the parsing functionality from the chrono crate, because that assumes RFC 3339
392// format (including a timezone), whereas the xs:dateTime type (as per
393// <https://www.w3.org/TR/xmlschema-2/#dateTime>) allows the timezone to be omitted. For more on the
394// complicated relationship between ISO 8601 and RFC 3339, see
395// <https://ijmacd.github.io/rfc3339-iso8601/>.
396fn parse_xs_datetime(s: &str) -> Result<XsDatetime, DashMpdError> {
397    use iso8601::Date;
398    use chrono::{LocalResult, NaiveDate, TimeZone};
399    use num_traits::cast::FromPrimitive;
400    match DateTime::<chrono::offset::FixedOffset>::parse_from_rfc3339(s) {
401        Ok(dt) => Ok(dt.into()),
402        Err(_) => match iso8601::datetime(s) {
403            Ok(dt) => {
404                let nd = match dt.date {
405                    Date::YMD { year, month, day } =>
406                        NaiveDate::from_ymd_opt(year, month, day)
407                        .ok_or(DashMpdError::InvalidDateTime(s.to_string()))?,
408                    Date::Week { year, ww, d } => {
409                        let d = chrono::Weekday::from_u32(d)
410                            .ok_or(DashMpdError::InvalidDateTime(s.to_string()))?;
411                        NaiveDate::from_isoywd_opt(year, ww, d)
412                            .ok_or(DashMpdError::InvalidDateTime(s.to_string()))?
413                    },
414                    Date::Ordinal { year, ddd } =>
415                        NaiveDate::from_yo_opt(year, ddd)
416                        .ok_or(DashMpdError::InvalidDateTime(s.to_string()))?,
417                };
418                let nd = nd.and_hms_nano_opt(dt.time.hour, dt.time.minute, dt.time.second, dt.time.millisecond*1000*1000)
419                    .ok_or(DashMpdError::InvalidDateTime(s.to_string()))?;
420                let tz_secs = dt.time.tz_offset_hours * 3600 + dt.time.tz_offset_minutes * 60;
421                match chrono::FixedOffset::east_opt(tz_secs)
422                    .ok_or(DashMpdError::InvalidDateTime(s.to_string()))?
423                    .from_local_datetime(&nd)
424                {
425                    LocalResult::Single(local) => Ok(local.with_timezone(&chrono::Utc)),
426                    _ => Err(DashMpdError::InvalidDateTime(s.to_string())),
427                }
428            },
429            Err(_) => Err(DashMpdError::InvalidDateTime(s.to_string())),
430        }
431    }
432}
433
434// Deserialize an optional XML datetime string (type xs:datetime) to an Option<XsDatetime>.
435fn deserialize_xs_datetime<'de, D>(deserializer: D) -> Result<Option<XsDatetime>, D::Error>
436where
437    D: de::Deserializer<'de>,
438{
439    match <Option<String>>::deserialize(deserializer) {
440        Ok(optstring) => match optstring {
441            Some(xs) => match parse_xs_datetime(&xs) {
442                Ok(d) => Ok(Some(d)),
443                Err(e) => Err(de::Error::custom(e)),
444            },
445            None => Ok(None),
446        },
447        // the field isn't present; return an Ok(None)
448        Err(_) => Ok(None),
449    }
450}
451
452// XSD type is "UIntVectorType", or whitespace-separated list of unsigned integers.
453// It's a <xs:list itemType="xs:unsignedInt"/>.
454fn serialize_xsd_uintvector<S>(v: &Vec<u64>, serializer: S) -> Result<S::Ok, S::Error>
455where
456    S: Serializer,
457{
458    let mut formatted = String::new();
459    for u in v {
460        formatted += &format!("{u} ");
461    }
462    serializer.serialize_str(&formatted)
463}
464
465fn deserialize_xsd_uintvector<'de, D>(deserializer: D) -> Result<Vec<u64>, D::Error>
466where
467    D: de::Deserializer<'de>,
468{
469    let s = String::deserialize(deserializer)?;
470    let mut out = Vec::<u64>::new();
471    for uint64_str in s.split_whitespace() {
472        match uint64_str.parse::<u64>() {
473            Ok(val) => out.push(val),
474            Err(e) => return Err(de::Error::custom(e)),
475        }
476    }
477    Ok(out)
478}
479
480// These serialization functions are need to serialize correct default values for various optional
481// namespaces specified as attributes of the root MPD struct (e.g. xmlns:xsi, xmlns:xlink). If a
482// value is present in the struct field (specified in the parsed XML or provided explicitly when
483// building the MPD struct) then we use that, and otherwise default to the well-known URLs for these
484// namespaces.
485//
486// The quick-xml support for #[serde(default = "fn")] (which would allow a less heavyweight solution
487// to this) does not seem to work.
488
489#[allow(clippy::ref_option)]
490fn serialize_xmlns<S>(os: &Option<String>, serializer: S) -> Result<S::Ok, S::Error>
491where S: serde::Serializer {
492    if let Some(s) = os {
493        serializer.serialize_str(s)
494    } else {
495        serializer.serialize_str("urn:mpeg:dash:schema:mpd:2011")
496    }
497}
498
499#[allow(clippy::ref_option)]
500fn serialize_xsi_ns<S>(os: &Option<String>, serializer: S) -> Result<S::Ok, S::Error>
501where S: serde::Serializer {
502    if let Some(s) = os {
503        serializer.serialize_str(s)
504    } else {
505        serializer.serialize_str("http://www.w3.org/2001/XMLSchema-instance")
506    }
507}
508
509#[allow(clippy::ref_option)]
510fn serialize_cenc_ns<S>(os: &Option<String>, serializer: S) -> Result<S::Ok, S::Error>
511where S: serde::Serializer {
512    if let Some(s) = os {
513        serializer.serialize_str(s)
514    } else {
515        serializer.serialize_str("urn:mpeg:cenc:2013")
516    }
517}
518
519#[allow(clippy::ref_option)]
520fn serialize_mspr_ns<S>(os: &Option<String>, serializer: S) -> Result<S::Ok, S::Error>
521where S: serde::Serializer {
522    if let Some(s) = os {
523        serializer.serialize_str(s)
524    } else {
525        serializer.serialize_str("urn:microsoft:playready")
526    }
527}
528
529#[allow(clippy::ref_option)]
530fn serialize_xlink_ns<S>(os: &Option<String>, serializer: S) -> Result<S::Ok, S::Error>
531where S: serde::Serializer {
532    if let Some(s) = os {
533        serializer.serialize_str(s)
534    } else {
535        serializer.serialize_str("http://www.w3.org/1999/xlink")
536    }
537}
538
539#[allow(clippy::ref_option)]
540fn serialize_dvb_ns<S>(os: &Option<String>, serializer: S) -> Result<S::Ok, S::Error>
541where S: serde::Serializer {
542    if let Some(s) = os {
543        serializer.serialize_str(s)
544    } else {
545        serializer.serialize_str("urn:dvb:dash-extensions:2014-1")
546    }
547}
548
549
550// These default_* functions are needed to provide defaults for serde deserialization of certain
551// elements, where the Default function for that type doesn't return a value compatible with the
552// default specified in the XSD specification.
553#[allow(clippy::unnecessary_wraps)]
554fn default_optstring_on_request() -> Option<String> {
555    Some("onRequest".to_string())
556}
557
558#[allow(clippy::unnecessary_wraps)]
559fn default_optstring_one() -> Option<String> {
560    Some(String::from("1"))
561}
562
563#[allow(clippy::unnecessary_wraps)]
564fn default_optstring_encoder() -> Option<String> {
565    Some(String::from("encoder"))
566}
567
568#[allow(clippy::unnecessary_wraps)]
569fn default_optstring_any() -> Option<String> {
570    Some(String::from("any"))
571}
572
573#[allow(clippy::unnecessary_wraps)]
574fn default_optstring_query() -> Option<String> {
575    Some(String::from("query"))
576}
577
578#[allow(clippy::unnecessary_wraps)]
579fn default_optstring_segment() -> Option<String> {
580    Some(String::from("segment"))
581}
582
583#[allow(clippy::unnecessary_wraps)]
584fn default_optbool_true() -> Option<bool> {
585    Some(true)
586}
587
588#[allow(clippy::unnecessary_wraps)]
589fn default_optbool_false() -> Option<bool> {
590    Some(false)
591}
592
593#[allow(clippy::unnecessary_wraps)]
594fn default_optu64_zero() -> Option<u64> {
595    Some(0)
596}
597
598#[allow(clippy::unnecessary_wraps)]
599fn default_optu64_one() -> Option<u64> {
600    Some(1)
601}
602
603
604// The MPD format is documented by ISO using an XML Schema at
605// https://standards.iso.org/ittf/PubliclyAvailableStandards/MPEG-DASH_schema_files/DASH-MPD-edition2.xsd
606// Historical spec: https://ptabdata.blob.core.windows.net/files/2020/IPR2020-01688/v67_EXHIBIT%201067%20-%20ISO-IEC%2023009-1%202019(E)%20-%20Info.%20Tech.%20-%20Dynamic%20Adaptive%20Streaming%20Over%20HTTP%20(DASH).pdf
607// We occasionally diverge from the standard when in-the-wild implementations do.
608// Some reference code for DASH is at https://github.com/bitmovin/libdash
609//
610// We are using the quick_xml + serde crates to deserialize the XML content to Rust structs, and the
611// reverse serialization process of programmatically generating XML from Rust structs. Note that
612// serde will ignore unknown fields when deserializing, so we don't need to cover every single
613// possible field.
614
615/// The title of the media stream.
616#[skip_serializing_none]
617#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq, Hash)]
618#[serde(default)]
619pub struct Title {
620    #[serde(rename = "$text")]
621    pub content: Option<String>,
622}
623
624/// The original source of the media stream.
625#[skip_serializing_none]
626#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq, Hash)]
627#[serde(default)]
628pub struct Source {
629    #[serde(rename = "$text")]
630    pub content: Option<String>,
631}
632
633/// Copyright information concerning the media stream.
634#[skip_serializing_none]
635#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq, Hash)]
636#[serde(default)]
637pub struct Copyright {
638    #[serde(rename = "$text")]
639    pub content: Option<String>,
640}
641
642/// Metainformation concerning the media stream (title, language, etc.)
643#[skip_serializing_none]
644#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq, Hash)]
645#[serde(default)]
646pub struct ProgramInformation {
647    /// Language in RFC 5646 format
648    #[serde(rename = "@lang")]
649    pub lang: Option<String>,
650    #[serde(rename = "@moreInformationURL")]
651    pub moreInformationURL: Option<String>,
652    pub Title: Option<Title>,
653    pub Source: Option<Source>,
654    pub Copyright: Option<Copyright>,
655    #[serde(rename(serialize = "scte214:ContentIdentifier", deserialize = "ContentIdentifier"))]
656    pub scte214ContentIdentifier: Option<Scte214ContentIdentifier>,
657}
658
659/// DASH specification MPEG extension (SCTE 214) program identification type.
660///
661/// Indicates how the program content is identified.
662#[skip_serializing_none]
663#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq, Hash)]
664#[serde(default)]
665pub struct Scte214ContentIdentifier {
666    #[serde(rename = "@type")]
667    pub idType: Option<String>,
668    #[serde(rename = "@value")]
669    pub idValue: Option<String>,
670}
671
672/// Describes a sequence of contiguous Segments with identical duration.
673#[skip_serializing_none]
674#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq, Hash)]
675#[serde(default)]
676pub struct S {
677    /// Time
678    #[serde(rename = "@t")]
679    pub t: Option<u64>,
680    #[serde(rename = "@n")]
681    pub n: Option<u64>,
682    /// The duration (shall not exceed the value of MPD@maxSegmentDuration).
683    #[serde(rename = "@d")]
684    pub d: u64,
685    /// The repeat count (number of contiguous Segments with identical MPD duration minus one),
686    /// defaulting to zero if not present.
687    #[serde(rename = "@r")]
688    pub r: Option<i64>,
689    #[serde(rename = "@k")]
690    pub k: Option<u64>,
691}
692
693/// Contains a sequence of `S` elements, each of which describes a sequence of contiguous segments of
694/// identical duration.
695#[skip_serializing_none]
696#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq, Hash)]
697#[serde(default)]
698pub struct SegmentTimeline {
699    /// There must be at least one S element.
700    #[serde(rename = "S")]
701    pub segments: Vec<S>,
702}
703
704/// Information on the bitstream switching capabilities for Representations.
705///
706/// When bitstream switching is enabled, the player can seamlessly switch between Representations in
707/// the manifest without reinitializing the media decoder. This means fewer perturbations for the
708/// viewer when the network conditions change. It requires the media segments to have been encoded
709/// respecting a certain number of constraints.
710#[skip_serializing_none]
711#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq, Hash)]
712#[serde(default)]
713pub struct BitstreamSwitching {
714    #[serde(rename = "@sourceURL")]
715    pub source_url: Option<String>,
716    #[serde(rename = "@range")]
717    pub range: Option<String>,
718}
719
720/// The first media segment in a sequence of Segments.
721///
722/// Subsequent segments can be concatenated to this segment to produce a media stream.
723#[skip_serializing_none]
724#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq, Hash)]
725#[serde(default)]
726pub struct Initialization {
727    #[serde(rename = "@sourceURL")]
728    pub sourceURL: Option<String>,
729    #[serde(rename = "@range")]
730    pub range: Option<String>,
731}
732
733#[skip_serializing_none]
734#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq, Hash)]
735#[serde(default)]
736pub struct RepresentationIndex {
737    #[serde(rename = "@range")]
738    pub range: Option<String>,
739    #[serde(rename = "@sourceURL")]
740    pub sourceURL: Option<String>,
741}
742
743/// Allows template-based `SegmentURL` construction. Specifies various substitution rules using
744/// dynamic values such as `$Time$` and `$Number$` that map to a sequence of Segments.
745#[skip_serializing_none]
746#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq)]
747#[serde(default)]
748pub struct SegmentTemplate {
749    #[serde(rename = "@media")]
750    pub media: Option<String>,
751    #[serde(rename = "@index")]
752    pub index: Option<String>,
753    #[serde(rename = "@initialization")]
754    pub initialization: Option<String>,
755    #[serde(rename = "@bitstreamSwitching")]
756    pub bitstreamSwitching: Option<String>,
757    #[serde(rename = "@indexRange")]
758    pub indexRange: Option<String>,
759    #[serde(rename = "@indexRangeExact")]
760    pub indexRangeExact: Option<bool>,
761    #[serde(rename = "@startNumber")]
762    pub startNumber: Option<u64>,
763    #[serde(rename = "@endNumber")]
764    pub endNumber: Option<u64>,
765    // note: the spec says this is an unsigned int, not an xs:duration. In practice, some manifests
766    // use a floating point value (eg.
767    // https://dash.akamaized.net/akamai/bbb_30fps/bbb_with_multiple_tiled_thumbnails.mpd)
768    #[serde(rename = "@duration")]
769    pub duration: Option<f64>,
770    #[serde(rename = "@timescale")]
771    pub timescale: Option<u64>,
772    /// Indicates a possible offset between media segment start/end points and period start/end points.
773    #[serde(rename = "@eptDelta")]
774    pub eptDelta: Option<i64>,
775    /// Specifies the difference between the presentation duration of this Representation and the
776    /// Period duration. Expressed in units of @timescale.
777    #[serde(rename = "@pdDelta")]
778    pub pbDelta: Option<i64>,
779    #[serde(rename = "@presentationTimeOffset")]
780    pub presentationTimeOffset: Option<u64>,
781    #[serde(rename = "@availabilityTimeOffset", serialize_with="serialize_opt_xsd_double")]
782    pub availabilityTimeOffset: Option<f64>,
783    #[serde(rename = "@availabilityTimeComplete")]
784    pub availabilityTimeComplete: Option<bool>,
785    pub Initialization: Option<Initialization>,
786    #[serde(rename = "RepresentationIndex")]
787    pub representation_index: Option<RepresentationIndex>,
788    // The XSD included in the DASH specification only includes a FailoverContent element on the
789    // SegmentBase element, but also includes it on a SegmentTemplate element in one of the
790    // examples. Even if examples are not normative, we choose to be tolerant in parsing.
791    #[serde(rename = "FailoverContent")]
792    pub failover_content: Option<FailoverContent>,
793    pub SegmentTimeline: Option<SegmentTimeline>,
794    pub BitstreamSwitching: Option<BitstreamSwitching>,
795}
796
797/// A URI string to which a new request for an updated manifest should be made.
798///
799/// This feature is intended for servers and clients that can't use sticky HTTP redirects.
800#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq, Hash)]
801#[serde(default)]
802pub struct Location {
803    #[serde(rename = "$text")]
804    pub url: String,
805}
806
807/// A URI string that specifies one or more common locations for Segments and other resources.
808///
809/// Used as a prefix for SegmentURLs. Can be specified at the level of the MPD node, or Period,
810/// AdaptationSet, Representation, and can be nested (the client should combine the prefix on MPD
811/// and on Representation, for example).
812#[skip_serializing_none]
813#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq)]
814#[serde(default)]
815pub struct BaseURL {
816    #[serde(rename = "@serviceLocation")]
817    pub serviceLocation: Option<String>,
818    #[serde(rename = "@byteRange")]
819    pub byte_range: Option<String>,
820    /// Elements with the same `@serviceLocation` value are likely to have their URLs resolve to
821    /// services at a common network location, for example the same CDN.
822    #[serde(rename = "@availabilityTimeOffset", serialize_with="serialize_opt_xsd_double")]
823    pub availability_time_offset: Option<f64>,
824    #[serde(rename = "@availabilityTimeComplete")]
825    pub availability_time_complete: Option<bool>,
826    #[serde(rename = "@timeShiftBufferDepth",
827            serialize_with = "serialize_xs_duration",
828            deserialize_with = "deserialize_xs_duration",
829            default)]
830    pub timeShiftBufferDepth: Option<Duration>,
831    /// Lowest value indicates the highest priority.
832    #[serde(rename = "@dvb:priority", alias = "@priority")]
833    pub priority: Option<u64>,
834    /// For load balancing between different base urls with the same @priority. The BaseURL to use
835    /// is chosen at random by the player, with the weight of any given BaseURL being its @weight
836    /// value divided by the sum of all @weight values.
837    #[serde(rename = "@dvb:weight", alias = "@weight")]
838    pub weight: Option<i64>,
839    #[serde(rename = "$text")]
840    pub base: String,
841}
842
843/// Failover Content Segment (FCS).
844///
845/// The time and optional duration for which a representation does not represent the main content
846/// but a failover version. It can and is also used to represent gaps where no segments are present
847/// at all - used within the `FailoverContent` element.
848#[skip_serializing_none]
849#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq, Hash)]
850#[serde(default)]
851pub struct Fcs {
852    /// The time at which no/failover segments for this representation starts (if the valid
853    /// flag is set to `true` in `FailoverContent`).
854    #[serde(rename = "@t")]
855    pub t: u64,
856
857    /// The optional duration for which there is failover or no content.  If `None` then
858    /// the duration is for the remainder of the `Period` the parent `Representation` is in.
859    #[serde(rename = "@d")]
860    pub d: Option<u64>,
861}
862
863/// Period of time for which either failover content or no content/segments exist for the
864/// parent `Representation`.
865#[skip_serializing_none]
866#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq, Hash)]
867#[serde(default)]
868pub struct FailoverContent {
869    // If true, the FCS represents failover content; if false, it represents a gap
870    // where there are no segments at all.
871    #[serde(rename = "@valid")]
872    pub valid: Option<bool>,
873    #[serde(rename = "FCS")]
874    pub fcs_list: Vec<Fcs>,
875}
876
877/// Specifies some common information concerning media segments.
878#[skip_serializing_none]
879#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq)]
880#[serde(default)]
881pub struct SegmentBase {
882    #[serde(rename = "@timescale")]
883    pub timescale: Option<u64>,
884    #[serde(rename = "@presentationTimeOffset")]
885    pub presentationTimeOffset: Option<u64>,
886    #[serde(rename = "@indexRange")]
887    pub indexRange: Option<String>,
888    #[serde(rename = "@indexRangeExact")]
889    pub indexRangeExact: Option<bool>,
890    #[serde(rename = "@availabilityTimeOffset", serialize_with="serialize_opt_xsd_double")]
891    pub availabilityTimeOffset: Option<f64>,
892    #[serde(rename = "@availabilityTimeComplete")]
893    pub availabilityTimeComplete: Option<bool>,
894    #[serde(rename = "@presentationDuration")]
895    pub presentationDuration: Option<u64>,
896    /// Indicates a possible offset between media segment start/end points and period start/end points.
897    #[serde(rename = "@eptDelta")]
898    pub eptDelta: Option<i64>,
899    /// Specifies the difference between the presentation duration of this Representation and the
900    /// Period duration. Expressed in units of @timescale.
901    #[serde(rename = "@pdDelta")]
902    pub pbDelta: Option<i64>,
903    pub Initialization: Option<Initialization>,
904    #[serde(rename = "RepresentationIndex")]
905    pub representation_index: Option<RepresentationIndex>,
906    #[serde(rename = "FailoverContent")]
907    pub failover_content: Option<FailoverContent>,
908}
909
910/// The URL of a media segment.
911#[skip_serializing_none]
912#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq, Hash)]
913#[serde(default)]
914pub struct SegmentURL {
915    #[serde(rename = "@media")]
916    pub media: Option<String>, // actually an URI
917    #[serde(rename = "@mediaRange")]
918    pub mediaRange: Option<String>,
919    #[serde(rename = "@index")]
920    pub index: Option<String>, // actually an URI
921    #[serde(rename = "@indexRange")]
922    pub indexRange: Option<String>,
923}
924
925/// Contains a sequence of SegmentURL elements.
926#[skip_serializing_none]
927#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq, Hash)]
928#[serde(default)]
929pub struct SegmentList {
930    // note: the spec says this is an unsigned int, not an xs:duration
931    #[serde(rename = "@duration")]
932    pub duration: Option<u64>,
933    #[serde(rename = "@timescale")]
934    pub timescale: Option<u64>,
935    #[serde(rename = "@indexRange")]
936    pub indexRange: Option<String>,
937    #[serde(rename = "@indexRangeExact")]
938    pub indexRangeExact: Option<bool>,
939    /// A "remote resource", following the XML Linking Language (XLink) specification.
940    #[serde(rename = "@xlink:href", alias = "@href")]
941    pub href: Option<String>,
942    #[serde(rename = "@xlink:actuate", alias = "@actuate", default="default_optstring_on_request")]
943    pub actuate: Option<String>,
944    #[serde(rename = "@xlink:type", alias = "@type")]
945    pub sltype: Option<String>,
946    #[serde(rename = "@xlink:show", alias = "@show")]
947    pub show: Option<String>,
948    pub Initialization: Option<Initialization>,
949    pub SegmentTimeline: Option<SegmentTimeline>,
950    pub BitstreamSwitching: Option<BitstreamSwitching>,
951    #[serde(rename = "SegmentURL")]
952    pub segment_urls: Vec<SegmentURL>,
953}
954
955#[skip_serializing_none]
956#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq)]
957#[serde(default)]
958pub struct Resync {
959    #[serde(rename = "@type")]
960    pub rtype: Option<String>,
961    #[serde(rename = "@dT")]
962    pub dT: Option<u64>,
963    #[serde(rename = "@dImax")]
964    pub dImax: Option<f64>,
965    #[serde(rename = "@dImin")]
966    pub dImin: Option<f64>,
967    #[serde(rename = "@marker")]
968    pub marker: Option<bool>,
969}
970
971/// Specifies information concerning the audio channel (e.g. stereo, multichannel).
972#[skip_serializing_none]
973#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq, Hash)]
974#[serde(default)]
975pub struct AudioChannelConfiguration {
976    #[serde(rename = "@schemeIdUri")]
977    pub schemeIdUri: String,
978    #[serde(rename = "@value")]
979    pub value: Option<String>,
980    #[serde(rename = "@id")]
981    pub id: Option<String>,
982}
983
984// This element is not specified in ISO/IEC 23009-1:2022; exact format is unclear.
985#[skip_serializing_none]
986#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq, Hash)]
987#[serde(default)]
988pub struct Language {
989    #[serde(rename = "$text")]
990    pub content: Option<String>,
991}
992
993/// A Preselection is a personalization option to produce a “complete audio experience”.
994///
995/// Used for audio signaling in the context of the ATSC 3.0 standard for advanced IP-based
996/// television broadcasting. Details are specified by the “DASH-IF Interoperability Point for ATSC
997/// 3.0” document.
998#[skip_serializing_none]
999#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq)]
1000#[serde(default)]
1001pub struct Preselection {
1002    #[serde(rename = "@id", default = "default_optstring_one")]
1003    pub id: Option<String>,
1004    /// Specifies the ids of the contained elements/content components of this Preselection list as
1005    /// white space separated list in processing order. The first id defines the main element.
1006    #[serde(rename = "@preselectionComponents")]
1007    pub preselectionComponents: String,
1008    #[serde(rename = "@lang")]
1009    pub lang: Option<String>,
1010    #[serde(rename = "@audioSamplingRate")]
1011    pub audioSamplingRate: Option<String>,
1012    /// An RFC6381 string, <https://tools.ietf.org/html/rfc6381>
1013    #[serde(rename = "@codecs")]
1014    pub codecs: String,
1015    #[serde(rename = "@selectionPriority")]
1016    pub selectionPriority: Option<u64>,
1017    #[serde(rename = "@tag")]
1018    pub tag: String,
1019    pub FramePacking: Vec<FramePacking>,
1020    pub AudioChannelConfiguration: Vec<AudioChannelConfiguration>,
1021    pub ContentProtection: Vec<ContentProtection>,
1022    pub OutputProtection: Option<OutputProtection>,
1023    #[serde(rename = "EssentialProperty")]
1024    pub essential_property: Vec<EssentialProperty>,
1025    #[serde(rename = "SupplementalProperty")]
1026    pub supplemental_property: Vec<SupplementalProperty>,
1027    pub InbandEventStream: Vec<InbandEventStream>,
1028    pub Switching: Vec<Switching>,
1029    // TODO: missing RandomAccess element
1030    #[serde(rename = "GroupLabel")]
1031    pub group_label: Vec<Label>,
1032    pub Label: Vec<Label>,
1033    pub ProducerReferenceTime: Option<ProducerReferenceTime>,
1034    // TODO: missing ContentPopularityRate element
1035    pub Resync: Option<Resync>,
1036    #[serde(rename = "Accessibility")]
1037    pub accessibilities: Vec<Accessibility>,
1038    #[serde(rename = "Role")]
1039    pub roles: Vec<Role>,
1040    #[serde(rename = "Rating")]
1041    pub ratings: Vec<Rating>,
1042    #[serde(rename = "Viewpoint")]
1043    pub viewpoints: Vec<Viewpoint>,
1044    // end PreselectionType specific elements
1045    #[serde(rename = "Language")]
1046    pub languages: Vec<Language>,
1047}
1048
1049/// Specifies that content is suitable for presentation to audiences for which that rating is known to be
1050/// appropriate, or for unrestricted audiences.
1051#[skip_serializing_none]
1052#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq, Hash)]
1053#[serde(default)]
1054pub struct Rating {
1055    #[serde(rename = "@id")]
1056    pub id: Option<String>,
1057    #[serde(rename = "@schemeIdUri")]
1058    pub schemeIdUri: String,
1059    #[serde(rename = "@value")]
1060    pub value: Option<String>,
1061}
1062
1063/// Specifies frame-packing arrangement information of the video media component type.
1064#[skip_serializing_none]
1065#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq, Hash)]
1066#[serde(default)]
1067pub struct FramePacking {
1068    #[serde(rename = "@id")]
1069    pub id: Option<String>,
1070    #[serde(rename = "@schemeIdUri")]
1071    pub schemeIdUri: String,
1072    #[serde(rename = "@value")]
1073    pub value: Option<String>,
1074}
1075
1076/// Information used to allow Adaptation Set Switching (for instance, allowing the player to switch
1077/// between camera angles).
1078///
1079/// This is different from "bitstream switching".
1080#[skip_serializing_none]
1081#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq, Hash)]
1082#[serde(default)]
1083pub struct Switching {
1084    #[serde(rename = "@interval")]
1085    pub interval: Option<u64>,
1086    /// Valid values are "media" and "bitstream".
1087    #[serde(rename = "@type")]
1088    pub stype: Option<String>,
1089}
1090
1091/// Specifies the accessibility scheme used by the media content.
1092#[skip_serializing_none]
1093#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq, Hash)]
1094#[serde(default)]
1095pub struct Accessibility {
1096    #[serde(rename = "@schemeIdUri")]
1097    pub schemeIdUri: String,
1098    #[serde(rename = "@value")]
1099    pub value: Option<String>,
1100    #[serde(rename = "@id")]
1101    pub id: Option<String>,
1102}
1103
1104/// Scope of a namespace.
1105#[skip_serializing_none]
1106#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq, Hash)]
1107#[serde(default)]
1108pub struct Scope {
1109    #[serde(rename = "@schemeIdUri")]
1110    pub schemeIdUri: String,
1111    #[serde(rename = "@value")]
1112    pub value: Option<String>,
1113    #[serde(rename = "@id")]
1114    pub id: Option<String>,
1115}
1116
1117/// A SubRepresentation contains information that only applies to one media stream in a Representation.
1118#[skip_serializing_none]
1119#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq)]
1120#[serde(default)]
1121pub struct SubRepresentation {
1122    #[serde(rename = "@level")]
1123    pub level: Option<u32>,
1124    #[serde(rename = "@dependencyLevel")]
1125    pub dependencyLevel: Option<String>,
1126    /// If present, a whitespace-separated list of values of ContentComponent@id values.
1127    #[serde(rename = "@contentComponent")]
1128    pub contentComponent: Option<String>,
1129    #[serde(rename = "@mimeType")]
1130    pub mimeType: Option<String>,
1131    /// An RFC6381 string, <https://tools.ietf.org/html/rfc6381>
1132    #[serde(rename = "@codecs")]
1133    pub codecs: Option<String>,
1134    #[serde(rename = "@contentType")]
1135    pub contentType: Option<String>,
1136    #[serde(rename = "@profiles")]
1137    pub profiles: Option<String>,
1138    #[serde(rename = "@segmentProfiles")]
1139    /// Specifies the profiles of Segments that are essential to process the Representation. The
1140    /// semantics depend on the value of the @mimeType attribute.
1141    pub segmentProfiles: Option<String>,
1142    /// If present, this attribute is expected to be set to "progressive".
1143    #[serde(rename = "@scanType")]
1144    pub scanType: Option<String>,
1145    #[serde(rename = "@frameRate")]
1146    pub frameRate: Option<String>, // can be something like "15/2"
1147    /// The Sample Aspect Ratio, eg. "1:1"
1148    #[serde(rename = "@sar")]
1149    pub sar: Option<String>,
1150    /// The average bandwidth of the Representation.
1151    #[serde(rename = "@bandwidth")]
1152    pub bandwidth: Option<u64>,
1153    #[serde(rename = "@audioSamplingRate")]
1154    pub audioSamplingRate: Option<String>,
1155    /// Indicates the possibility for accelerated playout allowed by this codec profile and level.
1156    #[serde(rename = "@maxPlayoutRate", serialize_with="serialize_opt_xsd_double")]
1157    pub maxPlayoutRate: Option<f64>,
1158    #[serde(rename = "@codingDependency")]
1159    pub codingDependency: Option<bool>,
1160    #[serde(rename = "@width")]
1161    pub width: Option<u64>,
1162    #[serde(rename = "@height")]
1163    pub height: Option<u64>,
1164    #[serde(rename = "@startWithSAP")]
1165    pub startWithSAP: Option<u64>,
1166    #[serde(rename = "@maximumSAPPeriod", serialize_with="serialize_opt_xsd_double")]
1167    pub maximumSAPPeriod: Option<f64>,
1168    pub FramePacking: Vec<FramePacking>,
1169    pub AudioChannelConfiguration: Vec<AudioChannelConfiguration>,
1170    pub ContentProtection: Vec<ContentProtection>,
1171    pub OutputProtection: Option<OutputProtection>,
1172    #[serde(rename = "EssentialProperty")]
1173    pub essential_property: Vec<EssentialProperty>,
1174    #[serde(rename = "SupplementalProperty")]
1175    pub supplemental_property: Vec<SupplementalProperty>,
1176    pub InbandEventStream: Vec<InbandEventStream>,
1177    pub Switching: Vec<Switching>,
1178    // TODO: missing RandomAccess element
1179    #[serde(rename = "GroupLabel")]
1180    pub group_label: Vec<Label>,
1181    pub Label: Vec<Label>,
1182    pub ProducerReferenceTime: Option<ProducerReferenceTime>,
1183    // TODO: missing ContentPopularityRate element
1184    pub Resync: Option<Resync>,
1185}
1186
1187/// A Representation describes a version of the content, using a specific encoding and bitrate.
1188///
1189/// Streams often have multiple representations with different bitrates, to allow the client to
1190/// select that most suitable to its network conditions (adaptive bitrate or ABR streaming).
1191#[skip_serializing_none]
1192#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq)]
1193#[serde(default)]
1194pub struct Representation {
1195    // no id for a linked Representation (with xlink:href), so this attribute is optional
1196    #[serde(rename = "@id")]
1197    pub id: Option<String>,
1198    /// The average bandwidth of the Representation.
1199    #[serde(rename = "@bandwidth")]
1200    pub bandwidth: Option<u64>,
1201    /// Specifies a quality ranking of this Representation relative to others in the same
1202    /// AdaptationSet. Lower values represent higher quality content. If not present, then no
1203    /// ranking is defined.
1204    #[serde(rename = "@qualityRanking")]
1205    pub qualityRanking: Option<u8>,
1206    /// Identifies the base layer representation of this enhancement layer representation.
1207    /// Separation between a base layer and a number of enhancement layers is used by certain
1208    /// content encoding mechanisms, such as HEVC Scalable and Dolby Vision.
1209    #[serde(rename = "@dependencyId")]
1210    pub dependencyId: Option<String>,
1211    #[serde(rename = "@associationId")]
1212    pub associationId: Option<String>,
1213    #[serde(rename = "@associationType")]
1214    pub associationType: Option<String>,
1215    #[serde(rename = "@mediaStreamStructureId")]
1216    pub mediaStreamStructureId: Option<String>,
1217    #[serde(rename = "@profiles")]
1218    pub profiles: Option<String>,
1219    #[serde(rename = "@width")]
1220    pub width: Option<u64>,
1221    #[serde(rename = "@height")]
1222    pub height: Option<u64>,
1223    /// The Sample Aspect Ratio, eg. "1:1".
1224    #[serde(rename = "@sar")]
1225    pub sar: Option<String>,
1226    #[serde(rename = "@frameRate")]
1227    pub frameRate: Option<String>, // can be something like "15/2"
1228    #[serde(rename = "@audioSamplingRate")]
1229    pub audioSamplingRate: Option<String>,
1230    // The specification says that @mimeType is mandatory, but it's not always present on
1231    // akamaized.net MPDs
1232    #[serde(rename = "@mimeType")]
1233    pub mimeType: Option<String>,
1234    /// Specifies the profiles of Segments that are essential to process the Representation. The
1235    /// semantics depend on the value of the @mimeType attribute.
1236    #[serde(rename = "@segmentProfiles")]
1237    pub segmentProfiles: Option<String>,
1238    /// A "remote resource", following the XML Linking Language (XLink) specification.
1239    /// An RFC6381 string, <https://tools.ietf.org/html/rfc6381>
1240    #[serde(rename = "@codecs")]
1241    pub codecs: Option<String>,
1242    #[serde(rename = "@containerProfiles")]
1243    pub containerProfiles: Option<String>,
1244    #[serde(rename = "@maximumSAPPeriod")]
1245    pub maximumSAPPeriod: Option<f64>,
1246    #[serde(rename = "@startWithSAP")]
1247    pub startWithSAP: Option<u64>,
1248    /// Indicates the possibility for accelerated playout allowed by this codec profile and level.
1249    #[serde(rename = "@maxPlayoutRate", serialize_with="serialize_opt_xsd_double")]
1250    pub maxPlayoutRate: Option<f64>,
1251    #[serde(rename = "@codingDependency")]
1252    pub codingDependency: Option<bool>,
1253    /// If present, this attribute is expected to be set to "progressive".
1254    #[serde(rename = "@scanType")]
1255    pub scanType: Option<String>,
1256    #[serde(rename = "@selectionPriority")]
1257    pub selectionPriority: Option<u64>,
1258    #[serde(rename = "@tag")]
1259    pub tag: Option<String>,
1260    #[serde(rename = "@contentType")]
1261    pub contentType: Option<String>,
1262    /// Language in RFC 5646 format.
1263    #[serde(rename = "@lang")]
1264    pub lang: Option<String>,
1265    #[serde(rename = "@sampleRate")]
1266    pub sampleRate: Option<u64>,
1267    #[serde(rename = "@numChannels")]
1268    pub numChannels: Option<u32>,
1269    #[serde(rename = "@xlink:href", alias = "@href")]
1270    pub href: Option<String>,
1271    #[serde(rename = "@xlink:actuate", alias = "@actuate", default = "default_optstring_on_request")]
1272    pub actuate: Option<String>,
1273    #[serde(rename = "@scte214:supplementalProfiles", alias = "@supplementalProfiles")]
1274    pub scte214_supplemental_profiles: Option<String>,
1275    #[serde(rename = "@scte214:supplementalCodecs", alias = "@supplementalCodecs")]
1276    pub scte214_supplemental_codecs: Option<String>,
1277    pub FramePacking: Vec<FramePacking>,
1278    pub AudioChannelConfiguration: Vec<AudioChannelConfiguration>,
1279    pub ContentProtection: Vec<ContentProtection>,
1280    pub OutputProtection: Option<OutputProtection>,
1281    #[serde(rename = "EssentialProperty")]
1282    pub essential_property: Vec<EssentialProperty>,
1283    #[serde(rename = "SupplementalProperty")]
1284    pub supplemental_property: Vec<SupplementalProperty>,
1285    pub InbandEventStream: Vec<InbandEventStream>,
1286    pub Switching: Vec<Switching>,
1287    // TODO: missing RandomAccess element
1288    #[serde(rename = "GroupLabel")]
1289    pub group_label: Vec<Label>,
1290    pub Label: Vec<Label>,
1291    pub ProducerReferenceTime: Vec<ProducerReferenceTime>,
1292    // TODO: missing ContentPopularityRate element
1293    pub Resync: Vec<Resync>,
1294    pub BaseURL: Vec<BaseURL>,
1295    // TODO: missing ExtendedBandwidth element
1296    pub SubRepresentation: Vec<SubRepresentation>,
1297    pub SegmentBase: Option<SegmentBase>,
1298    pub SegmentList: Option<SegmentList>,
1299    pub SegmentTemplate: Option<SegmentTemplate>,
1300    #[serde(rename = "RepresentationIndex")]
1301    pub representation_index: Option<RepresentationIndex>,
1302}
1303
1304/// Describes a media content component.
1305#[skip_serializing_none]
1306#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq, Hash)]
1307#[serde(default)]
1308pub struct ContentComponent {
1309    #[serde(rename = "@id")]
1310    pub id: Option<String>,
1311    /// Language in RFC 5646 format (eg. "fr-FR", "en-AU").
1312    #[serde(rename = "@lang")]
1313    pub lang: Option<String>,
1314    #[serde(rename = "@contentType")]
1315    pub contentType: Option<String>,
1316    #[serde(rename = "@par")]
1317    pub par: Option<String>,
1318    #[serde(rename = "@tag")]
1319    pub tag: Option<String>,
1320    pub Accessibility: Vec<Accessibility>,
1321    pub Role: Vec<Role>,
1322    pub Rating: Vec<Rating>,
1323    pub Viewpoint: Vec<Viewpoint>,
1324}
1325
1326/// A Common Encryption "Protection System Specific Header" box. Content is typically base64 encoded.
1327#[skip_serializing_none]
1328#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq, Hash)]
1329#[serde(default)]
1330pub struct CencPssh {
1331    #[serde(rename = "$text")]
1332    pub content: Option<String>,
1333}
1334
1335/// Licence acquisition URL for content using Microsoft PlayReady DRM.
1336#[skip_serializing_none]
1337#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq, Hash)]
1338#[serde(default)]
1339pub struct Laurl {
1340    #[serde(rename = "@Lic_type")]
1341    pub lic_type: Option<String>,
1342    #[serde(rename = "$text")]
1343    pub content: Option<String>,
1344}
1345
1346/// Initialization data that is specific to the Microsoft PlayReady DRM.
1347#[skip_serializing_none]
1348#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq, Hash)]
1349#[serde(default)]
1350pub struct MsprPro {
1351    #[serde(rename = "@xmlns", serialize_with="serialize_xmlns")]
1352    pub xmlns: Option<String>,
1353    #[serde(rename = "$text")]
1354    pub content: Option<String>,
1355}
1356
1357#[skip_serializing_none]
1358#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq, Hash)]
1359#[serde(default)]
1360pub struct MsprIsEncrypted {
1361    #[serde(rename = "$text")]
1362    pub content: Option<String>,
1363}
1364
1365#[skip_serializing_none]
1366#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq, Hash)]
1367#[serde(default)]
1368pub struct MsprIVSize {
1369    #[serde(rename = "$text")]
1370    pub content: Option<String>,
1371}
1372
1373#[skip_serializing_none]
1374#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq, Hash)]
1375#[serde(default)]
1376pub struct MsprKid {
1377    #[serde(rename = "$text")]
1378    pub content: Option<String>,
1379}
1380
1381#[skip_serializing_none]
1382#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq, Hash)]
1383#[serde(default)]
1384pub struct OutputProtection {
1385    #[serde(rename = "@schemeIdUri")]
1386    pub schemeIdUri: String,
1387    #[serde(rename = "@value")]
1388    pub value: Option<String>,
1389    #[serde(rename = "@id")]
1390    pub id: Option<String>,
1391}
1392
1393/// Contains information on DRM (rights management / encryption) mechanisms used in the stream.
1394///
1395/// If this node is not present, no content protection (such as Widevine and Playready) is applied
1396/// by the source.
1397#[skip_serializing_none]
1398#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq, Hash)]
1399#[serde(default)]
1400pub struct ContentProtection {
1401    /// The robustness level required for this content protection scheme.
1402    #[serde(rename = "@robustness")]
1403    pub robustness: Option<String>,
1404    #[serde(rename = "@refId")]
1405    pub refId: Option<String>,
1406    /// An xs:IDREF that references an identifier in this MPD.
1407    #[serde(rename = "@ref")]
1408    pub r#ref: Option<String>,
1409    /// References an identifier in this MPD.
1410    #[serde(rename = "@schemeIdUri")]
1411    pub schemeIdUri: String,
1412    #[serde(rename = "@value")]
1413    pub value: Option<String>,
1414    #[serde(rename = "@id")]
1415    pub id: Option<String>,
1416    /// The DRM initialization data (Protection System Specific Header).
1417    #[serde(rename="cenc:pssh", alias="pssh")]
1418    pub cenc_pssh: Vec<CencPssh>,
1419    /// The DRM key identifier.
1420    #[serde(rename = "@cenc:default_KID", alias = "@default_KID")]
1421    pub default_KID: Option<String>,
1422    /// License acquisition URL.
1423    #[serde(rename = "dashif:laurl", alias = "laurl")]
1424    pub laurl: Option<Laurl>,
1425    /// License acquisition URL. The name clearkey:Laurl is obsolete and replaced by dashif:laurl.
1426    /// Some manifests in the wild include both, and the parser does not allow for duplicate fields,
1427    /// so we need to allow for this field using a distinct name.
1428    #[serde(rename = "clearkey:Laurl", alias = "Laurl")]
1429    pub clearkey_laurl: Option<Laurl>,
1430    /// Content specific to initialization data using Microsoft PlayReady DRM.
1431    #[serde(rename = "mspr:pro", alias = "pro")]
1432    pub msprpro: Option<MsprPro>,
1433    #[serde(rename = "mspr:IsEncrypted", alias = "IsEncrypted")]
1434    pub mspr_is_encrypted: Option<MsprIsEncrypted>,
1435    #[serde(rename = "mspr:IV_Size", alias = "IV_Size")]
1436    pub mspr_iv_size: Option<MsprIVSize>,
1437    #[serde(rename = "mspr:kid", alias = "kid")]
1438    pub mspr_kid: Option<MsprKid>,
1439}
1440
1441/// The Role specifies the purpose of this media stream (caption, subtitle, main content, etc.).
1442///
1443/// Possible values include "caption", "subtitle", "main", "alternate", "supplementary",
1444/// "commentary", and "dub" (this is the attribute scheme for @value when the schemeIdUri is
1445/// "urn:mpeg:dash:role:2011").
1446#[skip_serializing_none]
1447#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq, Hash)]
1448#[serde(default)]
1449pub struct Role {
1450    #[serde(rename = "@id")]
1451    pub id: Option<String>,
1452    #[serde(rename = "@schemeIdUri")]
1453    pub schemeIdUri: String,
1454    #[serde(rename = "@value")]
1455    pub value: Option<String>,
1456}
1457
1458#[skip_serializing_none]
1459#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq, Hash)]
1460#[serde(default)]
1461pub struct Viewpoint {
1462    #[serde(rename = "@id")]
1463    pub id: Option<String>,
1464    #[serde(rename = "@schemeIdUri")]
1465    pub schemeIdUri: String,
1466    #[serde(rename = "@value")]
1467    pub value: Option<String>,
1468}
1469
1470#[skip_serializing_none]
1471#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq, Hash)]
1472#[serde(default)]
1473pub struct Selection {
1474    #[serde(rename = "@dataEncoding")]
1475    pub dataEncoding: Option<String>,
1476    #[serde(rename = "@parameter")]
1477    pub parameter: Option<String>,
1478    #[serde(rename = "@data")]
1479    pub data: Option<String>,
1480}
1481
1482#[skip_serializing_none]
1483#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq, Hash)]
1484#[serde(default)]
1485pub struct SelectionInfo {
1486    #[serde(rename = "@selectionInfo")]
1487    pub selectionInfo: Option<String>,
1488    #[serde(rename = "@contactURL")]
1489    pub contactURL: Option<String>,
1490    pub Selection: Vec<Selection>,
1491}
1492
1493/// A mechanism allowing the server to send additional information to the DASH client which is
1494/// synchronized with the media stream.
1495///
1496/// DASH Events are Used for various purposes such as dynamic ad insertion, providing additional
1497/// metainformation concerning the actors or location at a point in the media stream, providing
1498/// parental guidance information, or sending custom data to the DASH player application.
1499#[skip_serializing_none]
1500#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq)]
1501#[serde(default)]
1502pub struct Event {
1503    #[serde(rename = "@id")]
1504    pub id: Option<String>,
1505    #[serde(rename = "@presentationTime", default = "default_optu64_zero")]
1506    pub presentationTime: Option<u64>,
1507    #[serde(rename = "@presentationTimeOffset")]
1508    pub presentationTimeOffset: Option<u64>,
1509    #[serde(rename = "@duration")]
1510    pub duration: Option<u64>,
1511    #[serde(rename = "@timescale")]
1512    pub timescale: Option<u64>,
1513    /// Possible encoding (e.g. "base64") for the Event content or the value of the @messageData
1514    /// attribute.
1515    #[serde(rename = "@contentEncoding")]
1516    pub contentEncoding: Option<String>,
1517    /// The value for this event stream element. This attribute is present for backward
1518    /// compatibility; message content should be included in the Event element instead.
1519    #[serde(rename = "@messageData")]
1520    pub messageData: Option<String>,
1521    pub SelectionInfo: Option<SelectionInfo>,
1522    #[cfg(feature = "scte35")]
1523    #[serde(rename = "scte35:Signal", alias="Signal")]
1524    #[cfg(feature = "scte35")]
1525    pub signal: Vec<Signal>,
1526    #[cfg(feature = "scte35")]
1527    #[serde(rename = "scte35:SpliceInfoSection", alias="SpliceInfoSection")]
1528    #[cfg(feature = "scte35")]
1529    pub splice_info_section: Vec<SpliceInfoSection>,
1530    // #[serde(rename = "@schemeIdUri")]
1531    // pub schemeIdUri: String,
1532    #[serde(rename = "@value")]
1533    pub value: Option<String>,
1534    // The content may be base64 encoded, but may also be text. See for example
1535    // https://refapp.hbbtv.org/videos/00_llama_multiperiod_v1/manifest.mpd
1536    #[serde(rename = "$text")]
1537    pub content: Option<String>,
1538}
1539
1540#[skip_serializing_none]
1541#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq)]
1542#[serde(default)]
1543pub struct EventStream {
1544    #[serde(rename = "@xlink:href")]
1545    #[serde(alias = "@href")]
1546    pub href: Option<String>,
1547    #[serde(rename = "@xlink:actuate", alias = "@actuate", default = "default_optstring_on_request")]
1548    pub actuate: Option<String>,
1549    #[serde(rename = "@messageData")]
1550    // actually an xs:anyURI
1551    pub messageData: Option<String>,
1552    #[serde(rename = "@schemeIdUri")]
1553    pub schemeIdUri: String,
1554    #[serde(rename = "@value")]
1555    pub value: Option<String>,
1556    #[serde(rename = "@timescale")]
1557    pub timescale: Option<u64>,
1558    #[serde(rename = "@presentationTimeOffset")]
1559    pub presentationTimeOffset: Option<u64>,
1560    #[serde(rename = "Event")]
1561    pub event: Vec<Event>,
1562}
1563
1564/// "Inband" events are materialized by the presence of DASHEventMessageBoxes (emsg) in the media
1565/// segments.
1566///
1567/// The client is informed of their presence by the inclusion of an InbandEventStream element in the
1568/// AdaptationSet or Representation element.
1569#[skip_serializing_none]
1570#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq)]
1571#[serde(default)]
1572pub struct InbandEventStream {
1573    #[serde(rename = "@timescale")]
1574    pub timescale: Option<u64>,
1575    #[serde(rename = "@schemeIdUri")]
1576    pub schemeIdUri: String,
1577    #[serde(rename = "Event")]
1578    pub event: Vec<Event>,
1579    #[serde(rename = "@value")]
1580    pub value: Option<String>,
1581    /// A "remote resource", following the XML Linking Language (XLink) specification.
1582    #[serde(rename = "@xlink:href")]
1583    #[serde(alias = "@href")]
1584    pub href: Option<String>,
1585    #[serde(rename = "@xlink:actuate", alias = "@actuate")]
1586    pub actuate: Option<String>,
1587}
1588
1589#[skip_serializing_none]
1590#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq, Hash)]
1591#[serde(default)]
1592pub struct EssentialProperty {
1593    #[serde(rename = "@id")]
1594    pub id: Option<String>,
1595    #[serde(rename = "@schemeIdUri")]
1596    pub schemeIdUri: String,
1597    #[serde(rename = "@value")]
1598    pub value: Option<String>,
1599}
1600
1601#[skip_serializing_none]
1602#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq, Hash)]
1603#[serde(default)]
1604pub struct SupplementalProperty {
1605    #[serde(rename = "@id")]
1606    pub id: Option<String>,
1607    #[serde(rename = "@schemeIdUri")]
1608    pub schemeIdUri: String,
1609    #[serde(rename = "@value")]
1610    pub value: Option<String>,
1611    #[serde(rename(serialize = "scte214:ContentIdentifier"))]
1612    #[serde(rename(deserialize = "ContentIdentifier"))]
1613    pub scte214ContentIdentifiers: Vec<Scte214ContentIdentifier>,
1614}
1615
1616/// Provides a textual description of the content, which can be used by the client to allow
1617/// selection of the desired media stream.
1618#[skip_serializing_none]
1619#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq, Hash)]
1620#[serde(default)]
1621pub struct Label {
1622    #[serde(rename = "@id")]
1623    pub id: Option<String>,
1624    #[serde(rename = "@lang")]
1625    pub lang: Option<String>,
1626    #[serde(rename = "$text")]
1627    pub content: String,
1628}
1629
1630/// Contains a set of Representations.
1631///
1632/// For example, if multiple language streams are available for the audio content, each one can be
1633/// in its own AdaptationSet. DASH implementation guidelines indicate that "representations in the
1634/// same video adaptation set should be alternative encodings of the same source content, encoded
1635/// such that switching between them does not produce visual glitches due to picture size or aspect
1636/// ratio differences".
1637#[skip_serializing_none]
1638#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq)]
1639#[serde(default)]
1640pub struct AdaptationSet {
1641    #[serde(rename = "@id")]
1642    pub id: Option<String>,
1643    /// A "remote resource", following the XML Linking Language (XLink) specification.
1644    #[serde(rename = "@xlink:href", alias = "@href")]
1645    pub href: Option<String>,
1646    #[serde(rename = "@xlink:actuate", alias = "@actuate", default = "default_optstring_on_request")]
1647    pub actuate: Option<String>,
1648    #[serde(rename = "@group")]
1649    pub group: Option<i64>,
1650    #[serde(rename = "@selectionPriority")]
1651    pub selectionPriority: Option<u64>,
1652    // e.g. "audio", "video", "text"
1653    #[serde(rename = "@contentType")]
1654    pub contentType: Option<String>,
1655    #[serde(rename = "@profiles")]
1656    pub profiles: Option<String>,
1657    /// Content language, in RFC 5646 format.
1658    #[serde(rename = "@lang")]
1659    pub lang: Option<String>,
1660    /// The Sample Aspect Ratio, eg. "1:1".
1661    #[serde(rename = "@sar")]
1662    pub sar: Option<String>,
1663    /// The Pixel Aspect Ratio, eg. "16:9".
1664    #[serde(rename = "@par")]
1665    pub par: Option<String>,
1666    /// If present, this attribute is expected to be set to "progressive".
1667    #[serde(rename = "@scanType")]
1668    pub scanType: Option<String>,
1669    #[serde(rename = "@segmentAlignment")]
1670    pub segmentAlignment: Option<bool>,
1671    #[serde(rename = "@segmentProfiles")]
1672    /// Specifies the profiles of Segments that are essential to process the Representation. The
1673    /// semantics depend on the value of the @mimeType attribute.
1674    pub segmentProfiles: Option<String>,
1675    #[serde(rename = "@subsegmentAlignment")]
1676    pub subsegmentAlignment: Option<bool>,
1677    #[serde(rename = "@subsegmentStartsWithSAP")]
1678    pub subsegmentStartsWithSAP: Option<u64>,
1679    #[serde(rename = "@bitstreamSwitching")]
1680    pub bitstreamSwitching: Option<bool>,
1681    #[serde(rename = "@audioSamplingRate")]
1682    pub audioSamplingRate: Option<String>,
1683    #[serde(rename = "@width")]
1684    pub width: Option<u64>,
1685    #[serde(rename = "@height")]
1686    pub height: Option<u64>,
1687    // eg "video/mp4"
1688    #[serde(rename = "@mimeType")]
1689    pub mimeType: Option<String>,
1690    /// An RFC6381 string, <https://tools.ietf.org/html/rfc6381> (eg. "avc1.4D400C").
1691    #[serde(rename = "@codecs")]
1692    pub codecs: Option<String>,
1693    #[serde(rename = "@minBandwidth")]
1694    pub minBandwidth: Option<u64>,
1695    #[serde(rename = "@maxBandwidth")]
1696    pub maxBandwidth: Option<u64>,
1697    #[serde(rename = "@minWidth")]
1698    pub minWidth: Option<u64>,
1699    #[serde(rename = "@maxWidth")]
1700    pub maxWidth: Option<u64>,
1701    #[serde(rename = "@minHeight")]
1702    pub minHeight: Option<u64>,
1703    #[serde(rename = "@maxHeight")]
1704    pub maxHeight: Option<u64>,
1705    #[serde(rename = "@frameRate")]
1706    pub frameRate: Option<String>, // it can be something like "15/2"
1707    #[serde(rename = "@minFrameRate")]
1708    pub minFrameRate: Option<String>, // it can be something like "15/2"
1709    #[serde(rename = "@maxFrameRate")]
1710    pub maxFrameRate: Option<String>, // it can be something like "15/2"
1711    /// Indicates the possibility for accelerated playout allowed by this codec profile and level.
1712    #[serde(rename = "@maxPlayoutRate", serialize_with="serialize_opt_xsd_double")]
1713    pub maxPlayoutRate: Option<f64>,
1714    #[serde(rename = "@maximumSAPPeriod", serialize_with="serialize_opt_xsd_double")]
1715    pub maximumSAPPeriod: Option<f64>,
1716    #[serde(rename = "@startWithSAP")]
1717    pub startWithSAP: Option<u64>,
1718    #[serde(rename = "@codingDependency")]
1719    pub codingDependency: Option<bool>,
1720    pub FramePacking: Vec<FramePacking>,
1721    pub AudioChannelConfiguration: Vec<AudioChannelConfiguration>,
1722    pub ContentProtection: Vec<ContentProtection>,
1723    // TODO OutputProtection element
1724    #[serde(rename = "EssentialProperty")]
1725    pub essential_property: Vec<EssentialProperty>,
1726    #[serde(rename = "SupplementalProperty")]
1727    pub supplemental_property: Vec<SupplementalProperty>,
1728    pub InbandEventStream: Vec<InbandEventStream>,
1729    pub Switching: Vec<Switching>,
1730    // TODO RandomAccess element
1731    pub GroupLabel: Vec<Label>,
1732    pub Label: Vec<Label>,
1733    pub ProducerReferenceTime: Vec<ProducerReferenceTime>,
1734    // TODO ContentPopularityRate element
1735    pub Resync: Vec<Resync>,
1736    pub Accessibility: Vec<Accessibility>,
1737    pub Role: Vec<Role>,
1738    pub Rating: Vec<Rating>,
1739    pub Viewpoint: Vec<Viewpoint>,
1740    pub ContentComponent: Vec<ContentComponent>,
1741    pub BaseURL: Vec<BaseURL>,
1742    pub SegmentBase: Option<SegmentBase>,
1743    pub SegmentList: Option<SegmentList>,
1744    pub SegmentTemplate: Option<SegmentTemplate>,
1745    #[serde(rename = "Representation")]
1746    pub representations: Vec<Representation>,
1747    #[serde(rename = "@scte214:supplementalProfiles", alias = "@supplementalProfiles")]
1748    pub scte214_supplemental_profiles: Option<String>,
1749    #[serde(rename = "@scte214:supplementalCodecs", alias = "@supplementalCodecs")]
1750    pub scte214_supplemental_codecs: Option<String>,
1751}
1752
1753/// Identifies the asset to which a given Period belongs.
1754///
1755/// Can be used to implement client functionality that depends on distinguishing between ads and
1756/// main content.
1757#[skip_serializing_none]
1758#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq, Hash)]
1759#[serde(default)]
1760pub struct AssetIdentifier {
1761    #[serde(rename = "@schemeIdUri")]
1762    pub schemeIdUri: String,
1763    #[serde(rename = "@value")]
1764    pub value: Option<String>,
1765    #[serde(rename(serialize = "scte214:ContentIdentifier"))]
1766    #[serde(rename(deserialize = "ContentIdentifier"))]
1767    pub scte214ContentIdentifiers: Vec<Scte214ContentIdentifier>,
1768}
1769
1770/// Subsets provide a mechanism to restrict the combination of active Adaptation Sets.
1771///
1772/// An active Adaptation Set is one for which the DASH Client is presenting at least one of the
1773/// contained Representations.
1774#[skip_serializing_none]
1775#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq, Hash)]
1776#[serde(default)]
1777pub struct Subset {
1778    #[serde(rename = "@id")]
1779    pub id: Option<String>,
1780    /// Specifies the AdaptationSets contained in a Subset by providing a whitespace separated
1781    /// list of the @id values of the contained AdaptationSets.
1782    #[serde(rename = "@contains",
1783            deserialize_with = "deserialize_xsd_uintvector",
1784            serialize_with = "serialize_xsd_uintvector",
1785            default)]
1786    pub contains: Vec<u64>,
1787}
1788
1789/// Describes a chunk of the content with a start time and a duration. Content can be split up into
1790/// multiple periods (such as chapters, advertising segments).
1791#[skip_serializing_none]
1792#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq)]
1793#[serde(default)]
1794pub struct Period {
1795    /// A "remote resource", following the XML Linking Language (XLink) specification.
1796    #[serde(rename = "@xlink:href", alias = "@href")]
1797    pub href: Option<String>,
1798
1799    #[serde(rename = "@xlink:actuate", alias = "@actuate", default="default_optstring_on_request")]
1800    pub actuate: Option<String>,
1801
1802    #[serde(rename = "@id")]
1803    pub id: Option<String>,
1804
1805    /// The start time of the Period relative to the MPD availability start time.
1806    #[serde(rename = "@start",
1807            serialize_with = "serialize_xs_duration",
1808            deserialize_with = "deserialize_xs_duration",
1809            default)]
1810    pub start: Option<Duration>,
1811
1812    // note: the spec says that this is an xs:duration, not an unsigned int as for other "duration" fields
1813    #[serde(rename = "@duration",
1814            serialize_with = "serialize_xs_duration",
1815            deserialize_with = "deserialize_xs_duration",
1816            default)]
1817    pub duration: Option<Duration>,
1818
1819    // The default for the bitstreamSwitching attribute is specified to be "false".
1820    #[serde(rename = "@bitstreamSwitching", default)]
1821    pub bitstreamSwitching: Option<bool>,
1822
1823    pub BaseURL: Vec<BaseURL>,
1824
1825    pub SegmentBase: Option<SegmentBase>,
1826
1827    pub SegmentList: Option<SegmentList>,
1828
1829    pub SegmentTemplate: Option<SegmentTemplate>,
1830
1831    #[serde(rename = "AssetIdentifier")]
1832    pub asset_identifier: Option<AssetIdentifier>,
1833
1834    #[serde(rename = "EventStream")]
1835    pub event_streams: Vec<EventStream>,
1836
1837    #[serde(rename = "ServiceDescription")]
1838    pub service_description: Vec<ServiceDescription>,
1839
1840    pub ContentProtection: Vec<ContentProtection>,
1841
1842    #[serde(rename = "AdaptationSet")]
1843    pub adaptations: Vec<AdaptationSet>,
1844
1845    #[serde(rename = "Subset")]
1846    pub subsets: Vec<Subset>,
1847
1848    #[serde(rename = "SupplementalProperty")]
1849    pub supplemental_property: Vec<SupplementalProperty>,
1850
1851    #[serde(rename = "EmptyAdaptationSet")]
1852    pub empty_adaptations: Vec<AdaptationSet>,
1853
1854    #[serde(rename = "GroupLabel")]
1855    pub group_label: Vec<Label>,
1856
1857    #[serde(rename = "Preselection")]
1858    pub pre_selections: Vec<Preselection>,
1859
1860    #[serde(rename = "EssentialProperty")]
1861    pub essential_property: Vec<EssentialProperty>,
1862}
1863
1864#[skip_serializing_none]
1865#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq, Hash)]
1866#[serde(default)]
1867pub struct Reporting {
1868    #[serde(rename = "@id")]
1869    pub id: Option<String>,
1870    #[serde(rename = "@schemeIdUri")]
1871    pub schemeIdUri: String,
1872    #[serde(rename = "@value")]
1873    pub value: Option<String>,
1874    #[serde(rename = "@dvb:reportingUrl", alias = "@reportingUrl")]
1875    pub reportingUrl: Option<String>,
1876    #[serde(rename = "@dvb:probability", alias = "@probability")]
1877    pub probability: Option<u64>,
1878}
1879
1880#[skip_serializing_none]
1881#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq, Hash)]
1882#[serde(default)]
1883pub struct Range {
1884    #[serde(rename = "@starttime",
1885            serialize_with = "serialize_xs_duration",
1886            deserialize_with = "deserialize_xs_duration",
1887            default)]
1888    pub starttime: Option<Duration>,
1889    #[serde(rename = "@duration",
1890            serialize_with = "serialize_xs_duration",
1891            deserialize_with = "deserialize_xs_duration",
1892            default)]
1893    pub duration: Option<Duration>,
1894}
1895
1896#[skip_serializing_none]
1897#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq, Hash)]
1898#[serde(default)]
1899pub struct Metrics {
1900    #[serde(rename = "@metrics")]
1901    pub metrics: String,
1902    pub Reporting: Vec<Reporting>,
1903    pub Range: Vec<Range>,
1904}
1905
1906/// Service Description Latency.
1907#[skip_serializing_none]
1908#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq)]
1909#[serde(default)]
1910pub struct Latency {
1911    #[serde(rename = "@min", serialize_with="serialize_opt_xsd_double")]
1912    pub min: Option<f64>,
1913    #[serde(rename = "@max", serialize_with="serialize_opt_xsd_double")]
1914    pub max: Option<f64>,
1915    #[serde(rename = "@target", serialize_with="serialize_opt_xsd_double")]
1916    pub target: Option<f64>,
1917    #[serde(rename = "@referenceId")]
1918    pub referenceId: Option<String>,
1919}
1920
1921/// Service Description Playback Rate.
1922#[skip_serializing_none]
1923#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq)]
1924#[serde(default)]
1925pub struct PlaybackRate {
1926    #[serde(rename = "@min", serialize_with="serialize_opt_xsd_double")]
1927    pub min: Option<f64>,
1928    #[serde(rename = "@max", serialize_with="serialize_opt_xsd_double")]
1929    pub max: Option<f64>,
1930}
1931
1932/// Service Description Operating Quality.
1933#[skip_serializing_none]
1934#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq)]
1935#[serde(default)]
1936pub struct OperatingQuality {
1937    #[serde(default = "default_optstring_any")]
1938    pub mediaType: Option<String>,
1939    #[serde(rename = "@min")]
1940    pub min: Option<u64>,
1941    #[serde(rename = "@max")]
1942    pub max: Option<u64>,
1943    #[serde(rename = "@target")]
1944    pub target: Option<u64>,
1945    #[serde(rename = "@type")]
1946    pub _type: Option<String>,
1947    #[serde(rename = "@maxDifference")]
1948    pub maxDifference: Option<u64>,
1949}
1950
1951///Service Description Operating Bandwidth.
1952#[skip_serializing_none]
1953#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq)]
1954#[serde(default)]
1955pub struct OperatingBandwidth {
1956    #[serde(rename = "@mediaType", default = "default_optstring_any")]
1957    pub mediaType: Option<String>,
1958    #[serde(rename = "@min")]
1959    pub min: Option<u64>,
1960    #[serde(rename = "@max")]
1961    pub max: Option<u64>,
1962    #[serde(rename = "@target")]
1963    pub target: Option<u64>,
1964}
1965
1966#[skip_serializing_none]
1967#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq)]
1968#[serde(default)]
1969pub struct ContentSteering {
1970    #[serde(rename = "@defaultServiceLocation")]
1971    pub defaultServiceLocation: Option<String>,
1972    #[serde(rename = "@queryBeforeStart", default = "default_optbool_false")]
1973    pub queryBeforeStart: Option<bool>,
1974    #[serde(rename = "@clientRequirement", default = "default_optbool_true")]
1975    pub clientRequirement: Option<bool>,
1976}
1977
1978#[skip_serializing_none]
1979#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq)]
1980#[serde(default)]
1981pub struct CMCDParameters {
1982    #[serde(rename = "@version", default = "default_optu64_one")]
1983    pub version: Option<u64>,
1984    #[serde(rename = "@mode", default = "default_optstring_query")]
1985    pub mode: Option<String>,
1986    #[serde(rename = "@includeInRequests", default = "default_optstring_segment")]
1987    pub includeInRequests: Option<String>,
1988    #[serde(rename = "@keys")]
1989    pub keys: String,
1990    #[serde(rename = "@contentID")]
1991    pub contentID: Option<String>,
1992    #[serde(rename = "@sessionID")]
1993    pub sessionID: Option<String>,
1994}
1995
1996/// Generic Recording System descriptor.
1997#[skip_serializing_none]
1998#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq)]
1999#[serde(default)]
2000pub struct ClientDataReporting {
2001    pub CMCDParameters: Vec<CMCDParameters>,
2002    #[serde(rename = "@serviceLocations")]
2003    pub serviceLocations: Option<String>,
2004    #[serde(rename = "@adaptationSets")]
2005    pub adaptationSets: Option<String>,
2006}
2007
2008#[skip_serializing_none]
2009#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq)]
2010#[serde(default)]
2011pub struct PlaybackRestrictions {
2012    #[serde(rename = "@skipAfter",
2013            serialize_with = "serialize_xs_duration",
2014            deserialize_with = "deserialize_xs_duration",
2015            default)]
2016    pub skipAfter: Option<Duration>,
2017}
2018
2019#[skip_serializing_none]
2020#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq)]
2021#[serde(default)]
2022pub struct ServiceDescription {
2023    #[serde(rename = "Scope")]
2024    pub scopes: Vec<Scope>,
2025    pub Latency: Vec<Latency>,
2026    pub PlaybackRate: Vec<PlaybackRate>,
2027    pub OperatingQuality: Vec<OperatingQuality>,
2028    pub OperatingBandwidth: Vec<OperatingBandwidth>,
2029    pub ContentSteering: Vec<ContentSteering>,
2030    pub ClientDataReporting: Vec<ClientDataReporting>,
2031    pub PlaybackRestrictions: Vec<PlaybackRestrictions>,
2032    #[serde(rename = "@id")]
2033    pub id: Option<String>,
2034}
2035
2036/// Used to synchronize the clocks of the DASH client and server, to allow low-latency streaming.
2037#[skip_serializing_none]
2038#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq, Hash)]
2039#[serde(default)]
2040pub struct UTCTiming {
2041    #[serde(rename = "@id")]
2042    pub id: Option<String>,
2043    // prefixed with urn:mpeg:dash:utc, one of http-xsdate:2014, http-iso:2014,
2044    // http-ntp:2014, ntp:2014, http-head:2014, direct:2014
2045    #[serde(rename = "@schemeIdUri")]
2046    pub schemeIdUri: String,
2047    #[serde(rename = "@value")]
2048    pub value: Option<String>,
2049}
2050
2051/// Specifies wall‐clock times at which media fragments were produced.
2052///
2053/// This information helps clients consume the fragments at the same rate at which they were
2054/// produced. Used by the low-latency streaming extensions to DASH.
2055#[skip_serializing_none]
2056#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq, Hash)]
2057#[serde(default)]
2058pub struct ProducerReferenceTime {
2059    // This attribute is required according to the specification XSD.
2060    #[serde(rename = "@id")]
2061    pub id: Option<String>,
2062    #[serde(rename = "@inband", default = "default_optbool_false")]
2063    pub inband: Option<bool>,
2064    // This attribute is required according to the specification XSD.
2065    #[serde(rename = "@presentationTime")]
2066    pub presentationTime: Option<u64>,
2067    #[serde(rename = "@type", default = "default_optstring_encoder")]
2068    pub prtType: Option<String>,
2069    // There are two capitalizations for this attribute in the specification at
2070    // https://dashif.org/docs/CR-Low-Latency-Live-r8.pdf. The attribute is required according to
2071    // the specification XSD.
2072    #[serde(rename = "@wallClockTime",
2073            alias="@wallclockTime",
2074            deserialize_with = "deserialize_xs_datetime",
2075            default)]
2076    pub wallClockTime: Option<XsDatetime>,
2077    pub UTCTiming: Option<UTCTiming>,
2078}
2079
2080#[skip_serializing_none]
2081#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq, Hash)]
2082#[serde(default)]
2083pub struct LeapSecondInformation {
2084    #[serde(rename = "@availabilityStartLeapOffset")]
2085    pub availabilityStartLeapOffset: Option<i64>,
2086    #[serde(rename = "@nextAvailabilityStartLeapOffset")]
2087    pub nextAvailabilityStartLeapOffset: Option<i64>,
2088    #[serde(rename = "@nextLeapChangeTime",
2089            deserialize_with = "deserialize_xs_datetime",
2090            default)]
2091    pub nextLeapChangeTime: Option<XsDatetime>,
2092}
2093
2094/// The Patch mechanism allows the DASH client to retrieve a set of instructions for replacing
2095/// certain parts of the MPD manifest with updated information.
2096///
2097/// It is a bandwidth-friendly alternative to retrieving a new version of the full MPD manifest. The
2098/// MPD patch document is guaranteed to be available between MPD@publishTime and MPD@publishTime +
2099/// PatchLocation@ttl.
2100#[skip_serializing_none]
2101#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq)]
2102#[serde(default)]
2103pub struct PatchLocation {
2104    #[serde(rename = "@ttl", serialize_with="serialize_opt_xsd_double")]
2105    pub ttl: Option<f64>,
2106    #[serde(rename = "$text")]
2107    pub content: String,
2108}
2109
2110/// The root node of a parsed DASH MPD manifest.
2111#[skip_serializing_none]
2112#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq)]
2113#[serde(default)]
2114pub struct MPD {
2115    #[serde(rename = "@xmlns", serialize_with="serialize_xmlns")]
2116    pub xmlns: Option<String>,
2117    #[serde(rename = "@id")]
2118    pub id: Option<String>,
2119    #[serde(rename = "@profiles")]
2120    pub profiles: Option<String>,
2121    /// The Presentation Type, either "static" or "dynamic" (a live stream for which segments become
2122    /// available over time).
2123    #[serde(rename = "@type")]
2124    pub mpdtype: Option<String>,
2125    #[serde(rename = "@availabilityStartTime",
2126            deserialize_with = "deserialize_xs_datetime",
2127            default)]
2128    pub availabilityStartTime: Option<XsDatetime>,
2129    #[serde(rename = "@availabilityEndTime",
2130            deserialize_with = "deserialize_xs_datetime",
2131            default)]
2132    pub availabilityEndTime: Option<XsDatetime>,
2133    #[serde(rename = "@publishTime",
2134            deserialize_with = "deserialize_xs_datetime",
2135            default)]
2136    pub publishTime: Option<XsDatetime>,
2137    #[serde(rename = "@mediaPresentationDuration",
2138            serialize_with = "serialize_xs_duration",
2139            deserialize_with = "deserialize_xs_duration",
2140            default)]
2141    pub mediaPresentationDuration: Option<Duration>,
2142    #[serde(rename = "@minimumUpdatePeriod",
2143            serialize_with = "serialize_xs_duration",
2144            deserialize_with = "deserialize_xs_duration",
2145            default)]
2146    pub minimumUpdatePeriod: Option<Duration>,
2147    // This attribute is actually required by the XSD specification, but we make it optional.
2148    #[serde(rename = "@minBufferTime",
2149            serialize_with = "serialize_xs_duration",
2150            deserialize_with = "deserialize_xs_duration",
2151            default)]
2152    pub minBufferTime: Option<Duration>,
2153    /// Prescribes how many seconds of buffer a client should keep to avoid stalling when streaming
2154    /// under ideal network conditions with bandwidth matching the @bandwidth attribute.
2155    #[serde(rename = "@timeShiftBufferDepth",
2156            serialize_with = "serialize_xs_duration",
2157            deserialize_with = "deserialize_xs_duration",
2158            default)]
2159    pub timeShiftBufferDepth: Option<Duration>,
2160    /// A suggested delay of the presentation compared to the Live edge.
2161    #[serde(rename = "@suggestedPresentationDelay",
2162            serialize_with = "serialize_xs_duration",
2163            deserialize_with = "deserialize_xs_duration",
2164            default)]
2165    pub suggestedPresentationDelay: Option<Duration>,
2166    #[serde(rename = "@maxSegmentDuration",
2167            serialize_with = "serialize_xs_duration",
2168            deserialize_with = "deserialize_xs_duration",
2169            default)]
2170    pub maxSegmentDuration: Option<Duration>,
2171    #[serde(rename = "@maxSubsegmentDuration",
2172            serialize_with = "serialize_xs_duration",
2173            deserialize_with = "deserialize_xs_duration",
2174            default)]
2175    pub maxSubsegmentDuration: Option<Duration>,
2176    /// The XML namespace prefix used by convention for the XML Schema Instance namespace.
2177    #[serialize_always]
2178    #[serde(rename="@xmlns:xsi", alias="@xsi", serialize_with="serialize_xsi_ns")]
2179    pub xsi: Option<String>,
2180    #[serde(alias = "@ext", rename = "@xmlns:ext")]
2181    pub ext: Option<String>,
2182    /// The XML namespace prefix used by convention for the Common Encryption scheme.
2183    #[serialize_always]
2184    #[serde(rename="@xmlns:cenc", alias="@cenc", serialize_with="serialize_cenc_ns")]
2185    pub cenc: Option<String>,
2186    /// The XML namespace prefix used by convention for the Microsoft PlayReady scheme.
2187    #[serialize_always]
2188    #[serde(rename="@xmlns:mspr", alias="@mspr", serialize_with="serialize_mspr_ns")]
2189    pub mspr: Option<String>,
2190    /// The XML namespace prefix used by convention for the XML Linking Language.
2191    #[serialize_always]
2192    #[serde(rename="@xmlns:xlink", alias="@xlink", serialize_with="serialize_xlink_ns")]
2193    pub xlink: Option<String>,
2194    /// The XML namespace prefix used by convention for the “Digital Program Insertion Cueing
2195    /// Message for Cable” (SCTE 35) signaling standard.
2196    #[cfg(feature = "scte35")]
2197    #[serialize_always]
2198    #[serde(rename="@xmlns:scte35", alias="@scte35", serialize_with="scte35::serialize_scte35_ns")]
2199    pub scte35: Option<String>,
2200    /// The XML namespace prefix used by convention for DASH extensions proposed by the Digital
2201    /// Video Broadcasting Project, as per RFC 5328.
2202    #[serialize_always]
2203    #[serde(rename="@xmlns:dvb", alias="@dvb", serialize_with="serialize_dvb_ns")]
2204    pub dvb: Option<String>,
2205    #[serde(rename = "@xsi:schemaLocation", alias = "@schemaLocation")]
2206    pub schemaLocation: Option<String>,
2207    // scte214 namespace
2208    #[serde(alias = "@scte214", rename = "@xmlns:scte214")]
2209    pub scte214: Option<String>,
2210    pub ProgramInformation: Vec<ProgramInformation>,
2211    /// There may be several BaseURLs, for redundancy (for example multiple CDNs)
2212    #[serde(rename = "BaseURL")]
2213    pub base_url: Vec<BaseURL>,
2214    #[serde(rename = "Location", default)]
2215    pub locations: Vec<Location>,
2216    /// Specifies the location of an MPD “patch document”, a set of instructions for replacing
2217    /// certain parts of the MPD manifest with updated information.
2218    pub PatchLocation: Vec<PatchLocation>,
2219    pub ServiceDescription: Vec<ServiceDescription>,
2220    // TODO: elements InitializationSet, InitializationGroup, InitializationPresentation
2221    pub ContentProtection: Vec<ContentProtection>,
2222    #[serde(rename = "Period", default)]
2223    pub periods: Vec<Period>,
2224    pub Metrics: Vec<Metrics>,
2225    #[serde(rename = "EssentialProperty")]
2226    pub essential_property: Vec<EssentialProperty>,
2227    #[serde(rename = "SupplementalProperty")]
2228    pub supplemental_property: Vec<SupplementalProperty>,
2229    pub UTCTiming: Vec<UTCTiming>,
2230    /// Correction for leap seconds, used by the DASH Low Latency specification.
2231    pub LeapSecondInformation: Option<LeapSecondInformation>,
2232}
2233
2234impl std::fmt::Display for MPD {
2235    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2236        write!(f, "{}", quick_xml::se::to_string(self).map_err(|_| std::fmt::Error)?)
2237    }
2238}
2239
2240/// Parse an MPD manifest, provided as an XML string, returning an `MPD` node.
2241pub fn parse(xml: &str) -> Result<MPD, DashMpdError> {
2242    #[cfg(feature = "warn_ignored_elements")]
2243    {
2244        let xd = &mut quick_xml::de::Deserializer::from_str(xml);
2245        let _: MPD = serde_ignored::deserialize(xd, |path| {
2246            warn!("Unused XML element in manifest: {path}");
2247        }).map_err(|e| DashMpdError::Parsing(e.to_string()))?;
2248    }
2249    let xd = &mut quick_xml::de::Deserializer::from_str(xml);
2250    let mpd: MPD = serde_path_to_error::deserialize(xd)
2251        .map_err(|e| DashMpdError::Parsing(e.to_string()))?;
2252    Ok(mpd)
2253}
2254
2255
2256// Note that a codec name can be of the form "mp4a" or "mp4a.40.2".
2257fn is_audio_codec(name: &str) -> bool {
2258    name.starts_with("mp4a") ||
2259        name.starts_with("aac") ||
2260        name.starts_with("vorbis") ||
2261        name.starts_with("opus") ||
2262        name.starts_with("ogg") ||
2263        name.starts_with("webm") ||
2264        name.starts_with("flac") ||
2265        name.starts_with("mp3") ||
2266        name.starts_with("mpeg") ||
2267        name.starts_with("3gpp") ||
2268        name.starts_with("wav") ||
2269        name.starts_with("ec-3") ||
2270        name.starts_with("ac-4") ||
2271        name.starts_with("dtsc") ||
2272        name.starts_with("aptx") ||
2273        name.starts_with("aiff") ||
2274        name.starts_with("mha1")       // MPEG-H 3D Audio
2275}
2276
2277
2278/// Returns `true` if this AdaptationSet contains audio content.
2279///
2280/// It contains audio if the codec attribute corresponds to a known audio codec, or the
2281/// `contentType` attribute is `audio`, or the `mimeType` attribute is `audio/*`, or if one of its
2282/// child `Representation` nodes has an audio `contentType` or `mimeType` attribute.
2283#[must_use]
2284pub fn is_audio_adaptation(a: &&AdaptationSet) -> bool {
2285    if let Some(codec) = &a.codecs {
2286        if is_audio_codec(codec) {
2287            return true;
2288        }
2289    }
2290    if let Some(ct) = &a.contentType {
2291        if ct == "audio" {
2292            return true;
2293        }
2294    }
2295    if let Some(mimetype) = &a.mimeType {
2296        if mimetype.starts_with("audio/") {
2297            return true;
2298        }
2299    }
2300    for r in &a.representations {
2301        if let Some(ct) = &r.contentType {
2302            if ct == "audio" {
2303                return true;
2304            }
2305        }
2306        if let Some(mimetype) = &r.mimeType {
2307            if mimetype.starts_with("audio/") {
2308                return true;
2309            }
2310        }
2311    }
2312    false
2313}
2314
2315/// Returns `true` if this AdaptationSet contains video content.
2316///
2317/// It contains video if the `contentType` attribute` is `video`, or the `mimeType` attribute is
2318/// `video/*` (but without a codec specifying a subtitle format), or if one of its child
2319/// `Representation` nodes has an audio `contentType` or `mimeType` attribute.
2320///
2321/// Note: if it's an audio adaptation then it's not a video adaptation (an audio adaptation means
2322/// audio-only), but a video adaptation may contain audio.
2323pub fn is_video_adaptation(a: &&AdaptationSet) -> bool {
2324    if is_audio_adaptation(a) {
2325        return false;
2326    }
2327    if let Some(ct) = &a.contentType {
2328        if ct == "video" {
2329            return true;
2330        }
2331    }
2332    if let Some(mimetype) = &a.mimeType {
2333        if mimetype.starts_with("video/") {
2334            return true;
2335        }
2336    }
2337    for r in &a.representations {
2338        if let Some(ct) = &r.contentType {
2339            if ct == "video" {
2340                return true;
2341            }
2342        }
2343        // We can have a Representation with mimeType="video/mp4" and codecs="wvtt", which means
2344        // WebVTT in a (possibly fragmented) MP4 container.
2345        if r.codecs.as_deref().is_some_and(is_subtitle_codec) {
2346            return false;
2347        }
2348        if let Some(mimetype) = &r.mimeType {
2349            if mimetype.starts_with("video/") {
2350                return true;
2351            }
2352        }
2353    }
2354    false
2355}
2356
2357
2358fn is_subtitle_mimetype(mt: &str) -> bool {
2359    mt.eq("text/vtt") ||
2360    mt.eq("application/ttml+xml") ||
2361    mt.eq("application/x-sami")
2362
2363    // Some manifests use a @mimeType of "application/mp4" together with @contentType="text"; we'll
2364    // classify these only based on their contentType.
2365}
2366
2367fn is_subtitle_codec(c: &str) -> bool {
2368    c == "wvtt" ||
2369    c == "c608" ||
2370    c == "stpp" ||
2371    c == "tx3g" ||
2372    c.starts_with("stpp.")
2373}
2374
2375/// Returns `true` if this AdaptationSet contains subtitle content.
2376///
2377/// For now, it contains subtitles if the `@mimeType` attribute is "text/vtt" (WebVTT) or
2378/// "application/ttml+xml" or "application/x-sami" (SAMI). Further work needed to handle an
2379/// Adaptation that contains a Representation with @contentType="text" and @codecs="stpp" or a
2380/// subset like @codecs="stpp.ttml.im1t" (fragmented TTML in an MP4 container) or @codecs="wvtt"
2381/// (fragmented VTTcue in an MP4 container).
2382///
2383/// The DVB-DASH specification also allows for closed captions for hearing impaired viewers in an
2384/// AdaptationSet with Accessibility node having @SchemeIdUri =
2385/// "urn:tva:metadata:cs:AudioPurposeCS:2007" and @value=2.
2386pub fn is_subtitle_adaptation(a: &&AdaptationSet) -> bool {
2387    if a.mimeType.as_deref().is_some_and(is_subtitle_mimetype) {
2388        return true;
2389    }
2390    if a.contentType.as_deref().is_some_and(|ct| ct.eq("text")) {
2391        return true;
2392    }
2393    if a.codecs.as_deref().is_some_and(is_subtitle_codec) {
2394        return true;
2395    }
2396    for cc in &a.ContentComponent {
2397        if cc.contentType.as_deref().is_some_and(|ct| ct.eq("text")) {
2398            return true;
2399        }
2400    }
2401    for r in &a.Role {
2402        if r.value.as_deref().is_some_and(|rr| rr.eq("subtitle")) {
2403            return true;
2404        }
2405    }
2406    for r in &a.representations {
2407        if r.mimeType.as_deref().is_some_and(is_subtitle_mimetype) {
2408            return true;
2409        }
2410        // Often, but now always, the subtitle codec is also accompanied by a contentType of "text".
2411        if r.codecs.as_deref().is_some_and(is_subtitle_codec) {
2412            return true;
2413        }
2414    }
2415    false
2416}
2417
2418
2419// Incomplete, see https://en.wikipedia.org/wiki/Subtitles#Subtitle_formats
2420#[derive(Debug, PartialEq, Eq, Clone, Copy)]
2421pub enum SubtitleType {
2422    /// W3C WebVTT, as used in particular for HTML5 media
2423    Vtt,
2424    /// SubRip
2425    Srt,
2426    /// MPSub
2427    Sub,
2428    /// Advanced Substation Alpha
2429    Ass,
2430    /// MPEG-4 Timed Text, aka MP4TT aka 3GPP-TT (codec=tx3g)
2431    Ttxt,
2432    /// Timed Text Markup Language
2433    Ttml,
2434    /// Synchronized Accessible Media Interchange
2435    Sami,
2436    /// Binary WebVTT in a wvtt box in fragmented MP4 container, as specified by ISO/IEC
2437    /// 14496-30:2014. Mostly intended for live streams where it's not possible to provide a
2438    /// standalone VTT file.
2439    Wvtt,
2440    /// XML content (generally TTML) in an stpp box in fragmented MP4 container
2441    Stpp,
2442    /// EIA-608 aka CEA-608, a legacy standard for closed captioning for NTSC TV
2443    Eia608,
2444    Unknown,
2445}
2446
2447fn subtitle_type_for_mimetype(mt: &str) -> Option<SubtitleType> {
2448    match mt {
2449        "text/vtt" => Some(SubtitleType::Vtt),
2450        "application/ttml+xml" => Some(SubtitleType::Ttml),
2451        "application/x-sami" => Some(SubtitleType::Sami),
2452        _ => None
2453    }
2454}
2455
2456#[must_use]
2457pub fn subtitle_type(a: &&AdaptationSet) -> SubtitleType {
2458    if let Some(mimetype) = &a.mimeType {
2459        if let Some(st) = subtitle_type_for_mimetype(mimetype) {
2460            return st;
2461        }
2462    }
2463    if let Some(codecs) = &a.codecs {
2464        if codecs == "wvtt" {
2465            // can be extracted with https://github.com/xhlove/dash-subtitle-extractor
2466            return SubtitleType::Wvtt;
2467        }
2468        if codecs == "c608" {
2469            return SubtitleType::Eia608;
2470        }
2471        if codecs == "tx3g" {
2472            return SubtitleType::Ttxt;
2473        }
2474        if codecs == "stpp" {
2475            return SubtitleType::Stpp;
2476        }
2477        if codecs.starts_with("stpp.") {
2478            return SubtitleType::Stpp;
2479        }
2480    }
2481    for r in &a.representations {
2482        if let Some(mimetype) = &r.mimeType {
2483            if let Some(st) = subtitle_type_for_mimetype(mimetype) {
2484                return st;
2485            }
2486        }
2487        if let Some(codecs) = &r.codecs {
2488            if codecs == "wvtt" {
2489                return SubtitleType::Wvtt;
2490            }
2491            if codecs == "c608" {
2492                return SubtitleType::Eia608;
2493            }
2494            if codecs == "tx3g" {
2495                return SubtitleType::Ttxt;
2496            }
2497            if codecs == "stpp" {
2498                return SubtitleType::Stpp;
2499            }
2500            if codecs.starts_with("stpp.") {
2501                return SubtitleType::Stpp;
2502            }
2503        }
2504    }
2505    SubtitleType::Unknown
2506}
2507
2508
2509#[allow(dead_code)]
2510fn content_protection_type(cp: &ContentProtection) -> String {
2511    if let Some(v) = &cp.value {
2512        if v.eq("cenc") {
2513            return String::from("cenc");
2514        }
2515        if v.eq("Widevine") {
2516            return String::from("Widevine");
2517        }
2518        if v.eq("MSPR 2.0") {
2519            return String::from("PlayReady");
2520        }
2521    }
2522    // See list at https://dashif.org/identifiers/content_protection/
2523    let uri = &cp.schemeIdUri;
2524    let uri = uri.to_lowercase();
2525    if uri.eq("urn:mpeg:dash:mp4protection:2011") {
2526        return String::from("cenc");
2527    }
2528    if uri.eq("urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed") {
2529        return String::from("Widevine");
2530    }
2531    if uri.eq("urn:uuid:9a04f079-9840-4286-ab92-e65be0885f95") {
2532        return String::from("PlayReady");
2533    }
2534    if uri.eq("urn:uuid:94ce86fb-07ff-4f43-adb8-93d2fa968ca2") {
2535        return String::from("FairPlay");
2536    }
2537    if uri.eq("urn:uuid:3ea8778f-7742-4bf9-b18b-e834b2acbd47") {
2538        return String::from("Clear Key AES-128");
2539    }
2540    if uri.eq("urn:uuid:be58615b-19c4-4684-88b3-c8c57e99e957") {
2541        return String::from("Clear Key SAMPLE-AES");
2542    }
2543    if uri.eq("urn:uuid:adb41c24-2dbf-4a6d-958b-4457c0d27b95") {
2544        return String::from("Nagra");
2545    }
2546    if uri.eq("urn:uuid:5e629af5-38da-4063-8977-97ffbd9902d4") {
2547        return String::from("Marlin");
2548    }
2549    if uri.eq("urn:uuid:f239e769-efa3-4850-9c16-a903c6932efb") {
2550        return String::from("Adobe PrimeTime");
2551    }
2552    if uri.eq("urn:uuid:1077efec-c0b2-4d02-ace3-3c1e52e2fb4b") {
2553        return String::from("W3C Common PSSH box");
2554    }
2555    if uri.eq("urn:uuid:80a6be7e-1448-4c37-9e70-d5aebe04c8d2") {
2556        return String::from("Irdeto Content Protection");
2557    }
2558    if uri.eq("urn:uuid:3d5e6d35-9b9a-41e8-b843-dd3c6e72c42c") {
2559        return String::from("WisePlay-ChinaDRM");
2560    }
2561    if uri.eq("urn:uuid:616c7469-6361-7374-2d50-726f74656374") {
2562        return String::from("Alticast");
2563    }
2564    if uri.eq("urn:uuid:6dd8b3c3-45f4-4a68-bf3a-64168d01a4a6") {
2565        return String::from("ABV DRM");
2566    }
2567    // Segment encryption
2568    if uri.eq("urn:mpeg:dash:sea:2012") {
2569        return String::from("SEA");
2570    }
2571    String::from("<unknown>")
2572}
2573
2574
2575fn check_segment_template_duration(
2576    st: &SegmentTemplate,
2577    max_seg_duration: &Duration,
2578    outer_timescale: u64) -> Vec<String>
2579{
2580    let mut errors = Vec::new();
2581    if let Some(timeline) = &st.SegmentTimeline {
2582        for s in &timeline.segments {
2583            let sd = s.d / st.timescale.unwrap_or(outer_timescale);
2584            if sd > max_seg_duration.as_secs() {
2585                errors.push(String::from("SegmentTimeline has segment@d > @maxSegmentDuration"));
2586            }
2587        }
2588    }
2589    errors
2590}
2591
2592fn check_segment_template_conformity(st: &SegmentTemplate) -> Vec<String> {
2593    let mut errors = Vec::new();
2594    if let Some(md) = &st.media {
2595        if !valid_url_p(md) {
2596            errors.push(format!("invalid URL {md}"));
2597        }
2598        if md.contains("$Number$") && md.contains("$Time") {
2599            errors.push(String::from("both $Number$ and $Time$ are used in media template URL"));
2600        }
2601    }
2602    if let Some(init) = &st.initialization {
2603        if !valid_url_p(init) {
2604            errors.push(format!("invalid URL {init}"));
2605        }
2606        if init.contains("$Number") {
2607            errors.push(String::from("$Number$ identifier used in initialization segment URL"));
2608        }
2609        if init.contains("$Time") {
2610            errors.push(String::from("$Time$ identifier used in initialization segment URL"));
2611        }
2612    }
2613    if st.duration.is_some() && st.SegmentTimeline.is_some() {
2614        errors.push(String::from("both SegmentTemplate.duration and SegmentTemplate.SegmentTimeline present"));
2615    }
2616    errors
2617}
2618
2619
2620// Check the URL or URL path u for conformity. This is a very relaxed check because the Url crate is
2621// very tolerant, in particular concerning the syntax accepted for the path component of an URL.
2622fn valid_url_p(u: &str) -> bool {
2623    use url::ParseError;
2624
2625    match Url::parse(u) {
2626        Ok(url) => {
2627            url.scheme() == "https" ||
2628                url.scheme() == "http" ||
2629                url.scheme() == "ftp" ||
2630                url.scheme() == "file" ||
2631                url.scheme() == "data"
2632        },
2633        Err(ParseError::RelativeUrlWithoutBase) => true,
2634        Err(_) => false,
2635    }
2636}
2637
2638/// Returns a list of DASH conformity errors in the DASH manifest mpd.
2639#[must_use]
2640pub fn check_conformity(mpd: &MPD) -> Vec<String> {
2641    let mut errors = Vec::new();
2642
2643    // @maxHeight on the AdaptationSet should give the maximum value of the @height values of its
2644    // Representation elements.
2645    for p in &mpd.periods {
2646        if p.adaptations.is_empty() {
2647            errors.push(format!("Period with @id {} contains no AdaptationSet elements",
2648                                p.id.clone().unwrap_or(String::from("<unspecified>"))));
2649        }
2650        for a in &p.adaptations {
2651            if let Some(mh) = a.maxHeight {
2652                if let Some(mr) = a.representations.iter().max_by_key(|r| r.height.unwrap_or(0)) {
2653                    if mr.height.unwrap_or(0) > mh {
2654                        errors.push(String::from("invalid @maxHeight on AdaptationSet"));
2655                    }
2656                }
2657            }
2658        }
2659    }
2660    // @maxWidth on the AdaptationSet should give the maximum value of the @width values of its
2661    // Representation elements.
2662    for p in &mpd.periods {
2663        for a in &p.adaptations {
2664            if let Some(mw) = a.maxWidth {
2665                if let Some(mr) = a.representations.iter().max_by_key(|r| r.width.unwrap_or(0)) {
2666                    if mr.width.unwrap_or(0) > mw {
2667                        errors.push(String::from("invalid @maxWidth on AdaptationSet"));
2668                    }
2669                }
2670            }
2671        }
2672    }
2673    // @maxBandwidth on the AdaptationSet should give the maximum value of the @bandwidth values of its
2674    // Representation elements.
2675    for p in &mpd.periods {
2676        for a in &p.adaptations {
2677            if let Some(mb) = a.maxBandwidth {
2678                if let Some(mr) = a.representations.iter().max_by_key(|r| r.bandwidth.unwrap_or(0)) {
2679                    if mr.bandwidth.unwrap_or(0) > mb {
2680                        errors.push(String::from("invalid @maxBandwidth on AdaptationSet"));
2681                    }
2682                }
2683            }
2684        }
2685    }
2686    // No @d of a segment should be greater than @maxSegmentDuration.
2687    if let Some(max_seg_duration) = mpd.maxSegmentDuration {
2688        for p in &mpd.periods {
2689            for a in &p.adaptations {
2690                // We need to keep track of outer_timescale for situations with a nested SegmentTemplate.
2691                // For an example see test/fixtures/aws.xml.
2692                // <SegmentTemplate startNumber="1" timescale="90000"/>
2693                //   <Representation bandwidth="3296000" ...>
2694                //     <SegmentTemplate initialization="i.mp4" media="m$Number$.mp4">
2695                //       <SegmentTimeline>
2696                //         <S d="180000" r="6" t="0"/>
2697                //       </SegmentTimeline>
2698                //     </SegmentTemplate>
2699                // ...
2700                let mut outer_timescale = 1;
2701                if let Some(st) = &a.SegmentTemplate {
2702                    check_segment_template_duration(st, &max_seg_duration, outer_timescale)
2703                        .into_iter()
2704                        .for_each(|msg| errors.push(msg));
2705                    if let Some(ots) = st.timescale {
2706                        outer_timescale = ots;
2707                    }
2708                }
2709                for r in &a.representations {
2710                    if let Some(st) = &r.SegmentTemplate {
2711                        check_segment_template_duration(st, &max_seg_duration, outer_timescale)
2712                            .into_iter()
2713                            .for_each(|msg| errors.push(msg));
2714                    }
2715                }
2716            }
2717        }
2718    }
2719
2720    for bu in &mpd.base_url {
2721        if !valid_url_p(&bu.base) {
2722            errors.push(format!("invalid URL {}", bu.base));
2723        }
2724    }
2725    for p in &mpd.periods {
2726        for bu in &p.BaseURL {
2727            if !valid_url_p(&bu.base) {
2728                errors.push(format!("invalid URL {}", bu.base));
2729            }
2730        }
2731        for a in &p.adaptations {
2732            for bu in &a.BaseURL {
2733                if !valid_url_p(&bu.base) {
2734                    errors.push(format!("invalid URL {}", bu.base));
2735                }
2736            }
2737            if let Some(st) = &a.SegmentTemplate {
2738                check_segment_template_conformity(st)
2739                    .into_iter()
2740                    .for_each(|msg| errors.push(msg));
2741            }
2742            for r in &a.representations {
2743                for bu in &r.BaseURL {
2744                    if !valid_url_p(&bu.base) {
2745                        errors.push(format!("invalid URL {}", bu.base));
2746                    }
2747                }
2748                if let Some(sb) = &r.SegmentBase {
2749                    if let Some(init) = &sb.Initialization {
2750                        if let Some(su) = &init.sourceURL {
2751                            if !valid_url_p(su) {
2752                                errors.push(format!("invalid URL {su}"));
2753                            }
2754                            if su.contains("$Number") {
2755                                errors.push(String::from("$Number$ identifier used in initialization segment URL"));
2756                            }
2757                            if su.contains("$Time") {
2758                                errors.push(String::from("$Time$ identifier used in initialization segment URL"));
2759                            }
2760                        }
2761                    }
2762                    if let Some(ri) = &sb.representation_index {
2763                        if let Some(su) = &ri.sourceURL {
2764                            if !valid_url_p(su) {
2765                                errors.push(format!("invalid URL {su}"));
2766                            }
2767                        }
2768                    }
2769                }
2770                if let Some(sl) = &r.SegmentList {
2771                    if let Some(hr) = &sl.href {
2772                        if !valid_url_p(hr) {
2773                            errors.push(format!("invalid URL {hr}"));
2774                        }
2775                    }
2776                    if let Some(init) = &sl.Initialization {
2777                        if let Some(su) = &init.sourceURL {
2778                            if !valid_url_p(su) {
2779                                errors.push(format!("invalid URL {su}"));
2780                            }
2781                            if su.contains("$Number") {
2782                                errors.push(String::from("$Number$ identifier used in initialization segment URL"));
2783                            }
2784                            if su.contains("$Time") {
2785                                errors.push(String::from("$Time$ identifier used in initialization segment URL"));
2786                            }
2787                        }
2788                    }
2789                    for su in &sl.segment_urls {
2790                        if let Some(md) = &su.media {
2791                            if !valid_url_p(md) {
2792                                errors.push(format!("invalid URL {md}"));
2793                            }
2794                        }
2795                        if let Some(ix) = &su.index {
2796                            if !valid_url_p(ix) {
2797                                errors.push(format!("invalid URL {ix}"));
2798                            }
2799                        }
2800                    }
2801                }
2802                if let Some(st) = &r.SegmentTemplate {
2803                    check_segment_template_conformity(st)
2804                        .into_iter()
2805                        .for_each(|msg| errors.push(msg));
2806                }
2807            }
2808        }
2809    }
2810    for pi in &mpd.ProgramInformation {
2811        if let Some(u) = &pi.moreInformationURL {
2812            if !valid_url_p(u) {
2813                errors.push(format!("invalid URL {u}"));
2814            }
2815        }
2816    }
2817    errors
2818}
2819
2820#[cfg(test)]
2821mod tests {
2822    use proptest::prelude::*;
2823    use std::fs;
2824    use std::path::PathBuf;
2825    use std::time::Duration;
2826
2827    proptest! {
2828        #[test]
2829        fn doesnt_crash(s in "\\PC*") {
2830            let _ = super::parse_xs_duration(&s);
2831            let _ = super::parse_xs_datetime(&s);
2832        }
2833    }
2834
2835    #[test]
2836    fn test_parse_xs_duration() {
2837        use super::parse_xs_duration;
2838
2839        assert!(parse_xs_duration("").is_err());
2840        assert!(parse_xs_duration("foobles").is_err());
2841        assert!(parse_xs_duration("P").is_err());
2842        assert!(parse_xs_duration("PW").is_err());
2843        // assert!(parse_xs_duration("PT-4.5S").is_err());
2844        assert!(parse_xs_duration("-PT4.5S").is_err());
2845        assert!(parse_xs_duration("1Y2M3DT4H5M6S").is_err()); // missing initial P
2846        assert_eq!(parse_xs_duration("PT3H11M53S").ok(), Some(Duration::new(11513, 0)));
2847        assert_eq!(parse_xs_duration("PT42M30S").ok(), Some(Duration::new(2550, 0)));
2848        assert_eq!(parse_xs_duration("PT30M38S").ok(), Some(Duration::new(1838, 0)));
2849        assert_eq!(parse_xs_duration("PT0H10M0.00S").ok(), Some(Duration::new(600, 0)));
2850        assert_eq!(parse_xs_duration("PT1.5S").ok(), Some(Duration::new(1, 500_000_000)));
2851        assert_eq!(parse_xs_duration("PT1.500S").ok(), Some(Duration::new(1, 500_000_000)));
2852        assert_eq!(parse_xs_duration("PT1.500000000S").ok(), Some(Duration::new(1, 500_000_000)));
2853        assert_eq!(parse_xs_duration("PT0S").ok(), Some(Duration::new(0, 0)));
2854        assert_eq!(parse_xs_duration("PT0.001S").ok(), Some(Duration::new(0, 1_000_000)));
2855        assert_eq!(parse_xs_duration("PT0.00100S").ok(), Some(Duration::new(0, 1_000_000)));
2856        assert_eq!(parse_xs_duration("PT344S").ok(), Some(Duration::new(344, 0)));
2857        assert_eq!(parse_xs_duration("PT634.566S").ok(), Some(Duration::new(634, 566_000_000)));
2858        assert_eq!(parse_xs_duration("PT72H").ok(), Some(Duration::new(72*60*60, 0)));
2859        assert_eq!(parse_xs_duration("PT0H0M30.030S").ok(), Some(Duration::new(30, 30_000_000)));
2860        assert_eq!(parse_xs_duration("PT1004199059S").ok(), Some(Duration::new(1004199059, 0)));
2861        assert_eq!(parse_xs_duration("P0Y20M0D").ok(), Some(Duration::new(51840000, 0)));
2862        assert_eq!(parse_xs_duration("PT1M30.5S").ok(), Some(Duration::new(90, 500_000_000)));
2863        assert_eq!(parse_xs_duration("PT10M10S").ok(), Some(Duration::new(610, 0)));
2864        assert_eq!(parse_xs_duration("PT1H0.040S").ok(), Some(Duration::new(3600, 40_000_000)));
2865        assert_eq!(parse_xs_duration("PT00H03M30SZ").ok(), Some(Duration::new(210, 0)));
2866        assert_eq!(parse_xs_duration("PT3.14159S").ok(), Some(Duration::new(3, 141_590_000)));
2867        assert_eq!(parse_xs_duration("PT3.14159265S").ok(), Some(Duration::new(3, 141_592_650)));
2868        assert_eq!(parse_xs_duration("PT3.141592653S").ok(), Some(Duration::new(3, 141_592_653)));
2869        // We are truncating rather than rounding the number of nanoseconds
2870        assert_eq!(parse_xs_duration("PT3.141592653897S").ok(), Some(Duration::new(3, 141_592_653)));
2871        assert_eq!(parse_xs_duration("P0W").ok(), Some(Duration::new(0, 0)));
2872        assert_eq!(parse_xs_duration("P26W").ok(), Some(Duration::new(15724800, 0)));
2873        assert_eq!(parse_xs_duration("P52W").ok(), Some(Duration::new(31449600, 0)));
2874        assert_eq!(parse_xs_duration("P10D").ok(), Some(Duration::new(864000, 0)));
2875        assert_eq!(parse_xs_duration("P0Y").ok(), Some(Duration::new(0, 0)));
2876        assert_eq!(parse_xs_duration("P1Y").ok(), Some(Duration::new(31536000, 0)));
2877        assert_eq!(parse_xs_duration("P1Y0W0S").ok(), Some(Duration::new(31536000, 0)));
2878        assert_eq!(parse_xs_duration("PT4H").ok(), Some(Duration::new(14400, 0)));
2879        assert_eq!(parse_xs_duration("+PT4H").ok(), Some(Duration::new(14400, 0)));
2880        assert_eq!(parse_xs_duration("PT0004H").ok(), Some(Duration::new(14400, 0)));
2881        assert_eq!(parse_xs_duration("PT4H0M").ok(), Some(Duration::new(14400, 0)));
2882        assert_eq!(parse_xs_duration("PT4H0S").ok(), Some(Duration::new(14400, 0)));
2883        assert_eq!(parse_xs_duration("P23DT23H").ok(), Some(Duration::new(2070000, 0)));
2884        assert_eq!(parse_xs_duration("P0Y0M0DT0H4M20.880S").ok(), Some(Duration::new(260, 880_000_000)));
2885        assert_eq!(parse_xs_duration("P1Y2M3DT4H5M6.7S").ok(), Some(Duration::new(36993906, 700_000_000)));
2886        assert_eq!(parse_xs_duration("P1Y2M3DT4H5M6,7S").ok(), Some(Duration::new(36993906, 700_000_000)));
2887
2888        // we are not currently handling fractional parts except in the seconds
2889        // assert_eq!(parse_xs_duration("PT0.5H1S").ok(), Some(Duration::new(30*60+1, 0)));
2890        // assert_eq!(parse_xs_duration("P0001-02-03T04:05:06").ok(), Some(Duration::new(36993906, 0)));
2891    }
2892
2893    #[test]
2894    fn test_serialize_xs_duration() {
2895        use super::MPD;
2896
2897        fn serialized_xs_duration(d: Duration) -> String {
2898            let mpd = MPD {
2899                minBufferTime: Some(d),
2900                ..Default::default()
2901            };
2902            let xml = mpd.to_string();
2903            let doc = roxmltree::Document::parse(&xml).unwrap();
2904            String::from(doc.root_element().attribute("minBufferTime").unwrap())
2905        }
2906
2907        assert_eq!("PT0S", serialized_xs_duration(Duration::new(0, 0)));
2908        assert_eq!("PT0.001S", serialized_xs_duration(Duration::new(0, 1_000_000)));
2909        assert_eq!("PT42S", serialized_xs_duration(Duration::new(42, 0)));
2910        assert_eq!("PT1.5S", serialized_xs_duration(Duration::new(1, 500_000_000)));
2911        assert_eq!("PT30.03S", serialized_xs_duration(Duration::new(30, 30_000_000)));
2912        assert_eq!("PT1M30.5S", serialized_xs_duration(Duration::new(90, 500_000_000)));
2913        assert_eq!("PT5M44S", serialized_xs_duration(Duration::new(344, 0)));
2914        assert_eq!("PT42M30S", serialized_xs_duration(Duration::new(2550, 0)));
2915        assert_eq!("PT30M38S", serialized_xs_duration(Duration::new(1838, 0)));
2916        assert_eq!("PT10M10S", serialized_xs_duration(Duration::new(610, 0)));
2917        assert_eq!("PT1H0M0.04S", serialized_xs_duration(Duration::new(3600, 40_000_000)));
2918        assert_eq!("PT3H11M53S", serialized_xs_duration(Duration::new(11513, 0)));
2919        assert_eq!("PT4H", serialized_xs_duration(Duration::new(14400, 0)));
2920    }
2921
2922    #[test]
2923    fn test_parse_xs_datetime() {
2924        use chrono::{DateTime, NaiveDate};
2925        use chrono::offset::Utc;
2926        use super::parse_xs_datetime;
2927
2928        let date = NaiveDate::from_ymd_opt(2023, 4, 19)
2929            .unwrap()
2930            .and_hms_opt(1, 3, 2)
2931            .unwrap();
2932        assert_eq!(parse_xs_datetime("2023-04-19T01:03:02Z").ok(),
2933                   Some(DateTime::<Utc>::from_naive_utc_and_offset(date, Utc)));
2934        let date = NaiveDate::from_ymd_opt(2023, 4, 19)
2935            .unwrap()
2936            .and_hms_nano_opt(1, 3, 2, 958*1000*1000)
2937            .unwrap();
2938        assert_eq!(parse_xs_datetime("2023-04-19T01:03:02.958Z").ok(),
2939                   Some(DateTime::<Utc>::from_naive_utc_and_offset(date, Utc)));
2940    }
2941
2942    #[test]
2943    fn test_parse_failure() {
2944        use super::parse;
2945
2946        let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
2947        path.push("tests");
2948        path.push("fixtures");
2949        path.push("incomplete.mpd");
2950        let xml = fs::read_to_string(path).unwrap();
2951        assert!(matches!(parse(&xml), Err(crate::DashMpdError::Parsing(_))));
2952    }
2953
2954    #[test]
2955    fn test_conformity_checking() {
2956        use super::{parse, check_conformity};
2957
2958        // These test fixtures have no currently detected non-conformities.
2959        for fixture in [
2960            "a2d-tv.mpd",
2961            "ad-insertion-testcase1.mpd",
2962            "ad-insertion-testcase6-av1.mpd",
2963            "ad-insertion-testcase6-av2.mpd",
2964            "ad-insertion-testcase6-av5.mpd",
2965            "aws.xml",
2966            "dashif-live-atoinf.mpd",
2967            "dashif-low-latency.mpd",
2968            "dash-testcases-5b-1-thomson.mpd",
2969            "dolby-ac4.xml",
2970            "example_G22.mpd",
2971            "f64-inf.mpd",
2972            "jurassic-compact-5975.mpd",
2973            "mediapackage.xml",
2974            "multiple_supplementals.mpd",
2975            "orange.xml",
2976            "patch-location.mpd",
2977            "st-sl.mpd",
2978            "telenet-mid-ad-rolls.mpd",
2979            "manifest_wvcenc_1080p.mpd"] {
2980            let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
2981            path.push("tests");
2982            path.push("fixtures");
2983            path.push(fixture);
2984            let xml = fs::read_to_string(path)
2985                .unwrap_or_else(|_| panic!("failed to read fixture {fixture}"));
2986            let mpd = parse(&xml)
2987                .unwrap_or_else(|_| panic!("failed to parse fixture {fixture}"));
2988            let anomalies = check_conformity(&mpd);
2989            assert!(anomalies.is_empty());
2990        }
2991        // Now some manifests that have known non-conformities
2992        let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
2993        path.push("tests");
2994        path.push("fixtures");
2995        path.push("admanager.xml");
2996        let xml = fs::read_to_string(path).unwrap();
2997        let mpd = parse(&xml).unwrap();
2998        let anomalies = check_conformity(&mpd);
2999        assert!(!anomalies.is_empty());
3000        for anomaly in anomalies {
3001            assert!(anomaly.starts_with("SegmentTimeline has segment@d"));
3002        }
3003        let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
3004        path.push("tests");
3005        path.push("fixtures");
3006        path.push( "avod-mediatailor.mpd");
3007        let xml = fs::read_to_string(path).unwrap();
3008        let mpd = parse(&xml).unwrap();
3009        let anomalies = check_conformity(&mpd);
3010        assert!(!anomalies.is_empty());
3011        for anomaly in anomalies {
3012            assert!(anomaly.starts_with("SegmentTimeline has segment@d"));
3013        }
3014        let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
3015        path.push("tests");
3016        path.push("fixtures");
3017        path.push("telestream-binary.xml");
3018        let xml = fs::read_to_string(path).unwrap();
3019        let mpd = parse(&xml).unwrap();
3020        let anomalies = check_conformity(&mpd);
3021        assert!(!anomalies.is_empty());
3022        for anomaly in anomalies {
3023            assert!(anomaly.starts_with("Period with @id <unspecified> contains no AdaptationSet elements"));
3024        }
3025        let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
3026        path.push("tests");
3027        path.push("fixtures");
3028        path.push("telestream-elements.xml");
3029        let xml = fs::read_to_string(path).unwrap();
3030        let mpd = parse(&xml).unwrap();
3031        let anomalies = check_conformity(&mpd);
3032        assert!(!anomalies.is_empty());
3033        for anomaly in anomalies {
3034            assert!(anomaly.starts_with("Period with @id <unspecified> contains no AdaptationSet elements"));
3035        }
3036        let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
3037        path.push("tests");
3038        path.push("fixtures");
3039        path.push("vod-aip-unif-streaming.mpd");
3040        let xml = fs::read_to_string(path).unwrap();
3041        let mpd = parse(&xml).unwrap();
3042        let anomalies = check_conformity(&mpd);
3043        assert!(!anomalies.is_empty());
3044        for anomaly in anomalies {
3045            assert!(anomaly.starts_with("SegmentTimeline has segment@d > @maxSegmentDuration"));
3046        }
3047    }
3048}