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 compare;
23pub mod continuity;
24pub mod drop_frame;
25pub mod duration;
26pub mod embedded_tc;
27pub mod frame_offset;
28pub mod frame_rate;
29pub mod jam_sync;
30pub mod ltc;
31pub mod ltc_encoder;
32pub mod ltc_parser;
33pub mod ltc_simd;
34pub mod midi_timecode;
35pub mod reader;
36pub mod subframe;
37pub mod sync;
38pub mod sync_map;
39pub mod tc_calculator;
40pub mod tc_compare;
41pub mod tc_convert;
42pub mod tc_drift;
43pub mod tc_interpolate;
44pub mod tc_list;
45pub mod tc_math;
46pub mod tc_metadata;
47pub mod tc_offset_table;
48pub mod tc_range;
49pub mod tc_sequence;
50pub mod tc_smpte_ranges;
51pub mod tc_subtitle_sync;
52pub mod tc_validator;
53pub mod timecode_calculator;
54pub mod timecode_display;
55pub mod timecode_event;
56pub mod timecode_format;
57pub mod timecode_generator;
58pub mod timecode_log;
59pub mod timecode_overlay;
60pub mod timecode_range;
61pub mod vitc;
62
63use std::fmt;
64
65#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
67pub enum FrameRate {
68 Fps23976,
70 Fps23976DF,
72 Fps24,
74 Fps25,
76 Fps2997DF,
78 Fps2997NDF,
80 Fps30,
82 Fps47952,
84 Fps47952DF,
86 Fps50,
88 Fps5994,
90 Fps5994DF,
92 Fps60,
94 Fps120,
96}
97
98impl FrameRate {
99 pub fn as_float(&self) -> f64 {
101 match self {
102 FrameRate::Fps23976 | FrameRate::Fps23976DF => 24000.0 / 1001.0,
103 FrameRate::Fps24 => 24.0,
104 FrameRate::Fps25 => 25.0,
105 FrameRate::Fps2997DF | FrameRate::Fps2997NDF => 30000.0 / 1001.0,
106 FrameRate::Fps30 => 30.0,
107 FrameRate::Fps47952 | FrameRate::Fps47952DF => 48000.0 / 1001.0,
108 FrameRate::Fps50 => 50.0,
109 FrameRate::Fps5994 | FrameRate::Fps5994DF => 60000.0 / 1001.0,
110 FrameRate::Fps60 => 60.0,
111 FrameRate::Fps120 => 120.0,
112 }
113 }
114
115 pub fn as_rational(&self) -> (u32, u32) {
117 match self {
118 FrameRate::Fps23976 | FrameRate::Fps23976DF => (24000, 1001),
119 FrameRate::Fps24 => (24, 1),
120 FrameRate::Fps25 => (25, 1),
121 FrameRate::Fps2997DF | FrameRate::Fps2997NDF => (30000, 1001),
122 FrameRate::Fps30 => (30, 1),
123 FrameRate::Fps47952 | FrameRate::Fps47952DF => (48000, 1001),
124 FrameRate::Fps50 => (50, 1),
125 FrameRate::Fps5994 | FrameRate::Fps5994DF => (60000, 1001),
126 FrameRate::Fps60 => (60, 1),
127 FrameRate::Fps120 => (120, 1),
128 }
129 }
130
131 pub fn is_drop_frame(&self) -> bool {
133 matches!(
134 self,
135 FrameRate::Fps2997DF
136 | FrameRate::Fps23976DF
137 | FrameRate::Fps5994DF
138 | FrameRate::Fps47952DF
139 )
140 }
141
142 pub fn drop_frames_per_minute(&self) -> u64 {
149 match self {
150 FrameRate::Fps23976DF => 2,
151 FrameRate::Fps2997DF => 2,
152 FrameRate::Fps47952DF => 4,
153 FrameRate::Fps5994DF => 4,
154 _ => 0,
155 }
156 }
157
158 pub fn frames_per_second(&self) -> u32 {
160 match self {
161 FrameRate::Fps23976 | FrameRate::Fps23976DF => 24,
162 FrameRate::Fps24 => 24,
163 FrameRate::Fps25 => 25,
164 FrameRate::Fps2997DF | FrameRate::Fps2997NDF => 30,
165 FrameRate::Fps30 => 30,
166 FrameRate::Fps47952 | FrameRate::Fps47952DF => 48,
167 FrameRate::Fps50 => 50,
168 FrameRate::Fps5994 | FrameRate::Fps5994DF => 60,
169 FrameRate::Fps60 => 60,
170 FrameRate::Fps120 => 120,
171 }
172 }
173}
174
175#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize)]
177pub struct FrameRateInfo {
178 pub fps: u8,
180 pub drop_frame: bool,
182}
183
184impl PartialEq for FrameRateInfo {
185 fn eq(&self, other: &Self) -> bool {
186 self.fps == other.fps && self.drop_frame == other.drop_frame
187 }
188}
189
190impl Eq for FrameRateInfo {}
191
192pub fn frame_rate_from_info(info: &FrameRateInfo) -> FrameRate {
198 match (info.fps, info.drop_frame) {
199 (24, true) => FrameRate::Fps23976DF,
200 (24, false) => FrameRate::Fps23976, (25, _) => FrameRate::Fps25,
202 (30, true) => FrameRate::Fps2997DF,
203 (30, false) => FrameRate::Fps2997NDF,
204 (48, true) => FrameRate::Fps47952DF,
205 (48, false) => FrameRate::Fps47952,
206 (50, _) => FrameRate::Fps50,
207 (60, true) => FrameRate::Fps5994DF,
208 (60, false) => FrameRate::Fps5994,
209 (120, _) => FrameRate::Fps120,
210 _ => FrameRate::Fps25, }
212}
213
214#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize)]
221pub struct Timecode {
222 pub hours: u8,
224 pub minutes: u8,
226 pub seconds: u8,
228 pub frames: u8,
230 pub frame_rate: FrameRateInfo,
232 pub user_bits: u32,
234 #[serde(skip)]
236 frame_count_cache: u64,
237}
238
239impl PartialEq for Timecode {
240 fn eq(&self, other: &Self) -> bool {
241 self.hours == other.hours
242 && self.minutes == other.minutes
243 && self.seconds == other.seconds
244 && self.frames == other.frames
245 && self.frame_rate == other.frame_rate
246 && self.user_bits == other.user_bits
247 }
248}
249
250impl Eq for Timecode {}
251
252impl PartialOrd for Timecode {
253 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
254 Some(self.cmp(other))
255 }
256}
257
258impl Ord for Timecode {
259 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
260 self.to_frames().cmp(&other.to_frames())
261 }
262}
263
264impl Timecode {
265 fn compute_frames_from_fields(
268 hours: u8,
269 minutes: u8,
270 seconds: u8,
271 frames: u8,
272 fps: u64,
273 drop_frame: bool,
274 ) -> u64 {
275 let mut total = hours as u64 * 3600 * fps;
276 total += minutes as u64 * 60 * fps;
277 total += seconds as u64 * fps;
278 total += frames as u64;
279
280 if drop_frame {
281 let drop_per_min = if fps >= 60 { 4u64 } else { 2u64 };
282 let total_minutes = hours as u64 * 60 + minutes as u64;
283 let dropped_frames = drop_per_min * (total_minutes - total_minutes / 10);
284 total -= dropped_frames;
285 }
286
287 total
288 }
289
290 pub fn new(
292 hours: u8,
293 minutes: u8,
294 seconds: u8,
295 frames: u8,
296 frame_rate: FrameRate,
297 ) -> Result<Self, TimecodeError> {
298 let fps = frame_rate.frames_per_second() as u8;
299
300 if hours > 23 {
301 return Err(TimecodeError::InvalidHours);
302 }
303 if minutes > 59 {
304 return Err(TimecodeError::InvalidMinutes);
305 }
306 if seconds > 59 {
307 return Err(TimecodeError::InvalidSeconds);
308 }
309 if frames >= fps {
310 return Err(TimecodeError::InvalidFrames);
311 }
312
313 if frame_rate.is_drop_frame() {
315 let drop_count = frame_rate.drop_frames_per_minute() as u8;
316 if seconds == 0 && frames < drop_count && !minutes.is_multiple_of(10) {
319 return Err(TimecodeError::InvalidDropFrame);
320 }
321 }
322
323 let drop_frame = frame_rate.is_drop_frame();
324 let frame_count_cache = Self::compute_frames_from_fields(
325 hours, minutes, seconds, frames, fps as u64, drop_frame,
326 );
327
328 Ok(Timecode {
329 hours,
330 minutes,
331 seconds,
332 frames,
333 frame_rate: FrameRateInfo { fps, drop_frame },
334 user_bits: 0,
335 frame_count_cache,
336 })
337 }
338
339 pub fn from_string(s: &str, frame_rate: FrameRate) -> Result<Self, TimecodeError> {
352 let s = s.trim();
353 if s.len() < 11 {
355 return Err(TimecodeError::InvalidConfiguration);
356 }
357
358 let parts: Vec<&str> = s.split([':', ';']).collect();
360 if parts.len() != 4 {
361 return Err(TimecodeError::InvalidConfiguration);
362 }
363
364 let hours: u8 = parts[0].parse().map_err(|_| TimecodeError::InvalidHours)?;
365 let minutes: u8 = parts[1]
366 .parse()
367 .map_err(|_| TimecodeError::InvalidMinutes)?;
368 let seconds: u8 = parts[2]
369 .parse()
370 .map_err(|_| TimecodeError::InvalidSeconds)?;
371 let frames: u8 = parts[3].parse().map_err(|_| TimecodeError::InvalidFrames)?;
372
373 Self::new(hours, minutes, seconds, frames, frame_rate)
374 }
375
376 pub fn from_raw_fields(
382 hours: u8,
383 minutes: u8,
384 seconds: u8,
385 frames: u8,
386 fps: u8,
387 drop_frame: bool,
388 user_bits: u32,
389 ) -> Self {
390 let frame_count_cache = Self::compute_frames_from_fields(
391 hours, minutes, seconds, frames, fps as u64, drop_frame,
392 );
393 Self {
394 hours,
395 minutes,
396 seconds,
397 frames,
398 frame_rate: FrameRateInfo { fps, drop_frame },
399 user_bits,
400 frame_count_cache,
401 }
402 }
403
404 pub fn with_user_bits(mut self, user_bits: u32) -> Self {
406 self.user_bits = user_bits;
407 self
408 }
409
410 #[inline]
414 pub fn to_frames(&self) -> u64 {
415 self.frame_count_cache
416 }
417
418 #[allow(clippy::cast_precision_loss)]
423 pub fn to_seconds_f64(&self) -> f64 {
424 let rate = frame_rate_from_info(&self.frame_rate);
425 let (num, den) = rate.as_rational();
426 self.frame_count_cache as f64 * den as f64 / num as f64
428 }
429
430 pub fn from_frames(frames: u64, frame_rate: FrameRate) -> Result<Self, TimecodeError> {
432 let fps = frame_rate.frames_per_second() as u64;
433 let mut remaining = frames;
434
435 if frame_rate.is_drop_frame() {
437 let drop_per_min = frame_rate.drop_frames_per_minute();
438 let frames_per_minute = fps * 60 - drop_per_min;
439 let frames_per_10_minutes = frames_per_minute * 9 + fps * 60;
440
441 let ten_minute_blocks = remaining / frames_per_10_minutes;
442 remaining += ten_minute_blocks * (drop_per_min * 9);
443
444 let remaining_in_block = remaining % frames_per_10_minutes;
445 if remaining_in_block >= fps * 60 {
446 let extra_minutes = (remaining_in_block - fps * 60) / frames_per_minute;
447 remaining += (extra_minutes + 1) * drop_per_min;
448 }
449 }
450
451 let hours = (remaining / (fps * 3600)) as u8;
452 remaining %= fps * 3600;
453 let minutes = (remaining / (fps * 60)) as u8;
454 remaining %= fps * 60;
455 let seconds = (remaining / fps) as u8;
456 let frame = (remaining % fps) as u8;
457
458 Self::new(hours, minutes, seconds, frame, frame_rate)
459 }
460
461 pub fn increment(&mut self) -> Result<(), TimecodeError> {
463 self.frames += 1;
464
465 if self.frames >= self.frame_rate.fps {
466 self.frames = 0;
467 self.seconds += 1;
468
469 if self.seconds >= 60 {
470 self.seconds = 0;
471 self.minutes += 1;
472
473 if self.frame_rate.drop_frame && !self.minutes.is_multiple_of(10) {
475 let drop_count = if self.frame_rate.fps >= 60 { 4u8 } else { 2u8 };
476 self.frames = drop_count;
477 }
478
479 if self.minutes >= 60 {
480 self.minutes = 0;
481 self.hours += 1;
482
483 if self.hours >= 24 {
484 self.hours = 0;
485 }
486 }
487 }
488 }
489
490 self.frame_count_cache = Self::compute_frames_from_fields(
492 self.hours,
493 self.minutes,
494 self.seconds,
495 self.frames,
496 self.frame_rate.fps as u64,
497 self.frame_rate.drop_frame,
498 );
499
500 Ok(())
501 }
502
503 pub fn decrement(&mut self) -> Result<(), TimecodeError> {
505 if self.frames > 0 {
506 self.frames -= 1;
507
508 let drop_count = if self.frame_rate.fps >= 60 { 4u8 } else { 2u8 };
510 if self.frame_rate.drop_frame
511 && self.seconds == 0
512 && self.frames < drop_count
513 && !self.minutes.is_multiple_of(10)
514 {
515 self.frames = self.frame_rate.fps - 1;
516 if self.seconds > 0 {
517 self.seconds -= 1;
518 } else {
519 self.seconds = 59;
520 if self.minutes > 0 {
521 self.minutes -= 1;
522 } else {
523 self.minutes = 59;
524 if self.hours > 0 {
525 self.hours -= 1;
526 } else {
527 self.hours = 23;
528 }
529 }
530 }
531 }
532 } else if self.seconds > 0 {
533 self.seconds -= 1;
534 self.frames = self.frame_rate.fps - 1;
535 } else {
536 self.seconds = 59;
537 self.frames = self.frame_rate.fps - 1;
538
539 if self.minutes > 0 {
540 self.minutes -= 1;
541 } else {
542 self.minutes = 59;
543 if self.hours > 0 {
544 self.hours -= 1;
545 } else {
546 self.hours = 23;
547 }
548 }
549 }
550
551 self.frame_count_cache = Self::compute_frames_from_fields(
553 self.hours,
554 self.minutes,
555 self.seconds,
556 self.frames,
557 self.frame_rate.fps as u64,
558 self.frame_rate.drop_frame,
559 );
560
561 Ok(())
562 }
563}
564
565impl std::ops::Add for Timecode {
570 type Output = Result<Timecode, TimecodeError>;
571
572 fn add(self, rhs: Timecode) -> Self::Output {
577 let rate = frame_rate_from_info(&self.frame_rate);
578 let fps = self.frame_rate.fps as u64;
579 let frames_per_day = fps * 86_400;
580
581 let sum = if frames_per_day > 0 {
582 (self.frame_count_cache + rhs.frame_count_cache) % frames_per_day
583 } else {
584 self.frame_count_cache + rhs.frame_count_cache
585 };
586
587 Timecode::from_frames(sum, rate)
588 }
589}
590
591impl std::ops::Sub for Timecode {
592 type Output = Result<Timecode, TimecodeError>;
593
594 fn sub(self, rhs: Timecode) -> Self::Output {
599 let rate = frame_rate_from_info(&self.frame_rate);
600 let fps = self.frame_rate.fps as u64;
601 let frames_per_day = fps * 86_400;
602
603 let result = if frames_per_day > 0 {
604 if self.frame_count_cache >= rhs.frame_count_cache {
605 self.frame_count_cache - rhs.frame_count_cache
606 } else {
607 frames_per_day - (rhs.frame_count_cache - self.frame_count_cache) % frames_per_day
609 }
610 } else {
611 self.frame_count_cache.saturating_sub(rhs.frame_count_cache)
612 };
613
614 Timecode::from_frames(result, rate)
615 }
616}
617
618impl std::ops::Add<u32> for Timecode {
619 type Output = Result<Timecode, TimecodeError>;
620
621 fn add(self, rhs: u32) -> Self::Output {
625 let rate = frame_rate_from_info(&self.frame_rate);
626 let fps = self.frame_rate.fps as u64;
627 let frames_per_day = fps * 86_400;
628
629 let sum = if frames_per_day > 0 {
630 (self.frame_count_cache + rhs as u64) % frames_per_day
631 } else {
632 self.frame_count_cache + rhs as u64
633 };
634
635 Timecode::from_frames(sum, rate)
636 }
637}
638
639impl fmt::Display for Timecode {
644 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
645 let separator = if self.frame_rate.drop_frame { ';' } else { ':' };
646 write!(
647 f,
648 "{:02}:{:02}:{:02}{}{:02}",
649 self.hours, self.minutes, self.seconds, separator, self.frames
650 )
651 }
652}
653
654pub trait TimecodeReader {
660 fn read_timecode(&mut self) -> Result<Option<Timecode>, TimecodeError>;
662
663 fn frame_rate(&self) -> FrameRate;
665
666 fn is_synchronized(&self) -> bool;
668}
669
670pub trait TimecodeWriter {
672 fn write_timecode(&mut self, timecode: &Timecode) -> Result<(), TimecodeError>;
674
675 fn frame_rate(&self) -> FrameRate;
677
678 fn flush(&mut self) -> Result<(), TimecodeError>;
680}
681
682#[derive(Debug, Clone, PartialEq, Eq)]
688pub enum TimecodeError {
689 InvalidHours,
691 InvalidMinutes,
693 InvalidSeconds,
695 InvalidFrames,
697 InvalidDropFrame,
699 SyncNotFound,
701 CrcError,
703 BufferTooSmall,
705 InvalidConfiguration,
707 IoError(String),
709 NotSynchronized,
711}
712
713impl fmt::Display for TimecodeError {
714 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
715 match self {
716 TimecodeError::InvalidHours => write!(f, "Invalid hours value"),
717 TimecodeError::InvalidMinutes => write!(f, "Invalid minutes value"),
718 TimecodeError::InvalidSeconds => write!(f, "Invalid seconds value"),
719 TimecodeError::InvalidFrames => write!(f, "Invalid frames value"),
720 TimecodeError::InvalidDropFrame => write!(f, "Invalid drop frame timecode"),
721 TimecodeError::SyncNotFound => write!(f, "Sync word not found"),
722 TimecodeError::CrcError => write!(f, "CRC error"),
723 TimecodeError::BufferTooSmall => write!(f, "Buffer too small"),
724 TimecodeError::InvalidConfiguration => write!(f, "Invalid configuration"),
725 TimecodeError::IoError(e) => write!(f, "IO error: {}", e),
726 TimecodeError::NotSynchronized => write!(f, "Not synchronized"),
727 }
728 }
729}
730
731impl std::error::Error for TimecodeError {}
732
733#[cfg(test)]
738mod tests {
739 use super::*;
740
741 #[test]
742 fn test_timecode_creation() {
743 let tc = Timecode::new(1, 2, 3, 4, FrameRate::Fps25).expect("valid timecode");
744 assert_eq!(tc.hours, 1);
745 assert_eq!(tc.minutes, 2);
746 assert_eq!(tc.seconds, 3);
747 assert_eq!(tc.frames, 4);
748 }
749
750 #[test]
751 fn test_timecode_display() {
752 let tc = Timecode::new(1, 2, 3, 4, FrameRate::Fps25).expect("valid timecode");
753 assert_eq!(tc.to_string(), "01:02:03:04");
754
755 let tc_df = Timecode::new(1, 2, 3, 4, FrameRate::Fps2997DF).expect("valid timecode");
756 assert_eq!(tc_df.to_string(), "01:02:03;04");
757 }
758
759 #[test]
760 fn test_timecode_increment() {
761 let mut tc = Timecode::new(0, 0, 0, 24, FrameRate::Fps25).expect("valid timecode");
762 tc.increment().expect("increment should succeed");
763 assert_eq!(tc.frames, 0);
764 assert_eq!(tc.seconds, 1);
765 }
766
767 #[test]
768 fn test_frame_rate() {
769 assert_eq!(FrameRate::Fps25.as_float(), 25.0);
770 assert!((FrameRate::Fps2997DF.as_float() - 29.97002997).abs() < 1e-6);
771 assert!(FrameRate::Fps2997DF.is_drop_frame());
772 assert!(!FrameRate::Fps2997NDF.is_drop_frame());
773 }
774
775 #[test]
776 fn test_framerate_47952_and_120() {
777 assert_eq!(FrameRate::Fps47952.frames_per_second(), 48);
778 assert_eq!(FrameRate::Fps47952DF.frames_per_second(), 48);
779 assert_eq!(FrameRate::Fps120.frames_per_second(), 120);
780 assert!(!FrameRate::Fps47952.is_drop_frame());
781 assert!(FrameRate::Fps47952DF.is_drop_frame());
782 assert!(!FrameRate::Fps120.is_drop_frame());
783 assert_eq!(FrameRate::Fps47952.as_rational(), (48000, 1001));
784 assert_eq!(FrameRate::Fps120.as_rational(), (120, 1));
785 }
786
787 #[test]
788 fn test_from_string_ndf() {
789 let tc = Timecode::from_string("01:02:03:04", FrameRate::Fps25).expect("should parse");
790 assert_eq!(tc.hours, 1);
791 assert_eq!(tc.minutes, 2);
792 assert_eq!(tc.seconds, 3);
793 assert_eq!(tc.frames, 4);
794 }
795
796 #[test]
797 fn test_from_string_df() {
798 let tc = Timecode::from_string("01:02:03;04", FrameRate::Fps2997DF).expect("should parse");
800 assert_eq!(tc.frames, 4);
801 assert!(tc.frame_rate.drop_frame);
802 }
803
804 #[test]
805 fn test_from_string_invalid_too_short() {
806 assert!(Timecode::from_string("1:2:3:4", FrameRate::Fps25).is_err());
807 }
808
809 #[test]
810 fn test_from_string_invalid_parts() {
811 assert!(Timecode::from_string("01:02:03", FrameRate::Fps25).is_err());
812 }
813
814 #[test]
815 fn test_to_seconds_f64_one_hour_25fps() {
816 let tc = Timecode::new(1, 0, 0, 0, FrameRate::Fps25).expect("valid");
817 let secs = tc.to_seconds_f64();
818 assert!((secs - 3600.0).abs() < 1e-6);
819 }
820
821 #[test]
822 fn test_to_seconds_f64_pull_down() {
823 let tc = Timecode::new(0, 0, 0, 1, FrameRate::Fps2997NDF).expect("valid");
825 let expected = 1001.0 / 30000.0;
826 assert!((tc.to_seconds_f64() - expected).abs() < 1e-12);
827 }
828
829 #[test]
830 fn test_ord_timecodes() {
831 let tc1 = Timecode::new(0, 0, 0, 0, FrameRate::Fps25).expect("valid");
832 let tc2 = Timecode::new(0, 0, 0, 1, FrameRate::Fps25).expect("valid");
833 let tc3 = Timecode::new(1, 0, 0, 0, FrameRate::Fps25).expect("valid");
834 assert!(tc1 < tc2);
835 assert!(tc2 < tc3);
836 assert!(tc1 < tc3);
837 assert_eq!(tc1, tc1);
838 }
839
840 #[test]
841 fn test_add_timecodes() {
842 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");
845 assert_eq!(result.seconds, 3);
846 assert_eq!(result.frames, 0);
847 }
848
849 #[test]
850 fn test_sub_timecodes() {
851 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");
854 assert_eq!(result.seconds, 2);
855 assert_eq!(result.frames, 0);
856 }
857
858 #[test]
859 fn test_add_u32_frames() {
860 let tc = Timecode::new(0, 0, 0, 0, FrameRate::Fps25).expect("valid");
862 let result = (tc + 25_u32).expect("add u32 should succeed");
863 assert_eq!(result.seconds, 1);
864 assert_eq!(result.frames, 0);
865
866 let tc_near_end = Timecode::new(23, 59, 59, 24, FrameRate::Fps25).expect("valid");
868 let wrapped = (tc_near_end + 1_u32).expect("wrap should succeed");
869 assert_eq!(wrapped.hours, 0);
870 assert_eq!(wrapped.minutes, 0);
871 assert_eq!(wrapped.seconds, 0);
872 assert_eq!(wrapped.frames, 0);
873 }
874
875 #[test]
876 fn test_frame_count_cache_matches_recomputed() {
877 let tc = Timecode::new(1, 23, 45, 12, FrameRate::Fps25).expect("valid");
878 let expected: u64 = 1 * 3600 * 25 + 23 * 60 * 25 + 45 * 25 + 12;
879 assert_eq!(tc.to_frames(), expected);
880 }
881
882 #[test]
883 fn test_frame_count_cache_after_increment() {
884 let mut tc = Timecode::new(0, 0, 0, 24, FrameRate::Fps25).expect("valid");
885 let before = tc.to_frames();
886 tc.increment().expect("ok");
887 assert_eq!(tc.to_frames(), before + 1);
888 }
889
890 #[test]
891 fn test_frame_rate_from_info() {
892 let info = FrameRateInfo {
893 fps: 25,
894 drop_frame: false,
895 };
896 assert_eq!(frame_rate_from_info(&info), FrameRate::Fps25);
897
898 let info_df = FrameRateInfo {
899 fps: 30,
900 drop_frame: true,
901 };
902 assert_eq!(frame_rate_from_info(&info_df), FrameRate::Fps2997DF);
903
904 let info_120 = FrameRateInfo {
905 fps: 120,
906 drop_frame: false,
907 };
908 assert_eq!(frame_rate_from_info(&info_120), FrameRate::Fps120);
909 }
910}