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}