#![allow(dead_code)]
use crate::{FrameRate, Timecode, TimecodeError};
const MAX_MINUTES: usize = 24 * 60;
#[derive(Debug, Clone)]
pub struct OffsetTable {
rate: FrameRate,
minute_offsets: Vec<u64>,
fps: u64,
}
impl OffsetTable {
#[allow(clippy::cast_precision_loss)]
pub fn build(rate: FrameRate) -> Self {
let fps = rate.frames_per_second() as u64;
let is_df = rate.is_drop_frame();
let mut offsets = Vec::with_capacity(MAX_MINUTES);
let mut cumulative: u64 = 0;
for m in 0..MAX_MINUTES {
offsets.push(cumulative);
let frames_in_minute = fps * 60;
if is_df && m > 0 {
let next_m = m + 1;
if next_m % 10 != 0 {
cumulative += frames_in_minute - 2;
} else {
cumulative += frames_in_minute;
}
} else {
cumulative += frames_in_minute;
}
}
Self {
rate,
minute_offsets: offsets,
fps,
}
}
pub fn timecode_to_frame(&self, tc: &Timecode) -> Result<u64, TimecodeError> {
let minute_idx = tc.hours as usize * 60 + tc.minutes as usize;
if minute_idx >= MAX_MINUTES {
return Err(TimecodeError::InvalidHours);
}
let base = self.minute_offsets[minute_idx];
let extra = tc.seconds as u64 * self.fps + tc.frames as u64;
Ok(base + extra)
}
pub fn frame_to_timecode(&self, frame: u64) -> Result<Timecode, TimecodeError> {
let mut lo: usize = 0;
let mut hi: usize = self.minute_offsets.len();
while lo + 1 < hi {
let mid = (lo + hi) / 2;
if self.minute_offsets[mid] <= frame {
lo = mid;
} else {
hi = mid;
}
}
let minute_idx = lo;
let remaining = frame - self.minute_offsets[minute_idx];
let hours = (minute_idx / 60) as u8;
let minutes = (minute_idx % 60) as u8;
let seconds = (remaining / self.fps) as u8;
let frames = (remaining % self.fps) as u8;
Timecode::new(hours, minutes, seconds, frames, self.rate)
}
pub fn rate(&self) -> FrameRate {
self.rate
}
pub fn total_day_frames(&self) -> u64 {
let last_idx = MAX_MINUTES - 1;
let base = self.minute_offsets[last_idx];
if self.rate.is_drop_frame() {
base + self.fps * 60 - 2
} else {
base + self.fps * 60
}
}
pub fn minute_offset(&self, minute: usize) -> Option<u64> {
self.minute_offsets.get(minute).copied()
}
pub fn len(&self) -> usize {
self.minute_offsets.len()
}
pub fn is_empty(&self) -> bool {
self.minute_offsets.is_empty()
}
}
pub fn signed_frame_distance(
table: &OffsetTable,
a: &Timecode,
b: &Timecode,
) -> Result<i64, TimecodeError> {
let fa = table.timecode_to_frame(a)?;
let fb = table.timecode_to_frame(b)?;
Ok(fb as i64 - fa as i64)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_build_25fps() {
let table = OffsetTable::build(FrameRate::Fps25);
assert_eq!(table.len(), MAX_MINUTES);
assert_eq!(table.minute_offset(0), Some(0));
assert_eq!(table.minute_offset(1), Some(1500));
}
#[test]
fn test_roundtrip_ndf() {
let table = OffsetTable::build(FrameRate::Fps25);
let tc = Timecode::new(1, 30, 15, 12, FrameRate::Fps25).expect("valid timecode");
let frame = table.timecode_to_frame(&tc).expect("timecode should exist");
let tc2 = table
.frame_to_timecode(frame)
.expect("frame to timecode should succeed");
assert_eq!(tc.hours, tc2.hours);
assert_eq!(tc.minutes, tc2.minutes);
assert_eq!(tc.seconds, tc2.seconds);
assert_eq!(tc.frames, tc2.frames);
}
#[test]
fn test_roundtrip_30fps() {
let table = OffsetTable::build(FrameRate::Fps30);
let tc = Timecode::new(10, 45, 22, 18, FrameRate::Fps30).expect("valid timecode");
let frame = table.timecode_to_frame(&tc).expect("timecode should exist");
let tc2 = table
.frame_to_timecode(frame)
.expect("frame to timecode should succeed");
assert_eq!(tc.hours, tc2.hours);
assert_eq!(tc.minutes, tc2.minutes);
assert_eq!(tc.seconds, tc2.seconds);
assert_eq!(tc.frames, tc2.frames);
}
#[test]
fn test_zero_timecode() {
let table = OffsetTable::build(FrameRate::Fps25);
let tc = Timecode::new(0, 0, 0, 0, FrameRate::Fps25).expect("valid timecode");
assert_eq!(
table.timecode_to_frame(&tc).expect("timecode should exist"),
0
);
}
#[test]
fn test_total_day_frames_25() {
let table = OffsetTable::build(FrameRate::Fps25);
assert_eq!(table.total_day_frames(), 2_160_000);
}
#[test]
fn test_total_day_frames_30() {
let table = OffsetTable::build(FrameRate::Fps30);
assert_eq!(table.total_day_frames(), 2_592_000);
}
#[test]
fn test_signed_distance_positive() {
let table = OffsetTable::build(FrameRate::Fps25);
let a = Timecode::new(0, 0, 0, 0, FrameRate::Fps25).expect("valid timecode");
let b = Timecode::new(0, 0, 1, 0, FrameRate::Fps25).expect("valid timecode");
let dist = signed_frame_distance(&table, &a, &b).expect("signed distance should succeed");
assert_eq!(dist, 25);
}
#[test]
fn test_signed_distance_negative() {
let table = OffsetTable::build(FrameRate::Fps25);
let a = Timecode::new(0, 0, 1, 0, FrameRate::Fps25).expect("valid timecode");
let b = Timecode::new(0, 0, 0, 0, FrameRate::Fps25).expect("valid timecode");
let dist = signed_frame_distance(&table, &a, &b).expect("signed distance should succeed");
assert_eq!(dist, -25);
}
#[test]
fn test_table_is_not_empty() {
let table = OffsetTable::build(FrameRate::Fps24);
assert!(!table.is_empty());
}
#[test]
fn test_minute_offset_out_of_range() {
let table = OffsetTable::build(FrameRate::Fps25);
assert!(table.minute_offset(MAX_MINUTES).is_none());
}
#[test]
fn test_frame_to_timecode_minute_boundary() {
let table = OffsetTable::build(FrameRate::Fps25);
let tc = table
.frame_to_timecode(1500)
.expect("frame to timecode should succeed");
assert_eq!(tc.hours, 0);
assert_eq!(tc.minutes, 1);
assert_eq!(tc.seconds, 0);
assert_eq!(tc.frames, 0);
}
#[test]
fn test_rate_accessor() {
let table = OffsetTable::build(FrameRate::Fps50);
assert_eq!(table.rate(), FrameRate::Fps50);
}
}