#![allow(
clippy::cast_possible_truncation,
clippy::cast_possible_wrap,
clippy::cast_precision_loss,
clippy::cast_sign_loss
)]
use std::cmp::Ordering;
use std::fmt;
use std::ops::{Add, Sub};
use std::time::Duration;
use super::Rational;
#[derive(Debug, Clone, Copy)]
pub struct Timestamp {
pts: i64,
time_base: Rational,
}
impl Timestamp {
#[must_use]
pub const fn new(pts: i64, time_base: Rational) -> Self {
Self { pts, time_base }
}
#[must_use]
pub const fn zero(time_base: Rational) -> Self {
Self { pts: 0, time_base }
}
#[must_use]
pub fn from_duration(duration: Duration, time_base: Rational) -> Self {
let secs = duration.as_secs_f64();
let pts = (secs / time_base.as_f64()).round() as i64;
Self { pts, time_base }
}
#[must_use]
pub fn from_secs_f64(secs: f64, time_base: Rational) -> Self {
let pts = (secs / time_base.as_f64()).round() as i64;
Self { pts, time_base }
}
#[must_use]
pub fn from_millis(millis: i64, time_base: Rational) -> Self {
let secs = millis as f64 / 1000.0;
Self::from_secs_f64(secs, time_base)
}
#[must_use]
#[inline]
pub const fn pts(&self) -> i64 {
self.pts
}
#[must_use]
#[inline]
pub const fn time_base(&self) -> Rational {
self.time_base
}
#[must_use]
pub fn as_duration(&self) -> Duration {
let secs = self.as_secs_f64();
if secs < 0.0 {
log::warn!(
"timestamp is negative, clamping to zero \
secs={secs} fallback=Duration::ZERO"
);
Duration::ZERO
} else {
Duration::from_secs_f64(secs)
}
}
#[must_use]
#[inline]
pub fn as_secs_f64(&self) -> f64 {
self.pts as f64 * self.time_base.as_f64()
}
#[must_use]
#[inline]
pub fn as_millis(&self) -> i64 {
(self.as_secs_f64() * 1000.0).round() as i64
}
#[must_use]
#[inline]
pub fn as_micros(&self) -> i64 {
(self.as_secs_f64() * 1_000_000.0).round() as i64
}
#[must_use]
#[inline]
pub fn as_frame_number(&self, fps: f64) -> u64 {
let secs = self.as_secs_f64();
if secs < 0.0 {
log::warn!(
"timestamp is negative, returning frame 0 \
secs={secs} fps={fps} fallback=0"
);
0
} else {
(secs * fps).round() as u64
}
}
#[must_use]
pub fn as_frame_number_rational(&self, fps: Rational) -> u64 {
self.as_frame_number(fps.as_f64())
}
#[must_use]
pub fn rescale(&self, new_time_base: Rational) -> Self {
let secs = self.as_secs_f64();
Self::from_secs_f64(secs, new_time_base)
}
#[must_use]
#[inline]
pub const fn is_zero(&self) -> bool {
self.pts == 0
}
#[must_use]
#[inline]
pub const fn is_negative(&self) -> bool {
self.pts < 0
}
#[must_use]
pub const fn invalid() -> Self {
Self {
pts: i64::MIN,
time_base: Rational::new(1, 1),
}
}
#[must_use]
pub const fn is_valid(&self) -> bool {
self.pts != i64::MIN
}
}
impl Default for Timestamp {
fn default() -> Self {
Self::new(0, Rational::new(1, 90000))
}
}
impl fmt::Display for Timestamp {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let secs = self.as_secs_f64();
let hours = (secs / 3600.0).floor() as u32;
let mins = ((secs % 3600.0) / 60.0).floor() as u32;
let secs_remainder = secs % 60.0;
write!(f, "{hours:02}:{mins:02}:{secs_remainder:06.3}")
}
}
impl PartialEq for Timestamp {
fn eq(&self, other: &Self) -> bool {
(self.as_secs_f64() - other.as_secs_f64()).abs() < 1e-9
}
}
impl Eq for Timestamp {}
impl PartialOrd for Timestamp {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for Timestamp {
fn cmp(&self, other: &Self) -> Ordering {
self.as_secs_f64()
.partial_cmp(&other.as_secs_f64())
.unwrap_or_else(|| {
log::warn!(
"NaN timestamp comparison, treating as equal \
self_pts={} other_pts={} fallback=Ordering::Equal",
self.pts,
other.pts
);
Ordering::Equal
})
}
}
impl Add for Timestamp {
type Output = Self;
fn add(self, rhs: Self) -> Self::Output {
let secs = self.as_secs_f64() + rhs.as_secs_f64();
Self::from_secs_f64(secs, self.time_base)
}
}
impl Sub for Timestamp {
type Output = Self;
fn sub(self, rhs: Self) -> Self::Output {
let secs = self.as_secs_f64() - rhs.as_secs_f64();
Self::from_secs_f64(secs, self.time_base)
}
}
impl Add<Duration> for Timestamp {
type Output = Self;
fn add(self, rhs: Duration) -> Self::Output {
let secs = self.as_secs_f64() + rhs.as_secs_f64();
Self::from_secs_f64(secs, self.time_base)
}
}
impl Sub<Duration> for Timestamp {
type Output = Self;
fn sub(self, rhs: Duration) -> Self::Output {
let secs = self.as_secs_f64() - rhs.as_secs_f64();
Self::from_secs_f64(secs, self.time_base)
}
}
#[cfg(test)]
#[allow(
clippy::unwrap_used,
clippy::float_cmp,
clippy::similar_names,
clippy::redundant_closure_for_method_calls
)]
mod tests {
use super::*;
fn approx_eq(a: f64, b: f64) -> bool {
(a - b).abs() < 1e-9
}
mod timestamp_tests {
use super::*;
fn time_base_90k() -> Rational {
Rational::new(1, 90000)
}
fn time_base_1k() -> Rational {
Rational::new(1, 1000)
}
#[test]
fn test_new() {
let ts = Timestamp::new(90000, time_base_90k());
assert_eq!(ts.pts(), 90000);
assert_eq!(ts.time_base(), time_base_90k());
}
#[test]
fn test_zero() {
let ts = Timestamp::zero(time_base_90k());
assert_eq!(ts.pts(), 0);
assert!(ts.is_zero());
assert!(approx_eq(ts.as_secs_f64(), 0.0));
}
#[test]
fn test_from_duration() {
let ts = Timestamp::from_duration(Duration::from_secs(1), time_base_90k());
assert_eq!(ts.pts(), 90000);
let ts = Timestamp::from_duration(Duration::from_millis(500), time_base_90k());
assert_eq!(ts.pts(), 45000);
}
#[test]
fn test_from_secs_f64() {
let ts = Timestamp::from_secs_f64(1.5, time_base_1k());
assert_eq!(ts.pts(), 1500);
}
#[test]
fn test_from_millis() {
let ts = Timestamp::from_millis(1000, time_base_90k());
assert_eq!(ts.pts(), 90000);
let ts = Timestamp::from_millis(500, time_base_1k());
assert_eq!(ts.pts(), 500);
}
#[test]
fn test_as_duration() {
let ts = Timestamp::new(90000, time_base_90k());
let duration = ts.as_duration();
assert_eq!(duration, Duration::from_secs(1));
let ts = Timestamp::new(-100, time_base_90k());
assert_eq!(ts.as_duration(), Duration::ZERO);
}
#[test]
fn test_as_secs_f64() {
let ts = Timestamp::new(45000, time_base_90k());
assert!((ts.as_secs_f64() - 0.5).abs() < 0.0001);
}
#[test]
fn test_as_millis() {
let ts = Timestamp::new(90000, time_base_90k());
assert_eq!(ts.as_millis(), 1000);
let ts = Timestamp::new(45000, time_base_90k());
assert_eq!(ts.as_millis(), 500);
}
#[test]
fn test_as_micros() {
let ts = Timestamp::new(90, time_base_90k());
assert_eq!(ts.as_micros(), 1000); }
#[test]
fn test_as_frame_number() {
let ts = Timestamp::new(90000, time_base_90k()); assert_eq!(ts.as_frame_number(30.0), 30);
assert_eq!(ts.as_frame_number(60.0), 60);
assert_eq!(ts.as_frame_number(24.0), 24);
let ts = Timestamp::new(-90000, time_base_90k());
assert_eq!(ts.as_frame_number(30.0), 0);
}
#[test]
fn test_as_frame_number_rational() {
let ts = Timestamp::new(90000, time_base_90k()); let fps = Rational::new(30, 1);
assert_eq!(ts.as_frame_number_rational(fps), 30);
}
#[test]
fn test_rescale() {
let ts = Timestamp::new(1000, time_base_1k()); let rescaled = ts.rescale(time_base_90k());
assert_eq!(rescaled.pts(), 90000);
}
#[test]
fn test_is_zero() {
assert!(Timestamp::zero(time_base_90k()).is_zero());
assert!(!Timestamp::new(1, time_base_90k()).is_zero());
}
#[test]
fn test_is_negative() {
assert!(Timestamp::new(-100, time_base_90k()).is_negative());
assert!(!Timestamp::new(100, time_base_90k()).is_negative());
assert!(!Timestamp::new(0, time_base_90k()).is_negative());
}
#[test]
fn test_display() {
let secs = 3600.0 + 120.0 + 3.456;
let ts = Timestamp::from_secs_f64(secs, time_base_90k());
let display = format!("{ts}");
assert!(display.starts_with("01:02:03"));
}
#[test]
fn test_eq() {
let ts1 = Timestamp::new(90000, time_base_90k());
let ts2 = Timestamp::new(1000, time_base_1k());
assert_eq!(ts1, ts2); }
#[test]
fn test_ord() {
let ts1 = Timestamp::new(45000, time_base_90k()); let ts2 = Timestamp::new(90000, time_base_90k()); assert!(ts1 < ts2);
assert!(ts2 > ts1);
}
#[test]
fn test_add() {
let ts1 = Timestamp::new(45000, time_base_90k());
let ts2 = Timestamp::new(45000, time_base_90k());
let sum = ts1 + ts2;
assert_eq!(sum.pts(), 90000);
}
#[test]
fn test_sub() {
let ts1 = Timestamp::new(90000, time_base_90k());
let ts2 = Timestamp::new(45000, time_base_90k());
let diff = ts1 - ts2;
assert_eq!(diff.pts(), 45000);
}
#[test]
fn test_add_duration() {
let ts = Timestamp::new(45000, time_base_90k());
let result = ts + Duration::from_millis(500);
assert_eq!(result.pts(), 90000);
}
#[test]
fn test_sub_duration() {
let ts = Timestamp::new(90000, time_base_90k());
let result = ts - Duration::from_millis(500);
assert_eq!(result.pts(), 45000);
}
#[test]
fn test_default() {
let ts = Timestamp::default();
assert_eq!(ts.pts(), 0);
assert_eq!(ts.time_base(), Rational::new(1, 90000));
}
#[test]
fn test_video_timestamps() {
let time_base = Rational::new(1, 90000);
let frame_duration_pts = 90000 / 30;
assert_eq!(frame_duration_pts, 3000);
let frame0 = Timestamp::new(0, time_base);
assert_eq!(frame0.as_frame_number(30.0), 0);
let frame30 = Timestamp::new(90000, time_base);
assert_eq!(frame30.as_frame_number(30.0), 30);
}
#[test]
fn test_audio_timestamps() {
let time_base = Rational::new(1, 48000);
let ts = Timestamp::new(1024, time_base);
let ms = ts.as_secs_f64() * 1000.0;
assert!((ms - 21.333).abs() < 0.01); }
#[test]
fn invalid_timestamp_is_not_valid() {
let ts = Timestamp::invalid();
assert!(!ts.is_valid());
}
#[test]
fn zero_timestamp_is_valid() {
let ts = Timestamp::zero(Rational::new(1, 48000));
assert!(ts.is_valid());
}
#[test]
fn real_timestamp_is_valid() {
let ts = Timestamp::new(1000, Rational::new(1, 48000));
assert!(ts.is_valid());
}
#[test]
fn default_timestamp_is_valid() {
let ts = Timestamp::default();
assert!(ts.is_valid());
}
}
}