Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(mac): expose getDisplayMedia() permission decision handler #1196

Open
wants to merge 9 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changes/fix-unsuppress-media-request-for-mac.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"wry": patch
---

On macOS 14.0, unsuppressing the media permission request.
28 changes: 21 additions & 7 deletions examples/simple.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,19 @@ fn main() -> wry::Result<()> {
let event_loop = EventLoop::new();
let window = WindowBuilder::new().build(&event_loop).unwrap();

#[cfg(any(
target_os = "windows",
target_os = "macos",
target_os = "ios",
target_os = "android"
))]
#[cfg(any(target_os = "windows", target_os = "ios", target_os = "android"))]
let builder = WebViewBuilder::new(&window);

// TODO: remove this
#[cfg(target_os = "macos")]
let builder = {
use wry::WebViewBuilderExtMacOS;
WebViewBuilder::new(&window).with_display_capture_decision_handler(|capture_type| {
dbg!(capture_type);
wry::WKDisplayCapturePermissionDecision::WindowPrompt
})
};

#[cfg(not(any(
target_os = "windows",
target_os = "macos",
Expand All @@ -35,7 +40,7 @@ fn main() -> wry::Result<()> {
};

let _webview = builder
.with_url("http://tauri.app")
.with_url("https://webrtc.github.io/samples/src/content/getusermedia/getdisplaymedia/") // TODO: revert
.with_drag_drop_handler(|e| {
match e {
wry::DragDropEvent::Enter { paths, position } => {
Expand All @@ -53,6 +58,15 @@ fn main() -> wry::Result<()> {
})
.build()?;

// TODO: remove me
#[cfg(target_os = "macos")]
{
use wry::WebViewExtMacOS;
_webview.set_display_capture_decision_handler(|_capture_type| {
wry::WKDisplayCapturePermissionDecision::ScreenPrompt
});
}

event_loop.run(move |event, _, control_flow| {
*control_flow = ControlFlow::Wait;

Expand Down
42 changes: 41 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,8 @@ use webkitgtk::*;
pub(crate) mod wkwebview;
#[cfg(any(target_os = "macos", target_os = "ios"))]
use wkwebview::*;
#[cfg(target_os = "macos")]
pub use wkwebview::{WKDisplayCapturePermissionDecision, WKMediaCaptureType};

#[cfg(target_os = "windows")]
pub(crate) mod webview2;
Expand Down Expand Up @@ -1181,6 +1183,34 @@ impl WebViewBuilderExtAndroid for WebViewBuilder<'_> {
}
}

#[cfg(target_os = "macos")]
#[derive(Default)]
pub(crate) struct PlatformSpecificWebViewAttributes {
/// A closure that make the permission decision for display capture (e.g. getDisplayMedia()) requests.
///
/// Only available on macOS 13+.
pub display_capture_decision_handler:
Option<Box<dyn Fn(WKMediaCaptureType) -> WKDisplayCapturePermissionDecision>>,
}

#[cfg(target_os = "macos")]
pub trait WebViewBuilderExtMacOS {
fn with_display_capture_decision_handler<F>(self, handler: F) -> Self
where
F: Fn(WKMediaCaptureType) -> WKDisplayCapturePermissionDecision + 'static;
}

#[cfg(target_os = "macos")]
impl WebViewBuilderExtMacOS for WebViewBuilder<'_> {
fn with_display_capture_decision_handler<F>(mut self, handler: F) -> Self
where
F: Fn(WKMediaCaptureType) -> WKDisplayCapturePermissionDecision + 'static,
{
self.platform_specific.display_capture_decision_handler = Some(Box::new(handler));
self
}
}

#[cfg(any(
target_os = "linux",
target_os = "dragonfly",
Expand Down Expand Up @@ -1566,6 +1596,10 @@ pub trait WebViewExtMacOS {
fn ns_window(&self) -> cocoa::base::id;
/// Attaches this webview to the given NSWindow and removes it from the current one.
fn reparent(&self, window: cocoa::base::id) -> Result<()>;
/// Set display capture decision handler to decide if incoming display capture request is allowed and its target.
fn set_display_capture_decision_handler<F>(&self, handler: F)
where
F: Fn(WKMediaCaptureType) -> WKDisplayCapturePermissionDecision + 'static;
}

#[cfg(target_os = "macos")]
Expand All @@ -1588,6 +1622,13 @@ impl WebViewExtMacOS for WebView {
fn reparent(&self, window: cocoa::base::id) -> Result<()> {
self.webview.reparent(window)
}

fn set_display_capture_decision_handler<F>(&self, handler: F)
where
F: Fn(WKMediaCaptureType) -> WKDisplayCapturePermissionDecision + 'static,
{
self.webview.set_display_capture_decision_handler(handler);
}
}

/// Additional methods on `WebView` that are specific to iOS.
Expand Down Expand Up @@ -1654,7 +1695,6 @@ pub enum PageLoadEvent {
target_os = "netbsd",
target_os = "openbsd",
target_os = "ios",
target_os = "macos",
))]
#[derive(Default)]
pub(crate) struct PlatformSpecificWebViewAttributes;
Expand Down
115 changes: 115 additions & 0 deletions src/wkwebview/display_capture.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
use crate::operating_system_version;
use cocoa::base::id;
use objc::{
declare::ClassDecl,
runtime::{Object, Sel},
};
use std::ffi::c_void;

#[repr(isize)]
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum WKMediaCaptureType {
Camera = 0,
Microphone,
CameraAndMicrophone,
}

impl From<isize> for WKMediaCaptureType {
fn from(value: isize) -> Self {
match value {
0 => WKMediaCaptureType::Camera,
1 => WKMediaCaptureType::Microphone,
2 => WKMediaCaptureType::CameraAndMicrophone,
_ => panic!("Invalid WKMediaCaptureType value"),
}
}
}

#[repr(isize)]
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum WKDisplayCapturePermissionDecision {
Deny = 0,
ScreenPrompt,
WindowPrompt,
}

impl From<isize> for WKDisplayCapturePermissionDecision {
fn from(value: isize) -> Self {
match value {
0 => WKDisplayCapturePermissionDecision::Deny,
1 => WKDisplayCapturePermissionDecision::ScreenPrompt,
2 => WKDisplayCapturePermissionDecision::WindowPrompt,
_ => panic!("Invalid WKDisplayCapturePermissionDecision value"),
}
}
}

pub(crate) fn declare_decision_handler(ctl: &mut ClassDecl) {
#[cfg(target_os = "macos")]
if operating_system_version().0 >= 13 {
unsafe {
ctl.add_ivar::<*mut c_void>("display_capture_decision_handler");
ctl.add_method(sel!(_webView:requestDisplayCapturePermissionForOrigin:initiatedByFrame:withSystemAudio:decisionHandler:),
request_display_capture_permission as extern "C" fn(&Object, Sel, id, id, id, isize, id),);
}
}
}

pub(crate) fn set_decision_handler(
webview: id,
handler: Option<Box<dyn Fn(WKMediaCaptureType) -> WKDisplayCapturePermissionDecision + 'static>>,
) {
#[cfg(target_os = "macos")]
if operating_system_version().0 >= 13 {
unsafe {
if let Some(handler) = handler {
drop_decision_hanlder(webview);

let ui_delegate: id = msg_send![webview, UIDelegate];
let handler = Box::into_raw(Box::new(handler));
(*ui_delegate).set_ivar(
"display_capture_decision_handler",
handler as *mut _ as *mut c_void,
);
}
}
}
}

pub(crate) fn drop_decision_hanlder(webview: *mut Object) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
pub(crate) fn drop_decision_hanlder(webview: *mut Object) {
pub(crate) fn drop_decision_handler(webview: *mut Object) {

(the usages of this function have the same typo)

unsafe {
let ui_delegate: id = msg_send![webview, UIDelegate];
let function = (*ui_delegate).get_ivar::<*mut c_void>("display_capture_decision_handler");
if !function.is_null() {
let function = *function
as *mut Box<dyn for<'s> Fn(WKMediaCaptureType) -> WKDisplayCapturePermissionDecision>;
drop(Box::from_raw(function));
}
}
}

extern "C" fn request_display_capture_permission(
this: &Object,
_: Sel,
_webview: id,
_origin: id,
_frame: id,
capture_type: isize,
decision_handler: id,
) {
unsafe {
let decision_handler =
decision_handler as *mut block::Block<(WKDisplayCapturePermissionDecision,), c_void>;

let function = this.get_ivar::<*mut c_void>("display_capture_decision_handler");
if !function.is_null() {
let function = *function
as *mut Box<dyn for<'s> Fn(WKMediaCaptureType) -> WKDisplayCapturePermissionDecision>;

let decision = (*function)(WKMediaCaptureType::from(capture_type));
(*decision_handler).call((decision,));
} else {
(*decision_handler).call((WKDisplayCapturePermissionDecision::Deny,));
}
}
}
53 changes: 46 additions & 7 deletions src/wkwebview/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT

#[cfg(target_os = "macos")]
mod display_capture;
mod download;
#[cfg(target_os = "macos")]
mod drag_drop;
Expand All @@ -15,7 +17,7 @@ mod synthetic_mouse_events;
use cocoa::appkit::{NSView, NSViewHeightSizable, NSViewMinYMargin, NSViewWidthSizable};
use cocoa::{
base::{id, nil, NO, YES},
foundation::{NSDictionary, NSFastEnumeration, NSInteger},
foundation::{NSDictionary, NSFastEnumeration, NSInteger, NSOperatingSystemVersion},
};
use dpi::{LogicalPosition, LogicalSize};
use raw_window_handle::{HasWindowHandle, RawWindowHandle};
Expand Down Expand Up @@ -47,6 +49,9 @@ use crate::{
},
};

#[cfg(target_os = "macos")]
pub use display_capture::{WKDisplayCapturePermissionDecision, WKMediaCaptureType};

use crate::{
wkwebview::{
download::{
Expand Down Expand Up @@ -126,7 +131,7 @@ impl InnerWebView {
fn new_ns_view(
ns_view: id,
attributes: WebViewAttributes,
_pl_attrs: super::PlatformSpecificWebViewAttributes,
pl_attrs: super::PlatformSpecificWebViewAttributes,
_web_context: Option<&mut WebContext>,
is_child: bool,
) -> Result<Self> {
Expand Down Expand Up @@ -806,19 +811,20 @@ impl InnerWebView {
}
}

// getUserMedia permission request handler
extern "C" fn request_media_capture_permission(
_this: &Object,
_: Sel,
_webview: id,
_origin: id,
_frame: id,
_type: id,
_capture_type: isize,
decision_handler: id,
) {
unsafe {
let decision_handler = decision_handler as *mut block::Block<(NSInteger,), c_void>;
//https://developer.apple.com/documentation/webkit/wkpermissiondecision?language=objc
(*decision_handler).call((1,));
// https://developer.apple.com/documentation/webkit/wkpermissiondecision?language=objc
let decision_handler = decision_handler as *mut block::Block<(u64,), c_void>;
(*decision_handler).call((1,)); // should be 1 for mic/cam
}
}

Expand All @@ -831,16 +837,24 @@ impl InnerWebView {

ctl.add_method(
sel!(webView:requestMediaCapturePermissionForOrigin:initiatedByFrame:type:decisionHandler:),
request_media_capture_permission as extern "C" fn(&Object, Sel, id, id, id, id, id),
request_media_capture_permission as extern "C" fn(&Object, Sel, id, id, id, isize, id),
);

// Workaround for getDisplayMedia()
// https://github.com/tauri-apps/wry/issues/1195
#[cfg(target_os = "macos")]
display_capture::declare_decision_handler(&mut ctl);

ctl.register()
}
None => class!(WebViewUIDelegate),
};
let ui_delegate: id = msg_send![ui_delegate, new];
let _: () = msg_send![webview, setUIDelegate: ui_delegate];

#[cfg(target_os = "macos")]
display_capture::set_decision_handler(webview, pl_attrs.display_capture_decision_handler);

// File drop handling
#[cfg(target_os = "macos")]
let drag_drop_ptr = match attributes.drag_drop_handler {
Expand Down Expand Up @@ -1209,6 +1223,14 @@ r#"Object.defineProperty(window, 'ipc', {

Ok(())
}

#[cfg(target_os = "macos")]
pub fn set_display_capture_decision_handler<F>(&self, handler: F)
where
F: Fn(WKMediaCaptureType) -> WKDisplayCapturePermissionDecision + 'static,
{
display_capture::set_decision_handler(self.webview, Some(Box::new(handler)));
}
}

pub fn url_from_webview(webview: id) -> Result<String> {
Expand All @@ -1229,8 +1251,22 @@ pub fn url_from_webview(webview: id) -> Result<String> {
.map_err(Into::into)
}

pub fn operating_system_version() -> (u64, u64, u64) {
unsafe {
let process_info: id = msg_send![class!(NSProcessInfo), processInfo];
let version: NSOperatingSystemVersion = msg_send![process_info, operatingSystemVersion];
(
version.majorVersion,
version.minorVersion,
version.patchVersion,
)
}
}

/// **Warning:** Panic occurs when caller application's `CFBundleDisplayName` contains non-ASCII characters.
pub fn platform_webview_version() -> Result<String> {
unsafe {
// bundleWithIdentifier: panic when CFBundleDisplayName contains non-ASCII characters.
let bundle: id =
msg_send![class!(NSBundle), bundleWithIdentifier: NSString::new("com.apple.WebKit")];
let dict: id = msg_send![bundle, infoDictionary];
Expand Down Expand Up @@ -1279,6 +1315,9 @@ impl Drop for InnerWebView {
}
}

#[cfg(target_os = "macos")]
display_capture::drop_decision_hanlder(self.webview);

// Remove webview from window's NSView before dropping.
let () = msg_send![self.webview, removeFromSuperview];
let _: Id<_> = Id::from_retained_ptr(self.webview);
Expand Down
Loading