#![allow(dead_code)]
use crate::{FrameRate, Timecode, TimecodeError};
#[derive(Debug, Clone, PartialEq)]
pub struct TimecodeRun {
pub start: Timecode,
pub frame_count: u64,
}
impl TimecodeRun {
pub fn new(start: Timecode, frame_count: u64) -> Result<Self, TimecodeError> {
if frame_count == 0 {
return Err(TimecodeError::InvalidConfiguration);
}
Ok(Self { start, frame_count })
}
#[allow(clippy::cast_precision_loss)]
pub fn duration_secs(&self) -> f64 {
let fps = self.start.frame_rate.fps as f64;
self.frame_count as f64 / fps
}
pub fn contains(&self, tc: &Timecode) -> bool {
if tc.frame_rate != self.start.frame_rate {
return false;
}
let start_f = self.start.to_frames();
let tc_f = tc.to_frames();
tc_f >= start_f && tc_f < start_f + self.frame_count
}
pub fn end_timecode(&self, rate: FrameRate) -> Result<Timecode, TimecodeError> {
let end_frame = self.start.to_frames() + self.frame_count - 1;
Timecode::from_frames(end_frame, rate)
}
}
#[derive(Debug, Clone)]
pub struct TimecodeSequence {
runs: Vec<TimecodeRun>,
total_frames: u64,
}
impl TimecodeSequence {
pub fn new() -> Self {
Self {
runs: Vec::new(),
total_frames: 0,
}
}
pub fn push(&mut self, run: TimecodeRun) {
self.total_frames += run.frame_count;
self.runs.push(run);
}
pub fn total_frames(&self) -> u64 {
self.total_frames
}
pub fn run_count(&self) -> usize {
self.runs.len()
}
pub fn get(&self, index: usize) -> Option<&TimecodeRun> {
self.runs.get(index)
}
pub fn is_contiguous(&self, a_idx: usize, b_idx: usize) -> bool {
let (Some(a), Some(b)) = (self.runs.get(a_idx), self.runs.get(b_idx)) else {
return false;
};
if a.start.frame_rate != b.start.frame_rate {
return false;
}
let a_end = a.start.to_frames() + a.frame_count;
let b_start = b.start.to_frames();
a_end == b_start
}
#[allow(clippy::cast_precision_loss)]
pub fn total_duration_secs(&self) -> f64 {
self.runs.iter().map(TimecodeRun::duration_secs).sum()
}
pub fn find_run_at_offset(&self, offset: u64) -> Option<(usize, u64)> {
let mut cumulative = 0u64;
for (i, run) in self.runs.iter().enumerate() {
if offset < cumulative + run.frame_count {
return Some((i, offset - cumulative));
}
cumulative += run.frame_count;
}
None
}
pub fn compact(&mut self) {
if self.runs.len() < 2 {
return;
}
let mut merged: Vec<TimecodeRun> = Vec::with_capacity(self.runs.len());
merged.push(self.runs[0].clone());
for run in self.runs.iter().skip(1) {
let last = merged
.last_mut()
.expect("merged is non-empty: initial element was pushed before this loop");
if last.start.frame_rate == run.start.frame_rate {
let last_end = last.start.to_frames() + last.frame_count;
if last_end == run.start.to_frames() {
last.frame_count += run.frame_count;
continue;
}
}
merged.push(run.clone());
}
self.runs = merged;
}
pub fn iter(&self) -> std::slice::Iter<'_, TimecodeRun> {
self.runs.iter()
}
pub fn detect_gaps(&self) -> Vec<(usize, i64)> {
let mut gaps = Vec::new();
for i in 0..self.runs.len().saturating_sub(1) {
let a = &self.runs[i];
let b = &self.runs[i + 1];
if a.start.frame_rate == b.start.frame_rate {
let a_end = a.start.to_frames() + a.frame_count;
let b_start = b.start.to_frames();
let gap = b_start as i64 - a_end as i64;
if gap != 0 {
gaps.push((i, gap));
}
}
}
gaps
}
}
impl Default for TimecodeSequence {
fn default() -> Self {
Self::new()
}
}
pub fn build_sequence(items: &[(Timecode, u64)]) -> Result<TimecodeSequence, TimecodeError> {
let mut seq = TimecodeSequence::new();
for (tc, count) in items {
let run = TimecodeRun::new(*tc, *count)?;
seq.push(run);
}
Ok(seq)
}
#[cfg(test)]
mod tests {
use super::*;
fn tc(h: u8, m: u8, s: u8, f: u8) -> Timecode {
Timecode::new(h, m, s, f, FrameRate::Fps25).expect("valid timecode")
}
#[test]
fn test_run_creation() {
let run = TimecodeRun::new(tc(1, 0, 0, 0), 100).expect("valid timecode run");
assert_eq!(run.frame_count, 100);
}
#[test]
fn test_run_zero_frames_error() {
let result = TimecodeRun::new(tc(0, 0, 0, 0), 0);
assert!(result.is_err());
}
#[test]
fn test_run_duration_secs() {
let run = TimecodeRun::new(tc(0, 0, 0, 0), 50).expect("valid timecode run");
let dur = run.duration_secs();
assert!((dur - 2.0).abs() < 1e-9);
}
#[test]
fn test_run_contains() {
let run = TimecodeRun::new(tc(0, 0, 0, 0), 25).expect("valid timecode run");
assert!(run.contains(&tc(0, 0, 0, 10)));
assert!(run.contains(&tc(0, 0, 0, 24)));
assert!(!run.contains(&tc(0, 0, 1, 0)));
}
#[test]
fn test_sequence_push_and_total() {
let mut seq = TimecodeSequence::new();
seq.push(TimecodeRun::new(tc(0, 0, 0, 0), 25).expect("valid timecode run"));
seq.push(TimecodeRun::new(tc(0, 0, 1, 0), 25).expect("valid timecode run"));
assert_eq!(seq.total_frames(), 50);
assert_eq!(seq.run_count(), 2);
}
#[test]
fn test_sequence_is_contiguous() {
let mut seq = TimecodeSequence::new();
seq.push(TimecodeRun::new(tc(0, 0, 0, 0), 25).expect("valid timecode run"));
seq.push(TimecodeRun::new(tc(0, 0, 1, 0), 25).expect("valid timecode run"));
assert!(seq.is_contiguous(0, 1));
}
#[test]
fn test_sequence_not_contiguous() {
let mut seq = TimecodeSequence::new();
seq.push(TimecodeRun::new(tc(0, 0, 0, 0), 10).expect("valid timecode run"));
seq.push(TimecodeRun::new(tc(0, 0, 1, 0), 10).expect("valid timecode run"));
assert!(!seq.is_contiguous(0, 1));
}
#[test]
fn test_find_run_at_offset() {
let mut seq = TimecodeSequence::new();
seq.push(TimecodeRun::new(tc(0, 0, 0, 0), 25).expect("valid timecode run"));
seq.push(TimecodeRun::new(tc(0, 0, 1, 0), 25).expect("valid timecode run"));
assert_eq!(seq.find_run_at_offset(0), Some((0, 0)));
assert_eq!(seq.find_run_at_offset(24), Some((0, 24)));
assert_eq!(seq.find_run_at_offset(25), Some((1, 0)));
assert_eq!(seq.find_run_at_offset(50), None);
}
#[test]
fn test_compact_merges_contiguous() {
let mut seq = TimecodeSequence::new();
seq.push(TimecodeRun::new(tc(0, 0, 0, 0), 25).expect("valid timecode run"));
seq.push(TimecodeRun::new(tc(0, 0, 1, 0), 25).expect("valid timecode run"));
assert_eq!(seq.run_count(), 2);
seq.compact();
assert_eq!(seq.run_count(), 1);
assert_eq!(seq.total_frames(), 50);
}
#[test]
fn test_compact_preserves_gaps() {
let mut seq = TimecodeSequence::new();
seq.push(TimecodeRun::new(tc(0, 0, 0, 0), 10).expect("valid timecode run"));
seq.push(TimecodeRun::new(tc(0, 0, 2, 0), 10).expect("valid timecode run"));
seq.compact();
assert_eq!(seq.run_count(), 2);
}
#[test]
fn test_detect_gaps() {
let mut seq = TimecodeSequence::new();
seq.push(TimecodeRun::new(tc(0, 0, 0, 0), 10).expect("valid timecode run"));
seq.push(TimecodeRun::new(tc(0, 0, 1, 0), 10).expect("valid timecode run"));
let gaps = seq.detect_gaps();
assert_eq!(gaps.len(), 1);
assert_eq!(gaps[0].0, 0);
assert_eq!(gaps[0].1, 15); }
#[test]
fn test_build_sequence() {
let items = vec![(tc(0, 0, 0, 0), 25u64), (tc(0, 0, 1, 0), 50)];
let seq = build_sequence(&items).expect("build sequence should succeed");
assert_eq!(seq.run_count(), 2);
assert_eq!(seq.total_frames(), 75);
}
#[test]
fn test_total_duration_secs() {
let mut seq = TimecodeSequence::new();
seq.push(TimecodeRun::new(tc(0, 0, 0, 0), 25).expect("valid timecode run"));
seq.push(TimecodeRun::new(tc(0, 0, 1, 0), 50).expect("valid timecode run"));
let dur = seq.total_duration_secs();
assert!((dur - 3.0).abs() < 1e-9);
}
#[test]
fn test_end_timecode() {
let run = TimecodeRun::new(tc(0, 0, 0, 0), 26).expect("valid timecode run");
let end = run
.end_timecode(FrameRate::Fps25)
.expect("end timecode should succeed");
assert_eq!(end.hours, 0);
assert_eq!(end.minutes, 0);
assert_eq!(end.seconds, 1);
assert_eq!(end.frames, 0);
}
}