Skip to main content

azul_layout/widgets/
screencap.rs

1//! Screen-capture widget — a "dumb widget" identical in architecture to the
2//! [`CameraWidget`](super::camera), only the source differs (a display /
3//! window). SUPER_PLAN_2 §4 P6, widget pivot.
4//!
5//! `ScreenCaptureWidget::create(config).dom()` → an `<img>` a background
6//! capture thread keeps fed; each frame goes through
7//! [`super::capture_common::present_frame`] (GL-texture install-once /
8//! re-upload + recomposite). The shared core lives in `capture_common`; this
9//! widget is its config + worker. Test-pattern worker (a moving band) stands
10//! in for the real ScreenCaptureKit / MediaProjection / PipeWire worker.
11
12use alloc::vec::Vec;
13
14use azul_core::callbacks::Update;
15use azul_core::dom::{ComponentEventFilter, DatasetMergeCallbackType, Dom, EventFilter};
16use azul_core::refany::{OptionRefAny, RefAny};
17use azul_core::resources::{ImageRef, RawImageFormat};
18use azul_core::screencap::ScreenCaptureConfig;
19use azul_core::task::{ThreadId, ThreadReceiver};
20
21use azul_core::video::VideoFrame;
22
23use super::capture_common::{
24    invoke_on_frame, present_frame, screen_backend, OnVideoFrame, OnVideoFrameCallback,
25    OptionOnVideoFrame,
26};
27use crate::callbacks::{Callback, CallbackInfo, CallbackType};
28use crate::thread::{
29    Thread, ThreadCallback, ThreadReceiveMsg, ThreadSender, ThreadWriteBackMsg, WriteBackCallback,
30};
31
32/// Default capture size for the test pattern (the real backend reports the
33/// source's actual size).
34const DEFAULT_W: u32 = 1280;
35const DEFAULT_H: u32 = 720;
36
37/// Live state for one screencap widget, carried across relayout by
38/// [`merge_screencap_state`].
39#[derive(Debug)]
40pub struct ScreenCaptureWidgetState {
41    /// The requested capture configuration (the control POD).
42    pub config: ScreenCaptureConfig,
43    /// `true` once the capture thread has been started.
44    pub started: bool,
45    /// The stable external GL texture id once installed.
46    pub gl_texture_id: Option<u32>,
47    /// Optional user hook invoked with each captured frame (effects / save /
48    /// send). Re-set on every fresh build (see [`merge_screencap_state`]).
49    pub on_frame: OptionOnVideoFrame,
50}
51
52/// A screen-capture widget. `create(config).dom()` yields an `<img>` the
53/// capture thread keeps fed.
54#[repr(C)]
55#[derive(Debug)]
56pub struct ScreenCaptureWidget {
57    /// What to capture + fps + format.
58    pub config: ScreenCaptureConfig,
59    /// Optional per-frame user hook (effects / save / send - azul-meet).
60    pub on_frame: OptionOnVideoFrame,
61}
62
63impl ScreenCaptureWidget {
64    /// Create a screencap widget for the given config.
65    #[must_use] pub const fn create(config: ScreenCaptureConfig) -> Self {
66        Self {
67            config,
68            on_frame: OptionOnVideoFrame::None,
69        }
70    }
71
72    /// Set a hook invoked with every captured frame - for live effects, saving
73    /// frames into your data model, or sending them over the network
74    /// (azul-meet). The backreference DI pattern (see `architecture.md`).
75    pub fn set_on_frame<C: Into<OnVideoFrameCallback>>(&mut self, data: RefAny, on_frame: C) {
76        self.on_frame = Some(OnVideoFrame {
77            refany: data,
78            callback: on_frame.into(),
79        })
80        .into();
81    }
82
83    /// Builder form of [`set_on_frame`](Self::set_on_frame).
84    #[must_use]
85    pub fn with_on_frame<C: Into<OnVideoFrameCallback>>(
86        mut self,
87        data: RefAny,
88        on_frame: C,
89    ) -> Self {
90        self.set_on_frame(data, on_frame);
91        self
92    }
93
94    /// Build the widget's DOM: a single `<img>` node, fed by a background
95    /// capture thread started on mount.
96    #[must_use] pub fn dom(self) -> Dom {
97        let state = ScreenCaptureWidgetState {
98            config: self.config,
99            started: false,
100            gl_texture_id: None,
101            on_frame: self.on_frame,
102        };
103        let dataset = RefAny::new(state);
104
105        let placeholder = ImageRef::null_image(
106            DEFAULT_W as usize,
107            DEFAULT_H as usize,
108            RawImageFormat::BGRA8,
109            b"azul-screencap-placeholder".to_vec(),
110        );
111
112        Dom::create_image(placeholder)
113            .with_dataset(OptionRefAny::Some(dataset.clone()))
114            .with_merge_callback(azul_core::dom::DatasetMergeCallback::from_ptr(merge_screencap_state))
115            .with_callback(
116                EventFilter::Component(ComponentEventFilter::AfterMount),
117                dataset,
118                Callback::from_ptr(screencap_on_after_mount),
119            )
120    }
121}
122
123/// `AfterMount`: start the background capture thread exactly once.
124extern "C" fn screencap_on_after_mount(mut data: RefAny, mut info: CallbackInfo) -> Update {
125    {
126        let Some(mut s) = data.downcast_mut::<ScreenCaptureWidgetState>() else {
127            return Update::DoNothing;
128        };
129        if s.started {
130            return Update::DoNothing;
131        }
132        s.started = true;
133    }
134    info.add_thread(
135        ThreadId::unique(),
136        Thread::create(
137            RefAny::new(()),
138            data.clone(),
139            ThreadCallback::new(screencap_worker),
140        ),
141    );
142    Update::DoNothing
143}
144
145/// Background worker (test pattern): a downward-moving white band on dark grey,
146/// ~30x/s. Replaced by the real `ScreenCaptureKit` / `MediaProjection` worker.
147extern "C" fn screencap_worker(_init: RefAny, mut sender: ThreadSender, _recv: ThreadReceiver) {
148    // Real platform capture if the dll registered a screen backend
149    // (ScreenCaptureKit / X11 / DXGI; Wayland stays a dummy); else the test pattern.
150    if let Some(backend) = screen_backend() {
151        let handle = (backend.open)(0, DEFAULT_W, DEFAULT_H);
152        if handle != 0 {
153            let mut buf: alloc::vec::Vec<u8> = alloc::vec::Vec::new();
154            loop {
155                let (fw, fh) = (backend.read)(handle, &mut buf);
156                if fw == 0 || fh == 0 {
157                    break;
158                }
159                let frame = VideoFrame {
160                    width: fw,
161                    height: fh,
162                    bytes: buf.clone().into(),
163                };
164                if !sender.send(ThreadReceiveMsg::WriteBack(ThreadWriteBackMsg::new(
165                    WriteBackCallback::new(screencap_writeback),
166                    RefAny::new(frame),
167                ))) {
168                    break;
169                }
170            }
171            (backend.close)(handle);
172            return;
173        }
174    }
175
176    let (w, h) = (DEFAULT_W as usize, DEFAULT_H as usize);
177    let mut tick: u32 = 0;
178    loop {
179        let band = (tick as usize) % h;
180        let mut bytes = Vec::with_capacity(w * h * 4);
181        for y in 0..h {
182            let v = if y.abs_diff(band) < 8 { 235u8 } else { 28u8 };
183            for _ in 0..w {
184                bytes.extend_from_slice(&[v, v, v, 255]);
185            }
186        }
187        let frame = VideoFrame {
188            width: u32::try_from(w).unwrap_or(0),
189            height: u32::try_from(h).unwrap_or(0),
190            bytes: bytes.into(),
191        };
192        let sent = sender.send(ThreadReceiveMsg::WriteBack(ThreadWriteBackMsg::new(
193            WriteBackCallback::new(screencap_writeback),
194            RefAny::new(frame),
195        )));
196        if !sent {
197            break;
198        }
199        std::thread::sleep(std::time::Duration::from_millis(33));
200        tick = tick.wrapping_add(12);
201    }
202}
203
204/// Writeback (main thread): hand the frame to the shared GL presenter and
205/// store the (stable) texture id.
206extern "C" fn screencap_writeback(
207    mut writeback_data: RefAny,
208    mut frame_data: RefAny,
209    mut info: CallbackInfo,
210) -> Update {
211    let (current, hook) = writeback_data.downcast_ref::<ScreenCaptureWidgetState>().map_or_else(|| (None, OptionOnVideoFrame::None), |s| (s.gl_texture_id, s.on_frame.clone()));
212    let mut user_update = Update::DoNothing;
213    let new_id = match frame_data.downcast_ref::<VideoFrame>() {
214        Some(frame) => {
215            let id = present_frame(&mut info, writeback_data.clone(), current, &frame);
216            user_update = invoke_on_frame(&hook, &mut info, &frame);
217            id
218        }
219        None => return Update::DoNothing,
220    };
221    if let Some(mut s) = writeback_data.downcast_mut::<ScreenCaptureWidgetState>() {
222        s.gl_texture_id = new_id;
223    }
224    user_update
225}
226
227/// Carry live state forward across relayout.
228extern "C" fn merge_screencap_state(mut new_data: RefAny, mut old_data: RefAny) -> RefAny {
229    {
230        let new_guard = new_data.downcast_mut::<ScreenCaptureWidgetState>();
231        let old_guard = old_data.downcast_ref::<ScreenCaptureWidgetState>();
232        if let (Some(mut new_g), Some(old_g)) = (new_guard, old_guard) {
233            new_g.started = old_g.started;
234            new_g.gl_texture_id = old_g.gl_texture_id;
235        }
236    }
237    new_data
238}