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