How Meetily Handles Microphone and Screen Recording Permissions on macOS
Meetily triggers macOS permission dialogs by creating Core Audio capture taps in Rust, with dedicated Tauri commands in frontend/src-tauri/src/audio/permissions.rs that check status, open System Settings, and force the OS-level prompts for both microphone and system audio capture.
Meetily is a Tauri-based desktop application that captures meeting audio by accessing both the microphone and system audio (screen recording) on macOS. Because Tauri apps run native Rust code, the permission handling logic resides in the backend rather than the web frontend, specifically within the frontend/src-tauri/src/audio module. This architecture delegates the actual permission dialogs to macOS, which presents them automatically when the app attempts to create audio capture streams.
Screen Recording (System Audio) Permission Implementation
The system audio permission—required to capture speaker output on macOS—is managed through three distinct Rust functions exposed as Tauri commands. These handle the complete lifecycle from status checking to forcing the native permission dialog.
Checking Permission Status
The check_screen_recording_permission() function in frontend/src-tauri/src/audio/permissions.rs does not query the macOS privacy database directly. Instead, it logs that macOS will display the permission dialog automatically when a Core Audio tap is created, and it always returns true. This design acknowledges that the operating system controls the permission state, not the application code.
Opening System Settings
When the user needs to grant permission manually, request_screen_recording_permission() opens the macOS System Settings directly using the URL scheme x-apple.systempreferences:com.apple.preference.security. This navigates the user to the Privacy & Security section where they can enable Audio Capture (macOS's terminology for screen recording permission).
Triggering the Native Dialog
The trigger_system_audio_permission() function forces macOS to display the permission dialog by attempting to create a CoreAudioCapture tap. According to the source code in permissions.rs, this function:
- Attempts to instantiate
CoreAudioCapture::new() - Returns
Ok(true)if the tap creation succeeds (indicating permission was already granted or the user just approved it) - Inspects error messages for keywords like "permission" or "denied" if creation fails, returning
Ok(false)to signal that access was rejected
This approach relies on the NSAudioCaptureUsageDescription key being present in the app's Info.plist file, which macOS uses to populate the permission dialog message.
Microphone Permission Handling
Microphone access follows a similar pattern but is implemented in the device discovery layer rather than the permissions module.
Device Discovery and Permission Flow
In frontend/src-tauri/src/audio/devices/discovery.rs, the request_microphone_permission() function leverages the cpal (Cross-Platform Audio Library) API to trigger the permission flow. When the code attempts to create a capture stream via Device::new() or MicrophoneCapture::new(), macOS automatically presents the microphone permission dialog.
The function examines the result of the capture stream creation:
- Returns
trueif the device initializes successfully - Logs a warning and returns
falseif the initialization fails with a permissions-related error
Tauri Command Exposure
Both permission systems are exposed to the frontend through #[tauri::command] attributed functions. For microphone access, this is request_microphone_permission_command, allowing the React frontend in frontend/src/app/page.tsx to invoke the Rust logic directly via Tauri's IPC bridge.
End-to-End Permission Flow
The complete permission acquisition flow in Meetily follows these steps:
- Initial Check – The frontend calls
check_screen_recording_permission_commandon startup. If it returnsfalse(or the UI treats it as unverified), the interface prompts the user to enable system audio. - Settings Redirect – When the user clicks "Enable System Audio," the app invokes
request_screen_recording_permission_command, opening System Settings. - Microphone Trigger – The app attempts to start microphone capture via
request_microphone_permission, which forces macOS to show the microphone permission dialog automatically. - System Audio Trigger – The app then calls
trigger_system_audio_permission_command, creating a Core Audio tap that forces the screen-recording permission dialog if not yet granted. - Capture Start – Once both permissions are granted and the app restarts, the audio pipeline in
frontend/src-tauri/src/audio/pipeline.rsbegins mixing microphone and system audio streams.
Because macOS persists these permissions across app launches, subsequent sessions proceed directly to audio capture without repeating the permission flow.
Implementation Examples
The frontend invokes these permissions through Tauri's command system:
// Frontend TypeScript - Request microphone permission
import { invoke } from '@tauri-apps/api/tauri';
async function ensureMicrophone() {
const granted = await invoke<boolean>('request_microphone_permission_command');
if (!granted) {
alert('Please grant microphone access in System Settings → Privacy & Security.');
}
}
// Frontend TypeScript - Request screen recording permission
async function ensureScreenRecording() {
const hasPermission = await invoke<boolean>('check_screen_recording_permission_command');
if (!hasPermission) {
await invoke('request_screen_recording_permission_command');
}
const ok = await invoke<boolean>('trigger_system_audio_permission_command');
if (!ok) {
alert('System audio permission denied – enable it in System Settings.');
}
}
The Rust backend implements the screen recording trigger:
// Rust implementation in permissions.rs
#[tauri::command]
pub async fn request_screen_recording_permission_command() -> Result<(), String> {
request_screen_recording_permission().map_err(|e| e.to_string())
}
// Core Audio tap creation that triggers the permission dialog
#[cfg(target_os = "macos")]
pub fn trigger_system_audio_permission() -> Result<bool> {
match crate::audio::capture::CoreAudioCapture::new() {
Ok(_) => Ok(true),
Err(e) => {
let msg = e.to_string().to_lowercase();
if msg.contains("permission") { Ok(false) } else { Ok(false) }
}
}
}
Key Source Files
frontend/src-tauri/src/audio/permissions.rs– Containscheck_screen_recording_permission(),request_screen_recording_permission(), andtrigger_system_audio_permission()for system audio access.frontend/src-tauri/src/audio/devices/discovery.rs– Implementsrequest_microphone_permission()using the cpal crate to trigger microphone dialogs.frontend/src-tauri/src/audio/pipeline.rs– Orchestrates the audio mixing pipeline once permissions are granted.frontend/src-tauri/src/lib.rs– Registers Tauri commands includingrequest_screen_recording_permission_commandandrequest_microphone_permission_command.frontend/src/app/page.tsx– React frontend that invokes permission commands and handles user prompts.
Summary
- Meetily handles macOS permissions through Rust code in the Tauri backend, specifically in
frontend/src-tauri/src/audio/permissions.rsanddiscovery.rs. - Screen recording permission is triggered by creating a Core Audio tap, which forces macOS to display the native dialog automatically.
- Microphone permission is triggered when initializing cpal capture streams, with the OS handling the dialog presentation.
- The app cannot directly query permission status; instead, it attempts capture and handles success or failure, redirecting users to System Settings when manual intervention is required.
- All permission functions are exposed to the frontend via
#[tauri::command]attributes, enabling the React UI to coordinate the permission flow.
Frequently Asked Questions
Why does check_screen_recording_permission() always return true?
The function returns true because macOS does not provide a public API to query the screen recording permission state without triggering a dialog. Instead, the function logs that the OS will handle the dialog automatically when a Core Audio tap is created. The actual permission status is determined implicitly when the app attempts to create the capture tap in trigger_system_audio_permission().
What actually triggers the macOS permission dialogs?
The dialogs are triggered by attempting to create audio capture resources. For system audio, calling trigger_system_audio_permission() creates a CoreAudioCapture tap. For microphone, calling request_microphone_permission() initializes a cpal capture stream. Both actions signal the Core Audio system to check permissions, which prompts macOS to display the dialog if the user hasn't previously granted access.
Can Meetily request permissions programmatically without opening System Settings?
No. macOS security architecture requires the user to manually enable screen recording (Audio Capture) permission in System Settings. The request_screen_recording_permission() function can only open the System Settings window using the x-apple.systempreferences URL scheme; it cannot toggle the permission programmatically. The microphone permission similarly requires the user to interact with the native dialog that appears when capture is first attempted.
How does the frontend know when permissions are granted?
The frontend calls Tauri commands that return boolean values indicating whether the capture resource was successfully created. For example, trigger_system_audio_permission_command returns true if the Core Audio tap initializes successfully (meaning permission was granted) or false if it fails with a permission error. The frontend polls or awaits these command results to update the UI state, though it cannot detect when the user switches back from System Settings without re-invoking the check commands.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →