//! Frame-based time representation for video processing. //! //! This module provides a `FrameTime` struct that stores time as frame count //! with a given FPS (frames per second). This avoids floating-point precision //! issues when converting between seconds and frames. //! //! # Examples //! //! ``` //! use momentry_core::time::FrameTime; //! //! // Create a FrameTime from frames //! let time = FrameTime::from_frames(1234, 30.0); //! assert_eq!(time.seconds(), 41.13333333333333); //! assert_eq!(time.format_sec_frame(), "41.04"); //! //! // Create from seconds (useful for migration) //! let time = FrameTime::from_seconds(41.133333, 30.0); //! assert_eq!(time.frames(), 1234); //! ``` use serde::{Deserialize, Serialize}; use std::fmt; /// Frame-based time representation. /// /// Stores time as an integer frame count with a floating-point FPS. /// All calculations are performed using integer frame counts to avoid /// floating-point precision issues. #[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] pub struct FrameTime { /// Frame count (0-based) frames: i64, /// Frames per second (can be fractional, e.g., 29.97, 23.976) fps: f64, } impl FrameTime { /// Creates a new `FrameTime` from frame count and FPS. /// /// If `fps <= 0.0` or `fps.is_nan()`, defaults to 30.0 FPS. pub fn from_frames(frames: i64, fps: f64) -> Self { let fps = if fps <= 0.0 || !fps.is_finite() { 30.0 } else { fps }; Self { frames, fps } } /// Creates a new `FrameTime` from seconds and FPS. /// /// This is useful for migrating from existing time representations. /// The frame count is calculated as `(seconds * fps).round() as i64` /// to minimize precision loss. /// /// If `fps <= 0.0` or `fps.is_nan()`, defaults to 30.0 FPS. pub fn from_seconds(seconds: f64, fps: f64) -> Self { let fps = if fps <= 0.0 || !fps.is_finite() { 30.0 } else { fps }; let frames = (seconds * fps).round() as i64; Self { frames, fps } } /// Returns the frame count. pub fn frames(&self) -> i64 { self.frames } /// Returns the FPS (frames per second). pub fn fps(&self) -> f64 { self.fps } /// Returns the time in seconds as a floating-point value. /// /// Note: This may have precision limitations for fractional FPS values. /// For display purposes, use `format_sec_frame()` or `format_hms()` instead. pub fn seconds(&self) -> f64 { self.frames as f64 / self.fps } /// Formats the time as "seconds.frame" with fixed two-digit frame number. /// /// The frame number is displayed as a zero-padded two-digit number /// representing the frame within the current second. /// /// # Examples /// /// - `123.04` = 123 seconds, frame 4 (at 30 FPS, frame 4 = 0.133 seconds) /// - `5.29` = 5 seconds, frame 29 (at 30 FPS, last frame of that second) pub fn format_sec_frame(&self) -> String { let total_seconds = self.frames as f64 / self.fps; let seconds = total_seconds.floor() as i64; // For fractional FPS, use ceil of fps for modulo operation let fps_ceil = self.fps.ceil() as i64; // Ensure fps_ceil > 0 let frames_in_second = if fps_ceil == 0 { 0 } else { self.frames % fps_ceil }; // Handle negative frames let frames_in_second = if frames_in_second < 0 { // This shouldn't happen in practice 0 } else { frames_in_second }; format!("{}.{:02}", seconds, frames_in_second) } /// Formats the time as "HH:MM:SS" (hours, minutes, seconds). /// /// This displays whole seconds only, without frame information. /// Useful for human-readable time displays. pub fn format_hms(&self) -> String { let total_seconds = self.seconds(); let hours = (total_seconds / 3600.0) as i64; let minutes = ((total_seconds % 3600.0) / 60.0) as i64; let seconds = (total_seconds % 60.0) as i64; format!("{:02}:{:02}:{:02}", hours, minutes, seconds) } /// Formats the time as "HH:MM:SS.FF" (hours, minutes, seconds, frames). /// /// Displays full time with frame information. Frames are shown as /// zero-padded two-digit numbers. pub fn format_hms_frame(&self) -> String { let total_seconds = self.seconds(); let hours = (total_seconds / 3600.0) as i64; let minutes = ((total_seconds % 3600.0) / 60.0) as i64; let seconds = (total_seconds % 60.0) as i64; // For fractional FPS, use ceil of fps for modulo operation let fps_ceil = self.fps.ceil() as i64; let frames_in_second = if fps_ceil == 0 { 0 } else { self.frames % fps_ceil }; let frames_in_second = if frames_in_second < 0 { 0 } else { frames_in_second }; format!( "{:02}:{:02}:{:02}.{:02}", hours, minutes, seconds, frames_in_second ) } /// Adds frames to this time, returning a new `FrameTime`. /// /// # Panics /// /// Panics if the FPS doesn't match. pub fn add_frames(&self, frames: i64) -> Self { Self { frames: self.frames + frames, fps: self.fps, } } /// Subtracts frames from this time, returning a new `FrameTime`. /// /// # Panics /// /// Panics if the FPS doesn't match or if the result would be negative. pub fn sub_frames(&self, frames: i64) -> Self { assert!( self.frames >= frames, "Cannot subtract more frames than available" ); Self { frames: self.frames - frames, fps: self.fps, } } /// Adds seconds to this time, returning a new `FrameTime`. /// /// # Panics /// /// Panics if the FPS doesn't match. pub fn add_seconds(&self, seconds: f64) -> Self { let frames_to_add = (seconds * self.fps).round() as i64; self.add_frames(frames_to_add) } /// Subtracts seconds from this time, returning a new `FrameTime`. /// /// # Panics /// /// Panics if the FPS doesn't match or if the result would be negative. pub fn sub_seconds(&self, seconds: f64) -> Self { let frames_to_sub = (seconds * self.fps).round() as i64; self.sub_frames(frames_to_sub) } /// Returns the duration between two `FrameTime` instances. /// /// # Panics /// /// Panics if the FPS values don't match. pub fn duration(&self, other: &FrameTime) -> FrameDuration { assert!( (self.fps - other.fps).abs() < f64::EPSILON * 2.0, "FPS mismatch: {} != {}", self.fps, other.fps ); let frame_diff = (self.frames - other.frames).abs(); FrameDuration::from_frames(frame_diff, self.fps) } } impl fmt::Display for FrameTime { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.format_sec_frame()) } } /// Duration between two frame times. #[derive(Debug, Clone, Copy, PartialEq)] pub struct FrameDuration { frames: i64, fps: f64, } impl FrameDuration { /// Creates a duration from frame count and FPS. /// If `fps <= 0.0` or `fps.is_nan()`, defaults to 30.0 FPS. pub fn from_frames(frames: i64, fps: f64) -> Self { let fps = if fps <= 0.0 || !fps.is_finite() { 30.0 } else { fps }; Self { frames, fps } } /// Creates a duration from seconds and FPS. /// If `fps <= 0.0` or `fps.is_nan()`, defaults to 30.0 FPS. pub fn from_seconds(seconds: f64, fps: f64) -> Self { let fps = if fps <= 0.0 || !fps.is_finite() { 30.0 } else { fps }; let frames = (seconds * fps).round() as i64; Self { frames, fps } } /// Returns the duration in frames. pub fn frames(&self) -> i64 { self.frames } /// Returns the duration in seconds. pub fn seconds(&self) -> f64 { self.frames as f64 / self.fps } /// Formats the duration as "seconds.frame" (same as `FrameTime`). pub fn format_sec_frame(&self) -> String { let temp_time = FrameTime::from_frames(self.frames, self.fps); temp_time.format_sec_frame() } /// Formats the duration as "HH:MM:SS". pub fn format_hms(&self) -> String { let temp_time = FrameTime::from_frames(self.frames, self.fps); temp_time.format_hms() } } impl fmt::Display for FrameDuration { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.format_sec_frame()) } } #[cfg(test)] mod tests { use super::*; #[test] fn test_from_frames() { let time = FrameTime::from_frames(150, 30.0); assert_eq!(time.frames(), 150); assert_eq!(time.fps(), 30.0); assert_eq!(time.seconds(), 5.0); } #[test] fn test_from_seconds() { let time = FrameTime::from_seconds(5.0, 30.0); assert_eq!(time.frames(), 150); assert_eq!(time.seconds(), 5.0); } #[test] fn test_format_sec_frame() { let time = FrameTime::from_frames(123, 30.0); assert_eq!(time.format_sec_frame(), "4.03"); let time = FrameTime::from_frames(29, 30.0); assert_eq!(time.format_sec_frame(), "0.29"); let time = FrameTime::from_frames(60, 30.0); assert_eq!(time.format_sec_frame(), "2.00"); } #[test] fn test_format_sec_frame_fractional_fps() { // 29.97 fps (NTSC) let time = FrameTime::from_frames(30, 29.97); // 30 frames at 29.97 fps = 1.001 seconds = 1 second, frame 0 assert_eq!(time.format_sec_frame(), "1.00"); let time = FrameTime::from_frames(60, 29.97); // 60 frames at 29.97 fps = 2.002 seconds = 2 seconds, frame 0 assert_eq!(time.format_sec_frame(), "2.00"); } #[test] fn test_format_hms() { let time = FrameTime::from_frames(3661, 30.0); // 122.033 seconds = 2 minutes 2 seconds assert_eq!(time.format_hms(), "00:02:02"); let time = FrameTime::from_frames(4500, 30.0); // 150 seconds = 2 minutes 30 seconds assert_eq!(time.format_hms(), "00:02:30"); } #[test] fn test_format_hms_frame() { let time = FrameTime::from_frames(123, 30.0); // 4 seconds, 3 frames assert_eq!(time.format_hms_frame(), "00:00:04.03"); } #[test] fn test_add_sub_frames() { let time = FrameTime::from_frames(100, 30.0); let new_time = time.add_frames(50); assert_eq!(new_time.frames(), 150); let new_time = time.sub_frames(30); assert_eq!(new_time.frames(), 70); } #[test] fn test_add_sub_seconds() { let time = FrameTime::from_frames(100, 30.0); let new_time = time.add_seconds(2.0); assert_eq!(new_time.frames(), 160); // 100 + 60 let new_time = time.sub_seconds(1.0); assert_eq!(new_time.frames(), 70); // 100 - 30 } #[test] fn test_duration() { let time1 = FrameTime::from_frames(200, 30.0); let time2 = FrameTime::from_frames(150, 30.0); let duration = time1.duration(&time2); assert_eq!(duration.frames(), 50); assert_eq!(duration.seconds(), 50.0 / 30.0); } #[test] fn test_frame_duration() { let duration = FrameDuration::from_frames(90, 30.0); assert_eq!(duration.seconds(), 3.0); assert_eq!(duration.format_sec_frame(), "3.00"); assert_eq!(duration.format_hms(), "00:00:03"); } }