1use std::collections::VecDeque;
10use std::sync::{Arc, Mutex};
11
12use anyhow::Result;
13use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
14use cpal::SampleFormat;
15use nokhwa::pixel_format::RgbFormat;
16use nokhwa::utils::{CameraIndex, RequestedFormat, RequestedFormatType};
17use nokhwa::Camera;
18use wasmtime::{Caller, Linker};
19
20use crate::capabilities::{console_log, write_guest_bytes, ConsoleLevel, HostState};
21use crate::permissions::{check_or_request, PermissionKind, PermissionStatus, PERMISSION_PENDING};
22
23const MIC_RING_CAP: usize = 96_000;
24
25#[derive(Default)]
27pub struct MediaCaptureState {
28 camera: Option<Camera>,
29 last_frame_w: u32,
30 last_frame_h: u32,
31 camera_frames: u64,
32 microphone: Option<MicrophoneInput>,
33 screen_w: u32,
34 screen_h: u32,
35 screen_captures: u64,
36}
37
38impl MediaCaptureState {
39 pub fn reset(&mut self) {
44 if let Some(mut cam) = self.camera.take() {
45 let _ = cam.stop_stream();
46 }
47 *self = Self::default();
48 }
49}
50
51struct MicrophoneInput {
52 #[allow(dead_code)]
53 stream: cpal::Stream,
54 buffer: Arc<Mutex<VecDeque<f32>>>,
55 sample_rate: u32,
56}
57
58fn permission_gate(caller: &Caller<'_, HostState>, kind: PermissionKind) -> Option<i32> {
64 if !crate::manifest::manifest_allows(&caller.data().manifest, kind) {
65 console_log(
66 &caller.data().console,
67 ConsoleLevel::Warn,
68 format!(
69 "[PERMISSIONS] '{}' is not declared in the app manifest — denied",
70 kind.name()
71 ),
72 );
73 return Some(-1);
74 }
75 let origin = caller.data().module_origin.lock().unwrap().clone();
76 match check_or_request(&caller.data().permissions, &origin, kind) {
77 PermissionStatus::Granted => None,
78 PermissionStatus::Denied => Some(-1),
79 PermissionStatus::Pending => Some(PERMISSION_PENDING),
80 }
81}
82
83fn push_mono_f32(data: &[f32], channels: usize, ring: &Arc<Mutex<VecDeque<f32>>>) {
84 let ch = channels.max(1);
85 let frames = data.len() / ch;
86 let mut q = ring.lock().unwrap();
87 for i in 0..frames {
88 let mut sum = 0.0f32;
89 for c in 0..ch {
90 sum += data[i * ch + c];
91 }
92 let m = sum / ch as f32;
93 while q.len() >= MIC_RING_CAP {
94 q.pop_front();
95 }
96 q.push_back(m);
97 }
98}
99
100fn push_mono_i16(data: &[i16], channels: usize, ring: &Arc<Mutex<VecDeque<f32>>>) {
101 let ch = channels.max(1);
102 let frames = data.len() / ch;
103 let mut q = ring.lock().unwrap();
104 for i in 0..frames {
105 let mut sum = 0.0f32;
106 for c in 0..ch {
107 sum += data[i * ch + c] as f32 / 32768.0;
108 }
109 let m = sum / ch as f32;
110 while q.len() >= MIC_RING_CAP {
111 q.pop_front();
112 }
113 q.push_back(m);
114 }
115}
116
117fn push_mono_u16(data: &[u16], channels: usize, ring: &Arc<Mutex<VecDeque<f32>>>) {
118 let ch = channels.max(1);
119 let frames = data.len() / ch;
120 let mut q = ring.lock().unwrap();
121 for i in 0..frames {
122 let mut sum = 0.0f32;
123 for c in 0..ch {
124 sum += (data[i * ch + c] as f32 - 32768.0) / 32768.0;
125 }
126 let m = sum / ch as f32;
127 while q.len() >= MIC_RING_CAP {
128 q.pop_front();
129 }
130 q.push_back(m);
131 }
132}
133
134fn open_microphone(
135 console: &Arc<Mutex<Vec<crate::capabilities::ConsoleEntry>>>,
136) -> Result<MicrophoneInput, i32> {
137 let host = cpal::default_host();
138 let device = host
139 .default_input_device()
140 .ok_or_else(|| log_err(console, -2, "[MIC] No input device".to_string()))?;
141 let supported = match device.default_input_config() {
142 Ok(c) => c,
143 Err(e) => {
144 return Err(log_err(console, -3, format!("[MIC] Config: {e}")));
145 }
146 };
147 let sample_format = supported.sample_format();
148 let config: cpal::StreamConfig = supported.clone().into();
149 let channels = config.channels as usize;
150 let ring = Arc::new(Mutex::new(VecDeque::with_capacity(MIC_RING_CAP)));
151 let ring2 = ring.clone();
152 let console_err = console.clone();
153 let err_fn = move |e| {
154 console_log(
155 &console_err,
156 ConsoleLevel::Warn,
157 format!("[MIC] Stream error: {e}"),
158 );
159 };
160
161 let stream = match sample_format {
162 SampleFormat::F32 => device.build_input_stream(
163 &config,
164 move |data: &[f32], _| push_mono_f32(data, channels, &ring2),
165 err_fn,
166 None,
167 ),
168 SampleFormat::I16 => device.build_input_stream(
169 &config,
170 move |data: &[i16], _| push_mono_i16(data, channels, &ring2),
171 err_fn,
172 None,
173 ),
174 SampleFormat::U16 => device.build_input_stream(
175 &config,
176 move |data: &[u16], _| push_mono_u16(data, channels, &ring2),
177 err_fn,
178 None,
179 ),
180 other => {
181 return Err(log_err(
182 console,
183 -3,
184 format!("[MIC] Unsupported sample format {other:?}"),
185 ));
186 }
187 };
188 let stream = match stream {
189 Ok(s) => s,
190 Err(e) => {
191 return Err(log_err(console, -3, format!("[MIC] Build stream: {e}")));
192 }
193 };
194 if let Err(e) = stream.play() {
195 return Err(log_err(console, -3, format!("[MIC] Play: {e}")));
196 }
197 let sample_rate = supported.sample_rate();
198 Ok(MicrophoneInput {
199 stream,
200 buffer: ring,
201 sample_rate,
202 })
203}
204
205fn log_err(
206 console: &Arc<Mutex<Vec<crate::capabilities::ConsoleEntry>>>,
207 code: i32,
208 msg: String,
209) -> i32 {
210 console_log(console, ConsoleLevel::Warn, msg);
211 code
212}
213
214pub fn register_media_capture_functions(linker: &mut Linker<HostState>) -> Result<()> {
216 linker.func_wrap(
217 "oxide",
218 "api_camera_open",
219 |caller: Caller<'_, HostState>| -> i32 {
220 let console = caller.data().console.clone();
221 let st = caller.data().media_capture.clone();
222 if let Some(code) = permission_gate(&caller, PermissionKind::Camera) {
223 return code;
224 }
225 let mut g = st.lock().unwrap();
226 if let Some(mut cam) = g.camera.take() {
227 let _ = cam.stop_stream();
228 }
229 let cams = match nokhwa::query(nokhwa::utils::ApiBackend::Auto) {
230 Ok(c) => c,
231 Err(e) => {
232 return log_err(&console, -2, format!("[CAMERA] No cameras: {e}"));
233 }
234 };
235 if cams.is_empty() {
236 return log_err(&console, -2, "[CAMERA] No cameras found".to_string());
237 }
238 let req = RequestedFormat::new::<RgbFormat>(RequestedFormatType::HighestResolution(
239 nokhwa::utils::Resolution::new(1280, 720),
240 ));
241 let mut camera = match Camera::new(CameraIndex::Index(0), req) {
242 Ok(c) => c,
243 Err(e) => {
244 return log_err(&console, -3, format!("[CAMERA] Open failed: {e}"));
245 }
246 };
247 if let Err(e) = camera.open_stream() {
248 return log_err(&console, -3, format!("[CAMERA] Stream: {e}"));
249 }
250 g.camera = Some(camera);
251 0
252 },
253 )?;
254
255 linker.func_wrap(
256 "oxide",
257 "api_camera_close",
258 |caller: Caller<'_, HostState>| {
259 let st = caller.data().media_capture.clone();
260 let mut g = st.lock().unwrap();
261 if let Some(mut cam) = g.camera.take() {
262 let _ = cam.stop_stream();
263 }
264 },
265 )?;
266
267 linker.func_wrap(
268 "oxide",
269 "api_camera_capture_frame",
270 |mut caller: Caller<'_, HostState>, out_ptr: u32, out_cap: u32| -> u32 {
271 let mem = match caller.data().memory {
272 Some(m) => m,
273 None => return 0,
274 };
275 let st = caller.data().media_capture.clone();
276 let mut g = st.lock().unwrap();
277 let cam = match g.camera.as_mut() {
278 Some(c) => c,
279 None => return 0,
280 };
281 let buffer = match cam.frame() {
282 Ok(b) => b,
283 Err(e) => {
284 console_log(
285 &caller.data().console,
286 ConsoleLevel::Warn,
287 format!("[CAMERA] Frame: {e}"),
288 );
289 return 0;
290 }
291 };
292 let img = match buffer.decode_image::<RgbFormat>() {
293 Ok(i) => i,
294 Err(e) => {
295 console_log(
296 &caller.data().console,
297 ConsoleLevel::Warn,
298 format!("[CAMERA] Decode: {e}"),
299 );
300 return 0;
301 }
302 };
303 let w = img.width();
304 let h = img.height();
305 let mut rgba = Vec::with_capacity((w * h * 4) as usize);
306 for px in img.pixels() {
307 let p = px.0;
308 rgba.push(p[0]);
309 rgba.push(p[1]);
310 rgba.push(p[2]);
311 rgba.push(255);
312 }
313 g.last_frame_w = w;
314 g.last_frame_h = h;
315 g.camera_frames = g.camera_frames.saturating_add(1);
316 let write_len = rgba.len().min(out_cap as usize);
317 if write_guest_bytes(&mem, &mut caller, out_ptr, &rgba[..write_len]).is_err() {
318 return 0;
319 }
320 write_len as u32
321 },
322 )?;
323
324 linker.func_wrap(
325 "oxide",
326 "api_camera_frame_dimensions",
327 |caller: Caller<'_, HostState>| -> u64 {
328 let g = caller.data().media_capture.lock().unwrap();
329 ((g.last_frame_w as u64) << 32) | (g.last_frame_h as u64)
330 },
331 )?;
332
333 linker.func_wrap(
334 "oxide",
335 "api_microphone_open",
336 |caller: Caller<'_, HostState>| -> i32 {
337 let console = caller.data().console.clone();
338 let st = caller.data().media_capture.clone();
339 if let Some(code) = permission_gate(&caller, PermissionKind::Microphone) {
340 return code;
341 }
342 let mut g = st.lock().unwrap();
343 g.microphone = None;
344 match open_microphone(&console) {
345 Ok(m) => {
346 g.microphone = Some(m);
347 0
348 }
349 Err(code) => code,
350 }
351 },
352 )?;
353
354 linker.func_wrap(
355 "oxide",
356 "api_microphone_close",
357 |caller: Caller<'_, HostState>| {
358 let st = caller.data().media_capture.clone();
359 st.lock().unwrap().microphone = None;
360 },
361 )?;
362
363 linker.func_wrap(
364 "oxide",
365 "api_microphone_sample_rate",
366 |caller: Caller<'_, HostState>| -> u32 {
367 let g = caller.data().media_capture.lock().unwrap();
368 g.microphone.as_ref().map(|m| m.sample_rate).unwrap_or(0)
369 },
370 )?;
371
372 linker.func_wrap(
373 "oxide",
374 "api_microphone_read_samples",
375 |mut caller: Caller<'_, HostState>, out_ptr: u32, max_samples: u32| -> u32 {
376 let mem = match caller.data().memory {
377 Some(m) => m,
378 None => return 0,
379 };
380 let st = caller.data().media_capture.clone();
381 let g = st.lock().unwrap();
382 let mic = match g.microphone.as_ref() {
383 Some(m) => m,
384 None => return 0,
385 };
386 let mut q = mic.buffer.lock().unwrap();
387 let take = (max_samples as usize).min(q.len());
388 let mut chunk = Vec::with_capacity(take * 4);
389 for _ in 0..take {
390 if let Some(s) = q.pop_front() {
391 chunk.extend_from_slice(&s.to_le_bytes());
392 }
393 }
394 let write_len = chunk.len().min((max_samples as usize).saturating_mul(4));
395 if write_guest_bytes(&mem, &mut caller, out_ptr, &chunk[..write_len]).is_err() {
396 return 0;
397 }
398 (write_len / 4) as u32
399 },
400 )?;
401
402 linker.func_wrap(
403 "oxide",
404 "api_screen_capture",
405 |mut caller: Caller<'_, HostState>, out_ptr: u32, out_cap: u32| -> i32 {
406 let mem = match caller.data().memory {
407 Some(m) => m,
408 None => return -4,
409 };
410 let console = caller.data().console.clone();
411 if let Some(code) = permission_gate(&caller, PermissionKind::ScreenCapture) {
413 return code;
414 }
415 let screens = match screenshots::Screen::all() {
416 Ok(s) => s,
417 Err(e) => {
418 console_log(
419 &console,
420 ConsoleLevel::Warn,
421 format!("[SCREEN] Enumerate: {e}"),
422 );
423 return -2;
424 }
425 };
426 let screen = match screens.first() {
427 Some(s) => s,
428 None => {
429 return log_err(&console, -2, "[SCREEN] No displays".to_string());
430 }
431 };
432 let img = match screen.capture() {
433 Ok(i) => i,
434 Err(e) => {
435 console_log(
436 &console,
437 ConsoleLevel::Warn,
438 format!("[SCREEN] Capture: {e}"),
439 );
440 return -3;
441 }
442 };
443 let w = img.width();
444 let h = img.height();
445 let rgba = img.into_raw();
446 let st = caller.data().media_capture.clone();
447 {
448 let mut g = st.lock().unwrap();
449 g.screen_w = w;
450 g.screen_h = h;
451 g.screen_captures = g.screen_captures.saturating_add(1);
452 }
453 let write_len = rgba.len().min(out_cap as usize);
454 if write_guest_bytes(&mem, &mut caller, out_ptr, &rgba[..write_len]).is_err() {
455 return -4;
456 }
457 write_len as i32
458 },
459 )?;
460
461 linker.func_wrap(
462 "oxide",
463 "api_screen_capture_dimensions",
464 |caller: Caller<'_, HostState>| -> u64 {
465 let g = caller.data().media_capture.lock().unwrap();
466 ((g.screen_w as u64) << 32) | (g.screen_h as u64)
467 },
468 )?;
469
470 linker.func_wrap(
471 "oxide",
472 "api_media_pipeline_stats",
473 |caller: Caller<'_, HostState>| -> u64 {
474 let g = caller.data().media_capture.lock().unwrap();
475 let mic_ring = g
476 .microphone
477 .as_ref()
478 .map(|m| m.buffer.lock().unwrap().len() as u32)
479 .unwrap_or(0);
480 (g.camera_frames << 32) | (mic_ring as u64)
481 },
482 )?;
483
484 Ok(())
485}