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