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