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}