1use 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
18pub const DEFAULT_BASE_PRIORITY: u32 = 1;
20const QUERY_SEPARATOR: char = '&';
21const QUERY_PART_TEMPLATE: &str = "$querypart$";
22
23#[derive(Debug, Clone, PartialEq)]
26#[non_exhaustive]
27pub struct BaseCandidate {
28 pub chain: Vec<String>,
30 pub service_location: Option<String>,
32 pub priority: u32,
34 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 #[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#[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#[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#[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
149pub 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#[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}