Skip to main content

alpha2_time/
media_time.rs

1use core::fmt;
2use serde::{Deserialize, Serialize};
3
4/// Fixed alpha2 timeline ticks per second.
5pub const TICKS_PER_SECOND: i64 = 120_000;
6
7/// Largest magnitude of a serialized time: 2^53 - 1 ticks (about 2,378 years), the largest
8/// integer a JavaScript number holds exactly, so every host reads JSON times the same.
9pub const MAX_SERIALIZED_TICKS: i64 = (1 << 53) - 1;
10
11/// [`presentation_slack`] for an unknown time base: 0.5 ms plus one tick (61 ticks), as for
12/// milliseconds. Also the cap for streams whose timestamps show no rounding.
13pub const PRESENTATION_SLACK: MediaTime = MediaTime(TICKS_PER_SECOND / 2_000 + 1);
14
15/// Most leading packets whose timestamps feed [`rounding_quantum`], also capped by
16/// [`ROUNDING_SCAN_BYTES`].
17pub const ROUNDING_SCAN_FRAMES: usize = 64;
18
19/// Byte cap on the leading packets read for [`rounding_quantum`]. A native decoder holds the
20/// packets it read until it decodes them, so this keeps a 4K stream to about a second.
21pub const ROUNDING_SCAN_BYTES: u64 = 16 * 1024 * 1024;
22
23/// The unit a stream's timestamps are rounded to, in time-base units, or 0 when none shows.
24///
25/// `timestamps` are the stream's first presentation timestamps in any order. Rounding turns a
26/// frame interval into two lengths one unit apart (29.97 fps in milliseconds is 33 and 34 ms), so
27/// the unit is the GCD of the differences between intervals. Equal intervals mean exact frames.
28/// Differences of at least half the shortest interval are a variable frame rate, not rounding.
29/// Fewer than three distinct timestamps return 0.
30#[must_use]
31pub fn rounding_quantum(timestamps: &[i64]) -> i64 {
32    let mut sorted = timestamps.to_vec();
33    sorted.sort_unstable();
34    sorted.dedup();
35    let intervals = sorted
36        .windows(2)
37        .map(|pair| i128::from(pair[1]) - i128::from(pair[0]))
38        .collect::<Vec<_>>();
39    let (Some(&first), Some(&shortest)) = (intervals.first(), intervals.iter().min()) else {
40        return 0;
41    };
42    let quantum = intervals.iter().fold(0_i128, |quantum, &interval| {
43        gcd(quantum, (interval - first).abs())
44    });
45    if quantum == 0 || quantum.saturating_mul(2) >= shortest {
46        return 0;
47    }
48    i64::try_from(quantum).unwrap_or(0)
49}
50
51fn gcd(mut a: i128, mut b: i128) -> i128 {
52    while b != 0 {
53        (a, b) = (b, a % b);
54    }
55    a
56}
57
58/// How much later than source time `t` a decoded frame's timestamp may be and still be the
59/// frame shown at `t`, for a `num / den` time base rounded to `quantum` units
60/// ([`rounding_quantum`], 0 for none).
61///
62/// A frame timestamp is its true time rounded to the stream's unit and then to the tick grid,
63/// while sampled times are floored ([`crate::FrameRate::frame_start`]). A 29.97 fps Matroska
64/// file stores its third frame (66.73 ms) at 67 ms, 32 ticks after the project frame that shows
65/// it. The slack is half the rounding unit (or half the time-base unit when no rounding shows,
66/// capped at [`PRESENTATION_SLACK`]) rounded up to a tick, plus one tick. Hosts show the last
67/// frame at or before [`MediaTime::presentation_deadline`].
68///
69/// A non-positive `num` or `den` returns [`PRESENTATION_SLACK`]; a negative `quantum` means none.
70#[must_use]
71pub fn presentation_slack(num: i64, den: i64, quantum: i64) -> MediaTime {
72    if num <= 0 || den <= 0 {
73        return PRESENTATION_SLACK;
74    }
75    let (units, cap) = if quantum > 0 {
76        (i128::from(quantum), i128::MAX)
77    } else {
78        (1, i128::from(PRESENTATION_SLACK.0))
79    };
80    // Half the rounding in ticks, rounded up: (ticks per second * num * units) / (2 * den).
81    let (rounding, halves) = (
82        i128::from(TICKS_PER_SECOND)
83            .saturating_mul(i128::from(num))
84            .saturating_mul(units),
85        2 * i128::from(den),
86    );
87    let half = rounding.saturating_add(halves - 1) / halves;
88    let slack = half.saturating_add(1).min(cap);
89    MediaTime(i64::try_from(slack).unwrap_or(MAX_SERIALIZED_TICKS))
90}
91
92/// A signed position or duration in timeline ticks (120 000 per second).
93///
94/// Deserialization rejects magnitudes above [`MAX_SERIALIZED_TICKS`].
95#[derive(
96    Copy, Clone, Debug, Default, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize,
97)]
98#[serde(try_from = "i64")]
99pub struct MediaTime(pub i64);
100
101#[cfg(feature = "schema")]
102impl schemars::JsonSchema for MediaTime {
103    fn schema_name() -> std::borrow::Cow<'static, str> {
104        "MediaTime".into()
105    }
106
107    fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
108        schemars::json_schema!({
109            "description": "A signed position or duration in timeline ticks (120 000 per second), at most the largest integer a JavaScript number holds exactly either way.",
110            "type": "integer",
111            "format": "int64",
112            "minimum": -MAX_SERIALIZED_TICKS,
113            "maximum": MAX_SERIALIZED_TICKS,
114        })
115    }
116}
117
118impl TryFrom<i64> for MediaTime {
119    type Error = String;
120
121    fn try_from(ticks: i64) -> Result<Self, Self::Error> {
122        if ticks.unsigned_abs() > MAX_SERIALIZED_TICKS.unsigned_abs() {
123            return Err(format!(
124                "a time of {ticks} ticks is beyond {MAX_SERIALIZED_TICKS} either way"
125            ));
126        }
127        Ok(Self(ticks))
128    }
129}
130
131impl MediaTime {
132    /// Timeline origin.
133    pub const ZERO: Self = Self(0);
134    /// Creates a value from ticks.
135    #[must_use]
136    pub const fn from_ticks(ticks: i64) -> Self {
137        Self(ticks)
138    }
139    /// Returns raw ticks.
140    #[must_use]
141    pub const fn ticks(self) -> i64 {
142        self.0
143    }
144    /// Latest timestamp a decoded frame may carry and still be shown at this source time.
145    #[must_use]
146    pub const fn presentation_deadline(self, slack: Self) -> Self {
147        Self(self.0.saturating_add(slack.0))
148    }
149    /// Creates a value from whole seconds, saturating at the `i64` bounds.
150    #[must_use]
151    pub const fn from_seconds(seconds: i64) -> Self {
152        Self(seconds.saturating_mul(TICKS_PER_SECOND))
153    }
154    /// Checked addition.
155    #[must_use]
156    pub const fn checked_add(self, other: Self) -> Option<Self> {
157        match self.0.checked_add(other.0) {
158            Some(value) => Some(Self(value)),
159            None => None,
160        }
161    }
162    /// Checked subtraction.
163    #[must_use]
164    pub const fn checked_sub(self, other: Self) -> Option<Self> {
165        match self.0.checked_sub(other.0) {
166            Some(value) => Some(Self(value)),
167            None => None,
168        }
169    }
170}
171
172impl fmt::Display for MediaTime {
173    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
174        write!(f, "{} ticks", self.0)
175    }
176}
177
178#[cfg(test)]
179mod tests {
180    use super::*;
181    #[test]
182    fn seconds_and_overflow() {
183        assert_eq!(MediaTime::from_seconds(1).ticks(), TICKS_PER_SECOND);
184        assert_eq!(MediaTime(i64::MAX).checked_add(MediaTime(1)), None);
185    }
186
187    #[test]
188    fn a_time_base_bounds_how_late_a_shown_frame_may_be() {
189        // Milliseconds round by up to 60 ticks; 1/90000 by two thirds of one; 1/30000 by two.
190        assert_eq!(presentation_slack(1, 1_000, 0), MediaTime(61));
191        assert_eq!(presentation_slack(1, 90_000, 0), MediaTime(2));
192        assert_eq!(presentation_slack(1, 30_000, 0), MediaTime(3));
193        assert_eq!(presentation_slack(1, 15_360, 0), MediaTime(5));
194        assert_eq!(presentation_slack(1, TICKS_PER_SECOND, 0), MediaTime(2));
195        // A coarse unit with no rounding is capped at the millisecond slack.
196        assert_eq!(presentation_slack(1_001, 30_000, 0), PRESENTATION_SLACK);
197        assert_eq!(presentation_slack(1, 600, 0), PRESENTATION_SLACK);
198        assert_eq!(presentation_slack(0, 1, 0), PRESENTATION_SLACK);
199        assert_eq!(presentation_slack(1, 0, 0), PRESENTATION_SLACK);
200        assert_eq!(presentation_slack(1, 0, 16), PRESENTATION_SLACK);
201        assert_eq!(
202            MediaTime(8_008).presentation_deadline(presentation_slack(1, 1_000, 0)),
203            MediaTime(8_069)
204        );
205    }
206
207    #[test]
208    fn a_rounding_the_timestamps_show_bounds_it_however_coarse() {
209        // An MP4 that kept a Matroska file's milliseconds in 1/16000, 29.97 fps in 1/600 and 1/100.
210        assert_eq!(presentation_slack(1, 16_000, 16), MediaTime(61));
211        assert_eq!(presentation_slack(1, 600, 1), MediaTime(101));
212        assert_eq!(presentation_slack(1, 100, 1), MediaTime(601));
213        assert_eq!(presentation_slack(1, 1_000, 1), MediaTime(61));
214        assert_eq!(presentation_slack(1, 90_000, 90), MediaTime(61));
215        assert_eq!(presentation_slack(1, 1_000, -1), MediaTime(61));
216        assert_eq!(
217            presentation_slack(i64::MAX, 1, i64::MAX),
218            MediaTime(MAX_SERIALIZED_TICKS)
219        );
220    }
221
222    /// The first `count` frame starts of `num / den` fps, rounded to the nearest of `1 / base` s.
223    fn rounded(num: i64, den: i64, base: i64, count: i64) -> Vec<i64> {
224        (0..count)
225            .map(|frame| (2 * frame * den * base + num) / (2 * num))
226            .collect()
227    }
228
229    #[test]
230    fn the_spacing_of_the_first_timestamps_shows_their_rounding() {
231        // 29.97 fps in milliseconds (33, 34), the same kept in 1/16000 (528, 544), in 1/600
232        // (20, 21) and in 1/100 (3, 4).
233        assert_eq!(rounding_quantum(&rounded(30_000, 1_001, 1_000, 64)), 1);
234        let remuxed = rounded(30_000, 1_001, 1_000, 64)
235            .into_iter()
236            .map(|ms| ms * 16)
237            .collect::<Vec<_>>();
238        assert_eq!(rounding_quantum(&remuxed), 16);
239        assert_eq!(rounding_quantum(&rounded(30_000, 1_001, 600, 64)), 1);
240        assert_eq!(rounding_quantum(&rounded(30_000, 1_001, 100, 64)), 1);
241        // Exact frames: 30 fps in 1/600, 25 fps in milliseconds, 29.97 in 1001/30000.
242        assert_eq!(rounding_quantum(&rounded(30, 1, 600, 64)), 0);
243        assert_eq!(rounding_quantum(&rounded(25, 1, 1_000, 64)), 0);
244        assert_eq!(rounding_quantum(&(0..64).collect::<Vec<_>>()), 0);
245        // 60 and 30 fps frames in an exact 1/90000 time base are a variable rate, not a rounding.
246        assert_eq!(
247            rounding_quantum(&[0, 1_500, 3_000, 6_000, 7_500, 10_500]),
248            0
249        );
250        // A screen recording in milliseconds: 60 and 30 fps frames, each rounded.
251        assert_eq!(rounding_quantum(&[0, 17, 33, 67, 83, 100, 133, 150]), 1);
252        // Decode order, repeats, and too few frames to show a spacing.
253        assert_eq!(rounding_quantum(&[67, 0, 33, 33, 100, 133]), 1);
254        assert_eq!(rounding_quantum(&[0, 33]), 0);
255        assert_eq!(rounding_quantum(&[]), 0);
256        assert_eq!(rounding_quantum(&[i64::MIN, 0, i64::MAX]), 1);
257    }
258}