384 lines
12 KiB
Rust
384 lines
12 KiB
Rust
//! 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");
|
|
}
|
|
}
|