#![allow(dead_code)]
use crate::{FrameRate, Timecode, TimecodeError};
#[derive(Debug, Clone, PartialEq)]
pub struct SubtitleCue {
pub id: u32,
pub tc_in: Timecode,
pub tc_out: Timecode,
pub text: String,
}
impl SubtitleCue {
pub fn new(id: u32, tc_in: Timecode, tc_out: Timecode, text: String) -> Self {
Self {
id,
tc_in,
tc_out,
text,
}
}
pub fn duration_frames(&self) -> u64 {
let f_in = self.tc_in.to_frames();
let f_out = self.tc_out.to_frames();
f_out.saturating_sub(f_in)
}
#[allow(clippy::cast_precision_loss)]
pub fn duration_secs(&self) -> f64 {
let fps = self.tc_in.frame_rate.fps as f64;
self.duration_frames() as f64 / fps
}
pub fn overlaps(&self, other: &SubtitleCue) -> bool {
let a_in = self.tc_in.to_frames();
let a_out = self.tc_out.to_frames();
let b_in = other.tc_in.to_frames();
let b_out = other.tc_out.to_frames();
a_in < b_out && b_in < a_out
}
}
pub fn apply_frame_offset(
cues: &[SubtitleCue],
offset: i64,
rate: FrameRate,
) -> Result<Vec<SubtitleCue>, TimecodeError> {
let mut result = Vec::with_capacity(cues.len());
for cue in cues {
let f_in = cue.tc_in.to_frames() as i64 + offset;
let f_out = cue.tc_out.to_frames() as i64 + offset;
if f_in < 0 || f_out < 0 {
return Err(TimecodeError::InvalidFrames);
}
let new_in = Timecode::from_frames(f_in as u64, rate)?;
let new_out = Timecode::from_frames(f_out as u64, rate)?;
result.push(SubtitleCue::new(cue.id, new_in, new_out, cue.text.clone()));
}
Ok(result)
}
#[allow(
clippy::cast_precision_loss,
clippy::cast_possible_truncation,
clippy::cast_sign_loss
)]
pub fn apply_linear_stretch(
cues: &[SubtitleCue],
anchor_frame: u64,
factor: f64,
rate: FrameRate,
) -> Result<Vec<SubtitleCue>, TimecodeError> {
let mut result = Vec::with_capacity(cues.len());
let anchor = anchor_frame as f64;
for cue in cues {
let f_in = cue.tc_in.to_frames() as f64;
let f_out = cue.tc_out.to_frames() as f64;
let new_in = anchor + (f_in - anchor) * factor;
let new_out = anchor + (f_out - anchor) * factor;
if new_in < 0.0 || new_out < 0.0 {
return Err(TimecodeError::InvalidFrames);
}
let tc_in = Timecode::from_frames(new_in.round() as u64, rate)?;
let tc_out = Timecode::from_frames(new_out.round() as u64, rate)?;
result.push(SubtitleCue::new(cue.id, tc_in, tc_out, cue.text.clone()));
}
Ok(result)
}
#[allow(clippy::cast_precision_loss)]
pub fn compute_average_drift(cues: &[SubtitleCue], reference_in_frames: &[u64]) -> Option<f64> {
if cues.is_empty() || cues.len() != reference_in_frames.len() {
return None;
}
let total: f64 = cues
.iter()
.zip(reference_in_frames.iter())
.map(|(cue, &ref_f)| cue.tc_in.to_frames() as f64 - ref_f as f64)
.sum();
Some(total / cues.len() as f64)
}
pub fn sort_cues_by_in(cues: &mut [SubtitleCue]) {
cues.sort_by_key(|c| c.tc_in.to_frames());
}
pub fn find_overlaps(cues: &[SubtitleCue]) -> Vec<(usize, usize)> {
let mut overlaps = Vec::new();
for i in 0..cues.len() {
for j in (i + 1)..cues.len() {
if cues[i].overlaps(&cues[j]) {
overlaps.push((i, j));
}
}
}
overlaps
}
#[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")
}
fn make_cue(id: u32, s_in: u8, f_in: u8, s_out: u8, f_out: u8) -> SubtitleCue {
SubtitleCue::new(
id,
tc(0, 0, s_in, f_in),
tc(0, 0, s_out, f_out),
format!("cue {id}"),
)
}
#[test]
fn test_cue_duration_frames() {
let cue = make_cue(1, 0, 0, 1, 0);
assert_eq!(cue.duration_frames(), 25);
}
#[test]
fn test_cue_duration_secs() {
let cue = make_cue(1, 0, 0, 2, 0);
let d = cue.duration_secs();
assert!((d - 2.0).abs() < 1e-9);
}
#[test]
fn test_cue_overlap() {
let a = make_cue(1, 0, 0, 2, 0); let b = make_cue(2, 1, 0, 3, 0); assert!(a.overlaps(&b));
}
#[test]
fn test_cue_no_overlap() {
let a = make_cue(1, 0, 0, 1, 0); let b = make_cue(2, 1, 0, 2, 0); assert!(!a.overlaps(&b));
}
#[test]
fn test_apply_frame_offset_positive() {
let cues = vec![make_cue(1, 0, 0, 1, 0)];
let shifted =
apply_frame_offset(&cues, 10, FrameRate::Fps25).expect("frame offset should succeed");
assert_eq!(shifted[0].tc_in.to_frames(), 10);
assert_eq!(shifted[0].tc_out.to_frames(), 35);
}
#[test]
fn test_apply_frame_offset_negative_clamp() {
let cues = vec![make_cue(1, 0, 0, 1, 0)];
let result = apply_frame_offset(&cues, -100, FrameRate::Fps25);
assert!(result.is_err());
}
#[test]
fn test_linear_stretch_identity() {
let cues = vec![make_cue(1, 1, 0, 2, 0)];
let stretched = apply_linear_stretch(&cues, 0, 1.0, FrameRate::Fps25)
.expect("linear stretch should succeed");
assert_eq!(stretched[0].tc_in.to_frames(), cues[0].tc_in.to_frames());
assert_eq!(stretched[0].tc_out.to_frames(), cues[0].tc_out.to_frames());
}
#[test]
fn test_linear_stretch_double() {
let cues = vec![make_cue(1, 1, 0, 2, 0)]; let stretched = apply_linear_stretch(&cues, 0, 2.0, FrameRate::Fps25)
.expect("linear stretch should succeed");
assert_eq!(stretched[0].tc_in.to_frames(), 50);
assert_eq!(stretched[0].tc_out.to_frames(), 100);
}
#[test]
fn test_compute_average_drift_zero() {
let cues = vec![make_cue(1, 1, 0, 2, 0)];
let refs = vec![25];
let drift = compute_average_drift(&cues, &refs).expect("drift computation should succeed");
assert!((drift).abs() < 1e-9);
}
#[test]
fn test_compute_average_drift_some() {
let cues = vec![make_cue(1, 1, 0, 2, 0), make_cue(2, 2, 0, 3, 0)];
let refs = vec![20, 45];
let drift = compute_average_drift(&cues, &refs).expect("drift computation should succeed");
assert!((drift - 5.0).abs() < 1e-9);
}
#[test]
fn test_sort_cues_by_in() {
let mut cues = vec![make_cue(2, 2, 0, 3, 0), make_cue(1, 0, 0, 1, 0)];
sort_cues_by_in(&mut cues);
assert_eq!(cues[0].id, 1);
assert_eq!(cues[1].id, 2);
}
#[test]
fn test_find_overlaps() {
let cues = vec![
make_cue(1, 0, 0, 2, 0),
make_cue(2, 1, 0, 3, 0),
make_cue(3, 5, 0, 6, 0),
];
let overlaps = find_overlaps(&cues);
assert_eq!(overlaps.len(), 1);
assert_eq!(overlaps[0], (0, 1));
}
#[test]
fn test_compute_average_drift_empty() {
let drift = compute_average_drift(&[], &[]);
assert!(drift.is_none());
}
}