oximedia_timecode/
timecode_format.rs1#![allow(dead_code)]
2#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6pub enum TimecodeFormat {
7 Smpte,
9 Feet,
11 Frames,
13 Seconds,
15}
16
17impl TimecodeFormat {
18 pub fn separator(&self) -> char {
21 match self {
22 TimecodeFormat::Smpte => ':',
23 _ => ':',
24 }
25 }
26
27 pub fn drop_frame_separator(&self) -> char {
29 match self {
30 TimecodeFormat::Smpte => ';',
31 _ => ':',
32 }
33 }
34}
35
36#[derive(Debug, Clone)]
38pub struct TimecodeFormatter {
39 pub format: TimecodeFormat,
41 pub fps: u32,
43 pub drop_frame: bool,
45}
46
47impl TimecodeFormatter {
48 pub fn new(format: TimecodeFormat, fps: u32, drop_frame: bool) -> Option<Self> {
52 if fps == 0 {
53 None
54 } else {
55 Some(Self {
56 format,
57 fps,
58 drop_frame,
59 })
60 }
61 }
62
63 pub fn format_frames(&self, total_frames: u64) -> String {
65 match self.format {
66 TimecodeFormat::Frames => format!("{}", total_frames),
67
68 TimecodeFormat::Seconds => {
69 let secs = total_frames as f64 / self.fps as f64;
70 format!("{:.3}", secs)
71 }
72
73 TimecodeFormat::Feet => {
74 let feet = total_frames / 16;
76 let rem = total_frames % 16;
77 format!("{}+{:02}", feet, rem)
78 }
79
80 TimecodeFormat::Smpte => {
81 let fps = self.fps as u64;
82 let hours = total_frames / (fps * 3600);
83 let rem = total_frames % (fps * 3600);
84 let minutes = rem / (fps * 60);
85 let rem = rem % (fps * 60);
86 let seconds = rem / fps;
87 let frames = rem % fps;
88
89 let sep = if self.drop_frame { ';' } else { ':' };
90 format!(
91 "{:02}:{:02}:{:02}{}{:02}",
92 hours, minutes, seconds, sep, frames
93 )
94 }
95 }
96 }
97
98 pub fn parse_smpte(&self, s: &str) -> Option<u64> {
103 let normalized: String = s.chars().map(|c| if c == ';' { ':' } else { c }).collect();
105 let parts: Vec<&str> = normalized.split(':').collect();
106 if parts.len() != 4 {
107 return None;
108 }
109
110 let h: u64 = parts[0].parse().ok()?;
111 let m: u64 = parts[1].parse().ok()?;
112 let sec: u64 = parts[2].parse().ok()?;
113 let f: u64 = parts[3].parse().ok()?;
114
115 if m >= 60 || sec >= 60 || f >= self.fps as u64 {
116 return None;
117 }
118
119 let fps = self.fps as u64;
120 Some(h * 3600 * fps + m * 60 * fps + sec * fps + f)
121 }
122}
123
124#[cfg(test)]
125mod tests {
126 use super::*;
127
128 #[test]
129 fn test_format_enum_separator() {
130 assert_eq!(TimecodeFormat::Smpte.separator(), ':');
131 assert_eq!(TimecodeFormat::Frames.separator(), ':');
132 }
133
134 #[test]
135 fn test_drop_frame_separator() {
136 assert_eq!(TimecodeFormat::Smpte.drop_frame_separator(), ';');
137 assert_eq!(TimecodeFormat::Feet.drop_frame_separator(), ':');
138 }
139
140 #[test]
141 fn test_formatter_new_zero_fps_returns_none() {
142 assert!(TimecodeFormatter::new(TimecodeFormat::Smpte, 0, false).is_none());
143 }
144
145 #[test]
146 fn test_format_frames_frames() {
147 let fmt = TimecodeFormatter::new(TimecodeFormat::Frames, 25, false)
148 .expect("valid timecode formatter");
149 assert_eq!(fmt.format_frames(1234), "1234");
150 }
151
152 #[test]
153 fn test_format_frames_seconds() {
154 let fmt = TimecodeFormatter::new(TimecodeFormat::Seconds, 25, false)
155 .expect("valid timecode formatter");
156 assert_eq!(fmt.format_frames(25), "1.000");
158 }
159
160 #[test]
161 fn test_format_frames_feet() {
162 let fmt = TimecodeFormatter::new(TimecodeFormat::Feet, 24, false)
163 .expect("valid timecode formatter");
164 assert_eq!(fmt.format_frames(16), "1+00");
166 assert_eq!(fmt.format_frames(17), "1+01");
168 }
169
170 #[test]
171 fn test_format_frames_smpte_ndf() {
172 let fmt = TimecodeFormatter::new(TimecodeFormat::Smpte, 25, false)
173 .expect("valid timecode formatter");
174 assert_eq!(fmt.format_frames(90000), "01:00:00:00");
176 }
177
178 #[test]
179 fn test_format_frames_smpte_df() {
180 let fmt = TimecodeFormatter::new(TimecodeFormat::Smpte, 30, true)
181 .expect("valid timecode formatter");
182 assert_eq!(fmt.format_frames(30), "00:00:01;00");
184 }
185
186 #[test]
187 fn test_format_frames_smpte_mixed() {
188 let fmt = TimecodeFormatter::new(TimecodeFormat::Smpte, 25, false)
189 .expect("valid timecode formatter");
190 assert_eq!(fmt.format_frames(93079), "01:02:03:04");
192 }
193
194 #[test]
195 fn test_parse_smpte_valid_colon() {
196 let fmt = TimecodeFormatter::new(TimecodeFormat::Smpte, 25, false)
197 .expect("valid timecode formatter");
198 let frames = fmt.parse_smpte("01:02:03:04").expect("valid SMPTE parse");
199 assert_eq!(fmt.format_frames(frames), "01:02:03:04");
201 }
202
203 #[test]
204 fn test_parse_smpte_valid_semicolon() {
205 let fmt = TimecodeFormatter::new(TimecodeFormat::Smpte, 30, true)
206 .expect("valid timecode formatter");
207 let frames = fmt.parse_smpte("00:00:01;00").expect("valid SMPTE parse");
208 assert_eq!(frames, 30);
209 }
210
211 #[test]
212 fn test_parse_smpte_invalid_too_few_parts() {
213 let fmt = TimecodeFormatter::new(TimecodeFormat::Smpte, 25, false)
214 .expect("valid timecode formatter");
215 assert!(fmt.parse_smpte("01:02:03").is_none());
216 }
217
218 #[test]
219 fn test_parse_smpte_invalid_frames_exceed_fps() {
220 let fmt = TimecodeFormatter::new(TimecodeFormat::Smpte, 25, false)
221 .expect("valid timecode formatter");
222 assert!(fmt.parse_smpte("00:00:00:25").is_none());
223 }
224
225 #[test]
226 fn test_parse_smpte_invalid_minutes() {
227 let fmt = TimecodeFormatter::new(TimecodeFormat::Smpte, 25, false)
228 .expect("valid timecode formatter");
229 assert!(fmt.parse_smpte("00:60:00:00").is_none());
230 }
231}