1use 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
16pub const DEFAULT_BASE_PRIORITY: u32 = 1;
18const QUERY_SEPARATOR: char = '&';
19const QUERY_PART_TEMPLATE: &str = "$querypart$";
20
21#[derive(Debug, Clone, PartialEq)]
24#[non_exhaustive]
25pub struct BaseCandidate {
26 pub chain: Vec<String>,
28 pub service_location: Option<String>,
30 pub priority: u32,
32 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 #[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#[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#[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#[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
147pub 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#[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#[must_use]
218pub fn is_unsupported_urlparam(descriptor: &Descriptor) -> bool {
219 descriptor.scheme_id_uri == SCHEME_URLPARAM_2016
220}