From a4d919c2fd73095d88ec9c0a202bebdbb604e5a8 Mon Sep 17 00:00:00 2001 From: kess Date: Mon, 21 Sep 2026 00:00:40 -0500 Subject: [PATCH 1/2] fix(desktop): grant camera and microphone webview permission Install a webview permission handler from the plugin so getUserMedia works on Windows (WebView2) and Linux (webkitgtk) without extra consumer configuration. - Add desktop_permissions module with a testable should_grant policy - Windows: WebView2 PermissionRequested -> allow camera/microphone - Linux: webkitgtk permission-request -> allow user-media - Add a verify-windows CI job and document the desktop behavior --- .github/workflows/verify.yml | 21 +++++++ Cargo.toml | 7 +++ README.md | 15 ++++- src/desktop_permissions.rs | 108 +++++++++++++++++++++++++++++++++++ src/lib.rs | 9 +++ 5 files changed, 157 insertions(+), 3 deletions(-) create mode 100644 src/desktop_permissions.rs diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml index 54183d2..5c214be 100644 --- a/.github/workflows/verify.yml +++ b/.github/workflows/verify.yml @@ -146,3 +146,24 @@ jobs: test -n "$SCHEME" test -n "$SIM" xcodebuild test -scheme "$SCHEME" -destination "platform=iOS Simulator,name=$SIM" + + verify-windows: + name: verify (windows) + runs-on: windows-latest + steps: + - uses: actions/checkout@v4 + + - uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt, clippy + + - uses: Swatinem/rust-cache@v2 + + - name: format + run: cargo fmt --all -- --check + + - name: clippy + run: cargo clippy --all-targets -- -D warnings + + - name: test + run: cargo test diff --git a/Cargo.toml b/Cargo.toml index 2a24747..8417896 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,5 +16,12 @@ serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" thiserror = "2" +[target.'cfg(target_os = "windows")'.dependencies] +webview2-com = "0.38" +windows = { version = "0.61", features = ["Win32_Foundation", "Win32_System_Com"] } + +[target.'cfg(target_os = "linux")'.dependencies] +webkit2gtk = { version = "2", features = ["v2_40"] } + [build-dependencies] tauri-plugin = { version = "2.6.3", features = ["build"] } diff --git a/README.md b/README.md index d9dbbed..2cee9a1 100644 --- a/README.md +++ b/README.md @@ -95,9 +95,18 @@ Add the following usage descriptions to the app `Info.plist` ### Desktop -Desktop uses the browser's `getUserMedia`. On macOS, `WKWebView` additionally requires -`NSCameraUsageDescription` and `NSMicrophoneUsageDescription` in the `Info.plist` (the -same keys above). +Desktop uses the browser's `getUserMedia`. The plugin installs a webview permission +handler on **Windows** (WebView2 `PermissionRequested`) and **Linux** (webkitgtk +`permission-request`) that allows camera and microphone, so `startPreview()` / +`requestPermissions()` work without extra configuration. The webview asks the plugin, +and the real gate is the operating system: + +- **Windows**: Settings → Privacy & security → Camera → turn on camera access and + "Let desktop apps access your camera". +- **Linux**: the desktop portal (e.g. `xdg-desktop-portal`) governs camera access. + +On **macOS**, `WKWebView` additionally requires `NSCameraUsageDescription` and +`NSMicrophoneUsageDescription` in the `Info.plist` (the same keys above). ## Usage diff --git a/src/desktop_permissions.rs b/src/desktop_permissions.rs new file mode 100644 index 0000000..2b54442 --- /dev/null +++ b/src/desktop_permissions.rs @@ -0,0 +1,108 @@ +//! Grants webview media permissions (camera / microphone) on desktop. +//! +//! wry only registers a webview permission handler for the clipboard, so +//! WebView2 (Windows) denies `getUserMedia` without showing a prompt, and +//! webkitgtk (Linux) may do the same. This module installs a handler from the +//! plugin so camera and microphone requests are allowed, matching what the +//! plugin advertises in its README. The real gate remains the operating +//! system privacy settings. + +/// Whether the plugin should grant a webview permission request. +/// +/// Only camera and microphone are granted; every other permission is left to +/// the host's default handling. +pub(crate) fn should_grant(is_camera: bool, is_microphone: bool) -> bool { + is_camera || is_microphone +} + +/// Installs the media permission handler for the given desktop webview. +#[cfg(desktop)] +pub(crate) fn install(webview: &tauri::Webview) { + #[cfg(target_os = "windows")] + install_windows(webview); + + #[cfg(target_os = "linux")] + install_linux(webview); + + #[cfg(not(any(target_os = "windows", target_os = "linux")))] + { + // macOS: wry already handles `requestMediaCapturePermission`; the app only + // needs `NSCameraUsageDescription` / `NSMicrophoneUsageDescription`. + let _ = webview; + } +} + +#[cfg(target_os = "windows")] +fn install_windows(webview: &tauri::Webview) { + use webview2_com::Microsoft::Web::WebView2::Win32::*; + use webview2_com::PermissionRequestedEventHandler; + + let _ = webview.with_webview(|platform| unsafe { + let Ok(core) = platform.controller().CoreWebView2() else { + return; + }; + let mut token = 0; + let _ = core.add_PermissionRequested( + &PermissionRequestedEventHandler::create(Box::new(|_, args| { + let Some(args) = args else { return Ok(()) }; + let mut kind = COREWEBVIEW2_PERMISSION_KIND::default(); + args.PermissionKind(&mut kind)?; + let is_camera = kind == COREWEBVIEW2_PERMISSION_KIND_CAMERA; + let is_microphone = kind == COREWEBVIEW2_PERMISSION_KIND_MICROPHONE; + if should_grant(is_camera, is_microphone) { + args.SetState(COREWEBVIEW2_PERMISSION_STATE_ALLOW)?; + } + Ok(()) + })), + &mut token, + ); + }); +} + +#[cfg(target_os = "linux")] +fn install_linux(webview: &tauri::Webview) { + use webkit2gtk::glib::prelude::*; + use webkit2gtk::{ + PermissionRequestExt, UserMediaPermissionRequest, UserMediaPermissionRequestExt, WebViewExt, + }; + + let _ = webview.with_webview(|platform| { + let webview = platform.inner(); + webview.connect_permission_request(|_, request| { + if let Some(media) = request.downcast_ref::() { + let is_camera = media.is_for_video_device(); + let is_microphone = media.is_for_audio_device(); + if should_grant(is_camera, is_microphone) { + request.allow(); + return true; + } + } + false + }); + }); +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn grants_camera() { + assert!(should_grant(true, false)); + } + + #[test] + fn grants_microphone() { + assert!(should_grant(false, true)); + } + + #[test] + fn grants_camera_and_microphone() { + assert!(should_grant(true, true)); + } + + #[test] + fn denies_unrelated_permissions() { + assert!(!should_grant(false, false)); + } +} diff --git a/src/lib.rs b/src/lib.rs index 57f2497..eff6c33 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,6 +10,9 @@ mod desktop; #[cfg(mobile)] mod mobile; +#[cfg(desktop)] +mod desktop_permissions; + mod error; mod models; @@ -43,5 +46,11 @@ pub fn init() -> TauriPlugin { app.manage(camera); Ok(()) }) + .on_webview_ready(|webview| { + #[cfg(desktop)] + desktop_permissions::install(&webview); + #[cfg(not(desktop))] + let _ = webview; + }) .build() } From 9c4bdf4f32f6170546e68590d43a2f81253c0f6d Mon Sep 17 00:00:00 2001 From: kess Date: Mon, 21 Sep 2026 00:04:03 -0500 Subject: [PATCH 2/2] Merge remote-tracking branch 'plugin-public/main' --- .changes/pre.json | 4 +- .github/workflows/verify.yml | 21 ------- CHANGELOG.md | 4 ++ Cargo.toml | 9 +-- README.md | 15 +---- package.json | 11 +++- src/desktop_permissions.rs | 108 ----------------------------------- src/lib.rs | 9 --- 8 files changed, 20 insertions(+), 161 deletions(-) delete mode 100644 src/desktop_permissions.rs diff --git a/.changes/pre.json b/.changes/pre.json index e688efb..65f2a65 100644 --- a/.changes/pre.json +++ b/.changes/pre.json @@ -1,4 +1,6 @@ { "tag": "beta", - "changes": [] + "changes": [ + ".changes/beta.md" + ] } diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml index 5c214be..54183d2 100644 --- a/.github/workflows/verify.yml +++ b/.github/workflows/verify.yml @@ -146,24 +146,3 @@ jobs: test -n "$SCHEME" test -n "$SIM" xcodebuild test -scheme "$SCHEME" -destination "platform=iOS Simulator,name=$SIM" - - verify-windows: - name: verify (windows) - runs-on: windows-latest - steps: - - uses: actions/checkout@v4 - - - uses: dtolnay/rust-toolchain@stable - with: - components: rustfmt, clippy - - - uses: Swatinem/rust-cache@v2 - - - name: format - run: cargo fmt --all -- --check - - - name: clippy - run: cargo clippy --all-targets -- -D warnings - - - name: test - run: cargo test diff --git a/CHANGELOG.md b/CHANGELOG.md index 825c32f..e90a215 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1 +1,5 @@ # Changelog + +## [0.1.1-beta.0] + +- [`6e012b4`](https://github.com/kessdev/tauri-plugin-camera/commit/6e012b42e14af9b6b47c996544e5af3a3cc78b22) First beta release to validate the automated release pipeline. diff --git a/Cargo.toml b/Cargo.toml index 8417896..8e28066 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tauri-plugin-camera-kessdev" -version = "0.1.0" +version = "0.1.1-beta.0" authors = [ "kess" ] description = "Camera plugin for Tauri v2: photo capture, video recording and preview on Android, iOS and desktop." license = "MIT" @@ -16,12 +16,5 @@ serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" thiserror = "2" -[target.'cfg(target_os = "windows")'.dependencies] -webview2-com = "0.38" -windows = { version = "0.61", features = ["Win32_Foundation", "Win32_System_Com"] } - -[target.'cfg(target_os = "linux")'.dependencies] -webkit2gtk = { version = "2", features = ["v2_40"] } - [build-dependencies] tauri-plugin = { version = "2.6.3", features = ["build"] } diff --git a/README.md b/README.md index 2cee9a1..d9dbbed 100644 --- a/README.md +++ b/README.md @@ -95,18 +95,9 @@ Add the following usage descriptions to the app `Info.plist` ### Desktop -Desktop uses the browser's `getUserMedia`. The plugin installs a webview permission -handler on **Windows** (WebView2 `PermissionRequested`) and **Linux** (webkitgtk -`permission-request`) that allows camera and microphone, so `startPreview()` / -`requestPermissions()` work without extra configuration. The webview asks the plugin, -and the real gate is the operating system: - -- **Windows**: Settings → Privacy & security → Camera → turn on camera access and - "Let desktop apps access your camera". -- **Linux**: the desktop portal (e.g. `xdg-desktop-portal`) governs camera access. - -On **macOS**, `WKWebView` additionally requires `NSCameraUsageDescription` and -`NSMicrophoneUsageDescription` in the `Info.plist` (the same keys above). +Desktop uses the browser's `getUserMedia`. On macOS, `WKWebView` additionally requires +`NSCameraUsageDescription` and `NSMicrophoneUsageDescription` in the `Info.plist` (the +same keys above). ## Usage diff --git a/package.json b/package.json index 9db5c04..7e01cbc 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,18 @@ { "name": "tauri-plugin-camera-kessdev", - "version": "0.1.0", + "version": "0.1.1-beta.0", "author": "kess", "description": "Camera plugin for Tauri v2: photo capture, video recording and preview on Android, iOS and desktop.", "license": "MIT", "repository": "https://github.com/kessdev/tauri-plugin-camera", - "keywords": ["tauri", "tauri-plugin", "camera", "rust", "android", "ios"], + "keywords": [ + "tauri", + "tauri-plugin", + "camera", + "rust", + "android", + "ios" + ], "type": "module", "types": "./dist-js/index.d.ts", "main": "./dist-js/index.cjs", diff --git a/src/desktop_permissions.rs b/src/desktop_permissions.rs deleted file mode 100644 index 2b54442..0000000 --- a/src/desktop_permissions.rs +++ /dev/null @@ -1,108 +0,0 @@ -//! Grants webview media permissions (camera / microphone) on desktop. -//! -//! wry only registers a webview permission handler for the clipboard, so -//! WebView2 (Windows) denies `getUserMedia` without showing a prompt, and -//! webkitgtk (Linux) may do the same. This module installs a handler from the -//! plugin so camera and microphone requests are allowed, matching what the -//! plugin advertises in its README. The real gate remains the operating -//! system privacy settings. - -/// Whether the plugin should grant a webview permission request. -/// -/// Only camera and microphone are granted; every other permission is left to -/// the host's default handling. -pub(crate) fn should_grant(is_camera: bool, is_microphone: bool) -> bool { - is_camera || is_microphone -} - -/// Installs the media permission handler for the given desktop webview. -#[cfg(desktop)] -pub(crate) fn install(webview: &tauri::Webview) { - #[cfg(target_os = "windows")] - install_windows(webview); - - #[cfg(target_os = "linux")] - install_linux(webview); - - #[cfg(not(any(target_os = "windows", target_os = "linux")))] - { - // macOS: wry already handles `requestMediaCapturePermission`; the app only - // needs `NSCameraUsageDescription` / `NSMicrophoneUsageDescription`. - let _ = webview; - } -} - -#[cfg(target_os = "windows")] -fn install_windows(webview: &tauri::Webview) { - use webview2_com::Microsoft::Web::WebView2::Win32::*; - use webview2_com::PermissionRequestedEventHandler; - - let _ = webview.with_webview(|platform| unsafe { - let Ok(core) = platform.controller().CoreWebView2() else { - return; - }; - let mut token = 0; - let _ = core.add_PermissionRequested( - &PermissionRequestedEventHandler::create(Box::new(|_, args| { - let Some(args) = args else { return Ok(()) }; - let mut kind = COREWEBVIEW2_PERMISSION_KIND::default(); - args.PermissionKind(&mut kind)?; - let is_camera = kind == COREWEBVIEW2_PERMISSION_KIND_CAMERA; - let is_microphone = kind == COREWEBVIEW2_PERMISSION_KIND_MICROPHONE; - if should_grant(is_camera, is_microphone) { - args.SetState(COREWEBVIEW2_PERMISSION_STATE_ALLOW)?; - } - Ok(()) - })), - &mut token, - ); - }); -} - -#[cfg(target_os = "linux")] -fn install_linux(webview: &tauri::Webview) { - use webkit2gtk::glib::prelude::*; - use webkit2gtk::{ - PermissionRequestExt, UserMediaPermissionRequest, UserMediaPermissionRequestExt, WebViewExt, - }; - - let _ = webview.with_webview(|platform| { - let webview = platform.inner(); - webview.connect_permission_request(|_, request| { - if let Some(media) = request.downcast_ref::() { - let is_camera = media.is_for_video_device(); - let is_microphone = media.is_for_audio_device(); - if should_grant(is_camera, is_microphone) { - request.allow(); - return true; - } - } - false - }); - }); -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn grants_camera() { - assert!(should_grant(true, false)); - } - - #[test] - fn grants_microphone() { - assert!(should_grant(false, true)); - } - - #[test] - fn grants_camera_and_microphone() { - assert!(should_grant(true, true)); - } - - #[test] - fn denies_unrelated_permissions() { - assert!(!should_grant(false, false)); - } -} diff --git a/src/lib.rs b/src/lib.rs index eff6c33..57f2497 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,9 +10,6 @@ mod desktop; #[cfg(mobile)] mod mobile; -#[cfg(desktop)] -mod desktop_permissions; - mod error; mod models; @@ -46,11 +43,5 @@ pub fn init() -> TauriPlugin { app.manage(camera); Ok(()) }) - .on_webview_ready(|webview| { - #[cfg(desktop)] - desktop_permissions::install(&webview); - #[cfg(not(desktop))] - let _ = webview; - }) .build() }