diff --git a/.changes/restore-desktop-permission.md b/.changes/restore-desktop-permission.md new file mode 100644 index 0000000..ce89277 --- /dev/null +++ b/.changes/restore-desktop-permission.md @@ -0,0 +1,7 @@ +--- +"tauri-plugin-camera-kessdev": "patch" +"tauri-plugin-camera-kessdev-js": "patch" +--- + +Restore the desktop camera and microphone webview permission handler that was +dropped from main and shipped without it in 0.1.1-beta.1. 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 342ffdc..bc1b9ed 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() }