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