1use broadcast_common::impl_spec_display;
20
21use crate::address::template;
22use crate::mpd::descriptors::{self, Role, essential_understood};
23use crate::mpd::inherit::{SegmentInfo, effective_common, resolve_segment_info};
24use crate::mpd::model::{AdaptationSet, Mpd, Period, Representation};
25
26const CODEC_SEPARATOR: char = ',';
27const LANG_SUBTAG_SEPARATOR: char = '-';
28const MIME_VIDEO: &str = "video/";
29const MIME_AUDIO: &str = "audio/";
30const MIME_TEXT: &str = "text/";
31const MIME_APPLICATION: &str = "application/";
32const MIME_MP2T_SUFFIX: &str = "/mp2t";
33const MIME_WEBM_SUFFIX: &str = "/webm";
34const CONTENT_VIDEO: &str = "video";
35const CONTENT_AUDIO: &str = "audio";
36const CONTENT_TEXT: &str = "text";
37const CODEC_STPP: &str = "stpp";
38const CODEC_WVTT: &str = "wvtt";
39
40#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
42#[non_exhaustive]
43pub enum SetClass {
44 Video,
46 Audio,
48 Text,
50 Other,
52}
53
54impl SetClass {
55 #[must_use]
57 pub fn name(&self) -> &'static str {
58 match self {
59 Self::Video => "video",
60 Self::Audio => "audio",
61 Self::Text => "text",
62 Self::Other => "other",
63 }
64 }
65}
66impl_spec_display!(SetClass);
67
68#[derive(Debug, Clone, Copy, PartialEq, Eq)]
70#[non_exhaustive]
71pub enum DropReason {
72 Text,
74 NotMedia,
76 Mp2tSegments,
78 WebM,
80 TrickMode,
82 ProtectedAlternativeExists,
84 UnsupportedEncryption,
86 UnknownEssentialProperty,
88 DependencyRepresentation,
90 UnresolvedXlink,
92 InvalidSegmentInfo,
94}
95
96impl DropReason {
97 #[must_use]
99 pub fn name(&self) -> &'static str {
100 match self {
101 Self::Text => "text",
102 Self::NotMedia => "not_media",
103 Self::Mp2tSegments => "mp2t_segments",
104 Self::WebM => "webm",
105 Self::TrickMode => "trick_mode",
106 Self::ProtectedAlternativeExists => "protected_alternative_exists",
107 Self::UnsupportedEncryption => "unsupported_encryption",
108 Self::UnknownEssentialProperty => "unknown_essential_property",
109 Self::DependencyRepresentation => "dependency_representation",
110 Self::UnresolvedXlink => "unresolved_xlink",
111 Self::InvalidSegmentInfo => "invalid_segment_info",
112 }
113 }
114}
115impl_spec_display!(DropReason);
116
117#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
119#[non_exhaustive]
120pub struct RepRef {
121 pub period: usize,
123 pub set: usize,
125 pub rep: usize,
127}
128
129impl RepRef {
130 #[must_use]
133 pub fn at(period: usize, set: usize, rep: usize) -> Self {
134 Self { period, set, rep }
135 }
136
137 #[must_use]
139 pub fn resolve<'a>(
140 &self,
141 mpd: &'a Mpd,
142 ) -> Option<(&'a Period, &'a AdaptationSet, &'a Representation)> {
143 let period = mpd.periods.get(self.period)?;
144 let set = period.adaptation_sets.get(self.set)?;
145 Some((period, set, set.representations.get(self.rep)?))
146 }
147}
148
149#[derive(Debug, Clone, Copy, PartialEq, Eq)]
151#[non_exhaustive]
152pub struct Dropped {
153 pub set: usize,
155 pub rep: Option<usize>,
157 pub reason: DropReason,
159}
160
161#[derive(Debug, Clone, PartialEq, Eq, Default)]
164#[non_exhaustive]
165pub struct Policy {
166 pub max_bandwidth: Option<u64>,
168 pub max_height: Option<u32>,
170 pub codec_prefixes: Vec<String>,
173 pub preferred_audio_language: Option<String>,
175}
176
177#[derive(Debug, Clone, PartialEq, Eq)]
179#[non_exhaustive]
180pub struct AudioSet {
181 pub set: usize,
183 pub reps: Vec<RepRef>,
185 pub is_main: bool,
187}
188
189#[derive(Debug, Clone, PartialEq, Eq, Default)]
191#[non_exhaustive]
192pub struct Selection {
193 pub variants: Vec<RepRef>,
195 pub chosen_variant: Option<usize>,
197 pub audio_sets: Vec<AudioSet>,
199 pub chosen_audio: Option<RepRef>,
201 pub dropped: Vec<Dropped>,
203}
204
205fn contains_ci(haystack: &str, needle: &str) -> bool {
206 haystack
207 .to_ascii_lowercase()
208 .contains(&needle.to_ascii_lowercase())
209}
210
211#[must_use]
213pub fn classify(set: &AdaptationSet) -> SetClass {
214 let mime = set
215 .common
216 .mime_type
217 .as_deref()
218 .or_else(|| {
219 set.representations
220 .iter()
221 .find_map(|r| r.common.mime_type.as_deref())
222 })
223 .unwrap_or_default();
224 let codecs = set
225 .common
226 .codecs
227 .as_deref()
228 .or_else(|| {
229 set.representations
230 .iter()
231 .find_map(|r| r.common.codecs.as_deref())
232 })
233 .unwrap_or_default();
234 match set.content_type.as_deref() {
235 Some(CONTENT_VIDEO) => return SetClass::Video,
236 Some(CONTENT_AUDIO) => return SetClass::Audio,
237 Some(CONTENT_TEXT) => return SetClass::Text,
238 _ => {}
239 }
240 if mime.starts_with(MIME_VIDEO) {
241 SetClass::Video
242 } else if mime.starts_with(MIME_AUDIO) {
243 SetClass::Audio
244 } else if mime.starts_with(MIME_TEXT)
245 || contains_ci(codecs, CODEC_STPP)
246 || contains_ci(codecs, CODEC_WVTT)
247 || (mime.starts_with(MIME_APPLICATION) && contains_ci(mime, "ttml"))
248 {
249 SetClass::Text
250 } else {
251 SetClass::Other
252 }
253}
254
255fn container_drop(set: &AdaptationSet) -> Option<DropReason> {
256 let mimes = std::iter::once(set.common.mime_type.as_deref())
257 .chain(
258 set.representations
259 .iter()
260 .map(|r| r.common.mime_type.as_deref()),
261 )
262 .flatten();
263 for mime in mimes {
264 if mime.ends_with(MIME_MP2T_SUFFIX) {
265 return Some(DropReason::Mp2tSegments);
266 }
267 if mime.ends_with(MIME_WEBM_SUFFIX) {
268 return Some(DropReason::WebM);
269 }
270 }
271 None
272}
273
274fn has_trick_mode(set: &AdaptationSet) -> bool {
275 set.essential_properties
276 .iter()
277 .chain(&set.supplemental_properties)
278 .any(|d| d.scheme_id_uri == transmux::TRICKMODE_SCHEME)
279}
280
281fn unknown_essential(descs: &[crate::mpd::model::Descriptor]) -> bool {
282 descs.iter().any(|d| !essential_understood(d))
283}
284
285fn segment_info_ok(period: &Period, set: &AdaptationSet, rep: &Representation) -> bool {
286 match resolve_segment_info(period, set, rep) {
287 Ok(SegmentInfo::Template(t)) => {
288 let media_ok = t
289 .media
290 .as_deref()
291 .is_some_and(|m| template::scan_media(m).is_ok_and(|u| !u.sub_number));
292 let init_ok = t
293 .initialization
294 .as_deref()
295 .is_none_or(|i| template::scan_initialization(i).is_ok_and(|u| !u.sub_number));
296 let timeline_ok = t
297 .multi
298 .timeline
299 .as_ref()
300 .is_none_or(|tl| tl.unsupported.is_none());
301 media_ok && init_ok && timeline_ok
302 }
303 Ok(SegmentInfo::List(l)) => l
304 .multi
305 .timeline
306 .as_ref()
307 .is_none_or(|tl| tl.unsupported.is_none()),
308 Ok(SegmentInfo::Base(_)) => true,
309 Err(_) => false,
310 }
311}
312
313#[must_use]
315pub fn lang_matches(preferred: &str, lang: &str) -> bool {
316 let (p, l) = (preferred.to_ascii_lowercase(), lang.to_ascii_lowercase());
317 l == p
318 || l.strip_prefix(&p)
319 .is_some_and(|rest| rest.starts_with(LANG_SUBTAG_SEPARATOR))
320}
321
322fn codec_allowed(codecs: Option<&str>, prefixes: &[String]) -> bool {
323 if prefixes.is_empty() {
324 return true;
325 }
326 codecs.is_some_and(|c| {
327 c.split(CODEC_SEPARATOR)
328 .map(str::trim)
329 .any(|tok| prefixes.iter().any(|p| tok.starts_with(p.as_str())))
330 })
331}
332
333fn is_protected(set: &AdaptationSet) -> bool {
334 !set.content_protections.is_empty()
335 || (!set.representations.is_empty()
336 && set
337 .representations
338 .iter()
339 .all(|r| !r.content_protections.is_empty()))
340}
341
342fn is_main(set: &AdaptationSet) -> bool {
343 descriptors::roles(&set.roles).any(|r| r == Role::Main)
344}
345
346fn rep_allowed(set: &AdaptationSet, rep: &Representation, policy: &Policy) -> bool {
347 let c = effective_common(set, rep);
348 policy.max_bandwidth.is_none_or(|m| rep.bandwidth <= m)
349 && policy
350 .max_height
351 .is_none_or(|h| c.height.is_none_or(|rh| rh <= h))
352 && codec_allowed(c.codecs.as_deref(), &policy.codec_prefixes)
353}
354
355#[must_use]
357pub fn select(mpd: &Mpd, period_idx: usize, policy: &Policy) -> Selection {
358 let mut out = Selection::default();
359 let Some(period) = mpd.periods.get(period_idx) else {
360 return out;
361 };
362 let classes: Vec<SetClass> = period.adaptation_sets.iter().map(classify).collect();
363 let mut survivors: Vec<usize> = Vec::new();
364 for (si, set) in period.adaptation_sets.iter().enumerate() {
365 let class = classes[si];
366 let set_drop = if set.xlink.is_some() {
367 Some(DropReason::UnresolvedXlink)
368 } else if class == SetClass::Text {
369 Some(DropReason::Text)
370 } else if class == SetClass::Other {
371 Some(DropReason::NotMedia)
372 } else if let Some(r) = container_drop(set) {
373 Some(r)
374 } else if has_trick_mode(set) {
375 Some(DropReason::TrickMode)
376 } else if unknown_essential(&set.essential_properties) {
377 Some(DropReason::UnknownEssentialProperty)
378 } else if is_protected(set) {
379 let alternative = period
380 .adaptation_sets
381 .iter()
382 .enumerate()
383 .any(|(oi, other)| {
384 oi != si
385 && set.group.is_some()
386 && other.group == set.group
387 && classes[oi] == class
388 && !is_protected(other)
389 });
390 Some(if alternative {
391 DropReason::ProtectedAlternativeExists
392 } else {
393 DropReason::UnsupportedEncryption
394 })
395 } else {
396 None
397 };
398 match set_drop {
399 Some(reason) => out.dropped.push(Dropped {
400 set: si,
401 rep: None,
402 reason,
403 }),
404 None => survivors.push(si),
405 }
406 }
407
408 let mut usable: Vec<(usize, Vec<usize>)> = Vec::new();
409 for &si in &survivors {
410 let set = &period.adaptation_sets[si];
411 let mut reps = Vec::new();
412 for (ri, rep) in set.representations.iter().enumerate() {
413 let reason = if !rep.dependency_ids.is_empty() {
414 Some(DropReason::DependencyRepresentation)
415 } else if unknown_essential(&rep.essential_properties) {
416 Some(DropReason::UnknownEssentialProperty)
417 } else if !rep.content_protections.is_empty() {
418 Some(DropReason::UnsupportedEncryption)
419 } else if !segment_info_ok(period, set, rep) {
420 Some(DropReason::InvalidSegmentInfo)
421 } else {
422 None
423 };
424 match reason {
425 Some(reason) => out.dropped.push(Dropped {
426 set: si,
427 rep: Some(ri),
428 reason,
429 }),
430 None => reps.push(ri),
431 }
432 }
433 if !reps.is_empty() {
434 usable.push((si, reps));
435 }
436 }
437
438 let video_sets: Vec<&(usize, Vec<usize>)> = usable
439 .iter()
440 .filter(|(si, _)| classes[*si] == SetClass::Video)
441 .collect();
442 for (si, reps) in &video_sets {
443 for ri in reps {
444 out.variants.push(RepRef {
445 period: period_idx,
446 set: *si,
447 rep: *ri,
448 });
449 }
450 }
451 let set_has_allowed = |(si, reps): &(usize, Vec<usize>)| -> bool {
452 let set = &period.adaptation_sets[*si];
453 reps.iter()
454 .any(|ri| rep_allowed(set, &set.representations[*ri], policy))
455 };
456 let main_video = video_sets
457 .iter()
458 .find(|(si, _)| is_main(&period.adaptation_sets[*si]));
459 let chosen_video_set = video_sets
460 .iter()
461 .copied()
462 .filter(|x| set_has_allowed(x))
463 .find(|(si, _)| is_main(&period.adaptation_sets[*si]))
464 .or_else(|| video_sets.iter().copied().find(|x| set_has_allowed(x)))
465 .or_else(|| main_video.copied())
466 .or_else(|| video_sets.first().copied());
467 if let Some((si, reps)) = chosen_video_set {
468 let set = &period.adaptation_sets[*si];
469 let pick = reps
470 .iter()
471 .filter(|ri| rep_allowed(set, &set.representations[**ri], policy))
472 .max_by_key(|ri| set.representations[**ri].bandwidth)
473 .or_else(|| {
474 reps.iter()
475 .min_by_key(|ri| set.representations[**ri].bandwidth)
476 });
477 if let Some(ri) = pick {
478 out.chosen_variant = out
479 .variants
480 .iter()
481 .position(|v| v.set == *si && v.rep == *ri);
482 }
483 }
484
485 for (si, reps) in usable
486 .iter()
487 .filter(|(si, _)| classes[*si] == SetClass::Audio)
488 {
489 let set = &period.adaptation_sets[*si];
490 out.audio_sets.push(AudioSet {
491 set: *si,
492 reps: reps
493 .iter()
494 .map(|ri| RepRef {
495 period: period_idx,
496 set: *si,
497 rep: *ri,
498 })
499 .collect(),
500 is_main: is_main(set),
501 });
502 }
503 let secondary = |set: &AdaptationSet| {
504 descriptors::roles(&set.roles).any(|r| matches!(r, Role::Commentary | Role::Description))
505 };
506 let audio_set_has_allowed = |a: &AudioSet| -> bool {
507 let set = &period.adaptation_sets[a.set];
508 a.reps
509 .iter()
510 .any(|r| rep_allowed(set, &set.representations[r.rep], policy))
511 };
512 let audio_candidates: Vec<&AudioSet> = if out.audio_sets.iter().any(audio_set_has_allowed) {
513 out.audio_sets
514 .iter()
515 .filter(|a| audio_set_has_allowed(a))
516 .collect()
517 } else {
518 out.audio_sets.iter().collect()
519 };
520 let chosen_set = policy
521 .preferred_audio_language
522 .as_deref()
523 .and_then(|pref| {
524 audio_candidates.iter().copied().find(|a| {
525 let s = &period.adaptation_sets[a.set];
526 s.lang.as_deref().is_some_and(|l| lang_matches(pref, l)) && !secondary(s)
527 })
528 })
529 .or_else(|| audio_candidates.iter().copied().find(|a| a.is_main))
530 .or_else(|| audio_candidates.first().copied());
531 if let Some(a) = chosen_set {
532 let set = &period.adaptation_sets[a.set];
533 let best = a
534 .reps
535 .iter()
536 .filter(|r| {
537 let rep = &set.representations[r.rep];
538 codec_allowed(
539 effective_common(set, rep).codecs.as_deref(),
540 &policy.codec_prefixes,
541 )
542 })
543 .max_by_key(|r| set.representations[r.rep].bandwidth)
544 .or_else(|| a.reps.first());
545 out.chosen_audio = best.copied();
546 }
547 out
548}