azul_layout/widgets/capture_common.rs
1//! Shared core for the "video-ish" widgets (camera / screencap / video).
2//!
3//! All three are identical in architecture (RefAny dataset + AfterMount
4//! background capture/decode thread + writeback that uploads each frame into a
5//! stable external GL texture + recomposites). Only the *config* and the
6//! *worker* differ. This module holds the duplicated pieces - the [`VideoFrame`]
7//! the worker produces and [`present_frame`], the GL writeback core - so each
8//! widget is a thin config+worker wrapper and there's a single place for GL
9//! fixes + the real platform workers (AVFoundation / ScreenCaptureKit /
10//! vk-video) to plug in.
11//!
12//! NOTE: GL code - compile-verified here; the actual texture rendering must be
13//! verified on a machine with a window + GPU.
14
15use azul_core::resources::UpdateImageType;
16use azul_core::callbacks::Update;
17use azul_core::gl::gl::{RGBA, TEXTURE_2D, UNSIGNED_BYTE};
18use azul_core::gl::{GlContextPtr, OptionU8VecRef, Texture, U8VecRef};
19use azul_core::geom::PhysicalSizeU32;
20use azul_core::refany::RefAny;
21use azul_core::resources::ImageRef;
22use azul_core::video::VideoFrame;
23use azul_css::impl_option_inner; // brought into scope for impl_widget_callback!'s impl_option!
24use azul_css::props::basic::ColorU;
25
26use crate::callbacks::CallbackInfo;
27
28/// User hook fired once per captured/decoded frame - the backreference
29/// dependency-injection pattern (see `architecture.md`).
30///
31/// A capture widget's
32/// private writeback invokes it with each [`VideoFrame`], so application code
33/// can apply effects, save the frame into its own data model, or send it over
34/// the network (azul-meet). Returns `Update` like any callback. Wired via
35/// `CameraWidget::with_on_frame` / `ScreenCaptureWidget::with_on_frame` /
36/// `VideoWidget::with_on_frame`.
37pub type OnVideoFrameCallbackType = extern "C" fn(RefAny, CallbackInfo, VideoFrame) -> Update;
38impl_widget_callback!(
39 OnVideoFrame,
40 OptionOnVideoFrame,
41 OnVideoFrameCallback,
42 OnVideoFrameCallbackType
43);
44
45// Host-invoker plumbing for managed-FFI bindings - see core/src/host_invoker.rs.
46azul_core::impl_managed_callback! {
47 wrapper: OnVideoFrameCallback,
48 info_ty: CallbackInfo,
49 return_ty: Update,
50 default_ret: Update::DoNothing,
51 invoker_static: ON_VIDEO_FRAME_INVOKER,
52 invoker_ty: AzOnVideoFrameCallbackInvoker,
53 thunk_fn: az_on_video_frame_callback_thunk,
54 setter_fn: AzApp_setOnVideoFrameCallbackInvoker,
55 from_handle_fn: AzOnVideoFrameCallback_createFromHostHandle,
56 extra_args: [ frame: VideoFrame ],
57}
58
59/// Invoke a capture widget's optional `on_frame` hook with `frame`, returning
60/// the user's `Update` (`DoNothing` when no hook is set). Shared by all three
61/// capture widgets' writebacks.
62pub fn invoke_on_frame(
63 hook: &OptionOnVideoFrame,
64 info: &mut CallbackInfo,
65 frame: &VideoFrame,
66) -> Update {
67 match hook {
68 OptionOnVideoFrame::Some(h) => {
69 (h.callback.cb)(h.refany.clone(), *info, frame.clone())
70 }
71 OptionOnVideoFrame::None => Update::DoNothing,
72 }
73}
74
75/// Present `frame` for a video-ish widget and return the (stable) GL texture
76/// id to store back in the widget's state.
77///
78/// - First frame (`current_id` is `None`): allocate a GL texture, upload, wrap
79/// in an external-texture `ImageRef`, and install it on the widget's node
80/// **once** via `change_node_image` (the node is found via
81/// `get_node_id_of_root_dataset(dataset)`). Returns `Some(new_id)`.
82/// - Every frame after: re-upload into the same texture id + recomposite
83/// (`update_all_image_callbacks` -> `ShouldReRenderCurrentWindow`) - no
84/// relayout, no display-list rebuild, since the external texture's wr key
85/// (= the `ImageRef` data pointer) stays stable. Returns `current_id`.
86/// - No GL context (cpurender): installs the frame as a **raw RGBA
87/// `ImageRef`** on the node every frame (same per-frame `new_rawimage` the
88/// `VideoWidget` CPU path uses) - heavier than the GL re-upload (new image
89/// resource per frame) but the tile shows live frames on both renderers.
90pub fn present_frame(
91 info: &mut CallbackInfo,
92 dataset: RefAny,
93 current_id: Option<u32>,
94 frame: &VideoFrame,
95) -> Option<u32> {
96 use azul_core::resources::{RawImage, RawImageData, RawImageFormat};
97
98 let Some(gl) = info.get_gl_context().into_option() else {
99 // CPU renderer: swap the node's image content for this frame.
100 if let Some(img) = ImageRef::new_rawimage(RawImage {
101 pixels: RawImageData::U8(frame.bytes.clone()),
102 width: frame.width as usize,
103 height: frame.height as usize,
104 premultiplied_alpha: false,
105 data_format: RawImageFormat::RGBA8,
106 tag: b"azul-capture-frame".to_vec().into(),
107 }) {
108 if let Some(node) = info.get_node_id_of_root_dataset(dataset) {
109 if let Some(nid) = node.node.into_crate_internal() {
110 info.change_node_image(node.dom, nid, img, UpdateImageType::Content);
111 }
112 }
113 }
114 return current_id;
115 };
116
117 if let Some(id) = current_id {
118 upload_rgba(&gl, id, frame);
119 info.update_all_image_callbacks();
120 Some(id)
121 } else {
122 let tex = Texture::allocate_rgba8(
123 gl.clone(),
124 PhysicalSizeU32 {
125 width: frame.width,
126 height: frame.height,
127 },
128 ColorU {
129 r: 0,
130 g: 0,
131 b: 0,
132 a: 0,
133 },
134 );
135 let id = tex.texture_id;
136 upload_rgba(&gl, id, frame);
137 let image = ImageRef::new_gltexture(tex);
138 if let Some(node) = info.get_node_id_of_root_dataset(dataset) {
139 if let Some(nid) = node.node.into_crate_internal() {
140 info.change_node_image(node.dom, nid, image, UpdateImageType::Content);
141 }
142 }
143 Some(id)
144 }
145}
146
147/// Upload tightly-packed RGBA8 pixels into the GL texture `texture_id`.
148#[allow(clippy::cast_possible_wrap)] // bounded graphics/coord/counter/fixed-point cast
149pub fn upload_rgba(gl: &GlContextPtr, texture_id: u32, frame: &VideoFrame) {
150 gl.bind_texture(TEXTURE_2D, texture_id);
151 gl.tex_image_2d(
152 TEXTURE_2D,
153 0,
154 RGBA as i32,
155 frame.width as i32,
156 frame.height as i32,
157 0,
158 RGBA,
159 UNSIGNED_BYTE,
160 OptionU8VecRef::Some(U8VecRef::from(frame.bytes.as_ref())),
161 );
162}
163
164/// A platform frame-capture backend (camera / screen), registered by the dll at
165/// startup so the cross-platform capture widgets can pull **real** frames
166/// instead of their built-in test pattern.
167///
168/// The dll provides one per OS (v4l2 on
169/// Linux, `AVFoundation` on macOS, Media Foundation on Windows, `ScreenCaptureKit` /
170/// `PipeWire` / DXGI for screens, ...). These are plain Rust fn pointers - the dll
171/// links azul-layout statically, so registering + calling is a Rust-to-Rust
172/// call, no `extern "C"`/trait-object dance.
173#[derive(Debug, Clone, Copy)]
174pub struct CaptureVTable {
175 /// Open source `index` (camera device / display index) at the requested
176 /// `width` x `height`. Returns an opaque handle, or `0` on failure (the
177 /// worker then falls back to the test pattern).
178 pub open: fn(index: u32, width: u32, height: u32) -> u64,
179 /// Block for the next frame, writing tightly-packed RGBA8 into `out`
180 /// (resized as needed). Returns the actual frame `(width, height)`, or
181 /// `(0, 0)` on end-of-stream / error (the worker then stops + closes).
182 pub read: fn(handle: u64, out: &mut Vec<u8>) -> (u32, u32),
183 /// Close + free the source.
184 pub close: fn(handle: u64),
185}
186
187static CAMERA_BACKEND: std::sync::OnceLock<CaptureVTable> = std::sync::OnceLock::new();
188static SCREEN_BACKEND: std::sync::OnceLock<CaptureVTable> = std::sync::OnceLock::new();
189
190/// Register the platform **camera** capture backend (called once by the dll at
191/// startup; the first registration wins). Without it, `CameraWidget` shows its
192/// test pattern.
193pub fn register_camera_backend(vtable: CaptureVTable) {
194 let _ = CAMERA_BACKEND.set(vtable);
195}
196
197/// Register the platform **screen** capture backend (for `ScreenCaptureWidget`).
198pub fn register_screen_backend(vtable: CaptureVTable) {
199 let _ = SCREEN_BACKEND.set(vtable);
200}
201
202/// The registered camera backend, if the dll provided one for this platform.
203pub fn camera_backend() -> Option<CaptureVTable> {
204 CAMERA_BACKEND.get().copied()
205}
206
207/// The registered screen-capture backend, if any.
208pub fn screen_backend() -> Option<CaptureVTable> {
209 SCREEN_BACKEND.get().copied()
210}
211
212/// A platform **audio**-capture backend (microphone), registered by the dll so
213/// `MicrophoneWidget` can pull real samples instead of the test tone.
214///
215/// Like
216/// [`CaptureVTable`] but yields interleaved `f32` audio rather than RGBA video.
217#[derive(Debug, Clone, Copy)]
218pub struct AudioCaptureVTable {
219 /// Open the default mic at `sample_rate` x `channels`. Opaque handle, or
220 /// `0` on failure.
221 pub open: fn(sample_rate: u32, channels: u16) -> u64,
222 /// Block for the next chunk, writing interleaved `f32` into `out` (resized).
223 /// Returns the frame count (`out.len() / channels`), or `0` on error / EOF
224 /// (the worker then stops + closes).
225 pub read: fn(handle: u64, out: &mut Vec<f32>) -> u32,
226 /// Close + free the source.
227 pub close: fn(handle: u64),
228}
229
230static MIC_BACKEND: std::sync::OnceLock<AudioCaptureVTable> = std::sync::OnceLock::new();
231
232/// Register the platform microphone-capture backend (called once by the dll).
233pub fn register_mic_backend(vtable: AudioCaptureVTable) {
234 let _ = MIC_BACKEND.set(vtable);
235}
236
237/// The registered mic-capture backend, if the dll provided one for this platform.
238pub fn mic_backend() -> Option<AudioCaptureVTable> {
239 MIC_BACKEND.get().copied()
240}