Skip to content
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
3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down
1 change: 1 addition & 0 deletions src/host/coreaudio/macos/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
66 changes: 66 additions & 0 deletions src/host/coreaudio/macos/permissions.rs
Original file line number Diff line number Diff line change
@@ -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<Library> {
unsafe { Library::new(TCC_FRAMEWORK) }.ok()
}

fn tcc_service() -> CFRetained<CFString> {
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<unsafe extern "C" fn(*const c_void, *const c_void, *const c_void)>,
_,
> = lib.get(b"TCCAccessRequest\0") else {
return false;
};

let (tx, rx) = std::sync::mpsc::sync_channel::<bool>(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<bool>);
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();
}
2 changes: 1 addition & 1 deletion src/host/coreaudio/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down
4 changes: 4 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
5 changes: 5 additions & 0 deletions src/platform/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"))]
Expand Down