diff --git a/Cargo.toml b/Cargo.toml index 7e8f236b5..b5fbfc699 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -139,6 +139,9 @@ objc2 = { version = "0.6" } [target.'cfg(target_os = "macos")'.dependencies] jack = { version = "0.13", optional = true } +libloading = "0.8" +block2 = "0.6" +objc2-core-foundation = "0.3" [target.'cfg(target_os = "ios")'.dependencies] objc2-avf-audio = { version = "0.3", default-features = false, features = [ diff --git a/src/host/coreaudio/macos/mod.rs b/src/host/coreaudio/macos/mod.rs index f34041e63..0bb24e4da 100644 --- a/src/host/coreaudio/macos/mod.rs +++ b/src/host/coreaudio/macos/mod.rs @@ -20,6 +20,7 @@ use property_listener::AudioObjectPropertyListener; mod device; pub mod enumerate; mod loopback; +pub mod permissions; mod property_listener; pub use device::Device; diff --git a/src/host/coreaudio/macos/permissions.rs b/src/host/coreaudio/macos/permissions.rs new file mode 100644 index 000000000..e854efcae --- /dev/null +++ b/src/host/coreaudio/macos/permissions.rs @@ -0,0 +1,66 @@ +//! macOS system audio recording permission helpers. +//! +//! These functions check and request the "System Audio Recording" permission +//! (`kTCCServiceAudioCapture`) via the private TCC framework — required for +//! loopback recording via [`default_output_device`](super::enumerate::default_output_device). + +use block2::StackBlock; +use libloading::{Library, Symbol}; +use objc2_core_foundation::{CFRetained, CFString}; +use std::ffi::c_void; + +const TCC_FRAMEWORK: &str = "/System/Library/PrivateFrameworks/TCC.framework/Versions/A/TCC"; +const TCC_SERVICE: &str = "kTCCServiceAudioCapture"; + +fn load_tcc() -> Option { + unsafe { Library::new(TCC_FRAMEWORK) }.ok() +} + +fn tcc_service() -> CFRetained { + CFString::from_str(TCC_SERVICE) +} + +/// Request system audio recording permission, showing the system prompt if needed. +/// +/// **Blocking** — does not return until the user responds. +/// Returns `false` immediately (without showing UI) if previously denied — +/// call [`open_system_audio_settings`] in that case. +pub fn request_system_audio_permission() -> bool { + let Some(lib) = load_tcc() else { return false }; + unsafe { + let Ok(request_fn): Result< + Symbol, + _, + > = lib.get(b"TCCAccessRequest\0") else { + return false; + }; + + let (tx, rx) = std::sync::mpsc::sync_channel::(1); + // Store as usize (Copy) so TCC's internal block memcpy doesn't double-drop the sender. + let tx_ptr = Box::into_raw(Box::new(tx)) as usize; + + let completion = StackBlock::new(move |granted: u8| { + let tx = Box::from_raw(tx_ptr as *mut std::sync::mpsc::SyncSender); + tx.send(granted != 0).ok(); + }); + + let service = tcc_service(); + request_fn( + &*service as *const _ as *const c_void, + std::ptr::null(), + &completion as *const _ as *const c_void, + ); + + rx.recv().unwrap_or(false) + } +} + +/// Open Privacy & Security > System Audio Recording in System Settings. +/// +/// Call this when [`request_system_audio_permission`] returns `false` (previously denied). +pub fn open_system_audio_settings() { + std::process::Command::new("open") + .arg("x-apple.systempreferences:com.apple.settings.PrivacySecurity.extension?Privacy_AudioCapture") + .spawn() + .ok(); +} diff --git a/src/host/coreaudio/mod.rs b/src/host/coreaudio/mod.rs index f0ef906f3..e56a0ea61 100644 --- a/src/host/coreaudio/mod.rs +++ b/src/host/coreaudio/mod.rs @@ -15,7 +15,7 @@ use crate::{BackendSpecificError, SampleFormat, StreamConfig}; #[cfg(target_os = "ios")] mod ios; #[cfg(target_os = "macos")] -mod macos; +pub mod macos; #[cfg(target_os = "ios")] #[allow(unused_imports)] diff --git a/src/lib.rs b/src/lib.rs index 83dbeca94..9499eafb2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -183,6 +183,10 @@ pub use device_description::{ DeviceDescription, DeviceDescriptionBuilder, DeviceDirection, DeviceType, InterfaceType, }; pub use error::*; +#[cfg(target_os = "macos")] +pub use host::coreaudio::macos::permissions::{ + open_system_audio_settings, request_system_audio_permission, +}; pub use platform::{ available_hosts, default_host, host_from_id, Device, Devices, Host, HostId, Stream, SupportedInputConfigs, SupportedOutputConfigs, ALL_HOSTS, diff --git a/src/platform/mod.rs b/src/platform/mod.rs index 2d4715a4b..fa7e2c373 100644 --- a/src/platform/mod.rs +++ b/src/platform/mod.rs @@ -748,6 +748,11 @@ mod platform_impl { #[cfg(any(target_os = "macos", target_os = "ios"))] mod platform_impl { + #[cfg(target_os = "macos")] + #[cfg_attr(docsrs, doc(cfg(target_os = "macos")))] + pub use crate::host::coreaudio::macos::permissions::{ + open_system_audio_settings, request_system_audio_permission, + }; #[cfg_attr(docsrs, doc(cfg(any(target_os = "macos", target_os = "ios"))))] pub use crate::host::coreaudio::Host as CoreAudioHost; #[cfg(all(feature = "jack", target_os = "macos"))]