Skip to main content

video_rs/
time.rs

1extern crate ffmpeg_next as ffmpeg;
2
3use std::time::Duration;
4
5use ffmpeg::util::mathematics::rescale::{Rescale, TIME_BASE};
6use ffmpeg::Rational as AvRational;
7
8/// Represents a time or duration.
9///
10/// [`Time`] may represent a PTS (presentation timestamp), DTS (decoder timestamp) or a duration,
11/// depending on the function that returns it.
12///
13/// [`Time`] may represent a non-existing time, in which case [`Time::has_value`] will return
14/// `false`, and conversions to seconds will return `0.0`.
15///
16/// A [`Time`] object may be aligned with another [`Time`] object, which produces an [`Aligned`]
17/// object, on which arithmetic operations can be performed.
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub struct Time {
20    time: Option<i64>,
21    time_base: AvRational,
22}
23
24impl Time {
25    /// Create a new time by its time value and time base in which the time is expressed.
26    ///
27    /// # Arguments
28    ///
29    /// * `time` - Relative time in `time_base` units.
30    /// * `time_base` - Time base of source.
31    pub fn new(time: Option<i64>, time_base: AvRational) -> Time {
32        Self { time, time_base }
33    }
34
35    /// Align the timestamp with a different time base.
36    ///
37    /// # Arguments
38    ///
39    /// # Return value
40    ///
41    /// The same timestamp, with the time base changed.
42    #[inline]
43    pub fn with_time_base(&self, time_base: AvRational) -> Self {
44        self.aligned_with_rational(time_base)
45    }
46
47    /// Creates a new timestamp that reprsents `nth` of a second.
48    ///
49    /// # Arguments
50    ///
51    /// * `nth` - Denominator of the time in seconds as in `1 / nth`.
52    pub fn from_nth_of_a_second(nth: usize) -> Self {
53        Self {
54            time: Some(1),
55            time_base: AvRational::new(1, nth as i32),
56        }
57    }
58
59    /// Creates a new timestamp from a number of seconds.
60    ///
61    /// # Arguments
62    ///
63    /// * `secs` - Number of seconds.
64    pub fn from_secs(secs: f32) -> Self {
65        Self {
66            time: Some((secs * TIME_BASE.denominator() as f32).round() as i64),
67            time_base: TIME_BASE,
68        }
69    }
70
71    /// Creates a new timestamp from a number of seconds.
72    ///
73    /// # Arguments
74    ///
75    /// * `secs` - Number of seconds.
76    pub fn from_secs_f64(secs: f64) -> Self {
77        Self {
78            time: Some((secs * TIME_BASE.denominator() as f64).round() as i64),
79            time_base: TIME_BASE,
80        }
81    }
82
83    /// Creates a new timestamp with `time` time units, each represents one / `base_den` seconds.
84    ///
85    /// # Arguments
86    ///
87    /// * `time` - Relative time in `time_base` units.
88    /// * `base_den` - Time base denominator i.e. time base is `1 / base_den`.
89    pub fn from_units(time: usize, base_den: usize) -> Self {
90        Self {
91            time: Some(time as i64),
92            time_base: AvRational::new(1, base_den as i32),
93        }
94    }
95
96    /// Create a new zero-valued timestamp.
97    pub fn zero() -> Self {
98        Time {
99            time: Some(0),
100            time_base: (1, 90000).into(),
101        }
102    }
103
104    /// Whether or not the [`Time`] has a time at all.
105    pub fn has_value(&self) -> bool {
106        self.time.is_some()
107    }
108
109    /// Whether or not the [`Time`] value is `AV_NOPTS_VALUE`.
110    pub fn has_no_pts(&self) -> bool {
111        self.time == Some(ffmpeg::ffi::AV_NOPTS_VALUE)
112    }
113
114    /// Align the timestamp with another timestamp, which will convert the `rhs` timestamp to the
115    /// same time base, such that operations can be performed upon the aligned timestamps.
116    ///
117    /// # Arguments
118    ///
119    /// * `rhs` - Right-hand side timestamp.
120    ///
121    /// # Return value
122    ///
123    /// Two timestamps that are aligned.
124    pub fn aligned_with(&self, rhs: Time) -> Aligned {
125        Aligned {
126            lhs: self.time,
127            rhs: rhs
128                .time
129                .map(|rhs_time| rhs_time.rescale(rhs.time_base, self.time_base)),
130            time_base: self.time_base,
131        }
132    }
133
134    /// Get number of seconds as floating point value.
135    pub fn as_secs(&self) -> f32 {
136        if let Some(time) = self.time {
137            (time as f32)
138                * (self.time_base.numerator() as f32 / self.time_base.denominator() as f32)
139        } else {
140            0.0
141        }
142    }
143
144    /// Get number of seconds as floating point value.
145    pub fn as_secs_f64(&self) -> f64 {
146        if let Some(time) = self.time {
147            (time as f64)
148                * (self.time_base.numerator() as f64 / self.time_base.denominator() as f64)
149        } else {
150            0.0
151        }
152    }
153
154    /// Convert to underlying parts: the `time` and `time_base`.
155    pub fn into_parts(self) -> (Option<i64>, AvRational) {
156        (self.time, self.time_base)
157    }
158
159    /// Convert to underlying time to `i64` (the number of time units).
160    ///
161    /// # Safety
162    ///
163    /// Assumes that the caller knows the time base and applies it correctly when doing arithmetic
164    /// operations on the time value.
165    pub fn into_value(self) -> Option<i64> {
166        self.time
167    }
168
169    /// Align the timestamp along another `time_base`.
170    ///
171    /// # Arguments
172    ///
173    /// * `time_base` - Target time base.
174    pub(crate) fn aligned_with_rational(&self, time_base: AvRational) -> Time {
175        Time {
176            time: self
177                .time
178                .map(|time| time.rescale(self.time_base, time_base)),
179            time_base,
180        }
181    }
182}
183
184impl From<Duration> for Time {
185    /// Convert from a [`Duration`] to [`Time`].
186    #[inline]
187    fn from(duration: Duration) -> Self {
188        Time::from_secs_f64(duration.as_secs_f64())
189    }
190}
191
192impl From<Time> for Duration {
193    /// Convert from a [`Time`] to a Rust-native [`Duration`].
194    fn from(timestamp: Time) -> Self {
195        Duration::from_secs_f64(timestamp.as_secs_f64().max(0.0))
196    }
197}
198
199impl std::fmt::Display for Time {
200    /// Format [`Time`] as follows:
201    ///
202    /// * If the inner value is not `None`: `time/time_base`.
203    /// * If the inner value is `None`: `none`.
204    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
205        if let Some(time) = self.time {
206            let num = self.time_base.numerator() as i64 * time;
207            let den = self.time_base.denominator();
208            write!(f, "{num}/{den} secs")
209        } else {
210            write!(f, "none")
211        }
212    }
213}
214
215/// This is a virtual object that represents two aligned times.
216///
217/// On this object, arthmetic operations can be performed that operate on the two contained times.
218/// This virtual object ensures that the interface to these operations is safe.
219#[derive(Debug, Clone, PartialEq, Eq)]
220pub struct Aligned {
221    lhs: Option<i64>,
222    rhs: Option<i64>,
223    time_base: AvRational,
224}
225
226impl Aligned {
227    /// Add two timestamps together.
228    pub fn add(self) -> Time {
229        self.apply(|lhs, rhs| lhs + rhs)
230    }
231
232    /// Subtract the right-hand side timestamp from the left-hand side timestamp.
233    pub fn subtract(self) -> Time {
234        self.apply(|lhs, rhs| lhs - rhs)
235    }
236
237    /// Apply operation `f` on aligned timestamps.
238    ///
239    /// # Safety
240    ///
241    /// The closure operates on the numerator of two aligned times.
242    ///
243    /// # Arguments
244    ///
245    /// * `f` - Function to apply on the two aligned time numerator values.
246    fn apply<F>(self, f: F) -> Time
247    where
248        F: FnOnce(i64, i64) -> i64,
249    {
250        match (self.lhs, self.rhs) {
251            (Some(lhs_time), Some(rhs_time)) => Time {
252                time: Some(f(lhs_time, rhs_time)),
253                time_base: self.time_base,
254            },
255            _ => Time {
256                time: None,
257                time_base: self.time_base,
258            },
259        }
260    }
261}
262
263#[cfg(test)]
264mod tests {
265    use super::*;
266
267    #[test]
268    fn test_new() {
269        let time = Time::new(Some(2), AvRational::new(3, 9));
270        assert!(time.has_value());
271        assert_eq!(time.as_secs(), 2.0 / 3.0);
272        assert_eq!(time.into_value(), Some(2));
273    }
274
275    #[test]
276    fn test_with_time_base() {
277        let time = Time::new(Some(2), AvRational::new(3, 9));
278        assert_eq!(time.as_secs(), 2.0 / 3.0);
279        let time = time.with_time_base(AvRational::new(1, 9));
280        assert_eq!(time.as_secs(), 2.0 / 3.0);
281        assert_eq!(time.into_value(), Some(6));
282    }
283
284    #[test]
285    fn test_from_nth_of_a_second() {
286        let time = Time::from_nth_of_a_second(4);
287        assert!(time.has_value());
288        assert_eq!(time.as_secs(), 0.25);
289        assert_eq!(time.as_secs_f64(), 0.25);
290        assert_eq!(Duration::from(time), Duration::from_millis(250));
291    }
292
293    #[test]
294    fn test_from_secs() {
295        let time = Time::from_secs(2.5);
296        assert!(time.has_value());
297        assert_eq!(time.as_secs(), 2.5);
298        assert_eq!(time.as_secs_f64(), 2.5);
299        assert_eq!(Duration::from(time), Duration::from_millis(2500));
300    }
301
302    #[test]
303    fn test_from_secs_f64() {
304        let time = Time::from_secs(4.0);
305        assert!(time.has_value());
306        assert_eq!(time.as_secs_f64(), 4.0);
307    }
308
309    #[test]
310    fn test_from_units() {
311        let time = Time::from_units(3, 5);
312        assert!(time.has_value());
313        assert_eq!(time.as_secs(), 3.0 / 5.0);
314        assert_eq!(Duration::from(time), Duration::from_millis(600));
315    }
316
317    #[test]
318    fn test_zero() {
319        let time = Time::zero();
320        assert!(time.has_value());
321        assert_eq!(time.as_secs(), 0.0);
322        assert_eq!(time.as_secs_f64(), 0.0);
323        assert_eq!(Duration::from(time), Duration::ZERO);
324        let time = Time::zero();
325        assert_eq!(time.into_value(), Some(0));
326    }
327
328    #[test]
329    fn test_aligned_with() {
330        let a = Time::from_units(3, 16);
331        let b = Time::from_units(1, 8);
332        let aligned = a.aligned_with(b);
333        assert_eq!(aligned.lhs, Some(3));
334        assert_eq!(aligned.rhs, Some(2));
335    }
336
337    #[test]
338    fn test_into_aligned_with() {
339        let a = Time::from_units(2, 7);
340        let b = Time::from_units(2, 3);
341        let aligned = a.aligned_with(b);
342        assert_eq!(aligned.lhs, Some(2));
343        assert_eq!(aligned.rhs, Some(5));
344    }
345
346    #[test]
347    fn test_as_secs() {
348        let time = Time::from_nth_of_a_second(4);
349        assert_eq!(time.as_secs(), 0.25);
350        let time = Time::from_secs(0.3);
351        assert_eq!(time.as_secs(), 0.3);
352        let time = Time::new(None, AvRational::new(0, 0));
353        assert_eq!(time.as_secs(), 0.0);
354    }
355
356    #[test]
357    fn test_as_secs_f64() {
358        let time = Time::from_nth_of_a_second(4);
359        assert_eq!(time.as_secs_f64(), 0.25);
360        let time = Time::from_secs_f64(0.3);
361        assert_eq!(time.as_secs_f64(), 0.3);
362        let time = Time::new(None, AvRational::new(0, 0));
363        assert_eq!(time.as_secs_f64(), 0.0);
364    }
365
366    #[test]
367    fn test_into_parts() {
368        let time = Time::new(Some(1), AvRational::new(2, 3));
369        assert_eq!(time.into_parts(), (Some(1), AvRational::new(2, 3)));
370    }
371
372    #[test]
373    fn test_into_value_none() {
374        let time = Time::new(None, AvRational::new(0, 0));
375        assert_eq!(time.into_value(), None);
376    }
377
378    #[test]
379    fn test_add() {
380        let a = Time::from_secs(0.2);
381        let b = Time::from_secs(0.3);
382        assert_eq!(a.aligned_with(b).add(), Time::from_secs(0.5));
383    }
384
385    #[test]
386    fn test_subtract() {
387        let a = Time::from_secs(0.8);
388        let b = Time::from_secs(0.4);
389        assert_eq!(a.aligned_with(b).subtract(), Time::from_secs(0.4));
390    }
391
392    #[test]
393    fn test_apply() {
394        let a = Time::from_secs(2.0);
395        let b = Time::from_secs(0.25);
396        assert_eq!(
397            a.aligned_with(b).apply(|x, y| (2 * x) + (3 * y)),
398            Time::from_secs(4.75)
399        );
400    }
401
402    #[test]
403    fn test_apply_different_time_bases() {
404        let a = Time::new(Some(3), AvRational::new(2, 32));
405        let b = Time::from_nth_of_a_second(4);
406        assert!(
407            (a.aligned_with(b).apply(|x, y| x + y).as_secs()
408                - Time::from_secs(7.0 / 16.0).as_secs())
409            .abs()
410                < 0.001
411        );
412    }
413
414    #[test]
415    fn test_negative_into_duration_clamps() {
416        assert_eq!(
417            Duration::from(Time::new(Some(-100), AvRational::new(0, 0))),
418            Duration::ZERO,
419        )
420    }
421
422    #[test]
423    fn test_av_no_pts_value() {
424        let nopts = Time::new(Some(ffmpeg::ffi::AV_NOPTS_VALUE), AvRational::new(0, 0));
425        assert_eq!(nopts.into_value(), Some(ffmpeg::ffi::AV_NOPTS_VALUE));
426        assert_eq!(Duration::from(nopts).as_secs_f32(), 0.0);
427    }
428}