use_media_timestamp/
lib.rs1#![forbid(unsafe_code)]
2#[derive(Debug, Clone, Copy, PartialEq)]
19pub struct MediaTimestamp {
20 seconds: f64,
21}
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub enum MediaTimestampError {
25 InvalidSeconds,
26 InvalidDuration,
27 InvalidFps,
28}
29
30fn validate_seconds(seconds: f64) -> Result<f64, MediaTimestampError> {
31 if !seconds.is_finite() || seconds < 0.0 {
32 Err(MediaTimestampError::InvalidSeconds)
33 } else {
34 Ok(seconds)
35 }
36}
37
38fn validate_duration(duration_seconds: f64) -> Result<f64, MediaTimestampError> {
39 if !duration_seconds.is_finite() || duration_seconds < 0.0 {
40 Err(MediaTimestampError::InvalidDuration)
41 } else {
42 Ok(duration_seconds)
43 }
44}
45
46fn validate_fps(fps: f64) -> Result<f64, MediaTimestampError> {
47 if !fps.is_finite() || fps <= 0.0 {
48 Err(MediaTimestampError::InvalidFps)
49 } else {
50 Ok(fps)
51 }
52}
53
54impl MediaTimestamp {
55 pub fn new(seconds: f64) -> Result<Self, MediaTimestampError> {
56 Ok(Self {
57 seconds: validate_seconds(seconds)?,
58 })
59 }
60
61 #[must_use]
62 pub fn seconds(&self) -> f64 {
63 self.seconds
64 }
65
66 #[must_use]
67 pub fn millis(&self) -> u64 {
68 (self.seconds * 1_000.0).round() as u64
69 }
70
71 pub fn frame_index(&self, fps: f64) -> Result<u64, MediaTimestampError> {
72 frame_index_at_time(self.seconds, fps)
73 }
74}
75
76pub fn timestamp_from_frame(
77 frame_index: u64,
78 fps: f64,
79) -> Result<MediaTimestamp, MediaTimestampError> {
80 MediaTimestamp::new(frame_index as f64 / validate_fps(fps)?)
81}
82
83pub fn frame_index_at_time(seconds: f64, fps: f64) -> Result<u64, MediaTimestampError> {
84 let seconds = validate_seconds(seconds)?;
85 let fps = validate_fps(fps)?;
86 Ok((seconds * fps).floor() as u64)
87}
88
89pub fn clamp_timestamp(seconds: f64, duration_seconds: f64) -> Result<f64, MediaTimestampError> {
90 let seconds = validate_seconds(seconds)?;
91 let duration_seconds = validate_duration(duration_seconds)?;
92 Ok(seconds.min(duration_seconds))
93}
94
95pub fn format_timestamp(seconds: f64) -> Result<String, MediaTimestampError> {
96 let total_millis = (validate_seconds(seconds)? * 1_000.0).round() as u64;
97 let hours = total_millis / 3_600_000;
98 let minutes = (total_millis % 3_600_000) / 60_000;
99 let seconds = (total_millis % 60_000) / 1_000;
100 let millis = total_millis % 1_000;
101
102 Ok(format!("{hours:02}:{minutes:02}:{seconds:02}.{millis:03}"))
103}
104
105#[cfg(test)]
106mod tests {
107 use super::{
108 MediaTimestamp, MediaTimestampError, clamp_timestamp, format_timestamp,
109 frame_index_at_time, timestamp_from_frame,
110 };
111
112 #[test]
113 fn converts_between_frames_and_timestamps() {
114 let timestamp = timestamp_from_frame(60, 30.0).unwrap();
115
116 assert_eq!(timestamp, MediaTimestamp::new(2.0).unwrap());
117 assert_eq!(timestamp.seconds(), 2.0);
118 assert_eq!(timestamp.millis(), 2_000);
119 assert_eq!(timestamp.frame_index(30.0).unwrap(), 60);
120 assert_eq!(frame_index_at_time(2.5, 24.0).unwrap(), 60);
121 }
122
123 #[test]
124 fn clamps_and_formats_timestamps() {
125 assert_eq!(clamp_timestamp(12.0, 10.0).unwrap(), 10.0);
126 assert_eq!(clamp_timestamp(8.0, 10.0).unwrap(), 8.0);
127 assert_eq!(format_timestamp(3661.25).unwrap(), "01:01:01.250");
128 }
129
130 #[test]
131 fn rejects_invalid_timestamp_inputs() {
132 assert_eq!(
133 MediaTimestamp::new(-1.0),
134 Err(MediaTimestampError::InvalidSeconds)
135 );
136 assert_eq!(
137 timestamp_from_frame(60, 0.0),
138 Err(MediaTimestampError::InvalidFps)
139 );
140 assert_eq!(
141 frame_index_at_time(f64::NAN, 30.0),
142 Err(MediaTimestampError::InvalidSeconds)
143 );
144 assert_eq!(
145 clamp_timestamp(1.0, -1.0),
146 Err(MediaTimestampError::InvalidDuration)
147 );
148 }
149}