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