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