1use std::sync::Arc;
8use std::sync::atomic::{AtomicU64, Ordering};
9use std::time::{Duration, Instant};
10
11enum ClockState {
24 Stopped,
25 Running { started_at: Instant, base: Duration },
26 Paused { frozen_at: Duration },
27}
28
29pub struct PlaybackClock {
54 state: ClockState,
55 rate: f64,
57 seek_offset: Duration,
60}
61
62impl PlaybackClock {
63 #[must_use]
65 pub fn new() -> Self {
66 Self {
67 state: ClockState::Stopped,
68 rate: 1.0,
69 seek_offset: Duration::ZERO,
70 }
71 }
72
73 pub fn start(&mut self) {
81 let base = match &self.state {
82 ClockState::Running { .. } => return,
83 ClockState::Stopped => self.seek_offset,
84 ClockState::Paused { frozen_at } => *frozen_at,
85 };
86 self.state = ClockState::Running {
87 started_at: Instant::now(),
88 base,
89 };
90 }
91
92 pub fn stop(&mut self) {
97 self.state = ClockState::Stopped;
98 self.seek_offset = Duration::ZERO;
99 }
100
101 pub fn pause(&mut self) {
106 if let ClockState::Running { started_at, base } = &self.state {
107 let elapsed = started_at.elapsed().mul_f64(self.rate);
108 self.state = ClockState::Paused {
109 frozen_at: *base + elapsed,
110 };
111 }
112 }
113
114 pub fn resume(&mut self) {
116 if let ClockState::Paused { frozen_at } = self.state {
117 self.state = ClockState::Running {
118 started_at: Instant::now(),
119 base: frozen_at,
120 };
121 }
122 }
123
124 #[must_use]
129 pub fn current_time(&self) -> Duration {
130 match &self.state {
131 ClockState::Stopped => Duration::ZERO,
132 ClockState::Paused { frozen_at } => *frozen_at,
133 ClockState::Running { started_at, base } => {
134 *base + started_at.elapsed().mul_f64(self.rate)
135 }
136 }
137 }
138
139 #[must_use]
145 pub fn current_pts(&self) -> Duration {
146 match &self.state {
147 ClockState::Stopped => self.seek_offset,
148 _ => self.current_time(),
149 }
150 }
151
152 #[must_use]
154 pub fn is_running(&self) -> bool {
155 matches!(self.state, ClockState::Running { .. })
156 }
157
158 pub fn set_rate(&mut self, rate: f64) {
163 if rate <= 0.0 {
164 return;
165 }
166 if let ClockState::Running { started_at, base } = &mut self.state {
167 let elapsed = started_at.elapsed().mul_f64(self.rate);
169 *base += elapsed;
170 *started_at = Instant::now();
171 }
172 self.rate = rate;
173 }
174
175 #[must_use]
177 pub fn rate(&self) -> f64 {
178 self.rate
179 }
180
181 pub fn set_position(&mut self, pts: Duration) {
191 self.seek_offset = pts;
193 if matches!(self.state, ClockState::Running { .. }) {
194 self.state = ClockState::Running {
196 started_at: Instant::now(),
197 base: pts,
198 };
199 } else if matches!(self.state, ClockState::Paused { .. }) {
200 self.state = ClockState::Paused { frozen_at: pts };
201 }
202 }
204}
205
206impl Default for PlaybackClock {
207 fn default() -> Self {
208 Self::new()
209 }
210}
211
212pub(crate) enum MasterClock {
219 Audio {
220 samples_consumed: Arc<AtomicU64>,
221 sample_rate: u32,
222 rate: f64,
224 samples_base: u64,
226 pts_base: Duration,
228 fallback: Option<(Instant, Duration)>,
237 },
238 System {
239 started_at: Instant,
240 base_pts: Duration,
241 rate: f64,
243 },
244}
245
246impl MasterClock {
247 #[allow(clippy::cast_precision_loss)]
256 pub(crate) fn current_pts(&self) -> Duration {
257 match self {
258 Self::Audio {
259 samples_consumed,
260 sample_rate,
261 rate,
262 samples_base,
263 pts_base,
264 fallback,
265 } => {
266 let s = samples_consumed.load(Ordering::Relaxed);
267 let delta = s.saturating_sub(*samples_base);
268 let sample_pts = if delta > 0 || !pts_base.is_zero() {
269 Some(
270 *pts_base
271 + Duration::from_secs_f64(
272 delta as f64 / f64::from(*sample_rate) * *rate,
273 ),
274 )
275 } else {
276 None };
278 let fallback_pts = fallback
279 .as_ref()
280 .map(|(started_at, base_pts)| *base_pts + started_at.elapsed().mul_f64(*rate));
281 match (sample_pts, fallback_pts) {
282 (Some(sp), Some(fp)) => sp.max(fp),
287 (Some(sp), None) => sp,
288 (None, Some(fp)) => fp,
289 (None, None) => Duration::ZERO,
290 }
291 }
292 Self::System {
293 started_at,
294 base_pts,
295 rate,
296 } => *base_pts + started_at.elapsed().mul_f64(*rate),
297 }
298 }
299
300 pub(crate) fn should_sync(&self) -> bool {
311 match self {
312 Self::System { .. } => true,
313 Self::Audio {
314 samples_consumed,
315 samples_base,
316 pts_base,
317 fallback,
318 ..
319 } => {
320 let s = samples_consumed.load(Ordering::Relaxed);
321 s > *samples_base || !pts_base.is_zero() || fallback.is_some()
322 }
323 }
324 }
325
326 pub(crate) fn activate_fallback_if_no_audio(&mut self, base_pts: Duration) {
340 if let Self::Audio {
341 samples_consumed,
342 samples_base,
343 fallback,
344 ..
345 } = self
346 && samples_consumed.load(Ordering::Relaxed) == *samples_base
347 && fallback.is_none()
348 {
349 *fallback = Some((Instant::now(), base_pts));
350 }
351 }
352
353 pub(crate) fn rearm_fallback_at(&mut self, base_pts: Duration) {
365 if let Self::Audio { fallback, .. } = self {
366 *fallback = Some((Instant::now(), base_pts));
367 }
368 }
369
370 #[allow(clippy::cast_precision_loss)]
375 pub(crate) fn set_rate(&mut self, new_rate: f64) {
376 if new_rate <= 0.0 {
377 return;
378 }
379 match self {
380 Self::Audio {
381 samples_consumed,
382 sample_rate,
383 rate,
384 pts_base,
385 samples_base,
386 fallback,
387 } => {
388 let s = samples_consumed.load(Ordering::Relaxed);
390 let delta = s.saturating_sub(*samples_base);
391 let current = *pts_base
392 + Duration::from_secs_f64(delta as f64 / f64::from(*sample_rate) * *rate);
393 *samples_base = s;
394 *pts_base = current;
395 *rate = new_rate;
396 if let Some((started_at, base)) = fallback.as_mut() {
397 *base = current;
398 *started_at = Instant::now();
399 }
400 }
401 Self::System {
402 started_at,
403 base_pts,
404 rate,
405 } => {
406 let current = *base_pts + started_at.elapsed().mul_f64(*rate);
407 *base_pts = current;
408 *started_at = Instant::now();
409 *rate = new_rate;
410 }
411 }
412 }
413
414 pub(crate) fn audio_samples_snapshot(&self) -> u64 {
420 if let Self::Audio {
421 samples_consumed, ..
422 } = self
423 {
424 samples_consumed.load(Ordering::Relaxed)
425 } else {
426 0
427 }
428 }
429
430 pub(crate) fn reset(&mut self, base: Duration) {
441 match self {
442 Self::System {
443 started_at,
444 base_pts,
445 ..
446 } => {
447 *started_at = Instant::now();
448 *base_pts = base;
449 }
450 Self::Audio {
451 samples_consumed,
452 samples_base,
453 pts_base,
454 fallback,
455 ..
456 } => {
457 let s = samples_consumed.load(Ordering::Relaxed);
458 *samples_base = s;
459 *pts_base = base;
460 if fallback.is_some() {
461 *fallback = Some((Instant::now(), base));
462 }
463 }
464 }
465 }
466}
467
468#[cfg(test)]
471mod tests {
472 use super::*;
473 use std::thread;
474
475 #[test]
476 fn clock_stopped_should_return_zero() {
477 let clock = PlaybackClock::new();
479 assert_eq!(clock.current_time(), Duration::ZERO);
480
481 let mut clock = PlaybackClock::new();
483 clock.start();
484 thread::sleep(Duration::from_millis(5));
485 clock.stop();
486 assert_eq!(
487 clock.current_time(),
488 Duration::ZERO,
489 "current_time() must be ZERO after stop()"
490 );
491 }
492
493 #[test]
494 fn clock_paused_should_freeze_at_pause_time() {
495 let mut clock = PlaybackClock::new();
496 clock.start();
497 thread::sleep(Duration::from_millis(10));
498 clock.pause();
499
500 let t1 = clock.current_time();
501 thread::sleep(Duration::from_millis(10));
502 let t2 = clock.current_time();
503
504 assert_eq!(t1, t2, "current_time() must not advance while paused");
505 assert!(
506 !clock.is_running(),
507 "clock must not report running while paused"
508 );
509 }
510
511 #[test]
512 fn clock_resumed_should_continue_from_pause() {
513 let mut clock = PlaybackClock::new();
514 clock.start();
515 thread::sleep(Duration::from_millis(10));
516 clock.pause();
517 let t_paused = clock.current_time();
518
519 thread::sleep(Duration::from_millis(10));
521 assert_eq!(clock.current_time(), t_paused);
522
523 clock.resume();
524 assert!(clock.is_running());
525 thread::sleep(Duration::from_millis(10));
526
527 let t_after = clock.current_time();
528 assert!(
529 t_after > t_paused,
530 "current_time() must advance after resume(); paused={t_paused:?} after={t_after:?}"
531 );
532 }
533
534 #[test]
535 fn clock_start_should_be_noop_when_already_running() {
536 let mut clock = PlaybackClock::new();
537 clock.start();
538 thread::sleep(Duration::from_millis(10));
539 let t_before = clock.current_time();
540
541 clock.start();
543 let t_after = clock.current_time();
544
545 assert!(
546 t_after >= t_before,
547 "second start() must not reset the clock; before={t_before:?} after={t_after:?}"
548 );
549 }
550
551 #[test]
552 fn clock_resume_should_be_noop_when_not_paused() {
553 let mut clock = PlaybackClock::new();
555 clock.resume();
556 assert!(!clock.is_running());
557 assert_eq!(clock.current_time(), Duration::ZERO);
558
559 clock.start();
561 thread::sleep(Duration::from_millis(5));
562 let t = clock.current_time();
563 clock.resume(); assert!(clock.is_running());
565 assert!(clock.current_time() >= t);
566 }
567
568 #[test]
569 fn clock_default_should_equal_new() {
570 let a = PlaybackClock::new();
571 let b = PlaybackClock::default();
572 assert_eq!(a.current_time(), b.current_time());
573 assert_eq!(a.is_running(), b.is_running());
574 }
575
576 #[test]
577 fn set_rate_should_reject_non_positive_values() {
578 let mut clock = PlaybackClock::new();
579
580 clock.set_rate(0.0);
581 assert!(
582 (clock.rate() - 1.0).abs() < f64::EPSILON,
583 "rate must remain 1.0 after set_rate(0.0)"
584 );
585
586 clock.set_rate(-1.0);
587 assert!(
588 (clock.rate() - 1.0).abs() < f64::EPSILON,
589 "rate must remain 1.0 after set_rate(-1.0)"
590 );
591 }
592
593 #[test]
594 fn set_rate_should_update_rate_when_stopped_or_paused() {
595 let mut clock = PlaybackClock::new();
597 clock.set_rate(0.5);
598 assert!((clock.rate() - 0.5).abs() < f64::EPSILON);
599
600 let mut clock = PlaybackClock::new();
602 clock.start();
603 clock.pause();
604 clock.set_rate(2.0);
605 assert!((clock.rate() - 2.0).abs() < f64::EPSILON);
606 assert!(
607 !clock.is_running(),
608 "clock must remain paused after set_rate"
609 );
610 }
611
612 #[test]
613 fn set_rate_running_should_not_jump_current_time() {
614 let mut clock = PlaybackClock::new();
615 clock.start();
616 thread::sleep(Duration::from_millis(10));
617 let before = clock.current_time();
618 clock.set_rate(2.0);
619 let after = clock.current_time();
620
621 assert!(
624 after >= before,
625 "current_time() must not go backward on set_rate; before={before:?} after={after:?}"
626 );
627 assert!(
628 after - before < Duration::from_millis(20),
629 "current_time() must not jump forward on set_rate; before={before:?} after={after:?}"
630 );
631 assert!((clock.rate() - 2.0).abs() < f64::EPSILON);
632 }
633
634 #[test]
635 #[ignore = "performance thresholds are environment-dependent; run explicitly with -- --include-ignored"]
636 fn rate_two_x_should_advance_at_double_speed() {
637 let mut clock = PlaybackClock::new();
638 clock.set_rate(2.0);
639 clock.start();
640 thread::sleep(Duration::from_millis(50));
641 let elapsed = clock.current_time();
642
643 assert!(
645 elapsed >= Duration::from_millis(80),
646 "2× rate: expected ≥80 ms after 50 ms wall time, got {elapsed:?}"
647 );
648 }
649
650 #[test]
651 fn set_position_should_shift_pts_by_seek_offset() {
652 let seek_target = Duration::from_secs(30);
653
654 let mut clock = PlaybackClock::new();
656 clock.set_position(seek_target);
657 assert_eq!(
658 clock.current_pts(),
659 seek_target,
660 "current_pts() must reflect seek_offset when stopped"
661 );
662
663 clock.start();
665 let pts = clock.current_pts();
666 assert!(
667 pts >= seek_target,
668 "current_pts() must be ≥ seek target after start(); target={seek_target:?} pts={pts:?}"
669 );
670 assert!(
671 clock.is_running(),
672 "clock must be running after set_position + start()"
673 );
674 }
675
676 #[test]
677 fn set_position_while_paused_should_update_frozen_time() {
678 let mut clock = PlaybackClock::new();
679 clock.start();
680 thread::sleep(Duration::from_millis(5));
681 clock.pause();
682
683 let seek_target = Duration::from_secs(10);
684 clock.set_position(seek_target);
685
686 let pts = clock.current_pts();
687 assert_eq!(
688 pts, seek_target,
689 "frozen time must update to seek target; expected={seek_target:?} got={pts:?}"
690 );
691 assert!(
692 !clock.is_running(),
693 "clock must remain paused after set_position"
694 );
695
696 clock.resume();
698 thread::sleep(Duration::from_millis(5));
699 let pts_after = clock.current_pts();
700 assert!(
701 pts_after > seek_target,
702 "current_pts() must advance past seek target after resume(); target={seek_target:?} after={pts_after:?}"
703 );
704 }
705
706 #[test]
707 fn set_position_while_running_should_continue_from_new_position() {
708 let mut clock = PlaybackClock::new();
709 clock.start();
710 thread::sleep(Duration::from_millis(5));
711
712 let seek_target = Duration::from_secs(60);
713 clock.set_position(seek_target);
714
715 let pts = clock.current_pts();
716 assert!(
717 pts >= seek_target,
718 "current_pts() must be ≥ seek target immediately after set_position while running; \
719 target={seek_target:?} pts={pts:?}"
720 );
721 assert!(
722 clock.is_running(),
723 "clock must remain running after set_position"
724 );
725 }
726
727 #[test]
728 fn stop_should_clear_seek_offset() {
729 let mut clock = PlaybackClock::new();
730 clock.set_position(Duration::from_secs(30));
731 clock.stop();
732
733 assert_eq!(
734 clock.current_pts(),
735 Duration::ZERO,
736 "stop() must reset seek_offset to ZERO"
737 );
738 }
739
740 #[test]
743 fn master_clock_system_should_advance_from_base_pts() {
744 let clock = MasterClock::System {
745 started_at: Instant::now(),
746 base_pts: Duration::from_secs(5),
747 rate: 1.0,
748 };
749 let pts = clock.current_pts();
750 assert!(
751 pts >= Duration::from_secs(5),
752 "pts must be >= base_pts; got {pts:?}"
753 );
754 assert!(
755 pts < Duration::from_secs(6),
756 "pts must not advance 1 s in a unit test; got {pts:?}"
757 );
758 assert!(clock.should_sync(), "System clock must always sync");
759 }
760
761 #[test]
762 fn master_clock_system_reset_should_update_base_and_time_reference() {
763 let mut clock = MasterClock::System {
764 started_at: Instant::now() - Duration::from_secs(10),
765 base_pts: Duration::ZERO,
766 rate: 1.0,
767 };
768 assert!(
769 clock.current_pts() >= Duration::from_secs(9),
770 "clock should show ~10 s before reset"
771 );
772 clock.reset(Duration::from_secs(5));
773 let pts = clock.current_pts();
774 assert!(
775 pts >= Duration::from_secs(5),
776 "pts must be >= new base after reset; got {pts:?}"
777 );
778 assert!(
779 pts < Duration::from_secs(6),
780 "pts must not advance 1 s in a unit test after reset; got {pts:?}"
781 );
782 }
783
784 #[test]
785 fn master_clock_audio_should_not_sync_before_first_sample() {
786 let clock = MasterClock::Audio {
787 samples_consumed: Arc::new(AtomicU64::new(0)),
788 sample_rate: 48_000,
789 rate: 1.0,
790 samples_base: 0,
791 pts_base: Duration::ZERO,
792 fallback: None,
793 };
794 assert!(
795 !clock.should_sync(),
796 "audio clock must not sync before any samples are consumed and before fallback is armed"
797 );
798 assert_eq!(
799 clock.current_pts(),
800 Duration::ZERO,
801 "audio clock PTS must be zero before any samples and before fallback is armed"
802 );
803 }
804
805 #[test]
806 fn master_clock_audio_should_sync_and_report_pts_after_samples_consumed() {
807 let consumed = Arc::new(AtomicU64::new(48_000));
808 let clock = MasterClock::Audio {
809 samples_consumed: Arc::clone(&consumed),
810 sample_rate: 48_000,
811 rate: 1.0,
812 samples_base: 0,
813 pts_base: Duration::ZERO,
814 fallback: None,
815 };
816 assert!(
817 clock.should_sync(),
818 "audio clock must sync when samples > 0"
819 );
820 assert_eq!(
821 clock.current_pts(),
822 Duration::from_secs(1),
823 "48000 samples at 48000 Hz must equal 1 second"
824 );
825 }
826
827 #[test]
828 fn master_clock_audio_should_sync_after_fallback_activated() {
829 let mut clock = MasterClock::Audio {
830 samples_consumed: Arc::new(AtomicU64::new(0)),
831 sample_rate: 48_000,
832 rate: 1.0,
833 samples_base: 0,
834 pts_base: Duration::ZERO,
835 fallback: None,
836 };
837 assert!(
838 !clock.should_sync(),
839 "must not sync before fallback is armed"
840 );
841 clock.activate_fallback_if_no_audio(Duration::from_secs(1));
842 assert!(
843 clock.should_sync(),
844 "must sync after fallback is activated even when samples_consumed == 0"
845 );
846 }
847
848 #[test]
849 fn master_clock_audio_fallback_current_pts_should_advance_from_base_pts() {
850 let mut clock = MasterClock::Audio {
851 samples_consumed: Arc::new(AtomicU64::new(0)),
852 sample_rate: 48_000,
853 rate: 1.0,
854 samples_base: 0,
855 pts_base: Duration::ZERO,
856 fallback: None,
857 };
858 let base = Duration::from_secs(5);
859 clock.activate_fallback_if_no_audio(base);
860 let pts = clock.current_pts();
861 assert!(
862 pts >= base,
863 "fallback current_pts must be >= base_pts; got {pts:?}"
864 );
865 assert!(
866 pts < base + Duration::from_secs(1),
867 "fallback must not advance 1 s in a unit test; got {pts:?}"
868 );
869 }
870
871 #[test]
872 fn master_clock_audio_max_of_sample_and_fallback_should_prefer_further_ahead() {
873 let consumed = Arc::new(AtomicU64::new(0));
878 let mut clock = MasterClock::Audio {
879 samples_consumed: Arc::clone(&consumed),
880 sample_rate: 48_000,
881 rate: 1.0,
882 samples_base: 0,
883 pts_base: Duration::ZERO,
884 fallback: None,
885 };
886 clock.activate_fallback_if_no_audio(Duration::from_secs(2));
887 assert!(clock.should_sync(), "fallback must enable sync");
888 consumed.store(48_000, Ordering::Relaxed);
890 let pts = clock.current_pts();
892 assert!(
893 pts >= Duration::from_secs(2),
894 "max() must return fallback when fallback is further ahead; got {pts:?}"
895 );
896 assert!(
897 pts < Duration::from_secs(3),
898 "fallback must not be wildly ahead of 2 s; got {pts:?}"
899 );
900 }
901
902 #[test]
903 fn master_clock_audio_activate_fallback_should_be_idempotent() {
904 let mut clock = MasterClock::Audio {
905 samples_consumed: Arc::new(AtomicU64::new(0)),
906 sample_rate: 48_000,
907 rate: 1.0,
908 samples_base: 0,
909 pts_base: Duration::ZERO,
910 fallback: None,
911 };
912 clock.activate_fallback_if_no_audio(Duration::from_secs(1));
913 let pts1 = clock.current_pts();
914 thread::sleep(Duration::from_millis(5));
915 clock.activate_fallback_if_no_audio(Duration::from_secs(100));
917 let pts2 = clock.current_pts();
918 assert!(
919 pts2 > pts1,
920 "clock must keep advancing from the first base after second activate; \
921 pts1={pts1:?} pts2={pts2:?}"
922 );
923 assert!(
924 pts2 < Duration::from_secs(5),
925 "second activate must not reset clock to base=100 s; pts2={pts2:?}"
926 );
927 }
928
929 #[test]
930 fn master_clock_audio_reset_should_update_fallback_base_pts() {
931 let mut clock = MasterClock::Audio {
932 samples_consumed: Arc::new(AtomicU64::new(0)),
933 sample_rate: 48_000,
934 rate: 1.0,
935 samples_base: 0,
936 pts_base: Duration::ZERO,
937 fallback: None,
938 };
939 clock.activate_fallback_if_no_audio(Duration::from_secs(5));
940 clock.reset(Duration::from_secs(10));
942 let pts = clock.current_pts();
943 assert!(
944 pts >= Duration::from_secs(10),
945 "after reset, fallback must advance from the new base_pts; got {pts:?}"
946 );
947 assert!(
948 pts < Duration::from_secs(11),
949 "fallback must not advance 1 s in a unit test after reset; got {pts:?}"
950 );
951 }
952
953 #[test]
954 fn master_clock_audio_reset_should_not_arm_fallback_if_not_yet_active() {
955 let mut clock = MasterClock::Audio {
956 samples_consumed: Arc::new(AtomicU64::new(0)),
957 sample_rate: 48_000,
958 rate: 1.0,
959 samples_base: 0,
960 pts_base: Duration::ZERO,
961 fallback: None,
962 };
963 clock.reset(Duration::ZERO);
965 assert!(
966 !clock.should_sync(),
967 "reset() before activate_fallback_if_no_audio must not arm the fallback"
968 );
969 assert_eq!(
970 clock.current_pts(),
971 Duration::ZERO,
972 "PTS must remain ZERO when fallback is not yet armed"
973 );
974 }
975
976 #[test]
977 fn master_clock_audio_rearm_should_advance_past_frozen_sample_pts() {
978 let frozen_frames: u64 = (45_222 * 48_000) / 1_000; let consumed = Arc::new(AtomicU64::new(frozen_frames));
984 let mut clock = MasterClock::Audio {
985 samples_consumed: Arc::clone(&consumed),
986 sample_rate: 48_000,
987 rate: 1.0,
988 samples_base: 0,
989 pts_base: Duration::ZERO,
990 fallback: None,
991 };
992 let frozen_pts = Duration::from_secs_f64(frozen_frames as f64 / 48_000.0);
993 assert_eq!(
995 clock.current_pts(),
996 frozen_pts,
997 "clock must be frozen at audio EOF position before rearm"
998 );
999 clock.rearm_fallback_at(frozen_pts);
1001 thread::sleep(Duration::from_millis(10));
1002 let pts_after = clock.current_pts();
1004 assert!(
1005 pts_after > frozen_pts,
1006 "clock must advance past frozen sample_pts after rearm; \
1007 frozen={frozen_pts:?} after={pts_after:?}"
1008 );
1009 assert!(
1010 pts_after < frozen_pts + Duration::from_secs(1),
1011 "clock must not advance 1 s in a unit test after rearm; got {pts_after:?}"
1012 );
1013 }
1014
1015 #[test]
1016 fn master_clock_audio_rearm_should_be_noop_for_system_clock() {
1017 let mut clock = MasterClock::System {
1018 started_at: Instant::now(),
1019 base_pts: Duration::ZERO,
1020 rate: 1.0,
1021 };
1022 clock.rearm_fallback_at(Duration::from_secs(99));
1024 assert!(
1025 clock.should_sync(),
1026 "System clock must always sync after rearm_fallback_at"
1027 );
1028 }
1029
1030 #[test]
1031 fn audio_samples_snapshot_should_return_current_counter_for_audio_clock() {
1032 let consumed = Arc::new(AtomicU64::new(12_345));
1033 let clock = MasterClock::Audio {
1034 samples_consumed: Arc::clone(&consumed),
1035 sample_rate: 48_000,
1036 rate: 1.0,
1037 samples_base: 0,
1038 pts_base: Duration::ZERO,
1039 fallback: None,
1040 };
1041 assert_eq!(
1042 clock.audio_samples_snapshot(),
1043 12_345,
1044 "audio_samples_snapshot must reflect the current AtomicU64 value"
1045 );
1046 }
1047
1048 #[test]
1049 fn audio_samples_snapshot_should_return_zero_for_system_clock() {
1050 let clock = MasterClock::System {
1051 started_at: Instant::now(),
1052 base_pts: Duration::ZERO,
1053 rate: 1.0,
1054 };
1055 assert_eq!(
1056 clock.audio_samples_snapshot(),
1057 0,
1058 "audio_samples_snapshot must return 0 for System clock"
1059 );
1060 }
1061
1062 #[test]
1063 fn master_clock_audio_current_pts_should_advance_one_second_after_48k_frames() {
1064 let consumed = Arc::new(AtomicU64::new(48_000));
1068 let clock = MasterClock::Audio {
1069 samples_consumed: Arc::clone(&consumed),
1070 sample_rate: 48_000,
1071 rate: 1.0,
1072 samples_base: 0,
1073 pts_base: Duration::ZERO,
1074 fallback: None,
1075 };
1076 assert_eq!(
1077 clock.current_pts(),
1078 Duration::from_secs(1),
1079 "48 000 consumed frames / 48 000 Hz must equal exactly 1.0 s"
1080 );
1081 }
1082
1083 #[test]
1084 fn master_clock_audio_native_rate_mismatch_demonstrates_bug() {
1085 let consumed = Arc::new(AtomicU64::new(48_000));
1090 let clock_wrong = MasterClock::Audio {
1091 samples_consumed: Arc::clone(&consumed),
1092 sample_rate: 44_100, rate: 1.0,
1094 samples_base: 0,
1095 pts_base: Duration::ZERO,
1096 fallback: None,
1097 };
1098 let pts_wrong = clock_wrong.current_pts();
1099 assert!(
1101 pts_wrong > Duration::from_secs(1),
1102 "using native rate produces a clock that runs too fast; got {pts_wrong:?}"
1103 );
1104 assert!(
1105 pts_wrong < Duration::from_millis(1_100),
1106 "drift must be bounded to ~8.8 %; got {pts_wrong:?}"
1107 );
1108 }
1109
1110 #[test]
1111 fn master_clock_system_activate_fallback_should_be_noop() {
1112 let mut clock = MasterClock::System {
1113 started_at: Instant::now(),
1114 base_pts: Duration::ZERO,
1115 rate: 1.0,
1116 };
1117 clock.activate_fallback_if_no_audio(Duration::from_secs(99));
1119 assert!(
1120 clock.should_sync(),
1121 "System clock must always sync regardless of activate_fallback_if_no_audio"
1122 );
1123 }
1124
1125 #[test]
1126 fn set_rate_should_scale_audio_clock_pts() {
1127 let consumed = Arc::new(AtomicU64::new(48_000));
1129 let mut clock = MasterClock::Audio {
1130 samples_consumed: Arc::clone(&consumed),
1131 sample_rate: 48_000,
1132 rate: 1.0,
1133 samples_base: 0,
1134 pts_base: Duration::ZERO,
1135 fallback: None,
1136 };
1137
1138 assert_eq!(
1140 clock.current_pts(),
1141 Duration::from_secs(1),
1142 "before set_rate clock must report 1 s"
1143 );
1144
1145 clock.set_rate(2.0);
1147
1148 consumed.fetch_add(48_000, Ordering::Relaxed); let pts = clock.current_pts();
1154 let expected = Duration::from_secs(3);
1155 let tolerance = Duration::from_millis(1);
1156 assert!(
1157 pts >= expected.saturating_sub(tolerance) && pts <= expected + tolerance,
1158 "1 s at 1× + 1 real-s at 2× must equal ≈3 s; got {pts:?}"
1159 );
1160 }
1161
1162 #[test]
1163 fn set_rate_system_clock_should_scale_elapsed() {
1164 let mut clock = MasterClock::System {
1165 started_at: Instant::now(),
1166 base_pts: Duration::ZERO,
1167 rate: 1.0,
1168 };
1169
1170 thread::sleep(Duration::from_millis(10));
1172 let pts_before_rate_change = clock.current_pts();
1173 assert!(
1174 pts_before_rate_change >= Duration::from_millis(5),
1175 "clock must have advanced ~10 ms before rate change; got {pts_before_rate_change:?}"
1176 );
1177
1178 clock.set_rate(2.0);
1180 let pts_at_rate_change = clock.current_pts();
1181 assert!(
1183 pts_at_rate_change >= pts_before_rate_change,
1184 "clock must not go backward on set_rate; before={pts_before_rate_change:?} at={pts_at_rate_change:?}"
1185 );
1186
1187 thread::sleep(Duration::from_millis(10));
1189 let pts_after = clock.current_pts();
1190
1191 let media_elapsed = pts_after.saturating_sub(pts_at_rate_change);
1194 assert!(
1195 media_elapsed >= Duration::from_millis(15),
1196 "2× rate: 10 ms wall time must produce ≥15 ms media time; got media_elapsed={media_elapsed:?}"
1197 );
1198 assert!(
1199 pts_after > Duration::from_millis(20),
1200 "total PTS after ~10ms at 1× + ~10ms at 2× must be >20ms; got {pts_after:?}"
1201 );
1202 }
1203}