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}