oximedia_timecode/
tc_offset_table.rs1#![allow(dead_code)]
2use crate::{FrameRate, Timecode, TimecodeError};
11
12const MAX_MINUTES: usize = 24 * 60;
14
15#[derive(Debug, Clone)]
17pub struct OffsetTable {
18 rate: FrameRate,
20 minute_offsets: Vec<u64>,
22 fps: u64,
24}
25
26impl OffsetTable {
27 #[allow(clippy::cast_precision_loss)]
31 pub fn build(rate: FrameRate) -> Self {
32 let fps = rate.frames_per_second() as u64;
33 let is_df = rate.is_drop_frame();
34 let mut offsets = Vec::with_capacity(MAX_MINUTES);
35
36 let mut cumulative: u64 = 0;
37 for m in 0..MAX_MINUTES {
38 offsets.push(cumulative);
39 let frames_in_minute = fps * 60;
41 if is_df && m > 0 {
42 let next_m = m + 1;
45 if next_m % 10 != 0 {
46 cumulative += frames_in_minute - 2;
47 } else {
48 cumulative += frames_in_minute;
49 }
50 } else {
51 cumulative += frames_in_minute;
52 }
53 }
54
55 Self {
56 rate,
57 minute_offsets: offsets,
58 fps,
59 }
60 }
61
62 pub fn timecode_to_frame(&self, tc: &Timecode) -> Result<u64, TimecodeError> {
68 let minute_idx = tc.hours as usize * 60 + tc.minutes as usize;
69 if minute_idx >= MAX_MINUTES {
70 return Err(TimecodeError::InvalidHours);
71 }
72 let base = self.minute_offsets[minute_idx];
73 let extra = tc.seconds as u64 * self.fps + tc.frames as u64;
74 Ok(base + extra)
75 }
76
77 pub fn frame_to_timecode(&self, frame: u64) -> Result<Timecode, TimecodeError> {
86 let mut lo: usize = 0;
88 let mut hi: usize = self.minute_offsets.len();
89 while lo + 1 < hi {
90 let mid = (lo + hi) / 2;
91 if self.minute_offsets[mid] <= frame {
92 lo = mid;
93 } else {
94 hi = mid;
95 }
96 }
97 let minute_idx = lo;
98 let remaining = frame - self.minute_offsets[minute_idx];
99
100 let hours = (minute_idx / 60) as u8;
101 let minutes = (minute_idx % 60) as u8;
102 let seconds = (remaining / self.fps) as u8;
103 let frames = (remaining % self.fps) as u8;
104
105 Timecode::new(hours, minutes, seconds, frames, self.rate)
106 }
107
108 pub fn rate(&self) -> FrameRate {
110 self.rate
111 }
112
113 pub fn total_day_frames(&self) -> u64 {
115 let last_idx = MAX_MINUTES - 1;
116 let base = self.minute_offsets[last_idx];
117 if self.rate.is_drop_frame() {
119 base + self.fps * 60 - 2
121 } else {
122 base + self.fps * 60
123 }
124 }
125
126 pub fn minute_offset(&self, minute: usize) -> Option<u64> {
128 self.minute_offsets.get(minute).copied()
129 }
130
131 pub fn len(&self) -> usize {
133 self.minute_offsets.len()
134 }
135
136 pub fn is_empty(&self) -> bool {
138 self.minute_offsets.is_empty()
139 }
140}
141
142pub fn signed_frame_distance(
146 table: &OffsetTable,
147 a: &Timecode,
148 b: &Timecode,
149) -> Result<i64, TimecodeError> {
150 let fa = table.timecode_to_frame(a)?;
151 let fb = table.timecode_to_frame(b)?;
152 Ok(fb as i64 - fa as i64)
153}
154
155#[cfg(test)]
156mod tests {
157 use super::*;
158
159 #[test]
160 fn test_build_25fps() {
161 let table = OffsetTable::build(FrameRate::Fps25);
162 assert_eq!(table.len(), MAX_MINUTES);
163 assert_eq!(table.minute_offset(0), Some(0));
164 assert_eq!(table.minute_offset(1), Some(1500));
166 }
167
168 #[test]
169 fn test_roundtrip_ndf() {
170 let table = OffsetTable::build(FrameRate::Fps25);
171 let tc = Timecode::new(1, 30, 15, 12, FrameRate::Fps25).expect("valid timecode");
172 let frame = table.timecode_to_frame(&tc).expect("timecode should exist");
173 let tc2 = table
174 .frame_to_timecode(frame)
175 .expect("frame to timecode should succeed");
176 assert_eq!(tc.hours, tc2.hours);
177 assert_eq!(tc.minutes, tc2.minutes);
178 assert_eq!(tc.seconds, tc2.seconds);
179 assert_eq!(tc.frames, tc2.frames);
180 }
181
182 #[test]
183 fn test_roundtrip_30fps() {
184 let table = OffsetTable::build(FrameRate::Fps30);
185 let tc = Timecode::new(10, 45, 22, 18, FrameRate::Fps30).expect("valid timecode");
186 let frame = table.timecode_to_frame(&tc).expect("timecode should exist");
187 let tc2 = table
188 .frame_to_timecode(frame)
189 .expect("frame to timecode should succeed");
190 assert_eq!(tc.hours, tc2.hours);
191 assert_eq!(tc.minutes, tc2.minutes);
192 assert_eq!(tc.seconds, tc2.seconds);
193 assert_eq!(tc.frames, tc2.frames);
194 }
195
196 #[test]
197 fn test_zero_timecode() {
198 let table = OffsetTable::build(FrameRate::Fps25);
199 let tc = Timecode::new(0, 0, 0, 0, FrameRate::Fps25).expect("valid timecode");
200 assert_eq!(
201 table.timecode_to_frame(&tc).expect("timecode should exist"),
202 0
203 );
204 }
205
206 #[test]
207 fn test_total_day_frames_25() {
208 let table = OffsetTable::build(FrameRate::Fps25);
209 assert_eq!(table.total_day_frames(), 2_160_000);
211 }
212
213 #[test]
214 fn test_total_day_frames_30() {
215 let table = OffsetTable::build(FrameRate::Fps30);
216 assert_eq!(table.total_day_frames(), 2_592_000);
218 }
219
220 #[test]
221 fn test_signed_distance_positive() {
222 let table = OffsetTable::build(FrameRate::Fps25);
223 let a = Timecode::new(0, 0, 0, 0, FrameRate::Fps25).expect("valid timecode");
224 let b = Timecode::new(0, 0, 1, 0, FrameRate::Fps25).expect("valid timecode");
225 let dist = signed_frame_distance(&table, &a, &b).expect("signed distance should succeed");
226 assert_eq!(dist, 25);
227 }
228
229 #[test]
230 fn test_signed_distance_negative() {
231 let table = OffsetTable::build(FrameRate::Fps25);
232 let a = Timecode::new(0, 0, 1, 0, FrameRate::Fps25).expect("valid timecode");
233 let b = Timecode::new(0, 0, 0, 0, FrameRate::Fps25).expect("valid timecode");
234 let dist = signed_frame_distance(&table, &a, &b).expect("signed distance should succeed");
235 assert_eq!(dist, -25);
236 }
237
238 #[test]
239 fn test_table_is_not_empty() {
240 let table = OffsetTable::build(FrameRate::Fps24);
241 assert!(!table.is_empty());
242 }
243
244 #[test]
245 fn test_minute_offset_out_of_range() {
246 let table = OffsetTable::build(FrameRate::Fps25);
247 assert!(table.minute_offset(MAX_MINUTES).is_none());
248 }
249
250 #[test]
251 fn test_frame_to_timecode_minute_boundary() {
252 let table = OffsetTable::build(FrameRate::Fps25);
253 let tc = table
255 .frame_to_timecode(1500)
256 .expect("frame to timecode should succeed");
257 assert_eq!(tc.hours, 0);
258 assert_eq!(tc.minutes, 1);
259 assert_eq!(tc.seconds, 0);
260 assert_eq!(tc.frames, 0);
261 }
262
263 #[test]
264 fn test_rate_accessor() {
265 let table = OffsetTable::build(FrameRate::Fps50);
266 assert_eq!(table.rate(), FrameRate::Fps50);
267 }
268}