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}