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
37// ── RgbaFrame / RgbaSink ──────────────────────────────────────────────────────
38
39/// A decoded video frame as contiguous RGBA bytes.
40///
41/// Produced by [`RgbaSink`] and stored behind an [`Arc<Mutex>`] so it can be
42/// shared safely with a rendering thread.
43pub struct RgbaFrame {
44    /// Row-major RGBA pixel data.
45    ///
46    /// Total size: `width * height * 4` bytes. Each pixel is 4 bytes
47    /// (R, G, B, A) with alpha always 255.
48    pub data: Vec<u8>,
49    /// Frame width in pixels.
50    pub width: u32,
51    /// Frame height in pixels.
52    pub height: u32,
53    /// Presentation timestamp of the frame.
54    pub pts: Duration,
55}
56
57/// Reference [`FrameSink`] implementation that stores the latest frame in a
58/// shared [`Arc<Mutex<Option<RgbaFrame>>>`].
59///
60/// Clone [`frame_handle`](Self::frame_handle) to share access with a rendering
61/// thread:
62///
63/// ```ignore
64/// let sink   = RgbaSink::new();
65/// let handle = sink.frame_handle();
66/// player.set_sink(Box::new(sink));
67///
68/// // In the render loop (any thread):
69/// if let Some(frame) = handle.lock().unwrap().as_ref() {
70///     upload_to_gpu(&frame.data, frame.width, frame.height);
71/// }
72/// ```
73///
74/// Only the **latest** frame is stored — not a queue. Renderers typically only
75/// need the current frame, not a backlog.
76pub struct RgbaSink {
77    /// Shared storage for the most recently received RGBA frame.
78    pub last_frame: Arc<Mutex<Option<RgbaFrame>>>,
79}
80
81impl RgbaSink {
82    /// Create a new `RgbaSink` with an empty frame store.
83    #[must_use]
84    pub fn new() -> Self {
85        Self {
86            last_frame: Arc::new(Mutex::new(None)),
87        }
88    }
89
90    /// Clone the [`Arc`] for sharing with the rendering thread.
91    #[must_use]
92    pub fn frame_handle(&self) -> Arc<Mutex<Option<RgbaFrame>>> {
93        Arc::clone(&self.last_frame)
94    }
95}
96
97impl Default for RgbaSink {
98    fn default() -> Self {
99        Self::new()
100    }
101}
102
103impl FrameSink for RgbaSink {
104    fn push_frame(&mut self, rgba: &[u8], width: u32, height: u32, pts: Duration) {
105        let mut guard = self
106            .last_frame
107            .lock()
108            .unwrap_or_else(std::sync::PoisonError::into_inner);
109        *guard = Some(RgbaFrame {
110            data: rgba.to_vec(),
111            width,
112            height,
113            pts,
114        });
115    }
116    // flush() inherits the default no-op
117}
118
119// ── Tests ─────────────────────────────────────────────────────────────────────
120
121#[cfg(test)]
122mod tests {
123    use super::*;
124
125    #[test]
126    fn frame_sink_should_be_object_safe() {
127        // Verify the trait is object-safe: this must compile.
128        let _: Option<Box<dyn FrameSink>> = None;
129    }
130
131    #[test]
132    fn frame_sink_flush_default_should_be_a_noop() {
133        struct NoFlushSink;
134        impl FrameSink for NoFlushSink {
135            fn push_frame(&mut self, _rgba: &[u8], _width: u32, _height: u32, _pts: Duration) {}
136            // flush() intentionally NOT overridden — test the default is safe to call.
137        }
138        let mut sink = NoFlushSink;
139        sink.flush(); // must not panic
140    }
141
142    #[test]
143    fn rgba_sink_should_store_latest_frame_on_push() {
144        let mut sink = RgbaSink::new();
145        let handle = sink.frame_handle();
146
147        // Before any push, the frame is None.
148        assert!(
149            handle
150                .lock()
151                .unwrap_or_else(std::sync::PoisonError::into_inner)
152                .is_none(),
153            "frame_handle must be None before any push"
154        );
155
156        let rgba: Vec<u8> = vec![255u8, 0, 0, 255, 0, 255, 0, 255]; // 2 × 1 RGBA
157        let pts = Duration::from_millis(100);
158        sink.push_frame(&rgba, 2, 1, pts);
159
160        let guard = handle
161            .lock()
162            .unwrap_or_else(std::sync::PoisonError::into_inner);
163        let frame = guard.as_ref().expect("frame must be Some after push");
164
165        assert_eq!(frame.width, 2);
166        assert_eq!(frame.height, 1);
167        assert_eq!(frame.pts, pts);
168        assert_eq!(frame.data, rgba);
169    }
170
171    #[test]
172    fn rgba_sink_should_replace_frame_on_second_push() {
173        let mut sink = RgbaSink::new();
174        let handle = sink.frame_handle();
175
176        let first: Vec<u8> = vec![1, 2, 3, 255];
177        let second: Vec<u8> = vec![9, 8, 7, 255];
178        let pts1 = Duration::from_millis(0);
179        let pts2 = Duration::from_millis(33);
180
181        sink.push_frame(&first, 1, 1, pts1);
182        sink.push_frame(&second, 1, 1, pts2);
183
184        let guard = handle
185            .lock()
186            .unwrap_or_else(std::sync::PoisonError::into_inner);
187        let frame = guard.as_ref().expect("frame must be Some after two pushes");
188        assert_eq!(
189            frame.data, second,
190            "latest push must overwrite previous frame"
191        );
192        assert_eq!(frame.pts, pts2);
193    }
194
195    #[test]
196    fn rgba_sink_default_should_equal_new() {
197        let a = RgbaSink::new();
198        let b = RgbaSink::default();
199        // Both must start with None.
200        assert!(
201            a.frame_handle()
202                .lock()
203                .unwrap_or_else(std::sync::PoisonError::into_inner)
204                .is_none()
205        );
206        assert!(
207            b.frame_handle()
208                .lock()
209                .unwrap_or_else(std::sync::PoisonError::into_inner)
210                .is_none()
211        );
212    }
213}