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}