Skip to main content

oximedia_timecode/
continuity.rs

1//! Timecode continuity checking and gap detection.
2//!
3//! Provides tools for detecting discontinuities, gaps, and overlaps in timecode streams.
4
5#![allow(dead_code)]
6#![allow(clippy::cast_precision_loss)]
7
8use crate::{FrameRate, Timecode, TimecodeError};
9
10/// A detected gap or discontinuity in a timecode sequence.
11#[derive(Debug, Clone, PartialEq, Eq)]
12pub struct TimecodegGap {
13    /// Timecode immediately before the gap.
14    pub before: Timecode,
15    /// Timecode immediately after the gap.
16    pub after: Timecode,
17    /// Size of the gap in frames (positive = gap, negative = overlap).
18    pub gap_frames: i64,
19}
20
21/// Result of a continuity check on a single transition.
22#[derive(Debug, Clone, PartialEq, Eq)]
23pub enum ContinuityResult {
24    /// Timecodes are exactly one frame apart (continuous).
25    Continuous,
26    /// A gap of N frames was detected.
27    Gap(u64),
28    /// An overlap (reverse discontinuity) of N frames was detected.
29    Overlap(u64),
30    /// Same timecode repeated.
31    Repeat,
32}
33
34/// Check continuity between two consecutive timecodes.
35pub fn check_continuity(prev: &Timecode, next: &Timecode) -> ContinuityResult {
36    let prev_f = prev.to_frames();
37    let next_f = next.to_frames();
38
39    match next_f.cmp(&(prev_f + 1)) {
40        std::cmp::Ordering::Equal => ContinuityResult::Continuous,
41        std::cmp::Ordering::Greater => ContinuityResult::Gap(next_f - prev_f - 1),
42        std::cmp::Ordering::Less => {
43            if next_f == prev_f {
44                ContinuityResult::Repeat
45            } else {
46                ContinuityResult::Overlap(prev_f + 1 - next_f)
47            }
48        }
49    }
50}
51
52/// Continuity monitor for a stream of timecodes.
53#[derive(Debug, Clone)]
54pub struct ContinuityMonitor {
55    frame_rate: FrameRate,
56    last_tc: Option<Timecode>,
57    gaps: Vec<TimecodegGap>,
58    gap_count: u32,
59    overlap_count: u32,
60    repeat_count: u32,
61    frame_count: u64,
62}
63
64impl ContinuityMonitor {
65    /// Create a new continuity monitor.
66    pub fn new(frame_rate: FrameRate) -> Self {
67        Self {
68            frame_rate,
69            last_tc: None,
70            gaps: Vec::new(),
71            gap_count: 0,
72            overlap_count: 0,
73            repeat_count: 0,
74            frame_count: 0,
75        }
76    }
77
78    /// Feed a timecode to the monitor and return the continuity result.
79    pub fn feed(&mut self, tc: Timecode) -> ContinuityResult {
80        self.frame_count += 1;
81        let result = if let Some(ref last) = self.last_tc {
82            let r = check_continuity(last, &tc);
83            match &r {
84                ContinuityResult::Gap(n) => {
85                    self.gap_count += 1;
86                    self.gaps.push(TimecodegGap {
87                        before: *last,
88                        after: tc,
89                        gap_frames: *n as i64,
90                    });
91                }
92                ContinuityResult::Overlap(n) => {
93                    self.overlap_count += 1;
94                    self.gaps.push(TimecodegGap {
95                        before: *last,
96                        after: tc,
97                        gap_frames: -(*n as i64),
98                    });
99                }
100                ContinuityResult::Repeat => {
101                    self.repeat_count += 1;
102                }
103                ContinuityResult::Continuous => {}
104            }
105            r
106        } else {
107            ContinuityResult::Continuous
108        };
109        self.last_tc = Some(tc);
110        result
111    }
112
113    /// Get number of gaps detected.
114    pub fn gap_count(&self) -> u32 {
115        self.gap_count
116    }
117
118    /// Get number of overlaps detected.
119    pub fn overlap_count(&self) -> u32 {
120        self.overlap_count
121    }
122
123    /// Get number of repeated timecodes.
124    pub fn repeat_count(&self) -> u32 {
125        self.repeat_count
126    }
127
128    /// Get total frames processed.
129    pub fn frame_count(&self) -> u64 {
130        self.frame_count
131    }
132
133    /// Get all recorded gaps and overlaps.
134    pub fn gaps(&self) -> &[TimecodegGap] {
135        &self.gaps
136    }
137
138    /// Reset the monitor state.
139    pub fn reset(&mut self) {
140        self.last_tc = None;
141        self.gaps.clear();
142        self.gap_count = 0;
143        self.overlap_count = 0;
144        self.repeat_count = 0;
145        self.frame_count = 0;
146    }
147
148    /// Get the last seen timecode.
149    pub fn last_timecode(&self) -> Option<&Timecode> {
150        self.last_tc.as_ref()
151    }
152
153    /// Generate a summary report string.
154    pub fn report(&self) -> String {
155        format!(
156            "Frames: {}, Gaps: {}, Overlaps: {}, Repeats: {}",
157            self.frame_count, self.gap_count, self.overlap_count, self.repeat_count
158        )
159    }
160}
161
162/// Expected frame count between two timecodes.
163pub fn expected_frame_count(start: &Timecode, end: &Timecode) -> u64 {
164    let s = start.to_frames();
165    let e = end.to_frames();
166    e.saturating_sub(s)
167}
168
169/// Find all gaps in a slice of timecodes.
170pub fn find_gaps(timecodes: &[Timecode]) -> Vec<TimecodegGap> {
171    let mut gaps = Vec::new();
172    for window in timecodes.windows(2) {
173        let result = check_continuity(&window[0], &window[1]);
174        match result {
175            ContinuityResult::Gap(n) => {
176                gaps.push(TimecodegGap {
177                    before: window[0],
178                    after: window[1],
179                    gap_frames: n as i64,
180                });
181            }
182            ContinuityResult::Overlap(n) => {
183                gaps.push(TimecodegGap {
184                    before: window[0],
185                    after: window[1],
186                    gap_frames: -(n as i64),
187                });
188            }
189            _ => {}
190        }
191    }
192    gaps
193}
194
195/// Timecode range representing a continuous segment.
196#[derive(Debug, Clone, PartialEq, Eq)]
197pub struct TimecodeRange {
198    /// Start of the range.
199    pub start: Timecode,
200    /// End of the range (inclusive).
201    pub end: Timecode,
202}
203
204impl TimecodeRange {
205    /// Create a new range.
206    pub fn new(start: Timecode, end: Timecode) -> Result<Self, TimecodeError> {
207        if end.to_frames() < start.to_frames() {
208            return Err(TimecodeError::InvalidConfiguration);
209        }
210        Ok(Self { start, end })
211    }
212
213    /// Get the duration in frames.
214    pub fn duration_frames(&self) -> u64 {
215        self.end.to_frames().saturating_sub(self.start.to_frames()) + 1
216    }
217
218    /// Check if a timecode falls within this range.
219    pub fn contains(&self, tc: &Timecode) -> bool {
220        let f = tc.to_frames();
221        f >= self.start.to_frames() && f <= self.end.to_frames()
222    }
223
224    /// Check if two ranges overlap.
225    pub fn overlaps(&self, other: &TimecodeRange) -> bool {
226        self.start.to_frames() <= other.end.to_frames()
227            && other.start.to_frames() <= self.end.to_frames()
228    }
229}
230
231/// Split a slice of timecodes into continuous segments.
232pub fn split_into_segments(timecodes: &[Timecode]) -> Vec<Vec<Timecode>> {
233    if timecodes.is_empty() {
234        return Vec::new();
235    }
236
237    let mut segments = Vec::new();
238    let mut current = vec![timecodes[0]];
239
240    for window in timecodes.windows(2) {
241        match check_continuity(&window[0], &window[1]) {
242            ContinuityResult::Continuous => {
243                current.push(window[1]);
244            }
245            _ => {
246                segments.push(current);
247                current = vec![window[1]];
248            }
249        }
250    }
251    segments.push(current);
252    segments
253}
254
255#[cfg(test)]
256mod tests {
257    use super::*;
258
259    fn tc(h: u8, m: u8, s: u8, f: u8) -> Timecode {
260        Timecode::new(h, m, s, f, FrameRate::Fps25).expect("valid timecode")
261    }
262
263    #[test]
264    fn test_check_continuity_continuous() {
265        let a = tc(0, 0, 0, 0);
266        let b = tc(0, 0, 0, 1);
267        assert_eq!(check_continuity(&a, &b), ContinuityResult::Continuous);
268    }
269
270    #[test]
271    fn test_check_continuity_gap() {
272        let a = tc(0, 0, 0, 0);
273        let b = tc(0, 0, 0, 5);
274        assert_eq!(check_continuity(&a, &b), ContinuityResult::Gap(4));
275    }
276
277    #[test]
278    fn test_check_continuity_overlap() {
279        let a = tc(0, 0, 0, 5);
280        let b = tc(0, 0, 0, 3);
281        assert_eq!(check_continuity(&a, &b), ContinuityResult::Overlap(3));
282    }
283
284    #[test]
285    fn test_check_continuity_repeat() {
286        let a = tc(0, 0, 0, 5);
287        let b = tc(0, 0, 0, 5);
288        assert_eq!(check_continuity(&a, &b), ContinuityResult::Repeat);
289    }
290
291    #[test]
292    fn test_monitor_continuous() {
293        let mut mon = ContinuityMonitor::new(FrameRate::Fps25);
294        for f in 0u8..10 {
295            let t = tc(0, 0, 0, f);
296            mon.feed(t);
297        }
298        assert_eq!(mon.gap_count(), 0);
299        assert_eq!(mon.frame_count(), 10);
300    }
301
302    #[test]
303    fn test_monitor_gap_detection() {
304        let mut mon = ContinuityMonitor::new(FrameRate::Fps25);
305        mon.feed(tc(0, 0, 0, 0));
306        mon.feed(tc(0, 0, 0, 5)); // gap of 4
307        assert_eq!(mon.gap_count(), 1);
308        assert_eq!(mon.gaps()[0].gap_frames, 4);
309    }
310
311    #[test]
312    fn test_monitor_reset() {
313        let mut mon = ContinuityMonitor::new(FrameRate::Fps25);
314        mon.feed(tc(0, 0, 0, 0));
315        mon.feed(tc(0, 0, 0, 5));
316        mon.reset();
317        assert_eq!(mon.gap_count(), 0);
318        assert_eq!(mon.frame_count(), 0);
319        assert!(mon.last_timecode().is_none());
320    }
321
322    #[test]
323    fn test_monitor_report() {
324        let mut mon = ContinuityMonitor::new(FrameRate::Fps25);
325        mon.feed(tc(0, 0, 0, 0));
326        let report = mon.report();
327        assert!(report.contains("Frames: 1"));
328    }
329
330    #[test]
331    fn test_find_gaps() {
332        let tcs = vec![
333            tc(0, 0, 0, 0),
334            tc(0, 0, 0, 1),
335            tc(0, 0, 0, 5),
336            tc(0, 0, 0, 6),
337        ];
338        let gaps = find_gaps(&tcs);
339        assert_eq!(gaps.len(), 1);
340        assert_eq!(gaps[0].gap_frames, 3);
341    }
342
343    #[test]
344    fn test_timecode_range_contains() {
345        let start = tc(0, 0, 0, 0);
346        let end = tc(0, 0, 1, 0);
347        let range = TimecodeRange::new(start, end).expect("valid timecode range");
348        assert!(range.contains(&tc(0, 0, 0, 10)));
349        assert!(!range.contains(&tc(0, 0, 2, 0)));
350    }
351
352    #[test]
353    fn test_timecode_range_duration() {
354        let start = tc(0, 0, 0, 0);
355        let end = tc(0, 0, 0, 24);
356        let range = TimecodeRange::new(start, end).expect("valid timecode range");
357        assert_eq!(range.duration_frames(), 25);
358    }
359
360    #[test]
361    fn test_timecode_range_overlaps() {
362        let r1 = TimecodeRange::new(tc(0, 0, 0, 0), tc(0, 0, 0, 10)).expect("valid timecode range");
363        let r2 = TimecodeRange::new(tc(0, 0, 0, 5), tc(0, 0, 0, 20)).expect("valid timecode range");
364        let r3 = TimecodeRange::new(tc(0, 0, 1, 0), tc(0, 0, 1, 10)).expect("valid timecode range");
365        assert!(r1.overlaps(&r2));
366        assert!(!r1.overlaps(&r3));
367    }
368
369    #[test]
370    fn test_split_into_segments() {
371        let tcs = vec![
372            tc(0, 0, 0, 0),
373            tc(0, 0, 0, 1),
374            tc(0, 0, 0, 2),
375            tc(0, 0, 1, 0),
376            tc(0, 0, 1, 1),
377        ];
378        let segments = split_into_segments(&tcs);
379        assert_eq!(segments.len(), 2);
380        assert_eq!(segments[0].len(), 3);
381        assert_eq!(segments[1].len(), 2);
382    }
383
384    #[test]
385    fn test_expected_frame_count() {
386        let start = tc(0, 0, 0, 0);
387        let end = tc(0, 0, 1, 0);
388        assert_eq!(expected_frame_count(&start, &end), 25);
389    }
390}