wlr_capture/stream.rs
1//! Shared driver for a live capture session over one source.
2//!
3//! The capture engine delivers frames one at a time per session, re-armed each
4//! round, and a session can stop transiently (e.g. on resize) or for good (the
5//! window closed). Every streaming consumer — the live mirror, the recorder, the
6//! change monitor — needs the same arm / poll / reopen / give-up loop. [`Stream`]
7//! is that loop, factored out: hold one, call [`Stream::step`] each round, and act
8//! on the [`Step`] it returns.
9//!
10//! It deliberately yields raw [`Frame`]s (not decoded pixels): the mirror imports
11//! dma-buf frames straight into a GL texture (zero-copy), while the recorder and
12//! monitor read them back to CPU pixels. The driver stays in the dependency-free
13//! core so any tool can use it.
14
15use crate::gl::GpuReadback;
16use crate::wl::{CapturedImage, Client, Frame, SessionId};
17use anyhow::Result;
18use std::time::{Duration, Instant};
19
20/// Default time to wait for the source to appear before giving up.
21pub const DEFAULT_GRACE: Duration = Duration::from_secs(5);
22
23/// What to stream. Both variants are resolved by name/identifier each round, so a
24/// source that reappears (or an output that comes back) is picked up again.
25#[derive(Clone, Debug)]
26pub enum Source {
27 /// The output with this name (e.g. `DP-4`).
28 Output(String),
29 /// The toplevel with this `ext-foreign-toplevel` identifier.
30 Toplevel(String),
31}
32
33/// Why a stream ended.
34#[derive(Clone, Copy, Debug, PartialEq, Eq)]
35pub enum End {
36 /// The source was live and then vanished (window closed, output unplugged), or
37 /// the connection dropped.
38 SourceGone,
39 /// The source never appeared within the grace period.
40 NeverAppeared,
41}
42
43/// The outcome of one [`Stream::step`]: the frames that arrived this round, and
44/// whether the stream has ended.
45pub struct Step {
46 /// Frames delivered this round (every one is the single source's).
47 pub frames: Vec<Frame>,
48 /// `Some` once the stream is over — stop calling `step`.
49 pub end: Option<End>,
50}
51
52/// A live capture session for one [`Source`], reopened as needed.
53pub struct Stream {
54 source: Source,
55 session: Option<SessionId>,
56 appear_deadline: Instant,
57 /// Whether a session was ever successfully opened (distinguishes "gone" from
58 /// "never appeared" when the source is absent).
59 had_session: bool,
60}
61
62impl Stream {
63 /// Start a stream for `source`, giving it `grace` to first appear.
64 pub fn new(source: Source, grace: Duration) -> Self {
65 Self {
66 source,
67 session: None,
68 appear_deadline: Instant::now() + grace,
69 had_session: false,
70 }
71 }
72
73 /// Run one round: refresh state, (re)open the session, poll up to `budget`, and
74 /// return the frames that arrived. Returns a [`Step`] whose `end` is set once the
75 /// source is gone or never showed up.
76 pub fn step(&mut self, client: &mut Client, budget: Duration) -> Step {
77 if client.refresh().is_err() {
78 return self.ended(End::SourceGone);
79 }
80
81 if self.session.is_none() {
82 match self.open(client) {
83 Some(id) => {
84 self.session = Some(id);
85 self.had_session = true;
86 }
87 None if Instant::now() >= self.appear_deadline => {
88 let why = if self.had_session {
89 End::SourceGone
90 } else {
91 End::NeverAppeared
92 };
93 return self.ended(why);
94 }
95 // Not present yet: keep dispatching below so it can show up.
96 None => {}
97 }
98 } else if self.is_gone(client) {
99 return self.ended(End::SourceGone);
100 }
101
102 let (got, failed) = client.poll(budget);
103 // A stopped session (e.g. on resize): drop it and reopen next round.
104 for id in failed {
105 if self.session.as_ref() == Some(&id) {
106 self.session = None;
107 }
108 client.close_session(&id);
109 }
110 Step {
111 frames: got.into_iter().map(|(_id, f)| f).collect(),
112 end: None,
113 }
114 }
115
116 /// Open a session for the source if it is currently present.
117 fn open(&self, client: &mut Client) -> Option<SessionId> {
118 match &self.source {
119 Source::Output(name) => {
120 let out = client.outputs().iter().find(|o| o.name == *name).cloned()?;
121 client.open_output_session(&out).ok()
122 }
123 Source::Toplevel(id) => {
124 let tl = client
125 .toplevels()
126 .iter()
127 .find(|t| t.identifier == *id)
128 .cloned()?;
129 client.open_toplevel_session(&tl).ok()
130 }
131 }
132 }
133
134 /// Whether a source we had a session for has since disappeared.
135 fn is_gone(&self, client: &Client) -> bool {
136 match &self.source {
137 Source::Output(name) => !client.outputs().iter().any(|o| o.name == *name),
138 Source::Toplevel(id) => !client.toplevels().iter().any(|t| t.identifier == *id),
139 }
140 }
141
142 fn ended(&self, why: End) -> Step {
143 Step {
144 frames: Vec::new(),
145 end: Some(why),
146 }
147 }
148}
149
150/// Decode a streamed [`Frame`] to CPU pixels: shm frames pass through; a dma-buf
151/// frame is read back via `rb`, which is built on first need (a pure-shm stream never
152/// spins up a GL context). Hold one `Option<GpuReadback>` across the whole stream so
153/// the readback context is reused.
154pub fn decode_frame(rb: &mut Option<GpuReadback>, frame: Frame) -> Result<CapturedImage> {
155 match frame {
156 Frame::Shm(img) => Ok(img),
157 Frame::Dmabuf(d) => {
158 let rb = match rb {
159 Some(rb) => rb,
160 None => rb.insert(GpuReadback::new()?),
161 };
162 rb.readback(d)
163 }
164 }
165}