Skip to main content

ff_preview/playback/
sink.rs

1//! Frame sink types for ff-preview.
2//!
3//! [`FrameSink`] is the primary trait for receiving decoded video frames.
4//! [`RgbaSink`] is the reference implementation that stores the latest frame
5//! behind an [`Arc<Mutex>`] for rendering-thread access.
6
7use std::sync::{Arc, Mutex};
8use std::time::Duration;
9
10// FrameSink
11
12/// A sink that receives decoded video frames as contiguous RGBA bytes.
13///
14/// Implementations must be `Send` — [`PlayerRunner`](super::PlayerRunner) calls
15/// `push_frame` from a dedicated presentation thread.
16///
17/// # Threading
18///
19/// `push_frame` is called exclusively from [`PlayerRunner::run`](super::PlayerRunner::run).
20/// Do **not** call back into [`PlayerRunner`](super::PlayerRunner) from inside
21/// `push_frame` — this will deadlock.
22pub trait FrameSink: Send {
23    /// Receive a video frame at its presentation time.
24    ///
25    /// `rgba` is a contiguous, row-major RGBA buffer:
26    /// - 4 bytes per pixel (R, G, B, A), alpha always 255
27    /// - Total size: `width * height * 4` bytes
28    /// - Row stride: `width * 4` bytes (no padding)
29    fn push_frame(&mut self, rgba: &[u8], width: u32, height: u32, pts: Duration);
30
31    /// Called when playback ends (EOF or [`PlayerHandle::stop`](super::PlayerHandle::stop)). Default: no-op.
32    ///
33    /// Implementations should flush any pending output here.
34    fn flush(&mut self) {}
35
36    /// Whether this sink can receive a GPU-resident frame via
37    /// [`push_frame_gpu`](Self::push_frame_gpu). Default: `false` (CPU only).
38    ///
39    /// A GPU compositor queries this to decide whether it can hand off a
40    /// composited texture directly (no GPU-to-CPU readback) or must fall back to
41    /// the CPU [`push_frame`](Self::push_frame) path.
42    #[cfg(feature = "display")]
43    fn accepts_gpu_frame(&self) -> bool {
44        false
45    }
46
47    /// Receive a composited frame as a GPU texture, avoiding a GPU-to-CPU
48    /// readback. Only called when [`accepts_gpu_frame`](Self::accepts_gpu_frame)
49    /// returns `true`; the default ignores the frame.
50    ///
51    /// `texture` is a row-major RGBA texture (`view` is its default view) of
52    /// `width × height`. It is owned by the caller for the duration of the call;
53    /// a sink that needs it beyond the call must copy or clone it.
54    #[cfg(feature = "display")]
55    fn push_frame_gpu(
56        &mut self,
57        texture: &wgpu::Texture,
58        view: &wgpu::TextureView,
59        width: u32,
60        height: u32,
61        pts: Duration,
62    ) {
63        let _ = (texture, view, width, height, pts);
64    }
65}
66
67// RgbaFrame / RgbaSink
68
69/// A decoded video frame as contiguous RGBA bytes.
70///
71/// Produced by [`RgbaSink`] and stored behind an [`Arc<Mutex>`] so it can be
72/// shared safely with a rendering thread.
73pub struct RgbaFrame {
74    /// Row-major RGBA pixel data.
75    ///
76    /// Total size: `width * height * 4` bytes. Each pixel is 4 bytes
77    /// (R, G, B, A) with alpha always 255.
78    pub data: Vec<u8>,
79    /// Frame width in pixels.
80    pub width: u32,
81    /// Frame height in pixels.
82    pub height: u32,
83    /// Presentation timestamp of the frame.
84    pub pts: Duration,
85}
86
87/// Reference [`FrameSink`] implementation that stores the latest frame in a
88/// shared [`Arc<Mutex<Option<RgbaFrame>>>`].
89///
90/// Clone [`frame_handle`](Self::frame_handle) to share access with a rendering
91/// thread:
92///
93/// ```ignore
94/// let sink   = RgbaSink::new();
95/// let handle = sink.frame_handle();
96/// player.set_sink(Box::new(sink));
97///
98/// // In the render loop (any thread):
99/// if let Some(frame) = handle.lock().unwrap().as_ref() {
100///     upload_to_gpu(&frame.data, frame.width, frame.height);
101/// }
102/// ```
103///
104/// Only the **latest** frame is stored — not a queue. Renderers typically only
105/// need the current frame, not a backlog.
106pub struct RgbaSink {
107    /// Shared storage for the most recently received RGBA frame.
108    pub last_frame: Arc<Mutex<Option<RgbaFrame>>>,
109}
110
111impl RgbaSink {
112    /// Create a new `RgbaSink` with an empty frame store.
113    #[must_use]
114    pub fn new() -> Self {
115        Self {
116            last_frame: Arc::new(Mutex::new(None)),
117        }
118    }
119
120    /// Clone the [`Arc`] for sharing with the rendering thread.
121    #[must_use]
122    pub fn frame_handle(&self) -> Arc<Mutex<Option<RgbaFrame>>> {
123        Arc::clone(&self.last_frame)
124    }
125}
126
127impl Default for RgbaSink {
128    fn default() -> Self {
129        Self::new()
130    }
131}
132
133impl FrameSink for RgbaSink {
134    fn push_frame(&mut self, rgba: &[u8], width: u32, height: u32, pts: Duration) {
135        let mut guard = self
136            .last_frame
137            .lock()
138            .unwrap_or_else(std::sync::PoisonError::into_inner);
139        *guard = Some(RgbaFrame {
140            data: rgba.to_vec(),
141            width,
142            height,
143            pts,
144        });
145    }
146    // flush() inherits the default no-op
147}
148
149#[cfg(test)]
150mod tests {
151    use super::*;
152
153    #[test]
154    fn frame_sink_should_be_object_safe() {
155        // Verify the trait is object-safe: this must compile.
156        let _: Option<Box<dyn FrameSink>> = None;
157    }
158
159    #[test]
160    fn frame_sink_flush_default_should_be_a_noop() {
161        struct NoFlushSink;
162        impl FrameSink for NoFlushSink {
163            fn push_frame(&mut self, _rgba: &[u8], _width: u32, _height: u32, _pts: Duration) {}
164            // flush() intentionally NOT overridden — test the default is safe to call.
165        }
166        let mut sink = NoFlushSink;
167        sink.flush(); // must not panic
168    }
169
170    #[test]
171    fn rgba_sink_should_store_latest_frame_on_push() {
172        let mut sink = RgbaSink::new();
173        let handle = sink.frame_handle();
174
175        // Before any push, the frame is None.
176        assert!(
177            handle
178                .lock()
179                .unwrap_or_else(std::sync::PoisonError::into_inner)
180                .is_none(),
181            "frame_handle must be None before any push"
182        );
183
184        let rgba: Vec<u8> = vec![255u8, 0, 0, 255, 0, 255, 0, 255]; // 2 × 1 RGBA
185        let pts = Duration::from_millis(100);
186        sink.push_frame(&rgba, 2, 1, pts);
187
188        let guard = handle
189            .lock()
190            .unwrap_or_else(std::sync::PoisonError::into_inner);
191        let frame = guard.as_ref().expect("frame must be Some after push");
192
193        assert_eq!(frame.width, 2);
194        assert_eq!(frame.height, 1);
195        assert_eq!(frame.pts, pts);
196        assert_eq!(frame.data, rgba);
197    }
198
199    #[test]
200    fn rgba_sink_should_replace_frame_on_second_push() {
201        let mut sink = RgbaSink::new();
202        let handle = sink.frame_handle();
203
204        let first: Vec<u8> = vec![1, 2, 3, 255];
205        let second: Vec<u8> = vec![9, 8, 7, 255];
206        let pts1 = Duration::from_millis(0);
207        let pts2 = Duration::from_millis(33);
208
209        sink.push_frame(&first, 1, 1, pts1);
210        sink.push_frame(&second, 1, 1, pts2);
211
212        let guard = handle
213            .lock()
214            .unwrap_or_else(std::sync::PoisonError::into_inner);
215        let frame = guard.as_ref().expect("frame must be Some after two pushes");
216        assert_eq!(
217            frame.data, second,
218            "latest push must overwrite previous frame"
219        );
220        assert_eq!(frame.pts, pts2);
221    }
222
223    #[test]
224    fn rgba_sink_default_should_equal_new() {
225        let a = RgbaSink::new();
226        let b = RgbaSink::default();
227        // Both must start with None.
228        assert!(
229            a.frame_handle()
230                .lock()
231                .unwrap_or_else(std::sync::PoisonError::into_inner)
232                .is_none()
233        );
234        assert!(
235            b.frame_handle()
236                .lock()
237                .unwrap_or_else(std::sync::PoisonError::into_inner)
238                .is_none()
239        );
240    }
241}