Skip to main content

wallr_core/video/
playback.rs

1//! Video playback manager for integrating the decoder with the renderer.
2//!
3//! All methods are synchronous: the playback state is guarded by a plain
4//! `std::sync::Mutex`, so the same manager can be driven from the blocking
5//! render task (which presents every vsync) and from the async IPC handlers.
6//! Locks are only ever held for short, allocation-free critical sections.
7
8use crate::video::{
9    DecoderInfo, FrameScheduler, HwAccel, VideoDecoder, VideoError, VideoFrame, VideoMetadata,
10    VideoResult,
11};
12use std::path::Path;
13use std::sync::Mutex;
14use std::time::{Duration, Instant};
15
16/// Video playback state manager.
17pub struct VideoPlayback {
18    decoder: Mutex<Option<VideoDecoder>>,
19    scheduler: Mutex<Option<FrameScheduler>>,
20    current_frame: Mutex<Option<VideoFrame>>,
21}
22
23impl VideoPlayback {
24    /// Create a new video playback manager.
25    pub fn new() -> Self {
26        Self {
27            decoder: Mutex::new(None),
28            scheduler: Mutex::new(None),
29            current_frame: Mutex::new(None),
30        }
31    }
32
33    /// Start video playback from a file, replacing any active playback.
34    pub fn start(&self, path: &Path, hw_accel: HwAccel) -> VideoResult<VideoMetadata> {
35        // Stop the previous playback first: this drops the old decoder and
36        // joins its thread, releasing decoder buffers immediately.
37        self.stop();
38
39        let decoder = VideoDecoder::new(path, hw_accel)?;
40        let metadata = decoder.metadata().clone();
41        let scheduler = FrameScheduler::new(metadata.duration);
42
43        *self.lock_decoder() = Some(decoder);
44        *self.lock_scheduler() = Some(scheduler);
45
46        tracing::info!(
47            "Video playback started: {}x{} @ {:.2} fps, duration: {:?}",
48            metadata.width,
49            metadata.height,
50            metadata.fps,
51            metadata.duration
52        );
53
54        Ok(metadata)
55    }
56
57    /// Stop video playback and release all resources immediately.
58    pub fn stop(&self) {
59        *self.lock_decoder() = None;
60        *self.lock_scheduler() = None;
61        *self.lock_current() = None;
62    }
63
64    /// Pause playback: the display loop keeps presenting the last frame, the
65    /// scheduler freezes its clock, and the decode thread stops decoding so
66    /// the CPU stays idle.
67    pub fn pause(&self) {
68        if let Some(scheduler) = self.lock_scheduler().as_mut() {
69            scheduler.pause();
70        }
71        if let Some(decoder) = self.lock_decoder().as_ref() {
72            decoder.pause();
73        }
74    }
75
76    /// Resume playback after a pause.
77    pub fn resume(&self) {
78        if let Some(scheduler) = self.lock_scheduler().as_mut() {
79            scheduler.resume();
80        }
81        if let Some(decoder) = self.lock_decoder().as_ref() {
82            decoder.resume();
83        }
84    }
85
86    /// Seek to a specific timestamp. Both the scheduler clock and the decoder
87    /// stream are repositioned, and stale queued frames are discarded.
88    pub fn seek(&self, timestamp: Duration) -> VideoResult<()> {
89        {
90            let mut decoder = self.lock_decoder();
91            let mut scheduler = self.lock_scheduler();
92
93            let scheduler = scheduler.as_mut().ok_or_else(|| {
94                VideoError::SeekFailed(timestamp, anyhow::anyhow!("no video is playing"))
95            })?;
96            scheduler.seek(timestamp)?;
97
98            if let Some(decoder) = decoder.as_mut() {
99                decoder.seek(timestamp);
100                decoder.drain();
101            }
102        }
103        tracing::info!("Video playback seeked to {:?}", timestamp);
104        Ok(())
105    }
106
107    /// Pull the next frame that should be presented.
108    ///
109    /// Returns `Some(frame)` when a new frame must be uploaded to the GPU, and
110    /// `None` when the currently shown texture should be presented unchanged
111    /// (duplicate frame, decoder behind, or playback paused).
112    pub fn next_frame(&self) -> Option<VideoFrame> {
113        let mut decoder = self.lock_decoder();
114        let mut scheduler = self.lock_scheduler();
115
116        let (Some(decoder), Some(scheduler)) = (decoder.as_mut(), scheduler.as_mut()) else {
117            return None;
118        };
119
120        let mut newest: Option<VideoFrame> = None;
121        while let Some(frame) = decoder.next_frame() {
122            if scheduler.should_display(frame.pts) {
123                if scheduler.should_upload(frame.pts) {
124                    *self.lock_current() = Some(frame.clone());
125                    return Some(frame);
126                }
127                // Duplicate of the last uploaded frame: keep the newest one
128                // around in case the consumer only wants the current frame.
129                newest = Some(frame);
130            }
131        }
132        let _ = newest;
133        None
134    }
135
136    /// Block until the first frame is available or the timeout elapses.
137    ///
138    /// Used at commit time so the transition's incoming image is the video's
139    /// actual first frame instead of a black placeholder.
140    pub fn wait_first_frame(&self, timeout: Duration) -> Option<VideoFrame> {
141        let deadline = Instant::now() + timeout;
142        loop {
143            if let Some(frame) = self.next_frame() {
144                return Some(frame);
145            }
146            if Instant::now() >= deadline {
147                tracing::warn!("Timed out waiting for the first video frame");
148                return None;
149            }
150            thread_sleep(5);
151        }
152    }
153
154    /// Get the last presented frame without advancing playback.
155    pub fn current_frame(&self) -> Option<VideoFrame> {
156        self.lock_current().clone()
157    }
158
159    /// Get video metadata of the active playback.
160    pub fn metadata(&self) -> Option<VideoMetadata> {
161        self.lock_decoder().as_ref().map(|d| d.metadata().clone())
162    }
163
164    /// Get decoder diagnostics for the active playback.
165    pub fn decoder_info(&self) -> Option<DecoderInfo> {
166        self.lock_decoder().as_ref().map(|d| d.decoder_info())
167    }
168
169    /// The hardware backend actually used by the active decoder.
170    pub fn hw_accel_in_use(&self) -> HwAccel {
171        self.lock_decoder()
172            .as_ref()
173            .map(VideoDecoder::hw_accel_in_use)
174            .unwrap_or(HwAccel::Software)
175    }
176
177    /// Get the current playback position.
178    pub fn position(&self) -> Option<Duration> {
179        self.lock_scheduler().as_ref().map(|s| s.current_position())
180    }
181
182    /// Check if playback is paused.
183    pub fn is_paused(&self) -> bool {
184        self.lock_scheduler()
185            .as_ref()
186            .map(|s| s.is_paused())
187            .unwrap_or(false)
188    }
189
190    /// Check if a video is currently active.
191    pub fn is_playing(&self) -> bool {
192        self.lock_decoder().is_some()
193    }
194
195    fn lock_decoder(&self) -> std::sync::MutexGuard<'_, Option<VideoDecoder>> {
196        self.decoder.lock().unwrap_or_else(|p| p.into_inner())
197    }
198
199    fn lock_scheduler(&self) -> std::sync::MutexGuard<'_, Option<FrameScheduler>> {
200        self.scheduler.lock().unwrap_or_else(|p| p.into_inner())
201    }
202
203    fn lock_current(&self) -> std::sync::MutexGuard<'_, Option<VideoFrame>> {
204        self.current_frame.lock().unwrap_or_else(|p| p.into_inner())
205    }
206}
207
208/// Small sleep helper (kept behind a name so the intent is obvious in
209/// `wait_first_frame`).
210fn thread_sleep(ms: u64) {
211    std::thread::sleep(Duration::from_millis(ms));
212}
213
214impl Default for VideoPlayback {
215    fn default() -> Self {
216        Self::new()
217    }
218}