1use crate::{AlignError, AlignResult, Point2D};
12
13#[derive(Debug, Clone)]
15pub struct SyncMarker {
16 pub frame: usize,
18 pub marker_type: MarkerType,
20 pub confidence: f32,
22 pub location: Option<Point2D>,
24}
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub enum MarkerType {
29 ClapperClosure,
31 Flash,
33 LedMarker,
35 AudioSpike,
37 TimecodeDisplay,
39}
40
41impl SyncMarker {
42 #[must_use]
44 pub fn new(
45 frame: usize,
46 marker_type: MarkerType,
47 confidence: f32,
48 location: Option<Point2D>,
49 ) -> Self {
50 Self {
51 frame,
52 marker_type,
53 confidence,
54 location,
55 }
56 }
57}
58
59pub struct FlashDetector {
61 pub threshold: f32,
63 pub min_duration: usize,
65 pub max_duration: usize,
67}
68
69impl Default for FlashDetector {
70 fn default() -> Self {
71 Self {
72 threshold: 0.8,
73 min_duration: 1,
74 max_duration: 3,
75 }
76 }
77}
78
79impl FlashDetector {
80 #[must_use]
82 pub fn new(threshold: f32, min_duration: usize, max_duration: usize) -> Self {
83 Self {
84 threshold,
85 min_duration,
86 max_duration,
87 }
88 }
89
90 #[must_use]
92 pub fn detect(&self, frames: &[&[u8]], width: usize, height: usize) -> Vec<SyncMarker> {
93 let mut markers = Vec::new();
94 let brightness_values: Vec<f32> = frames
95 .iter()
96 .map(|frame| self.compute_brightness(frame, width, height))
97 .collect();
98
99 let mut in_flash = false;
100 let mut flash_start = 0;
101
102 for (i, &brightness) in brightness_values.iter().enumerate() {
103 if !in_flash && brightness > self.threshold {
104 in_flash = true;
105 flash_start = i;
106 } else if in_flash && brightness <= self.threshold {
107 let duration = i - flash_start;
108 if duration >= self.min_duration && duration <= self.max_duration {
109 let confidence = brightness_values[flash_start];
110 markers.push(SyncMarker::new(
111 flash_start,
112 MarkerType::Flash,
113 confidence,
114 None,
115 ));
116 }
117 in_flash = false;
118 }
119 }
120
121 markers
122 }
123
124 fn compute_brightness(&self, rgb: &[u8], width: usize, height: usize) -> f32 {
126 if rgb.len() != width * height * 3 {
127 return 0.0;
128 }
129
130 let sum: u32 = rgb
131 .chunks_exact(3)
132 .map(|pixel| {
133 let r = u32::from(pixel[0]);
134 let g = u32::from(pixel[1]);
135 let b = u32::from(pixel[2]);
136 (299 * r + 587 * g + 114 * b) / 1000
137 })
138 .sum();
139
140 (sum as f32 / (width * height) as f32) / 255.0
141 }
142
143 #[must_use]
145 pub fn detect_local(
146 &self,
147 frames: &[&[u8]],
148 width: usize,
149 _height: usize,
150 region: &Region,
151 ) -> Vec<SyncMarker> {
152 let mut markers = Vec::new();
153
154 for (frame_idx, frame) in frames.iter().enumerate() {
155 let brightness = self.compute_region_brightness(frame, width, region);
156
157 if brightness > self.threshold {
158 let center = Point2D::new(
159 (region.x + region.width / 2) as f64,
160 (region.y + region.height / 2) as f64,
161 );
162
163 markers.push(SyncMarker::new(
164 frame_idx,
165 MarkerType::Flash,
166 brightness,
167 Some(center),
168 ));
169 }
170 }
171
172 markers
173 }
174
175 fn compute_region_brightness(&self, rgb: &[u8], width: usize, region: &Region) -> f32 {
177 let mut sum = 0u32;
178 let mut count = 0u32;
179
180 for y in region.y..region.y + region.height {
181 for x in region.x..region.x + region.width {
182 let idx = (y * width + x) * 3;
183 if idx + 2 < rgb.len() {
184 let r = u32::from(rgb[idx]);
185 let g = u32::from(rgb[idx + 1]);
186 let b = u32::from(rgb[idx + 2]);
187 sum += (299 * r + 587 * g + 114 * b) / 1000;
188 count += 1;
189 }
190 }
191 }
192
193 if count > 0 {
194 (sum as f32 / count as f32) / 255.0
195 } else {
196 0.0
197 }
198 }
199}
200
201#[derive(Debug, Clone, Copy)]
203pub struct Region {
204 pub x: usize,
206 pub y: usize,
208 pub width: usize,
210 pub height: usize,
212}
213
214impl Region {
215 #[must_use]
217 pub fn new(x: usize, y: usize, width: usize, height: usize) -> Self {
218 Self {
219 x,
220 y,
221 width,
222 height,
223 }
224 }
225}
226
227pub struct ClapperDetector {
229 pub motion_threshold: f32,
231 pub min_motion_area: f32,
233}
234
235impl Default for ClapperDetector {
236 fn default() -> Self {
237 Self {
238 motion_threshold: 30.0,
239 min_motion_area: 0.1,
240 }
241 }
242}
243
244impl ClapperDetector {
245 #[must_use]
247 pub fn new(motion_threshold: f32, min_motion_area: f32) -> Self {
248 Self {
249 motion_threshold,
250 min_motion_area,
251 }
252 }
253
254 pub fn detect(
259 &self,
260 frames: &[&[u8]],
261 width: usize,
262 height: usize,
263 ) -> AlignResult<Vec<SyncMarker>> {
264 if frames.len() < 2 {
265 return Err(AlignError::InsufficientData(
266 "Need at least 2 frames".to_string(),
267 ));
268 }
269
270 let mut markers = Vec::new();
271
272 for i in 1..frames.len() {
273 let motion = self.compute_motion(frames[i - 1], frames[i], width, height);
274
275 if motion > self.min_motion_area {
276 markers.push(SyncMarker::new(i, MarkerType::ClapperClosure, motion, None));
277 }
278 }
279
280 Ok(markers)
281 }
282
283 fn compute_motion(&self, frame1: &[u8], frame2: &[u8], width: usize, height: usize) -> f32 {
285 let mut motion_pixels = 0;
286 let total_pixels = width * height;
287
288 for i in 0..total_pixels {
289 let idx = i * 3;
290 if idx + 2 < frame1.len() && idx + 2 < frame2.len() {
291 let diff_r = (i16::from(frame1[idx]) - i16::from(frame2[idx])).abs();
292 let diff_g = (i16::from(frame1[idx + 1]) - i16::from(frame2[idx + 1])).abs();
293 let diff_b = (i16::from(frame1[idx + 2]) - i16::from(frame2[idx + 2])).abs();
294
295 let diff = (diff_r + diff_g + diff_b) / 3;
296
297 if f32::from(diff) > self.motion_threshold {
298 motion_pixels += 1;
299 }
300 }
301 }
302
303 motion_pixels as f32 / total_pixels as f32
304 }
305}
306
307pub struct LedMarkerDetector {
309 pub expected_color: [f32; 3],
311 pub color_tolerance: f32,
313 pub min_blob_size: usize,
315}
316
317impl Default for LedMarkerDetector {
318 fn default() -> Self {
319 Self {
320 expected_color: [1.0, 0.0, 0.0], color_tolerance: 0.2,
322 min_blob_size: 10,
323 }
324 }
325}
326
327impl LedMarkerDetector {
328 #[must_use]
330 pub fn new(color: [f32; 3], tolerance: f32) -> Self {
331 Self {
332 expected_color: color,
333 color_tolerance: tolerance,
334 min_blob_size: 10,
335 }
336 }
337
338 #[must_use]
340 pub fn detect(&self, frame: &[u8], width: usize, height: usize) -> Vec<SyncMarker> {
341 let mut markers = Vec::new();
342
343 let mut visited = vec![false; width * height];
345
346 for y in 0..height {
347 for x in 0..width {
348 let idx = y * width + x;
349 if !visited[idx] && self.is_led_color(frame, width, x, y) {
350 let blob = self.flood_fill(frame, width, height, x, y, &mut visited);
351
352 if blob.len() >= self.min_blob_size {
353 let center = self.compute_centroid(&blob);
354 markers.push(SyncMarker::new(0, MarkerType::LedMarker, 1.0, Some(center)));
355 }
356 }
357 }
358 }
359
360 markers
361 }
362
363 fn is_led_color(&self, frame: &[u8], width: usize, x: usize, y: usize) -> bool {
365 let idx = (y * width + x) * 3;
366 if idx + 2 >= frame.len() {
367 return false;
368 }
369
370 let r = f32::from(frame[idx]) / 255.0;
371 let g = f32::from(frame[idx + 1]) / 255.0;
372 let b = f32::from(frame[idx + 2]) / 255.0;
373
374 let diff_r = (r - self.expected_color[0]).abs();
375 let diff_g = (g - self.expected_color[1]).abs();
376 let diff_b = (b - self.expected_color[2]).abs();
377
378 diff_r < self.color_tolerance
379 && diff_g < self.color_tolerance
380 && diff_b < self.color_tolerance
381 }
382
383 fn flood_fill(
385 &self,
386 frame: &[u8],
387 width: usize,
388 height: usize,
389 start_x: usize,
390 start_y: usize,
391 visited: &mut [bool],
392 ) -> Vec<Point2D> {
393 let mut blob = Vec::new();
394 let mut stack = vec![(start_x, start_y)];
395
396 while let Some((x, y)) = stack.pop() {
397 let idx = y * width + x;
398
399 if visited[idx] {
400 continue;
401 }
402
403 if !self.is_led_color(frame, width, x, y) {
404 continue;
405 }
406
407 visited[idx] = true;
408 blob.push(Point2D::new(x as f64, y as f64));
409
410 if x > 0 {
412 stack.push((x - 1, y));
413 }
414 if x + 1 < width {
415 stack.push((x + 1, y));
416 }
417 if y > 0 {
418 stack.push((x, y - 1));
419 }
420 if y + 1 < height {
421 stack.push((x, y + 1));
422 }
423 }
424
425 blob
426 }
427
428 fn compute_centroid(&self, blob: &[Point2D]) -> Point2D {
430 let n = blob.len() as f64;
431 let sum_x: f64 = blob.iter().map(|p| p.x).sum();
432 let sum_y: f64 = blob.iter().map(|p| p.y).sum();
433
434 Point2D::new(sum_x / n, sum_y / n)
435 }
436}
437
438pub struct AudioSpikeDetector {
440 pub threshold: f32,
442 pub window_size: usize,
444}
445
446impl Default for AudioSpikeDetector {
447 fn default() -> Self {
448 Self {
449 threshold: 0.8,
450 window_size: 512,
451 }
452 }
453}
454
455impl AudioSpikeDetector {
456 #[must_use]
458 pub fn new(threshold: f32, window_size: usize) -> Self {
459 Self {
460 threshold,
461 window_size,
462 }
463 }
464
465 #[must_use]
467 pub fn detect(&self, audio: &[f32], sample_rate: u32) -> Vec<SyncMarker> {
468 let mut markers = Vec::new();
469
470 let envelope = self.compute_envelope(audio);
472
473 for i in 1..envelope.len().saturating_sub(1) {
475 if envelope[i] > self.threshold
476 && envelope[i] > envelope[i - 1]
477 && envelope[i] > envelope[i + 1]
478 {
479 let frame = (i * 24) / sample_rate as usize;
481
482 markers.push(SyncMarker::new(
483 frame,
484 MarkerType::AudioSpike,
485 envelope[i],
486 None,
487 ));
488 }
489 }
490
491 markers
492 }
493
494 fn compute_envelope(&self, audio: &[f32]) -> Vec<f32> {
496 let mut envelope = Vec::new();
497
498 for chunk in audio.chunks(self.window_size) {
499 let max = chunk.iter().map(|&x| x.abs()).fold(0.0f32, f32::max);
500 envelope.push(max);
501 }
502
503 envelope
504 }
505}
506
507#[derive(Default)]
509pub struct TimecodeDetector {
510 pub region: Option<Region>,
512}
513
514impl TimecodeDetector {
515 #[must_use]
517 pub fn new(region: Option<Region>) -> Self {
518 Self { region }
519 }
520
521 #[must_use]
523 pub fn detect(&self, frame: &[u8], width: usize, height: usize) -> Option<SyncMarker> {
524 let region = self
525 .region
526 .unwrap_or_else(|| Region::new(0, height.saturating_sub(100), width, 100));
527
528 let contrast = self.compute_contrast(frame, width, ®ion);
530
531 if contrast > 0.5 {
532 Some(SyncMarker::new(
533 0,
534 MarkerType::TimecodeDisplay,
535 contrast,
536 Some(Point2D::new(
537 (region.x + region.width / 2) as f64,
538 (region.y + region.height / 2) as f64,
539 )),
540 ))
541 } else {
542 None
543 }
544 }
545
546 fn compute_contrast(&self, frame: &[u8], width: usize, region: &Region) -> f32 {
548 let mut min_val = 255u8;
549 let mut max_val = 0u8;
550
551 for y in region.y..region.y + region.height {
552 for x in region.x..region.x + region.width {
553 let idx = (y * width + x) * 3;
554 if idx < frame.len() {
555 let gray = ((u16::from(frame[idx])
556 + u16::from(frame[idx + 1])
557 + u16::from(frame[idx + 2]))
558 / 3) as u8;
559 min_val = min_val.min(gray);
560 max_val = max_val.max(gray);
561 }
562 }
563 }
564
565 f32::from(max_val - min_val) / 255.0
566 }
567}
568
569#[derive(Debug, Clone, Copy, PartialEq, Eq)]
575pub enum InterpolationMethod {
576 Linear,
578 Cubic,
581 Bezier,
583}
584
585#[allow(dead_code)]
595#[must_use]
596pub fn interpolate_markers(anchors: &[SyncMarker], method: InterpolationMethod) -> Vec<SyncMarker> {
597 if anchors.len() < 2 {
598 return Vec::new();
599 }
600
601 let mut sorted = anchors.to_vec();
603 sorted.sort_by_key(|m| m.frame);
604
605 let first = sorted[0].frame;
606 let last = sorted[sorted.len() - 1].frame;
607 if first >= last {
608 return Vec::new();
609 }
610
611 let total = last - first + 1;
612 let mut result = Vec::with_capacity(total);
613
614 let xs: Vec<f64> = sorted.iter().map(|m| m.frame as f64).collect();
616 let cs: Vec<f64> = sorted.iter().map(|m| f64::from(m.confidence)).collect();
617 let lx: Vec<f64> = sorted
619 .iter()
620 .map(|m| m.location.map_or(f64::NAN, |p| p.x))
621 .collect();
622 let ly: Vec<f64> = sorted
623 .iter()
624 .map(|m| m.location.map_or(f64::NAN, |p| p.y))
625 .collect();
626
627 let marker_type = sorted[0].marker_type;
628
629 for frame in first..=last {
630 let t = frame as f64;
631
632 let (conf, px, py) = match method {
633 InterpolationMethod::Linear => {
634 let (c, x, y) = interpolate_linear(&xs, &cs, &lx, &ly, t);
635 (c, x, y)
636 }
637 InterpolationMethod::Cubic => {
638 let (c, x, y) = interpolate_cubic(&xs, &cs, &lx, &ly, t);
639 (c, x, y)
640 }
641 InterpolationMethod::Bezier => {
642 let (c, x, y) = interpolate_bezier(&xs, &cs, &lx, &ly, t);
643 (c, x, y)
644 }
645 };
646
647 let location = if px.is_finite() && py.is_finite() {
648 Some(Point2D::new(px, py))
649 } else {
650 None
651 };
652
653 result.push(SyncMarker::new(frame, marker_type, conf as f32, location));
654 }
655
656 result
657}
658
659fn interpolate_linear(xs: &[f64], cs: &[f64], lx: &[f64], ly: &[f64], t: f64) -> (f64, f64, f64) {
661 for i in 0..xs.len().saturating_sub(1) {
663 if t >= xs[i] && t <= xs[i + 1] {
664 let alpha = (t - xs[i]) / (xs[i + 1] - xs[i]);
665 let c = cs[i] + alpha * (cs[i + 1] - cs[i]);
666 let x = lerp_nan(lx[i], lx[i + 1], alpha);
667 let y = lerp_nan(ly[i], ly[i + 1], alpha);
668 return (c, x, y);
669 }
670 }
671 (cs[cs.len() - 1], lx[lx.len() - 1], ly[ly.len() - 1])
672}
673
674fn interpolate_cubic(xs: &[f64], cs: &[f64], lx: &[f64], ly: &[f64], t: f64) -> (f64, f64, f64) {
676 if xs.len() < 4 {
677 return interpolate_linear(xs, cs, lx, ly, t);
678 }
679
680 for i in 0..xs.len().saturating_sub(1) {
681 if t >= xs[i] && t <= xs[i + 1] {
682 let alpha = (t - xs[i]) / (xs[i + 1] - xs[i]);
683
684 let i0 = i.saturating_sub(1);
685 let i1 = i;
686 let i2 = (i + 1).min(xs.len() - 1);
687 let i3 = (i + 2).min(xs.len() - 1);
688
689 let c = catmull_rom(cs[i0], cs[i1], cs[i2], cs[i3], alpha);
690 let x = catmull_rom_nan(lx[i0], lx[i1], lx[i2], lx[i3], alpha);
691 let y = catmull_rom_nan(ly[i0], ly[i1], ly[i2], ly[i3], alpha);
692 return (c.clamp(0.0, 1.0), x, y);
693 }
694 }
695 (cs[cs.len() - 1], lx[lx.len() - 1], ly[ly.len() - 1])
696}
697
698fn interpolate_bezier(xs: &[f64], cs: &[f64], lx: &[f64], ly: &[f64], t: f64) -> (f64, f64, f64) {
702 if xs.len() < 3 {
703 return interpolate_linear(xs, cs, lx, ly, t);
704 }
705
706 for i in 0..xs.len().saturating_sub(1) {
707 if t >= xs[i] && t <= xs[i + 1] {
708 let alpha = (t - xs[i]) / (xs[i + 1] - xs[i]);
709
710 let prev_c = if i == 0 { cs[i] } else { cs[i - 1] };
712 let next_c = if i + 2 < cs.len() {
713 cs[i + 2]
714 } else {
715 cs[i + 1]
716 };
717 let cp1_c = cs[i] + (cs[i + 1] - prev_c) / 6.0;
718 let cp2_c = cs[i + 1] - (next_c - cs[i]) / 6.0;
719 let c = cubic_bezier(cs[i], cp1_c, cp2_c, cs[i + 1], alpha).clamp(0.0, 1.0);
720
721 let x = bezier_nan(lx, i, alpha);
722 let y = bezier_nan(ly, i, alpha);
723 return (c, x, y);
724 }
725 }
726 (cs[cs.len() - 1], lx[lx.len() - 1], ly[ly.len() - 1])
727}
728
729fn lerp_nan(a: f64, b: f64, t: f64) -> f64 {
732 if a.is_nan() || b.is_nan() {
733 f64::NAN
734 } else {
735 a + t * (b - a)
736 }
737}
738
739fn catmull_rom(p0: f64, p1: f64, p2: f64, p3: f64, t: f64) -> f64 {
740 0.5 * ((2.0 * p1)
741 + (-p0 + p2) * t
742 + (2.0 * p0 - 5.0 * p1 + 4.0 * p2 - p3) * t * t
743 + (-p0 + 3.0 * p1 - 3.0 * p2 + p3) * t * t * t)
744}
745
746fn catmull_rom_nan(p0: f64, p1: f64, p2: f64, p3: f64, t: f64) -> f64 {
747 if p0.is_nan() || p1.is_nan() || p2.is_nan() || p3.is_nan() {
748 f64::NAN
749 } else {
750 catmull_rom(p0, p1, p2, p3, t)
751 }
752}
753
754fn cubic_bezier(p0: f64, p1: f64, p2: f64, p3: f64, t: f64) -> f64 {
755 let mt = 1.0 - t;
756 mt * mt * mt * p0 + 3.0 * mt * mt * t * p1 + 3.0 * mt * t * t * p2 + t * t * t * p3
757}
758
759fn bezier_nan(vals: &[f64], i: usize, t: f64) -> f64 {
760 if vals[i].is_nan() || vals[i + 1].is_nan() {
761 f64::NAN
762 } else {
763 let prev = if i == 0 { vals[i] } else { vals[i - 1] };
764 let next = if i + 2 < vals.len() {
765 vals[i + 2]
766 } else {
767 vals[i + 1]
768 };
769 let cp1 = vals[i] + (vals[i + 1] - prev) / 6.0;
770 let cp2 = vals[i + 1] - (next - vals[i]) / 6.0;
771 cubic_bezier(vals[i], cp1, cp2, vals[i + 1], t)
772 }
773}
774
775#[allow(dead_code)]
784#[must_use]
785pub fn cluster_markers(markers: &[SyncMarker], max_gap_frames: usize) -> Vec<Vec<SyncMarker>> {
786 if markers.is_empty() {
787 return Vec::new();
788 }
789
790 let mut sorted = markers.to_vec();
791 sorted.sort_by_key(|m| m.frame);
792
793 let mut clusters: Vec<Vec<SyncMarker>> = Vec::new();
794 let mut current: Vec<SyncMarker> = vec![sorted[0].clone()];
795
796 for m in sorted.into_iter().skip(1) {
797 let last_frame = current
799 .last()
800 .expect("current cluster is always non-empty")
801 .frame;
802 if m.frame.saturating_sub(last_frame) <= max_gap_frames {
803 current.push(m);
804 } else {
805 clusters.push(current);
806 current = vec![m];
807 }
808 }
809 clusters.push(current);
810
811 clusters
812}
813
814#[allow(dead_code)]
816#[must_use]
817pub fn cluster_best_markers(markers: &[SyncMarker], max_gap_frames: usize) -> Vec<SyncMarker> {
818 cluster_markers(markers, max_gap_frames)
819 .into_iter()
820 .filter_map(|cluster| {
821 cluster.into_iter().max_by(|a, b| {
822 a.confidence
823 .partial_cmp(&b.confidence)
824 .unwrap_or(std::cmp::Ordering::Equal)
825 })
826 })
827 .collect()
828}
829
830#[derive(Debug, Clone)]
836pub struct TemporalAlignment {
837 pub frame_offset: i64,
839 pub confidence: f32,
841 pub matched_pairs: usize,
843}
844
845#[allow(dead_code)]
853#[must_use]
854pub fn align_markers_temporal(
855 reference: &[SyncMarker],
856 target: &[SyncMarker],
857 tolerance_frames: usize,
858 search_range: i64,
859) -> Option<TemporalAlignment> {
860 if reference.is_empty() || target.is_empty() {
861 return None;
862 }
863
864 let mut best_offset = 0i64;
865 let mut best_matches = 0usize;
866 let mut best_conf = 0.0f32;
867
868 let mut best_total_dist = u64::MAX;
869
870 for delta in -search_range..=search_range {
871 let mut matches = 0usize;
872 let mut conf_sum = 0.0f32;
873 let mut total_dist = 0u64;
874
875 for ref_marker in reference {
876 let shifted_frame = ref_marker.frame as i64 - delta;
877 if let Some(closest) = target
879 .iter()
880 .min_by_key(|m| (m.frame as i64 - shifted_frame).unsigned_abs())
881 {
882 let dist = (closest.frame as i64 - shifted_frame).unsigned_abs() as usize;
883 if dist <= tolerance_frames {
884 matches += 1;
885 conf_sum += ref_marker.confidence * closest.confidence;
886 total_dist += dist as u64;
887 }
888 }
889 }
890
891 let is_better = matches > best_matches
892 || (matches == best_matches && conf_sum > best_conf)
893 || (matches == best_matches
894 && (conf_sum - best_conf).abs() < 1e-6
895 && total_dist < best_total_dist);
896
897 if is_better {
898 best_matches = matches;
899 best_conf = conf_sum;
900 best_total_dist = total_dist;
901 best_offset = delta;
902 }
903 }
904
905 if best_matches == 0 {
906 return None;
907 }
908
909 let avg_conf = best_conf / best_matches as f32;
910
911 Some(TemporalAlignment {
912 frame_offset: best_offset,
913 confidence: avg_conf,
914 matched_pairs: best_matches,
915 })
916}
917
918pub struct MultiMarkerSync {
920 flash: FlashDetector,
922 clapper: ClapperDetector,
924 audio: AudioSpikeDetector,
926}
927
928impl Default for MultiMarkerSync {
929 fn default() -> Self {
930 Self::new()
931 }
932}
933
934impl MultiMarkerSync {
935 #[must_use]
937 pub fn new() -> Self {
938 Self {
939 flash: FlashDetector::default(),
940 clapper: ClapperDetector::default(),
941 audio: AudioSpikeDetector::default(),
942 }
943 }
944
945 pub fn detect_all(
950 &self,
951 video_frames: &[&[u8]],
952 width: usize,
953 height: usize,
954 audio: &[f32],
955 sample_rate: u32,
956 ) -> AlignResult<Vec<SyncMarker>> {
957 let mut markers = Vec::new();
958
959 markers.extend(self.flash.detect(video_frames, width, height));
961 markers.extend(self.clapper.detect(video_frames, width, height)?);
962
963 markers.extend(self.audio.detect(audio, sample_rate));
965
966 markers.sort_by(|a, b| {
968 a.frame.cmp(&b.frame).then_with(|| {
969 b.confidence
970 .partial_cmp(&a.confidence)
971 .unwrap_or(std::cmp::Ordering::Equal)
972 })
973 });
974
975 Ok(markers)
976 }
977
978 #[must_use]
980 pub fn find_best_marker<'a>(&self, markers: &'a [SyncMarker]) -> Option<&'a SyncMarker> {
981 markers.iter().max_by(|a, b| {
982 a.confidence
983 .partial_cmp(&b.confidence)
984 .unwrap_or(std::cmp::Ordering::Equal)
985 })
986 }
987}
988
989#[cfg(test)]
990mod tests {
991 use super::*;
992
993 #[test]
996 fn test_interpolate_linear_count() {
997 let anchors = vec![
998 SyncMarker::new(0, MarkerType::Flash, 0.8, None),
999 SyncMarker::new(10, MarkerType::Flash, 0.6, None),
1000 ];
1001 let result = interpolate_markers(&anchors, InterpolationMethod::Linear);
1002 assert_eq!(result.len(), 11); }
1004
1005 #[test]
1006 fn test_interpolate_linear_endpoints() {
1007 let anchors = vec![
1008 SyncMarker::new(0, MarkerType::Flash, 1.0, None),
1009 SyncMarker::new(10, MarkerType::Flash, 0.0, None),
1010 ];
1011 let result = interpolate_markers(&anchors, InterpolationMethod::Linear);
1012 assert!((result[0].confidence - 1.0).abs() < 1e-5);
1014 assert!((result[10].confidence).abs() < 1e-5);
1016 }
1017
1018 #[test]
1019 fn test_interpolate_cubic_count() {
1020 let anchors = vec![
1021 SyncMarker::new(0, MarkerType::Flash, 1.0, None),
1022 SyncMarker::new(5, MarkerType::Flash, 0.8, None),
1023 SyncMarker::new(10, MarkerType::Flash, 0.9, None),
1024 SyncMarker::new(15, MarkerType::Flash, 0.6, None),
1025 ];
1026 let result = interpolate_markers(&anchors, InterpolationMethod::Cubic);
1027 assert_eq!(result.len(), 16);
1028 }
1029
1030 #[test]
1031 fn test_interpolate_bezier_count() {
1032 let anchors = vec![
1033 SyncMarker::new(0, MarkerType::Flash, 0.9, None),
1034 SyncMarker::new(4, MarkerType::Flash, 0.7, None),
1035 SyncMarker::new(8, MarkerType::Flash, 0.8, None),
1036 ];
1037 let result = interpolate_markers(&anchors, InterpolationMethod::Bezier);
1038 assert_eq!(result.len(), 9);
1039 }
1040
1041 #[test]
1042 fn test_interpolate_empty_returns_empty() {
1043 let result = interpolate_markers(&[], InterpolationMethod::Linear);
1044 assert!(result.is_empty());
1045 }
1046
1047 #[test]
1048 fn test_interpolate_single_returns_empty() {
1049 let result = interpolate_markers(
1050 &[SyncMarker::new(5, MarkerType::Flash, 0.9, None)],
1051 InterpolationMethod::Linear,
1052 );
1053 assert!(result.is_empty());
1054 }
1055
1056 #[test]
1057 fn test_interpolate_with_locations() {
1058 let loc_a = Some(Point2D::new(0.0, 0.0));
1059 let loc_b = Some(Point2D::new(10.0, 20.0));
1060 let anchors = vec![
1061 SyncMarker::new(0, MarkerType::Flash, 1.0, loc_a),
1062 SyncMarker::new(10, MarkerType::Flash, 1.0, loc_b),
1063 ];
1064 let result = interpolate_markers(&anchors, InterpolationMethod::Linear);
1065 let mid = &result[5];
1066 let loc = mid.location.expect("loc should be valid");
1067 assert!((loc.x - 5.0).abs() < 0.5);
1068 assert!((loc.y - 10.0).abs() < 1.0);
1069 }
1070
1071 #[test]
1074 fn test_cluster_markers_two_clusters() {
1075 let markers = vec![
1076 SyncMarker::new(0, MarkerType::Flash, 0.9, None),
1077 SyncMarker::new(2, MarkerType::Flash, 0.8, None),
1078 SyncMarker::new(100, MarkerType::Flash, 0.7, None),
1079 SyncMarker::new(102, MarkerType::Flash, 0.6, None),
1080 ];
1081 let clusters = cluster_markers(&markers, 5);
1082 assert_eq!(clusters.len(), 2);
1083 }
1084
1085 #[test]
1086 fn test_cluster_markers_one_cluster() {
1087 let markers = vec![
1088 SyncMarker::new(0, MarkerType::Flash, 0.9, None),
1089 SyncMarker::new(3, MarkerType::Flash, 0.8, None),
1090 SyncMarker::new(6, MarkerType::Flash, 0.7, None),
1091 ];
1092 let clusters = cluster_markers(&markers, 5);
1093 assert_eq!(clusters.len(), 1);
1094 }
1095
1096 #[test]
1097 fn test_cluster_markers_empty() {
1098 let clusters = cluster_markers(&[], 5);
1099 assert!(clusters.is_empty());
1100 }
1101
1102 #[test]
1103 fn test_cluster_best_markers_picks_highest_confidence() {
1104 let markers = vec![
1105 SyncMarker::new(0, MarkerType::Flash, 0.5, None),
1106 SyncMarker::new(2, MarkerType::Flash, 0.9, None),
1107 SyncMarker::new(100, MarkerType::Flash, 0.3, None),
1108 ];
1109 let best = cluster_best_markers(&markers, 5);
1110 assert_eq!(best.len(), 2);
1111 assert!((best[0].confidence - 0.9).abs() < 1e-5);
1113 }
1114
1115 #[test]
1116 fn test_cluster_markers_single() {
1117 let markers = vec![SyncMarker::new(42, MarkerType::AudioSpike, 1.0, None)];
1118 let clusters = cluster_markers(&markers, 5);
1119 assert_eq!(clusters.len(), 1);
1120 assert_eq!(clusters[0].len(), 1);
1121 }
1122
1123 #[test]
1126 fn test_align_markers_perfect_match() {
1127 let reference = vec![
1128 SyncMarker::new(10, MarkerType::Flash, 1.0, None),
1129 SyncMarker::new(50, MarkerType::Flash, 1.0, None),
1130 ];
1131 let target = vec![
1133 SyncMarker::new(15, MarkerType::Flash, 1.0, None),
1134 SyncMarker::new(55, MarkerType::Flash, 1.0, None),
1135 ];
1136 let alignment =
1137 align_markers_temporal(&reference, &target, 2, 20).expect("alignment should be valid");
1138 assert_eq!(alignment.frame_offset, -5);
1139 assert_eq!(alignment.matched_pairs, 2);
1140 }
1141
1142 #[test]
1143 fn test_align_markers_no_match() {
1144 let reference = vec![SyncMarker::new(0, MarkerType::Flash, 1.0, None)];
1145 let target = vec![SyncMarker::new(1000, MarkerType::Flash, 1.0, None)];
1146 let result = align_markers_temporal(&reference, &target, 2, 10);
1148 assert!(result.is_none());
1149 }
1150
1151 #[test]
1152 fn test_align_markers_empty_inputs() {
1153 let reference: Vec<SyncMarker> = vec![];
1154 let target = vec![SyncMarker::new(10, MarkerType::Flash, 1.0, None)];
1155 assert!(align_markers_temporal(&reference, &target, 5, 20).is_none());
1156 }
1157
1158 #[test]
1159 fn test_align_markers_confidence_nonzero() {
1160 let reference = vec![SyncMarker::new(5, MarkerType::Flash, 0.8, None)];
1161 let target = vec![SyncMarker::new(5, MarkerType::Flash, 0.9, None)];
1162 let result =
1163 align_markers_temporal(&reference, &target, 1, 5).expect("result should be valid");
1164 assert!(result.confidence > 0.0);
1165 }
1166
1167 #[test]
1168 fn test_temporal_alignment_fields() {
1169 let reference = vec![SyncMarker::new(0, MarkerType::Flash, 1.0, None)];
1170 let target = vec![SyncMarker::new(3, MarkerType::Flash, 1.0, None)];
1171 let result =
1172 align_markers_temporal(&reference, &target, 5, 10).expect("result should be valid");
1173 assert_eq!(result.matched_pairs, 1);
1174 assert!(result.confidence > 0.0);
1175 }
1176
1177 #[test]
1180 fn test_sync_marker_creation() {
1181 let marker = SyncMarker::new(100, MarkerType::Flash, 0.95, None);
1182 assert_eq!(marker.frame, 100);
1183 assert_eq!(marker.marker_type, MarkerType::Flash);
1184 assert_eq!(marker.confidence, 0.95);
1185 }
1186
1187 #[test]
1188 fn test_flash_detector() {
1189 let detector = FlashDetector::default();
1190 assert_eq!(detector.threshold, 0.8);
1191 assert_eq!(detector.min_duration, 1);
1192 }
1193
1194 #[test]
1195 fn test_brightness_computation() {
1196 let detector = FlashDetector::default();
1197 let frame = vec![255u8; 300]; let brightness = detector.compute_brightness(&frame, 10, 10);
1199 assert!((brightness - 1.0).abs() < 0.01);
1200 }
1201
1202 #[test]
1203 fn test_region_creation() {
1204 let region = Region::new(10, 20, 100, 200);
1205 assert_eq!(region.x, 10);
1206 assert_eq!(region.y, 20);
1207 assert_eq!(region.width, 100);
1208 assert_eq!(region.height, 200);
1209 }
1210
1211 #[test]
1212 fn test_clapper_detector() {
1213 let detector = ClapperDetector::default();
1214 let frame1 = vec![100u8; 300];
1215 let frame2 = vec![200u8; 300];
1216 let motion = detector.compute_motion(&frame1, &frame2, 10, 10);
1217 assert!(motion > 0.0);
1218 }
1219
1220 #[test]
1221 fn test_led_marker_detector() {
1222 let detector = LedMarkerDetector::new([1.0, 0.0, 0.0], 0.2);
1223 assert_eq!(detector.expected_color[0], 1.0);
1224 assert_eq!(detector.color_tolerance, 0.2);
1225 }
1226
1227 #[test]
1228 fn test_audio_spike_detector() {
1229 let detector = AudioSpikeDetector::new(0.8, 50);
1231 let mut audio = vec![0.0f32; 1000];
1232 audio[500] = 1.0; let markers = detector.detect(&audio, 48000);
1234 assert!(!markers.is_empty());
1235 }
1236
1237 #[test]
1238 fn test_audio_envelope() {
1239 let detector = AudioSpikeDetector::new(0.5, 100);
1240 let audio = vec![0.5f32; 1000];
1241 let envelope = detector.compute_envelope(&audio);
1242 assert!(!envelope.is_empty());
1243 assert!((envelope[0] - 0.5).abs() < 0.01);
1244 }
1245
1246 #[test]
1247 fn test_multi_marker_sync() {
1248 let sync = MultiMarkerSync::new();
1249 let marker1 = SyncMarker::new(100, MarkerType::Flash, 0.8, None);
1250 let marker2 = SyncMarker::new(101, MarkerType::AudioSpike, 0.9, None);
1251 let markers = vec![marker1, marker2];
1252
1253 let best = sync.find_best_marker(&markers);
1254 assert!(best.is_some());
1255 assert_eq!(best.expect("test expectation failed").confidence, 0.9);
1256 }
1257}