1use std::fmt;
10
11#[allow(dead_code)]
17pub struct DropFrameCalc;
18
19impl DropFrameCalc {
20 const FRAMES_PER_SEC: u64 = 30;
22 const DROP_PER_MIN: u64 = 2;
23 const FRAMES_PER_DROP_MIN: u64 = Self::FRAMES_PER_SEC * 60 - Self::DROP_PER_MIN; const FRAMES_PER_10_MIN: u64 = Self::FRAMES_PER_DROP_MIN * 9 + Self::FRAMES_PER_SEC * 60; const FRAMES_PER_HOUR: u64 = Self::FRAMES_PER_10_MIN * 6; #[must_use]
39 pub fn frame_count_to_df(frame_count: u64) -> (u8, u8, u8, u8) {
40 let d = frame_count % (Self::FRAMES_PER_HOUR * 24);
42
43 let d_ten = d / Self::FRAMES_PER_10_MIN;
45 let d_in_ten = d % Self::FRAMES_PER_10_MIN;
46
47 let (min_in_ten, d_in_min) = if d_in_ten < Self::FRAMES_PER_SEC * 60 {
51 (0u64, d_in_ten)
52 } else {
53 let d_after_first = d_in_ten - Self::FRAMES_PER_SEC * 60;
54 let extra_min = d_after_first / Self::FRAMES_PER_DROP_MIN;
55 let d_in_drop_min = d_after_first % Self::FRAMES_PER_DROP_MIN;
56 (extra_min + 1, d_in_drop_min)
57 };
58
59 let total_minutes = d_ten * 10 + min_in_ten;
60 let hh = (total_minutes / 60) as u8;
61 let mm = (total_minutes % 60) as u8;
62
63 let (ss, ff) = if min_in_ten > 0 {
67 let adjusted = d_in_min + Self::DROP_PER_MIN;
70 let ss = adjusted / Self::FRAMES_PER_SEC;
71 let ff = adjusted % Self::FRAMES_PER_SEC;
72 (ss as u8, ff as u8)
73 } else {
74 let ss = d_in_min / Self::FRAMES_PER_SEC;
76 let ff = d_in_min % Self::FRAMES_PER_SEC;
77 (ss as u8, ff as u8)
78 };
79
80 (hh, mm, ss, ff)
81 }
82
83 #[must_use]
91 pub fn df_to_frame_count(hh: u8, mm: u8, ss: u8, ff: u8) -> u64 {
92 let hh = u64::from(hh);
93 let mm = u64::from(mm);
94 let ss = u64::from(ss);
95 let ff = u64::from(ff);
96
97 let total_minutes = hh * 60 + mm;
98
99 let raw = hh * 108000 + mm * 1800 + ss * 30 + ff;
101
102 let dropped = Self::DROP_PER_MIN * (total_minutes - total_minutes / 10);
104
105 raw - dropped
106 }
107
108 #[must_use]
112 pub fn format_df(frame_count: u64) -> String {
113 let (hh, mm, ss, ff) = Self::frame_count_to_df(frame_count);
114 format!("{hh:02};{mm:02};{ss:02};{ff:02}")
115 }
116
117 #[must_use]
121 pub fn parse_df(tc: &str) -> Option<u64> {
122 let parts: Vec<&str> = tc.split(';').collect();
123 if parts.len() != 4 {
124 return None;
125 }
126
127 let hh: u8 = parts[0].parse().ok()?;
128 let mm: u8 = parts[1].parse().ok()?;
129 let ss: u8 = parts[2].parse().ok()?;
130 let ff: u8 = parts[3].parse().ok()?;
131
132 if hh > 23 || mm > 59 || ss > 59 || ff > 29 {
133 return None;
134 }
135
136 Some(Self::df_to_frame_count(hh, mm, ss, ff))
137 }
138
139 #[must_use]
143 pub fn is_dropped_frame(hh: u8, mm: u8, ss: u8, ff: u8) -> bool {
144 let _ = hh; ss == 0 && ff < 2 && !mm.is_multiple_of(10)
146 }
147}
148
149#[derive(Debug, Clone)]
151#[allow(dead_code)]
152pub struct TotalFrameCounter {
153 frame_count: u64,
155 drop_frame: bool,
157 fps: u8,
159}
160
161impl TotalFrameCounter {
162 #[must_use]
164 pub fn new_drop_frame() -> Self {
165 Self {
166 frame_count: 0,
167 drop_frame: true,
168 fps: 30,
169 }
170 }
171
172 #[must_use]
174 pub fn new_non_drop_frame(fps: u8) -> Self {
175 Self {
176 frame_count: 0,
177 drop_frame: false,
178 fps,
179 }
180 }
181
182 pub fn add_frames(&mut self, n: u64) {
184 self.frame_count = self.frame_count.wrapping_add(n);
185 }
186
187 #[must_use]
189 pub fn frame_count(&self) -> u64 {
190 self.frame_count
191 }
192
193 pub fn reset(&mut self) {
195 self.frame_count = 0;
196 }
197
198 #[must_use]
200 pub fn is_drop_frame(&self) -> bool {
201 self.drop_frame
202 }
203}
204
205impl fmt::Display for TotalFrameCounter {
206 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
207 if self.drop_frame {
208 write!(f, "{}", DropFrameCalc::format_df(self.frame_count))
209 } else {
210 let fps = u64::from(self.fps);
212 let seconds_total = self.frame_count / fps;
213 let frames = self.frame_count % fps;
214 let seconds = seconds_total % 60;
215 let minutes_total = seconds_total / 60;
216 let minutes = minutes_total % 60;
217 let hours = (minutes_total / 60) % 24;
218 write!(f, "{hours:02}:{minutes:02}:{seconds:02}:{frames:02}")
219 }
220 }
221}
222
223#[cfg(test)]
224mod tests {
225 use super::*;
226
227 #[test]
228 fn test_df_zero() {
229 let (hh, mm, ss, ff) = DropFrameCalc::frame_count_to_df(0);
230 assert_eq!((hh, mm, ss, ff), (0, 0, 0, 0));
231 }
232
233 #[test]
234 fn test_df_one_frame() {
235 let (hh, mm, ss, ff) = DropFrameCalc::frame_count_to_df(1);
236 assert_eq!((hh, mm, ss, ff), (0, 0, 0, 1));
237 }
238
239 #[test]
240 fn test_df_one_second() {
241 let (hh, mm, ss, ff) = DropFrameCalc::frame_count_to_df(30);
242 assert_eq!((hh, mm, ss, ff), (0, 0, 1, 0));
243 }
244
245 #[test]
246 fn test_df_one_minute() {
247 let (hh, mm, ss, ff) = DropFrameCalc::frame_count_to_df(1800);
250 assert_eq!(hh, 0);
251 assert_eq!(mm, 1);
252 assert_eq!(ss, 0);
253 assert_eq!(ff, 2); }
255
256 #[test]
257 fn test_df_ten_minutes() {
258 let frames = DropFrameCalc::df_to_frame_count(0, 10, 0, 0);
260 let (hh, mm, ss, ff) = DropFrameCalc::frame_count_to_df(frames);
261 assert_eq!((hh, mm, ss, ff), (0, 10, 0, 0));
262 }
263
264 #[test]
265 fn test_df_roundtrip() {
266 let test_cases = [
267 (0u8, 0u8, 0u8, 0u8),
268 (0, 0, 0, 15),
269 (0, 0, 30, 0),
270 (0, 10, 0, 0), (1, 0, 0, 0),
272 ];
273
274 for (hh, mm, ss, ff) in test_cases {
275 let frame_count = DropFrameCalc::df_to_frame_count(hh, mm, ss, ff);
276 let (rhh, rmm, rss, rff) = DropFrameCalc::frame_count_to_df(frame_count);
277 assert_eq!(
278 (hh, mm, ss, ff),
279 (rhh, rmm, rss, rff),
280 "Roundtrip failed for {hh:02};{mm:02};{ss:02};{ff:02}"
281 );
282 }
283 }
284
285 #[test]
286 fn test_format_df() {
287 let s = DropFrameCalc::format_df(0);
288 assert_eq!(s, "00;00;00;00");
289 }
290
291 #[test]
292 fn test_parse_df_valid() {
293 let count = DropFrameCalc::parse_df("00;00;00;00").expect("should succeed");
294 assert_eq!(count, 0);
295 }
296
297 #[test]
298 fn test_parse_df_invalid() {
299 assert!(DropFrameCalc::parse_df("00:00:00:00").is_none()); assert!(DropFrameCalc::parse_df("25;00;00;00").is_none()); assert!(DropFrameCalc::parse_df("not;a;timecode;x").is_none());
302 assert!(DropFrameCalc::parse_df("").is_none());
303 }
304
305 #[test]
306 fn test_is_dropped_frame() {
307 assert!(DropFrameCalc::is_dropped_frame(0, 1, 0, 0));
309 assert!(DropFrameCalc::is_dropped_frame(0, 1, 0, 1));
310 assert!(!DropFrameCalc::is_dropped_frame(0, 1, 0, 2));
311
312 assert!(!DropFrameCalc::is_dropped_frame(0, 10, 0, 0));
314 assert!(!DropFrameCalc::is_dropped_frame(0, 10, 0, 1));
315
316 assert!(!DropFrameCalc::is_dropped_frame(0, 1, 1, 0));
318 }
319
320 #[test]
321 fn test_total_frame_counter_drop_frame() {
322 let mut counter = TotalFrameCounter::new_drop_frame();
323 assert!(counter.is_drop_frame());
324 counter.add_frames(100);
325 assert_eq!(counter.frame_count(), 100);
326 let s = counter.to_string();
327 assert!(s.contains(';')); }
329
330 #[test]
331 fn test_total_frame_counter_non_drop_frame() {
332 let mut counter = TotalFrameCounter::new_non_drop_frame(25);
333 assert!(!counter.is_drop_frame());
334 counter.add_frames(25); assert_eq!(counter.frame_count(), 25);
336 let s = counter.to_string();
337 assert!(s.contains(':'));
338 assert_eq!(s, "00:00:01:00");
339 }
340
341 #[test]
342 fn test_total_frame_counter_reset() {
343 let mut counter = TotalFrameCounter::new_drop_frame();
344 counter.add_frames(1000);
345 counter.reset();
346 assert_eq!(counter.frame_count(), 0);
347 }
348
349 #[test]
350 fn test_parse_format_roundtrip() {
351 let original = "01;05;30;15";
352 let frame_count = DropFrameCalc::parse_df(original).expect("should succeed");
353 let formatted = DropFrameCalc::format_df(frame_count);
354 assert_eq!(formatted, original);
355 }
356}