Skip to main content

app_ui/
camera_ipc.rs

1//! JS-bridge bindings for the M-CAM.2 Tauri camera commands
2//! (M-CAM.4 / AUT-258 + M-REC.1 / AUT-260).
3//!
4//! Mirrors `crate::player_ipc` for the camera surface. Each function
5//! invokes a `__screen*` helper declared in `index.html`'s inline
6//! script, which in turn wraps `window.__TAURI__.core.invoke(...)`.
7//!
8//! The returned values come back as `JsValue` and are deserialised
9//! into typed Rust structs via `serde_wasm_bindgen` — the same
10//! pattern the player IPC layer uses.
11
12use serde::{Deserialize, Serialize};
13use wasm_bindgen::JsValue;
14use wasm_bindgen::prelude::*;
15
16/// Mirror of `crates/app/src/commands.rs::CameraView` (M-CAM.2).
17#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
18pub struct CameraView {
19    /// Stable device id.
20    pub id: String,
21    /// Human-readable label.
22    pub label: String,
23    /// First in the enumeration order.
24    pub is_default: bool,
25}
26
27/// Mirror of `crates/app/src/commands.rs::CameraPermission` (M-CAM.2).
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
29pub enum CameraPermission {
30    /// User has granted camera access.
31    Granted,
32    /// macOS has not yet asked the user.
33    NotDetermined,
34    /// User has explicitly denied access.
35    Denied,
36}
37
38#[wasm_bindgen]
39extern "C" {
40    /// `__screenListCameras()` in `index.html` — returns `Promise<CameraView[]>`.
41    #[wasm_bindgen(js_name = __screenListCameras, catch)]
42    pub async fn list_cameras_js() -> Result<JsValue, JsValue>;
43
44    /// `__screenStartPreview(cameraId)` in `index.html` — returns `Promise<void>`.
45    #[wasm_bindgen(js_name = __screenStartPreview, catch)]
46    pub async fn start_preview_js(camera_id: String) -> Result<JsValue, JsValue>;
47
48    /// `__screenStopPreview()` in `index.html` — returns `Promise<void>`.
49    #[wasm_bindgen(js_name = __screenStopPreview, catch)]
50    pub async fn stop_preview_js() -> Result<JsValue, JsValue>;
51
52    /// `__screenCameraPermissionStatus()` in `index.html` — returns
53    /// `Promise<CameraPermission>`.
54    #[wasm_bindgen(js_name = __screenCameraPermissionStatus, catch)]
55    pub async fn camera_permission_status_js() -> Result<JsValue, JsValue>;
56
57    /// `__screenOpenSettingsCamera()` (M-RECP.0 / AUT-261) — shells
58    /// out to open System Settings → Privacy & Security → Camera.
59    #[wasm_bindgen(js_name = __screenOpenSettingsCamera, catch)]
60    pub async fn open_settings_camera_js() -> Result<JsValue, JsValue>;
61
62    /// `__screenRequestAllPermissions()` (M-PIX.9) — fires the
63    /// macOS TCC prompts for Camera + Mic + Screen Recording.
64    /// Returns `{ camera, microphone, screen_recording }` statuses.
65    #[wasm_bindgen(js_name = __screenRequestAllPermissions, catch)]
66    pub async fn request_all_permissions_js() -> Result<JsValue, JsValue>;
67}
68
69/// Result of [`request_all_permissions`] — final TCC status for
70/// each protected resource.
71#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
72#[serde(rename_all = "snake_case")]
73pub struct AllPermissionsResult {
74    /// Camera access status.
75    pub camera: CameraPermission,
76    /// Microphone access status.
77    pub microphone: CameraPermission,
78    /// Screen Recording access status.
79    pub screen_recording: CameraPermission,
80}
81
82/// Async helper: list every camera the OS exposes.
83///
84/// Returns an empty `Vec` when running outside Tauri (e.g. plain
85/// `trunk serve` browser preview), or when the OS has no cameras.
86pub async fn list_cameras() -> Vec<CameraView> {
87    match list_cameras_js().await {
88        Ok(value) => serde_wasm_bindgen::from_value(value).unwrap_or_default(),
89        Err(_) => Vec::new(),
90    }
91}
92
93/// Async helper: kick off `start_preview` for the given camera id.
94///
95/// Failures are silently swallowed today — the M-CAM.3 frame channel
96/// hasn't landed yet, so observable behaviour is "state machine
97/// transitions to Running". Once frames flow the Recorder
98/// `<CameraPreview />` overlay will reflect any error via the
99/// `RecorderPreviewState` enum.
100pub async fn start_preview(camera_id: String) {
101    let _ = start_preview_js(camera_id).await;
102}
103
104/// Async helper: tear down the active preview.
105pub async fn stop_preview() {
106    let _ = stop_preview_js().await;
107}
108
109/// Async helper: probe OS camera permission state.
110///
111/// Returns `Granted` when running outside Tauri so the picker
112/// renders normally during `trunk serve` dev.
113pub async fn camera_permission_status() -> CameraPermission {
114    match camera_permission_status_js().await {
115        Ok(value) => serde_wasm_bindgen::from_value(value).unwrap_or(CameraPermission::Granted),
116        Err(_) => CameraPermission::Granted,
117    }
118}
119
120/// Async helper: shell out to open System Settings → Camera
121/// (M-RECP.0 / AUT-261). Silently no-ops outside Tauri (so
122/// `trunk serve` dev doesn't error on click) and silently swallows
123/// spawn errors — the user-facing "tell me if I can't open this"
124/// case is rare enough that surfacing it adds more noise than value.
125pub async fn open_settings_camera() {
126    let _ = open_settings_camera_js().await;
127}
128
129/// Async helper: fire the macOS TCC prompts (M-PIX.9). Returns the
130/// post-prompt statuses. Outside Tauri this returns `Granted` for
131/// every channel so the picker UX in `trunk serve` is unaffected.
132pub async fn request_all_permissions() -> AllPermissionsResult {
133    match request_all_permissions_js().await {
134        Ok(value) => serde_wasm_bindgen::from_value(value).unwrap_or(AllPermissionsResult {
135            camera: CameraPermission::Granted,
136            microphone: CameraPermission::Granted,
137            screen_recording: CameraPermission::Granted,
138        }),
139        Err(_) => AllPermissionsResult {
140            camera: CameraPermission::Granted,
141            microphone: CameraPermission::Granted,
142            screen_recording: CameraPermission::Granted,
143        },
144    }
145}