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

            
18
use alloc::vec::Vec;
19

            
20
use azul_core::callbacks::Update;
21
use azul_core::camera::CameraConfig;
22
use azul_core::dom::{ComponentEventFilter, DatasetMergeCallbackType, Dom, EventFilter};
23
use azul_core::refany::{OptionRefAny, RefAny};
24
use azul_core::resources::{ImageRef, RawImageFormat};
25
use azul_core::task::{ThreadId, ThreadReceiver};
26

            
27
use azul_core::video::VideoFrame;
28

            
29
use super::capture_common::{
30
    camera_backend, invoke_on_frame, present_frame, OnVideoFrame, OnVideoFrameCallback,
31
    OptionOnVideoFrame,
32
};
33
use crate::callbacks::{Callback, CallbackInfo, CallbackType};
34
use crate::thread::{
35
    Thread, ThreadCallback, ThreadReceiveMsg, ThreadSender, ThreadWriteBackMsg, WriteBackCallback,
36
};
37

            
38
/// Init data handed to the capture worker thread.
39
struct 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
pub struct CameraWidgetState {
47
    /// The requested capture configuration (the control POD).
48
    pub config: CameraConfig,
49
    /// `true` once the capture thread has been started.
50
    pub started: bool,
51
    /// The stable external GL texture id once the first frame installed it.
52
    pub gl_texture_id: Option<u32>,
53
    /// Optional user hook invoked with each captured frame (effects / save /
54
    /// send). Re-set on every fresh build (see [`merge_camera_state`]).
55
    pub on_frame: OptionOnVideoFrame,
56
}
57

            
58
/// A camera-preview widget. `create(config).dom()` yields an `<img>` the
59
/// capture thread keeps fed.
60
#[repr(C)]
61
pub struct CameraWidget {
62
    /// Requested capture config (camera facing, resolution, fps, format).
63
    pub config: CameraConfig,
64
    /// Optional per-frame user hook (effects / save / send - azul-meet).
65
    pub on_frame: OptionOnVideoFrame,
66
}
67

            
68
impl CameraWidget {
69
    /// Create a camera widget for the given capture config.
70
    pub fn create(config: CameraConfig) -> Self {
71
        Self {
72
            config,
73
            on_frame: OptionOnVideoFrame::None,
74
        }
75
    }
76

            
77
    /// Set a hook invoked with every captured frame - for live effects, saving
78
    /// frames into your data model, or sending them over the network
79
    /// (azul-meet). The backreference DI pattern (see `architecture.md`).
80
    pub fn set_on_frame<C: Into<OnVideoFrameCallback>>(&mut self, data: RefAny, on_frame: C) {
81
        self.on_frame = Some(OnVideoFrame {
82
            refany: data,
83
            callback: on_frame.into(),
84
        })
85
        .into();
86
    }
87

            
88
    /// Builder form of [`set_on_frame`](Self::set_on_frame).
89
    pub fn with_on_frame<C: Into<OnVideoFrameCallback>>(
90
        mut self,
91
        data: RefAny,
92
        on_frame: C,
93
    ) -> Self {
94
        self.set_on_frame(data, on_frame);
95
        self
96
    }
97

            
98
    /// Build the widget's DOM: a single `<img>` node, fed by a background
99
    /// capture thread started on mount.
100
    pub fn dom(self) -> Dom {
101
        let state = CameraWidgetState {
102
            config: self.config,
103
            started: false,
104
            gl_texture_id: None,
105
            on_frame: self.on_frame,
106
        };
107
        let dataset = RefAny::new(state);
108

            
109
        let (w, h) = frame_dims(&self.config);
110
        let placeholder = ImageRef::null_image(
111
            w as usize,
112
            h as usize,
113
            RawImageFormat::BGRA8,
114
            b"azul-camera-placeholder".to_vec(),
115
        );
116

            
117
        Dom::create_image(placeholder)
118
            .with_dataset(OptionRefAny::Some(dataset.clone()))
119
            .with_merge_callback(merge_camera_state as DatasetMergeCallbackType)
120
            .with_callback(
121
                EventFilter::Component(ComponentEventFilter::AfterMount),
122
                dataset,
123
                Callback::from(camera_on_after_mount as CallbackType),
124
            )
125
    }
126
}
127

            
128
/// Frame dimensions for a config (0 -> a sane default).
129
fn frame_dims(config: &CameraConfig) -> (u32, u32) {
130
    let w = if config.width > 0 { config.width } else { 640 };
131
    let h = if config.height > 0 { config.height } else { 480 };
132
    (w, h)
133
}
134

            
135
/// AfterMount: start the background capture thread exactly once.
136
extern "C" fn camera_on_after_mount(mut data: RefAny, mut info: CallbackInfo) -> Update {
137
    let dims = {
138
        let mut s = match data.downcast_mut::<CameraWidgetState>() {
139
            Some(s) => s,
140
            None => return Update::DoNothing,
141
        };
142
        if s.started {
143
            return Update::DoNothing;
144
        }
145
        s.started = true;
146
        frame_dims(&s.config)
147
    };
148

            
149
    info.add_thread(
150
        ThreadId::unique(),
151
        Thread::create(
152
            RefAny::new(CameraThreadInit {
153
                width: dims.0,
154
                height: dims.1,
155
            }),
156
            data.clone(),
157
            ThreadCallback::new(camera_worker),
158
        ),
159
    );
160
    Update::DoNothing
161
}
162

            
163
/// Background worker (test pattern): a colour-cycling solid frame ~30x/s until
164
/// the widget unmounts. The real AVFoundation/Camera2 capture loop replaces it.
165
extern "C" fn camera_worker(mut init: RefAny, mut sender: ThreadSender, _recv: ThreadReceiver) {
166
    let (w, h) = init
167
        .downcast_ref::<CameraThreadInit>()
168
        .map(|i| (i.width, i.height))
169
        .unwrap_or((640, 480));
170

            
171
    // Real platform capture if the dll registered a camera backend (v4l2 /
172
    // AVFoundation / Media Foundation); otherwise the colour-cycle test pattern.
173
    if let Some(backend) = camera_backend() {
174
        let handle = (backend.open)(0, w, h);
175
        if handle != 0 {
176
            let mut buf: alloc::vec::Vec<u8> = alloc::vec::Vec::new();
177
            loop {
178
                let (fw, fh) = (backend.read)(handle, &mut buf);
179
                if fw == 0 || fh == 0 {
180
                    break;
181
                }
182
                let frame = VideoFrame {
183
                    width: fw,
184
                    height: fh,
185
                    bytes: buf.clone().into(),
186
                };
187
                if !sender.send(ThreadReceiveMsg::WriteBack(ThreadWriteBackMsg::new(
188
                    WriteBackCallback::new(camera_writeback),
189
                    RefAny::new(frame),
190
                ))) {
191
                    break;
192
                }
193
            }
194
            (backend.close)(handle);
195
            return;
196
        }
197
    }
198

            
199
    let px = (w as usize) * (h as usize);
200
    let mut tick: u32 = 0;
201
    loop {
202
        let color = [
203
            (tick % 256) as u8,
204
            (tick.wrapping_mul(2) % 256) as u8,
205
            (tick.wrapping_mul(3) % 256) as u8,
206
            255u8,
207
        ];
208
        let mut bytes = Vec::with_capacity(px * 4);
209
        for _ in 0..px {
210
            bytes.extend_from_slice(&color);
211
        }
212
        let frame = VideoFrame {
213
            width: w,
214
            height: h,
215
            bytes: bytes.into(),
216
        };
217
        let sent = sender.send(ThreadReceiveMsg::WriteBack(ThreadWriteBackMsg::new(
218
            WriteBackCallback::new(camera_writeback),
219
            RefAny::new(frame),
220
        )));
221
        if !sent {
222
            break;
223
        }
224
        std::thread::sleep(std::time::Duration::from_millis(33));
225
        tick = tick.wrapping_add(8);
226
    }
227
}
228

            
229
/// Writeback (main thread): hand the frame to the shared GL presenter and
230
/// store the (stable) texture id back in the widget's state.
231
extern "C" fn camera_writeback(
232
    mut writeback_data: RefAny,
233
    mut frame_data: RefAny,
234
    mut info: CallbackInfo,
235
) -> Update {
236
    let (current, hook) = match writeback_data.downcast_ref::<CameraWidgetState>() {
237
        Some(s) => (s.gl_texture_id, s.on_frame.clone()),
238
        None => (None, OptionOnVideoFrame::None),
239
    };
240
    let mut user_update = Update::DoNothing;
241
    let new_id = match frame_data.downcast_ref::<VideoFrame>() {
242
        Some(frame) => {
243
            let id = present_frame(&mut info, writeback_data.clone(), current, &frame);
244
            user_update = invoke_on_frame(&hook, &mut info, &frame);
245
            id
246
        }
247
        None => return Update::DoNothing,
248
    };
249
    if let Some(mut s) = writeback_data.downcast_mut::<CameraWidgetState>() {
250
        s.gl_texture_id = new_id;
251
    }
252
    user_update
253
}
254

            
255
/// Carry live state forward across relayout (config from the fresh build,
256
/// thread / texture from the previous frame).
257
extern "C" fn merge_camera_state(mut new_data: RefAny, mut old_data: RefAny) -> RefAny {
258
    {
259
        let new_guard = new_data.downcast_mut::<CameraWidgetState>();
260
        let old_guard = old_data.downcast_ref::<CameraWidgetState>();
261
        if let (Some(mut new_g), Some(old_g)) = (new_guard, old_guard) {
262
            new_g.started = old_g.started;
263
            new_g.gl_texture_id = old_g.gl_texture_id;
264
        }
265
    }
266
    new_data
267
}