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

            
12
use alloc::vec::Vec;
13

            
14
use azul_core::callbacks::Update;
15
use azul_core::dom::{ComponentEventFilter, DatasetMergeCallbackType, Dom, EventFilter};
16
use azul_core::refany::{OptionRefAny, RefAny};
17
use azul_core::resources::{ImageRef, RawImageFormat};
18
use azul_core::screencap::ScreenCaptureConfig;
19
use azul_core::task::{ThreadId, ThreadReceiver};
20

            
21
use azul_core::video::VideoFrame;
22

            
23
use super::capture_common::{
24
    invoke_on_frame, present_frame, screen_backend, OnVideoFrame, OnVideoFrameCallback,
25
    OptionOnVideoFrame,
26
};
27
use crate::callbacks::{Callback, CallbackInfo, CallbackType};
28
use 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).
34
const DEFAULT_W: u32 = 1280;
35
const DEFAULT_H: u32 = 720;
36

            
37
/// Live state for one screencap widget, carried across relayout by
38
/// [`merge_screencap_state`].
39
pub 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)]
54
pub 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

            
61
impl 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.
121
extern "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.
145
extern "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.
204
extern "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.
229
extern "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
}