ff_format/time.rs
1//! Time primitives for video/audio processing.
2//!
3//! This module provides [`Rational`] for representing fractions (like time bases
4//! and frame rates) and [`Timestamp`] for representing media timestamps with
5//! their associated time base.
6//!
7//! # Examples
8//!
9//! ```
10//! use ff_format::{Rational, Timestamp};
11//! use std::time::Duration;
12//!
13//! // Create a rational number (e.g., 1/90000 time base)
14//! let time_base = Rational::new(1, 90000);
15//! assert_eq!(time_base.as_f64(), 1.0 / 90000.0);
16//!
17//! // Create a timestamp at 1 second (90000 * 1/90000)
18//! let ts = Timestamp::new(90000, time_base);
19//! assert!((ts.as_secs_f64() - 1.0).abs() < 0.0001);
20//!
21//! // Convert to Duration
22//! let duration = ts.as_duration();
23//! assert_eq!(duration.as_secs(), 1);
24//! ```
25
26// These casts are intentional for media timestamp arithmetic.
27// The values involved (PTS, time bases, frame rates) are well within
28// the safe ranges for these conversions in practical video/audio scenarios.
29#![allow(
30 clippy::cast_possible_truncation,
31 clippy::cast_possible_wrap,
32 clippy::cast_precision_loss,
33 clippy::cast_sign_loss
34)]
35
36use std::cmp::Ordering;
37use std::fmt;
38use std::ops::{Add, Div, Mul, Neg, Sub};
39use std::time::Duration;
40
41/// A rational number represented as a fraction (numerator / denominator).
42///
43/// This type is commonly used to represent:
44/// - Time bases (e.g., 1/90000 for MPEG-TS, 1/1000 for milliseconds)
45/// - Frame rates (e.g., 30000/1001 for 29.97 fps)
46/// - Aspect ratios (e.g., 16/9)
47///
48/// # Invariants
49///
50/// - Denominator is always positive (sign is in numerator)
51/// - Zero denominator is handled gracefully (returns infinity/NaN for conversions)
52///
53/// # Examples
54///
55/// ```
56/// use ff_format::Rational;
57///
58/// // Common time base for MPEG-TS
59/// let time_base = Rational::new(1, 90000);
60///
61/// // 29.97 fps (NTSC)
62/// let fps = Rational::new(30000, 1001);
63/// assert!((fps.as_f64() - 29.97).abs() < 0.01);
64///
65/// // Invert to get frame duration
66/// let frame_duration = fps.invert();
67/// assert_eq!(frame_duration.num(), 1001);
68/// assert_eq!(frame_duration.den(), 30000);
69/// ```
70#[derive(Debug, Clone, Copy)]
71pub struct Rational {
72 num: i32,
73 den: i32,
74}
75
76impl PartialEq for Rational {
77 fn eq(&self, other: &Self) -> bool {
78 // a/b == c/d iff a*d == b*c (cross-multiplication)
79 // Use i64 to avoid overflow
80 i64::from(self.num) * i64::from(other.den) == i64::from(other.num) * i64::from(self.den)
81 }
82}
83
84impl Eq for Rational {}
85
86impl std::hash::Hash for Rational {
87 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
88 // Hash the reduced form to ensure equal values have equal hashes
89 let reduced = self.reduce();
90 reduced.num.hash(state);
91 reduced.den.hash(state);
92 }
93}
94
95impl Rational {
96 /// Creates a new rational number.
97 ///
98 /// The denominator is normalized to always be positive (the sign is moved
99 /// to the numerator).
100 ///
101 /// # Panics
102 ///
103 /// Does not panic. A zero denominator is allowed but will result in
104 /// infinity or NaN when converted to floating-point.
105 ///
106 /// # Examples
107 ///
108 /// ```
109 /// use ff_format::Rational;
110 ///
111 /// let r = Rational::new(1, 2);
112 /// assert_eq!(r.num(), 1);
113 /// assert_eq!(r.den(), 2);
114 ///
115 /// // Negative denominator is normalized
116 /// let r = Rational::new(1, -2);
117 /// assert_eq!(r.num(), -1);
118 /// assert_eq!(r.den(), 2);
119 /// ```
120 #[must_use]
121 pub const fn new(num: i32, den: i32) -> Self {
122 // Normalize: denominator should always be positive
123 if den < 0 {
124 Self {
125 num: -num,
126 den: -den,
127 }
128 } else {
129 Self { num, den }
130 }
131 }
132
133 /// Creates a rational number representing zero (0/1).
134 ///
135 /// # Examples
136 ///
137 /// ```
138 /// use ff_format::Rational;
139 ///
140 /// let zero = Rational::zero();
141 /// assert_eq!(zero.as_f64(), 0.0);
142 /// assert!(zero.is_zero());
143 /// ```
144 #[must_use]
145 pub const fn zero() -> Self {
146 Self { num: 0, den: 1 }
147 }
148
149 /// Creates a rational number representing one (1/1).
150 ///
151 /// # Examples
152 ///
153 /// ```
154 /// use ff_format::Rational;
155 ///
156 /// let one = Rational::one();
157 /// assert_eq!(one.as_f64(), 1.0);
158 /// ```
159 #[must_use]
160 pub const fn one() -> Self {
161 Self { num: 1, den: 1 }
162 }
163
164 /// Returns the numerator.
165 ///
166 /// # Examples
167 ///
168 /// ```
169 /// use ff_format::Rational;
170 ///
171 /// let r = Rational::new(3, 4);
172 /// assert_eq!(r.num(), 3);
173 /// ```
174 #[must_use]
175 #[inline]
176 pub const fn num(&self) -> i32 {
177 self.num
178 }
179
180 /// Returns the denominator.
181 ///
182 /// The denominator is always non-negative.
183 ///
184 /// # Examples
185 ///
186 /// ```
187 /// use ff_format::Rational;
188 ///
189 /// let r = Rational::new(3, 4);
190 /// assert_eq!(r.den(), 4);
191 /// ```
192 #[must_use]
193 #[inline]
194 pub const fn den(&self) -> i32 {
195 self.den
196 }
197
198 /// Converts the rational number to a floating-point value.
199 ///
200 /// Returns `f64::INFINITY`, `f64::NEG_INFINITY`, or `f64::NAN` for
201 /// edge cases (division by zero).
202 ///
203 /// # Examples
204 ///
205 /// ```
206 /// use ff_format::Rational;
207 ///
208 /// let r = Rational::new(1, 4);
209 /// assert_eq!(r.as_f64(), 0.25);
210 ///
211 /// let r = Rational::new(1, 3);
212 /// assert!((r.as_f64() - 0.333333).abs() < 0.001);
213 /// ```
214 #[must_use]
215 #[inline]
216 pub fn as_f64(self) -> f64 {
217 if self.den == 0 {
218 match self.num.cmp(&0) {
219 Ordering::Greater => f64::INFINITY,
220 Ordering::Less => f64::NEG_INFINITY,
221 Ordering::Equal => f64::NAN,
222 }
223 } else {
224 f64::from(self.num) / f64::from(self.den)
225 }
226 }
227
228 /// Converts the rational number to a single-precision floating-point value.
229 ///
230 /// # Examples
231 ///
232 /// ```
233 /// use ff_format::Rational;
234 ///
235 /// let r = Rational::new(1, 2);
236 /// assert_eq!(r.as_f32(), 0.5);
237 /// ```
238 #[must_use]
239 #[inline]
240 pub fn as_f32(self) -> f32 {
241 self.as_f64() as f32
242 }
243
244 /// Returns the inverse (reciprocal) of this rational number.
245 ///
246 /// # Examples
247 ///
248 /// ```
249 /// use ff_format::Rational;
250 ///
251 /// let r = Rational::new(3, 4);
252 /// let inv = r.invert();
253 /// assert_eq!(inv.num(), 4);
254 /// assert_eq!(inv.den(), 3);
255 ///
256 /// // Negative values
257 /// let r = Rational::new(-3, 4);
258 /// let inv = r.invert();
259 /// assert_eq!(inv.num(), -4);
260 /// assert_eq!(inv.den(), 3);
261 /// ```
262 #[must_use]
263 pub const fn invert(self) -> Self {
264 // Handle sign normalization when inverting
265 if self.num < 0 {
266 Self {
267 num: -self.den,
268 den: -self.num,
269 }
270 } else {
271 Self {
272 num: self.den,
273 den: self.num,
274 }
275 }
276 }
277
278 /// Returns true if this rational number is zero.
279 ///
280 /// # Examples
281 ///
282 /// ```
283 /// use ff_format::Rational;
284 ///
285 /// assert!(Rational::new(0, 1).is_zero());
286 /// assert!(Rational::new(0, 100).is_zero());
287 /// assert!(!Rational::new(1, 100).is_zero());
288 /// ```
289 #[must_use]
290 #[inline]
291 pub const fn is_zero(self) -> bool {
292 self.num == 0
293 }
294
295 /// Returns true if this rational number is positive.
296 ///
297 /// # Examples
298 ///
299 /// ```
300 /// use ff_format::Rational;
301 ///
302 /// assert!(Rational::new(1, 2).is_positive());
303 /// assert!(!Rational::new(-1, 2).is_positive());
304 /// assert!(!Rational::new(0, 1).is_positive());
305 /// ```
306 #[must_use]
307 #[inline]
308 pub const fn is_positive(self) -> bool {
309 self.num > 0 && self.den > 0
310 }
311
312 /// Returns true if this rational number is negative.
313 ///
314 /// # Examples
315 ///
316 /// ```
317 /// use ff_format::Rational;
318 ///
319 /// assert!(Rational::new(-1, 2).is_negative());
320 /// assert!(!Rational::new(1, 2).is_negative());
321 /// assert!(!Rational::new(0, 1).is_negative());
322 /// ```
323 #[must_use]
324 #[inline]
325 pub const fn is_negative(self) -> bool {
326 self.num < 0 && self.den > 0
327 }
328
329 /// Returns the absolute value of this rational number.
330 ///
331 /// # Examples
332 ///
333 /// ```
334 /// use ff_format::Rational;
335 ///
336 /// assert_eq!(Rational::new(-3, 4).abs(), Rational::new(3, 4));
337 /// assert_eq!(Rational::new(3, 4).abs(), Rational::new(3, 4));
338 /// ```
339 #[must_use]
340 pub const fn abs(self) -> Self {
341 Self {
342 num: if self.num < 0 { -self.num } else { self.num },
343 den: self.den,
344 }
345 }
346
347 /// Reduces the rational to its lowest terms using GCD.
348 ///
349 /// # Examples
350 ///
351 /// ```
352 /// use ff_format::Rational;
353 ///
354 /// let r = Rational::new(4, 8);
355 /// let reduced = r.reduce();
356 /// assert_eq!(reduced.num(), 1);
357 /// assert_eq!(reduced.den(), 2);
358 /// ```
359 #[must_use]
360 pub fn reduce(self) -> Self {
361 if self.num == 0 {
362 return Self::new(0, 1);
363 }
364 let g = gcd(self.num.unsigned_abs(), self.den.unsigned_abs());
365 Self {
366 num: self.num / g as i32,
367 den: self.den / g as i32,
368 }
369 }
370}
371
372/// Computes the greatest common divisor using Euclidean algorithm.
373fn gcd(mut a: u32, mut b: u32) -> u32 {
374 while b != 0 {
375 let temp = b;
376 b = a % b;
377 a = temp;
378 }
379 a
380}
381
382impl Default for Rational {
383 /// Returns the default rational number (1/1).
384 fn default() -> Self {
385 Self::one()
386 }
387}
388
389impl fmt::Display for Rational {
390 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
391 write!(f, "{}/{}", self.num, self.den)
392 }
393}
394
395impl From<i32> for Rational {
396 fn from(n: i32) -> Self {
397 Self::new(n, 1)
398 }
399}
400
401impl From<(i32, i32)> for Rational {
402 fn from((num, den): (i32, i32)) -> Self {
403 Self::new(num, den)
404 }
405}
406
407// Arithmetic operations for Rational
408
409impl Add for Rational {
410 type Output = Self;
411
412 fn add(self, rhs: Self) -> Self::Output {
413 // a/b + c/d = (ad + bc) / bd
414 let num =
415 i64::from(self.num) * i64::from(rhs.den) + i64::from(rhs.num) * i64::from(self.den);
416 let den = i64::from(self.den) * i64::from(rhs.den);
417
418 // Try to reduce to fit in i32
419 let g = gcd(num.unsigned_abs() as u32, den.unsigned_abs() as u32);
420 Self::new((num / i64::from(g)) as i32, (den / i64::from(g)) as i32)
421 }
422}
423
424impl Sub for Rational {
425 type Output = Self;
426
427 fn sub(self, rhs: Self) -> Self::Output {
428 // a/b - c/d = (ad - bc) / bd
429 let num =
430 i64::from(self.num) * i64::from(rhs.den) - i64::from(rhs.num) * i64::from(self.den);
431 let den = i64::from(self.den) * i64::from(rhs.den);
432
433 let g = gcd(num.unsigned_abs() as u32, den.unsigned_abs() as u32);
434 Self::new((num / i64::from(g)) as i32, (den / i64::from(g)) as i32)
435 }
436}
437
438impl Mul for Rational {
439 type Output = Self;
440
441 fn mul(self, rhs: Self) -> Self::Output {
442 // a/b * c/d = ac / bd
443 let num = i64::from(self.num) * i64::from(rhs.num);
444 let den = i64::from(self.den) * i64::from(rhs.den);
445
446 let g = gcd(num.unsigned_abs() as u32, den.unsigned_abs() as u32);
447 Self::new((num / i64::from(g)) as i32, (den / i64::from(g)) as i32)
448 }
449}
450
451impl Div for Rational {
452 type Output = Self;
453
454 #[allow(clippy::suspicious_arithmetic_impl)]
455 fn div(self, rhs: Self) -> Self::Output {
456 // a/b / c/d = a/b * d/c = ad / bc
457 // Using multiplication by inverse is mathematically correct for rational division
458 self * rhs.invert()
459 }
460}
461
462impl Mul<i32> for Rational {
463 type Output = Self;
464
465 fn mul(self, rhs: i32) -> Self::Output {
466 let num = i64::from(self.num) * i64::from(rhs);
467 let g = gcd(num.unsigned_abs() as u32, self.den.unsigned_abs());
468 Self::new((num / i64::from(g)) as i32, self.den / g as i32)
469 }
470}
471
472impl Div<i32> for Rational {
473 type Output = Self;
474
475 fn div(self, rhs: i32) -> Self::Output {
476 let den = i64::from(self.den) * i64::from(rhs);
477 let g = gcd(self.num.unsigned_abs(), den.unsigned_abs() as u32);
478 Self::new(self.num / g as i32, (den / i64::from(g)) as i32)
479 }
480}
481
482impl Neg for Rational {
483 type Output = Self;
484
485 fn neg(self) -> Self::Output {
486 Self::new(-self.num, self.den)
487 }
488}
489
490impl PartialOrd for Rational {
491 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
492 Some(self.cmp(other))
493 }
494}
495
496impl Ord for Rational {
497 fn cmp(&self, other: &Self) -> Ordering {
498 // Compare a/b with c/d by comparing ad with bc
499 let left = i64::from(self.num) * i64::from(other.den);
500 let right = i64::from(other.num) * i64::from(self.den);
501 left.cmp(&right)
502 }
503}
504
505// ============================================================================
506// Timestamp
507// ============================================================================
508
509/// A timestamp representing a point in time within a media stream.
510///
511/// Timestamps are represented as a presentation timestamp (PTS) value
512/// combined with a time base that defines the unit of measurement.
513///
514/// # Time Base
515///
516/// The time base is a rational number that represents the duration of
517/// one timestamp unit. For example:
518/// - `1/90000`: Each PTS unit is 1/90000 of a second (MPEG-TS)
519/// - `1/1000`: Each PTS unit is 1 millisecond
520/// - `1/48000`: Each PTS unit is one audio sample at 48kHz
521///
522/// # Examples
523///
524/// ```
525/// use ff_format::{Rational, Timestamp};
526/// use std::time::Duration;
527///
528/// // Create a timestamp at 1 second using 90kHz time base
529/// let time_base = Rational::new(1, 90000);
530/// let ts = Timestamp::new(90000, time_base);
531///
532/// assert!((ts.as_secs_f64() - 1.0).abs() < 0.0001);
533/// assert_eq!(ts.as_millis(), 1000);
534///
535/// // Convert from Duration
536/// let ts2 = Timestamp::from_duration(Duration::from_secs(1), time_base);
537/// assert_eq!(ts2.pts(), 90000);
538/// ```
539#[derive(Debug, Clone, Copy)]
540pub struct Timestamp {
541 pts: i64,
542 time_base: Rational,
543}
544
545impl Timestamp {
546 /// Creates a new timestamp with the given PTS value and time base.
547 ///
548 /// # Arguments
549 ///
550 /// * `pts` - The presentation timestamp value
551 /// * `time_base` - The time base (duration of one PTS unit)
552 ///
553 /// # Examples
554 ///
555 /// ```
556 /// use ff_format::{Rational, Timestamp};
557 ///
558 /// let time_base = Rational::new(1, 1000); // milliseconds
559 /// let ts = Timestamp::new(500, time_base); // 500ms
560 /// assert_eq!(ts.as_millis(), 500);
561 /// ```
562 #[must_use]
563 pub const fn new(pts: i64, time_base: Rational) -> Self {
564 Self { pts, time_base }
565 }
566
567 /// Creates a timestamp representing zero (0 PTS).
568 ///
569 /// # Examples
570 ///
571 /// ```
572 /// use ff_format::{Rational, Timestamp};
573 ///
574 /// let time_base = Rational::new(1, 90000);
575 /// let zero = Timestamp::zero(time_base);
576 /// assert_eq!(zero.pts(), 0);
577 /// assert_eq!(zero.as_secs_f64(), 0.0);
578 /// ```
579 #[must_use]
580 pub const fn zero(time_base: Rational) -> Self {
581 Self { pts: 0, time_base }
582 }
583
584 /// Creates a timestamp from a Duration value.
585 ///
586 /// # Arguments
587 ///
588 /// * `duration` - The duration to convert
589 /// * `time_base` - The target time base for the resulting timestamp
590 ///
591 /// # Examples
592 ///
593 /// ```
594 /// use ff_format::{Rational, Timestamp};
595 /// use std::time::Duration;
596 ///
597 /// let time_base = Rational::new(1, 90000);
598 /// let ts = Timestamp::from_duration(Duration::from_millis(1000), time_base);
599 /// assert_eq!(ts.pts(), 90000);
600 /// ```
601 #[must_use]
602 pub fn from_duration(duration: Duration, time_base: Rational) -> Self {
603 let secs = duration.as_secs_f64();
604 let pts = (secs / time_base.as_f64()).round() as i64;
605 Self { pts, time_base }
606 }
607
608 /// Creates a timestamp from a seconds value.
609 ///
610 /// # Examples
611 ///
612 /// ```
613 /// use ff_format::{Rational, Timestamp};
614 ///
615 /// let time_base = Rational::new(1, 1000);
616 /// let ts = Timestamp::from_secs_f64(1.5, time_base);
617 /// assert_eq!(ts.pts(), 1500);
618 /// ```
619 #[must_use]
620 pub fn from_secs_f64(secs: f64, time_base: Rational) -> Self {
621 let pts = (secs / time_base.as_f64()).round() as i64;
622 Self { pts, time_base }
623 }
624
625 /// Creates a timestamp from milliseconds.
626 ///
627 /// # Examples
628 ///
629 /// ```
630 /// use ff_format::{Rational, Timestamp};
631 ///
632 /// let time_base = Rational::new(1, 90000);
633 /// let ts = Timestamp::from_millis(1000, time_base);
634 /// assert_eq!(ts.pts(), 90000);
635 /// ```
636 #[must_use]
637 pub fn from_millis(millis: i64, time_base: Rational) -> Self {
638 let secs = millis as f64 / 1000.0;
639 Self::from_secs_f64(secs, time_base)
640 }
641
642 /// Returns the presentation timestamp value.
643 ///
644 /// # Examples
645 ///
646 /// ```
647 /// use ff_format::{Rational, Timestamp};
648 ///
649 /// let ts = Timestamp::new(12345, Rational::new(1, 90000));
650 /// assert_eq!(ts.pts(), 12345);
651 /// ```
652 #[must_use]
653 #[inline]
654 pub const fn pts(&self) -> i64 {
655 self.pts
656 }
657
658 /// Returns the time base.
659 ///
660 /// # Examples
661 ///
662 /// ```
663 /// use ff_format::{Rational, Timestamp};
664 ///
665 /// let time_base = Rational::new(1, 90000);
666 /// let ts = Timestamp::new(100, time_base);
667 /// assert_eq!(ts.time_base(), time_base);
668 /// ```
669 #[must_use]
670 #[inline]
671 pub const fn time_base(&self) -> Rational {
672 self.time_base
673 }
674
675 /// Converts the timestamp to a Duration.
676 ///
677 /// Note: Negative timestamps will be clamped to zero Duration.
678 ///
679 /// # Examples
680 ///
681 /// ```
682 /// use ff_format::{Rational, Timestamp};
683 /// use std::time::Duration;
684 ///
685 /// let ts = Timestamp::new(90000, Rational::new(1, 90000));
686 /// let duration = ts.as_duration();
687 /// assert_eq!(duration, Duration::from_secs(1));
688 /// ```
689 #[must_use]
690 pub fn as_duration(&self) -> Duration {
691 let secs = self.as_secs_f64();
692 if secs < 0.0 {
693 log::warn!(
694 "timestamp is negative, clamping to zero \
695 secs={secs} fallback=Duration::ZERO"
696 );
697 Duration::ZERO
698 } else {
699 Duration::from_secs_f64(secs)
700 }
701 }
702
703 /// Converts the timestamp to seconds as a floating-point value.
704 ///
705 /// # Examples
706 ///
707 /// ```
708 /// use ff_format::{Rational, Timestamp};
709 ///
710 /// let ts = Timestamp::new(45000, Rational::new(1, 90000));
711 /// assert!((ts.as_secs_f64() - 0.5).abs() < 0.0001);
712 /// ```
713 #[must_use]
714 #[inline]
715 pub fn as_secs_f64(&self) -> f64 {
716 self.pts as f64 * self.time_base.as_f64()
717 }
718
719 /// Converts the timestamp to milliseconds.
720 ///
721 /// # Examples
722 ///
723 /// ```
724 /// use ff_format::{Rational, Timestamp};
725 ///
726 /// let ts = Timestamp::new(90000, Rational::new(1, 90000));
727 /// assert_eq!(ts.as_millis(), 1000);
728 /// ```
729 #[must_use]
730 #[inline]
731 pub fn as_millis(&self) -> i64 {
732 (self.as_secs_f64() * 1000.0).round() as i64
733 }
734
735 /// Converts the timestamp to microseconds.
736 ///
737 /// # Examples
738 ///
739 /// ```
740 /// use ff_format::{Rational, Timestamp};
741 ///
742 /// let ts = Timestamp::new(90, Rational::new(1, 90000));
743 /// assert_eq!(ts.as_micros(), 1000); // 90/90000 = 0.001 sec = 1000 microseconds
744 /// ```
745 #[must_use]
746 #[inline]
747 pub fn as_micros(&self) -> i64 {
748 (self.as_secs_f64() * 1_000_000.0).round() as i64
749 }
750
751 /// Converts the timestamp to a frame number at the given frame rate.
752 ///
753 /// # Arguments
754 ///
755 /// * `fps` - The frame rate (frames per second)
756 ///
757 /// # Examples
758 ///
759 /// ```
760 /// use ff_format::{Rational, Timestamp};
761 ///
762 /// let ts = Timestamp::new(90000, Rational::new(1, 90000)); // 1 second
763 /// assert_eq!(ts.as_frame_number(30.0), 30); // 30 fps
764 /// assert_eq!(ts.as_frame_number(60.0), 60); // 60 fps
765 /// ```
766 #[must_use]
767 #[inline]
768 pub fn as_frame_number(&self, fps: f64) -> u64 {
769 let secs = self.as_secs_f64();
770 if secs < 0.0 {
771 log::warn!(
772 "timestamp is negative, returning frame 0 \
773 secs={secs} fps={fps} fallback=0"
774 );
775 0
776 } else {
777 (secs * fps).round() as u64
778 }
779 }
780
781 /// Converts the timestamp to a frame number using a rational frame rate.
782 ///
783 /// # Arguments
784 ///
785 /// * `fps` - The frame rate as a rational number
786 ///
787 /// # Examples
788 ///
789 /// ```
790 /// use ff_format::{Rational, Timestamp};
791 ///
792 /// let ts = Timestamp::new(90000, Rational::new(1, 90000)); // 1 second
793 /// let fps = Rational::new(30000, 1001); // 29.97 fps
794 /// let frame = ts.as_frame_number_rational(fps);
795 /// assert!(frame == 29 || frame == 30); // Should be approximately 30
796 /// ```
797 #[must_use]
798 pub fn as_frame_number_rational(&self, fps: Rational) -> u64 {
799 self.as_frame_number(fps.as_f64())
800 }
801
802 /// Rescales this timestamp to a different time base.
803 ///
804 /// # Arguments
805 ///
806 /// * `new_time_base` - The target time base
807 ///
808 /// # Examples
809 ///
810 /// ```
811 /// use ff_format::{Rational, Timestamp};
812 ///
813 /// let ts = Timestamp::new(1000, Rational::new(1, 1000)); // 1 second
814 /// let rescaled = ts.rescale(Rational::new(1, 90000));
815 /// assert_eq!(rescaled.pts(), 90000);
816 /// ```
817 #[must_use]
818 pub fn rescale(&self, new_time_base: Rational) -> Self {
819 let secs = self.as_secs_f64();
820 Self::from_secs_f64(secs, new_time_base)
821 }
822
823 /// Returns true if this timestamp is zero.
824 ///
825 /// # Examples
826 ///
827 /// ```
828 /// use ff_format::{Rational, Timestamp};
829 ///
830 /// let zero = Timestamp::zero(Rational::new(1, 90000));
831 /// assert!(zero.is_zero());
832 ///
833 /// let non_zero = Timestamp::new(100, Rational::new(1, 90000));
834 /// assert!(!non_zero.is_zero());
835 /// ```
836 #[must_use]
837 #[inline]
838 pub const fn is_zero(&self) -> bool {
839 self.pts == 0
840 }
841
842 /// Returns true if this timestamp is negative.
843 ///
844 /// # Examples
845 ///
846 /// ```
847 /// use ff_format::{Rational, Timestamp};
848 ///
849 /// let negative = Timestamp::new(-100, Rational::new(1, 90000));
850 /// assert!(negative.is_negative());
851 /// ```
852 #[must_use]
853 #[inline]
854 pub const fn is_negative(&self) -> bool {
855 self.pts < 0
856 }
857
858 /// Returns a sentinel `Timestamp` representing "no PTS available".
859 ///
860 /// This mirrors `FFmpeg`'s `AV_NOPTS_VALUE` (`INT64_MIN`). Use [`is_valid`](Self::is_valid)
861 /// to check before calling any conversion method.
862 ///
863 /// # Examples
864 ///
865 /// ```
866 /// use ff_format::Timestamp;
867 ///
868 /// let ts = Timestamp::invalid();
869 /// assert!(!ts.is_valid());
870 /// ```
871 #[must_use]
872 pub const fn invalid() -> Self {
873 Self {
874 pts: i64::MIN,
875 time_base: Rational::new(1, 1),
876 }
877 }
878
879 /// Returns `true` if this timestamp represents a real PTS value.
880 ///
881 /// Returns `false` when the timestamp was constructed via [`invalid`](Self::invalid),
882 /// which corresponds to `FFmpeg`'s `AV_NOPTS_VALUE`.
883 ///
884 /// # Examples
885 ///
886 /// ```
887 /// use ff_format::{Timestamp, Rational};
888 ///
889 /// let valid = Timestamp::new(1000, Rational::new(1, 48000));
890 /// assert!(valid.is_valid());
891 ///
892 /// let invalid = Timestamp::invalid();
893 /// assert!(!invalid.is_valid());
894 /// ```
895 #[must_use]
896 pub const fn is_valid(&self) -> bool {
897 self.pts != i64::MIN
898 }
899}
900
901impl Default for Timestamp {
902 /// Returns a default timestamp (0 with 1/90000 time base).
903 fn default() -> Self {
904 Self::new(0, Rational::new(1, 90000))
905 }
906}
907
908impl fmt::Display for Timestamp {
909 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
910 let secs = self.as_secs_f64();
911 let hours = (secs / 3600.0).floor() as u32;
912 let mins = ((secs % 3600.0) / 60.0).floor() as u32;
913 let secs_remainder = secs % 60.0;
914 write!(f, "{hours:02}:{mins:02}:{secs_remainder:06.3}")
915 }
916}
917
918impl PartialEq for Timestamp {
919 fn eq(&self, other: &Self) -> bool {
920 // Compare by converting to common representation (seconds)
921 (self.as_secs_f64() - other.as_secs_f64()).abs() < 1e-9
922 }
923}
924
925impl Eq for Timestamp {}
926
927impl PartialOrd for Timestamp {
928 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
929 Some(self.cmp(other))
930 }
931}
932
933impl Ord for Timestamp {
934 fn cmp(&self, other: &Self) -> Ordering {
935 self.as_secs_f64()
936 .partial_cmp(&other.as_secs_f64())
937 .unwrap_or_else(|| {
938 log::warn!(
939 "NaN timestamp comparison, treating as equal \
940 self_pts={} other_pts={} fallback=Ordering::Equal",
941 self.pts,
942 other.pts
943 );
944 Ordering::Equal
945 })
946 }
947}
948
949impl Add for Timestamp {
950 type Output = Self;
951
952 fn add(self, rhs: Self) -> Self::Output {
953 let secs = self.as_secs_f64() + rhs.as_secs_f64();
954 Self::from_secs_f64(secs, self.time_base)
955 }
956}
957
958impl Sub for Timestamp {
959 type Output = Self;
960
961 fn sub(self, rhs: Self) -> Self::Output {
962 let secs = self.as_secs_f64() - rhs.as_secs_f64();
963 Self::from_secs_f64(secs, self.time_base)
964 }
965}
966
967impl Add<Duration> for Timestamp {
968 type Output = Self;
969
970 fn add(self, rhs: Duration) -> Self::Output {
971 let secs = self.as_secs_f64() + rhs.as_secs_f64();
972 Self::from_secs_f64(secs, self.time_base)
973 }
974}
975
976impl Sub<Duration> for Timestamp {
977 type Output = Self;
978
979 fn sub(self, rhs: Duration) -> Self::Output {
980 let secs = self.as_secs_f64() - rhs.as_secs_f64();
981 Self::from_secs_f64(secs, self.time_base)
982 }
983}
984
985#[cfg(test)]
986#[allow(
987 clippy::unwrap_used,
988 clippy::float_cmp,
989 clippy::similar_names,
990 clippy::redundant_closure_for_method_calls
991)]
992mod tests {
993 use super::*;
994
995 /// Helper for approximate float comparison in tests
996 fn approx_eq(a: f64, b: f64) -> bool {
997 (a - b).abs() < 1e-9
998 }
999
1000 // ==================== Rational Tests ====================
1001
1002 mod rational_tests {
1003 use super::*;
1004
1005 #[test]
1006 fn test_new() {
1007 let r = Rational::new(1, 2);
1008 assert_eq!(r.num(), 1);
1009 assert_eq!(r.den(), 2);
1010 }
1011
1012 #[test]
1013 fn test_new_negative_denominator() {
1014 // Negative denominator should be normalized
1015 let r = Rational::new(1, -2);
1016 assert_eq!(r.num(), -1);
1017 assert_eq!(r.den(), 2);
1018
1019 let r = Rational::new(-1, -2);
1020 assert_eq!(r.num(), 1);
1021 assert_eq!(r.den(), 2);
1022 }
1023
1024 #[test]
1025 fn test_zero_and_one() {
1026 let zero = Rational::zero();
1027 assert!(zero.is_zero());
1028 assert!(approx_eq(zero.as_f64(), 0.0));
1029
1030 let one = Rational::one();
1031 assert!(approx_eq(one.as_f64(), 1.0));
1032 assert!(!one.is_zero());
1033 }
1034
1035 #[test]
1036 fn test_as_f64() {
1037 assert!(approx_eq(Rational::new(1, 2).as_f64(), 0.5));
1038 assert!(approx_eq(Rational::new(1, 4).as_f64(), 0.25));
1039 assert!((Rational::new(1, 3).as_f64() - 0.333_333).abs() < 0.001);
1040 assert!(approx_eq(Rational::new(-1, 2).as_f64(), -0.5));
1041 }
1042
1043 #[test]
1044 fn test_as_f64_division_by_zero() {
1045 assert!(Rational::new(1, 0).as_f64().is_infinite());
1046 assert!(Rational::new(1, 0).as_f64().is_sign_positive());
1047 assert!(Rational::new(-1, 0).as_f64().is_infinite());
1048 assert!(Rational::new(-1, 0).as_f64().is_sign_negative());
1049 assert!(Rational::new(0, 0).as_f64().is_nan());
1050 }
1051
1052 #[test]
1053 fn test_as_f32() {
1054 assert_eq!(Rational::new(1, 2).as_f32(), 0.5);
1055 }
1056
1057 #[test]
1058 fn test_invert() {
1059 let r = Rational::new(3, 4);
1060 let inv = r.invert();
1061 assert_eq!(inv.num(), 4);
1062 assert_eq!(inv.den(), 3);
1063
1064 // Negative value
1065 let r = Rational::new(-3, 4);
1066 let inv = r.invert();
1067 assert_eq!(inv.num(), -4);
1068 assert_eq!(inv.den(), 3);
1069 }
1070
1071 #[test]
1072 fn test_is_positive_negative() {
1073 assert!(Rational::new(1, 2).is_positive());
1074 assert!(!Rational::new(-1, 2).is_positive());
1075 assert!(!Rational::new(0, 1).is_positive());
1076
1077 assert!(Rational::new(-1, 2).is_negative());
1078 assert!(!Rational::new(1, 2).is_negative());
1079 assert!(!Rational::new(0, 1).is_negative());
1080 }
1081
1082 #[test]
1083 fn test_abs() {
1084 assert_eq!(Rational::new(-3, 4).abs(), Rational::new(3, 4));
1085 assert_eq!(Rational::new(3, 4).abs(), Rational::new(3, 4));
1086 assert_eq!(Rational::new(0, 4).abs(), Rational::new(0, 4));
1087 }
1088
1089 #[test]
1090 fn test_reduce() {
1091 let r = Rational::new(4, 8);
1092 let reduced = r.reduce();
1093 assert_eq!(reduced.num(), 1);
1094 assert_eq!(reduced.den(), 2);
1095
1096 let r = Rational::new(6, 9);
1097 let reduced = r.reduce();
1098 assert_eq!(reduced.num(), 2);
1099 assert_eq!(reduced.den(), 3);
1100
1101 let r = Rational::new(0, 5);
1102 let reduced = r.reduce();
1103 assert_eq!(reduced.num(), 0);
1104 assert_eq!(reduced.den(), 1);
1105 }
1106
1107 #[test]
1108 fn test_add() {
1109 let a = Rational::new(1, 2);
1110 let b = Rational::new(1, 4);
1111 let result = a + b;
1112 assert!((result.as_f64() - 0.75).abs() < 0.0001);
1113 }
1114
1115 #[test]
1116 fn test_sub() {
1117 let a = Rational::new(1, 2);
1118 let b = Rational::new(1, 4);
1119 let result = a - b;
1120 assert!((result.as_f64() - 0.25).abs() < 0.0001);
1121 }
1122
1123 #[test]
1124 fn test_mul() {
1125 let a = Rational::new(1, 2);
1126 let b = Rational::new(2, 3);
1127 let result = a * b;
1128 assert!((result.as_f64() - (1.0 / 3.0)).abs() < 0.0001);
1129 }
1130
1131 #[test]
1132 fn test_div() {
1133 let a = Rational::new(1, 2);
1134 let b = Rational::new(2, 3);
1135 let result = a / b;
1136 assert!((result.as_f64() - 0.75).abs() < 0.0001);
1137 }
1138
1139 #[test]
1140 fn test_mul_i32() {
1141 let r = Rational::new(1, 4);
1142 let result = r * 2;
1143 assert!((result.as_f64() - 0.5).abs() < 0.0001);
1144 }
1145
1146 #[test]
1147 fn test_div_i32() {
1148 let r = Rational::new(1, 2);
1149 let result = r / 2;
1150 assert!((result.as_f64() - 0.25).abs() < 0.0001);
1151 }
1152
1153 #[test]
1154 fn test_neg() {
1155 let r = Rational::new(1, 2);
1156 let neg = -r;
1157 assert_eq!(neg.num(), -1);
1158 assert_eq!(neg.den(), 2);
1159 }
1160
1161 #[test]
1162 fn test_ord() {
1163 let a = Rational::new(1, 2);
1164 let b = Rational::new(1, 3);
1165 let c = Rational::new(2, 4);
1166
1167 assert!(a > b);
1168 assert!(b < a);
1169 assert_eq!(a, c);
1170 assert!(a >= c);
1171 assert!(a <= c);
1172 }
1173
1174 #[test]
1175 fn test_from_i32() {
1176 let r: Rational = 5.into();
1177 assert_eq!(r.num(), 5);
1178 assert_eq!(r.den(), 1);
1179 }
1180
1181 #[test]
1182 fn test_from_tuple() {
1183 let r: Rational = (3, 4).into();
1184 assert_eq!(r.num(), 3);
1185 assert_eq!(r.den(), 4);
1186 }
1187
1188 #[test]
1189 fn test_display() {
1190 assert_eq!(format!("{}", Rational::new(1, 2)), "1/2");
1191 assert_eq!(format!("{}", Rational::new(-3, 4)), "-3/4");
1192 }
1193
1194 #[test]
1195 fn test_default() {
1196 assert_eq!(Rational::default(), Rational::one());
1197 }
1198
1199 #[test]
1200 fn test_common_frame_rates() {
1201 // 23.976 fps (film)
1202 let fps = Rational::new(24000, 1001);
1203 assert!((fps.as_f64() - 23.976).abs() < 0.001);
1204
1205 // 29.97 fps (NTSC)
1206 let fps = Rational::new(30000, 1001);
1207 assert!((fps.as_f64() - 29.97).abs() < 0.01);
1208
1209 // 59.94 fps (NTSC interlaced as progressive)
1210 let fps = Rational::new(60000, 1001);
1211 assert!((fps.as_f64() - 59.94).abs() < 0.01);
1212 }
1213 }
1214
1215 // ==================== Timestamp Tests ====================
1216
1217 mod timestamp_tests {
1218 use super::*;
1219
1220 fn time_base_90k() -> Rational {
1221 Rational::new(1, 90000)
1222 }
1223
1224 fn time_base_1k() -> Rational {
1225 Rational::new(1, 1000)
1226 }
1227
1228 #[test]
1229 fn test_new() {
1230 let ts = Timestamp::new(90000, time_base_90k());
1231 assert_eq!(ts.pts(), 90000);
1232 assert_eq!(ts.time_base(), time_base_90k());
1233 }
1234
1235 #[test]
1236 fn test_zero() {
1237 let ts = Timestamp::zero(time_base_90k());
1238 assert_eq!(ts.pts(), 0);
1239 assert!(ts.is_zero());
1240 assert!(approx_eq(ts.as_secs_f64(), 0.0));
1241 }
1242
1243 #[test]
1244 fn test_from_duration() {
1245 let ts = Timestamp::from_duration(Duration::from_secs(1), time_base_90k());
1246 assert_eq!(ts.pts(), 90000);
1247
1248 let ts = Timestamp::from_duration(Duration::from_millis(500), time_base_90k());
1249 assert_eq!(ts.pts(), 45000);
1250 }
1251
1252 #[test]
1253 fn test_from_secs_f64() {
1254 let ts = Timestamp::from_secs_f64(1.5, time_base_1k());
1255 assert_eq!(ts.pts(), 1500);
1256 }
1257
1258 #[test]
1259 fn test_from_millis() {
1260 let ts = Timestamp::from_millis(1000, time_base_90k());
1261 assert_eq!(ts.pts(), 90000);
1262
1263 let ts = Timestamp::from_millis(500, time_base_1k());
1264 assert_eq!(ts.pts(), 500);
1265 }
1266
1267 #[test]
1268 fn test_as_duration() {
1269 let ts = Timestamp::new(90000, time_base_90k());
1270 let duration = ts.as_duration();
1271 assert_eq!(duration, Duration::from_secs(1));
1272
1273 // Negative timestamp clamps to zero
1274 let ts = Timestamp::new(-100, time_base_90k());
1275 assert_eq!(ts.as_duration(), Duration::ZERO);
1276 }
1277
1278 #[test]
1279 fn test_as_secs_f64() {
1280 let ts = Timestamp::new(45000, time_base_90k());
1281 assert!((ts.as_secs_f64() - 0.5).abs() < 0.0001);
1282 }
1283
1284 #[test]
1285 fn test_as_millis() {
1286 let ts = Timestamp::new(90000, time_base_90k());
1287 assert_eq!(ts.as_millis(), 1000);
1288
1289 let ts = Timestamp::new(45000, time_base_90k());
1290 assert_eq!(ts.as_millis(), 500);
1291 }
1292
1293 #[test]
1294 fn test_as_micros() {
1295 let ts = Timestamp::new(90, time_base_90k());
1296 assert_eq!(ts.as_micros(), 1000); // 90/90000 = 0.001 sec = 1000 us
1297 }
1298
1299 #[test]
1300 fn test_as_frame_number() {
1301 let ts = Timestamp::new(90000, time_base_90k()); // 1 second
1302 assert_eq!(ts.as_frame_number(30.0), 30);
1303 assert_eq!(ts.as_frame_number(60.0), 60);
1304 assert_eq!(ts.as_frame_number(24.0), 24);
1305
1306 // Negative timestamp
1307 let ts = Timestamp::new(-90000, time_base_90k());
1308 assert_eq!(ts.as_frame_number(30.0), 0);
1309 }
1310
1311 #[test]
1312 fn test_as_frame_number_rational() {
1313 let ts = Timestamp::new(90000, time_base_90k()); // 1 second
1314 let fps = Rational::new(30, 1);
1315 assert_eq!(ts.as_frame_number_rational(fps), 30);
1316 }
1317
1318 #[test]
1319 fn test_rescale() {
1320 let ts = Timestamp::new(1000, time_base_1k()); // 1 second
1321 let rescaled = ts.rescale(time_base_90k());
1322 assert_eq!(rescaled.pts(), 90000);
1323 }
1324
1325 #[test]
1326 fn test_is_zero() {
1327 assert!(Timestamp::zero(time_base_90k()).is_zero());
1328 assert!(!Timestamp::new(1, time_base_90k()).is_zero());
1329 }
1330
1331 #[test]
1332 fn test_is_negative() {
1333 assert!(Timestamp::new(-100, time_base_90k()).is_negative());
1334 assert!(!Timestamp::new(100, time_base_90k()).is_negative());
1335 assert!(!Timestamp::new(0, time_base_90k()).is_negative());
1336 }
1337
1338 #[test]
1339 fn test_display() {
1340 // 1 hour, 2 minutes, 3.456 seconds
1341 let secs = 3600.0 + 120.0 + 3.456;
1342 let ts = Timestamp::from_secs_f64(secs, time_base_90k());
1343 let display = format!("{ts}");
1344 assert!(display.starts_with("01:02:03"));
1345 }
1346
1347 #[test]
1348 fn test_eq() {
1349 let ts1 = Timestamp::new(90000, time_base_90k());
1350 let ts2 = Timestamp::new(1000, time_base_1k());
1351 assert_eq!(ts1, ts2); // Both are 1 second
1352 }
1353
1354 #[test]
1355 fn test_ord() {
1356 let ts1 = Timestamp::new(45000, time_base_90k()); // 0.5 sec
1357 let ts2 = Timestamp::new(90000, time_base_90k()); // 1.0 sec
1358 assert!(ts1 < ts2);
1359 assert!(ts2 > ts1);
1360 }
1361
1362 #[test]
1363 fn test_add() {
1364 let ts1 = Timestamp::new(45000, time_base_90k());
1365 let ts2 = Timestamp::new(45000, time_base_90k());
1366 let sum = ts1 + ts2;
1367 assert_eq!(sum.pts(), 90000);
1368 }
1369
1370 #[test]
1371 fn test_sub() {
1372 let ts1 = Timestamp::new(90000, time_base_90k());
1373 let ts2 = Timestamp::new(45000, time_base_90k());
1374 let diff = ts1 - ts2;
1375 assert_eq!(diff.pts(), 45000);
1376 }
1377
1378 #[test]
1379 fn test_add_duration() {
1380 let ts = Timestamp::new(45000, time_base_90k());
1381 let result = ts + Duration::from_millis(500);
1382 assert_eq!(result.pts(), 90000);
1383 }
1384
1385 #[test]
1386 fn test_sub_duration() {
1387 let ts = Timestamp::new(90000, time_base_90k());
1388 let result = ts - Duration::from_millis(500);
1389 assert_eq!(result.pts(), 45000);
1390 }
1391
1392 #[test]
1393 fn test_default() {
1394 let ts = Timestamp::default();
1395 assert_eq!(ts.pts(), 0);
1396 assert_eq!(ts.time_base(), Rational::new(1, 90000));
1397 }
1398
1399 #[test]
1400 fn test_video_timestamps() {
1401 // Common video time base: 1/90000 (MPEG-TS)
1402 let time_base = Rational::new(1, 90000);
1403
1404 // At 30 fps, each frame is 3000 PTS units
1405 let frame_duration_pts = 90000 / 30;
1406 assert_eq!(frame_duration_pts, 3000);
1407
1408 // Frame 0
1409 let frame0 = Timestamp::new(0, time_base);
1410 assert_eq!(frame0.as_frame_number(30.0), 0);
1411
1412 // Frame 30 (1 second)
1413 let frame30 = Timestamp::new(90000, time_base);
1414 assert_eq!(frame30.as_frame_number(30.0), 30);
1415 }
1416
1417 #[test]
1418 fn test_audio_timestamps() {
1419 // Audio at 48kHz - each sample is 1/48000 seconds
1420 let time_base = Rational::new(1, 48000);
1421
1422 // 1024 samples (common audio frame size)
1423 let ts = Timestamp::new(1024, time_base);
1424 let ms = ts.as_secs_f64() * 1000.0;
1425 assert!((ms - 21.333).abs() < 0.01); // ~21.33 ms
1426 }
1427
1428 #[test]
1429 fn invalid_timestamp_is_not_valid() {
1430 let ts = Timestamp::invalid();
1431 assert!(!ts.is_valid());
1432 }
1433
1434 #[test]
1435 fn zero_timestamp_is_valid() {
1436 let ts = Timestamp::zero(Rational::new(1, 48000));
1437 assert!(ts.is_valid());
1438 }
1439
1440 #[test]
1441 fn real_timestamp_is_valid() {
1442 let ts = Timestamp::new(1000, Rational::new(1, 48000));
1443 assert!(ts.is_valid());
1444 }
1445
1446 #[test]
1447 fn default_timestamp_is_valid() {
1448 // Timestamp::default() has pts=0 (not the sentinel)
1449 let ts = Timestamp::default();
1450 assert!(ts.is_valid());
1451 }
1452 }
1453
1454 // ==================== GCD Tests ====================
1455
1456 #[test]
1457 fn test_gcd() {
1458 assert_eq!(gcd(12, 8), 4);
1459 assert_eq!(gcd(17, 13), 1);
1460 assert_eq!(gcd(100, 25), 25);
1461 assert_eq!(gcd(0, 5), 5);
1462 assert_eq!(gcd(5, 0), 5);
1463 }
1464}