Skip to main content

dash_runtime/client/
engine.rs

1//! [`DashClient`]: the sans-IO engine for static (VOD) and dynamic (live)
2//! presentations. The live half (clock ladder, refresh, join, availability)
3//! is in `live`.
4
5mod live;
6
7use std::collections::{BTreeMap, BTreeSet, VecDeque};
8use std::sync::Arc;
9
10use container_probe::segment;
11use transmux::{TrackId, TrackSpec};
12use url::Url;
13
14use super::action::{Action, RepId, ResourceId};
15use super::config::{DashConfig, PullLimits};
16use super::demux::{self, Staged};
17use super::error::{Error, Failure, FailureClass, FailureKind, FatalKind, Rejection, Result};
18use super::meta::ResponseMeta;
19use super::output::{AudioRendition, DropCause, ManifestSelection, Notice, Output, VariantInfo};
20use crate::address::base_index;
21use crate::address::{AddressError, PlanError, PlanOptions, SegmentPlan, plan_representation};
22use crate::error::Unsupported;
23use crate::mpd::urls::{BaseCandidate, append_query, representation_candidates, segment_url_query};
24use crate::selection::{RepRef, Selection, SetClass, select};
25use crate::timing::clock::UtcClock;
26use crate::timing::period::period_timings;
27use broadcast_dash::descriptors::SCHEME_PERIOD_CONTINUITY;
28use broadcast_dash::inherit::{SegmentInfo, effective_common, resolve_segment_info};
29use broadcast_dash::{ByteRange, Descriptor, Mpd, PresentationType};
30
31/// A missing last segment shorter than this, in microseconds, is the tail a
32/// muxer declared past the media (one frame of the slowest common video
33/// rate), not a lost segment.
34const MISSING_TAIL_MAX_US: i128 = 50_000;
35/// Consecutive failures on one BaseURL alternate before the next is used
36/// (ETSI TS 103 285 10.8.2).
37const ROTATE_AFTER_FAILURES: u32 = 2;
38const STATUS_NOT_FOUND: u16 = 404;
39const STATUS_GONE: u16 = 410;
40const STATUS_TOO_EARLY: u16 = 425;
41/// Delivered resources remembered to answer a second delivery with
42/// `DuplicateResource`; older ones are forgotten so a long live session stays
43/// bounded.
44const MAX_REMEMBERED_DONE: usize = 4096;
45const START_WITH_SAP_1: u8 = 1;
46const START_WITH_SAP_2: u8 = 2;
47
48#[derive(Debug, Clone, Copy, PartialEq, Eq)]
49enum Phase {
50    NeedMpd,
51    Running,
52    Ended,
53    Fatal,
54}
55
56enum Candidate {
57    Ready(i128),
58    Wait(i128),
59    Idle,
60    Unaddressable(&'static str),
61}
62
63#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
64struct InitKey {
65    url: String,
66    range: Option<(u64, u64)>,
67}
68
69#[derive(Debug, Clone, Copy, PartialEq, Eq)]
70enum InitState {
71    Wanted,
72    InFlight,
73    Ready,
74}
75
76struct InitEntry {
77    state: InitState,
78    bytes: Option<Arc<Vec<u8>>>,
79    specs: Vec<TrackSpec>,
80    skipped_reported: BTreeSet<(Option<u32>, String)>,
81}
82
83#[derive(Debug, Clone, Copy, PartialEq, Eq)]
84enum ReqKind {
85    Init,
86    Index,
87    Segment,
88}
89
90struct ReqInfo {
91    rep: RepId,
92    reference: String,
93    range: Option<(u64, u64)>,
94    kind: ReqKind,
95    avail_from_us: Option<i128>,
96    avail_until_us: Option<i128>,
97}
98
99struct IndexReq {
100    range: ByteRange,
101}
102
103struct RepState {
104    id: RepId,
105    at: RepRef,
106    candidates: Vec<BaseCandidate>,
107    cand: usize,
108    cand_failures: u32,
109    query: Option<String>,
110    plan: Option<SegmentPlan>,
111    index: Option<IndexReq>,
112    init_key: InitKey,
113    next_issue: u64,
114    start_time_us: Option<i128>,
115    dead: bool,
116    set_id: Option<u64>,
117    set_pos: usize,
118    rep_key: String,
119    joined: bool,
120    last_time: Option<u64>,
121}
122
123enum SlotState {
124    Issued,
125    Held(Vec<u8>),
126    Staged(Staged),
127    Skipped { marker: bool },
128    Marker,
129}
130
131struct Slot {
132    id: Option<ResourceId>,
133    rep: usize,
134    plan_index: u64,
135    state: SlotState,
136}
137
138#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
139struct TrackIdentity {
140    kind: SetClass,
141    set_id: Option<u64>,
142    group: Option<u64>,
143    lang: Option<String>,
144    roles: Vec<(String, Option<String>)>,
145}
146
147struct Lane {
148    kind: SetClass,
149    set_id: Option<u64>,
150    identity: TrackIdentity,
151    reps: Vec<RepState>,
152    queue: VecDeque<Slot>,
153    base: u32,
154    count: u32,
155    announced_rep: Option<usize>,
156    announced_init: Option<InitKey>,
157    expired_run: u32,
158}
159
160impl Lane {
161    fn current(&self) -> Option<&RepState> {
162        self.reps.last().filter(|r| !r.dead)
163    }
164
165    fn exhausted(&self) -> bool {
166        match self.current() {
167            None => true,
168            Some(rep) => rep
169                .plan
170                .as_ref()
171                .is_some_and(|p| p.len().is_some_and(|n| rep.next_issue >= n)),
172        }
173    }
174}
175
176struct PeriodRun {
177    index: usize,
178    id: Option<String>,
179    start_us: i128,
180    lanes: Vec<Lane>,
181    selection: Selection,
182    notices: Vec<Output>,
183    started: bool,
184    issue_done: bool,
185    finished: bool,
186}
187
188struct PrevPeriod {
189    id: Option<String>,
190    lanes: Vec<(SetClass, Option<u64>, InitKey)>,
191}
192
193/// The sizes of a [`DashClient`]'s bookkeeping, see [`DashClient::state_size`].
194#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
195#[non_exhaustive]
196pub struct StateSize {
197    /// Period runs held (finished ones are dropped).
198    pub periods: usize,
199    /// Resources with a request on record.
200    pub requests: usize,
201    /// Requests awaiting an answer.
202    pub inflight: usize,
203    /// Failed resources awaiting the caller's retry.
204    pub parked: usize,
205    /// Resources with a consecutive-failure count.
206    pub failures: usize,
207    /// Delivered resources remembered for duplicate detection.
208    pub done: usize,
209    /// Initialization Segments held.
210    pub inits: usize,
211    /// Representations indexed.
212    pub reps: usize,
213    /// Fetches queued for the next poll.
214    pub queued: usize,
215    /// Outputs not yet drained.
216    pub outputs: usize,
217    /// Live-only bookkeeping entries (early waits, missing-segment flags).
218    pub live: usize,
219    /// Track identities holding an allocated `TrackId` range (current and previous Period only).
220    pub track_identities: usize,
221}
222
223/// The sans-IO DASH client for static and dynamic presentations.
224///
225/// Drive it like `hls_runtime::client::HlsClient`: call [`DashClient::poll`]
226/// for the next fetch, perform it, feed the result to the matching `on_*`
227/// call (or [`DashClient::on_failure`]), and drain
228/// [`DashClient::next_output`]. The core never opens a socket and never
229/// reads a clock; the `now_us` arguments are the caller's monotonic clock.
230/// A static presentation ignores it; a dynamic one maps it to UTC through an
231/// anchor (a `UTCTiming` sample the client takes through its own requests,
232/// [`DashClient::set_wall_clock`], or the MPD response `Date`) and uses that
233/// to join near the live edge, never request a segment before it exists,
234/// refresh the MPD and classify early, expired and missing segments. After
235/// each batch of calls ask [`DashClient::next_wake`] when to call
236/// [`DashClient::poll`] again if nothing else happens first.
237pub struct DashClient {
238    cfg: PullLimits,
239    dash: DashConfig,
240    mpd_url: String,
241    doc_url: Option<Url>,
242    mpd_query: Option<String>,
243    phase: Phase,
244    want_mpd: bool,
245    mpd_inflight: bool,
246    mpd: Option<Mpd>,
247    periods: Vec<PeriodRun>,
248    issue: usize,
249    deliver: usize,
250    next_rep: u32,
251    next_track_id: u32,
252    track_ids: BTreeMap<TrackIdentity, (u32, u32)>,
253    previous_identities: Vec<TrackIdentity>,
254    reqs: BTreeMap<ResourceId, ReqInfo>,
255    queue: VecDeque<ResourceId>,
256    inflight: BTreeSet<ResourceId>,
257    parked: BTreeSet<ResourceId>,
258    failures: BTreeMap<ResourceId, u32>,
259    done: BTreeSet<ResourceId>,
260    inits: BTreeMap<InitKey, InitEntry>,
261    rep_loc: BTreeMap<RepId, (usize, usize, usize)>,
262    outputs: VecDeque<Output>,
263    announced_tracks: Option<String>,
264    prev: Option<PrevPeriod>,
265    any_started: bool,
266    outstanding: usize,
267    notices_sent: Vec<Notice>,
268    clock: UtcClock,
269    mpd_failures: u32,
270    live: Option<live::Live>,
271    next_utc_id: u32,
272    last_was_discontinuity: bool,
273    remembered_done: VecDeque<ResourceId>,
274}
275
276fn without_query(url: &str) -> String {
277    match Url::parse(url) {
278        Ok(mut u) => {
279            u.set_query(None);
280            u.set_fragment(None);
281            u.to_string()
282        }
283        Err(_) => url.to_owned(),
284    }
285}
286
287fn same_origin(a: &Url, b: &Url) -> bool {
288    a.scheme() == b.scheme()
289        && a.host_str() == b.host_str()
290        && a.port_or_known_default() == b.port_or_known_default()
291}
292
293pub(crate) fn filter_candidates(
294    candidates: Vec<BaseCandidate>,
295    doc_url: Option<&Url>,
296    cross_origin_urls: bool,
297) -> Vec<BaseCandidate> {
298    if cross_origin_urls {
299        return candidates;
300    }
301    let Some(doc) = doc_url else {
302        return candidates;
303    };
304    candidates
305        .into_iter()
306        .filter(|c| {
307            c.resolve(Some(doc), "")
308                .and_then(|u| Url::parse(&u).ok())
309                .map(|p| same_origin(doc, &p))
310                .unwrap_or(false)
311        })
312        .collect()
313}
314
315fn address_name(e: &AddressError) -> &'static str {
316    match e {
317        AddressError::InvalidTemplate { .. } => "invalid_template",
318        AddressError::MissingTemplateValue { .. } => "missing_template_value",
319        AddressError::Unsupported(u) => u.name(),
320        AddressError::MissingMediaTemplate => "missing_media_template",
321        AddressError::MissingDuration => "missing_duration",
322        AddressError::TimelineNotIncreasing { .. } => "timeline_not_increasing",
323        AddressError::NumberNotIncreasing { .. } => "number_not_increasing",
324        AddressError::TimelineTooLong { .. } => "timeline_too_long",
325        AddressError::ListTimelineMismatch { .. } => "list_timeline_mismatch",
326        AddressError::Sidx(_) => "bad_sidx",
327        AddressError::Overflow => "overflow",
328        AddressError::IndexRequired => "index_required",
329        AddressError::EmptySegmentUrl => "empty_segment_url",
330        AddressError::ZeroSegmentDuration => "zero_segment_duration",
331        _ => "unsupported_feature",
332    }
333}
334
335fn plan_error_name(e: &PlanError) -> &'static str {
336    match e {
337        PlanError::NoSuchRepresentation => "no_such_representation",
338        PlanError::Inherit(_) => "inherit",
339        PlanError::Address(a) => address_name(a),
340        PlanError::PeriodUnresolved => "period_unresolved",
341        PlanError::MissingAvailabilityStartTime => "missing_availability_start_time",
342    }
343}
344
345fn range_pair(r: Option<ByteRange>) -> std::result::Result<Option<(u64, u64)>, DropCause> {
346    r.map(|r| r.offset_len().map_err(|u| DropCause::Addressing(u.name())))
347        .transpose()
348}
349
350fn continuity_signals(set: &broadcast_dash::AdaptationSet, previous: &str) -> bool {
351    let named = |d: &Descriptor| {
352        d.scheme_id_uri == SCHEME_PERIOD_CONTINUITY && d.value.as_deref() == Some(previous)
353    };
354    set.supplemental_properties.iter().any(named) || set.essential_properties.iter().any(named)
355}
356
357/// What a [`DashClient`] is configured by: the MPD it follows, the playback
358/// configuration and the pull limits.
359#[derive(Debug, Clone)]
360#[non_exhaustive]
361pub struct DashClientConfig {
362    /// The MPD URL to follow.
363    pub mpd_url: String,
364    /// Playback configuration.
365    pub dash: DashConfig,
366    /// Bounds on what the client pulls.
367    pub limits: PullLimits,
368}
369
370impl DashClientConfig {
371    /// A config following `mpd_url` with default [`DashConfig`] and
372    /// [`PullLimits`].
373    pub fn new(mpd_url: impl Into<String>) -> Self {
374        DashClientConfig {
375            mpd_url: mpd_url.into(),
376            dash: DashConfig::default(),
377            limits: PullLimits::default(),
378        }
379    }
380
381    /// Replace [`DashClientConfig::dash`].
382    #[must_use]
383    pub fn with_dash(mut self, dash: DashConfig) -> Self {
384        self.dash = dash;
385        self
386    }
387
388    /// Replace [`DashClientConfig::limits`].
389    #[must_use]
390    pub fn with_limits(mut self, limits: PullLimits) -> Self {
391        self.limits = limits;
392        self
393    }
394}
395
396impl DashClient {
397    /// A client for the MPD in `config`. Performs no I/O.
398    #[must_use]
399    pub fn new(config: DashClientConfig) -> Self {
400        let DashClientConfig {
401            mpd_url,
402            dash,
403            limits,
404        } = config;
405        let doc_url = Url::parse(&mpd_url).ok();
406        let mpd_query = doc_url.as_ref().and_then(|u| u.query()).map(str::to_owned);
407        Self {
408            cfg: limits,
409            dash,
410            mpd_url,
411            doc_url,
412            mpd_query,
413            phase: Phase::NeedMpd,
414            want_mpd: true,
415            mpd_inflight: false,
416            mpd: None,
417            periods: Vec::new(),
418            issue: 0,
419            deliver: 0,
420            next_rep: 0,
421            next_track_id: 1,
422            track_ids: BTreeMap::new(),
423            previous_identities: Vec::new(),
424            reqs: BTreeMap::new(),
425            queue: VecDeque::new(),
426            inflight: BTreeSet::new(),
427            parked: BTreeSet::new(),
428            failures: BTreeMap::new(),
429            done: BTreeSet::new(),
430            inits: BTreeMap::new(),
431            rep_loc: BTreeMap::new(),
432            outputs: VecDeque::new(),
433            announced_tracks: None,
434            prev: None,
435            any_started: false,
436            outstanding: 0,
437            notices_sent: Vec::new(),
438            clock: UtcClock::new(),
439            mpd_failures: 0,
440            live: None,
441            next_utc_id: 0,
442            last_was_discontinuity: false,
443            remembered_done: VecDeque::new(),
444        }
445    }
446
447    /// The MPD URL given to [`DashClient::new`].
448    #[must_use]
449    pub fn mpd_url(&self) -> &str {
450        &self.mpd_url
451    }
452
453    /// Use `base` (the URL the MPD was finally served from, after redirects)
454    /// as the document URL every relative reference resolves against
455    /// (23009-1 A.11).
456    pub fn set_mpd_base(&mut self, base: impl Into<String>) {
457        self.doc_url = Url::parse(&base.into()).ok();
458    }
459
460    /// `Some(true)` for a static MPD, `Some(false)` for a dynamic one, `None`
461    /// before the MPD arrived.
462    #[must_use]
463    pub fn is_static(&self) -> Option<bool> {
464        self.mpd
465            .as_ref()
466            .map(|m| m.kind == PresentationType::Static)
467    }
468
469    /// Whether [`Output::EndOfStream`] was produced.
470    #[must_use]
471    pub fn presentation_ended(&self) -> bool {
472        self.phase == Phase::Ended
473    }
474
475    /// The `@maxSegmentDuration` of the MPD, whole seconds rounded up.
476    #[must_use]
477    pub fn max_segment_duration_secs(&self) -> Option<u64> {
478        let d = self.mpd.as_ref()?.max_segment_duration?;
479        Some(d.as_secs() + u64::from(d.subsec_nanos() > 0))
480    }
481
482    /// The sizes of the client's bookkeeping, for tests and monitoring: every
483    /// collection is bounded by the in-flight window and the Periods in play
484    /// (a live session of any length does not grow them).
485    #[must_use]
486    pub fn state_size(&self) -> StateSize {
487        StateSize {
488            periods: self.periods.len(),
489            requests: self.reqs.len(),
490            inflight: self.inflight.len(),
491            parked: self.parked.len(),
492            failures: self.failures.len(),
493            done: self.done.len(),
494            inits: self.inits.len(),
495            reps: self.rep_loc.len(),
496            queued: self.queue.len(),
497            outputs: self.outputs.len(),
498            live: self.live.as_ref().map_or(0, live::Live::bookkeeping),
499            track_identities: self.track_ids.len(),
500        }
501    }
502
503    /// Whether the client still waits for the answer to the fetch of `id`.
504    /// An MPD reset (or a rejoin) abandons the requests of the old timeline:
505    /// a caller may cancel those downloads; a late answer is rejected with
506    /// [`Error::UnrequestedResource`] either way.
507    #[must_use]
508    pub fn awaits(&self, id: ResourceId) -> bool {
509        self.inflight.contains(&id)
510    }
511
512    /// The next drained output, if any.
513    pub fn next_output(&mut self) -> Option<Output> {
514        self.outputs.pop_front()
515    }
516
517    fn stopped(&self) -> bool {
518        matches!(self.phase, Phase::Ended | Phase::Fatal)
519    }
520
521    fn ensure_live(&self) -> Result<()> {
522        if self.stopped() {
523            Err(Error::Stopped)
524        } else {
525            Ok(())
526        }
527    }
528
529    fn go_fatal(&mut self, kind: FatalKind) {
530        if self.phase == Phase::Fatal {
531            return;
532        }
533        self.phase = Phase::Fatal;
534        self.queue.clear();
535        self.live = None;
536        self.outputs.push_back(Output::Fatal(kind));
537    }
538
539    fn discontinuity(&mut self) {
540        if !self.last_was_discontinuity {
541            self.last_was_discontinuity = true;
542            self.outputs.push_back(Output::Discontinuity);
543        }
544    }
545
546    fn mark_done(&mut self, id: ResourceId) {
547        if self.done.insert(id) {
548            self.remembered_done.push_back(id);
549            while self.remembered_done.len() > MAX_REMEMBERED_DONE {
550                if let Some(old) = self.remembered_done.pop_front() {
551                    self.done.remove(&old);
552                }
553            }
554        }
555    }
556
557    fn notice_once(&mut self, n: Notice) {
558        if !self.notices_sent.contains(&n) {
559            self.notices_sent.push(n);
560            self.outputs.push_back(Output::Notice(n));
561        }
562    }
563
564    // MPD ------------------------------------------------------------------
565
566    /// Feed the MPD body of a response with no headers of interest (a static
567    /// presentation, or a caller with no `Date`, `ETag` or redirect to
568    /// report). See [`DashClient::on_mpd_with`].
569    ///
570    /// # Errors
571    /// As [`DashClient::on_mpd_with`].
572    pub fn on_mpd(&mut self, now_us: u64, bytes: &[u8]) -> Result<()> {
573        self.on_mpd_with(now_us, bytes, &ResponseMeta::at(now_us))
574    }
575
576    /// Feed the MPD body with what the caller measured and read: the first
577    /// MPD starts the presentation, a later one is a refresh of a dynamic
578    /// presentation.
579    ///
580    /// # Errors
581    /// [`Error::Mpd`] for a body that does not parse (request it again with
582    /// [`DashClient::retry_mpd`]; the MPD already held stays in force);
583    /// [`Error::MpdNotAwaited`] when no MPD is awaited; [`Error::Stopped`].
584    pub fn on_mpd_with(
585        &mut self,
586        now_us: u64,
587        bytes: &[u8],
588        meta: &ResponseMeta<'_>,
589    ) -> Result<()> {
590        self.ensure_live()?;
591        if !self.mpd_inflight {
592            return Err(Error::MpdNotAwaited);
593        }
594        self.mpd_inflight = false;
595        let mpd = match Mpd::parse_with(bytes, &self.cfg.limits) {
596            Ok(m) => m,
597            Err(e) => {
598                self.mpd_failed(now_us);
599                return Err(Error::Mpd(e));
600            }
601        };
602        if self.mpd.is_some() {
603            self.refresh_mpd(now_us, mpd, bytes, meta);
604            return Ok(());
605        }
606        if mpd.kind == PresentationType::List {
607            // `@type="list"` (ISO/IEC 23009-1 ed.6) presents imported MPDs the
608            // client does not fetch: nothing here is playable as one stream.
609            self.go_fatal(FatalKind::Unsupported(Unsupported::PresentationTypeList));
610            return Ok(());
611        }
612        if mpd.periods.is_empty() {
613            self.go_fatal(FatalKind::MpdInvalid {
614                reason: "no_period",
615            });
616            return Ok(());
617        }
618        let dynamic = mpd.kind == PresentationType::Dynamic;
619        if dynamic && mpd.availability_start_time_us.is_none() {
620            self.go_fatal(FatalKind::MpdInvalid {
621                reason: "missing_availability_start_time",
622            });
623            return Ok(());
624        }
625        let features = mpd.features;
626        let event_streams = mpd.has_event_streams();
627        let preselections = mpd.has_preselections();
628        let patch_location = mpd.has_patch_location();
629        self.mpd = Some(mpd);
630        self.phase = Phase::Running;
631        if event_streams {
632            self.notice_once(Notice::EventStreamIgnored);
633        }
634        if preselections {
635            self.notice_once(Notice::PreselectionIgnored);
636        }
637        if patch_location {
638            self.notice_once(Notice::PatchIgnored);
639        }
640        if dynamic && features.low_latency {
641            self.notice_once(Notice::LowLatencyIgnored);
642        }
643        if dynamic {
644            self.start_live(now_us, bytes, meta);
645        } else {
646            if let Some(u) = meta.final_url.and_then(|f| self.accept_final_url(f)) {
647                self.doc_url = Some(u);
648            }
649            self.activate(0);
650            self.issue = 0;
651        }
652        Ok(())
653    }
654
655    /// The MPD fetch failed; nothing is counted against the presentation, an
656    /// MPD is never given up on. The caller backs off and calls
657    /// [`DashClient::retry_mpd`]. The returned class carries the count of
658    /// consecutive MPD failures.
659    pub fn on_mpd_failure(&mut self, now_us: u64, failure: Failure) -> FailureClass {
660        self.mpd_inflight = false;
661        self.want_mpd = false;
662        let attempt = self.mpd_failed(now_us);
663        let kind = match failure {
664            Failure::Status(STATUS_NOT_FOUND | STATUS_GONE) => FailureKind::Missing,
665            Failure::Status(STATUS_TOO_EARLY) => FailureKind::TooEarly,
666            Failure::Status(_) => FailureKind::Server,
667            Failure::Transport | Failure::Timeout => FailureKind::Transport,
668            Failure::TooLarge => FailureKind::Invalid,
669        };
670        FailureClass::Retry { attempt, kind }
671    }
672
673    /// Ask for the MPD again after a rejected body or a failed fetch.
674    pub fn retry_mpd(&mut self, _now_us: u64) {
675        if self.mpd_inflight || self.stopped() {
676            return;
677        }
678        let waiting = self.phase == Phase::NeedMpd
679            || self.live.as_ref().is_some_and(|l| l.refresh.awaiting_retry);
680        if waiting {
681            self.want_mpd = true;
682        }
683    }
684
685    // Poll -----------------------------------------------------------------
686
687    /// The next fetch the caller must perform, if any. Call it until it
688    /// returns `None`, then wait for a response or for
689    /// [`DashClient::next_wake`], whichever is first.
690    pub fn poll(&mut self, now_us: u64) -> Option<Action> {
691        if self.stopped() {
692            return None;
693        }
694        self.tick(now_us);
695        if self.stopped() {
696            return None;
697        }
698        if let Some(a) = self.mpd_action(now_us) {
699            return Some(a);
700        }
701        if self.phase != Phase::Running {
702            return None;
703        }
704        if let Some(a) = self.utc_action(now_us) {
705            return Some(a);
706        }
707        loop {
708            if self.queue.is_empty() {
709                self.fill_queue(now_us);
710                if self.queue.is_empty() {
711                    self.drain();
712                }
713            }
714            let id = self.queue.pop_front()?;
715            if let Some(action) = self.emit(id) {
716                return Some(action);
717            }
718            if self.stopped() {
719                return None;
720            }
721        }
722    }
723
724    fn window_open(&self) -> bool {
725        self.outstanding < self.cfg.window()
726    }
727
728    fn fill_queue(&mut self, now_us: u64) {
729        self.reset_wake();
730        loop {
731            let Some(pi) = (self.issue < self.periods.len()).then_some(self.issue) else {
732                return;
733            };
734            if self.live_blocked() {
735                return;
736            }
737            let exhausted = self.periods[pi].lanes.iter().all(Lane::exhausted);
738            if self.periods[pi].issue_done || (exhausted && !self.period_open(pi, now_us)) {
739                self.periods[pi].issue_done = true;
740                let next = self.periods[pi].index + 1;
741                if self.can_activate(next) {
742                    self.activate(next);
743                    self.issue = self.periods.len() - 1;
744                    continue;
745                }
746                return;
747            }
748            if exhausted {
749                self.note_exhausted(now_us);
750                return;
751            }
752            if !self.window_open() {
753                return;
754            }
755            let utc = self.utc_now(now_us);
756            let mut pick: Option<(usize, i128)> = None;
757            for li in 0..self.periods[pi].lanes.len() {
758                match self.lane_candidate(pi, li, utc, now_us) {
759                    Candidate::Ready(start_us) => {
760                        if pick.is_none_or(|(_, best)| start_us < best) {
761                            pick = Some((li, start_us));
762                        }
763                    }
764                    Candidate::Wait(at_utc_us) => self.note_wake(at_utc_us),
765                    Candidate::Idle => {}
766                    Candidate::Unaddressable(reason) => {
767                        self.rep_segment_error(pi, li, reason);
768                        if self.stopped() {
769                            return;
770                        }
771                        match self.lane_candidate(pi, li, utc, now_us) {
772                            Candidate::Ready(start_us) => pick = Some((li, start_us)),
773                            Candidate::Wait(at_utc_us) => self.note_wake(at_utc_us),
774                            Candidate::Idle | Candidate::Unaddressable(_) => {}
775                        }
776                    }
777                }
778            }
779            let Some((li, _)) = pick else {
780                return;
781            };
782            if !self.issue_segment(pi, li) {
783                if self.stopped() || self.periods[pi].lanes[li].current().is_some() {
784                    return;
785                }
786                continue;
787            }
788        }
789    }
790
791    fn lane_candidate(
792        &mut self,
793        pi: usize,
794        li: usize,
795        utc: Option<i128>,
796        now_us: u64,
797    ) -> Candidate {
798        match utc {
799            Some(utc) if self.is_live_mpd() => self.live_lane_candidate(pi, li, utc, now_us),
800            _ => {
801                let lane = &self.periods[pi].lanes[li];
802                let Some(rep) = lane.current() else {
803                    return Candidate::Idle;
804                };
805                let Some(plan) = rep.plan.as_ref() else {
806                    return Candidate::Idle;
807                };
808                match plan.get(rep.next_issue) {
809                    Ok(Some(s)) => Candidate::Ready(s.start_us),
810                    Ok(None) => Candidate::Idle,
811                    Err(e) => Candidate::Unaddressable(address_name(&e)),
812                }
813            }
814        }
815    }
816
817    fn issue_segment(&mut self, pi: usize, li: usize) -> bool {
818        let lane = &mut self.periods[pi].lanes[li];
819        let ri = lane.reps.len() - 1;
820        let rep = &mut lane.reps[ri];
821        let seg = match rep.plan.as_ref().map(|p| p.get(rep.next_issue)) {
822            Some(Ok(Some(seg))) => seg,
823            Some(Err(e)) => {
824                let reason = address_name(&e);
825                self.rep_segment_error(pi, li, reason);
826                return false;
827            }
828            _ => return false,
829        };
830        let range = match range_pair(seg.byte_range) {
831            Ok(r) => r,
832            Err(cause) => {
833                let reason = match cause {
834                    DropCause::Addressing(r) => r,
835                    _ => "unusable_url",
836                };
837                self.go_fatal(FatalKind::UnsupportedAddressing { reason });
838                return false;
839            }
840        };
841        let id = ResourceId::Segment {
842            rep: rep.id,
843            number: seg.number,
844        };
845        let plan_index = rep.next_issue;
846        rep.next_issue += 1;
847        rep.last_time = Some(seg.time_ticks);
848        let rep_id = rep.id;
849        lane.queue.push_back(Slot {
850            id: Some(id),
851            rep: ri,
852            plan_index,
853            state: SlotState::Issued,
854        });
855        self.outstanding += 1;
856        self.reqs.insert(
857            id,
858            ReqInfo {
859                rep: rep_id,
860                reference: seg.reference,
861                range,
862                kind: ReqKind::Segment,
863                avail_from_us: seg.available_from_us,
864                avail_until_us: seg.available_until_us,
865            },
866        );
867        self.queue.push_back(id);
868        true
869    }
870
871    fn rep(&self, id: RepId) -> Option<&RepState> {
872        let (p, l, r) = *self.rep_loc.get(&id)?;
873        self.periods.get(p)?.lanes.get(l)?.reps.get(r)
874    }
875
876    fn rep_mut(&mut self, id: RepId) -> Option<&mut RepState> {
877        let (p, l, r) = *self.rep_loc.get(&id)?;
878        self.periods.get_mut(p)?.lanes.get_mut(l)?.reps.get_mut(r)
879    }
880
881    fn rep_segment_error(&mut self, pi: usize, li: usize, reason: &'static str) -> bool {
882        if self.periods[pi].lanes[li].current().is_none() {
883            return false;
884        }
885        self.drop_rep(pi, li, DropCause::Addressing(reason));
886        if self
887            .periods
888            .iter()
889            .all(|p| p.lanes.iter().all(|l| l.current().is_none()))
890        {
891            self.go_fatal(FatalKind::UnsupportedAddressing { reason });
892        }
893        true
894    }
895
896    fn drop_rep(&mut self, pi: usize, li: usize, cause: DropCause) {
897        if let Some(ri) = self.periods[pi].lanes[li].reps.len().checked_sub(1) {
898            self.drop_rep_at(pi, li, ri, cause);
899        }
900        self.note_wake(i128::MIN);
901    }
902
903    fn drop_rep_in(&mut self, rep: RepId, cause: DropCause) {
904        if let Some((pi, li, _)) = self.rep_loc.get(&rep).copied() {
905            self.drop_rep(pi, li, cause);
906        }
907    }
908
909    fn drop_rep_at(&mut self, pi: usize, li: usize, ri: usize, cause: DropCause) {
910        self.periods[pi].lanes[li].reps[ri].dead = true;
911        let at = self.periods[pi].lanes[li].reps[ri].at;
912        self.outputs.push_back(Output::TrackDropped {
913            period: self.periods[pi].index,
914            set: at.set,
915            rep: Some(at.rep),
916            cause,
917        });
918    }
919
920    fn resolve(
921        &self,
922        rep: &RepState,
923        reference: &str,
924        media: bool,
925    ) -> std::result::Result<String, DropCause> {
926        let cand = rep
927            .candidates
928            .get(rep.cand % rep.candidates.len().max(1))
929            .ok_or(DropCause::UnusableUrl)?;
930        let mut url = cand
931            .resolve(self.doc_url.as_ref(), reference)
932            .ok_or(DropCause::UnusableUrl)?;
933        if media && let Some(q) = &rep.query {
934            url = append_query(&url, q);
935        }
936        let parsed = Url::parse(&url).map_err(|_| DropCause::UnusableUrl)?;
937        if parsed.scheme() == "data" {
938            return Err(DropCause::Unsupported(Unsupported::DataUrl));
939        }
940        if !matches!(parsed.scheme(), "http" | "https") {
941            return Err(DropCause::UnusableUrl);
942        }
943        if !self.dash.cross_origin_urls
944            && let Some(doc) = &self.doc_url
945            && !same_origin(doc, &parsed)
946        {
947            return Err(DropCause::CrossOrigin);
948        }
949        Ok(url)
950    }
951
952    pub(super) fn accept_final_url(&self, final_url: &str) -> Option<Url> {
953        let parsed = Url::parse(final_url).ok()?;
954        if !matches!(parsed.scheme(), "http" | "https") {
955            return None;
956        }
957        let pinned_elsewhere = !self.dash.cross_origin_urls
958            && self
959                .doc_url
960                .as_ref()
961                .is_some_and(|d| !same_origin(d, &parsed));
962        (!pinned_elsewhere).then_some(parsed)
963    }
964
965    fn emit(&mut self, id: ResourceId) -> Option<Action> {
966        let (rep_id, reference, range, kind) = {
967            let info = self.reqs.get(&id)?;
968            (info.rep, info.reference.clone(), info.range, info.kind)
969        };
970        let url = {
971            let rep = self.rep(rep_id)?;
972            match self.resolve(rep, &reference, kind == ReqKind::Segment) {
973                Ok(u) => u,
974                Err(cause) => {
975                    let reason = match cause {
976                        DropCause::CrossOrigin => "cross_origin",
977                        DropCause::Unsupported(u) => u.name(),
978                        _ => "unusable_url",
979                    };
980                    self.go_fatal(FatalKind::UnsupportedAddressing { reason });
981                    return None;
982                }
983            }
984        };
985        self.parked.remove(&id);
986        self.inflight.insert(id);
987        match kind {
988            ReqKind::Init => {
989                let key = self.rep(rep_id)?.init_key.clone();
990                if let Some(e) = self.inits.get_mut(&key) {
991                    e.state = InitState::InFlight;
992                }
993                Some(Action::FetchInit {
994                    id,
995                    url,
996                    byte_range: range,
997                })
998            }
999            ReqKind::Index => Some(Action::FetchIndex {
1000                id,
1001                url,
1002                byte_range: range,
1003            }),
1004            ReqKind::Segment => Some(Action::FetchSegment {
1005                id,
1006                url,
1007                byte_range: range,
1008            }),
1009        }
1010    }
1011
1012    // Period activation ----------------------------------------------------
1013
1014    fn activate(&mut self, index: usize) {
1015        let Some(mpd) = self.mpd.as_ref() else { return };
1016        let timing = period_timings(mpd).get(index).copied();
1017        let start_us = timing.and_then(|t| t.start_us).unwrap_or(0);
1018        let selection = select(mpd, index, &self.cfg.policy);
1019        let period_id = mpd.periods.get(index).and_then(|p| p.id.clone());
1020        let mut notices = Vec::new();
1021        let mut encryption = false;
1022        for d in &selection.dropped {
1023            if d.reason == crate::selection::DropReason::UnsupportedEncryption {
1024                encryption = true;
1025            }
1026            notices.push(Output::TrackDropped {
1027                period: index,
1028                set: d.set,
1029                rep: d.rep,
1030                cause: DropCause::Selection(d.reason),
1031            });
1032        }
1033        if encryption {
1034            notices.push(Output::Notice(Notice::UnsupportedEncryption));
1035        }
1036        if selection
1037            .dropped
1038            .iter()
1039            .any(|d| d.reason == crate::selection::DropReason::UnresolvedXlink)
1040        {
1041            notices.push(Output::Notice(Notice::XlinkSkipped));
1042        }
1043        let video = selection
1044            .chosen_variant
1045            .and_then(|i| selection.variants.get(i).copied());
1046        let audio = selection.chosen_audio;
1047        let mut run = PeriodRun {
1048            index,
1049            id: period_id,
1050            start_us,
1051            lanes: Vec::new(),
1052            selection,
1053            notices,
1054            started: false,
1055            issue_done: false,
1056            finished: false,
1057        };
1058        let pi = self.periods.len();
1059        for (kind, at) in [(SetClass::Video, video), (SetClass::Audio, audio)] {
1060            let Some(at) = at else { continue };
1061            match self.build_rep(at) {
1062                Ok(rep) => {
1063                    let resolved = at.resolve(self.mpd.as_ref().expect("mpd present"));
1064                    let set_id = resolved.and_then(|(_, s, _)| s.id);
1065                    let identity = resolved.map_or_else(
1066                        || TrackIdentity {
1067                            kind,
1068                            set_id: None,
1069                            group: None,
1070                            lang: None,
1071                            roles: Vec::new(),
1072                        },
1073                        |(_, s, _)| TrackIdentity {
1074                            kind,
1075                            set_id: s.id,
1076                            group: s.group,
1077                            lang: s.lang.clone(),
1078                            roles: s
1079                                .roles
1080                                .iter()
1081                                .map(|d| (d.scheme_id_uri.clone(), d.value.clone()))
1082                                .collect(),
1083                        },
1084                    );
1085                    run.lanes.push(Lane {
1086                        kind,
1087                        set_id,
1088                        identity,
1089                        reps: vec![rep],
1090                        queue: VecDeque::new(),
1091                        base: 0,
1092                        count: 0,
1093                        announced_rep: None,
1094                        announced_init: None,
1095                        expired_run: 0,
1096                    });
1097                }
1098                Err(cause) => run.notices.push(Output::TrackDropped {
1099                    period: index,
1100                    set: at.set,
1101                    rep: Some(at.rep),
1102                    cause,
1103                }),
1104            }
1105        }
1106        self.periods.push(run);
1107        for li in 0..self.periods[pi].lanes.len() {
1108            self.register_rep(pi, li, 0);
1109        }
1110    }
1111
1112    fn init_reference(
1113        mpd: &Mpd,
1114        at: RepRef,
1115        planned: &std::result::Result<crate::address::PlannedRepresentation, PlanError>,
1116    ) -> std::result::Result<Option<(String, Option<ByteRange>)>, DropCause> {
1117        match planned {
1118            Ok(p) => Ok(p.plan.init().map(|r| (r.reference, r.byte_range))),
1119            Err(PlanError::Address(AddressError::IndexRequired)) => {
1120                let Some((period, set, rep)) = at.resolve(mpd) else {
1121                    return Ok(None);
1122                };
1123                Ok(match resolve_segment_info(period, set, rep).ok() {
1124                    Some(SegmentInfo::Base(b)) => b
1125                        .initialization
1126                        .as_ref()
1127                        .map(|u| (u.source_url.clone().unwrap_or_default(), u.range)),
1128                    _ => None,
1129                })
1130            }
1131            Err(e) => Err(DropCause::Addressing(plan_error_name(e))),
1132        }
1133    }
1134
1135    fn build_rep(&mut self, at: RepRef) -> std::result::Result<RepState, DropCause> {
1136        let mpd = self.mpd.as_ref().expect("mpd present");
1137        let (period, set, rep) = at
1138            .resolve(mpd)
1139            .ok_or(DropCause::Addressing("no_such_representation"))?;
1140        let candidates = filter_candidates(
1141            representation_candidates(mpd, period, set, rep),
1142            self.doc_url.as_ref(),
1143            self.dash.cross_origin_urls,
1144        );
1145        if candidates.is_empty() && !self.dash.cross_origin_urls {
1146            return Err(DropCause::CrossOrigin);
1147        }
1148        let query = segment_url_query(mpd, period, set, rep, self.mpd_query.as_deref())
1149            .map_err(|_| DropCause::UnusableUrl)?;
1150        let planned = plan_representation(mpd, at, &self.plan_options());
1151        let init = Self::init_reference(mpd, at, &planned)?.ok_or(DropCause::NoInitialization)?;
1152        let (plan, index) = match planned {
1153            Ok(p) => {
1154                if p.plan.len().is_none() && mpd.kind != PresentationType::Dynamic {
1155                    return Err(DropCause::UnboundedPlan);
1156                }
1157                (Some(p.plan), None)
1158            }
1159            Err(PlanError::Address(AddressError::IndexRequired)) => {
1160                let info = resolve_segment_info(period, set, rep)
1161                    .map_err(|_| DropCause::Addressing("inherit"))?;
1162                let range = match info {
1163                    SegmentInfo::Base(b) => b.index_range,
1164                    _ => None,
1165                };
1166                let range = range.ok_or(DropCause::Addressing("index_required"))?;
1167                (None, Some(IndexReq { range }))
1168            }
1169            Err(e) => return Err(DropCause::Addressing(plan_error_name(&e))),
1170        };
1171        let init_range = range_pair(init.1)?;
1172        let id = RepId(self.next_rep);
1173        self.next_rep = self.next_rep.saturating_add(1);
1174        let mut state = RepState {
1175            id,
1176            at,
1177            candidates,
1178            cand: 0,
1179            cand_failures: 0,
1180            query,
1181            plan,
1182            index,
1183            init_key: InitKey {
1184                url: String::new(),
1185                range: init_range,
1186            },
1187            next_issue: 0,
1188            start_time_us: None,
1189            dead: false,
1190            set_id: set.id,
1191            set_pos: at.set,
1192            rep_key: rep.id.clone(),
1193            joined: mpd.kind != PresentationType::Dynamic,
1194            last_time: None,
1195        };
1196        let init_url = self.resolve(&state, &init.0, false)?;
1197        state.init_key.url = without_query(&init_url);
1198        if let Some(ix) = &state.index {
1199            range_pair(Some(ix.range))?;
1200            self.resolve(&state, "", false)?;
1201        }
1202        let first = state
1203            .plan
1204            .as_ref()
1205            .map(|p| p.get(0))
1206            .transpose()
1207            .map_err(|e| DropCause::Addressing(address_name(&e)))?
1208            .flatten();
1209        if let Some(seg) = first {
1210            range_pair(seg.byte_range)?;
1211            self.resolve(&state, &seg.reference, true)?;
1212        }
1213        Ok(state)
1214    }
1215
1216    fn register_rep(&mut self, pi: usize, li: usize, ri: usize) {
1217        let rep = &self.periods[pi].lanes[li].reps[ri];
1218        let rep_id = rep.id;
1219        let key = rep.init_key.clone();
1220        let has_index = rep.index.is_some();
1221        let index_range = rep.index.as_ref().map(|i| i.range);
1222        let init_reference = {
1223            let mpd = self.mpd.as_ref().expect("mpd present");
1224            let planned = rep.plan.as_ref().map(|p| p.init());
1225            match planned {
1226                Some(init) => init.map(|r| (r.reference, r.byte_range)),
1227                None => Self::init_reference(
1228                    mpd,
1229                    rep.at,
1230                    &Err(PlanError::Address(AddressError::IndexRequired)),
1231                )
1232                .ok()
1233                .flatten(),
1234            }
1235        };
1236        self.rep_loc.insert(rep_id, (pi, li, ri));
1237        if let std::collections::btree_map::Entry::Vacant(v) = self.inits.entry(key) {
1238            v.insert(InitEntry {
1239                state: InitState::Wanted,
1240                bytes: None,
1241                specs: Vec::new(),
1242                skipped_reported: BTreeSet::new(),
1243            });
1244            let (reference, range) = init_reference.unwrap_or_default();
1245            let init_id = ResourceId::Init { rep: rep_id };
1246            self.reqs.insert(
1247                init_id,
1248                ReqInfo {
1249                    rep: rep_id,
1250                    reference,
1251                    range: range.and_then(|r| r.offset_len().ok()),
1252                    kind: ReqKind::Init,
1253                    avail_from_us: None,
1254                    avail_until_us: None,
1255                },
1256            );
1257            self.queue.push_back(init_id);
1258        }
1259        if has_index && let Some(r) = index_range {
1260            let index_id = ResourceId::Index { rep: rep_id };
1261            self.reqs.insert(
1262                index_id,
1263                ReqInfo {
1264                    rep: rep_id,
1265                    reference: String::new(),
1266                    range: r.offset_len().ok(),
1267                    kind: ReqKind::Index,
1268                    avail_from_us: None,
1269                    avail_until_us: None,
1270                },
1271            );
1272            self.queue.push_back(index_id);
1273        }
1274    }
1275
1276    // Deliveries -----------------------------------------------------------
1277
1278    fn requested(&self, id: ResourceId) -> Result<()> {
1279        if self.done.contains(&id) {
1280            return Err(Error::DuplicateResource { id });
1281        }
1282        if !self.inflight.contains(&id) {
1283            return Err(Error::UnrequestedResource { id });
1284        }
1285        Ok(())
1286    }
1287
1288    fn body_rejection(&self, id: ResourceId, bytes: &[u8]) -> Option<Rejection> {
1289        if bytes.len() > self.cfg.max_segment_bytes {
1290            return Some(Rejection::TooLarge);
1291        }
1292        let range = self.reqs.get(&id).and_then(|r| r.range)?;
1293        (u64::try_from(bytes.len()).ok() != Some(range.1)).then_some(Rejection::WrongLength)
1294    }
1295
1296    fn max_failures(&self, id: ResourceId) -> u32 {
1297        match id {
1298            ResourceId::Segment { .. } => self.cfg.max_segment_failures,
1299            ResourceId::Init { .. } | ResourceId::Index { .. } => self.cfg.max_init_failures,
1300        }
1301    }
1302
1303    fn count_failure(
1304        &mut self,
1305        now_us: u64,
1306        id: ResourceId,
1307        kind: FailureKind,
1308        status: Option<u16>,
1309    ) -> FailureClass {
1310        self.inflight.remove(&id);
1311        let mut rotated = false;
1312        let count = {
1313            let n = self.failures.entry(id).or_insert(0);
1314            *n = n.saturating_add(1);
1315            *n
1316        };
1317        if count >= self.max_failures(id) {
1318            if self.is_live_mpd() && matches!(id, ResourceId::Segment { .. }) {
1319                return self.give_up_live_segment(id, status);
1320            }
1321            let fatal = match id {
1322                ResourceId::Segment { .. } => FatalKind::SegmentFetchFailed { id, status },
1323                ResourceId::Init { .. } | ResourceId::Index { .. } => {
1324                    FatalKind::InitFetchFailed { id, status }
1325                }
1326            };
1327            self.go_fatal(fatal);
1328            return FailureClass::GiveUp;
1329        }
1330        let doc = self.doc_url.clone();
1331        let cross = self.dash.cross_origin_urls;
1332        if let Some(rep) = self.rep_mut(id.rep()) {
1333            rep.cand_failures += 1;
1334            if rep.cand_failures >= ROTATE_AFTER_FAILURES && rep.candidates.len() > 1 {
1335                let len = rep.candidates.len();
1336                let mut next = (rep.cand + 1) % len;
1337                let mut found = false;
1338                for _ in 0..len {
1339                    let ok = cross
1340                        || doc.as_ref().is_none_or(|d| {
1341                            rep.candidates[next]
1342                                .resolve(Some(d), "")
1343                                .and_then(|u| Url::parse(&u).ok())
1344                                .map(|p| same_origin(d, &p))
1345                                .unwrap_or(false)
1346                        });
1347                    if ok {
1348                        found = true;
1349                        break;
1350                    }
1351                    next = (next + 1) % len;
1352                }
1353                if found && next != rep.cand {
1354                    rep.cand = next;
1355                    rep.cand_failures = 0;
1356                    rotated = true;
1357                }
1358            }
1359        }
1360        if rotated {
1361            self.after_rotation(now_us);
1362        }
1363        self.parked.insert(id);
1364        FailureClass::Retry {
1365            attempt: count,
1366            kind,
1367        }
1368    }
1369
1370    fn reject(&mut self, now_us: u64, id: ResourceId, reason: Rejection) -> Error {
1371        let class = self.count_failure(now_us, id, FailureKind::Invalid, None);
1372        Error::Rejected { id, reason, class }
1373    }
1374
1375    fn clear_success(&mut self, id: ResourceId) {
1376        self.inflight.remove(&id);
1377        self.parked.remove(&id);
1378        self.failures.remove(&id);
1379        self.mark_done(id);
1380    }
1381
1382    /// Feed an Initialization Segment.
1383    ///
1384    /// # Errors
1385    /// [`Error::Rejected`] for an unusable body (counted; see
1386    /// [`FailureClass`]), also for a segment held for this init that the
1387    /// demuxer then refused; [`Error::UnrequestedResource`],
1388    /// [`Error::DuplicateResource`], [`Error::Stopped`].
1389    pub fn on_init(&mut self, now_us: u64, id: ResourceId, bytes: &[u8]) -> Result<()> {
1390        self.ensure_live()?;
1391        let ResourceId::Init { rep } = id else {
1392            return Err(Error::UnrequestedResource { id });
1393        };
1394        self.requested(id)?;
1395        if let Some(r) = self.body_rejection(id, bytes) {
1396            return Err(self.reject(now_us, id, r));
1397        }
1398        if !segment::progressive_movie(bytes) {
1399            return Err(self.reject(now_us, id, Rejection::InitUndecodable));
1400        }
1401        let info = match demux::init_info(bytes) {
1402            Ok(i) => i,
1403            Err(_) => return Err(self.reject(now_us, id, Rejection::DemuxFailed)),
1404        };
1405        let key = self
1406            .rep(rep)
1407            .ok_or(Error::UnrequestedResource { id })?
1408            .init_key
1409            .clone();
1410        if let Some(e) = self.inits.get_mut(&key) {
1411            e.state = InitState::Ready;
1412            e.bytes = Some(Arc::new(bytes.to_vec()));
1413            e.specs = info.specs;
1414            for s in info.skipped {
1415                let k = (s.track_id.map(TrackId::value), s.fourcc.clone());
1416                if e.skipped_reported.insert(k) {
1417                    self.outputs.push_back(Output::TrackSkipped {
1418                        track_id: None,
1419                        fourcc: s.fourcc,
1420                        reason: s.reason,
1421                    });
1422                }
1423            }
1424        }
1425        self.clear_success(id);
1426        self.reqs.remove(&id);
1427        let held = self.demux_held(now_us, &key);
1428        self.drain();
1429        held
1430    }
1431
1432    fn demux_held(&mut self, now_us: u64, key: &InitKey) -> Result<()> {
1433        let Some(init) = self.inits.get(key).and_then(|e| e.bytes.clone()) else {
1434            return Ok(());
1435        };
1436        let mut first_error = None;
1437        let from = self.deliver.min(self.periods.len());
1438        for pi in from..self.periods.len() {
1439            for li in 0..self.periods[pi].lanes.len() {
1440                let n = self.periods[pi].lanes[li].queue.len();
1441                for qi in 0..n {
1442                    let (id, matches) = {
1443                        let lane = &self.periods[pi].lanes[li];
1444                        let slot = &lane.queue[qi];
1445                        let m = matches!(slot.state, SlotState::Held(_))
1446                            && lane.reps.get(slot.rep).is_some_and(|r| &r.init_key == key);
1447                        (slot.id, m)
1448                    };
1449                    if !matches {
1450                        continue;
1451                    }
1452                    let slot = &mut self.periods[pi].lanes[li].queue[qi];
1453                    let SlotState::Held(bytes) =
1454                        std::mem::replace(&mut slot.state, SlotState::Issued)
1455                    else {
1456                        continue;
1457                    };
1458                    match demux::demux_segment(&init, &bytes) {
1459                        Ok(staged) => {
1460                            self.periods[pi].lanes[li].queue[qi].state = SlotState::Staged(staged);
1461                        }
1462                        Err(_) => {
1463                            if let Some(id) = id {
1464                                self.inflight.insert(id);
1465                                let e = self.reject(now_us, id, Rejection::DemuxFailed);
1466                                first_error.get_or_insert(e);
1467                            }
1468                        }
1469                    }
1470                }
1471            }
1472        }
1473        match first_error {
1474            Some(e) => Err(e),
1475            None => Ok(()),
1476        }
1477    }
1478
1479    /// Feed the `sidx` of a `SegmentBase` Representation.
1480    ///
1481    /// # Errors
1482    /// As [`DashClient::on_init`].
1483    pub fn on_index(&mut self, now_us: u64, id: ResourceId, bytes: &[u8]) -> Result<()> {
1484        self.ensure_live()?;
1485        let ResourceId::Index { rep } = id else {
1486            return Err(Error::UnrequestedResource { id });
1487        };
1488        self.requested(id)?;
1489        if let Some(r) = self.body_rejection(id, bytes) {
1490            return Err(self.reject(now_us, id, r));
1491        }
1492        let sidx = match base_index::parse_index(bytes) {
1493            Ok(s) => s,
1494            Err(_) => return Err(self.reject(now_us, id, Rejection::BadIndex)),
1495        };
1496        let (at, range) = {
1497            let r = self.rep(rep).ok_or(Error::UnrequestedResource { id })?;
1498            let range = r.index.as_ref().map(|i| i.range);
1499            (r.at, range)
1500        };
1501        let Some(range) = range else {
1502            return Err(Error::UnrequestedResource { id });
1503        };
1504        let anchor = match base_index::anchor_after(&range) {
1505            Ok(a) => a,
1506            Err(_) => return Err(self.reject(now_us, id, Rejection::BadIndex)),
1507        };
1508        let planned = {
1509            let mpd = self.mpd.as_ref().expect("mpd present");
1510            plan_representation(mpd, at, &self.plan_options().with_index(&sidx, anchor))
1511        };
1512        self.clear_success(id);
1513        self.reqs.remove(&id);
1514        match planned {
1515            Ok(p) => {
1516                let start = self.rep_mut(rep).and_then(|r| r.start_time_us.take());
1517                match start.map_or(Ok(None), |t| p.plan.index_at_or_before(t)) {
1518                    Ok(index) => {
1519                        if let Some(r) = self.rep_mut(rep) {
1520                            r.next_issue = index.unwrap_or(0);
1521                            r.plan = Some(p.plan);
1522                        }
1523                    }
1524                    Err(e) => self.drop_rep_in(rep, DropCause::Addressing(address_name(&e))),
1525                }
1526            }
1527            Err(e) => self.drop_rep_in(rep, DropCause::Addressing(plan_error_name(&e))),
1528        }
1529        self.drain();
1530        Ok(())
1531    }
1532
1533    /// Feed a Media Segment.
1534    ///
1535    /// # Errors
1536    /// As [`DashClient::on_init`]; a progressive MP4 additionally ends the
1537    /// presentation.
1538    pub fn on_segment(&mut self, now_us: u64, id: ResourceId, bytes: &[u8]) -> Result<()> {
1539        self.ensure_live()?;
1540        let ResourceId::Segment { rep, .. } = id else {
1541            return Err(Error::UnrequestedResource { id });
1542        };
1543        self.requested(id)?;
1544        if let Some(r) = self.body_rejection(id, bytes) {
1545            return Err(self.reject(now_us, id, r));
1546        }
1547        if segment::progressive_movie(bytes) {
1548            self.inflight.remove(&id);
1549            self.go_fatal(FatalKind::NonFragmentedMp4 { id });
1550            return Err(Error::Rejected {
1551                id,
1552                reason: Rejection::NonFragmentedMp4,
1553                class: FailureClass::GiveUp,
1554            });
1555        }
1556        if !segment::media_fragment(bytes) {
1557            return Err(self.reject(now_us, id, Rejection::NotMedia));
1558        }
1559        let key = self
1560            .rep(rep)
1561            .ok_or(Error::UnrequestedResource { id })?
1562            .init_key
1563            .clone();
1564        let init = self.inits.get(&key).and_then(|e| e.bytes.clone());
1565        let state = match init {
1566            Some(init) => match demux::demux_segment(&init, bytes) {
1567                Ok(staged) => SlotState::Staged(staged),
1568                Err(_) => return Err(self.reject(now_us, id, Rejection::DemuxFailed)),
1569            },
1570            None => SlotState::Held(bytes.to_vec()),
1571        };
1572        let Some(slot) = self.slot_mut(id) else {
1573            return Err(Error::UnrequestedResource { id });
1574        };
1575        slot.state = state;
1576        self.inflight.remove(&id);
1577        self.parked.remove(&id);
1578        self.failures.remove(&id);
1579        self.mark_done(id);
1580        self.segment_delivered(id);
1581        self.drain();
1582        Ok(())
1583    }
1584
1585    fn slot_mut(&mut self, id: ResourceId) -> Option<&mut Slot> {
1586        let (p, l, _) = *self.rep_loc.get(&id.rep())?;
1587        self.periods
1588            .get_mut(p)?
1589            .lanes
1590            .get_mut(l)?
1591            .queue
1592            .iter_mut()
1593            .find(|s| s.id == Some(id))
1594    }
1595
1596    /// Report a failed fetch.
1597    ///
1598    /// # Errors
1599    /// [`Error::UnrequestedResource`] when `id` is not outstanding;
1600    /// [`Error::Stopped`].
1601    pub fn on_failure(
1602        &mut self,
1603        now_us: u64,
1604        id: ResourceId,
1605        failure: Failure,
1606    ) -> Result<FailureClass> {
1607        self.ensure_live()?;
1608        if !self.inflight.contains(&id) {
1609            return Err(Error::UnrequestedResource { id });
1610        }
1611        if self.is_live_mpd() && matches!(id, ResourceId::Segment { .. }) {
1612            return self.live_segment_failure(now_us, id, failure);
1613        }
1614        let status = match failure {
1615            Failure::Status(s) => Some(s),
1616            Failure::Transport | Failure::Timeout | Failure::TooLarge => None,
1617        };
1618        if matches!(status, Some(STATUS_NOT_FOUND | STATUS_GONE)) && self.is_missing_tail(id) {
1619            self.inflight.remove(&id);
1620            self.failures.remove(&id);
1621            if let Some(live) = self.live.as_mut() {
1622                live.early.remove(&id);
1623            }
1624            self.mark_done(id);
1625            if let Some(slot) = self.slot_mut(id) {
1626                slot.state = SlotState::Skipped { marker: false };
1627            }
1628            self.drain();
1629            return Ok(FailureClass::EndTail);
1630        }
1631        Ok(self.count_failure(now_us, id, live::failure_kind(failure), status))
1632    }
1633
1634    fn is_missing_tail(&self, id: ResourceId) -> bool {
1635        let ResourceId::Segment { rep, .. } = id else {
1636            return false;
1637        };
1638        let Some((pi, li, _)) = self.rep_loc.get(&rep).copied() else {
1639            return false;
1640        };
1641        let lane = &self.periods[pi].lanes[li];
1642        let Some(slot) = lane.queue.iter().find(|s| s.id == Some(id)) else {
1643            return false;
1644        };
1645        let Some(r) = lane.reps.get(slot.rep) else {
1646            return false;
1647        };
1648        let Some(plan) = r.plan.as_ref() else {
1649            return false;
1650        };
1651        let last = plan.len().and_then(|n| n.checked_sub(1));
1652        last == Some(slot.plan_index)
1653            && plan.len().is_some_and(|n| n > 1)
1654            && plan
1655                .get(slot.plan_index)
1656                .is_ok_and(|s| s.is_some_and(|s| s.duration_us < MISSING_TAIL_MAX_US))
1657    }
1658
1659    /// Request again a resource that [`DashClient::on_failure`] or a
1660    /// rejection classified [`FailureClass::Retry`], after the caller's
1661    /// backoff. `false` when `id` is not waiting for a retry.
1662    pub fn retry(&mut self, _now_us: u64, id: ResourceId) -> bool {
1663        if self.stopped() || !self.parked.remove(&id) {
1664            return false;
1665        }
1666        self.queue.push_front(id);
1667        true
1668    }
1669
1670    /// Give up on one segment: it counts as delivered and a
1671    /// [`Output::Discontinuity`] marks the gap. `false` when `id` is not a
1672    /// segment waiting for a retry.
1673    pub fn skip(&mut self, _now_us: u64, id: ResourceId) -> bool {
1674        if self.stopped() || !matches!(id, ResourceId::Segment { .. }) || !self.parked.remove(&id) {
1675            return false;
1676        }
1677        self.failures.remove(&id);
1678        if let Some(live) = self.live.as_mut() {
1679            live.early.remove(&id);
1680        }
1681        self.mark_done(id);
1682        if let Some(slot) = self.slot_mut(id) {
1683            slot.state = SlotState::Skipped { marker: true };
1684        }
1685        self.drain();
1686        true
1687    }
1688
1689    // Delivery -------------------------------------------------------------
1690
1691    fn drain(&mut self) {
1692        loop {
1693            if self.phase != Phase::Running {
1694                return;
1695            }
1696            let Some(pi) = (self.deliver < self.periods.len()).then_some(self.deliver) else {
1697                return;
1698            };
1699            if !self.periods[pi].started {
1700                if !self.ready_to_start(pi) {
1701                    return;
1702                }
1703                self.start_period(pi);
1704            }
1705            self.deliver_slots(pi);
1706            let p = &self.periods[pi];
1707            let complete = p.issue_done && p.lanes.iter().all(|l| l.queue.is_empty());
1708            if !complete {
1709                return;
1710            }
1711            self.periods[pi].finished = true;
1712            self.periods[pi].lanes.clear();
1713            let total = self.mpd.as_ref().map_or(0, |m| m.periods.len());
1714            let last_of_mpd = self.periods[pi].index + 1 >= total;
1715            self.deliver += 1;
1716            self.prune_finished();
1717            if last_of_mpd {
1718                if self.live_open() {
1719                    return;
1720                }
1721                if self.any_started {
1722                    self.outputs.push_back(Output::EndOfStream);
1723                    self.phase = Phase::Ended;
1724                    self.live = None;
1725                } else {
1726                    self.go_fatal(FatalKind::MpdInvalid {
1727                        reason: "no_playable_representation",
1728                    });
1729                }
1730                return;
1731            }
1732        }
1733    }
1734
1735    fn prune_finished(&mut self) {
1736        while self.deliver > 0 && self.issue > 0 && self.periods.first().is_some_and(|p| p.finished)
1737        {
1738            self.periods.remove(0);
1739            self.deliver -= 1;
1740            self.issue -= 1;
1741            self.rep_loc.retain(|_, loc| loc.0 != 0);
1742            for loc in self.rep_loc.values_mut() {
1743                loc.0 -= 1;
1744            }
1745        }
1746        let mut keep: BTreeSet<InitKey> = self
1747            .periods
1748            .iter()
1749            .flat_map(|p| p.lanes.iter())
1750            .flat_map(|l| l.reps.iter())
1751            .map(|r| r.init_key.clone())
1752            .collect();
1753        if let Some(prev) = &self.prev {
1754            keep.extend(prev.lanes.iter().map(|(_, _, k)| k.clone()));
1755        }
1756        self.inits.retain(|k, _| keep.contains(k));
1757    }
1758
1759    fn ready_to_start(&self, pi: usize) -> bool {
1760        !self.live_blocked()
1761            && self.periods[pi].lanes.iter().all(|lane| {
1762                lane.current().is_none_or(|rep| {
1763                    self.inits
1764                        .get(&rep.init_key)
1765                        .is_some_and(|e| e.state == InitState::Ready)
1766                })
1767            })
1768    }
1769
1770    fn start_period(&mut self, pi: usize) {
1771        self.periods[pi].started = true;
1772        let mut assignments: Vec<(usize, u32, u32)> = Vec::new();
1773        let mut used_ranges: Vec<(u32, u32)> = Vec::new();
1774        for li in 0..self.periods[pi].lanes.len() {
1775            let n = self.periods[pi].lanes[li]
1776                .current()
1777                .and_then(|r| self.inits.get(&r.init_key))
1778                .map_or(0, |e| u32::try_from(e.specs.len()).unwrap_or(u32::MAX));
1779            let identity = self.periods[pi].lanes[li].identity.clone();
1780            let base = allocate_track_base(
1781                &mut self.track_ids,
1782                &mut self.next_track_id,
1783                &mut used_ranges,
1784                identity,
1785                n,
1786            );
1787            assignments.push((li, base, n));
1788        }
1789        for (li, base, n) in assignments {
1790            let lane = &mut self.periods[pi].lanes[li];
1791            lane.base = base;
1792            lane.count = n;
1793        }
1794        let current: Vec<TrackIdentity> = self.periods[pi]
1795            .lanes
1796            .iter()
1797            .map(|lane| lane.identity.clone())
1798            .collect();
1799        prune_track_ids(
1800            &mut self.track_ids,
1801            current.iter().chain(self.previous_identities.iter()),
1802        );
1803        self.previous_identities = current;
1804        let notices = std::mem::take(&mut self.periods[pi].notices);
1805        self.outputs.extend(notices);
1806        let live: Vec<usize> = (0..self.periods[pi].lanes.len())
1807            .filter(|&li| self.periods[pi].lanes[li].current().is_some())
1808            .collect();
1809        if live.is_empty() {
1810            self.periods[pi].issue_done = true;
1811            return;
1812        }
1813        let first = !self.any_started;
1814        self.any_started = true;
1815        let continuous = !first && self.is_continuous(pi);
1816        let manifest = self.manifest(pi);
1817        let p = &self.periods[pi];
1818        self.outputs.push_back(Output::Manifest(manifest));
1819        self.outputs.push_back(Output::PeriodChanged {
1820            index: p.index,
1821            id: p.id.clone(),
1822            start_us: p.start_us,
1823            continuous,
1824        });
1825        if !first && !continuous {
1826            self.discontinuity();
1827        }
1828        let mut summary = Vec::new();
1829        for li in live {
1830            let lane = &mut self.periods[pi].lanes[li];
1831            let ri = lane.reps.len() - 1;
1832            let key = lane.reps[ri].init_key.clone();
1833            summary.push((lane.kind, lane.set_id, key.clone()));
1834            if !continuous && let Some(bytes) = self.inits.get(&key).and_then(|e| e.bytes.as_ref())
1835            {
1836                self.outputs.push_back(Output::Init(bytes.as_ref().clone()));
1837            }
1838            lane.announced_init = Some(key);
1839            lane.announced_rep = Some(ri);
1840        }
1841        self.announce_tracks(pi);
1842        self.prev = Some(PrevPeriod {
1843            id: self.periods[pi].id.clone(),
1844            lanes: summary,
1845        });
1846    }
1847
1848    fn is_continuous(&self, pi: usize) -> bool {
1849        let Some(prev) = &self.prev else { return false };
1850        let Some(prev_id) = prev.id.as_deref() else {
1851            return false;
1852        };
1853        let Some(mpd) = self.mpd.as_ref() else {
1854            return false;
1855        };
1856        let p = &self.periods[pi];
1857        let live: Vec<&Lane> = p.lanes.iter().filter(|l| l.current().is_some()).collect();
1858        if live.len() != prev.lanes.len() {
1859            return false;
1860        }
1861        live.iter()
1862            .zip(&prev.lanes)
1863            .all(|(lane, (kind, set_id, key))| {
1864                let Some(rep) = lane.current() else {
1865                    return false;
1866                };
1867                let Some((_, set, _)) = rep.at.resolve(mpd) else {
1868                    return false;
1869                };
1870                lane.kind == *kind
1871                    && lane.set_id.is_some()
1872                    && lane.set_id == *set_id
1873                    && &rep.init_key == key
1874                    && continuity_signals(set, prev_id)
1875            })
1876    }
1877
1878    fn announce_tracks(&mut self, pi: usize) {
1879        let mut specs: Vec<TrackSpec> = Vec::new();
1880        for lane in &self.periods[pi].lanes {
1881            let Some(rep) = lane.current() else { continue };
1882            let Some(entry) = self.inits.get(&rep.init_key) else {
1883                continue;
1884            };
1885            for (pos, spec) in entry.specs.iter().enumerate() {
1886                let pos = u32::try_from(pos).unwrap_or(u32::MAX);
1887                if pos >= lane.count {
1888                    break;
1889                }
1890                let mut s = spec.clone();
1891                s.track_id = TrackId::new(lane.base.saturating_add(pos));
1892                specs.push(s);
1893            }
1894        }
1895        let fingerprint = format!("{specs:?}");
1896        if self.announced_tracks.as_deref() != Some(fingerprint.as_str()) {
1897            self.announced_tracks = Some(fingerprint);
1898            self.outputs.push_back(Output::Tracks(specs));
1899        }
1900    }
1901
1902    fn deliver_slots(&mut self, pi: usize) {
1903        for li in 0..self.periods[pi].lanes.len() {
1904            while self.periods[pi].lanes[li]
1905                .queue
1906                .front()
1907                .is_some_and(|f| !matches!(f.state, SlotState::Issued | SlotState::Held(_)))
1908            {
1909                let Some(slot) = self.periods[pi].lanes[li].queue.pop_front() else {
1910                    break;
1911                };
1912                if !matches!(slot.state, SlotState::Marker) {
1913                    self.outstanding = self.outstanding.saturating_sub(1);
1914                }
1915                if let Some(id) = slot.id {
1916                    self.reqs.remove(&id);
1917                }
1918                match slot.state {
1919                    SlotState::Marker => self.discontinuity(),
1920                    SlotState::Skipped { marker } => {
1921                        if marker {
1922                            self.discontinuity();
1923                        }
1924                    }
1925                    SlotState::Staged(staged) => {
1926                        self.emit_staged(pi, li, slot.rep, slot.id, staged);
1927                    }
1928                    SlotState::Issued | SlotState::Held(_) => {}
1929                }
1930            }
1931        }
1932    }
1933
1934    fn emit_staged(
1935        &mut self,
1936        pi: usize,
1937        li: usize,
1938        rep_idx: usize,
1939        id: Option<ResourceId>,
1940        staged: Staged,
1941    ) {
1942        if self.periods[pi].lanes[li].announced_rep != Some(rep_idx) {
1943            let key = self.periods[pi].lanes[li].reps[rep_idx].init_key.clone();
1944            if self.periods[pi].lanes[li].announced_init.as_ref() != Some(&key) {
1945                if let Some(bytes) = self.inits.get(&key).and_then(|e| e.bytes.as_ref()) {
1946                    self.outputs.push_back(Output::Init(bytes.as_ref().clone()));
1947                }
1948                self.periods[pi].lanes[li].announced_init = Some(key);
1949            }
1950            self.periods[pi].lanes[li].announced_rep = Some(rep_idx);
1951            self.announce_tracks(pi);
1952        }
1953        let lane = &self.periods[pi].lanes[li];
1954        let key = &lane.reps[rep_idx].init_key;
1955        let locals: Vec<u32> = self
1956            .inits
1957            .get(key)
1958            .map(|e| e.specs.iter().map(|s| s.track_id.value()).collect())
1959            .unwrap_or_default();
1960        let (base, count) = (lane.base, lane.count);
1961        let map = |local: u32| -> Option<TrackId> {
1962            let pos = locals.iter().position(|l| *l == local)?;
1963            let pos = u32::try_from(pos).ok()?;
1964            (pos < count).then(|| TrackId::new(base.saturating_add(pos)))
1965        };
1966        if staged.damaged > 0
1967            && let Some(id) = id
1968        {
1969            self.outputs.push_back(Output::FragmentDamaged {
1970                id,
1971                skipped: staged.damaged,
1972            });
1973        }
1974        for s in staged.skipped {
1975            let k = (s.track_id.map(TrackId::value), s.fourcc.clone());
1976            let first = self
1977                .inits
1978                .get_mut(&self.periods[pi].lanes[li].reps[rep_idx].init_key.clone())
1979                .is_some_and(|e| e.skipped_reported.insert(k));
1980            if first {
1981                self.outputs.push_back(Output::TrackSkipped {
1982                    track_id: s.track_id.and_then(|t| map(t.value())),
1983                    fourcc: s.fourcc,
1984                    reason: s.reason,
1985                });
1986            }
1987        }
1988        for (local, samples) in staged.tracks {
1989            if samples.is_empty() {
1990                continue;
1991            }
1992            if let Some(track_id) = map(local) {
1993                self.last_was_discontinuity = false;
1994                self.outputs
1995                    .push_back(Output::Samples { track_id, samples });
1996            }
1997        }
1998    }
1999
2000    // Manifest and switching ----------------------------------------------
2001
2002    fn init_url_of(&self, at: RepRef) -> std::result::Result<Option<String>, DropCause> {
2003        let Some(mpd) = self.mpd.as_ref() else {
2004            return Ok(None);
2005        };
2006        let planned = plan_representation(mpd, at, &PlanOptions::default());
2007        let Some((reference, _)) = Self::init_reference(mpd, at, &planned)? else {
2008            return Ok(None);
2009        };
2010        let Some((period, set, rep)) = at.resolve(mpd) else {
2011            return Ok(None);
2012        };
2013        let cands = filter_candidates(
2014            representation_candidates(mpd, period, set, rep),
2015            self.doc_url.as_ref(),
2016            self.dash.cross_origin_urls,
2017        );
2018        Ok(cands
2019            .first()
2020            .and_then(|c| c.resolve(self.doc_url.as_ref(), &reference)))
2021    }
2022
2023    fn manifest(&self, pi: usize) -> ManifestSelection {
2024        let mpd = self.mpd.as_ref().expect("mpd present");
2025        let p = &self.periods[pi];
2026        let period = &mpd.periods[p.index];
2027        let audio_group = p
2028            .selection
2029            .chosen_audio
2030            .and_then(|a| period.adaptation_sets.get(a.set))
2031            .and_then(|s| s.group.or(s.id))
2032            .map(|g| g.to_string());
2033        let variants = p
2034            .selection
2035            .variants
2036            .iter()
2037            .filter_map(|&at| {
2038                let (_, set, rep) = at.resolve(mpd)?;
2039                let common = effective_common(set, rep);
2040                let init_url = self.init_url_of(at).ok().flatten();
2041                Some(VariantInfo {
2042                    rep: at,
2043                    init_url,
2044                    bandwidth: rep.bandwidth,
2045                    resolution: common.width.zip(common.height),
2046                    codecs: common.codecs,
2047                    audio_group: audio_group.clone(),
2048                })
2049            })
2050            .collect();
2051        let audio: Vec<AudioRendition> = p
2052            .selection
2053            .audio_sets
2054            .iter()
2055            .filter_map(|a| {
2056                let set = period.adaptation_sets.get(a.set)?;
2057                let first = a.reps.first()?;
2058                let (_, _, first_rep) = first.resolve(mpd)?;
2059                let chosen = p
2060                    .selection
2061                    .chosen_audio
2062                    .filter(|c| c.set == a.set)
2063                    .unwrap_or(*first);
2064                Some(AudioRendition {
2065                    set: a.set,
2066                    group: set.group.or(set.id).map(|g| g.to_string()),
2067                    name: set
2068                        .labels
2069                        .first()
2070                        .map(|l| l.text.clone())
2071                        .or_else(|| set.lang.clone())
2072                        .unwrap_or_else(|| first_rep.id.clone()),
2073                    language: set.lang.clone(),
2074                    default: a.is_main,
2075                    autoselect: true,
2076                    init_url: self.init_url_of(chosen).ok().flatten(),
2077                    reps: a.reps.clone(),
2078                })
2079            })
2080            .collect();
2081        let chosen_audio = p
2082            .selection
2083            .chosen_audio
2084            .and_then(|c| p.selection.audio_sets.iter().position(|a| a.set == c.set));
2085        ManifestSelection {
2086            period: p.index,
2087            variants,
2088            chosen: p.selection.chosen_variant,
2089            audio,
2090            chosen_audio,
2091        }
2092    }
2093
2094    /// Switch the video lane to variant `index` of the latest
2095    /// [`Output::Manifest`]. Segments already requested keep their
2096    /// Representation; the new one continues from the segment containing the
2097    /// next unrequested media time, its init is fetched, and the track set is
2098    /// re-announced (DESIGN 5.6). A switch that is not segment-aligned
2099    /// (`@segmentAlignment` with `@startWithSAP` 1 or 2 on the new set) is
2100    /// marked with a [`Output::Discontinuity`].
2101    ///
2102    /// # Errors
2103    /// [`Error::NoSuchVariant`]; [`Error::NotSwitchable`] when the Period
2104    /// being played is not the one being fetched, there is no video lane, or
2105    /// nothing is left to fetch; [`Error::Stopped`].
2106    pub fn select_representation(&mut self, index: usize) -> Result<()> {
2107        self.ensure_live()?;
2108        let pi = self.switchable_period()?;
2109        let at = *self.periods[pi]
2110            .selection
2111            .variants
2112            .get(index)
2113            .ok_or(Error::NoSuchVariant)?;
2114        self.switch_lane(pi, SetClass::Video, at)?;
2115        self.periods[pi].selection.chosen_variant = Some(index);
2116        Ok(())
2117    }
2118
2119    /// Play audio set `index` of the latest [`Output::Manifest`]'s `audio`
2120    /// list, or stop audio with `None`. The best Representation of the set
2121    /// the codec policy allows is used; segments already requested keep their
2122    /// Representation, the new one continues from the segment containing the
2123    /// next unrequested media time and the track set is re-announced. The
2124    /// audio track ids keep their numbers. `None` stops requesting audio
2125    /// segments (those already requested are still delivered); a later
2126    /// `Some` starts audio again.
2127    ///
2128    /// # Errors
2129    /// [`Error::NoSuchAudio`] for an index outside the list;
2130    /// [`Error::NotSwitchable`] when the Period being played is not the one
2131    /// being fetched or it has no audio lane; [`Error::Stopped`].
2132    pub fn select_audio(&mut self, index: Option<usize>) -> Result<()> {
2133        self.ensure_live()?;
2134        let pi = self.switchable_period()?;
2135        let Some(index) = index else {
2136            let lane = self.periods[pi]
2137                .lanes
2138                .iter_mut()
2139                .find(|l| l.kind == SetClass::Audio)
2140                .ok_or(Error::NotSwitchable)?;
2141            if let Some(rep) = lane.reps.last_mut() {
2142                rep.dead = true;
2143            }
2144            self.periods[pi].selection.chosen_audio = None;
2145            return Ok(());
2146        };
2147        let at = self.best_audio_rep(pi, index)?;
2148        self.switch_lane(pi, SetClass::Audio, at)?;
2149        self.periods[pi].selection.chosen_audio = Some(at);
2150        Ok(())
2151    }
2152
2153    fn switchable_period(&self) -> Result<usize> {
2154        let pi = self.deliver;
2155        if pi != self.issue || self.periods.get(pi).is_none_or(|p| !p.started) {
2156            return Err(Error::NotSwitchable);
2157        }
2158        Ok(pi)
2159    }
2160
2161    fn best_audio_rep(&self, pi: usize, index: usize) -> Result<RepRef> {
2162        let mpd = self.mpd.as_ref().expect("mpd present");
2163        let set = self.periods[pi]
2164            .selection
2165            .audio_sets
2166            .get(index)
2167            .ok_or(Error::NoSuchAudio)?;
2168        let bandwidth = |r: &RepRef| r.resolve(mpd).map_or(0, |(_, _, rep)| rep.bandwidth);
2169        set.reps
2170            .iter()
2171            .max_by_key(|r| bandwidth(r))
2172            .copied()
2173            .ok_or(Error::NoSuchAudio)
2174    }
2175
2176    fn switch_lane(&mut self, pi: usize, kind: SetClass, at: RepRef) -> Result<()> {
2177        let revive = kind == SetClass::Audio;
2178        let li = self.periods[pi]
2179            .lanes
2180            .iter()
2181            .position(|l| l.kind == kind && (revive || l.current().is_some()))
2182            .ok_or(Error::NotSwitchable)?;
2183        let read = {
2184            let lane = &self.periods[pi].lanes[li];
2185            let cur = lane.reps.last().ok_or(Error::NotSwitchable)?;
2186            let plan = cur.plan.as_ref().ok_or(Error::NotSwitchable)?;
2187            let end = plan
2188                .len()
2189                .and_then(|n| n.checked_sub(1))
2190                .map_or(Ok(None), |last| plan.get(last));
2191            (cur.at, cur.dead, plan.get(cur.next_issue), end)
2192        };
2193        let (current_at, was_dead, current, end) = read;
2194        let switch_us = match current {
2195            Ok(Some(seg)) => seg.start_us,
2196            found => {
2197                let end = end.map_err(|_| Error::NotSwitchable)?;
2198                let end = end.ok_or(Error::NotSwitchable)?;
2199                if let Err(e) = found {
2200                    self.rep_segment_error(pi, li, address_name(&e));
2201                    return Err(Error::NotSwitchable);
2202                }
2203                end.start_us.saturating_add(end.duration_us)
2204            }
2205        };
2206        if current_at == at && !was_dead {
2207            return Ok(());
2208        }
2209        let aligned = {
2210            let mpd = self.mpd.as_ref().expect("mpd present");
2211            at.resolve(mpd).is_some_and(|(_, set, _)| {
2212                set.segment_alignment
2213                    && matches!(
2214                        set.start_with_sap,
2215                        Some(START_WITH_SAP_1 | START_WITH_SAP_2)
2216                    )
2217            })
2218        };
2219        let mut rep = match self.build_rep(at) {
2220            Ok(r) => r,
2221            Err(cause) => {
2222                self.drop_rep(pi, li, cause);
2223                return Err(Error::NotSwitchable);
2224            }
2225        };
2226        match rep.plan.as_ref() {
2227            Some(plan) => {
2228                rep.next_issue = match plan.index_at_or_before(switch_us) {
2229                    Ok(Some(i)) => i,
2230                    _ => 0,
2231                };
2232                rep.joined = true;
2233                let last = rep
2234                    .next_issue
2235                    .checked_sub(1)
2236                    .map_or(Ok(None), |i| plan.get(i))
2237                    .map_err(|_| Error::NotSwitchable)?;
2238                rep.last_time = last.map(|s| s.time_ticks);
2239            }
2240            None => rep.start_time_us = Some(switch_us),
2241        }
2242        if !aligned {
2243            self.periods[pi].lanes[li].queue.push_back(Slot {
2244                id: None,
2245                rep: 0,
2246                plan_index: 0,
2247                state: SlotState::Marker,
2248            });
2249        }
2250        let ri = self.periods[pi].lanes[li].reps.len();
2251        self.periods[pi].lanes[li].reps.push(rep);
2252        self.periods[pi].issue_done = false;
2253        self.register_rep(pi, li, ri);
2254        Ok(())
2255    }
2256}
2257
2258fn ranges_overlap(a_base: u32, a_len: u32, b_base: u32, b_len: u32) -> bool {
2259    let a_end = a_base.saturating_add(a_len);
2260    let b_end = b_base.saturating_add(b_len);
2261    a_base < b_end && b_base < a_end
2262}
2263
2264fn allocate_track_base(
2265    track_ids: &mut BTreeMap<TrackIdentity, (u32, u32)>,
2266    next_track_id: &mut u32,
2267    used: &mut Vec<(u32, u32)>,
2268    identity: TrackIdentity,
2269    n: u32,
2270) -> u32 {
2271    if n == 0 {
2272        return 0;
2273    }
2274    if let Some((existing, len)) = track_ids.get(&identity).copied()
2275        && len >= n
2276        && !used.iter().any(|&(b, l)| ranges_overlap(existing, n, b, l))
2277    {
2278        used.push((existing, n));
2279        return existing;
2280    }
2281    let base = *next_track_id;
2282    *next_track_id = next_track_id.saturating_add(n);
2283    track_ids.insert(identity, (base, n));
2284    used.push((base, n));
2285    base
2286}
2287
2288/// Drops identities outside `keep`. `next_track_id` is never lowered, so a
2289/// pruned identity that returns gets a fresh range and no id is announced for
2290/// two identities. `next_track_id` saturates at `u32::MAX`; at 20 tracks per
2291/// Period and one Period every 10 s that takes over 700 years.
2292fn prune_track_ids<'a>(
2293    track_ids: &mut BTreeMap<TrackIdentity, (u32, u32)>,
2294    keep: impl Iterator<Item = &'a TrackIdentity>,
2295) {
2296    let keep: BTreeSet<&TrackIdentity> = keep.collect();
2297    track_ids.retain(|identity, _| keep.contains(identity));
2298}
2299
2300#[cfg(test)]
2301mod track_id_allocation_tests {
2302    use super::*;
2303
2304    #[test]
2305    fn two_same_identity_lanes_get_unique_bases() {
2306        let mut track_ids: BTreeMap<TrackIdentity, (u32, u32)> = BTreeMap::new();
2307        let mut next = 1u32;
2308        let mut used = Vec::new();
2309        let id = TrackIdentity {
2310            kind: SetClass::Audio,
2311            set_id: None,
2312            group: None,
2313            lang: Some("en".to_string()),
2314            roles: Vec::new(),
2315        };
2316        let a = allocate_track_base(&mut track_ids, &mut next, &mut used, id.clone(), 1);
2317        let b = allocate_track_base(&mut track_ids, &mut next, &mut used, id, 1);
2318        assert_ne!(a, b, "duplicate base for two lanes with equal identity");
2319    }
2320
2321    #[test]
2322    fn growing_identity_does_not_overlap_neighbor() {
2323        let mut track_ids: BTreeMap<TrackIdentity, (u32, u32)> = BTreeMap::new();
2324        let mut next = 1u32;
2325        let mut used = Vec::new();
2326        let a = TrackIdentity {
2327            kind: SetClass::Video,
2328            set_id: None,
2329            group: None,
2330            lang: None,
2331            roles: Vec::new(),
2332        };
2333        let b = TrackIdentity {
2334            kind: SetClass::Audio,
2335            set_id: None,
2336            group: None,
2337            lang: Some("en".to_string()),
2338            roles: Vec::new(),
2339        };
2340        // Period 1: A has 1 track.
2341        let a1 = allocate_track_base(&mut track_ids, &mut next, &mut used, a.clone(), 1);
2342        // B allocated in between.
2343        let b1 = allocate_track_base(&mut track_ids, &mut next, &mut used, b.clone(), 1);
2344        // Period 2: A needs 2 tracks; used reset for the new Period.
2345        let mut used2 = Vec::new();
2346        let a2 = allocate_track_base(&mut track_ids, &mut next, &mut used2, a.clone(), 2);
2347        // All allocated ids unique.
2348        let mut ids = std::collections::BTreeSet::new();
2349        ids.insert(a1);
2350        ids.insert(b1);
2351        ids.insert(a2);
2352        ids.insert(a2 + 1);
2353        assert_eq!(ids.len(), 4, "range overlap or duplicate ids");
2354    }
2355
2356    fn video(set_id: u64) -> TrackIdentity {
2357        TrackIdentity {
2358            kind: SetClass::Video,
2359            set_id: Some(set_id),
2360            group: None,
2361            lang: None,
2362            roles: Vec::new(),
2363        }
2364    }
2365
2366    fn audio(set_id: u64) -> TrackIdentity {
2367        TrackIdentity {
2368            kind: SetClass::Audio,
2369            set_id: Some(set_id),
2370            group: None,
2371            lang: None,
2372            roles: Vec::new(),
2373        }
2374    }
2375
2376    #[test]
2377    fn renumbered_periods_keep_the_identity_map_bounded_and_ids_unique_per_kind() {
2378        let mut track_ids: BTreeMap<TrackIdentity, (u32, u32)> = BTreeMap::new();
2379        let mut next = 1u32;
2380        let mut owner: BTreeMap<u32, SetClass> = BTreeMap::new();
2381        let mut previous: Vec<TrackIdentity> = Vec::new();
2382        let mut peak = 0usize;
2383        for period in 0..200u64 {
2384            let current = vec![video(period), audio(period)];
2385            let mut used = Vec::new();
2386            for identity in &current {
2387                let base =
2388                    allocate_track_base(&mut track_ids, &mut next, &mut used, identity.clone(), 1);
2389                let kind = owner.entry(base).or_insert(identity.kind);
2390                assert_eq!(*kind, identity.kind, "id {base} announced for two kinds");
2391            }
2392            prune_track_ids(&mut track_ids, current.iter().chain(previous.iter()));
2393            peak = peak.max(track_ids.len());
2394            previous = current;
2395        }
2396        assert!(peak <= 4, "identity map grew to {peak}");
2397        assert_eq!(next, 401, "ids must stay monotonic, never recycled");
2398    }
2399}