Skip to main content

dash_runtime/timing/
utc_timing.rs

1//! The client-measured clock sample and the `UTCTiming` readers over it.
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
9pub use broadcast_dash::utc_timing::UtcScheme;
10use broadcast_dash::xs_datetime::{XsDateTime, parse_ntp_timestamp, parse_xs_datetime};
11
12use super::TimingError;
13use super::http_date::parse_http_date;
14use broadcast_common::units::MICROS_PER_SEC;
15
16/// 5.8.5.7: `urn:mpeg:dash:utc:ntp:2014`.
17pub const SCHEME_NTP: &str = "urn:mpeg:dash:utc:ntp:2014";
18/// 5.8.5.7: `urn:mpeg:dash:utc:sntp:2014`.
19pub const SCHEME_SNTP: &str = "urn:mpeg:dash:utc:sntp:2014";
20/// 5.8.5.7: `urn:mpeg:dash:utc:http-head:2014`.
21pub const SCHEME_HTTP_HEAD: &str = "urn:mpeg:dash:utc:http-head:2014";
22/// 5.8.5.7: `urn:mpeg:dash:utc:http-xsdate:2014`.
23pub const SCHEME_HTTP_XSDATE: &str = "urn:mpeg:dash:utc:http-xsdate:2014";
24/// 5.8.5.7: `urn:mpeg:dash:utc:http-iso:2014`.
25pub const SCHEME_HTTP_ISO: &str = "urn:mpeg:dash:utc:http-iso:2014";
26/// 5.8.5.7: `urn:mpeg:dash:utc:http-ntp:2014`.
27pub const SCHEME_HTTP_NTP: &str = "urn:mpeg:dash:utc:http-ntp:2014";
28/// 5.8.5.7: `urn:mpeg:dash:utc:direct:2014`.
29pub const SCHEME_DIRECT: &str = "urn:mpeg:dash:utc:direct:2014";
30
31/// Largest UTC response body read (DESIGN risk R3).
32pub const UTC_BODY_MAX_BYTES: usize = 4 * 1024;
33/// `Date` carries whole seconds (RFC 9110 5.6.7): the sample is taken at the
34/// middle of the second and the half second is added to the uncertainty.
35pub const DATE_HEADER_HALF_SECOND_US: u64 = 500_000;
36/// Resolution of an NTP timestamp read over HTTP, in microseconds.
37pub const HTTP_NTP_RESOLUTION_US: u64 = 1_000;
38
39/// The request/response pair the caller timed on its monotonic clock.
40#[derive(Debug, Clone, Copy, PartialEq, Eq)]
41#[non_exhaustive]
42pub struct Exchange {
43    /// Monotonic microseconds when the request was emitted.
44    pub sent_us: u64,
45    /// Monotonic microseconds when the first response byte arrived.
46    pub received_us: u64,
47}
48
49impl Exchange {
50    /// A new exchange.
51    #[must_use]
52    pub fn new(sent_us: u64, received_us: u64) -> Self {
53        Self {
54            sent_us,
55            received_us,
56        }
57    }
58}
59
60/// One clock measurement: at monotonic `mono_us` the UTC time was `utc_us`
61/// (Unix microseconds), give or take `uncertainty_us`.
62#[derive(Debug, Clone, Copy, PartialEq, Eq)]
63#[non_exhaustive]
64pub struct UtcSample {
65    /// Monotonic microseconds the sample refers to.
66    pub mono_us: u64,
67    /// Unix microseconds at that instant.
68    pub utc_us: i64,
69    /// Half-width of the error bar.
70    pub uncertainty_us: u64,
71}
72
73/// Build a sample from a timed exchange and the server time it returned.
74///
75/// The server stamps the time near response generation, so the sample sits at
76/// the exchange midpoint; the error bar is half the round trip plus the
77/// source's own `resolution_us`.
78///
79/// # Errors
80/// [`TimingError::ExchangeReversed`] when `received_us < sent_us`;
81/// [`TimingError::RttTooLarge`] when the round trip exceeds `max_rtt_us`.
82pub fn sample_from_exchange(
83    exchange: Exchange,
84    server_utc_us: i64,
85    resolution_us: u64,
86    max_rtt_us: u64,
87) -> Result<UtcSample, TimingError> {
88    let rtt = exchange
89        .received_us
90        .checked_sub(exchange.sent_us)
91        .ok_or(TimingError::ExchangeReversed)?;
92    if rtt > max_rtt_us {
93        return Err(TimingError::RttTooLarge {
94            rtt_us: rtt,
95            max_us: max_rtt_us,
96        });
97    }
98    let half = rtt / 2;
99    Ok(UtcSample {
100        mono_us: exchange.sent_us + half,
101        utc_us: server_utc_us,
102        uncertainty_us: half.saturating_add(resolution_us),
103    })
104}
105
106/// Sample from the body of an `http-xsdate`, `http-iso` or `http-ntp`
107/// response.
108///
109/// # Errors
110/// [`TimingError`] for an oversized or unreadable body, a scheme that has no
111/// body time, or a bad exchange.
112pub fn sample_from_body(
113    scheme: UtcScheme,
114    body: &[u8],
115    exchange: Exchange,
116    max_rtt_us: u64,
117) -> Result<UtcSample, TimingError> {
118    if body.len() > UTC_BODY_MAX_BYTES {
119        return Err(TimingError::BodyTooLarge {
120            max: UTC_BODY_MAX_BYTES,
121        });
122    }
123    match scheme {
124        UtcScheme::HttpXsdate | UtcScheme::HttpIso => {
125            let text = core::str::from_utf8(body).map_err(|_| TimingError::NotUtf8)?;
126            sample_from_datetime(parse_xs_datetime(text)?, exchange, max_rtt_us)
127        }
128        UtcScheme::HttpNtp => {
129            let utc = parse_ntp_timestamp(body)?;
130            sample_from_exchange(exchange, utc, HTTP_NTP_RESOLUTION_US, max_rtt_us)
131        }
132        other => Err(TimingError::SchemeNotSampled(other)),
133    }
134}
135
136/// Sample from the `Date` header of an `http-head` (or any) response.
137///
138/// # Errors
139/// [`TimingError`] when the header is not a date or the exchange is bad.
140pub fn sample_from_date_header(
141    date: &str,
142    exchange: Exchange,
143    max_rtt_us: u64,
144) -> Result<UtcSample, TimingError> {
145    let seconds_us = parse_http_date(date)?;
146    let half_second =
147        i64::try_from(DATE_HEADER_HALF_SECOND_US).unwrap_or((MICROS_PER_SEC as i64) / 2);
148    sample_from_exchange(
149        exchange,
150        seconds_us.saturating_add(half_second),
151        DATE_HEADER_HALF_SECOND_US,
152        max_rtt_us,
153    )
154}
155
156/// Sample from a `direct` scheme `@value`, taken at the midpoint of the MPD
157/// response exchange.
158///
159/// # Errors
160/// [`TimingError`] when the value is not an accepted `xs:dateTime` or the
161/// exchange is bad.
162pub fn sample_from_direct(
163    value: &str,
164    mpd_exchange: Exchange,
165    max_rtt_us: u64,
166) -> Result<UtcSample, TimingError> {
167    sample_from_datetime(parse_xs_datetime(value)?, mpd_exchange, max_rtt_us)
168}
169
170fn sample_from_datetime(
171    dt: XsDateTime,
172    exchange: Exchange,
173    max_rtt_us: u64,
174) -> Result<UtcSample, TimingError> {
175    if dt.fraction_digits > 0 {
176        return sample_from_exchange(exchange, dt.unix_us, dt.resolution_us(), max_rtt_us);
177    }
178    let half_second =
179        i64::try_from(DATE_HEADER_HALF_SECOND_US).unwrap_or((MICROS_PER_SEC as i64) / 2);
180    sample_from_exchange(
181        exchange,
182        dt.unix_us.saturating_add(half_second),
183        DATE_HEADER_HALF_SECOND_US,
184        max_rtt_us,
185    )
186}