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).unwrap();
148 assert_eq!(fmt.format_frames(1234), "1234");
149 }
150
151 #[test]
152 fn test_format_frames_seconds() {
153 let fmt = TimecodeFormatter::new(TimecodeFormat::Seconds, 25, false).unwrap();
154 assert_eq!(fmt.format_frames(25), "1.000");
156 }
157
158 #[test]
159 fn test_format_frames_feet() {
160 let fmt = TimecodeFormatter::new(TimecodeFormat::Feet, 24, false).unwrap();
161 assert_eq!(fmt.format_frames(16), "1+00");
163 assert_eq!(fmt.format_frames(17), "1+01");
165 }
166
167 #[test]
168 fn test_format_frames_smpte_ndf() {
169 let fmt = TimecodeFormatter::new(TimecodeFormat::Smpte, 25, false).unwrap();
170 assert_eq!(fmt.format_frames(90000), "01:00:00:00");
172 }
173
174 #[test]
175 fn test_format_frames_smpte_df() {
176 let fmt = TimecodeFormatter::new(TimecodeFormat::Smpte, 30, true).unwrap();
177 assert_eq!(fmt.format_frames(30), "00:00:01;00");
179 }
180
181 #[test]
182 fn test_format_frames_smpte_mixed() {
183 let fmt = TimecodeFormatter::new(TimecodeFormat::Smpte, 25, false).unwrap();
184 assert_eq!(fmt.format_frames(93079), "01:02:03:04");
186 }
187
188 #[test]
189 fn test_parse_smpte_valid_colon() {
190 let fmt = TimecodeFormatter::new(TimecodeFormat::Smpte, 25, false).unwrap();
191 let frames = fmt.parse_smpte("01:02:03:04").unwrap();
192 assert_eq!(fmt.format_frames(frames), "01:02:03:04");
194 }
195
196 #[test]
197 fn test_parse_smpte_valid_semicolon() {
198 let fmt = TimecodeFormatter::new(TimecodeFormat::Smpte, 30, true).unwrap();
199 let frames = fmt.parse_smpte("00:00:01;00").unwrap();
200 assert_eq!(frames, 30);
201 }
202
203 #[test]
204 fn test_parse_smpte_invalid_too_few_parts() {
205 let fmt = TimecodeFormatter::new(TimecodeFormat::Smpte, 25, false).unwrap();
206 assert!(fmt.parse_smpte("01:02:03").is_none());
207 }
208
209 #[test]
210 fn test_parse_smpte_invalid_frames_exceed_fps() {
211 let fmt = TimecodeFormatter::new(TimecodeFormat::Smpte, 25, false).unwrap();
212 assert!(fmt.parse_smpte("00:00:00:25").is_none());
213 }
214
215 #[test]
216 fn test_parse_smpte_invalid_minutes() {
217 let fmt = TimecodeFormatter::new(TimecodeFormat::Smpte, 25, false).unwrap();
218 assert!(fmt.parse_smpte("00:60:00:00").is_none());
219 }
220}