use std::hash::{DefaultHasher, Hash, Hasher};
use std::ops::{Bound, Range, RangeBounds};
use std::sync::Arc;
use crate::media::ImageData;
use crate::video_data::FFramesSyncedVideoFrame;
use crate::{
Duration, FFramesContext, Frame, FrameConvertOptions, PlaybackSpeed, ResizeVideoFrame,
};
const FRAME_EPSILON_SECONDS: f64 = 1e-4;
#[derive(Debug, Clone)]
pub struct Clip<'a> {
pub video: &'a str,
pub source: Range<f64>,
pub at: f32,
pub speed: f32,
pub reverse: bool,
pub looping: bool,
pub editor_fallback_image: Option<&'a ImageData<'a>>,
}
impl<'a> Clip<'a> {
pub fn new(video: &'a str, source: impl RangeBounds<f64>) -> Self {
let start = match source.start_bound() {
Bound::Included(&start) | Bound::Excluded(&start) => start,
Bound::Unbounded => 0.0,
};
let end = match source.end_bound() {
Bound::Included(&end) | Bound::Excluded(&end) => end,
Bound::Unbounded => f64::INFINITY,
};
Self {
video,
source: start..end,
at: 0.0,
speed: 1.0,
reverse: false,
looping: false,
editor_fallback_image: None,
}
}
#[must_use]
pub fn at(mut self, seconds: f32) -> Self {
self.at = seconds;
self
}
#[must_use]
pub fn speed(mut self, speed: f32) -> Self {
self.speed = speed;
self
}
#[must_use]
pub fn reverse(mut self) -> Self {
self.reverse = true;
self
}
#[must_use]
pub fn looping(mut self) -> Self {
self.looping = true;
self
}
#[must_use]
pub fn editor_fallback_image(mut self, image: Option<&'a ImageData<'a>>) -> Self {
self.editor_fallback_image = image;
self
}
fn speed_value(&self) -> f64 {
f64::from(PlaybackSpeed::new(self.speed).thousandths()) / f64::from(PlaybackSpeed::SCALE)
}
fn source_seconds_len(&self) -> f64 {
(self.source.end - self.source.start).max(0.0)
}
pub fn seconds(&self) -> f64 {
self.source_seconds_len() / self.speed_value()
}
pub fn duration(&self) -> Duration<'a> {
if self.source.end.is_finite() {
Duration::Seconds((self.seconds() + FRAME_EPSILON_SECONDS) as f32)
} else if self.source.start > 0.0 {
Duration::FromVideo(self.video) - Duration::Seconds(self.source.start as f32)
} else {
Duration::FromVideo(self.video)
}
}
pub fn frames(&self, fps: usize) -> usize {
((self.seconds() + FRAME_EPSILON_SECONDS) * fps as f64) as usize
}
pub fn source_seconds(&self, index: usize, fps: usize) -> Option<f64> {
let frames = self.frames(fps);
if frames == 0 || (index >= frames && !self.looping) {
return None;
}
let step = self.speed_value() / fps as f64;
let played = (index % frames) as f64 * step;
let seconds = if self.reverse {
(self.source.end - step - played).max(self.source.start)
} else {
self.source.start + played
};
Some(seconds)
}
pub fn frame(
&self,
frame: &Frame,
ctx: &FFramesContext<'_, '_>,
) -> Option<Arc<impl FFramesSyncedVideoFrame<'a> + use<'a>>> {
let start_frame = (f64::from(self.at) * frame.fps as f64).round() as usize;
let index = frame.index.checked_sub(start_frame)?;
let seconds = self.source_seconds(index, frame.fps)?;
let loop_start =
(self.looping && !self.source.end.is_finite()).then_some(self.source.start);
frame.video_frame_at_keyed(
ctx,
self.video,
seconds,
loop_start,
self.decoder_key(),
self.editor_fallback_image,
)
}
pub fn image(&self, frame: &Frame, ctx: &FFramesContext<'_, '_>) -> Option<ImageData<'a>> {
Some(self.frame(frame, ctx)?.into_image())
}
pub fn image_at_width(
&self,
frame: &Frame,
ctx: &FFramesContext<'_, '_>,
width: u32,
) -> Option<ImageData<'a>> {
let video_frame = self.frame(frame, ctx)?;
let height = (f64::from(width) * f64::from(video_frame.height())
/ f64::from(video_frame.width().max(1)))
.round() as u32;
video_frame.into_resized_image(&FrameConvertOptions {
resize: ResizeVideoFrame {
width: width.max(1),
height: height.max(1),
},
})
}
fn decoder_key(&self) -> u64 {
let mut hasher = DefaultHasher::new();
self.video.hash(&mut hasher);
self.source.start.to_bits().hash(&mut hasher);
self.source.end.to_bits().hash(&mut hasher);
PlaybackSpeed::new(self.speed)
.thousandths()
.hash(&mut hasher);
self.reverse.hash(&mut hasher);
self.looping.hash(&mut hasher);
self.at.to_bits().hash(&mut hasher);
hasher.finish().max(1)
}
}