Skip to main content

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}