1#![allow(dead_code)]
9
10use std::collections::HashMap;
11
12use crate::clip::ClipId;
13use crate::error::{EditError, EditResult};
14
15pub type AngleId = u64;
17
18pub type MultiCamId = u64;
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub enum SyncMethod {
24 Timecode,
26 AudioWaveform,
28 ManualMarker,
30 CommonStart,
32}
33
34impl SyncMethod {
35 #[must_use]
37 pub fn label(self) -> &'static str {
38 match self {
39 Self::Timecode => "Timecode",
40 Self::AudioWaveform => "Audio Waveform",
41 Self::ManualMarker => "Manual Marker",
42 Self::CommonStart => "Common Start",
43 }
44 }
45}
46
47#[derive(Debug, Clone, PartialEq)]
53pub struct SyncPoint {
54 pub timeline_position: i64,
56 pub source_offset: i64,
58 pub confidence: f64,
60 pub method: SyncMethod,
62}
63
64impl SyncPoint {
65 #[must_use]
67 pub fn new(
68 timeline_position: i64,
69 source_offset: i64,
70 confidence: f64,
71 method: SyncMethod,
72 ) -> Self {
73 Self {
74 timeline_position,
75 source_offset,
76 confidence: confidence.clamp(0.0, 1.0),
77 method,
78 }
79 }
80
81 #[must_use]
83 pub fn offset_delta(&self) -> i64 {
84 self.timeline_position - self.source_offset
85 }
86
87 #[must_use]
89 pub fn is_confident(&self, threshold: f64) -> bool {
90 self.confidence >= threshold
91 }
92}
93
94#[derive(Debug, Clone)]
96pub struct CameraAngle {
97 pub id: AngleId,
99 pub label: String,
101 pub clips: Vec<ClipId>,
103 pub sync_points: Vec<SyncPoint>,
105 pub active: bool,
107 pub alignment_offset: i64,
110}
111
112impl CameraAngle {
113 #[must_use]
115 pub fn new(id: AngleId, label: String) -> Self {
116 Self {
117 id,
118 label,
119 clips: Vec::new(),
120 sync_points: Vec::new(),
121 active: false,
122 alignment_offset: 0,
123 }
124 }
125
126 pub fn add_clip(&mut self, clip_id: ClipId) {
128 if !self.clips.contains(&clip_id) {
129 self.clips.push(clip_id);
130 }
131 }
132
133 pub fn remove_clip(&mut self, clip_id: ClipId) -> bool {
135 if let Some(pos) = self.clips.iter().position(|&id| id == clip_id) {
136 self.clips.remove(pos);
137 true
138 } else {
139 false
140 }
141 }
142
143 pub fn add_sync_point(&mut self, point: SyncPoint) {
145 self.sync_points.push(point);
146 }
147
148 #[must_use]
153 #[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
154 pub fn compute_alignment(&self) -> Option<i64> {
155 if self.sync_points.is_empty() {
156 return None;
157 }
158 let total_weight: f64 = self.sync_points.iter().map(|sp| sp.confidence).sum();
159 if total_weight < 1e-12 {
160 let sum: i64 = self.sync_points.iter().map(SyncPoint::offset_delta).sum();
162 return Some(sum / self.sync_points.len() as i64);
163 }
164 let weighted_sum: f64 = self
165 .sync_points
166 .iter()
167 .map(|sp| sp.offset_delta() as f64 * sp.confidence)
168 .sum();
169 Some((weighted_sum / total_weight).round() as i64)
170 }
171}
172
173#[derive(Debug, Clone, PartialEq)]
176pub struct AngleSwitch {
177 pub position: i64,
179 pub angle_id: AngleId,
181}
182
183impl AngleSwitch {
184 #[must_use]
186 pub fn new(position: i64, angle_id: AngleId) -> Self {
187 Self { position, angle_id }
188 }
189}
190
191#[derive(Debug, Clone)]
197pub struct MultiCamGroup {
198 pub id: MultiCamId,
200 pub name: String,
202 pub angles: Vec<CameraAngle>,
204 pub switches: Vec<AngleSwitch>,
206 next_angle_id: AngleId,
208}
209
210impl MultiCamGroup {
211 #[must_use]
213 pub fn new(id: MultiCamId, name: String) -> Self {
214 Self {
215 id,
216 name,
217 angles: Vec::new(),
218 switches: Vec::new(),
219 next_angle_id: 1,
220 }
221 }
222
223 pub fn add_angle(&mut self, label: String) -> AngleId {
225 let id = self.next_angle_id;
226 self.next_angle_id += 1;
227 self.angles.push(CameraAngle::new(id, label));
228 id
229 }
230
231 pub fn remove_angle(&mut self, angle_id: AngleId) -> Option<CameraAngle> {
233 if let Some(pos) = self.angles.iter().position(|a| a.id == angle_id) {
234 let angle = self.angles.remove(pos);
235 self.switches.retain(|s| s.angle_id != angle_id);
237 Some(angle)
238 } else {
239 None
240 }
241 }
242
243 #[must_use]
245 pub fn get_angle(&self, angle_id: AngleId) -> Option<&CameraAngle> {
246 self.angles.iter().find(|a| a.id == angle_id)
247 }
248
249 pub fn get_angle_mut(&mut self, angle_id: AngleId) -> Option<&mut CameraAngle> {
251 self.angles.iter_mut().find(|a| a.id == angle_id)
252 }
253
254 pub fn add_switch(&mut self, position: i64, angle_id: AngleId) -> EditResult<()> {
256 if !self.angles.iter().any(|a| a.id == angle_id) {
257 return Err(EditError::InvalidEdit(format!(
258 "Angle {angle_id} not found in multi-cam group"
259 )));
260 }
261 self.switches.push(AngleSwitch::new(position, angle_id));
262 self.switches.sort_by_key(|s| s.position);
263 Ok(())
264 }
265
266 pub fn remove_switch_at(&mut self, position: i64) -> Option<AngleSwitch> {
268 if let Some(pos) = self.switches.iter().position(|s| s.position == position) {
269 Some(self.switches.remove(pos))
270 } else {
271 None
272 }
273 }
274
275 #[must_use]
279 pub fn active_angle_at(&self, position: i64) -> Option<AngleId> {
280 self.switches
281 .iter()
282 .rev()
283 .find(|s| s.position <= position)
284 .map(|s| s.angle_id)
285 }
286
287 pub fn sync_all_angles(&mut self) {
289 for angle in &mut self.angles {
290 if let Some(offset) = angle.compute_alignment() {
291 angle.alignment_offset = offset;
292 }
293 }
294 }
295
296 #[must_use]
298 pub fn angle_count(&self) -> usize {
299 self.angles.len()
300 }
301
302 #[must_use]
304 pub fn switch_count(&self) -> usize {
305 self.switches.len()
306 }
307}
308
309#[derive(Debug, Default)]
311pub struct MultiCamManager {
312 groups: HashMap<MultiCamId, MultiCamGroup>,
314 next_id: MultiCamId,
316}
317
318impl MultiCamManager {
319 #[must_use]
321 pub fn new() -> Self {
322 Self {
323 groups: HashMap::new(),
324 next_id: 1,
325 }
326 }
327
328 pub fn create_group(&mut self, name: String) -> MultiCamId {
330 let id = self.next_id;
331 self.next_id += 1;
332 self.groups.insert(id, MultiCamGroup::new(id, name));
333 id
334 }
335
336 pub fn delete_group(&mut self, id: MultiCamId) -> Option<MultiCamGroup> {
338 self.groups.remove(&id)
339 }
340
341 #[must_use]
343 pub fn get_group(&self, id: MultiCamId) -> Option<&MultiCamGroup> {
344 self.groups.get(&id)
345 }
346
347 pub fn get_group_mut(&mut self, id: MultiCamId) -> Option<&mut MultiCamGroup> {
349 self.groups.get_mut(&id)
350 }
351
352 #[must_use]
354 pub fn all_groups(&self) -> Vec<&MultiCamGroup> {
355 self.groups.values().collect()
356 }
357
358 pub fn clear(&mut self) {
360 self.groups.clear();
361 }
362
363 #[must_use]
365 pub fn len(&self) -> usize {
366 self.groups.len()
367 }
368
369 #[must_use]
371 pub fn is_empty(&self) -> bool {
372 self.groups.is_empty()
373 }
374}
375
376#[derive(Debug, Clone, PartialEq)]
383pub struct ClipRef {
384 pub clip_id: ClipId,
386 pub start_ms: i64,
388 pub duration_ms: i64,
390}
391
392impl ClipRef {
393 #[must_use]
395 pub fn new(clip_id: ClipId, start_ms: i64, duration_ms: i64) -> Self {
396 Self {
397 clip_id,
398 start_ms,
399 duration_ms,
400 }
401 }
402
403 #[must_use]
405 pub fn end_ms(&self) -> i64 {
406 self.start_ms + self.duration_ms
407 }
408}
409
410#[derive(Debug, Clone)]
413pub struct CameraTrack {
414 pub id: String,
416 pub clips: Vec<ClipRef>,
418 pub timecode_start: i64,
421}
422
423impl CameraTrack {
424 #[must_use]
426 pub fn new(id: impl Into<String>, timecode_start: i64) -> Self {
427 Self {
428 id: id.into(),
429 clips: Vec::new(),
430 timecode_start,
431 }
432 }
433
434 pub fn add_clip(&mut self, clip: ClipRef) {
436 self.clips.push(clip);
437 }
438
439 #[must_use]
444 pub fn clip_at_ms(&self, ms: i64) -> Option<&ClipRef> {
445 let local_ms = ms - self.timecode_start;
446 self.clips
447 .iter()
448 .find(|c| local_ms >= c.start_ms && local_ms < c.start_ms + c.duration_ms)
449 }
450
451 #[must_use]
453 pub fn clip_count(&self) -> usize {
454 self.clips.len()
455 }
456}
457
458#[derive(Debug, Clone, PartialEq)]
460pub struct MultiCamCut {
461 pub timestamp_ms: u64,
463 pub from_camera: usize,
465 pub to_camera: usize,
467}
468
469impl MultiCamCut {
470 #[must_use]
472 pub fn new(timestamp_ms: u64, from_camera: usize, to_camera: usize) -> Self {
473 Self {
474 timestamp_ms,
475 from_camera,
476 to_camera,
477 }
478 }
479}
480
481#[derive(Debug, Clone, Default)]
483pub struct MultiCamSession {
484 pub cameras: Vec<CameraTrack>,
486 pub sync_offset_ms: Vec<i64>,
489}
490
491impl MultiCamSession {
492 #[must_use]
494 pub fn new() -> Self {
495 Self::default()
496 }
497
498 pub fn add_camera(&mut self, track: CameraTrack) {
500 self.cameras.push(track);
501 self.sync_offset_ms.push(0);
502 }
503
504 pub fn set_sync_offset(&mut self, camera_idx: usize, offset_ms: i64) -> EditResult<()> {
510 if camera_idx >= self.cameras.len() {
511 return Err(EditError::InvalidEdit(format!(
512 "Camera index {camera_idx} out of range (session has {} cameras)",
513 self.cameras.len()
514 )));
515 }
516 self.sync_offset_ms[camera_idx] = offset_ms;
517 Ok(())
518 }
519
520 #[must_use]
522 pub fn camera_count(&self) -> usize {
523 self.cameras.len()
524 }
525
526 #[must_use]
530 pub fn camera_local_ms(&self, camera_idx: usize, abs_ms: i64) -> i64 {
531 let offset = self.sync_offset_ms.get(camera_idx).copied().unwrap_or(0);
532 abs_ms - offset
533 }
534}
535
536#[derive(Debug)]
541pub struct MultiCamEditor {
542 pub session: MultiCamSession,
544 pub cuts: Vec<MultiCamCut>,
546}
547
548impl MultiCamEditor {
549 #[must_use]
551 pub fn new(session: MultiCamSession) -> Self {
552 Self {
553 session,
554 cuts: Vec::new(),
555 }
556 }
557
558 pub fn add_cut(&mut self, cut: MultiCamCut) -> EditResult<()> {
565 let n = self.session.camera_count();
566 if cut.from_camera >= n {
567 return Err(EditError::InvalidEdit(format!(
568 "from_camera {} is out of range (session has {n} cameras)",
569 cut.from_camera
570 )));
571 }
572 if cut.to_camera >= n {
573 return Err(EditError::InvalidEdit(format!(
574 "to_camera {} is out of range (session has {n} cameras)",
575 cut.to_camera
576 )));
577 }
578 self.cuts.push(cut);
579 self.cuts.sort_by_key(|c| c.timestamp_ms);
580 Ok(())
581 }
582
583 pub fn remove_cut_at(&mut self, timestamp_ms: u64) -> Option<MultiCamCut> {
585 if let Some(pos) = self
586 .cuts
587 .iter()
588 .position(|c| c.timestamp_ms == timestamp_ms)
589 {
590 Some(self.cuts.remove(pos))
591 } else {
592 None
593 }
594 }
595
596 #[must_use]
598 pub fn cut_count(&self) -> usize {
599 self.cuts.len()
600 }
601
602 #[must_use]
610 pub fn generate_timeline(&self) -> Vec<ClipRef> {
611 if self.cuts.is_empty() {
612 return Vec::new();
613 }
614
615 let mut result = Vec::with_capacity(self.cuts.len());
616
617 for (i, cut) in self.cuts.iter().enumerate() {
618 let interval_start_ms = cut.timestamp_ms as i64;
619 let interval_end_ms = self
620 .cuts
621 .get(i + 1)
622 .map_or(interval_start_ms + 5000, |next| next.timestamp_ms as i64);
623
624 let midpoint_ms = (interval_start_ms + interval_end_ms) / 2;
625 let cam_idx = cut.to_camera;
626
627 let local_ms = self.session.camera_local_ms(cam_idx, midpoint_ms);
629
630 if let Some(cam) = self.session.cameras.get(cam_idx) {
631 let clip_ref = cam.clip_at_ms(local_ms + cam.timecode_start);
633 let duration_ms = interval_end_ms - interval_start_ms;
634
635 if let Some(cr) = clip_ref {
636 result.push(ClipRef::new(cr.clip_id, interval_start_ms, duration_ms));
637 } else if let Some(first_clip) = cam.clips.first() {
638 result.push(ClipRef::new(
640 first_clip.clip_id,
641 interval_start_ms,
642 duration_ms,
643 ));
644 }
645 }
647 }
648
649 result
650 }
651}
652
653#[must_use]
665pub fn sync_by_audio_fingerprint(camera_a: &[f32], camera_b: &[f32]) -> i64 {
666 if camera_a.is_empty() || camera_b.is_empty() {
667 return 0;
668 }
669
670 let len_a = camera_a.len();
671 let len_b = camera_b.len();
672 let window = len_a.min(len_b);
673 let max_lag = (window / 4).max(1) as i64;
674
675 let mut best_lag: i64 = 0;
676 let mut best_corr = f64::NEG_INFINITY;
677
678 for lag in -max_lag..=max_lag {
679 let mut dot: f64 = 0.0;
680 for j in 0..window {
681 let b_idx = j as i64 + lag;
682 if b_idx >= 0 && (b_idx as usize) < len_b {
683 dot += f64::from(camera_a[j]) * f64::from(camera_b[b_idx as usize]);
684 }
685 }
686 if dot > best_corr {
687 best_corr = dot;
688 best_lag = lag;
689 }
690 }
691
692 best_lag
693}
694
695#[cfg(test)]
700mod tests {
701 use super::*;
702
703 #[test]
704 fn test_sync_method_label() {
705 assert_eq!(SyncMethod::Timecode.label(), "Timecode");
706 assert_eq!(SyncMethod::AudioWaveform.label(), "Audio Waveform");
707 assert_eq!(SyncMethod::ManualMarker.label(), "Manual Marker");
708 assert_eq!(SyncMethod::CommonStart.label(), "Common Start");
709 }
710
711 #[test]
712 fn test_sync_point_creation() {
713 let sp = SyncPoint::new(1000, 500, 0.95, SyncMethod::Timecode);
714 assert_eq!(sp.timeline_position, 1000);
715 assert_eq!(sp.source_offset, 500);
716 assert!((sp.confidence - 0.95).abs() < 1e-9);
717 assert_eq!(sp.offset_delta(), 500);
718 }
719
720 #[test]
721 fn test_sync_point_confidence_clamped() {
722 let sp = SyncPoint::new(0, 0, 1.5, SyncMethod::Timecode);
723 assert!((sp.confidence - 1.0).abs() < 1e-9);
724 let sp2 = SyncPoint::new(0, 0, -0.5, SyncMethod::Timecode);
725 assert!((sp2.confidence).abs() < 1e-9);
726 }
727
728 #[test]
729 fn test_sync_point_is_confident() {
730 let sp = SyncPoint::new(0, 0, 0.8, SyncMethod::Timecode);
731 assert!(sp.is_confident(0.5));
732 assert!(sp.is_confident(0.8));
733 assert!(!sp.is_confident(0.9));
734 }
735
736 #[test]
737 fn test_camera_angle_clips() {
738 let mut angle = CameraAngle::new(1, "Camera A".to_string());
739 angle.add_clip(10);
740 angle.add_clip(20);
741 angle.add_clip(10); assert_eq!(angle.clips.len(), 2);
743 assert!(angle.remove_clip(10));
744 assert!(!angle.remove_clip(999));
745 assert_eq!(angle.clips.len(), 1);
746 }
747
748 #[test]
749 fn test_camera_angle_compute_alignment() {
750 let mut angle = CameraAngle::new(1, "A".to_string());
751 assert!(angle.compute_alignment().is_none());
752
753 angle.add_sync_point(SyncPoint::new(1000, 500, 1.0, SyncMethod::Timecode));
754 angle.add_sync_point(SyncPoint::new(2000, 1500, 1.0, SyncMethod::Timecode));
755 let offset = angle.compute_alignment();
757 assert_eq!(offset, Some(500));
758 }
759
760 #[test]
761 fn test_camera_angle_compute_alignment_weighted() {
762 let mut angle = CameraAngle::new(1, "A".to_string());
763 angle.add_sync_point(SyncPoint::new(100, 0, 0.9, SyncMethod::Timecode));
765 angle.add_sync_point(SyncPoint::new(200, 0, 0.1, SyncMethod::AudioWaveform));
766 let offset = angle.compute_alignment();
767 assert_eq!(offset, Some(110));
769 }
770
771 #[test]
772 fn test_multicam_group_add_remove_angle() {
773 let mut group = MultiCamGroup::new(1, "Test".to_string());
774 let a1 = group.add_angle("Camera A".to_string());
775 let a2 = group.add_angle("Camera B".to_string());
776 assert_eq!(group.angle_count(), 2);
777
778 assert!(group.get_angle(a1).is_some());
779 assert!(group.remove_angle(a2).is_some());
780 assert_eq!(group.angle_count(), 1);
781 assert!(group.remove_angle(999).is_none());
782 }
783
784 #[test]
785 fn test_multicam_group_switches() {
786 let mut group = MultiCamGroup::new(1, "Test".to_string());
787 let a1 = group.add_angle("Camera A".to_string());
788 let a2 = group.add_angle("Camera B".to_string());
789
790 assert!(group.add_switch(0, a1).is_ok());
791 assert!(group.add_switch(5000, a2).is_ok());
792 assert!(group.add_switch(10000, a1).is_ok());
793 assert_eq!(group.switch_count(), 3);
794
795 assert!(group.add_switch(15000, 999).is_err());
797
798 assert_eq!(group.active_angle_at(2500), Some(a1));
800 assert_eq!(group.active_angle_at(5000), Some(a2));
801 assert_eq!(group.active_angle_at(7500), Some(a2));
802 assert_eq!(group.active_angle_at(10000), Some(a1));
803 assert!(group.active_angle_at(-100).is_none());
804 }
805
806 #[test]
807 fn test_multicam_group_remove_switch() {
808 let mut group = MultiCamGroup::new(1, "Test".to_string());
809 let a1 = group.add_angle("A".to_string());
810 let _ = group.add_switch(0, a1);
811 assert!(group.remove_switch_at(0).is_some());
812 assert!(group.remove_switch_at(0).is_none());
813 assert_eq!(group.switch_count(), 0);
814 }
815
816 #[test]
817 fn test_multicam_group_remove_angle_removes_switches() {
818 let mut group = MultiCamGroup::new(1, "Test".to_string());
819 let a1 = group.add_angle("A".to_string());
820 let a2 = group.add_angle("B".to_string());
821 let _ = group.add_switch(0, a1);
822 let _ = group.add_switch(5000, a2);
823 group.remove_angle(a1);
824 assert_eq!(group.switch_count(), 1);
826 assert_eq!(group.switches[0].angle_id, a2);
827 }
828
829 #[test]
830 fn test_multicam_group_sync_all_angles() {
831 let mut group = MultiCamGroup::new(1, "Test".to_string());
832 let a1 = group.add_angle("A".to_string());
833 let angle = group.get_angle_mut(a1).expect("angle should exist");
834 angle.add_sync_point(SyncPoint::new(1000, 800, 1.0, SyncMethod::Timecode));
835 group.sync_all_angles();
836 assert_eq!(
837 group
838 .get_angle(a1)
839 .expect("angle should exist")
840 .alignment_offset,
841 200
842 );
843 }
844
845 #[test]
846 fn test_multicam_manager() {
847 let mut mgr = MultiCamManager::new();
848 assert!(mgr.is_empty());
849
850 let g1 = mgr.create_group("Group 1".to_string());
851 let g2 = mgr.create_group("Group 2".to_string());
852 assert_eq!(mgr.len(), 2);
853
854 assert!(mgr.get_group(g1).is_some());
855 assert!(mgr.delete_group(g2).is_some());
856 assert_eq!(mgr.len(), 1);
857
858 mgr.clear();
859 assert!(mgr.is_empty());
860 }
861
862 #[test]
863 fn test_multicam_manager_all_groups() {
864 let mut mgr = MultiCamManager::new();
865 let _ = mgr.create_group("A".to_string());
866 let _ = mgr.create_group("B".to_string());
867 assert_eq!(mgr.all_groups().len(), 2);
868 }
869
870 #[test]
871 fn test_angle_switch_creation() {
872 let sw = AngleSwitch::new(5000, 2);
873 assert_eq!(sw.position, 5000);
874 assert_eq!(sw.angle_id, 2);
875 }
876}
877
878#[cfg(test)]
883mod multicam_session_tests {
884 use super::*;
885
886 fn make_clip_ref(id: ClipId, start_ms: i64, duration_ms: i64) -> ClipRef {
887 ClipRef::new(id, start_ms, duration_ms)
888 }
889
890 fn make_track(id: &str, timecode_start: i64, clips: Vec<ClipRef>) -> CameraTrack {
891 let mut t = CameraTrack::new(id, timecode_start);
892 for c in clips {
893 t.add_clip(c);
894 }
895 t
896 }
897
898 #[test]
901 fn test_clip_ref_creation() {
902 let cr = make_clip_ref(42, 1000, 500);
903 assert_eq!(cr.clip_id, 42);
904 assert_eq!(cr.start_ms, 1000);
905 assert_eq!(cr.duration_ms, 500);
906 }
907
908 #[test]
909 fn test_clip_ref_end_ms() {
910 let cr = make_clip_ref(1, 200, 300);
911 assert_eq!(cr.end_ms(), 500);
912 }
913
914 #[test]
917 fn test_camera_track_add_and_count() {
918 let mut t = CameraTrack::new("cam-a", 0);
919 assert_eq!(t.clip_count(), 0);
920 t.add_clip(make_clip_ref(10, 0, 1000));
921 t.add_clip(make_clip_ref(11, 1000, 1000));
922 assert_eq!(t.clip_count(), 2);
923 }
924
925 #[test]
926 fn test_camera_track_clip_at_ms_found() {
927 let clips = vec![make_clip_ref(10, 0, 1000), make_clip_ref(11, 1000, 1000)];
928 let track = make_track("cam-a", 0, clips);
929 let found = track.clip_at_ms(500);
930 assert!(found.is_some());
931 assert_eq!(found.expect("clip should exist").clip_id, 10);
932 }
933
934 #[test]
935 fn test_camera_track_clip_at_ms_second_clip() {
936 let clips = vec![make_clip_ref(10, 0, 1000), make_clip_ref(11, 1000, 1000)];
937 let track = make_track("cam-a", 0, clips);
938 let found = track.clip_at_ms(1500);
939 assert!(found.is_some());
940 assert_eq!(found.expect("clip should exist").clip_id, 11);
941 }
942
943 #[test]
944 fn test_camera_track_clip_at_ms_not_found() {
945 let clips = vec![make_clip_ref(10, 0, 1000)];
946 let track = make_track("cam-a", 0, clips);
947 assert!(track.clip_at_ms(5000).is_none());
948 }
949
950 #[test]
951 fn test_camera_track_timecode_start_offset() {
952 let clips = vec![make_clip_ref(99, 0, 1000)];
955 let track = make_track("cam-b", 10_000, clips);
956 let found = track.clip_at_ms(10_500);
957 assert!(found.is_some(), "Should find clip at abs 10_500");
958 }
959
960 #[test]
963 fn test_multicam_cut_creation() {
964 let cut = MultiCamCut::new(3000, 0, 1);
965 assert_eq!(cut.timestamp_ms, 3000);
966 assert_eq!(cut.from_camera, 0);
967 assert_eq!(cut.to_camera, 1);
968 }
969
970 #[test]
973 fn test_session_add_camera() {
974 let mut session = MultiCamSession::new();
975 assert_eq!(session.camera_count(), 0);
976 session.add_camera(CameraTrack::new("A", 0));
977 session.add_camera(CameraTrack::new("B", 0));
978 assert_eq!(session.camera_count(), 2);
979 assert_eq!(session.sync_offset_ms.len(), 2);
980 }
981
982 #[test]
983 fn test_session_set_sync_offset_valid() {
984 let mut session = MultiCamSession::new();
985 session.add_camera(CameraTrack::new("A", 0));
986 let result = session.set_sync_offset(0, 250);
987 assert!(result.is_ok());
988 assert_eq!(session.sync_offset_ms[0], 250);
989 }
990
991 #[test]
992 fn test_session_set_sync_offset_out_of_bounds() {
993 let mut session = MultiCamSession::new();
994 session.add_camera(CameraTrack::new("A", 0));
995 let result = session.set_sync_offset(5, 100);
996 assert!(result.is_err());
997 }
998
999 #[test]
1000 fn test_session_camera_local_ms() {
1001 let mut session = MultiCamSession::new();
1002 session.add_camera(CameraTrack::new("A", 0));
1003 session.set_sync_offset(0, 500).expect("set ok");
1004 assert_eq!(session.camera_local_ms(0, 2000), 1500);
1006 }
1007
1008 #[test]
1011 fn test_editor_add_cut_valid() {
1012 let mut session = MultiCamSession::new();
1013 session.add_camera(CameraTrack::new("A", 0));
1014 session.add_camera(CameraTrack::new("B", 0));
1015 let mut editor = MultiCamEditor::new(session);
1016 let result = editor.add_cut(MultiCamCut::new(1000, 0, 1));
1017 assert!(result.is_ok());
1018 assert_eq!(editor.cut_count(), 1);
1019 }
1020
1021 #[test]
1022 fn test_editor_add_cut_invalid_from_camera() {
1023 let mut session = MultiCamSession::new();
1024 session.add_camera(CameraTrack::new("A", 0));
1025 let mut editor = MultiCamEditor::new(session);
1026 let result = editor.add_cut(MultiCamCut::new(1000, 5, 0));
1027 assert!(result.is_err());
1028 }
1029
1030 #[test]
1031 fn test_editor_add_cut_invalid_to_camera() {
1032 let mut session = MultiCamSession::new();
1033 session.add_camera(CameraTrack::new("A", 0));
1034 let mut editor = MultiCamEditor::new(session);
1035 let result = editor.add_cut(MultiCamCut::new(1000, 0, 5));
1036 assert!(result.is_err());
1037 }
1038
1039 #[test]
1040 fn test_editor_cuts_sorted() {
1041 let mut session = MultiCamSession::new();
1042 session.add_camera(CameraTrack::new("A", 0));
1043 session.add_camera(CameraTrack::new("B", 0));
1044 let mut editor = MultiCamEditor::new(session);
1045 editor.add_cut(MultiCamCut::new(5000, 0, 1)).expect("ok");
1046 editor.add_cut(MultiCamCut::new(1000, 1, 0)).expect("ok");
1047 editor.add_cut(MultiCamCut::new(3000, 0, 1)).expect("ok");
1048 assert_eq!(editor.cuts[0].timestamp_ms, 1000);
1049 assert_eq!(editor.cuts[1].timestamp_ms, 3000);
1050 assert_eq!(editor.cuts[2].timestamp_ms, 5000);
1051 }
1052
1053 #[test]
1054 fn test_editor_remove_cut_at_found() {
1055 let mut session = MultiCamSession::new();
1056 session.add_camera(CameraTrack::new("A", 0));
1057 session.add_camera(CameraTrack::new("B", 0));
1058 let mut editor = MultiCamEditor::new(session);
1059 editor.add_cut(MultiCamCut::new(1000, 0, 1)).expect("ok");
1060 let removed = editor.remove_cut_at(1000);
1061 assert!(removed.is_some());
1062 assert_eq!(editor.cut_count(), 0);
1063 }
1064
1065 #[test]
1066 fn test_editor_remove_cut_at_not_found() {
1067 let mut session = MultiCamSession::new();
1068 session.add_camera(CameraTrack::new("A", 0));
1069 let mut editor = MultiCamEditor::new(session);
1070 assert!(editor.remove_cut_at(9999).is_none());
1071 }
1072
1073 #[test]
1074 fn test_editor_generate_timeline_empty_cuts() {
1075 let session = MultiCamSession::new();
1076 let editor = MultiCamEditor::new(session);
1077 assert!(editor.generate_timeline().is_empty());
1078 }
1079
1080 #[test]
1081 fn test_editor_generate_timeline_single_cut() {
1082 let mut session = MultiCamSession::new();
1083 let mut cam = CameraTrack::new("A", 0);
1084 cam.add_clip(ClipRef::new(7, 0, 10_000));
1085 session.add_camera(cam);
1086 session.add_camera(CameraTrack::new("B", 0)); let mut editor = MultiCamEditor::new(session);
1089 editor.add_cut(MultiCamCut::new(0, 0, 0)).expect("ok");
1090
1091 let tl = editor.generate_timeline();
1092 assert_eq!(tl.len(), 1);
1094 assert_eq!(tl[0].clip_id, 7);
1095 }
1096
1097 #[test]
1100 fn test_sync_empty_a() {
1101 assert_eq!(sync_by_audio_fingerprint(&[], &[1.0, 2.0, 3.0]), 0);
1102 }
1103
1104 #[test]
1105 fn test_sync_empty_b() {
1106 assert_eq!(sync_by_audio_fingerprint(&[1.0, 2.0, 3.0], &[]), 0);
1107 }
1108
1109 #[test]
1110 fn test_sync_both_empty() {
1111 assert_eq!(sync_by_audio_fingerprint(&[], &[]), 0);
1112 }
1113
1114 #[test]
1115 fn test_sync_identical_signals_zero_lag() {
1116 let signal: Vec<f32> = (0..64).map(|i| (i as f32).sin()).collect();
1117 let lag = sync_by_audio_fingerprint(&signal, &signal);
1118 assert_eq!(lag, 0, "Identical signals should have zero lag");
1119 }
1120
1121 #[test]
1122 fn test_sync_shifted_signal() {
1123 let len = 64usize;
1125 let shift = 4i64;
1126 let base: Vec<f32> = (0..len).map(|i| ((i as f32) * 0.3).sin()).collect();
1127 let mut shifted = vec![0.0f32; len];
1128 for i in shift as usize..len {
1129 shifted[i] = base[i - shift as usize];
1130 }
1131 let lag = sync_by_audio_fingerprint(&base, &shifted);
1132 assert_eq!(lag, shift, "Expected lag={shift}, got {lag}");
1134 }
1135}