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}