wlr_capture/sink.rs
1//! Common output seam for capture-consuming tools (screenshot, record, timelapse).
2//!
3//! A capture round yields a [`Frame`] — CPU pixels (shm) or a GPU dma-buf
4//! (zero-copy). A [`FrameSink`] consumes a stream of those frames. Most sinks only
5//! want CPU pixels, so the default [`FrameSink::push_dmabuf`] reads the dma-buf back
6//! via a [`GpuReadback`] and forwards to [`FrameSink::push`]; a GPU-native sink (a
7//! future hardware video encoder, say) can override it to keep the buffer on the
8//! GPU. [`pump`] routes a `Frame` to whichever path applies, creating the readback
9//! lazily so pure-shm streams never spin up an EGL context.
10
11use crate::gl::GpuReadback;
12use crate::wl::{CapturedImage, DmabufFrame, Frame};
13use anyhow::Result;
14use std::time::Duration;
15
16/// A consumer of a capture stream. `ts` is each frame's capture time relative to a
17/// start the sink defines (a screenshot ignores it; a recorder uses it for timing).
18pub trait FrameSink {
19 /// Consume one CPU-pixel frame.
20 fn push(&mut self, img: &CapturedImage, ts: Duration) -> Result<()>;
21
22 /// Feed interleaved PCM for an optional audio track. The default ignores it (sinks
23 /// without sound — GIF/WebP — and silent recordings); the video encoder buffers it
24 /// and muxes it on the next [`FrameSink::push`].
25 fn push_audio(&mut self, _pcm: &[f32]) {}
26
27 /// Consume one GPU dma-buf frame. The default reads it back to CPU pixels via
28 /// `rb` and forwards to [`FrameSink::push`]; override to consume it on the GPU.
29 fn push_dmabuf(
30 &mut self,
31 rb: &mut GpuReadback,
32 frame: DmabufFrame,
33 ts: Duration,
34 ) -> Result<()> {
35 let img = rb.readback(frame)?;
36 self.push(&img, ts)
37 }
38
39 /// Flush and finalize (write the file, close the encoder, …). Call once, last.
40 fn finish(&mut self) -> Result<()> {
41 Ok(())
42 }
43}
44
45/// Route one [`Frame`] to `sink`, picking the CPU or dma-buf path. The readback
46/// context lives in `rb` and is built on first need — a stream that only ever
47/// produces shm frames (a no-GPU build) never constructs one. Hold a single
48/// `Option<GpuReadback>` across the whole stream so the context is reused.
49pub fn pump(
50 sink: &mut dyn FrameSink,
51 rb: &mut Option<GpuReadback>,
52 frame: Frame,
53 ts: Duration,
54) -> Result<()> {
55 match frame {
56 Frame::Shm(img) => sink.push(&img, ts),
57 Frame::Dmabuf(d) => {
58 let rb = match rb {
59 Some(rb) => rb,
60 None => rb.insert(GpuReadback::new()?),
61 };
62 sink.push_dmabuf(rb, d, ts)
63 }
64 }
65}
66
67#[cfg(test)]
68mod tests {
69 use super::*;
70
71 /// A sink that records what it received — exercises the shm path of [`pump`]
72 /// and the trait's stamping without needing a GPU.
73 #[derive(Default)]
74 struct Collect {
75 frames: Vec<(u32, u32, Duration)>,
76 finished: bool,
77 }
78
79 impl FrameSink for Collect {
80 fn push(&mut self, img: &CapturedImage, ts: Duration) -> Result<()> {
81 self.frames.push((img.width, img.height, ts));
82 Ok(())
83 }
84 fn finish(&mut self) -> Result<()> {
85 self.finished = true;
86 Ok(())
87 }
88 }
89
90 #[test]
91 fn pump_routes_shm_frames() {
92 let mut sink = Collect::default();
93 let mut rb = None; // never built: no dma-buf frames in this stream
94 let img = CapturedImage {
95 width: 4,
96 height: 2,
97 rgba: vec![0; 4 * 2 * 4],
98 };
99 pump(
100 &mut sink,
101 &mut rb,
102 Frame::Shm(img),
103 Duration::from_millis(40),
104 )
105 .unwrap();
106 sink.finish().unwrap();
107
108 assert!(rb.is_none());
109 assert_eq!(sink.frames, vec![(4, 2, Duration::from_millis(40))]);
110 assert!(sink.finished);
111 }
112}