Skip to main content

dash_runtime/timing/
utc_timing.rs

1//! `UTCTiming` schemes and the client-measured clock sample.
2//!
3//! ISO/IEC 23009-1 5.8.5.7 Table 35 names the schemes; the sampling rule
4//! (midpoint of the request/response exchange, uncertainty = half the round
5//! trip plus the time source's own resolution) is
6//! `docs/dash-runtime/DESIGN.md` 3.3. The caller measures `sent_us` and
7//! `received_us` on its own monotonic clock; nothing here reads a clock.
8
9use broadcast_common::impl_spec_display;
10
11use super::TimingError;
12use super::datetime::{parse_http_date, parse_ntp_timestamp, parse_xs_datetime};
13
14/// 5.8.5.7: `urn:mpeg:dash:utc:ntp:2014`.
15pub const SCHEME_NTP: &str = "urn:mpeg:dash:utc:ntp:2014";
16/// 5.8.5.7: `urn:mpeg:dash:utc:sntp:2014`.
17pub const SCHEME_SNTP: &str = "urn:mpeg:dash:utc:sntp:2014";
18/// 5.8.5.7: `urn:mpeg:dash:utc:http-head:2014`.
19pub const SCHEME_HTTP_HEAD: &str = "urn:mpeg:dash:utc:http-head:2014";
20/// 5.8.5.7: `urn:mpeg:dash:utc:http-xsdate:2014`.
21pub const SCHEME_HTTP_XSDATE: &str = "urn:mpeg:dash:utc:http-xsdate:2014";
22/// 5.8.5.7: `urn:mpeg:dash:utc:http-iso:2014`.
23pub const SCHEME_HTTP_ISO: &str = "urn:mpeg:dash:utc:http-iso:2014";
24/// 5.8.5.7: `urn:mpeg:dash:utc:http-ntp:2014`.
25pub const SCHEME_HTTP_NTP: &str = "urn:mpeg:dash:utc:http-ntp:2014";
26/// 5.8.5.7: `urn:mpeg:dash:utc:direct:2014`.
27pub const SCHEME_DIRECT: &str = "urn:mpeg:dash:utc:direct:2014";
28
29/// Largest UTC response body read (DESIGN risk R3).
30pub const UTC_BODY_MAX_BYTES: usize = 4 * 1024;
31/// `Date` carries whole seconds (RFC 9110 5.6.7): the sample is taken at the
32/// middle of the second and the half second is added to the uncertainty.
33pub const DATE_HEADER_HALF_SECOND_US: u64 = 500_000;
34/// Resolution of an NTP timestamp read over HTTP, in microseconds.
35pub const HTTP_NTP_RESOLUTION_US: u64 = 1_000;
36const MICROS_PER_SECOND: i64 = 1_000_000;
37
38/// A `UTCTiming` scheme (Table 35).
39#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
40#[non_exhaustive]
41pub enum UtcScheme {
42    /// NTP servers; not acted on (no UDP in a sans-IO core).
43    Ntp,
44    /// SNTP servers; not acted on.
45    Sntp,
46    /// `HEAD`, read the `Date` header.
47    HttpHead,
48    /// `GET`, body is an `xs:dateTime`.
49    HttpXsdate,
50    /// `GET`, body is an ISO 8601 time.
51    HttpIso,
52    /// `GET`, body is an NTP timestamp.
53    HttpNtp,
54    /// `@value` is the time.
55    Direct,
56    /// A scheme this core does not know.
57    Unknown,
58}
59
60impl UtcScheme {
61    /// Classify a `schemeIdUri`.
62    #[must_use]
63    pub fn from_scheme_id_uri(uri: &str) -> Self {
64        match uri {
65            SCHEME_NTP => Self::Ntp,
66            SCHEME_SNTP => Self::Sntp,
67            SCHEME_HTTP_HEAD => Self::HttpHead,
68            SCHEME_HTTP_XSDATE => Self::HttpXsdate,
69            SCHEME_HTTP_ISO => Self::HttpIso,
70            SCHEME_HTTP_NTP => Self::HttpNtp,
71            SCHEME_DIRECT => Self::Direct,
72            _ => Self::Unknown,
73        }
74    }
75
76    /// Stable snake_case label.
77    #[must_use]
78    pub fn name(&self) -> &'static str {
79        match self {
80            Self::Ntp => "ntp",
81            Self::Sntp => "sntp",
82            Self::HttpHead => "http_head",
83            Self::HttpXsdate => "http_xsdate",
84            Self::HttpIso => "http_iso",
85            Self::HttpNtp => "http_ntp",
86            Self::Direct => "direct",
87            Self::Unknown => "unknown",
88        }
89    }
90
91    /// Whether the client can act on the scheme without UDP.
92    #[must_use]
93    pub fn is_actionable(&self) -> bool {
94        !matches!(self, Self::Ntp | Self::Sntp | Self::Unknown)
95    }
96
97    /// Whether the scheme needs an HTTP request (`@value` lists URLs).
98    #[must_use]
99    pub fn needs_request(&self) -> bool {
100        matches!(
101            self,
102            Self::HttpHead | Self::HttpXsdate | Self::HttpIso | Self::HttpNtp
103        )
104    }
105}
106impl_spec_display!(UtcScheme);
107
108/// The request/response pair the caller timed on its monotonic clock.
109#[derive(Debug, Clone, Copy, PartialEq, Eq)]
110#[non_exhaustive]
111pub struct Exchange {
112    /// Monotonic microseconds when the request was emitted.
113    pub sent_us: u64,
114    /// Monotonic microseconds when the first response byte arrived.
115    pub received_us: u64,
116}
117
118impl Exchange {
119    /// A new exchange.
120    #[must_use]
121    pub fn new(sent_us: u64, received_us: u64) -> Self {
122        Self {
123            sent_us,
124            received_us,
125        }
126    }
127}
128
129/// One clock measurement: at monotonic `mono_us` the UTC time was `utc_us`
130/// (Unix microseconds), give or take `uncertainty_us`.
131#[derive(Debug, Clone, Copy, PartialEq, Eq)]
132#[non_exhaustive]
133pub struct UtcSample {
134    /// Monotonic microseconds the sample refers to.
135    pub mono_us: u64,
136    /// Unix microseconds at that instant.
137    pub utc_us: i64,
138    /// Half-width of the error bar.
139    pub uncertainty_us: u64,
140}
141
142/// Build a sample from a timed exchange and the server time it returned.
143///
144/// The server stamps the time near response generation, so the sample sits at
145/// the exchange midpoint; the error bar is half the round trip plus the
146/// source's own `resolution_us`.
147///
148/// # Errors
149/// [`TimingError::ExchangeReversed`] when `received_us < sent_us`;
150/// [`TimingError::RttTooLarge`] when the round trip exceeds `max_rtt_us`.
151pub fn sample_from_exchange(
152    exchange: Exchange,
153    server_utc_us: i64,
154    resolution_us: u64,
155    max_rtt_us: u64,
156) -> Result<UtcSample, TimingError> {
157    let rtt = exchange
158        .received_us
159        .checked_sub(exchange.sent_us)
160        .ok_or(TimingError::ExchangeReversed)?;
161    if rtt > max_rtt_us {
162        return Err(TimingError::RttTooLarge {
163            rtt_us: rtt,
164            max_us: max_rtt_us,
165        });
166    }
167    let half = rtt / 2;
168    Ok(UtcSample {
169        mono_us: exchange.sent_us + half,
170        utc_us: server_utc_us,
171        uncertainty_us: half.saturating_add(resolution_us),
172    })
173}
174
175/// Sample from the body of an `http-xsdate`, `http-iso` or `http-ntp`
176/// response.
177///
178/// # Errors
179/// [`TimingError`] for an oversized or unreadable body, a scheme that has no
180/// body time, or a bad exchange.
181pub fn sample_from_body(
182    scheme: UtcScheme,
183    body: &[u8],
184    exchange: Exchange,
185    max_rtt_us: u64,
186) -> Result<UtcSample, TimingError> {
187    if body.len() > UTC_BODY_MAX_BYTES {
188        return Err(TimingError::BodyTooLarge {
189            max: UTC_BODY_MAX_BYTES,
190        });
191    }
192    match scheme {
193        UtcScheme::HttpXsdate | UtcScheme::HttpIso => {
194            let text = core::str::from_utf8(body).map_err(|_| TimingError::NotUtf8)?;
195            let dt = parse_xs_datetime(text)?;
196            sample_from_exchange(exchange, dt.unix_us, dt.resolution_us(), max_rtt_us)
197        }
198        UtcScheme::HttpNtp => {
199            let utc = parse_ntp_timestamp(body)?;
200            sample_from_exchange(exchange, utc, HTTP_NTP_RESOLUTION_US, max_rtt_us)
201        }
202        other => Err(TimingError::SchemeNotSampled(other)),
203    }
204}
205
206/// Sample from the `Date` header of an `http-head` (or any) response.
207///
208/// # Errors
209/// [`TimingError`] when the header is not a date or the exchange is bad.
210pub fn sample_from_date_header(
211    date: &str,
212    exchange: Exchange,
213    max_rtt_us: u64,
214) -> Result<UtcSample, TimingError> {
215    let seconds_us = parse_http_date(date)?;
216    let half_second = i64::try_from(DATE_HEADER_HALF_SECOND_US).unwrap_or(MICROS_PER_SECOND / 2);
217    sample_from_exchange(
218        exchange,
219        seconds_us.saturating_add(half_second),
220        DATE_HEADER_HALF_SECOND_US,
221        max_rtt_us,
222    )
223}
224
225/// Sample from a `direct` scheme `@value`, taken at the midpoint of the MPD
226/// response exchange.
227///
228/// # Errors
229/// [`TimingError`] when the value is not an accepted `xs:dateTime` or the
230/// exchange is bad.
231pub fn sample_from_direct(
232    value: &str,
233    mpd_exchange: Exchange,
234    max_rtt_us: u64,
235) -> Result<UtcSample, TimingError> {
236    let dt = parse_xs_datetime(value)?;
237    sample_from_exchange(mpd_exchange, dt.unix_us, dt.resolution_us(), max_rtt_us)
238}