use std::path::PathBuf;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::mpsc::Receiver;
use std::thread::JoinHandle;
use std::time::Duration;
use ff_filter::{AnimatedValue, LavfiSource, RealtimeLayerDescriptor, XfadeTransition};
use ff_format::{Rational, Timestamp, VideoFrame};
use crate::audio::AudioTrackHandle;
use crate::error::PreviewError;
use crate::playback::SwsRgbaConverter;
use crate::playback::decode_buffer::{DecodeBuffer, FrameResult};
use super::audio_resampling::spawn_audio_track_thread;
pub(super) enum ClipVideoSource {
File(DecodeBuffer),
Held {
frame: Option<VideoFrame>,
next_pts: Duration,
step: Duration,
},
}
fn micros_timestamp(pts: Duration) -> Timestamp {
Timestamp::from_duration(pts, Rational::new(1, 1_000_000))
}
impl ClipVideoSource {
pub(super) fn held(frame: Option<VideoFrame>, start_pts: Duration, fps: f64) -> Self {
Self::Held {
frame,
next_pts: start_pts,
step: Duration::from_secs_f64(1.0 / fps.max(1.0)),
}
}
pub(super) fn pop_frame(&mut self) -> FrameResult {
match self {
Self::File(buf) => buf.pop_frame(),
Self::Held {
frame: Some(frame),
next_pts,
step,
} => {
let mut out = frame.clone();
out.set_timestamp(micros_timestamp(*next_pts));
*next_pts = next_pts.saturating_add(*step);
FrameResult::Frame(out)
}
Self::Held { frame: None, .. } => FrameResult::Eof,
}
}
pub(super) fn seek(&mut self, target_pts: Duration) -> Result<(), PreviewError> {
match self {
Self::File(buf) => buf.seek(target_pts),
Self::Held { next_pts, .. } => {
*next_pts = target_pts;
Ok(())
}
}
}
pub(super) fn seek_coarse(&mut self, target_pts: Duration) -> Result<(), PreviewError> {
match self {
Self::File(buf) => buf.seek_coarse(target_pts),
Self::Held { next_pts, .. } => {
*next_pts = target_pts;
Ok(())
}
}
}
pub(super) fn error_events(&self) -> Option<&Receiver<String>> {
match self {
Self::File(buf) => Some(buf.error_events()),
Self::Held { .. } => None,
}
}
pub(super) fn held_frame_dims(&self) -> Option<(u32, u32)> {
match self {
Self::Held {
frame: Some(frame), ..
} => Some((frame.width(), frame.height())),
Self::File(_) | Self::Held { frame: None, .. } => None,
}
}
}
#[allow(clippy::cast_possible_truncation)]
pub(super) fn db_to_linear(db: f64) -> f32 {
10.0_f64.powf(db / 20.0) as f32
}
pub(super) struct ClipState {
pub(super) source: super::types::SceneSource,
pub(super) decode_buf: ClipVideoSource,
pub(super) timeline_start: Duration,
pub(super) timeline_end: Duration,
pub(super) in_point: Duration,
pub(super) out_point: Option<Duration>,
pub(super) xfade_dur: Duration,
pub(super) xfade_kind: Option<XfadeTransition>,
pub(super) video_handle: Duration,
pub(super) audio_track: Option<AudioTrackHandle>,
pub(super) speed: f64,
pub(super) opacity: f32,
pub(super) layer_desc: RealtimeLayerDescriptor,
pub(super) volume: AnimatedValue<f64>,
pub(super) fade_in: Duration,
pub(super) fade_out: Duration,
pub(super) pitch: f64,
}
pub(super) struct TransitionState {
pub(super) next_idx: usize,
pub(super) start: Duration,
pub(super) duration: Duration,
pub(super) kind: XfadeTransition,
}
pub(super) struct OverlayLayer {
pub(super) clips: Vec<ClipState>,
pub(super) active: usize,
pub(super) sws: SwsRgbaConverter,
pub(super) rgba: Vec<u8>,
pub(super) cur_dims: Option<(u32, u32)>,
pub(super) pending: Option<VideoFrame>,
}
pub(super) struct LavfiOverlayState {
lavfi: String,
source: LavfiSource,
sws: SwsRgbaConverter,
pub(super) rgba: Vec<u8>,
pub(super) dims: Option<(u32, u32)>,
pub(super) pending: Option<VideoFrame>,
}
impl LavfiOverlayState {
pub(super) fn new(lavfi: &str) -> Option<Self> {
match LavfiSource::new(lavfi) {
Ok(source) => Some(Self {
lavfi: lavfi.to_string(),
source,
sws: SwsRgbaConverter::new(),
rgba: Vec::new(),
dims: None,
pending: None,
}),
Err(e) => {
log::warn!("lavfi overlay unavailable, dropped from preview: {e}");
None
}
}
}
pub(super) fn advance_to(&mut self, target_pts: Duration) -> Option<(u32, u32)> {
let mut latest: Option<VideoFrame> = None;
loop {
let f = match self.pending.take() {
Some(pf) => pf,
None => match self.source.pull() {
Ok(Some(f)) => f,
_ => break,
},
};
if f.timestamp().as_duration() > target_pts {
self.pending = Some(f);
break;
}
latest = Some(f);
}
if let Some(f) = latest
&& self.sws.convert(&f, &mut self.rgba)
{
self.dims = Some((f.width(), f.height()));
}
self.dims
}
pub(super) fn rebuild(&mut self) {
match LavfiSource::new(&self.lavfi) {
Ok(source) => {
self.source = source;
self.pending = None;
self.rgba.clear();
self.dims = None;
}
Err(e) => log::warn!("lavfi overlay seek rebuild failed: {e}"),
}
}
}
#[derive(Clone, Copy)]
pub(super) struct AudioFadeConfig {
pub(super) fade_in: Duration,
pub(super) fade_out: Duration,
pub(super) clip_dur: Duration,
pub(super) in_point: Duration,
pub(super) speed: f64,
pub(super) pitch: f64,
}
impl AudioFadeConfig {
pub(super) const NONE: Self = Self {
fade_in: Duration::ZERO,
fade_out: Duration::ZERO,
clip_dur: Duration::ZERO,
in_point: Duration::ZERO,
speed: 1.0,
pitch: 0.0,
};
}
pub(super) struct AudioOnlyTrack {
pub(super) source: PathBuf,
pub(super) timeline_start: Duration,
pub(super) timeline_end: Duration,
pub(super) in_point: Duration,
pub(super) fade_in: Duration,
pub(super) fade_out: Duration,
pub(super) clip_dur: Duration,
pub(super) speed: f64,
pub(super) pitch: f64,
pub(super) handle: AudioTrackHandle,
pub(super) volume: AnimatedValue<f64>,
pub(super) cancel: Option<Arc<AtomicBool>>,
pub(super) thread: Option<JoinHandle<()>>,
}
impl AudioOnlyTrack {
pub(super) fn start_at(&mut self, from_pts: Duration) {
if let Some(c) = self.cancel.take() {
c.store(true, Ordering::Release);
}
drop(self.thread.take());
self.handle.clear();
let cancel = Arc::new(AtomicBool::new(false));
let t = spawn_audio_track_thread(
self.source.clone(),
from_pts,
self.handle.clone(),
Arc::clone(&cancel),
AudioFadeConfig {
fade_in: self.fade_in,
fade_out: self.fade_out,
clip_dur: self.clip_dur,
in_point: self.in_point,
speed: self.speed,
pitch: self.pitch,
},
);
self.cancel = Some(cancel);
self.thread = Some(t);
}
pub(super) fn stop(&mut self) {
if let Some(c) = self.cancel.take() {
c.store(true, Ordering::Release);
}
drop(self.thread.take());
}
}
impl Drop for AudioOnlyTrack {
fn drop(&mut self) {
self.stop();
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
use ff_format::{Rational, Timestamp};
#[test]
fn db_to_linear_should_convert_gain() {
assert!((db_to_linear(0.0) - 1.0).abs() < 1e-6, "0 dB = unity");
assert!(
(db_to_linear(-6.0) - 0.501_187).abs() < 1e-3,
"-6 dB ≈ 0.501"
);
assert!((db_to_linear(6.0) - 1.995).abs() < 1e-3, "+6 dB ≈ 1.995");
assert!(db_to_linear(-120.0) < 1e-5, "very quiet ≈ 0");
}
#[test]
fn held_source_should_return_injected_frame_with_advancing_pts() {
let frame = VideoFrame::from_rgba(2, 3, vec![10u8; 2 * 3 * 4]).unwrap();
let mut src = ClipVideoSource::held(Some(frame), Duration::ZERO, 30.0);
let step = Duration::from_secs_f64(1.0 / 30.0);
for i in 0..3u32 {
let FrameResult::Frame(f) = src.pop_frame() else {
panic!("a held source must return its constant frame every pull");
};
assert_eq!((f.width(), f.height()), (2, 3), "pixels are constant");
let want = step * i;
let got = f.timestamp().as_duration();
assert!(
got.abs_diff(want) < Duration::from_micros(1),
"held PTS must advance by 1/fps: pull {i} want {want:?} got {got:?}"
);
}
assert!(src.seek(Duration::from_secs(5)).is_ok());
let FrameResult::Frame(f) = src.pop_frame() else {
panic!("expected a frame after seek");
};
assert!(
f.timestamp().as_duration().abs_diff(Duration::from_secs(5)) < Duration::from_micros(1)
);
assert!(src.seek_coarse(Duration::from_secs(1)).is_ok());
assert!(
src.error_events().is_none(),
"a held source has no decode error channel"
);
let mut empty = ClipVideoSource::held(None, Duration::ZERO, 30.0);
assert!(matches!(empty.pop_frame(), FrameResult::Eof));
}
#[test]
fn held_frame_dims_should_report_held_size_and_none_when_empty() {
let frame = VideoFrame::from_rgba(2, 3, vec![0u8; 2 * 3 * 4]).unwrap();
assert_eq!(
ClipVideoSource::held(Some(frame), Duration::ZERO, 30.0).held_frame_dims(),
Some((2, 3))
);
assert_eq!(
ClipVideoSource::held(None, Duration::ZERO, 30.0).held_frame_dims(),
None
);
}
#[test]
fn lavfi_advance_to_should_surface_due_frames_and_hold_future_ones() {
let Some(mut st) = LavfiOverlayState::new("color=c=red:s=8x8:d=1") else {
println!("Skipping: movie/lavfi filter unavailable");
return;
};
let stamped = |w: u32, h: u32, secs: u64| {
let mut f = VideoFrame::from_rgba(w, h, vec![200u8; (w * h * 4) as usize]).unwrap();
f.set_timestamp(Timestamp::from_duration(
Duration::from_secs(secs),
Rational::new(1, 1_000_000),
));
f
};
st.pending = Some(stamped(8, 8, 0));
assert_eq!(st.advance_to(Duration::ZERO), Some((8, 8)));
assert!(!st.rgba.is_empty(), "the due frame was converted into rgba");
st.pending = Some(stamped(4, 4, 10));
assert_eq!(st.advance_to(Duration::from_secs(1)), Some((8, 8)));
assert!(st.pending.is_some(), "the future frame is held for later");
}
}