Skip to main content

oximedia_timecode/
tc_smpte_ranges.rs

1//! SMPTE timecode range definitions and boundary checking.
2//!
3//! Provides [`SmpteRange`] to represent a closed interval of timecodes and
4//! utilities for checking whether a timecode falls inside a particular
5//! SMPTE-defined boundary (e.g. a valid programme segment).
6
7#![allow(dead_code)]
8
9use crate::{FrameRate, Timecode};
10
11// -- SmpteBoundary -----------------------------------------------------------
12
13/// A named SMPTE boundary definition.
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum SmpteBoundary {
16    /// Standard 24-hour day (00:00:00:00 .. 23:59:59:ff).
17    FullDay,
18    /// First 12 hours (00:00:00:00 .. 11:59:59:ff).
19    FirstHalf,
20    /// Second 12 hours (12:00:00:00 .. 23:59:59:ff).
21    SecondHalf,
22    /// A single hour starting at `hour`.
23    SingleHour(u8),
24}
25
26impl std::fmt::Display for SmpteBoundary {
27    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
28        match self {
29            Self::FullDay => write!(f, "full-day"),
30            Self::FirstHalf => write!(f, "first-half"),
31            Self::SecondHalf => write!(f, "second-half"),
32            Self::SingleHour(h) => write!(f, "hour-{h:02}"),
33        }
34    }
35}
36
37// -- SmpteRange --------------------------------------------------------------
38
39/// A closed timecode range `[start, end]` expressed in total frames.
40#[derive(Debug, Clone, Copy, PartialEq, Eq)]
41pub struct SmpteRange {
42    /// Inclusive start position in frames since midnight.
43    pub start_frames: u64,
44    /// Inclusive end position in frames since midnight.
45    pub end_frames: u64,
46    /// Frame-rate context for display / validation.
47    pub fps: u8,
48}
49
50impl SmpteRange {
51    /// Create a range from two `Timecode` values.
52    pub fn from_timecodes(start: &Timecode, end: &Timecode) -> Self {
53        Self {
54            start_frames: start.to_frames(),
55            end_frames: end.to_frames(),
56            fps: start.frame_rate.fps,
57        }
58    }
59
60    /// Create a range from raw frame positions.
61    pub fn from_frames(start: u64, end: u64, fps: u8) -> Self {
62        Self {
63            start_frames: start,
64            end_frames: end,
65            fps,
66        }
67    }
68
69    /// Create a range covering the full 24-hour day for a given frame rate.
70    pub fn full_day(frame_rate: FrameRate) -> Self {
71        let fps = frame_rate.frames_per_second();
72        let max_frame = (fps as u64) * 86400 - 1;
73        Self {
74            start_frames: 0,
75            end_frames: max_frame,
76            fps: fps as u8,
77        }
78    }
79
80    /// Create a range covering a named SMPTE boundary.
81    pub fn from_boundary(boundary: SmpteBoundary, frame_rate: FrameRate) -> Self {
82        let fps = frame_rate.frames_per_second() as u64;
83        match boundary {
84            SmpteBoundary::FullDay => Self::from_frames(0, fps * 86400 - 1, fps as u8),
85            SmpteBoundary::FirstHalf => Self::from_frames(0, fps * 43200 - 1, fps as u8),
86            SmpteBoundary::SecondHalf => Self::from_frames(fps * 43200, fps * 86400 - 1, fps as u8),
87            SmpteBoundary::SingleHour(h) => {
88                let start = fps * 3600 * h as u64;
89                let end = start + fps * 3600 - 1;
90                Self::from_frames(start, end, fps as u8)
91            }
92        }
93    }
94
95    /// Check whether a `Timecode` falls inside this range (inclusive).
96    pub fn contains(&self, tc: &Timecode) -> bool {
97        let pos = tc.to_frames();
98        pos >= self.start_frames && pos <= self.end_frames
99    }
100
101    /// Duration of the range in frames (inclusive).
102    pub fn duration_frames(&self) -> u64 {
103        if self.end_frames >= self.start_frames {
104            self.end_frames - self.start_frames + 1
105        } else {
106            0
107        }
108    }
109
110    /// Duration in seconds (approximate, based on fps).
111    #[allow(clippy::cast_precision_loss)]
112    pub fn duration_seconds(&self) -> f64 {
113        self.duration_frames() as f64 / self.fps as f64
114    }
115
116    /// Check whether two ranges overlap.
117    pub fn overlaps(&self, other: &SmpteRange) -> bool {
118        self.start_frames <= other.end_frames && other.start_frames <= self.end_frames
119    }
120
121    /// Compute the intersection of two ranges. Returns `None` if they do not overlap.
122    pub fn intersection(&self, other: &SmpteRange) -> Option<SmpteRange> {
123        if !self.overlaps(other) {
124            return None;
125        }
126        Some(SmpteRange {
127            start_frames: self.start_frames.max(other.start_frames),
128            end_frames: self.end_frames.min(other.end_frames),
129            fps: self.fps,
130        })
131    }
132
133    /// Return `true` if this range entirely contains `other`.
134    pub fn encompasses(&self, other: &SmpteRange) -> bool {
135        self.start_frames <= other.start_frames && self.end_frames >= other.end_frames
136    }
137
138    /// Split the range at a given frame position into two sub-ranges.
139    /// The split point becomes the last frame of the first range and the
140    /// first frame of the second range is `split_at + 1`.
141    pub fn split_at(&self, split_at: u64) -> Option<(SmpteRange, SmpteRange)> {
142        if split_at < self.start_frames || split_at >= self.end_frames {
143            return None;
144        }
145        let left = SmpteRange::from_frames(self.start_frames, split_at, self.fps);
146        let right = SmpteRange::from_frames(split_at + 1, self.end_frames, self.fps);
147        Some((left, right))
148    }
149}
150
151// -- BoundaryChecker ---------------------------------------------------------
152
153/// Checks timecodes against a set of allowed SMPTE ranges.
154#[derive(Debug, Clone)]
155pub struct BoundaryChecker {
156    /// Allowed ranges.
157    ranges: Vec<SmpteRange>,
158}
159
160impl BoundaryChecker {
161    /// Create a checker with no allowed ranges (everything is out-of-bounds).
162    pub fn new() -> Self {
163        Self { ranges: Vec::new() }
164    }
165
166    /// Add an allowed range.
167    pub fn add_range(&mut self, range: SmpteRange) {
168        self.ranges.push(range);
169    }
170
171    /// Create a checker from a slice of ranges.
172    pub fn from_ranges(ranges: &[SmpteRange]) -> Self {
173        Self {
174            ranges: ranges.to_vec(),
175        }
176    }
177
178    /// Check whether a timecode is inside any of the allowed ranges.
179    pub fn is_allowed(&self, tc: &Timecode) -> bool {
180        self.ranges.iter().any(|r| r.contains(tc))
181    }
182
183    /// Return how many allowed ranges contain the given timecode.
184    pub fn matching_ranges(&self, tc: &Timecode) -> usize {
185        self.ranges.iter().filter(|r| r.contains(tc)).count()
186    }
187}
188
189impl Default for BoundaryChecker {
190    fn default() -> Self {
191        Self::new()
192    }
193}
194
195// -- helper ------------------------------------------------------------------
196
197/// Build a `Timecode` from raw fields (bypasses constructor checks).
198fn raw_tc(hours: u8, minutes: u8, seconds: u8, frames: u8, fps: u8) -> Timecode {
199    Timecode::from_raw_fields(hours, minutes, seconds, frames, fps, false, 0)
200}
201
202// -- Tests -------------------------------------------------------------------
203
204#[cfg(test)]
205mod tests {
206    use super::*;
207
208    fn tc25(h: u8, m: u8, s: u8, f: u8) -> Timecode {
209        Timecode::new(h, m, s, f, FrameRate::Fps25).expect("valid timecode")
210    }
211
212    #[test]
213    fn test_range_from_timecodes() {
214        let start = tc25(1, 0, 0, 0);
215        let end = tc25(1, 0, 10, 0);
216        let range = SmpteRange::from_timecodes(&start, &end);
217        assert!(range.start_frames < range.end_frames);
218        assert_eq!(range.fps, 25);
219    }
220
221    #[test]
222    fn test_range_contains_inside() {
223        let range = SmpteRange::from_frames(100, 200, 25);
224        let tc = raw_tc(0, 0, 6, 0, 25); // 150 frames
225        assert!(range.contains(&tc));
226    }
227
228    #[test]
229    fn test_range_contains_outside() {
230        let range = SmpteRange::from_frames(100, 200, 25);
231        let tc = raw_tc(0, 0, 0, 5, 25); // 5 frames
232        assert!(!range.contains(&tc));
233    }
234
235    #[test]
236    fn test_range_contains_on_boundary() {
237        let range = SmpteRange::from_frames(100, 200, 25);
238        let tc = raw_tc(0, 0, 4, 0, 25); // 100 frames
239        assert!(range.contains(&tc));
240    }
241
242    #[test]
243    fn test_duration_frames() {
244        let range = SmpteRange::from_frames(0, 99, 25);
245        assert_eq!(range.duration_frames(), 100);
246    }
247
248    #[test]
249    fn test_duration_seconds() {
250        let range = SmpteRange::from_frames(0, 49, 25);
251        let dur = range.duration_seconds();
252        assert!((dur - 2.0).abs() < 1e-6);
253    }
254
255    #[test]
256    fn test_full_day_range() {
257        let range = SmpteRange::full_day(FrameRate::Fps25);
258        assert_eq!(range.start_frames, 0);
259        assert_eq!(range.duration_frames(), 25 * 86400);
260    }
261
262    #[test]
263    fn test_from_boundary_first_half() {
264        let range = SmpteRange::from_boundary(SmpteBoundary::FirstHalf, FrameRate::Fps25);
265        assert_eq!(range.start_frames, 0);
266        assert_eq!(range.duration_frames(), 25 * 43200);
267    }
268
269    #[test]
270    fn test_from_boundary_single_hour() {
271        let range = SmpteRange::from_boundary(SmpteBoundary::SingleHour(2), FrameRate::Fps25);
272        assert_eq!(range.start_frames, 25 * 3600 * 2);
273        assert_eq!(range.duration_frames(), 25 * 3600);
274    }
275
276    #[test]
277    fn test_overlaps_true() {
278        let a = SmpteRange::from_frames(0, 100, 25);
279        let b = SmpteRange::from_frames(50, 150, 25);
280        assert!(a.overlaps(&b));
281        assert!(b.overlaps(&a));
282    }
283
284    #[test]
285    fn test_overlaps_false() {
286        let a = SmpteRange::from_frames(0, 50, 25);
287        let b = SmpteRange::from_frames(100, 200, 25);
288        assert!(!a.overlaps(&b));
289    }
290
291    #[test]
292    fn test_intersection_some() {
293        let a = SmpteRange::from_frames(0, 100, 25);
294        let b = SmpteRange::from_frames(50, 150, 25);
295        let inter = a.intersection(&b).expect("intersection should succeed");
296        assert_eq!(inter.start_frames, 50);
297        assert_eq!(inter.end_frames, 100);
298    }
299
300    #[test]
301    fn test_intersection_none() {
302        let a = SmpteRange::from_frames(0, 50, 25);
303        let b = SmpteRange::from_frames(100, 200, 25);
304        assert!(a.intersection(&b).is_none());
305    }
306
307    #[test]
308    fn test_encompasses() {
309        let outer = SmpteRange::from_frames(0, 1000, 25);
310        let inner = SmpteRange::from_frames(100, 500, 25);
311        assert!(outer.encompasses(&inner));
312        assert!(!inner.encompasses(&outer));
313    }
314
315    #[test]
316    fn test_split_at() {
317        let range = SmpteRange::from_frames(0, 200, 25);
318        let (left, right) = range.split_at(100).expect("split should succeed");
319        assert_eq!(left.end_frames, 100);
320        assert_eq!(right.start_frames, 101);
321    }
322
323    #[test]
324    fn test_split_at_invalid() {
325        let range = SmpteRange::from_frames(100, 200, 25);
326        assert!(range.split_at(50).is_none());
327        assert!(range.split_at(200).is_none());
328    }
329
330    #[test]
331    fn test_boundary_checker_allowed() {
332        let range = SmpteRange::from_frames(0, 1000, 25);
333        let checker = BoundaryChecker::from_ranges(&[range]);
334        let tc = tc25(0, 0, 1, 0);
335        assert!(checker.is_allowed(&tc));
336    }
337
338    #[test]
339    fn test_boundary_checker_not_allowed() {
340        let range = SmpteRange::from_frames(0, 10, 25);
341        let checker = BoundaryChecker::from_ranges(&[range]);
342        let tc = tc25(1, 0, 0, 0);
343        assert!(!checker.is_allowed(&tc));
344    }
345
346    #[test]
347    fn test_boundary_checker_matching_ranges() {
348        let r1 = SmpteRange::from_frames(0, 1000, 25);
349        let r2 = SmpteRange::from_frames(500, 2000, 25);
350        let checker = BoundaryChecker::from_ranges(&[r1, r2]);
351        let tc = raw_tc(0, 0, 24, 0, 25); // 600 frames
352        assert_eq!(checker.matching_ranges(&tc), 2);
353    }
354
355    #[test]
356    fn test_boundary_display() {
357        assert_eq!(SmpteBoundary::FullDay.to_string(), "full-day");
358        assert_eq!(SmpteBoundary::SingleHour(5).to_string(), "hour-05");
359    }
360
361    #[test]
362    fn test_empty_range_duration() {
363        let range = SmpteRange::from_frames(200, 100, 25);
364        assert_eq!(range.duration_frames(), 0);
365    }
366}