Skip to main content

timed_metadata/
anchor.rs

1//! Media-time ↔ wall-clock mapping for conversions that cross into UTC.
2use crate::event::MediaTime;
3use alloc::{format, string::String};
4
5/// Maps a known 90 kHz PTS to the UTC instant it represents (linear at 90 kHz).
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
8pub struct TimeAnchor {
9    /// A reference PTS, in 90 kHz ticks.
10    pub pts_90k: u64,
11    /// The UTC time that `pts_90k` corresponds to, in milliseconds since the Unix epoch.
12    pub utc_epoch_ms: i64,
13}
14
15impl TimeAnchor {
16    /// Map a media instant to milliseconds since the Unix epoch.
17    pub fn media_to_epoch_ms(&self, t: MediaTime) -> i64 {
18        let delta_ticks = t.0 as i64 - self.pts_90k as i64;
19        // ticks / 90_000 * 1000 == ticks / 90 ; do it in i128 to avoid overflow.
20        self.utc_epoch_ms + (delta_ticks as i128 * 1000 / crate::PTS_HZ as i128) as i64
21    }
22
23    /// Map a media instant to an RFC3339 / ISO-8601 UTC string (millisecond precision).
24    pub fn rfc3339(&self, t: MediaTime) -> String {
25        format_rfc3339_ms(self.media_to_epoch_ms(t))
26    }
27}
28
29/// Format milliseconds-since-epoch as `YYYY-MM-DDTHH:MM:SS.sssZ`.
30pub fn format_rfc3339_ms(epoch_ms: i64) -> String {
31    let (secs, ms) = (epoch_ms.div_euclid(1000), epoch_ms.rem_euclid(1000));
32    let days = secs.div_euclid(86_400);
33    let tod = secs.rem_euclid(86_400);
34    let (h, m, s) = (tod / 3600, (tod % 3600) / 60, tod % 60);
35    let (y, mo, d) = civil_from_days(days);
36    format!(
37        "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}.{:03}Z",
38        y, mo, d, h, m, s, ms
39    )
40}
41
42/// Convert days-since-Unix-epoch to (year, month, day). Hinnant's algorithm.
43fn civil_from_days(z: i64) -> (i64, u32, u32) {
44    let z = z + 719_468;
45    let era = if z >= 0 { z } else { z - 146_096 } / 146_097;
46    let doe = z - era * 146_097; // [0, 146096]
47    let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365; // [0, 399]
48    let y = yoe + era * 400;
49    let doy = doe - (365 * yoe + yoe / 4 - yoe / 100); // [0, 365]
50    let mp = (5 * doy + 2) / 153; // [0, 11]
51    let d = (doy - (153 * mp + 2) / 5 + 1) as u32; // [1, 31]
52    let m = if mp < 10 { mp + 3 } else { mp - 9 } as u32; // [1, 12]
53    (if m <= 2 { y + 1 } else { y }, m, d)
54}
55
56// chrono interop helpers can be added behind cfg(feature="chrono") later
57
58#[cfg(test)]
59mod tests {
60    use super::*;
61    use crate::event::MediaTime;
62
63    #[test]
64    fn epoch_zero_formats_unix_epoch() {
65        assert_eq!(format_rfc3339_ms(0), "1970-01-01T00:00:00.000Z");
66        assert_eq!(format_rfc3339_ms(86_400_000), "1970-01-02T00:00:00.000Z");
67        assert_eq!(format_rfc3339_ms(1_000), "1970-01-01T00:00:01.000Z");
68    }
69
70    #[test]
71    fn anchor_maps_media_to_wallclock() {
72        // anchor: pts 0 == epoch 1000ms. +90000 ticks (1s) -> 2000ms.
73        let a = TimeAnchor {
74            pts_90k: 0,
75            utc_epoch_ms: 1_000,
76        };
77        assert_eq!(a.media_to_epoch_ms(MediaTime(90_000)), 2_000);
78        assert_eq!(a.rfc3339(MediaTime(0)), "1970-01-01T00:00:01.000Z");
79    }
80}