1use broadcast_common::impl_spec_display;
10
11use super::TimingError;
12use super::datetime::{parse_http_date, parse_ntp_timestamp, parse_xs_datetime};
13
14pub const SCHEME_NTP: &str = "urn:mpeg:dash:utc:ntp:2014";
16pub const SCHEME_SNTP: &str = "urn:mpeg:dash:utc:sntp:2014";
18pub const SCHEME_HTTP_HEAD: &str = "urn:mpeg:dash:utc:http-head:2014";
20pub const SCHEME_HTTP_XSDATE: &str = "urn:mpeg:dash:utc:http-xsdate:2014";
22pub const SCHEME_HTTP_ISO: &str = "urn:mpeg:dash:utc:http-iso:2014";
24pub const SCHEME_HTTP_NTP: &str = "urn:mpeg:dash:utc:http-ntp:2014";
26pub const SCHEME_DIRECT: &str = "urn:mpeg:dash:utc:direct:2014";
28
29pub const UTC_BODY_MAX_BYTES: usize = 4 * 1024;
31pub const DATE_HEADER_HALF_SECOND_US: u64 = 500_000;
34pub const HTTP_NTP_RESOLUTION_US: u64 = 1_000;
36const MICROS_PER_SECOND: i64 = 1_000_000;
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
40#[non_exhaustive]
41pub enum UtcScheme {
42 Ntp,
44 Sntp,
46 HttpHead,
48 HttpXsdate,
50 HttpIso,
52 HttpNtp,
54 Direct,
56 Unknown,
58}
59
60impl UtcScheme {
61 #[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 #[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 #[must_use]
93 pub fn is_actionable(&self) -> bool {
94 !matches!(self, Self::Ntp | Self::Sntp | Self::Unknown)
95 }
96
97 #[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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
110#[non_exhaustive]
111pub struct Exchange {
112 pub sent_us: u64,
114 pub received_us: u64,
116}
117
118impl Exchange {
119 #[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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
132#[non_exhaustive]
133pub struct UtcSample {
134 pub mono_us: u64,
136 pub utc_us: i64,
138 pub uncertainty_us: u64,
140}
141
142pub 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
175pub 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
206pub 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
225pub 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}