Skip to main content

dash_runtime/client/engine/
live.rs

1//! The live half of [`DashClient`]: the clock ladder, `UTCTiming` sampling,
2//! MPD refresh, the join near the live edge, availability gating and the
3//! classification of early, expired and missing segments.
4//!
5//! **Spec:** ISO/IEC 23009-1:2022 5.3.9.5.3 (availability), 5.4 (MPD
6//! updates and Reset), 5.8.5.7 (`UTCTiming`), Annex A.2, A.7, A.11;
7//! DASH-IF IOP v4.3 4.3.3, 4.4.4, 4.7, 4.8; RFC 8470. Verbatim text:
8//! `private/specs-verified/dash/23009-1/` and `dashif-iop-4.3/`. Design:
9//! `docs/dash-runtime/DESIGN.md` sections 3, 5.2-5.4 and 12.
10
11use std::collections::{BTreeMap, BTreeSet};
12
13use url::Url;
14
15use super::*;
16use crate::client::action::{MpdValidators, UtcId};
17use crate::client::config::DashClockPolicy;
18use crate::client::meta::UtcResponse;
19use crate::client::utc_sampler::{Step, UtcSampler};
20use crate::mpd::descriptors::SCHEME_RESET;
21use crate::timing::clock::{AdoptReason, Anchor, ClockSource, OfferOutcome, OfferPolicy};
22use crate::timing::live_edge::{join_index, oldest_in_window, presentation_delay_us};
23use crate::timing::period::duration_us;
24use crate::timing::utc_timing::{Exchange, UtcSample, sample_from_date_header};
25
26/// A forced second look at the UTC clock after a segment error is taken at
27/// most this often (23009-1 A.7: check the clock first, then the MPD).
28const FORCED_RESYNC_MIN_GAP_US: u64 = 30_000_000;
29/// Cap of the early-request cadence (DESIGN 5.4: 250 ms doubling to 1 s).
30const EARLY_RETRY_CAP_US: u64 = 1_000_000;
31/// Expiries in a row on one lane before the client rejoins the live edge.
32const EXPIRED_REJOIN_RUN: u32 = 3;
33/// A closed Period's lists are final this many segment durations (or one
34/// `minimumUpdatePeriod`, if longer) after its end (23009-1 5.4.1).
35const PERIOD_FINAL_SEGMENTS: i128 = 2;
36/// Bound on the steps one lane evaluation may take.
37const MAX_LANE_STEPS: usize = 8;
38/// The early-retry doubling stops growing here (the cap applies first).
39const EARLY_SHIFT_MAX: u32 = 20;
40
41#[derive(Default)]
42pub(super) struct Refresh {
43    pub(super) next_at: Option<u64>,
44    pub(super) forced_at: Option<u64>,
45    pub(super) last_request_us: Option<u64>,
46    pub(super) publish_time_us: Option<i64>,
47    pub(super) body: Vec<u8>,
48    pub(super) validators: MpdValidators,
49    pub(super) redirect_url: Option<String>,
50    pub(super) awaiting_retry: bool,
51}
52
53#[derive(Default, Clone, Copy)]
54pub(super) struct Early {
55    attempts: u32,
56    retry_at: Option<u64>,
57}
58
59pub(super) struct Live {
60    pub(super) ready: bool,
61    sampler: Option<UtcSampler>,
62    next_resync_us: Option<u64>,
63    last_forced_resync_us: Option<u64>,
64    pub(super) refresh: Refresh,
65    pub(super) early: BTreeMap<ResourceId, Early>,
66    missing_seen: BTreeSet<RepId>,
67    announced_clock: Option<ClockSource>,
68    wake_utc: Option<i128>,
69    mpd_exchange: Exchange,
70    fetch_utc_us: Option<i128>,
71}
72
73impl Live {
74    pub(super) fn bookkeeping(&self) -> usize {
75        self.early.len() + self.missing_seen.len()
76    }
77
78    fn new(now_us: u64) -> Self {
79        Self {
80            ready: false,
81            sampler: None,
82            next_resync_us: None,
83            last_forced_resync_us: None,
84            refresh: Refresh::default(),
85            early: BTreeMap::new(),
86            missing_seen: BTreeSet::new(),
87            announced_clock: None,
88            wake_utc: None,
89            mpd_exchange: Exchange::new(now_us, now_us),
90            fetch_utc_us: None,
91        }
92    }
93}
94
95fn status_of(failure: Failure) -> Option<u16> {
96    match failure {
97        Failure::Status(s) => Some(s),
98        Failure::Transport | Failure::Timeout | Failure::TooLarge => None,
99    }
100}
101
102pub(super) fn failure_kind(failure: Failure) -> FailureKind {
103    match failure {
104        Failure::Status(STATUS_NOT_FOUND | STATUS_GONE) => FailureKind::Missing,
105        Failure::Status(STATUS_TOO_EARLY) => FailureKind::TooEarly,
106        Failure::Status(_) => FailureKind::Server,
107        Failure::Transport | Failure::Timeout => FailureKind::Transport,
108        Failure::TooLarge => FailureKind::Invalid,
109    }
110}
111
112fn i128_from_i64(v: i64) -> i128 {
113    i128::from(v)
114}
115
116fn unix_i64(us: i128) -> i64 {
117    i64::try_from(us.clamp(i128::from(i64::MIN), i128::from(i64::MAX))).unwrap_or(i64::MAX)
118}
119
120impl DashClient {
121    // Public surface ---------------------------------------------------------
122
123    /// At the monotonic instant `now_us` the host's UTC clock read `unix_us`
124    /// microseconds since 1970-01-01T00:00:00Z (POSIX, no leap seconds). Both
125    /// reads must be taken back to back. Callable at any time and repeatedly
126    /// (at open, every ten minutes, on a system clock-change notification):
127    /// a dynamic presentation uses it when no `UTCTiming` answer is available
128    /// (or first, with [`DashClockPolicy::PreferHost`]); a static one ignores
129    /// it.
130    pub fn set_wall_clock(&mut self, now_us: u64, unix_us: u64) {
131        if self.stopped() {
132            return;
133        }
134        let sample = UtcSample {
135            mono_us: now_us,
136            utc_us: unix_i64(i128::from(unix_us)),
137            uncertainty_us: self.dash.host_clock_uncertainty_us,
138        };
139        let allow_downgrade = self.dash.clock_policy == DashClockPolicy::PreferHost;
140        let outcome = self.clock.offer(
141            sample,
142            ClockSource::Host,
143            now_us,
144            OfferPolicy::new(self.dash.utc_resync_us, allow_downgrade),
145        );
146        let stepped = matches!(outcome, OfferOutcome::Adopted(AdoptReason::Step));
147        let Some(live) = self.live.as_mut() else {
148            return;
149        };
150        if allow_downgrade && !live.ready {
151            let gating = live.sampler.as_ref().is_some_and(|s| s.gating);
152            if gating {
153                live.sampler = None;
154                self.finish_ladder(now_us);
155            }
156        }
157        if stepped {
158            self.force_refresh(now_us);
159        }
160        self.announce_clock();
161    }
162
163    /// The current clock anchor of a dynamic presentation.
164    #[must_use]
165    pub fn clock_anchor(&self) -> Option<Anchor> {
166        self.clock.anchor().copied()
167    }
168
169    /// UTC in Unix microseconds at the monotonic instant `now_us`, once an
170    /// anchor exists.
171    #[must_use]
172    pub fn utc_at(&self, now_us: u64) -> Option<i64> {
173        self.clock.utc_at(now_us)
174    }
175
176    /// The earliest monotonic instant at which [`DashClient::poll`] may have
177    /// something to do although no response arrived: a refresh due, a
178    /// segment becoming available, an early retry, a `UTCTiming` timeout or
179    /// resync. `None` when only a response (or a delivery) can move the client.
180    #[must_use]
181    pub fn next_wake(&self) -> Option<u64> {
182        let live = self.live.as_ref()?;
183        if self.stopped() {
184            return None;
185        }
186        let mut best: Option<u64> = None;
187        let mut take = |t: u64| best = Some(best.map_or(t, |b| b.min(t)));
188        if let Some(t) = self.refresh_due_at() {
189            take(t);
190        }
191        if let Some(s) = live.sampler.as_ref() {
192            if let Some(d) = s.deadline_us() {
193                take(d);
194            }
195            if let Some(w) = s.wait_deadline_us.filter(|_| s.gating) {
196                take(w);
197            }
198        } else if live.ready
199            && let Some(t) = live.next_resync_us
200        {
201            take(t);
202        }
203        for e in live.early.values() {
204            if let Some(t) = e.retry_at {
205                take(t);
206            }
207        }
208        if let Some(w) = live.wake_utc
209            && let Some(m) = self.clock.mono_for_utc(unix_i64(w))
210        {
211            take(m);
212        }
213        best
214    }
215
216    /// Report a `UTCTiming` answer.
217    ///
218    /// # Errors
219    /// [`Error::UnrequestedUtc`] when `id` is not the request awaited;
220    /// [`Error::Stopped`].
221    pub fn on_utc_response(
222        &mut self,
223        now_us: u64,
224        id: UtcId,
225        response: &UtcResponse<'_>,
226    ) -> Result<()> {
227        self.ensure_live()?;
228        let dash = self.dash;
229        let sample = {
230            let sampler = self
231                .live
232                .as_mut()
233                .and_then(|l| l.sampler.as_mut())
234                .filter(|s| s.awaits(id))
235                .ok_or(Error::UnrequestedUtc { id })?;
236            sampler.on_response(id, response, &dash)
237        };
238        if let Some(sample) = sample {
239            self.utc_sample(now_us, sample);
240        }
241        Ok(())
242    }
243
244    /// Report a failed `UTCTiming` request (the client abandons it by itself
245    /// after `DashConfig::utc_timeout_us`; a late answer then returns
246    /// [`Error::UnrequestedUtc`], which the caller ignores).
247    ///
248    /// # Errors
249    /// [`Error::UnrequestedUtc`] when `id` is not the request awaited;
250    /// [`Error::Stopped`].
251    pub fn on_utc_failure(&mut self, _now_us: u64, id: UtcId, failure: Failure) -> Result<()> {
252        self.ensure_live()?;
253        let sampler = self
254            .live
255            .as_mut()
256            .and_then(|l| l.sampler.as_mut())
257            .filter(|s| s.awaits(id))
258            .ok_or(Error::UnrequestedUtc { id })?;
259        sampler.on_failure(id, failure);
260        Ok(())
261    }
262
263    /// The MPD request was answered `304`: the held MPD is current. The
264    /// exchange moves `FetchTime` forward (23009-1 A.2).
265    ///
266    /// # Errors
267    /// [`Error::MpdNotAwaited`]; [`Error::Stopped`].
268    pub fn on_mpd_unchanged(&mut self, now_us: u64, meta: &ResponseMeta<'_>) -> Result<()> {
269        self.ensure_live()?;
270        if !self.mpd_inflight || self.live.is_none() {
271            return Err(Error::MpdNotAwaited);
272        }
273        self.mpd_inflight = false;
274        self.note_exchange(now_us, meta);
275        self.schedule_refresh(now_us);
276        Ok(())
277    }
278
279    // Predicates --------------------------------------------------------------
280
281    pub(super) fn is_live_mpd(&self) -> bool {
282        self.mpd
283            .as_ref()
284            .is_some_and(|m| m.kind == PresentationType::Dynamic)
285    }
286
287    pub(super) fn live_blocked(&self) -> bool {
288        self.is_live_mpd() && self.live.as_ref().is_none_or(|l| !l.ready)
289    }
290
291    pub(super) fn live_open(&self) -> bool {
292        self.mpd.as_ref().is_some_and(|m| {
293            m.kind == PresentationType::Dynamic
294                && m.minimum_update_period.is_some()
295                && m.media_presentation_duration.is_none()
296        })
297    }
298
299    /// Whether more segments may still be added to Period `pi` by a refresh:
300    /// its end is unknown, or it is closed but its lists do not yet reach the
301    /// end and the grace after which 5.4.1 makes them final has not passed.
302    pub(super) fn period_open(&self, pi: usize, now_us: u64) -> bool {
303        if !self.live_open() {
304            return false;
305        }
306        let Some(mpd) = self.mpd.as_ref() else {
307            return false;
308        };
309        let run = &self.periods[pi];
310        let Some(end) = period_timings(mpd).get(run.index).and_then(|t| t.end_us) else {
311            return true;
312        };
313        let len = end.saturating_sub(run.start_us);
314        let reaches = run.lanes.iter().filter_map(Lane::current).all(|r| {
315            r.plan.as_ref().is_some_and(|p| {
316                p.len()
317                    .and_then(|n| n.checked_sub(1))
318                    .and_then(|last| p.get(last))
319                    .is_some_and(|s| {
320                        s.start_us
321                            .saturating_add(s.duration_us)
322                            .saturating_add(MISSING_TAIL_MAX_US)
323                            >= len
324                    })
325            })
326        });
327        if reaches {
328            return false;
329        }
330        let Some(utc) = self.utc_now(now_us) else {
331            return false;
332        };
333        let ast = i128::from(mpd.availability_start_time_us.unwrap_or(0));
334        let mup = mpd.minimum_update_period.map_or(0, duration_us);
335        let seg = mpd.max_segment_duration.map_or(0, duration_us);
336        let grace = mup.max(seg.saturating_mul(PERIOD_FINAL_SEGMENTS));
337        utc < ast.saturating_add(end).saturating_add(grace)
338    }
339
340    pub(super) fn can_activate(&self, next: usize) -> bool {
341        let Some(mpd) = self.mpd.as_ref() else {
342            return false;
343        };
344        if next >= mpd.periods.len() {
345            return false;
346        }
347        mpd.kind == PresentationType::Static
348            || period_timings(mpd)
349                .get(next)
350                .is_some_and(|t| t.start_us.is_some())
351    }
352
353    pub(super) fn utc_now(&self, now_us: u64) -> Option<i128> {
354        self.clock.utc_at(now_us).map(i128_from_i64)
355    }
356
357    pub(super) fn plan_options(&self) -> PlanOptions<'static> {
358        let mut o = PlanOptions::default()
359            .with_availability_time_offset(self.dash.use_availability_time_offset);
360        if let Some(h) = self.horizon() {
361            o = o.with_horizon_utc_us(h);
362        }
363        o
364    }
365
366    fn horizon(&self) -> Option<i128> {
367        let mpd = self.mpd.as_ref()?;
368        let mup = duration_us(mpd.minimum_update_period?);
369        let fetch = self.live.as_ref()?.fetch_utc_us?;
370        Some(fetch.saturating_add(mup))
371    }
372
373    // Wake bookkeeping --------------------------------------------------------
374
375    pub(super) fn reset_wake(&mut self) {
376        if let Some(l) = self.live.as_mut() {
377            l.wake_utc = None;
378        }
379    }
380
381    pub(super) fn note_wake(&mut self, at_utc_us: i128) {
382        if let Some(l) = self.live.as_mut() {
383            l.wake_utc = Some(l.wake_utc.map_or(at_utc_us, |w| w.min(at_utc_us)));
384        }
385    }
386
387    pub(super) fn note_exhausted(&mut self, now_us: u64) {
388        if !self.live_open() {
389            return;
390        }
391        let tail_us = self
392            .periods
393            .get(self.issue)
394            .map(|p| {
395                p.lanes
396                    .iter()
397                    .filter_map(|l| l.current())
398                    .filter_map(|r| {
399                        let p = r.plan.as_ref()?;
400                        let last = p.len()?.checked_sub(1)?;
401                        p.get(last).map(|s| s.duration_us)
402                    })
403                    .max()
404                    .unwrap_or(0)
405            })
406            .unwrap_or(0);
407        let wait = u64::try_from(tail_us.max(0)).unwrap_or(u64::MAX);
408        self.force_refresh_after(now_us, wait);
409    }
410
411    // Timers ------------------------------------------------------------------
412
413    fn refresh_due_at(&self) -> Option<u64> {
414        let live = self.live.as_ref()?;
415        if !self.live_open() || self.mpd_inflight || self.want_mpd || live.refresh.awaiting_retry {
416            return None;
417        }
418        match (live.refresh.next_at, live.refresh.forced_at) {
419            (Some(a), Some(b)) => Some(a.min(b)),
420            (a, b) => a.or(b),
421        }
422    }
423
424    pub(super) fn tick(&mut self, now_us: u64) {
425        if self.live.is_none() {
426            return;
427        }
428        let gate_expired = self
429            .live
430            .as_ref()
431            .and_then(|l| l.sampler.as_ref())
432            .is_some_and(|s| s.gating && s.wait_deadline_us.is_some_and(|d| now_us >= d));
433        if gate_expired {
434            if let Some(l) = self.live.as_mut() {
435                l.sampler = None;
436                l.next_resync_us = Some(now_us.saturating_add(self.dash.utc_resync_us));
437            }
438            self.finish_ladder(now_us);
439            if self.stopped() {
440                return;
441            }
442        }
443        let resync_due = self.live.as_ref().is_some_and(|l| {
444            l.ready && l.sampler.is_none() && l.next_resync_us.is_some_and(|t| now_us >= t)
445        });
446        if resync_due {
447            if let Some(l) = self.live.as_mut() {
448                l.next_resync_us = None;
449            }
450            self.start_sampler(now_us, false);
451        }
452        let mut due: Vec<ResourceId> = Vec::new();
453        if let Some(l) = self.live.as_mut() {
454            for (id, e) in &mut l.early {
455                if e.retry_at.is_some_and(|t| now_us >= t) {
456                    e.retry_at = None;
457                    due.push(*id);
458                }
459            }
460        }
461        for id in due.into_iter().rev() {
462            if self.reqs.contains_key(&id) && !self.inflight.contains(&id) {
463                self.queue.push_front(id);
464            }
465        }
466        if self.refresh_due_at().is_some_and(|t| now_us >= t) {
467            self.want_mpd = true;
468        }
469    }
470
471    pub(super) fn mpd_action(&mut self, now_us: u64) -> Option<Action> {
472        if !self.want_mpd || self.mpd_inflight {
473            return None;
474        }
475        self.want_mpd = false;
476        self.mpd_inflight = true;
477        let mut url = self.mpd_url.clone();
478        let mut validators = MpdValidators::default();
479        if let Some(live) = self.live.as_mut() {
480            live.refresh.last_request_us = Some(now_us);
481            live.refresh.next_at = None;
482            live.refresh.forced_at = None;
483            validators = live.refresh.validators.clone();
484        }
485        if let Some(u) = self.update_url() {
486            url = u;
487        }
488        Some(Action::FetchMpd { url, validators })
489    }
490
491    fn update_url(&self) -> Option<String> {
492        let live = self.live.as_ref()?;
493        let mpd = self.mpd.as_ref()?;
494        if let Some(loc) = mpd.locations.first() {
495            let base = self.doc_url.as_ref();
496            let resolved = match base {
497                Some(b) => b.join(loc).ok().map(|u| u.to_string()),
498                None => Url::parse(loc).ok().map(|u| u.to_string()),
499            };
500            if let Some(url) = resolved
501                && let Ok(parsed) = Url::parse(&url)
502                && matches!(parsed.scheme(), "http" | "https")
503            {
504                let pin = !self.dash.cross_origin_urls;
505                let origin_ok = !pin
506                    || self
507                        .doc_url
508                        .as_ref()
509                        .is_none_or(|d| same_origin(d, &parsed));
510                if origin_ok {
511                    return Some(url);
512                }
513            }
514        }
515        live.refresh.redirect_url.clone()
516    }
517
518    pub(super) fn utc_action(&mut self, now_us: u64) -> Option<Action> {
519        let dash = self.dash;
520        let step = {
521            let live = self.live.as_mut()?;
522            let sampler = live.sampler.as_mut()?;
523            sampler.poll(now_us, &mut self.next_utc_id, &dash)
524        };
525        match step {
526            Step::Fetch(a) => Some(a),
527            Step::Sample(sample) => {
528                self.utc_sample(now_us, sample);
529                None
530            }
531            Step::Exhausted => {
532                self.sampler_exhausted(now_us);
533                None
534            }
535            Step::Idle => None,
536        }
537    }
538
539    // Clock ladder --------------------------------------------------------------
540
541    pub(super) fn start_live(&mut self, now_us: u64, bytes: &[u8], meta: &ResponseMeta<'_>) {
542        self.live = Some(Live::new(now_us));
543        if let Some(f) = meta.final_url
544            && let Some(u) = self.accept_final_url(f)
545        {
546            if let Some(l) = self.live.as_mut() {
547                l.refresh.redirect_url = Some(u.to_string());
548            }
549            self.doc_url = Some(u);
550        }
551        self.note_exchange(now_us, meta);
552        if let Some(l) = self.live.as_mut() {
553            l.refresh.body = bytes.to_vec();
554            l.refresh.publish_time_us = self.mpd.as_ref().and_then(|m| m.publish_time_us);
555            l.refresh.last_request_us = Some(meta.sent_us.min(meta.received_us));
556        }
557        self.speculative_activate(now_us);
558        let want_sampler = self.begin_sampler(now_us, meta);
559        if !want_sampler {
560            self.finish_ladder(now_us);
561        }
562        self.schedule_refresh(now_us);
563    }
564
565    fn note_exchange(&mut self, now_us: u64, meta: &ResponseMeta<'_>) {
566        let adopted = meta.final_url.and_then(|f| self.accept_final_url(f));
567        let Some(live) = self.live.as_mut() else {
568            return;
569        };
570        live.mpd_exchange = Exchange::new(meta.sent_us.min(meta.received_us), meta.received_us);
571        if let Some(e) = meta.etag {
572            live.refresh.validators.etag = Some(e.to_owned());
573        }
574        if let Some(m) = meta.last_modified {
575            live.refresh.validators.last_modified = Some(m.to_owned());
576        }
577        if let Some(u) = adopted {
578            live.refresh.redirect_url = Some(u.to_string());
579            self.doc_url = Some(u);
580        }
581        if let Some(date) = meta.date {
582            let exch = live.mpd_exchange;
583            if let Ok(sample) = sample_from_date_header(date, exch, self.dash.utc_max_rtt_us) {
584                self.clock.offer(
585                    sample,
586                    ClockSource::MpdDate,
587                    now_us,
588                    OfferPolicy::new(self.dash.utc_resync_us, false),
589                );
590            }
591        }
592        let mid = self.live.as_ref().map_or(now_us, |l| {
593            l.mpd_exchange.sent_us + (l.mpd_exchange.received_us - l.mpd_exchange.sent_us) / 2
594        });
595        let fetch_utc = self.clock.utc_at(mid).map(i128_from_i64).or_else(|| {
596            self.mpd
597                .as_ref()
598                .and_then(|m| m.publish_time_us)
599                .map(i128_from_i64)
600        });
601        if let Some(l) = self.live.as_mut() {
602            l.fetch_utc_us = fetch_utc;
603        }
604    }
605
606    fn begin_sampler(&mut self, now_us: u64, meta: &ResponseMeta<'_>) -> bool {
607        let _ = meta;
608        let host_first = self.dash.clock_policy == DashClockPolicy::PreferHost
609            && self
610                .clock
611                .anchor()
612                .is_some_and(|a| a.source == ClockSource::Host);
613        if host_first {
614            return false;
615        }
616        self.start_sampler(now_us, true)
617    }
618
619    fn start_sampler(&mut self, now_us: u64, gating: bool) -> bool {
620        let host_holds = self.dash.clock_policy == DashClockPolicy::PreferHost
621            && self
622                .clock
623                .anchor()
624                .is_some_and(|a| a.source == ClockSource::Host);
625        if !gating && (host_holds || !self.is_live_mpd()) {
626            return false;
627        }
628        let Some(mpd) = self.mpd.as_ref() else {
629            return false;
630        };
631        let elements = mpd.utc_timings.clone();
632        let unsupported = elements.iter().any(|e| {
633            matches!(
634                e.scheme,
635                crate::timing::utc_timing::UtcScheme::Ntp
636                    | crate::timing::utc_timing::UtcScheme::Sntp
637                    | crate::timing::utc_timing::UtcScheme::Unknown
638            )
639        });
640        let doc = self.doc_url.clone();
641        let pin = !self.dash.cross_origin_urls;
642        let mut cross = false;
643        let exchange = self
644            .live
645            .as_ref()
646            .map_or(Exchange::new(now_us, now_us), |l| l.mpd_exchange);
647        let mut accept = |value: &str| -> Option<String> {
648            let url = match &doc {
649                Some(d) => d.join(value).ok()?,
650                None => Url::parse(value).ok()?,
651            };
652            if pin
653                && let Some(d) = &doc
654                && !same_origin(d, &url)
655            {
656                cross = true;
657                return None;
658            }
659            matches!(url.scheme(), "http" | "https").then(|| url.to_string())
660        };
661        let mut sampler = UtcSampler::new(&elements, &mut accept, exchange);
662        if unsupported {
663            self.notice_once(Notice::UtcSchemeUnsupported);
664        }
665        if cross {
666            self.notice_once(Notice::UtcCrossOrigin);
667        }
668        if sampler.is_empty() {
669            return false;
670        }
671        sampler.gating = gating;
672        if gating {
673            sampler.wait_deadline_us = Some(now_us.saturating_add(self.dash.utc_wait_us));
674        }
675        if let Some(l) = self.live.as_mut() {
676            l.sampler = Some(sampler);
677        }
678        true
679    }
680
681    fn utc_sample(&mut self, now_us: u64, sample: UtcSample) {
682        let outcome = self.clock.offer(
683            sample,
684            ClockSource::UtcTiming,
685            now_us,
686            OfferPolicy::new(self.dash.utc_resync_us, false),
687        );
688        let stepped = matches!(outcome, OfferOutcome::Adopted(AdoptReason::Step));
689        let ready = {
690            let Some(live) = self.live.as_mut() else {
691                return;
692            };
693            live.sampler = None;
694            live.next_resync_us = Some(now_us.saturating_add(self.dash.utc_resync_us));
695            live.ready
696        };
697        if stepped {
698            self.force_refresh(now_us);
699        }
700        if ready {
701            self.announce_clock();
702        } else {
703            self.finish_ladder(now_us);
704        }
705    }
706
707    fn sampler_exhausted(&mut self, now_us: u64) {
708        let ready = {
709            let Some(live) = self.live.as_mut() else {
710                return;
711            };
712            live.sampler = None;
713            live.next_resync_us = Some(now_us.saturating_add(self.dash.utc_resync_us));
714            live.ready
715        };
716        if !ready {
717            self.finish_ladder(now_us);
718        }
719    }
720
721    fn finish_ladder(&mut self, now_us: u64) {
722        if self.clock.anchor().is_none() {
723            self.go_fatal(FatalKind::ClockUnavailable);
724            return;
725        }
726        if let Some(live) = self.live.as_mut() {
727            if live.ready {
728                return;
729            }
730            live.ready = true;
731        }
732        self.announce_clock();
733        self.on_clock_ready(now_us);
734    }
735
736    fn announce_clock(&mut self) {
737        if self.stopped() {
738            return;
739        }
740        let Some(anchor) = self.clock.anchor().copied() else {
741            return;
742        };
743        let Some(live) = self.live.as_mut() else {
744            return;
745        };
746        if !live.ready || live.announced_clock == Some(anchor.source) {
747            return;
748        }
749        live.announced_clock = Some(anchor.source);
750        self.outputs.push_back(Output::ClockSynced {
751            source: anchor.source,
752            uncertainty_us: anchor.uncertainty_us,
753        });
754    }
755
756    fn request_resync(&mut self, now_us: u64) {
757        let Some(live) = self.live.as_mut() else {
758            return;
759        };
760        if !live.ready || live.sampler.is_some() {
761            return;
762        }
763        let spaced = live
764            .last_forced_resync_us
765            .is_none_or(|t| now_us >= t.saturating_add(FORCED_RESYNC_MIN_GAP_US));
766        if spaced {
767            live.last_forced_resync_us = Some(now_us);
768            live.next_resync_us = Some(now_us);
769        }
770    }
771
772    // Join ----------------------------------------------------------------------
773
774    fn delay_us(&self, plan_hint_us: i128) -> i128 {
775        let Some(mpd) = self.mpd.as_ref() else {
776            return 0;
777        };
778        let max_seg = mpd.max_segment_duration.map_or(plan_hint_us, duration_us);
779        presentation_delay_us(
780            mpd.suggested_presentation_delay,
781            max_seg,
782            mpd.time_shift_buffer_depth.map(duration_us),
783            self.dash.live_delay_us.map(i128::from),
784        )
785    }
786
787    fn choose_join_period(&self, utc: i128) -> Option<usize> {
788        let mpd = self.mpd.as_ref()?;
789        let ast = i128::from(mpd.availability_start_time_us?);
790        let target = utc.saturating_sub(self.delay_us(0)).saturating_sub(ast);
791        let timings = period_timings(mpd);
792        let mut chosen: Option<usize> = None;
793        for (i, t) in timings.iter().enumerate() {
794            let Some(start) = t.start_us else { continue };
795            if chosen.is_none() || start <= target {
796                chosen = Some(i);
797            }
798        }
799        chosen
800    }
801
802    fn speculative_activate(&mut self, now_us: u64) {
803        let provisional = self.utc_now(now_us);
804        let pick = provisional
805            .and_then(|u| self.choose_join_period(u))
806            .or_else(|| {
807                let mpd = self.mpd.as_ref()?;
808                period_timings(mpd)
809                    .iter()
810                    .rposition(|t| t.start_us.is_some())
811            });
812        if let Some(index) = pick {
813            self.activate(index);
814            self.issue = self.periods.len() - 1;
815        }
816    }
817
818    fn on_clock_ready(&mut self, now_us: u64) {
819        let Some(utc) = self.utc_now(now_us) else {
820            return;
821        };
822        let target = self.choose_join_period(utc);
823        let current = self.periods.last().map(|p| p.index);
824        let started = self.periods.last().is_some_and(|p| p.started);
825        match (target, current) {
826            (Some(t), Some(c)) if t != c && !started => {
827                self.abandon_last_run();
828                self.activate(t);
829            }
830            (Some(t), None) => self.activate(t),
831            _ => {}
832        }
833        if !self.periods.is_empty() {
834            self.issue = self.periods.len() - 1;
835            if self.deliver >= self.periods.len() {
836                self.deliver = self.periods.len() - 1;
837            }
838        }
839        self.replan_all();
840        self.drain();
841    }
842
843    fn abandon_last_run(&mut self) {
844        let Some(run) = self.periods.pop() else {
845            return;
846        };
847        let pi = self.periods.len();
848        let mut ids: Vec<RepId> = Vec::new();
849        for lane in &run.lanes {
850            for rep in &lane.reps {
851                ids.push(rep.id);
852            }
853        }
854        self.rep_loc.retain(|_, (p, _, _)| *p != pi);
855        let rep_set: BTreeSet<RepId> = ids.iter().copied().collect();
856        let gone: Vec<ResourceId> = self
857            .reqs
858            .iter()
859            .filter(|(_, i)| rep_set.contains(&i.rep))
860            .map(|(id, _)| *id)
861            .collect();
862        for id in gone {
863            self.reqs.remove(&id);
864            self.inflight.remove(&id);
865            self.parked.remove(&id);
866            self.queue.retain(|q| *q != id);
867        }
868        let used: BTreeSet<InitKey> = self
869            .periods
870            .iter()
871            .flat_map(|p| p.lanes.iter())
872            .flat_map(|l| l.reps.iter())
873            .map(|r| r.init_key.clone())
874            .collect();
875        self.inits
876            .retain(|k, e| e.state == InitState::Ready || used.contains(k));
877        self.issue = self.periods.len().saturating_sub(1);
878    }
879
880    pub(super) fn live_lane_candidate(
881        &mut self,
882        pi: usize,
883        li: usize,
884        utc: i128,
885        now_us: u64,
886    ) -> Candidate {
887        let margin = i128::from(self.dash.availability_margin_us);
888        for _ in 0..MAX_LANE_STEPS {
889            let lane = &self.periods[pi].lanes[li];
890            let Some(rep) = lane.current() else {
891                return Candidate::Idle;
892            };
893            let Some(plan) = rep.plan.as_ref() else {
894                return Candidate::Idle;
895            };
896            if !rep.joined {
897                let hint = plan.get(0).map_or(0, |s| s.duration_us);
898                let delay = self.delay_us(hint);
899                match join_index(plan, utc, delay) {
900                    Some(idx) => {
901                        let last = idx
902                            .checked_sub(1)
903                            .and_then(|i| plan.get(i))
904                            .map(|s| s.time_ticks);
905                        let lane = &mut self.periods[pi].lanes[li];
906                        let ri = lane.reps.len() - 1;
907                        let rep = &mut lane.reps[ri];
908                        rep.next_issue = idx;
909                        rep.joined = true;
910                        rep.last_time = last;
911                        continue;
912                    }
913                    None => {
914                        let first = plan
915                            .get(0)
916                            .and_then(|s| s.available_from_us)
917                            .map(|a| a.saturating_add(margin));
918                        return match first {
919                            Some(a) if a > utc => Candidate::Wait(a),
920                            _ => {
921                                self.force_refresh(now_us);
922                                self.request_resync(now_us);
923                                Candidate::Idle
924                            }
925                        };
926                    }
927                }
928            }
929            let Some(seg) = plan.get(rep.next_issue) else {
930                return Candidate::Idle;
931            };
932            if seg.available_until_us.is_some_and(|u| u < utc) {
933                self.expire_before_request(pi, li, utc);
934                continue;
935            }
936            let from = seg
937                .available_from_us
938                .unwrap_or(i128::MIN)
939                .saturating_add(margin);
940            if utc < from {
941                return Candidate::Wait(from);
942            }
943            return Candidate::Ready(seg.start_us);
944        }
945        Candidate::Idle
946    }
947
948    fn expire_before_request(&mut self, pi: usize, li: usize, utc: i128) {
949        let (oldest, next) = {
950            let lane = &self.periods[pi].lanes[li];
951            let Some(rep) = lane.current() else { return };
952            let Some(plan) = rep.plan.as_ref() else {
953                return;
954            };
955            (oldest_in_window(plan, utc), rep.next_issue)
956        };
957        self.outputs.push_back(Output::Notice(Notice::FellBehind));
958        let lane = &mut self.periods[pi].lanes[li];
959        lane.expired_run = lane.expired_run.saturating_add(1);
960        let rejoin = lane.expired_run >= EXPIRED_REJOIN_RUN || oldest.is_none();
961        match oldest {
962            Some(o) if o > next && !rejoin => {
963                let ri = lane.reps.len() - 1;
964                lane.reps[ri].next_issue = o;
965                let last = lane.reps[ri]
966                    .plan
967                    .as_ref()
968                    .and_then(|p| o.checked_sub(1).and_then(|i| p.get(i)))
969                    .map(|s| s.time_ticks);
970                lane.reps[ri].last_time = last;
971                push_marker(lane);
972            }
973            _ => self.rejoin_lane(pi, li),
974        }
975    }
976
977    fn rejoin_lane(&mut self, pi: usize, li: usize) {
978        let lane = &mut self.periods[pi].lanes[li];
979        lane.expired_run = 0;
980        if let Some(rep) = lane.reps.last_mut() {
981            rep.joined = false;
982        }
983        push_marker(lane);
984    }
985
986    pub(super) fn segment_delivered(&mut self, id: ResourceId) {
987        let rep = id.rep();
988        if let Some(live) = self.live.as_mut() {
989            live.early.remove(&id);
990            live.missing_seen.remove(&rep);
991        }
992        if let Some((pi, li, _)) = self.rep_loc.get(&rep).copied()
993            && let Some(lane) = self.periods.get_mut(pi).and_then(|p| p.lanes.get_mut(li))
994        {
995            lane.expired_run = 0;
996        }
997    }
998
999    // Failures ------------------------------------------------------------------
1000
1001    pub(super) fn live_segment_failure(
1002        &mut self,
1003        now_us: u64,
1004        id: ResourceId,
1005        failure: Failure,
1006    ) -> Result<FailureClass> {
1007        let status = status_of(failure);
1008        if matches!(
1009            status,
1010            Some(STATUS_NOT_FOUND | STATUS_GONE | STATUS_TOO_EARLY)
1011        ) && let Some(utc) = self.utc_now(now_us)
1012        {
1013            let (from, until) = self
1014                .reqs
1015                .get(&id)
1016                .map_or((None, None), |r| (r.avail_from_us, r.avail_until_us));
1017            let grace = i128::from(self.dash.early_grace_us);
1018            let past_window = until.is_some_and(|u| utc > u);
1019            let refused_past_window = past_window
1020                && status == Some(STATUS_TOO_EARLY)
1021                && self
1022                    .clock
1023                    .anchor()
1024                    .is_some_and(|a| a.source == ClockSource::UtcTiming);
1025            let early = !refused_past_window
1026                && (status == Some(STATUS_TOO_EARLY)
1027                    || from.is_some_and(|f| utc < f.saturating_add(grace)));
1028            if early {
1029                return Ok(self.early_wait(now_us, id, from, utc));
1030            }
1031            if past_window {
1032                return Ok(self.expired_segment(now_us, id, utc));
1033            }
1034        }
1035        let kind = failure_kind(failure);
1036        if kind == FailureKind::Missing {
1037            let first = self
1038                .live
1039                .as_mut()
1040                .is_some_and(|l| l.missing_seen.insert(id.rep()));
1041            if first {
1042                self.force_refresh(now_us);
1043                self.request_resync(now_us);
1044            }
1045        }
1046        Ok(self.count_failure(now_us, id, kind, status))
1047    }
1048
1049    fn early_wait(
1050        &mut self,
1051        now_us: u64,
1052        id: ResourceId,
1053        from: Option<i128>,
1054        utc: i128,
1055    ) -> FailureClass {
1056        self.inflight.remove(&id);
1057        let base = self.dash.early_retry_us;
1058        let cap = EARLY_RETRY_CAP_US.max(base);
1059        let retry_at = {
1060            let Some(live) = self.live.as_mut() else {
1061                return FailureClass::Retry {
1062                    attempt: 1,
1063                    kind: FailureKind::TooEarly,
1064                };
1065            };
1066            let e = live.early.entry(id).or_default();
1067            e.attempts = e.attempts.saturating_add(1);
1068            let shift = e.attempts.saturating_sub(1).min(EARLY_SHIFT_MAX);
1069            let delay = base.saturating_mul(1u64 << shift).min(cap);
1070            let at = now_us.saturating_add(delay);
1071            e.retry_at = Some(at);
1072            at
1073        };
1074        if from.is_some_and(|f| utc >= f) {
1075            self.request_resync(now_us);
1076        }
1077        FailureClass::NotYetAvailable {
1078            retry_at_us: retry_at,
1079        }
1080    }
1081
1082    fn expired_segment(&mut self, _now_us: u64, id: ResourceId, utc: i128) -> FailureClass {
1083        self.inflight.remove(&id);
1084        self.failures.remove(&id);
1085        if let Some(live) = self.live.as_mut() {
1086            live.early.remove(&id);
1087        }
1088        self.mark_done(id);
1089        if let Some(slot) = self.slot_mut(id) {
1090            slot.state = SlotState::Skipped { marker: true };
1091        }
1092        self.outputs.push_back(Output::Notice(Notice::FellBehind));
1093        if let Some((pi, li, _)) = self.rep_loc.get(&id.rep()).copied() {
1094            let (oldest, next) = {
1095                let lane = &self.periods[pi].lanes[li];
1096                match lane.current().and_then(|r| r.plan.as_ref().map(|p| (r, p))) {
1097                    Some((r, p)) => (oldest_in_window(p, utc), r.next_issue),
1098                    None => (None, 0),
1099                }
1100            };
1101            let lane = &mut self.periods[pi].lanes[li];
1102            lane.expired_run = lane.expired_run.saturating_add(1);
1103            let rejoin = lane.expired_run >= EXPIRED_REJOIN_RUN || oldest.is_none();
1104            match oldest {
1105                Some(o) if o > next && !rejoin => {
1106                    let ri = lane.reps.len() - 1;
1107                    lane.reps[ri].next_issue = o;
1108                    let last = lane.reps[ri]
1109                        .plan
1110                        .as_ref()
1111                        .and_then(|p| o.checked_sub(1).and_then(|i| p.get(i)))
1112                        .map(|s| s.time_ticks);
1113                    lane.reps[ri].last_time = last;
1114                }
1115                Some(o) if o <= next && !rejoin => {}
1116                _ => self.rejoin_lane(pi, li),
1117            }
1118        }
1119        self.drain();
1120        FailureClass::Expired
1121    }
1122
1123    pub(super) fn give_up_live_segment(
1124        &mut self,
1125        id: ResourceId,
1126        status: Option<u16>,
1127    ) -> FailureClass {
1128        self.inflight.remove(&id);
1129        self.failures.remove(&id);
1130        if let Some(live) = self.live.as_mut() {
1131            live.early.remove(&id);
1132        }
1133        self.mark_done(id);
1134        if let Some(slot) = self.slot_mut(id) {
1135            slot.state = SlotState::Skipped { marker: true };
1136        }
1137        self.outputs.push_back(Output::SegmentLost { id, status });
1138        self.drain();
1139        FailureClass::SegmentSkipped
1140    }
1141
1142    pub(super) fn after_rotation(&mut self, now_us: u64) {
1143        if self.is_live_mpd() {
1144            self.force_refresh(now_us);
1145        }
1146    }
1147
1148    pub(super) fn mpd_failed(&mut self, _now_us: u64) -> u32 {
1149        self.mpd_failures = self.mpd_failures.saturating_add(1);
1150        if let Some(live) = self.live.as_mut() {
1151            live.refresh.awaiting_retry = true;
1152        }
1153        self.mpd_failures
1154    }
1155
1156    // Refresh ---------------------------------------------------------------------
1157
1158    fn force_refresh(&mut self, now_us: u64) {
1159        self.force_refresh_after(now_us, 0);
1160    }
1161
1162    fn force_refresh_after(&mut self, now_us: u64, extra_us: u64) {
1163        let floor = self.dash.min_mpd_refresh_us.max(extra_us);
1164        let live_open = self.live_open();
1165        let Some(live) = self.live.as_mut() else {
1166            return;
1167        };
1168        if !live_open {
1169            return;
1170        }
1171        let earliest = live
1172            .refresh
1173            .last_request_us
1174            .map_or(now_us, |t| t.saturating_add(floor))
1175            .max(now_us);
1176        live.refresh.forced_at = Some(live.refresh.forced_at.map_or(earliest, |f| f.min(earliest)));
1177    }
1178
1179    fn schedule_refresh(&mut self, _now_us: u64) {
1180        let floor = self.dash.min_mpd_refresh_us;
1181        let mup = self
1182            .mpd
1183            .as_ref()
1184            .and_then(|m| m.minimum_update_period)
1185            .map(|d| u64::try_from(duration_us(d)).unwrap_or(u64::MAX));
1186        let Some(live) = self.live.as_mut() else {
1187            return;
1188        };
1189        self.mpd_failures = 0;
1190        live.refresh.awaiting_retry = false;
1191        let ex = live.mpd_exchange;
1192        let mid = ex
1193            .sent_us
1194            .saturating_add(ex.received_us.saturating_sub(ex.sent_us) / 2);
1195        live.refresh.next_at = mup.map(|m| mid.saturating_add(m.max(floor)));
1196    }
1197
1198    pub(super) fn refresh_mpd(
1199        &mut self,
1200        now_us: u64,
1201        new: Mpd,
1202        bytes: &[u8],
1203        meta: &ResponseMeta<'_>,
1204    ) {
1205        if self.live.is_none() {
1206            return;
1207        }
1208        self.note_exchange(now_us, meta);
1209        let (stale, same) = {
1210            let live = self.live.as_ref().expect("live present");
1211            let stale = matches!(
1212                (new.publish_time_us, live.refresh.publish_time_us),
1213                (Some(n), Some(o)) if n < o
1214            );
1215            (stale, live.refresh.body == bytes)
1216        };
1217        if stale {
1218            self.schedule_refresh(now_us);
1219            let floor = self.dash.min_mpd_refresh_us;
1220            if let Some(l) = self.live.as_mut() {
1221                l.refresh.next_at = Some(now_us.saturating_add(floor));
1222            }
1223            return;
1224        }
1225        self.schedule_refresh(now_us);
1226        if same {
1227            return;
1228        }
1229        if let Some(l) = self.live.as_mut() {
1230            l.refresh.body = bytes.to_vec();
1231            l.refresh.publish_time_us = new.publish_time_us.or(l.refresh.publish_time_us);
1232        }
1233        self.apply_mpd(now_us, new);
1234        self.refresh_schedule_after_apply(now_us);
1235    }
1236
1237    fn refresh_schedule_after_apply(&mut self, now_us: u64) {
1238        self.schedule_refresh(now_us);
1239    }
1240
1241    fn apply_mpd(&mut self, now_us: u64, new: Mpd) {
1242        if new.periods.is_empty() {
1243            return;
1244        }
1245        let dynamic = new.kind == PresentationType::Dynamic;
1246        if dynamic && new.availability_start_time_us.is_none() {
1247            return;
1248        }
1249        let features = new.features;
1250        let reset = {
1251            let old = self.mpd.as_ref().expect("mpd present");
1252            let ids_differ = matches!((&old.id, &new.id), (Some(a), Some(b)) if a != b);
1253            let has_reset = new
1254                .essential_properties
1255                .iter()
1256                .chain(new.supplemental_properties.iter())
1257                .any(|d| d.scheme_id_uri == SCHEME_RESET);
1258            let old_dynamic = old.kind == PresentationType::Dynamic;
1259            (old_dynamic
1260                && dynamic
1261                && old.availability_start_time_us != new.availability_start_time_us)
1262                || ids_differ
1263                || has_reset
1264        };
1265        self.mpd = Some(new);
1266        if features.patch_location {
1267            self.notice_once(Notice::PatchIgnored);
1268        }
1269        if features.event_stream || features.inband_event_stream {
1270            self.notice_once(Notice::EventStreamIgnored);
1271        }
1272        if reset || !self.remap_periods() {
1273            self.reset_presentation(now_us);
1274            return;
1275        }
1276        self.replan_all();
1277        self.drain();
1278    }
1279
1280    fn remap_periods(&mut self) -> bool {
1281        let Some(mpd) = self.mpd.as_ref() else {
1282            return false;
1283        };
1284        let timings = period_timings(mpd);
1285        let mut moves: Vec<(usize, usize)> = Vec::new();
1286        for (pi, run) in self.periods.iter().enumerate() {
1287            if run.finished {
1288                continue;
1289            }
1290            let found = mpd
1291                .periods
1292                .iter()
1293                .enumerate()
1294                .position(|(i, p)| match (&run.id, &p.id) {
1295                    (Some(a), Some(b)) => a == b,
1296                    (None, None) => timings.get(i).and_then(|t| t.start_us) == Some(run.start_us),
1297                    _ => false,
1298                });
1299            match found {
1300                Some(ni) => moves.push((pi, ni)),
1301                None => return false,
1302            }
1303        }
1304        let mut vanished: Vec<(usize, usize)> = Vec::new();
1305        for (pi, ni) in moves {
1306            let policy = self.cfg.policy.clone();
1307            let mpd = self.mpd.as_ref().expect("mpd present");
1308            let selection = select(mpd, ni, &policy);
1309            let period = &mpd.periods[ni];
1310            let run = &mut self.periods[pi];
1311            run.index = ni;
1312            let mut kept_audio: Option<Option<RepRef>> = None;
1313            for (li, lane) in run.lanes.iter_mut().enumerate() {
1314                let audio_lane = lane.kind == SetClass::Audio;
1315                let Some(ri) = lane.reps.len().checked_sub(1) else {
1316                    continue;
1317                };
1318                let rep = &mut lane.reps[ri];
1319                let new_set = period
1320                    .adaptation_sets
1321                    .iter()
1322                    .position(|s| match rep.set_id {
1323                        Some(id) => s.id == Some(id),
1324                        None => false,
1325                    });
1326                let new_set = new_set.or_else(|| {
1327                    (rep.set_id.is_none() && rep.set_pos < period.adaptation_sets.len())
1328                        .then_some(rep.set_pos)
1329                });
1330                let found = new_set.and_then(|si| {
1331                    period.adaptation_sets[si]
1332                        .representations
1333                        .iter()
1334                        .position(|r| r.id == rep.rep_key)
1335                        .map(|rj| (si, rj))
1336                });
1337                match found {
1338                    Some((si, rj)) => {
1339                        rep.at = RepRef::at(ni, si, rj);
1340                        rep.set_pos = si;
1341                        if audio_lane {
1342                            kept_audio = Some((!rep.dead).then_some(rep.at));
1343                        }
1344                        if let Some((p, s, r)) = rep.at.resolve(mpd) {
1345                            rep.candidates = super::filter_candidates(
1346                                representation_candidates(mpd, p, s, r),
1347                                self.doc_url.as_ref(),
1348                                self.dash.cross_origin_urls,
1349                            );
1350                        }
1351                    }
1352                    None => vanished.push((pi, li)),
1353                }
1354            }
1355            let chosen = run
1356                .lanes
1357                .iter()
1358                .filter_map(|l| l.current())
1359                .map(|r| r.at)
1360                .collect::<Vec<_>>();
1361            run.selection = selection;
1362            if let Some(audio) = kept_audio {
1363                run.selection.chosen_audio = audio;
1364            }
1365            for at in chosen {
1366                if let Some(pos) = run.selection.variants.iter().position(|v| *v == at) {
1367                    run.selection.chosen_variant = Some(pos);
1368                }
1369            }
1370        }
1371        for (pi, li) in vanished {
1372            self.replace_vanished(pi, li);
1373        }
1374        true
1375    }
1376
1377    fn replace_vanished(&mut self, pi: usize, li: usize) {
1378        let (kind, index) = {
1379            let lane = &self.periods[pi].lanes[li];
1380            (lane.kind, self.periods[pi].index)
1381        };
1382        let replacement = {
1383            let sel = &self.periods[pi].selection;
1384            match kind {
1385                SetClass::Video => sel
1386                    .chosen_variant
1387                    .and_then(|i| sel.variants.get(i).copied()),
1388                _ => sel.chosen_audio,
1389            }
1390        };
1391        let switch_us = {
1392            let lane = &self.periods[pi].lanes[li];
1393            lane.current()
1394                .and_then(|c| c.plan.as_ref().and_then(|p| p.get(c.next_issue)))
1395                .map(|s| s.start_us)
1396        };
1397        let Some(at) = replacement.filter(|a| a.period == index) else {
1398            if let Some(rep) = self.periods[pi].lanes[li].reps.last_mut() {
1399                rep.dead = true;
1400            }
1401            return;
1402        };
1403        match self.build_rep(at) {
1404            Ok(mut rep) => {
1405                if let (Some(plan), Some(us)) = (rep.plan.as_ref(), switch_us) {
1406                    rep.next_issue = plan.index_at_or_before(us).unwrap_or(0);
1407                    rep.joined = true;
1408                    rep.last_time = rep
1409                        .next_issue
1410                        .checked_sub(1)
1411                        .and_then(|i| plan.get(i))
1412                        .map(|s| s.time_ticks);
1413                }
1414                let ri = self.periods[pi].lanes[li].reps.len();
1415                push_marker(&mut self.periods[pi].lanes[li]);
1416                self.periods[pi].lanes[li].reps.push(rep);
1417                self.periods[pi].issue_done = false;
1418                self.register_rep(pi, li, ri);
1419            }
1420            Err(cause) => {
1421                self.outputs.push_back(Output::TrackDropped {
1422                    period: index,
1423                    set: at.set,
1424                    rep: Some(at.rep),
1425                    cause,
1426                });
1427                if let Some(rep) = self.periods[pi].lanes[li].reps.last_mut() {
1428                    rep.dead = true;
1429                }
1430            }
1431        }
1432    }
1433
1434    pub(super) fn replan_all(&mut self) {
1435        let mut targets: Vec<(usize, usize, usize)> = Vec::new();
1436        for (pi, run) in self.periods.iter().enumerate() {
1437            if run.finished {
1438                continue;
1439            }
1440            for (li, lane) in run.lanes.iter().enumerate() {
1441                if let Some(ri) = lane.reps.len().checked_sub(1)
1442                    && !lane.reps[ri].dead
1443                {
1444                    targets.push((pi, li, ri));
1445                }
1446            }
1447        }
1448        for (pi, li, ri) in targets {
1449            self.replan_rep(pi, li, ri);
1450        }
1451    }
1452
1453    fn replan_rep(&mut self, pi: usize, li: usize, ri: usize) {
1454        let (at, has_index) = {
1455            let rep = &self.periods[pi].lanes[li].reps[ri];
1456            (rep.at, rep.index.is_some())
1457        };
1458        if has_index {
1459            return;
1460        }
1461        let planned = {
1462            let mpd = self.mpd.as_ref().expect("mpd present");
1463            plan_representation(mpd, at, &self.plan_options())
1464        };
1465        if let Ok(p) = planned {
1466            let marker = {
1467                let rep = &mut self.periods[pi].lanes[li].reps[ri];
1468                if rep.joined {
1469                    let bound = p.plan.len().unwrap_or(u64::MAX / 2);
1470                    rep.next_issue = match rep.last_time {
1471                        Some(t) => p.plan.leading_true(bound, |s| s.time_ticks <= t),
1472                        None => 0,
1473                    };
1474                }
1475                rep.plan = Some(p.plan);
1476                rep.joined && rep.next_issue == 0 && rep.last_time.is_some()
1477            };
1478            if marker {
1479                push_marker(&mut self.periods[pi].lanes[li]);
1480            }
1481        }
1482    }
1483
1484    fn reset_presentation(&mut self, now_us: u64) {
1485        self.periods.clear();
1486        self.issue = 0;
1487        self.deliver = 0;
1488        self.queue.clear();
1489        self.reqs.clear();
1490        self.inflight.clear();
1491        self.parked.clear();
1492        self.failures.clear();
1493        self.done.clear();
1494        self.remembered_done.clear();
1495        self.outstanding = 0;
1496        self.rep_loc.clear();
1497        self.prev = None;
1498        self.inits.clear();
1499        if let Some(live) = self.live.as_mut() {
1500            live.early.clear();
1501            live.missing_seen.clear();
1502        }
1503        self.on_clock_ready(now_us);
1504    }
1505}
1506
1507fn push_marker(lane: &mut Lane) {
1508    if !lane
1509        .queue
1510        .back()
1511        .is_some_and(|s| matches!(s.state, SlotState::Marker))
1512    {
1513        lane.queue.push_back(Slot {
1514            id: None,
1515            rep: 0,
1516            plan_index: 0,
1517            state: SlotState::Marker,
1518        });
1519    }
1520}