1#![allow(
14 clippy::cast_possible_truncation,
15 clippy::cast_precision_loss,
16 clippy::cast_sign_loss,
17 dead_code,
18 clippy::pedantic
19)]
20
21pub mod burn_in;
22pub mod continuity;
23pub mod drop_frame;
24pub mod duration;
25pub mod frame_offset;
26pub mod frame_rate;
27pub mod jam_sync;
28pub mod ltc;
29pub mod ltc_encoder;
30pub mod ltc_parser;
31pub mod midi_timecode;
32pub mod reader;
33pub mod sync;
34pub mod sync_map;
35pub mod tc_calculator;
36pub mod tc_compare;
37pub mod tc_convert;
38pub mod tc_drift;
39pub mod tc_interpolate;
40pub mod tc_math;
41pub mod tc_metadata;
42pub mod tc_offset_table;
43pub mod tc_range;
44pub mod tc_sequence;
45pub mod tc_smpte_ranges;
46pub mod tc_subtitle_sync;
47pub mod tc_validator;
48pub mod timecode_calculator;
49pub mod timecode_event;
50pub mod timecode_format;
51pub mod timecode_generator;
52pub mod timecode_range;
53pub mod vitc;
54
55use std::fmt;
56
57#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
59pub enum FrameRate {
60 Fps23976,
62 Fps23976DF,
64 Fps24,
66 Fps25,
68 Fps2997DF,
70 Fps2997NDF,
72 Fps30,
74 Fps47952,
76 Fps47952DF,
78 Fps50,
80 Fps5994,
82 Fps5994DF,
84 Fps60,
86 Fps120,
88}
89
90impl FrameRate {
91 pub fn as_float(&self) -> f64 {
93 match self {
94 FrameRate::Fps23976 | FrameRate::Fps23976DF => 24000.0 / 1001.0,
95 FrameRate::Fps24 => 24.0,
96 FrameRate::Fps25 => 25.0,
97 FrameRate::Fps2997DF | FrameRate::Fps2997NDF => 30000.0 / 1001.0,
98 FrameRate::Fps30 => 30.0,
99 FrameRate::Fps47952 | FrameRate::Fps47952DF => 48000.0 / 1001.0,
100 FrameRate::Fps50 => 50.0,
101 FrameRate::Fps5994 | FrameRate::Fps5994DF => 60000.0 / 1001.0,
102 FrameRate::Fps60 => 60.0,
103 FrameRate::Fps120 => 120.0,
104 }
105 }
106
107 pub fn as_rational(&self) -> (u32, u32) {
109 match self {
110 FrameRate::Fps23976 | FrameRate::Fps23976DF => (24000, 1001),
111 FrameRate::Fps24 => (24, 1),
112 FrameRate::Fps25 => (25, 1),
113 FrameRate::Fps2997DF | FrameRate::Fps2997NDF => (30000, 1001),
114 FrameRate::Fps30 => (30, 1),
115 FrameRate::Fps47952 | FrameRate::Fps47952DF => (48000, 1001),
116 FrameRate::Fps50 => (50, 1),
117 FrameRate::Fps5994 | FrameRate::Fps5994DF => (60000, 1001),
118 FrameRate::Fps60 => (60, 1),
119 FrameRate::Fps120 => (120, 1),
120 }
121 }
122
123 pub fn is_drop_frame(&self) -> bool {
125 matches!(
126 self,
127 FrameRate::Fps2997DF
128 | FrameRate::Fps23976DF
129 | FrameRate::Fps5994DF
130 | FrameRate::Fps47952DF
131 )
132 }
133
134 pub fn drop_frames_per_minute(&self) -> u64 {
141 match self {
142 FrameRate::Fps23976DF => 2,
143 FrameRate::Fps2997DF => 2,
144 FrameRate::Fps47952DF => 4,
145 FrameRate::Fps5994DF => 4,
146 _ => 0,
147 }
148 }
149
150 pub fn frames_per_second(&self) -> u32 {
152 match self {
153 FrameRate::Fps23976 | FrameRate::Fps23976DF => 24,
154 FrameRate::Fps24 => 24,
155 FrameRate::Fps25 => 25,
156 FrameRate::Fps2997DF | FrameRate::Fps2997NDF => 30,
157 FrameRate::Fps30 => 30,
158 FrameRate::Fps47952 | FrameRate::Fps47952DF => 48,
159 FrameRate::Fps50 => 50,
160 FrameRate::Fps5994 | FrameRate::Fps5994DF => 60,
161 FrameRate::Fps60 => 60,
162 FrameRate::Fps120 => 120,
163 }
164 }
165}
166
167#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize)]
169pub struct FrameRateInfo {
170 pub fps: u8,
172 pub drop_frame: bool,
174}
175
176impl PartialEq for FrameRateInfo {
177 fn eq(&self, other: &Self) -> bool {
178 self.fps == other.fps && self.drop_frame == other.drop_frame
179 }
180}
181
182impl Eq for FrameRateInfo {}
183
184pub fn frame_rate_from_info(info: &FrameRateInfo) -> FrameRate {
190 match (info.fps, info.drop_frame) {
191 (24, true) => FrameRate::Fps23976DF,
192 (24, false) => FrameRate::Fps23976, (25, _) => FrameRate::Fps25,
194 (30, true) => FrameRate::Fps2997DF,
195 (30, false) => FrameRate::Fps2997NDF,
196 (48, true) => FrameRate::Fps47952DF,
197 (48, false) => FrameRate::Fps47952,
198 (50, _) => FrameRate::Fps50,
199 (60, true) => FrameRate::Fps5994DF,
200 (60, false) => FrameRate::Fps5994,
201 (120, _) => FrameRate::Fps120,
202 _ => FrameRate::Fps25, }
204}
205
206#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize)]
213pub struct Timecode {
214 pub hours: u8,
216 pub minutes: u8,
218 pub seconds: u8,
220 pub frames: u8,
222 pub frame_rate: FrameRateInfo,
224 pub user_bits: u32,
226 #[serde(skip)]
228 frame_count_cache: u64,
229}
230
231impl PartialEq for Timecode {
232 fn eq(&self, other: &Self) -> bool {
233 self.hours == other.hours
234 && self.minutes == other.minutes
235 && self.seconds == other.seconds
236 && self.frames == other.frames
237 && self.frame_rate == other.frame_rate
238 && self.user_bits == other.user_bits
239 }
240}
241
242impl Eq for Timecode {}
243
244impl PartialOrd for Timecode {
245 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
246 Some(self.cmp(other))
247 }
248}
249
250impl Ord for Timecode {
251 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
252 self.to_frames().cmp(&other.to_frames())
253 }
254}
255
256impl Timecode {
257 fn compute_frames_from_fields(
260 hours: u8,
261 minutes: u8,
262 seconds: u8,
263 frames: u8,
264 fps: u64,
265 drop_frame: bool,
266 ) -> u64 {
267 let mut total = hours as u64 * 3600 * fps;
268 total += minutes as u64 * 60 * fps;
269 total += seconds as u64 * fps;
270 total += frames as u64;
271
272 if drop_frame {
273 let drop_per_min = if fps >= 60 { 4u64 } else { 2u64 };
274 let total_minutes = hours as u64 * 60 + minutes as u64;
275 let dropped_frames = drop_per_min * (total_minutes - total_minutes / 10);
276 total -= dropped_frames;
277 }
278
279 total
280 }
281
282 pub fn new(
284 hours: u8,
285 minutes: u8,
286 seconds: u8,
287 frames: u8,
288 frame_rate: FrameRate,
289 ) -> Result<Self, TimecodeError> {
290 let fps = frame_rate.frames_per_second() as u8;
291
292 if hours > 23 {
293 return Err(TimecodeError::InvalidHours);
294 }
295 if minutes > 59 {
296 return Err(TimecodeError::InvalidMinutes);
297 }
298 if seconds > 59 {
299 return Err(TimecodeError::InvalidSeconds);
300 }
301 if frames >= fps {
302 return Err(TimecodeError::InvalidFrames);
303 }
304
305 if frame_rate.is_drop_frame() {
307 let drop_count = frame_rate.drop_frames_per_minute() as u8;
308 if seconds == 0 && frames < drop_count && !minutes.is_multiple_of(10) {
311 return Err(TimecodeError::InvalidDropFrame);
312 }
313 }
314
315 let drop_frame = frame_rate.is_drop_frame();
316 let frame_count_cache = Self::compute_frames_from_fields(
317 hours, minutes, seconds, frames, fps as u64, drop_frame,
318 );
319
320 Ok(Timecode {
321 hours,
322 minutes,
323 seconds,
324 frames,
325 frame_rate: FrameRateInfo { fps, drop_frame },
326 user_bits: 0,
327 frame_count_cache,
328 })
329 }
330
331 pub fn from_string(s: &str, frame_rate: FrameRate) -> Result<Self, TimecodeError> {
344 let s = s.trim();
345 if s.len() < 11 {
347 return Err(TimecodeError::InvalidConfiguration);
348 }
349
350 let parts: Vec<&str> = s.split([':', ';']).collect();
352 if parts.len() != 4 {
353 return Err(TimecodeError::InvalidConfiguration);
354 }
355
356 let hours: u8 = parts[0].parse().map_err(|_| TimecodeError::InvalidHours)?;
357 let minutes: u8 = parts[1]
358 .parse()
359 .map_err(|_| TimecodeError::InvalidMinutes)?;
360 let seconds: u8 = parts[2]
361 .parse()
362 .map_err(|_| TimecodeError::InvalidSeconds)?;
363 let frames: u8 = parts[3].parse().map_err(|_| TimecodeError::InvalidFrames)?;
364
365 Self::new(hours, minutes, seconds, frames, frame_rate)
366 }
367
368 pub fn from_raw_fields(
374 hours: u8,
375 minutes: u8,
376 seconds: u8,
377 frames: u8,
378 fps: u8,
379 drop_frame: bool,
380 user_bits: u32,
381 ) -> Self {
382 let frame_count_cache = Self::compute_frames_from_fields(
383 hours, minutes, seconds, frames, fps as u64, drop_frame,
384 );
385 Self {
386 hours,
387 minutes,
388 seconds,
389 frames,
390 frame_rate: FrameRateInfo { fps, drop_frame },
391 user_bits,
392 frame_count_cache,
393 }
394 }
395
396 pub fn with_user_bits(mut self, user_bits: u32) -> Self {
398 self.user_bits = user_bits;
399 self
400 }
401
402 #[inline]
406 pub fn to_frames(&self) -> u64 {
407 self.frame_count_cache
408 }
409
410 #[allow(clippy::cast_precision_loss)]
415 pub fn to_seconds_f64(&self) -> f64 {
416 let rate = frame_rate_from_info(&self.frame_rate);
417 let (num, den) = rate.as_rational();
418 self.frame_count_cache as f64 * den as f64 / num as f64
420 }
421
422 pub fn from_frames(frames: u64, frame_rate: FrameRate) -> Result<Self, TimecodeError> {
424 let fps = frame_rate.frames_per_second() as u64;
425 let mut remaining = frames;
426
427 if frame_rate.is_drop_frame() {
429 let drop_per_min = frame_rate.drop_frames_per_minute();
430 let frames_per_minute = fps * 60 - drop_per_min;
431 let frames_per_10_minutes = frames_per_minute * 9 + fps * 60;
432
433 let ten_minute_blocks = remaining / frames_per_10_minutes;
434 remaining += ten_minute_blocks * (drop_per_min * 9);
435
436 let remaining_in_block = remaining % frames_per_10_minutes;
437 if remaining_in_block >= fps * 60 {
438 let extra_minutes = (remaining_in_block - fps * 60) / frames_per_minute;
439 remaining += (extra_minutes + 1) * drop_per_min;
440 }
441 }
442
443 let hours = (remaining / (fps * 3600)) as u8;
444 remaining %= fps * 3600;
445 let minutes = (remaining / (fps * 60)) as u8;
446 remaining %= fps * 60;
447 let seconds = (remaining / fps) as u8;
448 let frame = (remaining % fps) as u8;
449
450 Self::new(hours, minutes, seconds, frame, frame_rate)
451 }
452
453 pub fn increment(&mut self) -> Result<(), TimecodeError> {
455 self.frames += 1;
456
457 if self.frames >= self.frame_rate.fps {
458 self.frames = 0;
459 self.seconds += 1;
460
461 if self.seconds >= 60 {
462 self.seconds = 0;
463 self.minutes += 1;
464
465 if self.frame_rate.drop_frame && !self.minutes.is_multiple_of(10) {
467 let drop_count = if self.frame_rate.fps >= 60 { 4u8 } else { 2u8 };
468 self.frames = drop_count;
469 }
470
471 if self.minutes >= 60 {
472 self.minutes = 0;
473 self.hours += 1;
474
475 if self.hours >= 24 {
476 self.hours = 0;
477 }
478 }
479 }
480 }
481
482 self.frame_count_cache = Self::compute_frames_from_fields(
484 self.hours,
485 self.minutes,
486 self.seconds,
487 self.frames,
488 self.frame_rate.fps as u64,
489 self.frame_rate.drop_frame,
490 );
491
492 Ok(())
493 }
494
495 pub fn decrement(&mut self) -> Result<(), TimecodeError> {
497 if self.frames > 0 {
498 self.frames -= 1;
499
500 let drop_count = if self.frame_rate.fps >= 60 { 4u8 } else { 2u8 };
502 if self.frame_rate.drop_frame
503 && self.seconds == 0
504 && self.frames < drop_count
505 && !self.minutes.is_multiple_of(10)
506 {
507 self.frames = self.frame_rate.fps - 1;
508 if self.seconds > 0 {
509 self.seconds -= 1;
510 } else {
511 self.seconds = 59;
512 if self.minutes > 0 {
513 self.minutes -= 1;
514 } else {
515 self.minutes = 59;
516 if self.hours > 0 {
517 self.hours -= 1;
518 } else {
519 self.hours = 23;
520 }
521 }
522 }
523 }
524 } else if self.seconds > 0 {
525 self.seconds -= 1;
526 self.frames = self.frame_rate.fps - 1;
527 } else {
528 self.seconds = 59;
529 self.frames = self.frame_rate.fps - 1;
530
531 if self.minutes > 0 {
532 self.minutes -= 1;
533 } else {
534 self.minutes = 59;
535 if self.hours > 0 {
536 self.hours -= 1;
537 } else {
538 self.hours = 23;
539 }
540 }
541 }
542
543 self.frame_count_cache = Self::compute_frames_from_fields(
545 self.hours,
546 self.minutes,
547 self.seconds,
548 self.frames,
549 self.frame_rate.fps as u64,
550 self.frame_rate.drop_frame,
551 );
552
553 Ok(())
554 }
555}
556
557impl std::ops::Add for Timecode {
562 type Output = Result<Timecode, TimecodeError>;
563
564 fn add(self, rhs: Timecode) -> Self::Output {
569 let rate = frame_rate_from_info(&self.frame_rate);
570 let fps = self.frame_rate.fps as u64;
571 let frames_per_day = fps * 86_400;
572
573 let sum = if frames_per_day > 0 {
574 (self.frame_count_cache + rhs.frame_count_cache) % frames_per_day
575 } else {
576 self.frame_count_cache + rhs.frame_count_cache
577 };
578
579 Timecode::from_frames(sum, rate)
580 }
581}
582
583impl std::ops::Sub for Timecode {
584 type Output = Result<Timecode, TimecodeError>;
585
586 fn sub(self, rhs: Timecode) -> Self::Output {
591 let rate = frame_rate_from_info(&self.frame_rate);
592 let fps = self.frame_rate.fps as u64;
593 let frames_per_day = fps * 86_400;
594
595 let result = if frames_per_day > 0 {
596 if self.frame_count_cache >= rhs.frame_count_cache {
597 self.frame_count_cache - rhs.frame_count_cache
598 } else {
599 frames_per_day - (rhs.frame_count_cache - self.frame_count_cache) % frames_per_day
601 }
602 } else {
603 self.frame_count_cache.saturating_sub(rhs.frame_count_cache)
604 };
605
606 Timecode::from_frames(result, rate)
607 }
608}
609
610impl std::ops::Add<u32> for Timecode {
611 type Output = Result<Timecode, TimecodeError>;
612
613 fn add(self, rhs: u32) -> Self::Output {
617 let rate = frame_rate_from_info(&self.frame_rate);
618 let fps = self.frame_rate.fps as u64;
619 let frames_per_day = fps * 86_400;
620
621 let sum = if frames_per_day > 0 {
622 (self.frame_count_cache + rhs as u64) % frames_per_day
623 } else {
624 self.frame_count_cache + rhs as u64
625 };
626
627 Timecode::from_frames(sum, rate)
628 }
629}
630
631impl fmt::Display for Timecode {
636 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
637 let separator = if self.frame_rate.drop_frame { ';' } else { ':' };
638 write!(
639 f,
640 "{:02}:{:02}:{:02}{}{:02}",
641 self.hours, self.minutes, self.seconds, separator, self.frames
642 )
643 }
644}
645
646pub trait TimecodeReader {
652 fn read_timecode(&mut self) -> Result<Option<Timecode>, TimecodeError>;
654
655 fn frame_rate(&self) -> FrameRate;
657
658 fn is_synchronized(&self) -> bool;
660}
661
662pub trait TimecodeWriter {
664 fn write_timecode(&mut self, timecode: &Timecode) -> Result<(), TimecodeError>;
666
667 fn frame_rate(&self) -> FrameRate;
669
670 fn flush(&mut self) -> Result<(), TimecodeError>;
672}
673
674#[derive(Debug, Clone, PartialEq, Eq)]
680pub enum TimecodeError {
681 InvalidHours,
683 InvalidMinutes,
685 InvalidSeconds,
687 InvalidFrames,
689 InvalidDropFrame,
691 SyncNotFound,
693 CrcError,
695 BufferTooSmall,
697 InvalidConfiguration,
699 IoError(String),
701 NotSynchronized,
703}
704
705impl fmt::Display for TimecodeError {
706 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
707 match self {
708 TimecodeError::InvalidHours => write!(f, "Invalid hours value"),
709 TimecodeError::InvalidMinutes => write!(f, "Invalid minutes value"),
710 TimecodeError::InvalidSeconds => write!(f, "Invalid seconds value"),
711 TimecodeError::InvalidFrames => write!(f, "Invalid frames value"),
712 TimecodeError::InvalidDropFrame => write!(f, "Invalid drop frame timecode"),
713 TimecodeError::SyncNotFound => write!(f, "Sync word not found"),
714 TimecodeError::CrcError => write!(f, "CRC error"),
715 TimecodeError::BufferTooSmall => write!(f, "Buffer too small"),
716 TimecodeError::InvalidConfiguration => write!(f, "Invalid configuration"),
717 TimecodeError::IoError(e) => write!(f, "IO error: {}", e),
718 TimecodeError::NotSynchronized => write!(f, "Not synchronized"),
719 }
720 }
721}
722
723impl std::error::Error for TimecodeError {}
724
725#[cfg(test)]
730mod tests {
731 use super::*;
732
733 #[test]
734 fn test_timecode_creation() {
735 let tc = Timecode::new(1, 2, 3, 4, FrameRate::Fps25).expect("valid timecode");
736 assert_eq!(tc.hours, 1);
737 assert_eq!(tc.minutes, 2);
738 assert_eq!(tc.seconds, 3);
739 assert_eq!(tc.frames, 4);
740 }
741
742 #[test]
743 fn test_timecode_display() {
744 let tc = Timecode::new(1, 2, 3, 4, FrameRate::Fps25).expect("valid timecode");
745 assert_eq!(tc.to_string(), "01:02:03:04");
746
747 let tc_df = Timecode::new(1, 2, 3, 4, FrameRate::Fps2997DF).expect("valid timecode");
748 assert_eq!(tc_df.to_string(), "01:02:03;04");
749 }
750
751 #[test]
752 fn test_timecode_increment() {
753 let mut tc = Timecode::new(0, 0, 0, 24, FrameRate::Fps25).expect("valid timecode");
754 tc.increment().expect("increment should succeed");
755 assert_eq!(tc.frames, 0);
756 assert_eq!(tc.seconds, 1);
757 }
758
759 #[test]
760 fn test_frame_rate() {
761 assert_eq!(FrameRate::Fps25.as_float(), 25.0);
762 assert!((FrameRate::Fps2997DF.as_float() - 29.97002997).abs() < 1e-6);
763 assert!(FrameRate::Fps2997DF.is_drop_frame());
764 assert!(!FrameRate::Fps2997NDF.is_drop_frame());
765 }
766
767 #[test]
768 fn test_framerate_47952_and_120() {
769 assert_eq!(FrameRate::Fps47952.frames_per_second(), 48);
770 assert_eq!(FrameRate::Fps47952DF.frames_per_second(), 48);
771 assert_eq!(FrameRate::Fps120.frames_per_second(), 120);
772 assert!(!FrameRate::Fps47952.is_drop_frame());
773 assert!(FrameRate::Fps47952DF.is_drop_frame());
774 assert!(!FrameRate::Fps120.is_drop_frame());
775 assert_eq!(FrameRate::Fps47952.as_rational(), (48000, 1001));
776 assert_eq!(FrameRate::Fps120.as_rational(), (120, 1));
777 }
778
779 #[test]
780 fn test_from_string_ndf() {
781 let tc = Timecode::from_string("01:02:03:04", FrameRate::Fps25).expect("should parse");
782 assert_eq!(tc.hours, 1);
783 assert_eq!(tc.minutes, 2);
784 assert_eq!(tc.seconds, 3);
785 assert_eq!(tc.frames, 4);
786 }
787
788 #[test]
789 fn test_from_string_df() {
790 let tc = Timecode::from_string("01:02:03;04", FrameRate::Fps2997DF).expect("should parse");
792 assert_eq!(tc.frames, 4);
793 assert!(tc.frame_rate.drop_frame);
794 }
795
796 #[test]
797 fn test_from_string_invalid_too_short() {
798 assert!(Timecode::from_string("1:2:3:4", FrameRate::Fps25).is_err());
799 }
800
801 #[test]
802 fn test_from_string_invalid_parts() {
803 assert!(Timecode::from_string("01:02:03", FrameRate::Fps25).is_err());
804 }
805
806 #[test]
807 fn test_to_seconds_f64_one_hour_25fps() {
808 let tc = Timecode::new(1, 0, 0, 0, FrameRate::Fps25).expect("valid");
809 let secs = tc.to_seconds_f64();
810 assert!((secs - 3600.0).abs() < 1e-6);
811 }
812
813 #[test]
814 fn test_to_seconds_f64_pull_down() {
815 let tc = Timecode::new(0, 0, 0, 1, FrameRate::Fps2997NDF).expect("valid");
817 let expected = 1001.0 / 30000.0;
818 assert!((tc.to_seconds_f64() - expected).abs() < 1e-12);
819 }
820
821 #[test]
822 fn test_ord_timecodes() {
823 let tc1 = Timecode::new(0, 0, 0, 0, FrameRate::Fps25).expect("valid");
824 let tc2 = Timecode::new(0, 0, 0, 1, FrameRate::Fps25).expect("valid");
825 let tc3 = Timecode::new(1, 0, 0, 0, FrameRate::Fps25).expect("valid");
826 assert!(tc1 < tc2);
827 assert!(tc2 < tc3);
828 assert!(tc1 < tc3);
829 assert_eq!(tc1, tc1);
830 }
831
832 #[test]
833 fn test_add_timecodes() {
834 let tc1 = Timecode::new(0, 0, 1, 0, FrameRate::Fps25).expect("valid"); let tc2 = Timecode::new(0, 0, 2, 0, FrameRate::Fps25).expect("valid"); let result = (tc1 + tc2).expect("add should succeed");
837 assert_eq!(result.seconds, 3);
838 assert_eq!(result.frames, 0);
839 }
840
841 #[test]
842 fn test_sub_timecodes() {
843 let tc1 = Timecode::new(0, 0, 3, 0, FrameRate::Fps25).expect("valid"); let tc2 = Timecode::new(0, 0, 1, 0, FrameRate::Fps25).expect("valid"); let result = (tc1 - tc2).expect("sub should succeed");
846 assert_eq!(result.seconds, 2);
847 assert_eq!(result.frames, 0);
848 }
849
850 #[test]
851 fn test_add_u32_frames() {
852 let tc = Timecode::new(0, 0, 0, 0, FrameRate::Fps25).expect("valid");
854 let result = (tc + 25_u32).expect("add u32 should succeed");
855 assert_eq!(result.seconds, 1);
856 assert_eq!(result.frames, 0);
857
858 let tc_near_end = Timecode::new(23, 59, 59, 24, FrameRate::Fps25).expect("valid");
860 let wrapped = (tc_near_end + 1_u32).expect("wrap should succeed");
861 assert_eq!(wrapped.hours, 0);
862 assert_eq!(wrapped.minutes, 0);
863 assert_eq!(wrapped.seconds, 0);
864 assert_eq!(wrapped.frames, 0);
865 }
866
867 #[test]
868 fn test_frame_count_cache_matches_recomputed() {
869 let tc = Timecode::new(1, 23, 45, 12, FrameRate::Fps25).expect("valid");
870 let expected: u64 = 1 * 3600 * 25 + 23 * 60 * 25 + 45 * 25 + 12;
871 assert_eq!(tc.to_frames(), expected);
872 }
873
874 #[test]
875 fn test_frame_count_cache_after_increment() {
876 let mut tc = Timecode::new(0, 0, 0, 24, FrameRate::Fps25).expect("valid");
877 let before = tc.to_frames();
878 tc.increment().expect("ok");
879 assert_eq!(tc.to_frames(), before + 1);
880 }
881
882 #[test]
883 fn test_frame_rate_from_info() {
884 let info = FrameRateInfo {
885 fps: 25,
886 drop_frame: false,
887 };
888 assert_eq!(frame_rate_from_info(&info), FrameRate::Fps25);
889
890 let info_df = FrameRateInfo {
891 fps: 30,
892 drop_frame: true,
893 };
894 assert_eq!(frame_rate_from_info(&info_df), FrameRate::Fps2997DF);
895
896 let info_120 = FrameRateInfo {
897 fps: 120,
898 drop_frame: false,
899 };
900 assert_eq!(frame_rate_from_info(&info_120), FrameRate::Fps120);
901 }
902}