Skip to main content

azul_layout/widgets/
camera.rs

1//! Camera-preview widget - a "dumb widget" (like [`MapWidget`](super::map))
2//! that owns a background capture thread + a GL-texture `ImageRef`, with **no**
3//! camera-specific logic in the core framework (SUPER_PLAN_2 ยง4 P6, widget
4//! pivot - see the MASTER PLAN in `MOBILE_SESSION_LOG.md`).
5//!
6//! `CameraWidget::create(config).dom()` -> a static `<img>` whose pixels a
7//! background thread keeps fed. On `AfterMount` the capture thread starts
8//! (`CallbackInfo::add_thread`); each frame goes through
9//! [`super::capture_common::present_frame`], which uploads it into a stable
10//! external GL texture + recomposites - no relayout, no display-list rebuild.
11//! The shared thread/writeback/GL core lives in `capture_common`; this widget
12//! is just its config + worker.
13//!
14//! This tick uses a self-contained **test-pattern** worker (colour cycle, no
15//! platform deps); the real AVFoundation/Camera2 worker (dll-side) swaps in
16//! later.
17
18use alloc::vec::Vec;
19
20use azul_core::callbacks::Update;
21use azul_core::camera::CameraConfig;
22use azul_core::dom::{ComponentEventFilter, DatasetMergeCallbackType, Dom, EventFilter};
23use azul_core::refany::{OptionRefAny, RefAny};
24use azul_core::resources::{ImageRef, RawImageFormat};
25use azul_core::task::{ThreadId, ThreadReceiver};
26
27use azul_core::video::VideoFrame;
28
29use super::capture_common::{
30    camera_backend, invoke_on_frame, present_frame, OnVideoFrame, OnVideoFrameCallback,
31    OptionOnVideoFrame,
32};
33use crate::callbacks::{Callback, CallbackInfo, CallbackType};
34use crate::thread::{
35    Thread, ThreadCallback, ThreadReceiveMsg, ThreadSender, ThreadWriteBackMsg, WriteBackCallback,
36};
37
38/// Init data handed to the capture worker thread.
39struct CameraThreadInit {
40    width: u32,
41    height: u32,
42}
43
44/// Live state for one camera widget, carried across relayout by
45/// [`merge_camera_state`].
46#[derive(Debug)]
47pub struct CameraWidgetState {
48    /// The requested capture configuration (the control POD).
49    pub config: CameraConfig,
50    /// `true` once the capture thread has been started.
51    pub started: bool,
52    /// The stable external GL texture id once the first frame installed it.
53    pub gl_texture_id: Option<u32>,
54    /// Optional user hook invoked with each captured frame (effects / save /
55    /// send). Re-set on every fresh build (see [`merge_camera_state`]).
56    pub on_frame: OptionOnVideoFrame,
57}
58
59/// A camera-preview widget. `create(config).dom()` yields an `<img>` the
60/// capture thread keeps fed.
61#[repr(C)]
62#[derive(Debug)]
63pub struct CameraWidget {
64    /// Requested capture config (camera facing, resolution, fps, format).
65    pub config: CameraConfig,
66    /// Optional per-frame user hook (effects / save / send - azul-meet).
67    pub on_frame: OptionOnVideoFrame,
68}
69
70impl CameraWidget {
71    /// Create a camera widget for the given capture config.
72    #[must_use] pub const fn create(config: CameraConfig) -> Self {
73        Self {
74            config,
75            on_frame: OptionOnVideoFrame::None,
76        }
77    }
78
79    /// Set a hook invoked with every captured frame - for live effects, saving
80    /// frames into your data model, or sending them over the network
81    /// (azul-meet). The backreference DI pattern (see `architecture.md`).
82    pub fn set_on_frame<C: Into<OnVideoFrameCallback>>(&mut self, data: RefAny, on_frame: C) {
83        self.on_frame = Some(OnVideoFrame {
84            refany: data,
85            callback: on_frame.into(),
86        })
87        .into();
88    }
89
90    /// Builder form of [`set_on_frame`](Self::set_on_frame).
91    #[must_use]
92    pub fn with_on_frame<C: Into<OnVideoFrameCallback>>(
93        mut self,
94        data: RefAny,
95        on_frame: C,
96    ) -> Self {
97        self.set_on_frame(data, on_frame);
98        self
99    }
100
101    /// Build the widget's DOM: a single `<img>` node, fed by a background
102    /// capture thread started on mount.
103    #[must_use] pub fn dom(self) -> Dom {
104        let state = CameraWidgetState {
105            config: self.config,
106            started: false,
107            gl_texture_id: None,
108            on_frame: self.on_frame,
109        };
110        let dataset = RefAny::new(state);
111
112        let (w, h) = frame_dims(&self.config);
113        let placeholder = ImageRef::null_image(
114            w as usize,
115            h as usize,
116            RawImageFormat::BGRA8,
117            b"azul-camera-placeholder".to_vec(),
118        );
119
120        Dom::create_image(placeholder)
121            .with_dataset(OptionRefAny::Some(dataset.clone()))
122            .with_merge_callback(azul_core::dom::DatasetMergeCallback::from_ptr(merge_camera_state))
123            .with_callback(
124                EventFilter::Component(ComponentEventFilter::AfterMount),
125                dataset,
126                Callback::from_ptr(camera_on_after_mount),
127            )
128    }
129}
130
131/// Frame dimensions for a config (0 -> a sane default).
132const fn frame_dims(config: &CameraConfig) -> (u32, u32) {
133    let w = if config.width > 0 { config.width } else { 640 };
134    let h = if config.height > 0 { config.height } else { 480 };
135    (w, h)
136}
137
138/// `AfterMount`: start the background capture thread exactly once.
139extern "C" fn camera_on_after_mount(mut data: RefAny, mut info: CallbackInfo) -> Update {
140    let dims = {
141        let Some(mut s) = data.downcast_mut::<CameraWidgetState>() else {
142            return Update::DoNothing;
143        };
144        if s.started {
145            return Update::DoNothing;
146        }
147        s.started = true;
148        frame_dims(&s.config)
149    };
150
151    info.add_thread(
152        ThreadId::unique(),
153        Thread::create(
154            RefAny::new(CameraThreadInit {
155                width: dims.0,
156                height: dims.1,
157            }),
158            data.clone(),
159            ThreadCallback::new(camera_worker),
160        ),
161    );
162    Update::DoNothing
163}
164
165/// Background worker (test pattern): a colour-cycling solid frame ~30x/s until
166/// the widget unmounts. The real AVFoundation/Camera2 capture loop replaces it.
167#[allow(clippy::cast_possible_truncation)] // bounded graphics/coord/counter/fixed-point cast
168extern "C" fn camera_worker(mut init: RefAny, mut sender: ThreadSender, _recv: ThreadReceiver) {
169    let (w, h) = init
170        .downcast_ref::<CameraThreadInit>()
171        .map_or((640, 480), |i| (i.width, i.height));
172
173    // Real platform capture if the dll registered a camera backend (v4l2 /
174    // AVFoundation / Media Foundation); otherwise the colour-cycle test pattern.
175    if let Some(backend) = camera_backend() {
176        let handle = (backend.open)(0, w, h);
177        if handle != 0 {
178            let mut buf: alloc::vec::Vec<u8> = alloc::vec::Vec::new();
179            loop {
180                let (fw, fh) = (backend.read)(handle, &mut buf);
181                if fw == 0 || fh == 0 {
182                    break;
183                }
184                let frame = VideoFrame {
185                    width: fw,
186                    height: fh,
187                    bytes: buf.clone().into(),
188                };
189                if !sender.send(ThreadReceiveMsg::WriteBack(ThreadWriteBackMsg::new(
190                    WriteBackCallback::new(camera_writeback),
191                    RefAny::new(frame),
192                ))) {
193                    break;
194                }
195            }
196            (backend.close)(handle);
197            return;
198        }
199    }
200
201    let px = (w as usize) * (h as usize);
202    let mut tick: u32 = 0;
203    loop {
204        let color = [
205            (tick % 256) as u8,
206            (tick.wrapping_mul(2) % 256) as u8,
207            (tick.wrapping_mul(3) % 256) as u8,
208            255u8,
209        ];
210        let mut bytes = Vec::with_capacity(px * 4);
211        for _ in 0..px {
212            bytes.extend_from_slice(&color);
213        }
214        let frame = VideoFrame {
215            width: w,
216            height: h,
217            bytes: bytes.into(),
218        };
219        let sent = sender.send(ThreadReceiveMsg::WriteBack(ThreadWriteBackMsg::new(
220            WriteBackCallback::new(camera_writeback),
221            RefAny::new(frame),
222        )));
223        if !sent {
224            break;
225        }
226        std::thread::sleep(std::time::Duration::from_millis(33));
227        tick = tick.wrapping_add(8);
228    }
229}
230
231/// Writeback (main thread): hand the frame to the shared GL presenter and
232/// store the (stable) texture id back in the widget's state.
233extern "C" fn camera_writeback(
234    mut writeback_data: RefAny,
235    mut frame_data: RefAny,
236    mut info: CallbackInfo,
237) -> Update {
238    let (current, hook) = writeback_data.downcast_ref::<CameraWidgetState>().map_or_else(|| (None, OptionOnVideoFrame::None), |s| (s.gl_texture_id, s.on_frame.clone()));
239    let mut user_update = Update::DoNothing;
240    let new_id = match frame_data.downcast_ref::<VideoFrame>() {
241        Some(frame) => {
242            let id = present_frame(&mut info, writeback_data.clone(), current, &frame);
243            user_update = invoke_on_frame(&hook, &mut info, &frame);
244            id
245        }
246        None => return Update::DoNothing,
247    };
248    if let Some(mut s) = writeback_data.downcast_mut::<CameraWidgetState>() {
249        s.gl_texture_id = new_id;
250    }
251    user_update
252}
253
254/// Carry live state forward across relayout (config from the fresh build,
255/// thread / texture from the previous frame).
256extern "C" fn merge_camera_state(mut new_data: RefAny, mut old_data: RefAny) -> RefAny {
257    {
258        let new_guard = new_data.downcast_mut::<CameraWidgetState>();
259        let old_guard = old_data.downcast_ref::<CameraWidgetState>();
260        if let (Some(mut new_g), Some(old_g)) = (new_guard, old_guard) {
261            new_g.started = old_g.started;
262            new_g.gl_texture_id = old_g.gl_texture_id;
263        }
264    }
265    new_data
266}