Skip to main content

ff_preview/playback/
player_handle.rs

1//! Shared, cloneable control handle for a running [`PlayerRunner`](super::player_runner::PlayerRunner).
2
3#[cfg(feature = "timeline")]
4use ff_pipeline::timeline::Timeline;
5use std::collections::VecDeque;
6use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
7use std::sync::{Arc, Mutex, mpsc};
8use std::time::Duration;
9
10pub(crate) use super::player::DECODED_SAMPLE_RATE;
11use super::player::PlayerCommand;
12use crate::audio::AudioMixer;
13use crate::event::PlayerEvent;
14
15// ── PlayerHandle ─────────────────────────────────────────────────────────────
16
17/// Shared, cloneable handle to a running [`PlayerRunner`](super::player_runner::PlayerRunner).
18///
19/// All methods are non-blocking. Commands that cannot be queued immediately
20/// (channel full) are silently dropped.
21///
22/// # Thread safety
23///
24/// `PlayerHandle` is `Clone + Send + Sync` and can be shared freely across
25/// threads without locking.
26#[derive(Clone)]
27pub struct PlayerHandle {
28    pub(crate) cmd_tx: mpsc::SyncSender<PlayerCommand>,
29    pub(crate) event_rx: Arc<Mutex<mpsc::Receiver<PlayerEvent>>>,
30    /// Current PTS in microseconds. Written by [`PlayerRunner`] on each frame.
31    pub(crate) current_pts: Arc<AtomicU64>,
32    pub(crate) audio_buf: Option<Arc<Mutex<VecDeque<f32>>>>,
33    /// Advances the audio master clock when `pop_audio_samples` drains samples.
34    pub(crate) samples_consumed: Option<Arc<AtomicU64>>,
35    /// Mirrors the runner's paused state; updated immediately by `play`/`pause`.
36    pub(crate) paused: Arc<AtomicBool>,
37    /// Mirrors the runner's stopped state; updated immediately by `stop`.
38    pub(crate) stopped: Arc<AtomicBool>,
39    pub(crate) duration_millis: u64,
40    /// Multi-track mixer — present when the runner was created by `TimelinePlayer`.
41    pub(crate) audio_mixer: Option<Arc<Mutex<AudioMixer>>>,
42}
43
44impl PlayerHandle {
45    /// Resume playback.
46    pub fn play(&self) {
47        self.stopped.store(false, Ordering::Release);
48        self.paused.store(false, Ordering::Release);
49        let _ = self.cmd_tx.try_send(PlayerCommand::Play);
50    }
51
52    /// Pause playback.
53    pub fn pause(&self) {
54        self.paused.store(true, Ordering::Release);
55        let _ = self.cmd_tx.try_send(PlayerCommand::Pause);
56    }
57
58    /// Stop the presentation loop.
59    pub fn stop(&self) {
60        self.stopped.store(true, Ordering::Release);
61        let _ = self.cmd_tx.try_send(PlayerCommand::Stop);
62    }
63
64    /// Seek to `pts`.
65    ///
66    /// Consecutive calls before the runner processes them are coalesced —
67    /// only the most recent `pts` executes.
68    pub fn seek(&self, pts: Duration) {
69        let _ = self.cmd_tx.try_send(PlayerCommand::Seek(pts));
70    }
71
72    /// Set the playback rate.
73    ///
74    /// - Positive values play forward at the given speed multiplier (e.g. `2.0` = 2×).
75    /// - Negative values play in reverse at `abs(rate)` speed (e.g. `-1.0` = 1× reverse).
76    ///   Audio is muted during reverse playback and automatically resumes on the next
77    ///   positive-rate call.
78    /// - `0.0` is ignored.
79    pub fn set_rate(&self, rate: f64) {
80        let _ = self.cmd_tx.try_send(PlayerCommand::SetRate(rate));
81    }
82
83    /// Set the A/V offset correction in milliseconds.
84    ///
85    /// Positive: video PTS is shifted down relative to audio (video appears
86    /// delayed). Negative: video PTS is shifted up (audio appears delayed).
87    pub fn set_av_offset(&self, ms: i64) {
88        let _ = self.cmd_tx.try_send(PlayerCommand::SetAvOffset(ms));
89    }
90
91    /// Replace the running timeline's clip layout in place.
92    ///
93    /// Sends a [`PlayerCommand::UpdateLayout`] to `TimelineRunner`. The runner
94    /// updates `timeline_start` / `timeline_end` / `in_point` / `out_point` for
95    /// every existing clip, stops audio decode threads, and seeks all decode
96    /// buffers to the last known media PTS — so the next presented frame is
97    /// spatially correct after the move.
98    ///
99    /// The `MasterClock` and `paused` / `stopped` atomics are unaffected.
100    /// Drops silently if the command channel (capacity 64) is full.
101    ///
102    /// No-op when called on a [`PlayerRunner`](super::player_runner::PlayerRunner)-backed
103    /// handle (single-track player). Only `TimelineRunner` handles this command.
104    #[cfg(feature = "timeline")]
105    pub fn update_timeline(&self, timeline: Timeline) {
106        let _ = self
107            .cmd_tx
108            .try_send(PlayerCommand::UpdateLayout(Box::new(timeline)));
109    }
110
111    /// PTS of the most recently presented frame.
112    ///
113    /// Returns [`Duration::ZERO`] before the first frame is presented.
114    #[must_use]
115    pub fn current_pts(&self) -> Duration {
116        Duration::from_micros(self.current_pts.load(Ordering::Relaxed))
117    }
118
119    /// Container-reported duration, or `None` for live / streaming sources.
120    #[must_use]
121    pub fn duration(&self) -> Option<Duration> {
122        if self.duration_millis == u64::MAX {
123            None
124        } else {
125            Some(Duration::from_millis(self.duration_millis))
126        }
127    }
128
129    /// Sample rate of the PCM data returned by [`pop_audio_samples`](Self::pop_audio_samples).
130    ///
131    /// Returns `Some(48_000)` for files that contain an audio stream, and
132    /// `None` for video-only files (where `pop_audio_samples` always returns
133    /// an empty `Vec`).
134    ///
135    /// Use this to configure your audio backend without hardcoding a magic
136    /// constant:
137    ///
138    /// ```ignore
139    /// let cfg = cpal::StreamConfig {
140    ///     channels: 2,
141    ///     sample_rate: cpal::SampleRate(handle.audio_sample_rate().unwrap_or(48_000)),
142    ///     ..Default::default()
143    /// };
144    /// ```
145    #[must_use]
146    pub fn audio_sample_rate(&self) -> Option<u32> {
147        self.audio_buf.as_ref().map(|_| DECODED_SAMPLE_RATE)
148    }
149
150    /// Pull up to `n` interleaved stereo `f32` PCM samples at 48 kHz.
151    ///
152    /// Returns an empty `Vec` when:
153    /// - playback is paused or stopped,
154    /// - `n` is 0,
155    /// - there is no audio track, or
156    /// - the ring buffer is empty (underrun — caller should output silence).
157    ///
158    /// Advances the audio master clock by `samples.len() / 2` stereo frames.
159    #[allow(clippy::cast_precision_loss)]
160    pub fn pop_audio_samples(&self, n: usize) -> Vec<f32> {
161        if self.paused.load(Ordering::Relaxed) || self.stopped.load(Ordering::Relaxed) {
162            return Vec::new();
163        }
164        if n == 0 {
165            return Vec::new();
166        }
167        // Mixer path — used when the handle was created by TimelinePlayer.
168        // The timeline clock is System-based so samples_consumed is not advanced here.
169        if let Some(mixer) = &self.audio_mixer {
170            return mixer
171                .lock()
172                .unwrap_or_else(std::sync::PoisonError::into_inner)
173                .mix(n);
174        }
175        // Legacy ring-buffer path — used by PlayerRunner (single-track audio).
176        let Some(buf) = &self.audio_buf else {
177            return Vec::new();
178        };
179        let mut guard = buf
180            .lock()
181            .unwrap_or_else(std::sync::PoisonError::into_inner);
182        let take = n.min(guard.len());
183        if take == 0 {
184            return Vec::new();
185        }
186        let samples: Vec<f32> = guard.drain(..take).collect();
187        if let Some(sc) = &self.samples_consumed {
188            sc.fetch_add((take / 2) as u64, Ordering::Relaxed);
189        }
190        samples
191    }
192
193    /// Pull up to `pop_n` interleaved stereo `f32` PCM samples at 48 kHz and
194    /// advance the A/V sync clock by exactly `clock_stereo_pairs` — independent
195    /// of how many samples are actually available in the ring buffer.
196    ///
197    /// Use this instead of [`pop_audio_samples`](Self::pop_audio_samples) when
198    /// playing at rates other than 1×.  The cpal callback pops `out_len * rate`
199    /// decoded samples to drive rate-scaled audio, but the master clock must
200    /// still advance at the **hardware** output rate (`out_len / 2` per callback)
201    /// so that `MasterClock::Audio`'s `pts_base + delta / sr * rate` formula
202    /// yields the correct media PTS without double-counting the rate.
203    ///
204    /// # Arguments
205    ///
206    /// * `pop_n` — decoded samples to drain from the ring buffer
207    ///   (`output_buf.len() * rate`, rounded).
208    /// * `clock_stereo_pairs` — hardware stereo pairs to add to the sync counter
209    ///   (`output_buf.len() / 2`, constant regardless of rate).
210    #[allow(clippy::cast_precision_loss)]
211    pub fn pop_audio_samples_for_rate(&self, pop_n: usize, clock_stereo_pairs: u64) -> Vec<f32> {
212        if self.paused.load(Ordering::Relaxed) || self.stopped.load(Ordering::Relaxed) {
213            // Clock still advances — the hardware keeps running even during silence.
214            if let Some(sc) = &self.samples_consumed {
215                sc.fetch_add(clock_stereo_pairs, Ordering::Relaxed);
216            }
217            return Vec::new();
218        }
219        if pop_n == 0 {
220            if let Some(sc) = &self.samples_consumed {
221                sc.fetch_add(clock_stereo_pairs, Ordering::Relaxed);
222            }
223            return Vec::new();
224        }
225        // Mixer path (TimelinePlayer) — System clock, no samples_consumed tracking.
226        if let Some(mixer) = &self.audio_mixer {
227            return mixer
228                .lock()
229                .unwrap_or_else(std::sync::PoisonError::into_inner)
230                .mix(pop_n);
231        }
232        // Ring-buffer path (PlayerRunner single-track audio).
233        let Some(buf) = &self.audio_buf else {
234            if let Some(sc) = &self.samples_consumed {
235                sc.fetch_add(clock_stereo_pairs, Ordering::Relaxed);
236            }
237            return Vec::new();
238        };
239        let mut guard = buf
240            .lock()
241            .unwrap_or_else(std::sync::PoisonError::into_inner);
242        let take = pop_n.min(guard.len());
243        let samples: Vec<f32> = if take > 0 {
244            guard.drain(..take).collect()
245        } else {
246            Vec::new()
247        };
248        drop(guard);
249        // Advance the clock by the hardware output size, not the decoded drain size.
250        if let Some(sc) = &self.samples_consumed {
251            sc.fetch_add(clock_stereo_pairs, Ordering::Relaxed);
252        }
253        samples
254    }
255
256    /// Poll for the next [`PlayerEvent`] without blocking.
257    ///
258    /// Returns `None` when no events are pending.
259    #[must_use]
260    pub fn poll_event(&self) -> Option<PlayerEvent> {
261        self.event_rx.lock().ok()?.try_recv().ok()
262    }
263
264    /// Block until the next [`PlayerEvent`] arrives or the channel closes.
265    ///
266    /// Returns `None` when the runner has exited and all events have been
267    /// drained. Intended for use inside `spawn_blocking`.
268    #[must_use]
269    pub fn recv_event(&self) -> Option<PlayerEvent> {
270        self.event_rx.lock().ok()?.recv().ok()
271    }
272
273    /// Construct a handle for a non-`PlayerRunner` runner (e.g., `TimelineRunner`).
274    ///
275    /// Audio fields are set to `None`; the handle's
276    /// [`pop_audio_samples`](Self::pop_audio_samples) always returns an empty `Vec`.
277    #[cfg(feature = "timeline")]
278    pub(crate) fn for_timeline(
279        cmd_tx: mpsc::SyncSender<PlayerCommand>,
280        event_rx: Arc<Mutex<mpsc::Receiver<PlayerEvent>>>,
281        current_pts: Arc<AtomicU64>,
282        paused: Arc<AtomicBool>,
283        stopped: Arc<AtomicBool>,
284        duration_millis: u64,
285        audio_mixer: Option<Arc<Mutex<AudioMixer>>>,
286    ) -> Self {
287        Self {
288            cmd_tx,
289            event_rx,
290            current_pts,
291            audio_buf: None,
292            samples_consumed: None,
293            audio_mixer,
294            paused,
295            stopped,
296            duration_millis,
297        }
298    }
299}