Skip to main content

ff_preview/playback/
player.rs

1//! Actor-model playback types for ff-preview.
2//!
3//! This module holds [`PreviewPlayer`] (the thin builder) and [`PlayerCommand`].
4//! The implementation of the split parts lives in sibling modules:
5//! - `player_handle`: [`PlayerHandle`]
6//! - `player_runner`: [`PlayerRunner`] + `spawn_audio_thread`
7
8use std::collections::VecDeque;
9use std::path::{Path, PathBuf};
10use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
11use std::sync::{Arc, Mutex, mpsc};
12use std::thread::JoinHandle;
13use std::time::{Duration, Instant};
14
15use ff_decode::HardwareAccel;
16#[cfg(feature = "timeline")]
17use ff_pipeline::timeline::Timeline;
18
19use super::decode_buffer::DecodeBuffer;
20use super::master_clock::MasterClock;
21use super::player_handle::PlayerHandle;
22use super::player_runner::{PlayerRunner, spawn_audio_thread};
23
24use crate::error::PreviewError;
25
26// -- Constants -----------------------------------------------------------
27
28const CHANNEL_CAP: usize = 64;
29/// Fixed output sample rate of the audio decode thread.
30///
31/// Must match the value used by `spawn_audio_thread` and `MasterClock::Audio`.
32pub(crate) const DECODED_SAMPLE_RATE: u32 = 48_000;
33
34// ── PlayerCommand ─────────────────────────────────────────────────────────────
35
36/// Commands sent from [`PlayerHandle`] to [`PlayerRunner`] via a
37/// bounded sync channel (capacity 64).
38pub enum PlayerCommand {
39    /// Resume playback (clear the paused flag).
40    Play,
41    /// Pause playback.
42    Pause,
43    /// Stop the presentation loop; [`PlayerRunner::run`] returns after the
44    /// current frame.
45    Stop,
46    /// Seek to `pts`. Consecutive seeks are coalesced — only the last one
47    /// executes.
48    Seek(Duration),
49    /// Set the playback rate. Values ≤ 0.0 are ignored.
50    SetRate(f64),
51    /// Set the A/V offset in milliseconds. Clamped to ±5 000 ms.
52    SetAvOffset(i64),
53    /// Replace the timeline clip layout without stopping playback.
54    ///
55    /// Handled only by `TimelineRunner`; `PlayerRunner` ignores it.
56    /// The runner updates its internal `ClipState` / `AudioOnlyTrack` positions
57    /// in place and seeks to the last known media PTS so the next frame is
58    /// spatially correct after the layout change.
59    #[cfg(feature = "timeline")]
60    UpdateLayout(Box<Timeline>),
61}
62
63// ── PreviewPlayer (thin builder) ──────────────────────────────────────────────
64
65/// Thin builder for a ([`PlayerRunner`], [`PlayerHandle`]) pair.
66///
67/// # Usage
68///
69/// ```ignore
70/// let (mut runner, handle) = PreviewPlayer::open("clip.mp4")?.split();
71///
72/// runner.set_sink(Box::new(MySink::new()));
73///
74/// let handle_audio = handle.clone();
75///
76/// std::thread::spawn(move || { let _ = runner.run(); });
77///
78/// handle.seek(Duration::from_secs(30));
79/// handle.play();
80///
81/// // cpal audio callback:
82/// device.build_output_stream(&cfg, move |buf: &mut [f32], _| {
83///     let s = handle_audio.pop_audio_samples(buf.len());
84///     buf[..s.len()].copy_from_slice(&s);
85/// }, ...);
86/// ```
87pub struct PreviewPlayer {
88    path: PathBuf,
89    /// `None` after `split()` consumes it.
90    decode_buf: Option<DecodeBuffer>,
91    fps: f64,
92    /// `None` after `split()` consumes it.
93    clock: Option<MasterClock>,
94    audio_buf: Option<Arc<Mutex<VecDeque<f32>>>>,
95    audio_cancel: Option<Arc<AtomicBool>>,
96    audio_handle: Option<JoinHandle<()>>,
97    duration_millis: u64,
98    active_path: PathBuf,
99}
100
101impl PreviewPlayer {
102    /// Open a media file and prepare for playback.
103    ///
104    /// Probes the file to detect audio/video streams, opens a
105    /// [`DecodeBuffer`] for the video stream (when present), and spawns a
106    /// background audio decode thread (when present). Returns
107    /// [`PreviewError`] if the file is missing or contains neither stream.
108    ///
109    /// # Errors
110    ///
111    /// Returns [`PreviewError`] if the file cannot be probed or decoded.
112    pub fn open(path: impl AsRef<Path>) -> Result<Self, PreviewError> {
113        let path = path.as_ref();
114        let info = ff_probe::open(path)?;
115
116        if !info.has_video() && !info.has_audio() {
117            return Err(PreviewError::Ffmpeg {
118                code: -1,
119                message: "file has neither a video nor an audio stream".into(),
120            });
121        }
122
123        let fps = info.frame_rate().unwrap_or(30.0).max(1.0);
124
125        let d = info.duration();
126        let duration_millis = if d.is_zero() {
127            u64::MAX
128        } else {
129            u64::try_from(d.as_millis()).unwrap_or(u64::MAX)
130        };
131
132        let clock = if info.has_audio() {
133            MasterClock::Audio {
134                samples_consumed: Arc::new(AtomicU64::new(0)),
135                sample_rate: DECODED_SAMPLE_RATE,
136                rate: 1.0,
137                samples_base: 0,
138                pts_base: Duration::ZERO,
139                fallback: None,
140            }
141        } else {
142            log::debug!(
143                "using system clock fallback path={} no_audio=true",
144                path.display()
145            );
146            MasterClock::System {
147                started_at: Instant::now(),
148                base_pts: Duration::ZERO,
149                rate: 1.0,
150            }
151        };
152
153        let decode_buf = if info.has_video() {
154            Some(DecodeBuffer::open(path).build()?)
155        } else {
156            log::debug!(
157                "audio-only file; skipping video decode buffer path={}",
158                path.display()
159            );
160            None
161        };
162
163        let (audio_buf, audio_cancel, audio_handle) = if let MasterClock::Audio { .. } = &clock {
164            let buf = Arc::new(Mutex::new(VecDeque::<f32>::new()));
165            let cancel = Arc::new(AtomicBool::new(false));
166            let handle = spawn_audio_thread(
167                path.to_path_buf(),
168                Duration::ZERO,
169                Arc::clone(&buf),
170                Arc::clone(&cancel),
171            );
172            (Some(buf), Some(cancel), Some(handle))
173        } else {
174            (None, None, None)
175        };
176
177        Ok(PreviewPlayer {
178            path: path.to_path_buf(),
179            decode_buf,
180            fps,
181            clock: Some(clock),
182            audio_buf,
183            audio_cancel,
184            audio_handle,
185            duration_millis,
186            active_path: path.to_path_buf(),
187        })
188    }
189
190    /// Consume `self` and return an exclusive [`PlayerRunner`] and a shared
191    /// [`PlayerHandle`].
192    ///
193    /// The runner owns the decode pipeline; move it to a background thread
194    /// and call [`PlayerRunner::run`].
195    /// The handle is `Clone + Send + Sync` and can be shared freely.
196    ///
197    /// # Panics
198    ///
199    /// Never panics in practice — the internal clock is always `Some` when
200    /// `split` is first called.
201    #[must_use]
202    #[allow(clippy::expect_used)]
203    pub fn split(mut self) -> (PlayerRunner, PlayerHandle) {
204        let current_pts = Arc::new(AtomicU64::new(0));
205        let paused = Arc::new(AtomicBool::new(false));
206        let stopped = Arc::new(AtomicBool::new(false));
207        let (cmd_tx, cmd_rx) = mpsc::sync_channel(CHANNEL_CAP);
208        let (event_tx, event_rx) = mpsc::sync_channel(CHANNEL_CAP);
209
210        let clock = self.clock.take().expect("clock consumed before split");
211        let samples_consumed = match &clock {
212            MasterClock::Audio {
213                samples_consumed, ..
214            } => Some(Arc::clone(samples_consumed)),
215            MasterClock::System { .. } => None,
216        };
217
218        let audio_buf_for_handle = self.audio_buf.clone();
219        let duration_millis = self.duration_millis;
220
221        let runner = PlayerRunner {
222            path: self.path.clone(),
223            cmd_rx,
224            event_tx,
225            decode_buf: self.decode_buf.take(),
226            fps: self.fps,
227            sink: None,
228            clock,
229            audio_buf: self.audio_buf.take(),
230            audio_cancel: self.audio_cancel.take(),
231            audio_handle: self.audio_handle.take(),
232            sws: super::playback_inner::SwsRgbaConverter::new(),
233            rgba_buf: Vec::new(),
234            active_path: self.active_path.clone(),
235            current_pts: Arc::clone(&current_pts),
236            paused: Arc::clone(&paused),
237            stopped: Arc::clone(&stopped),
238            av_offset_ms: 0,
239            rate: 1.0,
240            duration_millis,
241            frame_cache: None,
242            hw_accel: HardwareAccel::Auto,
243        };
244
245        let handle = PlayerHandle {
246            cmd_tx,
247            event_rx: Arc::new(Mutex::new(event_rx)),
248            current_pts,
249            audio_buf: audio_buf_for_handle,
250            samples_consumed,
251            audio_mixer: None,
252            paused,
253            stopped,
254            duration_millis,
255        };
256
257        (runner, handle)
258    }
259}
260
261impl Drop for PreviewPlayer {
262    fn drop(&mut self) {
263        if let Some(cancel) = &self.audio_cancel {
264            cancel.store(true, Ordering::Release);
265        }
266        if let Some(h) = self.audio_handle.take() {
267            let _ = h.join();
268        }
269    }
270}