Skip to main content

dash_runtime/client/
config.rs

1//! `DashConfig`: the DASH-specific knobs of a client session.
2//!
3//! `docs/dash-runtime/DESIGN.md` 6.4 (fields and defaults), 3.2-3.4 (clock
4//! ladder), 5.2 (refresh floor), 5.4 (early-request cadence), D9 (LL-DASH) and
5//! 10.2 R3 (cross-origin restriction). Pure data: nothing here reads a clock.
6//! Every duration is in microseconds. A value of `u64::MAX` is accepted
7//! wherever zero is not: consumers combine these with saturating arithmetic,
8//! so `MAX` reads as "never" or "unbounded" rather than overflowing.
9
10use broadcast_common::impl_spec_display;
11use broadcast_common::units::{MICROS_PER_MILLI, MICROS_PER_SEC};
12use thiserror::Error;
13
14/// Default time one `UTCTiming` request may take.
15pub const DEFAULT_UTC_TIMEOUT_US: u64 = 2 * MICROS_PER_SEC;
16/// Default total wait for a `UTCTiming` sample before the clock ladder falls
17/// through to the next source.
18pub const DEFAULT_UTC_WAIT_US: u64 = 5 * MICROS_PER_SEC;
19/// Default longest round trip a `UTCTiming` sample may have and still be used.
20pub const DEFAULT_UTC_MAX_RTT_US: u64 = 2 * MICROS_PER_SEC;
21/// Default age after which a same-source clock anchor is replaced.
22pub const DEFAULT_UTC_RESYNC_US: u64 = 600 * MICROS_PER_SEC;
23/// Default error bar reported for a host-supplied clock.
24pub const DEFAULT_HOST_CLOCK_UNCERTAINTY_US: u64 = 500 * MICROS_PER_MILLI;
25/// Default margin added to a segment's availability start before requesting it.
26pub const DEFAULT_AVAILABILITY_MARGIN_US: u64 = 0;
27/// Default first retry delay for a segment requested before it exists.
28pub const DEFAULT_EARLY_RETRY_US: u64 = 250 * MICROS_PER_MILLI;
29/// Default window after a segment's availability start in which a 404, 410 or
30/// 425 still means "too early" rather than "missing".
31pub const DEFAULT_EARLY_GRACE_US: u64 = MICROS_PER_SEC;
32/// Default floor under the MPD refresh interval.
33pub const DEFAULT_MIN_MPD_REFRESH_US: u64 = MICROS_PER_SEC;
34
35/// Which clock source the ladder tries first for a dynamic MPD.
36#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
37#[non_exhaustive]
38pub enum DashClockPolicy {
39    /// `UTCTiming`, then the host clock, then the MPD response `Date`.
40    #[default]
41    Auto,
42    /// The host clock first (a host with a trusted NTP clock saves the round
43    /// trip), then `UTCTiming`, then the MPD response `Date`.
44    PreferHost,
45}
46
47impl DashClockPolicy {
48    /// Stable snake_case label.
49    #[must_use]
50    pub fn name(&self) -> &'static str {
51        match self {
52            Self::Auto => "auto",
53            Self::PreferHost => "prefer_host",
54        }
55    }
56}
57impl_spec_display!(DashClockPolicy);
58
59/// A [`DashConfig`] field no client can run with.
60#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
61#[error("invalid dash config: {field}")]
62pub struct DashConfigError {
63    /// The offending field name.
64    pub field: &'static str,
65}
66
67/// DASH-specific configuration. The knobs shared with every segment-pull
68/// source (redirects, headers, cookies, backoff, in-flight window, body caps,
69/// playout, timeline-jump detection) stay on the session's own config.
70#[derive(Debug, Clone, Copy, PartialEq, Eq)]
71#[non_exhaustive]
72pub struct DashConfig {
73    /// Clock ladder order, see [`DashClockPolicy`].
74    pub clock_policy: DashClockPolicy,
75    /// Time one `UTCTiming` request may take, microseconds; must be non-zero.
76    pub utc_timeout_us: u64,
77    /// Total wait for a `UTCTiming` sample before the ladder falls through,
78    /// microseconds; must be non-zero and not shorter than
79    /// [`DashConfig::utc_timeout_us`] (a wait that ends before one request can
80    /// time out would never let the first request finish).
81    pub utc_wait_us: u64,
82    /// Longest round trip a `UTCTiming` sample may have, microseconds; a
83    /// slower exchange is discarded. Must be non-zero.
84    pub utc_max_rtt_us: u64,
85    /// Age after which a same-source clock anchor is replaced by any new
86    /// sample of that source, microseconds; must be non-zero. `u64::MAX`
87    /// never re-syncs on age.
88    pub utc_resync_us: u64,
89    /// Error bar reported with a clock taken from the host, microseconds.
90    /// `0` claims an exact host clock.
91    pub host_clock_uncertainty_us: u64,
92    /// Override of the presentation delay a live join keeps behind the live
93    /// edge, microseconds. `None` derives it from the MPD. `Some(0)` joins at
94    /// the edge.
95    pub live_delay_us: Option<u64>,
96    /// Added to a segment's availability start before it is requested,
97    /// microseconds. `0` requests at the availability start.
98    pub availability_margin_us: u64,
99    /// First retry delay for a segment requested before it exists,
100    /// microseconds; must be non-zero. The delay doubles up to one second.
101    pub early_retry_us: u64,
102    /// Window after a segment's availability start in which a 404, 410 or
103    /// 425 still means "too early", microseconds. `0` treats every such
104    /// response after the start as a missing segment.
105    pub early_grace_us: u64,
106    /// Floor under the MPD refresh interval, microseconds; must be non-zero
107    /// so `minimumUpdatePeriod="PT0S"` cannot become a request storm.
108    pub min_mpd_refresh_us: u64,
109    /// Request low-latency segments at `availabilityTimeOffset` earlier than
110    /// their complete-segment time. Delivery stays whole-segment.
111    pub use_availability_time_offset: bool,
112    /// `false` fails any MPD-derived URL (BaseURL, segment, `UTCTiming`)
113    /// whose origin differs from the MPD's, for server-side pulls.
114    pub cross_origin_urls: bool,
115}
116
117impl Default for DashConfig {
118    fn default() -> Self {
119        Self {
120            clock_policy: DashClockPolicy::Auto,
121            utc_timeout_us: DEFAULT_UTC_TIMEOUT_US,
122            utc_wait_us: DEFAULT_UTC_WAIT_US,
123            utc_max_rtt_us: DEFAULT_UTC_MAX_RTT_US,
124            utc_resync_us: DEFAULT_UTC_RESYNC_US,
125            host_clock_uncertainty_us: DEFAULT_HOST_CLOCK_UNCERTAINTY_US,
126            live_delay_us: None,
127            availability_margin_us: DEFAULT_AVAILABILITY_MARGIN_US,
128            early_retry_us: DEFAULT_EARLY_RETRY_US,
129            early_grace_us: DEFAULT_EARLY_GRACE_US,
130            min_mpd_refresh_us: DEFAULT_MIN_MPD_REFRESH_US,
131            use_availability_time_offset: false,
132            cross_origin_urls: true,
133        }
134    }
135}
136
137impl DashConfig {
138    /// Check the configuration.
139    ///
140    /// # Errors
141    /// [`DashConfigError`] naming the first field a client cannot run with.
142    pub fn validate(&self) -> Result<(), DashConfigError> {
143        let invalid = |field| Err(DashConfigError { field });
144        if self.utc_timeout_us == 0 {
145            return invalid("utc_timeout_us");
146        }
147        if self.utc_wait_us < self.utc_timeout_us {
148            return invalid("utc_wait_us");
149        }
150        if self.utc_max_rtt_us == 0 {
151            return invalid("utc_max_rtt_us");
152        }
153        if self.utc_resync_us == 0 {
154            return invalid("utc_resync_us");
155        }
156        if self.early_retry_us == 0 {
157            return invalid("early_retry_us");
158        }
159        if self.min_mpd_refresh_us == 0 {
160            return invalid("min_mpd_refresh_us");
161        }
162        Ok(())
163    }
164}
165
166/// Default bound on one fetched resource body (init, index or segment).
167pub const DEFAULT_MAX_SEGMENT_BYTES: usize = 64 * 1024 * 1024;
168/// Default number of segment requests kept outstanding at once.
169pub const DEFAULT_MAX_INFLIGHT_SEGMENTS: usize = 4;
170/// Default consecutive failures of one segment before it is given up on.
171pub const DEFAULT_MAX_SEGMENT_FAILURES: u32 = 8;
172/// Default consecutive failures of one init or index resource before the
173/// presentation is given up on.
174pub const DEFAULT_MAX_INIT_FAILURES: u32 = 8;
175
176/// A rejected [`PullLimits`].
177#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
178#[error("invalid PullLimits: {field} must be non-zero")]
179pub struct PullLimitsError {
180    /// The offending field.
181    pub field: &'static str,
182}
183
184/// The knobs every segment-pull source shares (DESIGN 6.4, "shared with
185/// HLS"): the session owns them in its own config and passes them in; they
186/// are deliberately not part of [`DashConfig`].
187#[derive(Debug, Clone, PartialEq)]
188#[non_exhaustive]
189pub struct PullLimits {
190    /// Representation and AdaptationSet selection policy.
191    pub policy: crate::selection::Policy,
192    /// MPD parse bounds; `limits.max_bytes` is the MPD body cap.
193    pub limits: broadcast_dash::limits::Limits,
194    /// Largest accepted init, index or segment body, bytes.
195    pub max_segment_bytes: usize,
196    /// Segment requests kept outstanding at once, counting bodies that have
197    /// arrived but are not yet delivered; `0` behaves as `1`.
198    pub max_inflight_segments: usize,
199    /// Consecutive failures of one segment after which a static presentation
200    /// ends with a fatal error; `0` makes the first failure final and
201    /// `u32::MAX` retries forever (the `broadcast-client` rule).
202    pub max_segment_failures: u32,
203    /// The same for the init and index resources.
204    pub max_init_failures: u32,
205}
206
207impl Default for PullLimits {
208    fn default() -> Self {
209        Self {
210            policy: crate::selection::Policy::default(),
211            limits: broadcast_dash::limits::Limits::default(),
212            max_segment_bytes: DEFAULT_MAX_SEGMENT_BYTES,
213            max_inflight_segments: DEFAULT_MAX_INFLIGHT_SEGMENTS,
214            max_segment_failures: DEFAULT_MAX_SEGMENT_FAILURES,
215            max_init_failures: DEFAULT_MAX_INIT_FAILURES,
216        }
217    }
218}
219
220impl PullLimits {
221    /// Reject limits that cannot work.
222    ///
223    /// # Errors
224    /// [`PullLimitsError`] naming the first zero size bound.
225    pub fn validate(&self) -> Result<(), PullLimitsError> {
226        if self.limits.max_bytes == 0 {
227            return Err(PullLimitsError {
228                field: "limits.max_bytes",
229            });
230        }
231        if self.max_segment_bytes == 0 {
232            return Err(PullLimitsError {
233                field: "max_segment_bytes",
234            });
235        }
236        Ok(())
237    }
238
239    pub(crate) fn window(&self) -> usize {
240        self.max_inflight_segments.max(1)
241    }
242}