use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::time::Duration;
use ff_filter::XfadeTransition;
#[derive(Debug, Clone)]
pub struct Clip {
pub source: PathBuf,
pub in_point: Option<Duration>,
pub out_point: Option<Duration>,
pub timeline_offset: Duration,
pub metadata: HashMap<String, String>,
pub transition: Option<XfadeTransition>,
pub transition_duration: Duration,
pub volume_db: f64,
pub fade_in: Duration,
pub fade_out: Duration,
pub brightness: f32,
pub contrast: f32,
pub saturation: f32,
pub speed: f64,
}
impl Clip {
pub fn new(source: impl AsRef<Path>) -> Self {
Self {
source: source.as_ref().to_path_buf(),
in_point: None,
out_point: None,
timeline_offset: Duration::ZERO,
metadata: HashMap::new(),
transition: None,
transition_duration: Duration::ZERO,
volume_db: 0.0,
fade_in: Duration::ZERO,
fade_out: Duration::ZERO,
brightness: 0.0,
contrast: 1.0,
saturation: 1.0,
speed: 1.0,
}
}
#[must_use]
pub fn trim(self, in_point: Duration, out_point: Duration) -> Self {
Self {
in_point: Some(in_point),
out_point: Some(out_point),
..self
}
}
#[must_use]
pub fn offset(self, timeline_offset: Duration) -> Self {
Self {
timeline_offset,
..self
}
}
#[must_use]
pub fn with_transition(self, kind: XfadeTransition, duration: Duration) -> Self {
Self {
transition: Some(kind),
transition_duration: duration,
..self
}
}
#[must_use]
pub fn volume(self, db: f64) -> Self {
Self {
volume_db: db,
..self
}
}
#[must_use]
pub fn with_fade_in(self, duration: Duration) -> Self {
Self {
fade_in: duration,
..self
}
}
#[must_use]
pub fn with_fade_out(self, duration: Duration) -> Self {
Self {
fade_out: duration,
..self
}
}
#[must_use]
pub fn with_color_correction(self, brightness: f32, contrast: f32, saturation: f32) -> Self {
Self {
brightness,
contrast,
saturation,
..self
}
}
#[must_use]
pub fn with_speed(self, speed: f64) -> Self {
Self { speed, ..self }
}
pub fn duration(&self) -> Option<Duration> {
match (self.in_point, self.out_point) {
(Some(in_pt), Some(out_pt)) => out_pt.checked_sub(in_pt),
_ => None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn clip_new_should_have_zero_offset() {
let clip = Clip::new("video.mp4");
assert_eq!(clip.timeline_offset, Duration::ZERO);
assert!(clip.in_point.is_none());
assert!(clip.out_point.is_none());
assert!(clip.metadata.is_empty());
}
#[test]
fn clip_new_should_default_transition_to_none() {
let clip = Clip::new("video.mp4");
assert!(clip.transition.is_none());
assert_eq!(clip.transition_duration, Duration::ZERO);
}
#[test]
fn clip_with_transition_should_set_fields() {
use ff_filter::XfadeTransition;
let clip = Clip::new("video.mp4")
.with_transition(XfadeTransition::Fade, Duration::from_millis(500));
assert_eq!(clip.transition, Some(XfadeTransition::Fade));
assert_eq!(clip.transition_duration, Duration::from_millis(500));
}
#[test]
fn clip_trim_should_set_in_out_points() {
let clip = Clip::new("video.mp4").trim(Duration::from_secs(3), Duration::from_secs(9));
assert_eq!(clip.in_point, Some(Duration::from_secs(3)));
assert_eq!(clip.out_point, Some(Duration::from_secs(9)));
}
#[test]
fn clip_duration_should_return_none_when_out_point_unset() {
let clip = Clip::new("video.mp4");
assert!(clip.duration().is_none());
}
#[test]
fn clip_duration_should_return_difference_when_both_points_set() {
let clip = Clip::new("video.mp4").trim(Duration::from_secs(2), Duration::from_secs(10));
assert_eq!(clip.duration(), Some(Duration::from_secs(8)));
}
#[test]
fn clip_new_should_default_volume_db_to_zero() {
let clip = Clip::new("audio.wav");
assert_eq!(clip.volume_db, 0.0);
}
#[test]
fn clip_volume_should_set_volume_db() {
let clip = Clip::new("audio.wav").volume(-6.0);
assert_eq!(clip.volume_db, -6.0);
}
#[test]
fn clip_volume_positive_should_set_volume_db() {
let clip = Clip::new("audio.wav").volume(3.0);
assert_eq!(clip.volume_db, 3.0);
}
#[test]
fn clip_new_should_default_fade_fields_to_zero() {
let clip = Clip::new("audio.wav");
assert_eq!(clip.fade_in, Duration::ZERO);
assert_eq!(clip.fade_out, Duration::ZERO);
}
#[test]
fn clip_with_fade_in_should_set_fade_in() {
let clip = Clip::new("audio.wav").with_fade_in(Duration::from_secs(2));
assert_eq!(clip.fade_in, Duration::from_secs(2));
assert_eq!(clip.fade_out, Duration::ZERO);
}
#[test]
fn clip_with_fade_out_should_set_fade_out() {
let clip = Clip::new("audio.wav")
.trim(Duration::ZERO, Duration::from_secs(10))
.with_fade_out(Duration::from_secs(1));
assert_eq!(clip.fade_out, Duration::from_secs(1));
assert_eq!(clip.fade_in, Duration::ZERO);
}
#[test]
fn clip_fade_in_and_fade_out_can_be_chained() {
let clip = Clip::new("audio.wav")
.trim(Duration::ZERO, Duration::from_secs(10))
.with_fade_in(Duration::from_millis(500))
.with_fade_out(Duration::from_millis(500));
assert_eq!(clip.fade_in, Duration::from_millis(500));
assert_eq!(clip.fade_out, Duration::from_millis(500));
}
#[test]
fn clip_new_should_default_color_correction_to_neutral() {
let clip = Clip::new("video.mp4");
assert_eq!(clip.brightness, 0.0);
assert_eq!(clip.contrast, 1.0);
assert_eq!(clip.saturation, 1.0);
}
#[test]
fn clip_with_color_correction_should_set_fields() {
let clip = Clip::new("scene.mp4").with_color_correction(0.1, 1.2, 0.9);
assert_eq!(clip.brightness, 0.1);
assert_eq!(clip.contrast, 1.2);
assert_eq!(clip.saturation, 0.9);
}
#[test]
fn clip_new_should_default_speed_to_one() {
let clip = Clip::new("video.mp4");
assert_eq!(clip.speed, 1.0);
}
#[test]
fn clip_with_speed_should_set_speed() {
let clip = Clip::new("video.mp4").with_speed(2.0);
assert_eq!(clip.speed, 2.0);
}
#[test]
fn clip_with_speed_slow_motion_should_set_speed() {
let clip = Clip::new("video.mp4").with_speed(0.5);
assert_eq!(clip.speed, 0.5);
}
}