ff_format/time/timestamp.rs
1//! [`Timestamp`] type for representing media timestamps.
2
3// These casts are intentional for media timestamp arithmetic.
4// The values involved (PTS, time bases, frame rates) are well within
5// the safe ranges for these conversions in practical video/audio scenarios.
6#![allow(
7 clippy::cast_possible_truncation,
8 clippy::cast_possible_wrap,
9 clippy::cast_precision_loss,
10 clippy::cast_sign_loss
11)]
12
13use std::cmp::Ordering;
14use std::fmt;
15use std::ops::{Add, Sub};
16use std::time::Duration;
17
18use super::Rational;
19
20/// A timestamp representing a point in time within a media stream.
21///
22/// Timestamps are represented as a presentation timestamp (PTS) value
23/// combined with a time base that defines the unit of measurement.
24///
25/// # Time Base
26///
27/// The time base is a rational number that represents the duration of
28/// one timestamp unit. For example:
29/// - `1/90000`: Each PTS unit is 1/90000 of a second (MPEG-TS)
30/// - `1/1000`: Each PTS unit is 1 millisecond
31/// - `1/48000`: Each PTS unit is one audio sample at 48kHz
32///
33/// # Examples
34///
35/// ```
36/// use ff_format::{Rational, Timestamp};
37/// use std::time::Duration;
38///
39/// // Create a timestamp at 1 second using 90kHz time base
40/// let time_base = Rational::new(1, 90000);
41/// let ts = Timestamp::new(90000, time_base);
42///
43/// assert!((ts.as_secs_f64() - 1.0).abs() < 0.0001);
44/// assert_eq!(ts.as_millis(), 1000);
45///
46/// // Convert from Duration
47/// let ts2 = Timestamp::from_duration(Duration::from_secs(1), time_base);
48/// assert_eq!(ts2.pts(), 90000);
49/// ```
50#[derive(Debug, Clone, Copy)]
51pub struct Timestamp {
52 pts: i64,
53 time_base: Rational,
54}
55
56impl Timestamp {
57 /// Creates a new timestamp with the given PTS value and time base.
58 ///
59 /// # Arguments
60 ///
61 /// * `pts` - The presentation timestamp value
62 /// * `time_base` - The time base (duration of one PTS unit)
63 ///
64 /// # Examples
65 ///
66 /// ```
67 /// use ff_format::{Rational, Timestamp};
68 ///
69 /// let time_base = Rational::new(1, 1000); // milliseconds
70 /// let ts = Timestamp::new(500, time_base); // 500ms
71 /// assert_eq!(ts.as_millis(), 500);
72 /// ```
73 #[must_use]
74 pub const fn new(pts: i64, time_base: Rational) -> Self {
75 Self { pts, time_base }
76 }
77
78 /// Creates a timestamp representing zero (0 PTS).
79 ///
80 /// # Examples
81 ///
82 /// ```
83 /// use ff_format::{Rational, Timestamp};
84 ///
85 /// let time_base = Rational::new(1, 90000);
86 /// let zero = Timestamp::zero(time_base);
87 /// assert_eq!(zero.pts(), 0);
88 /// assert_eq!(zero.as_secs_f64(), 0.0);
89 /// ```
90 #[must_use]
91 pub const fn zero(time_base: Rational) -> Self {
92 Self { pts: 0, time_base }
93 }
94
95 /// Creates a timestamp from a Duration value.
96 ///
97 /// # Arguments
98 ///
99 /// * `duration` - The duration to convert
100 /// * `time_base` - The target time base for the resulting timestamp
101 ///
102 /// # Examples
103 ///
104 /// ```
105 /// use ff_format::{Rational, Timestamp};
106 /// use std::time::Duration;
107 ///
108 /// let time_base = Rational::new(1, 90000);
109 /// let ts = Timestamp::from_duration(Duration::from_millis(1000), time_base);
110 /// assert_eq!(ts.pts(), 90000);
111 /// ```
112 #[must_use]
113 pub fn from_duration(duration: Duration, time_base: Rational) -> Self {
114 let secs = duration.as_secs_f64();
115 let pts = (secs / time_base.as_f64()).round() as i64;
116 Self { pts, time_base }
117 }
118
119 /// Creates a timestamp from a seconds value.
120 ///
121 /// # Examples
122 ///
123 /// ```
124 /// use ff_format::{Rational, Timestamp};
125 ///
126 /// let time_base = Rational::new(1, 1000);
127 /// let ts = Timestamp::from_secs_f64(1.5, time_base);
128 /// assert_eq!(ts.pts(), 1500);
129 /// ```
130 #[must_use]
131 pub fn from_secs_f64(secs: f64, time_base: Rational) -> Self {
132 let pts = (secs / time_base.as_f64()).round() as i64;
133 Self { pts, time_base }
134 }
135
136 /// Creates a timestamp from milliseconds.
137 ///
138 /// # Examples
139 ///
140 /// ```
141 /// use ff_format::{Rational, Timestamp};
142 ///
143 /// let time_base = Rational::new(1, 90000);
144 /// let ts = Timestamp::from_millis(1000, time_base);
145 /// assert_eq!(ts.pts(), 90000);
146 /// ```
147 #[must_use]
148 pub fn from_millis(millis: i64, time_base: Rational) -> Self {
149 let secs = millis as f64 / 1000.0;
150 Self::from_secs_f64(secs, time_base)
151 }
152
153 /// Returns the presentation timestamp value.
154 ///
155 /// # Examples
156 ///
157 /// ```
158 /// use ff_format::{Rational, Timestamp};
159 ///
160 /// let ts = Timestamp::new(12345, Rational::new(1, 90000));
161 /// assert_eq!(ts.pts(), 12345);
162 /// ```
163 #[must_use]
164 #[inline]
165 pub const fn pts(&self) -> i64 {
166 self.pts
167 }
168
169 /// Returns the time base.
170 ///
171 /// # Examples
172 ///
173 /// ```
174 /// use ff_format::{Rational, Timestamp};
175 ///
176 /// let time_base = Rational::new(1, 90000);
177 /// let ts = Timestamp::new(100, time_base);
178 /// assert_eq!(ts.time_base(), time_base);
179 /// ```
180 #[must_use]
181 #[inline]
182 pub const fn time_base(&self) -> Rational {
183 self.time_base
184 }
185
186 /// Converts the timestamp to a Duration.
187 ///
188 /// Note: Negative timestamps will be clamped to zero Duration.
189 ///
190 /// # Examples
191 ///
192 /// ```
193 /// use ff_format::{Rational, Timestamp};
194 /// use std::time::Duration;
195 ///
196 /// let ts = Timestamp::new(90000, Rational::new(1, 90000));
197 /// let duration = ts.as_duration();
198 /// assert_eq!(duration, Duration::from_secs(1));
199 /// ```
200 #[must_use]
201 pub fn as_duration(&self) -> Duration {
202 let secs = self.as_secs_f64();
203 if secs < 0.0 {
204 log::warn!(
205 "timestamp is negative, clamping to zero \
206 secs={secs} fallback=Duration::ZERO"
207 );
208 Duration::ZERO
209 } else {
210 Duration::from_secs_f64(secs)
211 }
212 }
213
214 /// Converts the timestamp to seconds as a floating-point value.
215 ///
216 /// # Examples
217 ///
218 /// ```
219 /// use ff_format::{Rational, Timestamp};
220 ///
221 /// let ts = Timestamp::new(45000, Rational::new(1, 90000));
222 /// assert!((ts.as_secs_f64() - 0.5).abs() < 0.0001);
223 /// ```
224 #[must_use]
225 #[inline]
226 pub fn as_secs_f64(&self) -> f64 {
227 self.pts as f64 * self.time_base.as_f64()
228 }
229
230 /// Converts the timestamp to milliseconds.
231 ///
232 /// # Examples
233 ///
234 /// ```
235 /// use ff_format::{Rational, Timestamp};
236 ///
237 /// let ts = Timestamp::new(90000, Rational::new(1, 90000));
238 /// assert_eq!(ts.as_millis(), 1000);
239 /// ```
240 #[must_use]
241 #[inline]
242 pub fn as_millis(&self) -> i64 {
243 (self.as_secs_f64() * 1000.0).round() as i64
244 }
245
246 /// Converts the timestamp to microseconds.
247 ///
248 /// # Examples
249 ///
250 /// ```
251 /// use ff_format::{Rational, Timestamp};
252 ///
253 /// let ts = Timestamp::new(90, Rational::new(1, 90000));
254 /// assert_eq!(ts.as_micros(), 1000); // 90/90000 = 0.001 sec = 1000 microseconds
255 /// ```
256 #[must_use]
257 #[inline]
258 pub fn as_micros(&self) -> i64 {
259 (self.as_secs_f64() * 1_000_000.0).round() as i64
260 }
261
262 /// Converts the timestamp to a frame number at the given frame rate.
263 ///
264 /// # Arguments
265 ///
266 /// * `fps` - The frame rate (frames per second)
267 ///
268 /// # Examples
269 ///
270 /// ```
271 /// use ff_format::{Rational, Timestamp};
272 ///
273 /// let ts = Timestamp::new(90000, Rational::new(1, 90000)); // 1 second
274 /// assert_eq!(ts.as_frame_number(30.0), 30); // 30 fps
275 /// assert_eq!(ts.as_frame_number(60.0), 60); // 60 fps
276 /// ```
277 #[must_use]
278 #[inline]
279 pub fn as_frame_number(&self, fps: f64) -> u64 {
280 let secs = self.as_secs_f64();
281 if secs < 0.0 {
282 log::warn!(
283 "timestamp is negative, returning frame 0 \
284 secs={secs} fps={fps} fallback=0"
285 );
286 0
287 } else {
288 (secs * fps).round() as u64
289 }
290 }
291
292 /// Converts the timestamp to a frame number using a rational frame rate.
293 ///
294 /// # Arguments
295 ///
296 /// * `fps` - The frame rate as a rational number
297 ///
298 /// # Examples
299 ///
300 /// ```
301 /// use ff_format::{Rational, Timestamp};
302 ///
303 /// let ts = Timestamp::new(90000, Rational::new(1, 90000)); // 1 second
304 /// let fps = Rational::new(30000, 1001); // 29.97 fps
305 /// let frame = ts.as_frame_number_rational(fps);
306 /// assert!(frame == 29 || frame == 30); // Should be approximately 30
307 /// ```
308 #[must_use]
309 pub fn as_frame_number_rational(&self, fps: Rational) -> u64 {
310 self.as_frame_number(fps.as_f64())
311 }
312
313 /// Rescales this timestamp to a different time base.
314 ///
315 /// # Arguments
316 ///
317 /// * `new_time_base` - The target time base
318 ///
319 /// # Examples
320 ///
321 /// ```
322 /// use ff_format::{Rational, Timestamp};
323 ///
324 /// let ts = Timestamp::new(1000, Rational::new(1, 1000)); // 1 second
325 /// let rescaled = ts.rescale(Rational::new(1, 90000));
326 /// assert_eq!(rescaled.pts(), 90000);
327 /// ```
328 #[must_use]
329 pub fn rescale(&self, new_time_base: Rational) -> Self {
330 let secs = self.as_secs_f64();
331 Self::from_secs_f64(secs, new_time_base)
332 }
333
334 /// Returns true if this timestamp is zero.
335 ///
336 /// # Examples
337 ///
338 /// ```
339 /// use ff_format::{Rational, Timestamp};
340 ///
341 /// let zero = Timestamp::zero(Rational::new(1, 90000));
342 /// assert!(zero.is_zero());
343 ///
344 /// let non_zero = Timestamp::new(100, Rational::new(1, 90000));
345 /// assert!(!non_zero.is_zero());
346 /// ```
347 #[must_use]
348 #[inline]
349 pub const fn is_zero(&self) -> bool {
350 self.pts == 0
351 }
352
353 /// Returns true if this timestamp is negative.
354 ///
355 /// # Examples
356 ///
357 /// ```
358 /// use ff_format::{Rational, Timestamp};
359 ///
360 /// let negative = Timestamp::new(-100, Rational::new(1, 90000));
361 /// assert!(negative.is_negative());
362 /// ```
363 #[must_use]
364 #[inline]
365 pub const fn is_negative(&self) -> bool {
366 self.pts < 0
367 }
368
369 /// Returns a sentinel `Timestamp` representing "no PTS available".
370 ///
371 /// This mirrors `FFmpeg`'s `AV_NOPTS_VALUE` (`INT64_MIN`). Use [`is_valid`](Self::is_valid)
372 /// to check before calling any conversion method.
373 ///
374 /// # Examples
375 ///
376 /// ```
377 /// use ff_format::Timestamp;
378 ///
379 /// let ts = Timestamp::invalid();
380 /// assert!(!ts.is_valid());
381 /// ```
382 #[must_use]
383 pub const fn invalid() -> Self {
384 Self {
385 pts: i64::MIN,
386 time_base: Rational::new(1, 1),
387 }
388 }
389
390 /// Returns `true` if this timestamp represents a real PTS value.
391 ///
392 /// Returns `false` when the timestamp was constructed via [`invalid`](Self::invalid),
393 /// which corresponds to `FFmpeg`'s `AV_NOPTS_VALUE`.
394 ///
395 /// # Examples
396 ///
397 /// ```
398 /// use ff_format::{Timestamp, Rational};
399 ///
400 /// let valid = Timestamp::new(1000, Rational::new(1, 48000));
401 /// assert!(valid.is_valid());
402 ///
403 /// let invalid = Timestamp::invalid();
404 /// assert!(!invalid.is_valid());
405 /// ```
406 #[must_use]
407 pub const fn is_valid(&self) -> bool {
408 self.pts != i64::MIN
409 }
410}
411
412impl Default for Timestamp {
413 /// Returns a default timestamp (0 with 1/90000 time base).
414 fn default() -> Self {
415 Self::new(0, Rational::new(1, 90000))
416 }
417}
418
419impl fmt::Display for Timestamp {
420 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
421 let secs = self.as_secs_f64();
422 let hours = (secs / 3600.0).floor() as u32;
423 let mins = ((secs % 3600.0) / 60.0).floor() as u32;
424 let secs_remainder = secs % 60.0;
425 write!(f, "{hours:02}:{mins:02}:{secs_remainder:06.3}")
426 }
427}
428
429impl PartialEq for Timestamp {
430 fn eq(&self, other: &Self) -> bool {
431 // Compare by converting to common representation (seconds)
432 (self.as_secs_f64() - other.as_secs_f64()).abs() < 1e-9
433 }
434}
435
436impl Eq for Timestamp {}
437
438impl PartialOrd for Timestamp {
439 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
440 Some(self.cmp(other))
441 }
442}
443
444impl Ord for Timestamp {
445 fn cmp(&self, other: &Self) -> Ordering {
446 self.as_secs_f64()
447 .partial_cmp(&other.as_secs_f64())
448 .unwrap_or_else(|| {
449 log::warn!(
450 "NaN timestamp comparison, treating as equal \
451 self_pts={} other_pts={} fallback=Ordering::Equal",
452 self.pts,
453 other.pts
454 );
455 Ordering::Equal
456 })
457 }
458}
459
460impl Add for Timestamp {
461 type Output = Self;
462
463 fn add(self, rhs: Self) -> Self::Output {
464 let secs = self.as_secs_f64() + rhs.as_secs_f64();
465 Self::from_secs_f64(secs, self.time_base)
466 }
467}
468
469impl Sub for Timestamp {
470 type Output = Self;
471
472 fn sub(self, rhs: Self) -> Self::Output {
473 let secs = self.as_secs_f64() - rhs.as_secs_f64();
474 Self::from_secs_f64(secs, self.time_base)
475 }
476}
477
478impl Add<Duration> for Timestamp {
479 type Output = Self;
480
481 fn add(self, rhs: Duration) -> Self::Output {
482 let secs = self.as_secs_f64() + rhs.as_secs_f64();
483 Self::from_secs_f64(secs, self.time_base)
484 }
485}
486
487impl Sub<Duration> for Timestamp {
488 type Output = Self;
489
490 fn sub(self, rhs: Duration) -> Self::Output {
491 let secs = self.as_secs_f64() - rhs.as_secs_f64();
492 Self::from_secs_f64(secs, self.time_base)
493 }
494}
495
496#[cfg(test)]
497#[allow(
498 clippy::unwrap_used,
499 clippy::float_cmp,
500 clippy::similar_names,
501 clippy::redundant_closure_for_method_calls
502)]
503mod tests {
504 use super::*;
505
506 /// Helper for approximate float comparison in tests
507 fn approx_eq(a: f64, b: f64) -> bool {
508 (a - b).abs() < 1e-9
509 }
510
511 mod timestamp_tests {
512 use super::*;
513
514 fn time_base_90k() -> Rational {
515 Rational::new(1, 90000)
516 }
517
518 fn time_base_1k() -> Rational {
519 Rational::new(1, 1000)
520 }
521
522 #[test]
523 fn test_new() {
524 let ts = Timestamp::new(90000, time_base_90k());
525 assert_eq!(ts.pts(), 90000);
526 assert_eq!(ts.time_base(), time_base_90k());
527 }
528
529 #[test]
530 fn test_zero() {
531 let ts = Timestamp::zero(time_base_90k());
532 assert_eq!(ts.pts(), 0);
533 assert!(ts.is_zero());
534 assert!(approx_eq(ts.as_secs_f64(), 0.0));
535 }
536
537 #[test]
538 fn test_from_duration() {
539 let ts = Timestamp::from_duration(Duration::from_secs(1), time_base_90k());
540 assert_eq!(ts.pts(), 90000);
541
542 let ts = Timestamp::from_duration(Duration::from_millis(500), time_base_90k());
543 assert_eq!(ts.pts(), 45000);
544 }
545
546 #[test]
547 fn test_from_secs_f64() {
548 let ts = Timestamp::from_secs_f64(1.5, time_base_1k());
549 assert_eq!(ts.pts(), 1500);
550 }
551
552 #[test]
553 fn test_from_millis() {
554 let ts = Timestamp::from_millis(1000, time_base_90k());
555 assert_eq!(ts.pts(), 90000);
556
557 let ts = Timestamp::from_millis(500, time_base_1k());
558 assert_eq!(ts.pts(), 500);
559 }
560
561 #[test]
562 fn test_as_duration() {
563 let ts = Timestamp::new(90000, time_base_90k());
564 let duration = ts.as_duration();
565 assert_eq!(duration, Duration::from_secs(1));
566
567 // Negative timestamp clamps to zero
568 let ts = Timestamp::new(-100, time_base_90k());
569 assert_eq!(ts.as_duration(), Duration::ZERO);
570 }
571
572 #[test]
573 fn test_as_secs_f64() {
574 let ts = Timestamp::new(45000, time_base_90k());
575 assert!((ts.as_secs_f64() - 0.5).abs() < 0.0001);
576 }
577
578 #[test]
579 fn test_as_millis() {
580 let ts = Timestamp::new(90000, time_base_90k());
581 assert_eq!(ts.as_millis(), 1000);
582
583 let ts = Timestamp::new(45000, time_base_90k());
584 assert_eq!(ts.as_millis(), 500);
585 }
586
587 #[test]
588 fn test_as_micros() {
589 let ts = Timestamp::new(90, time_base_90k());
590 assert_eq!(ts.as_micros(), 1000); // 90/90000 = 0.001 sec = 1000 us
591 }
592
593 #[test]
594 fn test_as_frame_number() {
595 let ts = Timestamp::new(90000, time_base_90k()); // 1 second
596 assert_eq!(ts.as_frame_number(30.0), 30);
597 assert_eq!(ts.as_frame_number(60.0), 60);
598 assert_eq!(ts.as_frame_number(24.0), 24);
599
600 // Negative timestamp
601 let ts = Timestamp::new(-90000, time_base_90k());
602 assert_eq!(ts.as_frame_number(30.0), 0);
603 }
604
605 #[test]
606 fn test_as_frame_number_rational() {
607 let ts = Timestamp::new(90000, time_base_90k()); // 1 second
608 let fps = Rational::new(30, 1);
609 assert_eq!(ts.as_frame_number_rational(fps), 30);
610 }
611
612 #[test]
613 fn test_rescale() {
614 let ts = Timestamp::new(1000, time_base_1k()); // 1 second
615 let rescaled = ts.rescale(time_base_90k());
616 assert_eq!(rescaled.pts(), 90000);
617 }
618
619 #[test]
620 fn test_is_zero() {
621 assert!(Timestamp::zero(time_base_90k()).is_zero());
622 assert!(!Timestamp::new(1, time_base_90k()).is_zero());
623 }
624
625 #[test]
626 fn test_is_negative() {
627 assert!(Timestamp::new(-100, time_base_90k()).is_negative());
628 assert!(!Timestamp::new(100, time_base_90k()).is_negative());
629 assert!(!Timestamp::new(0, time_base_90k()).is_negative());
630 }
631
632 #[test]
633 fn test_display() {
634 // 1 hour, 2 minutes, 3.456 seconds
635 let secs = 3600.0 + 120.0 + 3.456;
636 let ts = Timestamp::from_secs_f64(secs, time_base_90k());
637 let display = format!("{ts}");
638 assert!(display.starts_with("01:02:03"));
639 }
640
641 #[test]
642 fn test_eq() {
643 let ts1 = Timestamp::new(90000, time_base_90k());
644 let ts2 = Timestamp::new(1000, time_base_1k());
645 assert_eq!(ts1, ts2); // Both are 1 second
646 }
647
648 #[test]
649 fn test_ord() {
650 let ts1 = Timestamp::new(45000, time_base_90k()); // 0.5 sec
651 let ts2 = Timestamp::new(90000, time_base_90k()); // 1.0 sec
652 assert!(ts1 < ts2);
653 assert!(ts2 > ts1);
654 }
655
656 #[test]
657 fn test_add() {
658 let ts1 = Timestamp::new(45000, time_base_90k());
659 let ts2 = Timestamp::new(45000, time_base_90k());
660 let sum = ts1 + ts2;
661 assert_eq!(sum.pts(), 90000);
662 }
663
664 #[test]
665 fn test_sub() {
666 let ts1 = Timestamp::new(90000, time_base_90k());
667 let ts2 = Timestamp::new(45000, time_base_90k());
668 let diff = ts1 - ts2;
669 assert_eq!(diff.pts(), 45000);
670 }
671
672 #[test]
673 fn test_add_duration() {
674 let ts = Timestamp::new(45000, time_base_90k());
675 let result = ts + Duration::from_millis(500);
676 assert_eq!(result.pts(), 90000);
677 }
678
679 #[test]
680 fn test_sub_duration() {
681 let ts = Timestamp::new(90000, time_base_90k());
682 let result = ts - Duration::from_millis(500);
683 assert_eq!(result.pts(), 45000);
684 }
685
686 #[test]
687 fn test_default() {
688 let ts = Timestamp::default();
689 assert_eq!(ts.pts(), 0);
690 assert_eq!(ts.time_base(), Rational::new(1, 90000));
691 }
692
693 #[test]
694 fn test_video_timestamps() {
695 // Common video time base: 1/90000 (MPEG-TS)
696 let time_base = Rational::new(1, 90000);
697
698 // At 30 fps, each frame is 3000 PTS units
699 let frame_duration_pts = 90000 / 30;
700 assert_eq!(frame_duration_pts, 3000);
701
702 // Frame 0
703 let frame0 = Timestamp::new(0, time_base);
704 assert_eq!(frame0.as_frame_number(30.0), 0);
705
706 // Frame 30 (1 second)
707 let frame30 = Timestamp::new(90000, time_base);
708 assert_eq!(frame30.as_frame_number(30.0), 30);
709 }
710
711 #[test]
712 fn test_audio_timestamps() {
713 // Audio at 48kHz - each sample is 1/48000 seconds
714 let time_base = Rational::new(1, 48000);
715
716 // 1024 samples (common audio frame size)
717 let ts = Timestamp::new(1024, time_base);
718 let ms = ts.as_secs_f64() * 1000.0;
719 assert!((ms - 21.333).abs() < 0.01); // ~21.33 ms
720 }
721
722 #[test]
723 fn invalid_timestamp_is_not_valid() {
724 let ts = Timestamp::invalid();
725 assert!(!ts.is_valid());
726 }
727
728 #[test]
729 fn zero_timestamp_is_valid() {
730 let ts = Timestamp::zero(Rational::new(1, 48000));
731 assert!(ts.is_valid());
732 }
733
734 #[test]
735 fn real_timestamp_is_valid() {
736 let ts = Timestamp::new(1000, Rational::new(1, 48000));
737 assert!(ts.is_valid());
738 }
739
740 #[test]
741 fn default_timestamp_is_valid() {
742 // Timestamp::default() has pts=0 (not the sentinel)
743 let ts = Timestamp::default();
744 assert!(ts.is_valid());
745 }
746 }
747}