1#![allow(dead_code)]
7#![allow(clippy::cast_precision_loss)]
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
11pub struct FrameTimecode {
12 pub frame_number: u64,
14 pub fps_num: u32,
16 pub fps_den: u32,
18}
19
20impl FrameTimecode {
21 #[must_use]
23 pub fn new(frame_number: u64, fps_num: u32, fps_den: u32) -> Self {
24 Self {
25 frame_number,
26 fps_num,
27 fps_den,
28 }
29 }
30
31 #[must_use]
33 pub fn from_hmsf(hours: u64, minutes: u64, seconds: u64, frames: u64, fps: u64) -> Self {
34 let total_frames = ((hours * 3600 + minutes * 60 + seconds) * fps) + frames;
35 Self {
36 frame_number: total_frames,
37 fps_num: fps as u32,
38 fps_den: 1,
39 }
40 }
41
42 #[must_use]
44 pub fn fps_f64(&self) -> f64 {
45 self.fps_num as f64 / self.fps_den as f64
46 }
47
48 #[must_use]
50 pub fn as_seconds(&self) -> f64 {
51 self.frame_number as f64 / self.fps_f64()
52 }
53
54 #[must_use]
58 pub fn frame_diff(&self, other: &Self) -> i64 {
59 self.frame_number as i64 - other.frame_number as i64
60 }
61}
62
63#[derive(Debug, Clone, PartialEq, Eq)]
65pub struct SyncPoint {
66 pub proxy_tc: FrameTimecode,
68 pub original_tc: FrameTimecode,
70 pub verified: bool,
72}
73
74impl SyncPoint {
75 #[must_use]
77 pub fn new(proxy_tc: FrameTimecode, original_tc: FrameTimecode) -> Self {
78 Self {
79 proxy_tc,
80 original_tc,
81 verified: false,
82 }
83 }
84
85 #[must_use]
87 pub fn verified(mut self) -> Self {
88 self.verified = true;
89 self
90 }
91
92 #[must_use]
94 pub fn frame_offset(&self) -> i64 {
95 self.proxy_tc.frame_number as i64 - self.original_tc.frame_number as i64
96 }
97}
98
99#[derive(Debug, Clone)]
101pub struct SyncVerificationResult {
102 pub clip_id: String,
104 pub in_sync: bool,
106 pub max_drift_frames: i64,
108 pub points_checked: usize,
110 pub points_passed: usize,
112}
113
114impl SyncVerificationResult {
115 #[must_use]
117 pub fn pass_rate(&self) -> f32 {
118 if self.points_checked == 0 {
119 return 1.0;
120 }
121 self.points_passed as f32 / self.points_checked as f32
122 }
123}
124
125#[derive(Debug, Clone, Copy)]
127pub struct SyncTolerance {
128 pub max_drift_frames: u32,
130 pub min_pass_rate: f32,
132}
133
134impl SyncTolerance {
135 #[must_use]
137 pub fn new(max_drift_frames: u32, min_pass_rate: f32) -> Self {
138 Self {
139 max_drift_frames,
140 min_pass_rate: min_pass_rate.clamp(0.0, 1.0),
141 }
142 }
143
144 #[must_use]
146 pub fn strict() -> Self {
147 Self::new(0, 1.0)
148 }
149
150 #[must_use]
152 pub fn lenient() -> Self {
153 Self::new(2, 0.9)
154 }
155}
156
157impl Default for SyncTolerance {
158 fn default() -> Self {
159 Self::new(1, 0.95)
160 }
161}
162
163#[allow(dead_code)]
165pub struct ProxySyncVerifier {
166 sync_points: Vec<SyncPoint>,
168 tolerance: SyncTolerance,
170}
171
172impl ProxySyncVerifier {
173 #[must_use]
175 pub fn new() -> Self {
176 Self {
177 sync_points: Vec::new(),
178 tolerance: SyncTolerance::default(),
179 }
180 }
181
182 #[must_use]
184 pub fn with_tolerance(mut self, tolerance: SyncTolerance) -> Self {
185 self.tolerance = tolerance;
186 self
187 }
188
189 pub fn add_sync_point(&mut self, point: SyncPoint) {
191 self.sync_points.push(point);
192 }
193
194 #[must_use]
196 pub fn verify(&self, clip_id: impl Into<String>) -> SyncVerificationResult {
197 let clip_id = clip_id.into();
198 if self.sync_points.is_empty() {
199 return SyncVerificationResult {
200 clip_id,
201 in_sync: true,
202 max_drift_frames: 0,
203 points_checked: 0,
204 points_passed: 0,
205 };
206 }
207
208 let mut max_drift = 0i64;
209 let mut passed = 0usize;
210
211 for sp in &self.sync_points {
212 let drift = sp.frame_offset().abs();
213 if drift > max_drift {
214 max_drift = drift;
215 }
216 if drift <= self.tolerance.max_drift_frames as i64 {
217 passed += 1;
218 }
219 }
220
221 let total = self.sync_points.len();
222 let pass_rate = passed as f32 / total as f32;
223
224 SyncVerificationResult {
225 clip_id,
226 in_sync: pass_rate >= self.tolerance.min_pass_rate,
227 max_drift_frames: max_drift,
228 points_checked: total,
229 points_passed: passed,
230 }
231 }
232
233 #[must_use]
235 pub fn point_count(&self) -> usize {
236 self.sync_points.len()
237 }
238}
239
240impl Default for ProxySyncVerifier {
241 fn default() -> Self {
242 Self::new()
243 }
244}
245
246#[allow(dead_code)]
248pub struct TimecodeAligner {
249 offset_frames: i64,
251 fps_num: u32,
253 fps_den: u32,
255}
256
257impl TimecodeAligner {
258 #[must_use]
260 pub fn new(offset_frames: i64, fps_num: u32, fps_den: u32) -> Self {
261 Self {
262 offset_frames,
263 fps_num,
264 fps_den,
265 }
266 }
267
268 #[must_use]
270 pub fn zero(fps_num: u32, fps_den: u32) -> Self {
271 Self::new(0, fps_num, fps_den)
272 }
273
274 #[must_use]
276 pub fn original_to_proxy(&self, original_frame: u64) -> u64 {
277 (original_frame as i64 + self.offset_frames).max(0) as u64
278 }
279
280 #[must_use]
282 pub fn proxy_to_original(&self, proxy_frame: u64) -> u64 {
283 (proxy_frame as i64 - self.offset_frames).max(0) as u64
284 }
285
286 #[must_use]
288 pub fn offset_frames(&self) -> i64 {
289 self.offset_frames
290 }
291
292 #[must_use]
294 pub fn offset_seconds(&self) -> f64 {
295 self.offset_frames as f64 / (self.fps_num as f64 / self.fps_den as f64)
296 }
297}
298
299#[cfg(test)]
300mod tests {
301 use super::*;
302
303 #[test]
304 fn test_frame_timecode_fps_f64() {
305 let tc = FrameTimecode::new(0, 24, 1);
306 assert!((tc.fps_f64() - 24.0).abs() < 1e-10);
307 }
308
309 #[test]
310 fn test_frame_timecode_from_hmsf() {
311 let tc = FrameTimecode::from_hmsf(1, 0, 0, 0, 24);
313 assert_eq!(tc.frame_number, 86400);
314 }
315
316 #[test]
317 fn test_frame_timecode_as_seconds() {
318 let tc = FrameTimecode::new(240, 24, 1);
319 assert!((tc.as_seconds() - 10.0).abs() < 1e-10);
320 }
321
322 #[test]
323 fn test_frame_timecode_frame_diff() {
324 let tc1 = FrameTimecode::new(100, 24, 1);
325 let tc2 = FrameTimecode::new(95, 24, 1);
326 assert_eq!(tc1.frame_diff(&tc2), 5);
327 assert_eq!(tc2.frame_diff(&tc1), -5);
328 }
329
330 #[test]
331 fn test_sync_point_frame_offset_zero() {
332 let tc = FrameTimecode::new(100, 24, 1);
333 let sp = SyncPoint::new(tc, tc);
334 assert_eq!(sp.frame_offset(), 0);
335 }
336
337 #[test]
338 fn test_sync_point_frame_offset_nonzero() {
339 let proxy_tc = FrameTimecode::new(105, 24, 1);
340 let orig_tc = FrameTimecode::new(100, 24, 1);
341 let sp = SyncPoint::new(proxy_tc, orig_tc);
342 assert_eq!(sp.frame_offset(), 5);
343 }
344
345 #[test]
346 fn test_sync_point_verified() {
347 let tc = FrameTimecode::new(100, 24, 1);
348 let sp = SyncPoint::new(tc, tc).verified();
349 assert!(sp.verified);
350 }
351
352 #[test]
353 fn test_sync_tolerance_default() {
354 let t = SyncTolerance::default();
355 assert_eq!(t.max_drift_frames, 1);
356 assert!((t.min_pass_rate - 0.95).abs() < 1e-5);
357 }
358
359 #[test]
360 fn test_sync_tolerance_strict() {
361 let t = SyncTolerance::strict();
362 assert_eq!(t.max_drift_frames, 0);
363 assert!((t.min_pass_rate - 1.0).abs() < 1e-5);
364 }
365
366 #[test]
367 fn test_proxy_sync_verifier_empty() {
368 let verifier = ProxySyncVerifier::new();
369 let result = verifier.verify("clip001");
370 assert!(result.in_sync);
371 assert_eq!(result.points_checked, 0);
372 }
373
374 #[test]
375 fn test_proxy_sync_verifier_all_in_sync() {
376 let mut verifier = ProxySyncVerifier::new();
377 let tc = FrameTimecode::new(100, 24, 1);
378 verifier.add_sync_point(SyncPoint::new(tc, tc));
379 verifier.add_sync_point(SyncPoint::new(
380 FrameTimecode::new(200, 24, 1),
381 FrameTimecode::new(200, 24, 1),
382 ));
383 let result = verifier.verify("clip001");
384 assert!(result.in_sync);
385 assert_eq!(result.max_drift_frames, 0);
386 }
387
388 #[test]
389 fn test_proxy_sync_verifier_drift_exceeds_tolerance() {
390 let mut verifier = ProxySyncVerifier::new().with_tolerance(SyncTolerance::strict());
391 verifier.add_sync_point(SyncPoint::new(
392 FrameTimecode::new(105, 24, 1),
393 FrameTimecode::new(100, 24, 1),
394 ));
395 let result = verifier.verify("clip001");
396 assert!(!result.in_sync);
397 assert_eq!(result.max_drift_frames, 5);
398 }
399
400 #[test]
401 fn test_timecode_aligner_original_to_proxy() {
402 let aligner = TimecodeAligner::new(10, 24, 1);
403 assert_eq!(aligner.original_to_proxy(100), 110);
404 }
405
406 #[test]
407 fn test_timecode_aligner_proxy_to_original() {
408 let aligner = TimecodeAligner::new(10, 24, 1);
409 assert_eq!(aligner.proxy_to_original(110), 100);
410 }
411
412 #[test]
413 fn test_timecode_aligner_zero() {
414 let aligner = TimecodeAligner::zero(25, 1);
415 assert_eq!(aligner.original_to_proxy(500), 500);
416 assert_eq!(aligner.proxy_to_original(500), 500);
417 }
418
419 #[test]
420 fn test_timecode_aligner_offset_seconds() {
421 let aligner = TimecodeAligner::new(48, 24, 1); assert!((aligner.offset_seconds() - 2.0).abs() < 1e-10);
423 }
424
425 #[test]
426 fn test_sync_verification_pass_rate() {
427 let result = SyncVerificationResult {
428 clip_id: "c1".to_string(),
429 in_sync: true,
430 max_drift_frames: 0,
431 points_checked: 10,
432 points_passed: 9,
433 };
434 assert!((result.pass_rate() - 0.9).abs() < 1e-5);
435 }
436}