use std::path::PathBuf;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::thread::JoinHandle;
use std::time::Duration;
use ff_filter::{AnimatedValue, LavfiSource, RealtimeLayerDescriptor, XfadeTransition};
use ff_format::VideoFrame;
use crate::audio::AudioTrackHandle;
use crate::playback::SwsRgbaConverter;
use crate::playback::decode_buffer::DecodeBuffer;
use super::audio_resampling::spawn_audio_track_thread;
#[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: PathBuf,
pub(super) decode_buf: DecodeBuffer,
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) 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) 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,
}
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,
};
}
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) 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,
},
);
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 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");
}
}