Skip to main content

dash_runtime/mpd/
urls.rs

1//! BaseURL alternates and URL resolution (ISO/IEC 23009-1 5.6).
2//!
3//! Resolution is RFC 3986 5.2 through [`crate::mpd::base_url::resolve_chain`]
4//! (which also rejects control characters), never string concatenation
5//! (5.6.4 informative note). Alternates (5.6.5) are ordered by DVB `@priority`
6//! (ETSI TS 103 285 10.8.2.1, default 1) then document order; `@weight` is
7//! parsed but not used, because weighted random choice needs a random source
8//! and this core is deterministic.
9
10use url::Url;
11
12use crate::limits::MAX_BASE_CANDIDATES;
13use broadcast_dash::descriptors::SCHEME_URLPARAM;
14use broadcast_dash::{
15    AdaptationSet, BaseUrl, Descriptor, Mpd, Period, Representation, UrlQueryInfo,
16};
17
18/// ETSI TS 103 285 10.8.2.1: the `@dvb:priority` default.
19pub const DEFAULT_BASE_PRIORITY: u32 = 1;
20const QUERY_SEPARATOR: char = '&';
21const QUERY_PART_TEMPLATE: &str = "$querypart$";
22
23/// One way of reaching the content: a chain of `BaseURL` texts, one per
24/// level that declared any, outermost first.
25#[derive(Debug, Clone, PartialEq)]
26#[non_exhaustive]
27pub struct BaseCandidate {
28    /// The `BaseURL` texts, outermost first.
29    pub chain: Vec<String>,
30    /// `@serviceLocation` of the innermost entry.
31    pub service_location: Option<String>,
32    /// `@dvb:priority` of the innermost entry (default 1).
33    pub priority: u32,
34    /// Sum of `@availabilityTimeOffset` over the chain, seconds.
35    pub availability_time_offset_s: f64,
36}
37
38impl BaseCandidate {
39    fn root() -> Self {
40        Self {
41            chain: Vec::new(),
42            service_location: None,
43            priority: DEFAULT_BASE_PRIORITY,
44            availability_time_offset_s: 0.0,
45        }
46    }
47
48    /// Resolve `reference` through this candidate onto the MPD's own URL.
49    /// `None` when any entry or the reference is unusable.
50    #[must_use]
51    pub fn resolve(&self, document_url: Option<&Url>, reference: &str) -> Option<String> {
52        super::base_url::resolve_chain(document_url, &self.chain, reference)
53    }
54}
55
56/// The candidate locations for the `levels` of base URLs (MPD, Period,
57/// AdaptationSet, Representation; a level without `BaseURL` is skipped), best
58/// first. Candidates sharing a `@serviceLocation` pair up across levels;
59/// otherwise a level's alternates extend the best candidate above.
60#[must_use]
61pub fn base_candidates(levels: &[&[BaseUrl]]) -> Vec<BaseCandidate> {
62    let mut candidates = vec![BaseCandidate::root()];
63    for level in levels.iter().filter(|l| !l.is_empty()) {
64        let mut ordered: Vec<&BaseUrl> = level.iter().collect();
65        ordered.sort_by_key(|b| b.priority.unwrap_or(DEFAULT_BASE_PRIORITY));
66        let mut next = Vec::with_capacity(ordered.len());
67        for b in ordered {
68            let parent = candidates
69                .iter()
70                .find(|c| c.service_location.is_some() && c.service_location == b.service_location)
71                .unwrap_or(&candidates[0]);
72            let mut chain = parent.chain.clone();
73            chain.push(b.url.clone());
74            next.push(BaseCandidate {
75                chain,
76                service_location: b.service_location.clone(),
77                priority: b.priority.unwrap_or(DEFAULT_BASE_PRIORITY),
78                availability_time_offset_s: parent.availability_time_offset_s
79                    + b.availability_time_offset.unwrap_or(0.0),
80            });
81            if next.len() >= MAX_BASE_CANDIDATES {
82                break;
83            }
84        }
85        candidates = next;
86    }
87    candidates
88}
89
90/// The candidates for one Representation.
91#[must_use]
92pub fn representation_candidates(
93    mpd: &Mpd,
94    period: &Period,
95    set: &AdaptationSet,
96    rep: &Representation,
97) -> Vec<BaseCandidate> {
98    base_candidates(&[
99        &mpd.base_urls,
100        &period.base_urls,
101        &set.base_urls,
102        &rep.base_urls,
103    ])
104}
105
106/// The `urn:mpeg:dash:urlparam:2014` query of a Representation was written in
107/// a form this core does not interpret.
108#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
109#[error("urlparam descriptor uses a queryTemplate this core does not understand")]
110#[non_exhaustive]
111pub struct UrlQueryNotUnderstood;
112
113fn level_query(
114    descriptors: &[&[Descriptor]],
115    mpd_query: Option<&str>,
116    out: &mut Vec<String>,
117) -> Result<(), UrlQueryNotUnderstood> {
118    for list in descriptors {
119        for d in list.iter().filter(|d| d.scheme_id_uri == SCHEME_URLPARAM) {
120            let Some(info) = d.url_query.as_ref() else {
121                continue;
122            };
123            if let Some(t) = &info.query_template
124                && t != QUERY_PART_TEMPLATE
125            {
126                return Err(UrlQueryNotUnderstood);
127            }
128            push_query(info, mpd_query, out);
129        }
130    }
131    Ok(())
132}
133
134fn push_query(info: &UrlQueryInfo, mpd_query: Option<&str>, out: &mut Vec<String>) {
135    let mut parts: Vec<&str> = Vec::new();
136    if info.use_mpd_url_query
137        && let Some(q) = mpd_query.filter(|q| !q.is_empty())
138    {
139        parts.push(q);
140    }
141    if let Some(s) = info.query_string.as_deref().filter(|s| !s.is_empty()) {
142        parts.push(s);
143    }
144    if !parts.is_empty() {
145        out.push(parts.join(&QUERY_SEPARATOR.to_string()));
146    }
147}
148
149/// The query to append to every Media Segment URL of `rep` (Annex I.2.3):
150/// Representation level, then AdaptationSet, Period, MPD, joined with `&`.
151///
152/// `mpd_query` is the query of the MPD's own URL. `None` when no descriptor
153/// asks for one. An `urn:mpeg:dash:urlparam:2016` property is not understood
154/// (callers treat an essential one as dropping the Representation, a
155/// supplemental one as ignored).
156///
157/// # Errors
158/// [`UrlQueryNotUnderstood`] for a `@queryTemplate` other than `$querypart$`.
159pub fn segment_url_query(
160    mpd: &Mpd,
161    period: &Period,
162    set: &AdaptationSet,
163    rep: &Representation,
164    mpd_query: Option<&str>,
165) -> Result<Option<String>, UrlQueryNotUnderstood> {
166    let mut parts = Vec::new();
167    level_query(
168        &[&rep.essential_properties, &rep.supplemental_properties],
169        mpd_query,
170        &mut parts,
171    )?;
172    level_query(
173        &[&set.essential_properties, &set.supplemental_properties],
174        mpd_query,
175        &mut parts,
176    )?;
177    level_query(
178        &[
179            &period.essential_properties,
180            &period.supplemental_properties,
181        ],
182        mpd_query,
183        &mut parts,
184    )?;
185    level_query(
186        &[&mpd.essential_properties, &mpd.supplemental_properties],
187        mpd_query,
188        &mut parts,
189    )?;
190    if parts.is_empty() {
191        Ok(None)
192    } else {
193        Ok(Some(parts.join(&QUERY_SEPARATOR.to_string())))
194    }
195}
196
197/// Append `query` to `url`: `?` when it has none, `&` when it has one
198/// (Annex I.2.3 step 4).
199#[must_use]
200pub fn append_query(url: &str, query: &str) -> String {
201    let (base, fragment) = match url.split_once('#') {
202        Some((b, f)) => (b, Some(f)),
203        None => (url, None),
204    };
205    let sep = if base.contains('?') {
206        QUERY_SEPARATOR
207    } else {
208        '?'
209    };
210    let mut out = format!("{base}{sep}{query}");
211    if let Some(f) = fragment {
212        out.push('#');
213        out.push_str(f);
214    }
215    out
216}