1mod audio_resampling;
25mod inner;
26mod runner;
27mod runner_layout;
28mod state;
29mod types;
30
31use std::path::PathBuf;
32use std::sync::atomic::{AtomicBool, AtomicU64};
33use std::sync::{Arc, Mutex, mpsc};
34use std::time::{Duration, Instant};
35
36use crate::audio::{AudioMixer, AudioTrackHandle};
37use crate::error::PreviewError;
38use crate::event::PlayerEvent;
39use crate::playback::SwsRgbaConverter;
40use crate::playback::decode_buffer::DecodeBuffer;
41use crate::playback::master_clock::MasterClock;
42use crate::playback::player_handle::PlayerHandle;
43
44pub use runner::SceneRunner;
45pub use types::{Scene, SceneAudioPlacement, SceneAudioTrack, ScenePlacement, SceneVideoTrack};
46
47use audio_resampling::spawn_audio_track_thread;
48use ff_filter::{AnimatedValue, XfadeTransition};
49use state::{
50 AudioFadeConfig, AudioOnlyTrack, ClipState, LavfiOverlayState, OverlayLayer, db_to_linear,
51};
52
53const CHANNEL_CAP: usize = 64;
56
57pub struct ScenePlayer;
69
70impl ScenePlayer {
71 #[allow(clippy::too_many_lines)]
85 pub fn open(scene: &Scene) -> Result<(SceneRunner, PlayerHandle), PreviewError> {
86 struct ProbeResult {
87 source: PathBuf,
88 in_pt: Duration,
89 clip_dur: Duration,
90 offset: Duration,
91 out_point: Option<Duration>,
92 xfade_dur: Duration,
93 xfade_kind: Option<XfadeTransition>,
94 has_audio: bool,
95 video_w: u32,
98 video_h: u32,
99 speed: f64,
100 opacity: f32,
101 }
102
103 let v_tracks = &scene.video_tracks;
104 if v_tracks.is_empty() || v_tracks[0].placements.is_empty() {
105 return Err(PreviewError::Ffmpeg {
106 code: 0,
107 message: "timeline has no video clips in the primary track".into(),
108 });
109 }
110
111 let fps = scene.fps.max(1.0);
112 let clip_list = &v_tracks[0].placements;
113
114 let mut probes: Vec<ProbeResult> = Vec::with_capacity(clip_list.len());
117 let mut has_any_audio = false;
118
119 for p in clip_list {
120 let in_pt = p.in_point;
121 let info = ff_probe::open(&p.source)?;
122 let speed = p.speed;
123
124 let unscaled_dur = p.out_point.map_or_else(
127 || info.duration().saturating_sub(in_pt),
128 |op| op.saturating_sub(in_pt),
129 );
130 let clip_dur = if (speed - 1.0).abs() < 1e-9 {
131 unscaled_dur
132 } else {
133 unscaled_dur.div_f64(speed)
134 };
135
136 let has_audio = info.has_audio();
137 has_any_audio |= has_audio;
138
139 let (video_w, video_h) = info
140 .primary_video()
141 .map_or((0, 0), |v| (v.width(), v.height()));
142
143 probes.push(ProbeResult {
144 source: p.source.clone(),
145 in_pt,
146 clip_dur,
147 offset: p.offset,
148 out_point: p.out_point,
149 xfade_dur: p.xfade_dur,
150 xfade_kind: p.xfade_kind,
151 has_audio,
152 video_w,
153 video_h,
154 speed,
155 opacity: p.opacity,
156 });
157 }
158
159 let (mut mixer_arc, audio_track_handles): (
162 Option<Arc<Mutex<AudioMixer>>>,
163 Vec<Option<AudioTrackHandle>>,
164 ) = if has_any_audio {
165 let mut mixer = AudioMixer::new(48_000);
166 let handles: Vec<Option<AudioTrackHandle>> = probes
167 .iter()
168 .map(|p| {
169 if p.has_audio {
170 Some(mixer.add_track())
171 } else {
172 None
173 }
174 })
175 .collect();
176 (Some(Arc::new(Mutex::new(mixer))), handles)
177 } else {
178 (None, probes.iter().map(|_| None).collect())
179 };
180
181 let mut clip_states: Vec<ClipState> = Vec::with_capacity(probes.len());
184 for (i, p) in probes.iter().enumerate() {
185 let timeline_start = p.offset;
186 let timeline_end = timeline_start + p.clip_dur;
187
188 let mut decode_buf = DecodeBuffer::open(&p.source).build()?;
189 if p.in_pt > Duration::ZERO {
190 decode_buf.seek(p.in_pt)?;
191 }
192
193 if let (Some(handle), AnimatedValue::Static(db)) =
196 (&audio_track_handles[i], &clip_list[i].volume)
197 && *db != 0.0
198 {
199 handle.set_volume(db_to_linear(*db));
200 }
201 clip_states.push(ClipState {
202 source: p.source.clone(),
203 decode_buf,
204 timeline_start,
205 timeline_end,
206 in_point: p.in_pt,
207 out_point: p.out_point,
208 xfade_dur: p.xfade_dur,
209 xfade_kind: p.xfade_kind,
210 audio_track: audio_track_handles[i].clone(),
211 speed: p.speed,
212 opacity: p.opacity,
213 layer_desc: clip_list[i].layer.clone(),
214 volume: clip_list[i].volume.clone(),
215 fade_in: clip_list[i].fade_in,
216 fade_out: clip_list[i].fade_out,
217 });
218 }
219
220 let mut audio_only_tracks: Vec<AudioOnlyTrack> = Vec::new();
225
226 let mut overlay_layers: Vec<OverlayLayer> = Vec::new();
227 for layer in v_tracks.iter().skip(1) {
228 if layer.placements.is_empty() {
229 continue;
230 }
231 let mut layer_clips: Vec<ClipState> = Vec::new();
232 for p in &layer.placements {
233 let in_pt = p.in_point;
234 let info = ff_probe::open(&p.source)?;
235 let clip_dur = p.out_point.map_or_else(
236 || info.duration().saturating_sub(in_pt),
237 |op| op.saturating_sub(in_pt),
238 );
239 let timeline_start = p.offset;
240 let timeline_end = timeline_start + clip_dur;
241 let mut decode_buf = DecodeBuffer::open(&p.source).build()?;
242 if in_pt > Duration::ZERO {
243 decode_buf.seek(in_pt)?;
244 }
245 if info.has_audio() {
246 let mixer_ref = mixer_arc
247 .get_or_insert_with(|| Arc::new(Mutex::new(AudioMixer::new(48_000))));
248 let handle = mixer_ref
249 .lock()
250 .unwrap_or_else(std::sync::PoisonError::into_inner)
251 .add_track();
252 if let AnimatedValue::Static(db) = &p.volume
253 && *db != 0.0
254 {
255 handle.set_volume(db_to_linear(*db));
256 }
257 audio_only_tracks.push(AudioOnlyTrack {
258 source: p.source.clone(),
259 timeline_start,
260 timeline_end,
261 in_point: in_pt,
262 fade_in: p.fade_in,
263 fade_out: p.fade_out,
264 clip_dur,
265 speed: p.speed,
266 handle,
267 volume: p.volume.clone(),
268 cancel: None,
269 thread: None,
270 });
271 }
272 layer_clips.push(ClipState {
273 source: p.source.clone(),
274 decode_buf,
275 timeline_start,
276 timeline_end,
277 in_point: in_pt,
278 out_point: p.out_point,
279 xfade_dur: Duration::ZERO,
280 xfade_kind: None,
281 audio_track: None,
282 speed: p.speed,
283 opacity: p.opacity,
284 layer_desc: p.layer.clone(),
285 volume: p.volume.clone(),
286 fade_in: p.fade_in,
287 fade_out: p.fade_out,
288 });
289 }
290 overlay_layers.push(OverlayLayer {
291 clips: layer_clips,
292 active: 0,
293 sws: SwsRgbaConverter::new(),
294 rgba: Vec::new(),
295 cur_dims: None,
296 pending: None,
297 });
298 }
299
300 for track in &scene.audio_tracks {
303 for p in &track.placements {
304 let in_pt = p.in_point;
305 let info = ff_probe::open(&p.source)?;
306 if !info.has_audio() {
307 continue;
308 }
309 let clip_dur = p.out_point.map_or_else(
310 || info.duration().saturating_sub(in_pt),
311 |op| op.saturating_sub(in_pt),
312 );
313 let timeline_start = p.offset;
314 let timeline_end = timeline_start + clip_dur;
315 let mixer_ref =
317 mixer_arc.get_or_insert_with(|| Arc::new(Mutex::new(AudioMixer::new(48_000))));
318 let handle = mixer_ref
319 .lock()
320 .unwrap_or_else(std::sync::PoisonError::into_inner)
321 .add_track();
322 if let AnimatedValue::Static(db) = &p.volume
325 && *db != 0.0
326 {
327 handle.set_volume(db_to_linear(*db));
328 }
329 audio_only_tracks.push(AudioOnlyTrack {
330 source: p.source.clone(),
331 timeline_start,
332 timeline_end,
333 in_point: in_pt,
334 fade_in: p.fade_in,
335 fade_out: p.fade_out,
336 clip_dur,
337 speed: p.speed,
338 handle,
339 volume: p.volume.clone(),
340 cancel: None,
341 thread: None,
342 });
343 }
344 }
345
346 let total_dur = clip_states
349 .iter()
350 .map(|c| c.timeline_end)
351 .max()
352 .unwrap_or(Duration::ZERO);
353 let duration_millis = u64::try_from(total_dur.as_millis()).unwrap_or(u64::MAX);
354
355 let current_pts = Arc::new(AtomicU64::new(0));
358 let paused = Arc::new(AtomicBool::new(false));
359 let stopped = Arc::new(AtomicBool::new(false));
360 let (cmd_tx, cmd_rx) = mpsc::sync_channel(CHANNEL_CAP);
361 let (event_tx, event_rx) = mpsc::sync_channel::<PlayerEvent>(CHANNEL_CAP);
362
363 let first_clip_at_origin = clip_states
367 .first()
368 .is_some_and(|c| c.timeline_start == Duration::ZERO);
369 let (initial_audio_cancel, initial_audio_thread) = if first_clip_at_origin {
370 if let Some(handle) = clip_states.first().and_then(|c| c.audio_track.clone()) {
371 let source = clip_states[0].source.clone();
372 let in_pt = clip_states[0].in_point;
373 let clip0_speed = clip_states[0].speed;
374 let cancel = Arc::new(AtomicBool::new(false));
375 let thread = spawn_audio_track_thread(
376 source,
377 in_pt,
378 handle,
379 Arc::clone(&cancel),
380 AudioFadeConfig {
381 speed: clip0_speed,
382 ..AudioFadeConfig::NONE
383 },
384 );
385 (Some(cancel), Some(thread))
386 } else {
387 (None, None)
388 }
389 } else {
390 (None, None)
391 };
392
393 let (initial_last_w, initial_last_h) =
396 probes.first().map_or((0, 0), |p| (p.video_w, p.video_h));
397
398 let runner = SceneRunner {
399 clips: clip_states,
400 overlay_layers,
401 audio_only_tracks,
402 active: 0,
403 transition: None,
404 cmd_rx,
405 event_tx,
406 sink: None,
407 current_pts: Arc::clone(¤t_pts),
408 paused: Arc::clone(&paused),
409 stopped: Arc::clone(&stopped),
410 fps,
411 rate: 1.0,
412 clock: MasterClock::System {
413 started_at: Instant::now(),
414 base_pts: Duration::ZERO,
415 rate: 1.0,
416 },
417 resume_pts: Duration::ZERO,
418 sws_a: SwsRgbaConverter::new(),
419 sws_b: SwsRgbaConverter::new(),
420 rgba_a: Vec::new(),
421 rgba_b: Vec::new(),
422 blend_buf: Vec::new(),
423 last_frame_w: initial_last_w,
424 last_frame_h: initial_last_h,
425 gap_buf: Vec::new(),
426 audio_mixer: mixer_arc.clone(),
427 active_audio_cancel: initial_audio_cancel,
428 active_audio_thread: initial_audio_thread,
429 composer: None,
430 composer_key: Vec::new(),
431 canvas: scene.canvas,
432 lavfi: scene
433 .lavfi_overlay
434 .as_deref()
435 .and_then(LavfiOverlayState::new),
436 };
437
438 let handle = PlayerHandle::for_timeline(
439 cmd_tx,
440 Arc::new(Mutex::new(event_rx)),
441 current_pts,
442 paused,
443 stopped,
444 duration_millis,
445 mixer_arc,
446 );
447
448 Ok((runner, handle))
449 }
450}
451
452#[cfg(test)]
455mod tests {
456 use super::*;
457
458 #[test]
461 fn inner_blend_rgba_at_zero_alpha_should_return_a() {
462 let a = vec![255u8, 0, 0, 255];
463 let b = vec![0u8, 0, 255, 255];
464 let mut dst = Vec::new();
465 inner::blend_rgba(&a, &b, 0.0, &mut dst);
466 assert_eq!(dst, a);
467 }
468
469 #[test]
472 fn timeline_player_open_should_fail_when_no_video_tracks() {
473 let _ = PreviewError::SeekOutOfRange {
474 pts: Duration::from_secs(1),
475 };
476 }
477}