Skip to content
This repository was archived by the owner on Apr 27, 2026. It is now read-only.

Commit afd123a

Browse files
Match Philo microphone permission flow
Use Philo-style AVFoundation checks for microphone status and requests, remove the old Swift microphone bridge, and align microphone permission copy.
1 parent 15c66b4 commit afd123a

5 files changed

Lines changed: 146 additions & 60 deletions

File tree

src-tauri/Cargo.lock

Lines changed: 86 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src-tauri/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ aec3 = "0.1.7"
3737
swift-rs = { version = "1.0.7", features = ["build"] }
3838

3939
[target.'cfg(target_os = "macos")'.dependencies]
40+
block2 = "0.6.2"
41+
objc2 = "0.6.3"
42+
objc2-av-foundation = "0.3.2"
4043
swift-rs = "1.0.7"
4144
cidre = { version = "0.15", features = ["av"] }
4245
rodio = "0.22"

src-tauri/src/permissions.rs

Lines changed: 49 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
use serde::{Deserialize, Serialize};
22

33
#[cfg(target_os = "macos")]
4-
use cidre::{cf, core_audio as ca, ns, os};
4+
use block2::RcBlock;
55
#[cfg(target_os = "macos")]
6-
use swift_rs::{swift, Bool, Int};
7-
6+
use cidre::{cf, core_audio as ca, ns, os};
87
#[cfg(target_os = "macos")]
9-
swift!(fn _microphone_permission_status() -> Int);
8+
use objc2_av_foundation::{AVAuthorizationStatus, AVCaptureDevice, AVMediaTypeAudio};
109
#[cfg(target_os = "macos")]
11-
swift!(fn _request_microphone_permission() -> Bool);
10+
use swift_rs::{swift, Int};
11+
1212
#[cfg(target_os = "macos")]
1313
swift!(fn _audio_capture_permission_status() -> Int);
1414

@@ -55,16 +55,17 @@ pub fn snapshot() -> Result<PermissionSnapshot, String> {
5555
pub fn check(permission: PermissionKind) -> Result<PermissionStatus, String> {
5656
#[cfg(target_os = "macos")]
5757
{
58-
let raw = match permission {
59-
PermissionKind::Microphone => unsafe { _microphone_permission_status() as isize },
60-
PermissionKind::SystemAudio => unsafe { _audio_capture_permission_status() as isize },
61-
};
62-
63-
Ok(match raw {
64-
GRANTED => PermissionStatus::Authorized,
65-
NEVER_REQUESTED => PermissionStatus::NeverRequested,
66-
DENIED => PermissionStatus::Denied,
67-
_ => PermissionStatus::Denied,
58+
Ok(match permission {
59+
PermissionKind::Microphone => check_microphone(),
60+
PermissionKind::SystemAudio => {
61+
let raw = unsafe { _audio_capture_permission_status() as isize };
62+
match raw {
63+
GRANTED => PermissionStatus::Authorized,
64+
NEVER_REQUESTED => PermissionStatus::NeverRequested,
65+
DENIED => PermissionStatus::Denied,
66+
_ => PermissionStatus::Denied,
67+
}
68+
}
6869
})
6970
}
7071

@@ -79,7 +80,10 @@ pub fn request(permission: PermissionKind) -> Result<PermissionStatus, String> {
7980
#[cfg(target_os = "macos")]
8081
{
8182
let granted = match permission {
82-
PermissionKind::Microphone => unsafe { _request_microphone_permission() },
83+
PermissionKind::Microphone => {
84+
request_microphone();
85+
true
86+
}
8387
PermissionKind::SystemAudio => {
8488
request_system_audio_probe()?;
8589
true
@@ -120,6 +124,35 @@ pub fn open_settings(permission: PermissionKind) -> Result<(), String> {
120124
Ok(())
121125
}
122126

127+
#[cfg(target_os = "macos")]
128+
fn check_microphone() -> PermissionStatus {
129+
let status = unsafe {
130+
let media_type = AVMediaTypeAudio.unwrap();
131+
AVCaptureDevice::authorizationStatusForMediaType(media_type)
132+
};
133+
134+
match status {
135+
AVAuthorizationStatus::NotDetermined => PermissionStatus::NeverRequested,
136+
AVAuthorizationStatus::Authorized => PermissionStatus::Authorized,
137+
_ => PermissionStatus::Denied,
138+
}
139+
}
140+
141+
#[cfg(target_os = "macos")]
142+
fn request_microphone() {
143+
let (tx, rx) = std::sync::mpsc::channel::<bool>();
144+
let completion = RcBlock::new(move |granted: objc2::runtime::Bool| {
145+
let _ = tx.send(granted.as_bool());
146+
});
147+
148+
unsafe {
149+
let media_type = AVMediaTypeAudio.unwrap();
150+
AVCaptureDevice::requestAccessForMediaType_completionHandler(media_type, &completion);
151+
}
152+
153+
let _ = rx.recv_timeout(std::time::Duration::from_secs(60));
154+
}
155+
123156
#[cfg(target_os = "macos")]
124157
fn request_system_audio_probe() -> Result<(), String> {
125158
let stop_silence = play_silence();

src-tauri/swift-permissions/src/lib.swift

Lines changed: 0 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import AVFoundation
21
import Foundation
32

43
private let grantedValue = 0
@@ -14,49 +13,6 @@ private let tccHandle: UnsafeMutableRawPointer? = {
1413

1514
private typealias TCCPreflightFunc = @convention(c) (CFString, CFDictionary?) -> Int
1615

17-
private func mapCaptureMicrophoneStatus(_ status: AVAuthorizationStatus) -> Int {
18-
switch status {
19-
case .authorized:
20-
grantedValue
21-
case .notDetermined:
22-
neverRequestedValue
23-
case .denied, .restricted:
24-
deniedValue
25-
@unknown default:
26-
errorValue
27-
}
28-
}
29-
30-
@_cdecl("_microphone_permission_status")
31-
public func _microphone_permission_status() -> Int {
32-
mapCaptureMicrophoneStatus(AVCaptureDevice.authorizationStatus(for: .audio))
33-
}
34-
35-
@_cdecl("_request_microphone_permission")
36-
public func _request_microphone_permission() -> Bool {
37-
let status = AVCaptureDevice.authorizationStatus(for: .audio)
38-
39-
switch status {
40-
case .authorized:
41-
return true
42-
case .denied, .restricted:
43-
return false
44-
case .notDetermined:
45-
let semaphore = DispatchSemaphore(value: 0)
46-
var granted = false
47-
48-
AVCaptureDevice.requestAccess(for: .audio) { allowed in
49-
granted = allowed
50-
semaphore.signal()
51-
}
52-
53-
_ = semaphore.wait(timeout: .now() + .seconds(60))
54-
return granted
55-
@unknown default:
56-
return false
57-
}
58-
}
59-
6016
@_cdecl("_audio_capture_permission_status")
6117
public func _audio_capture_permission_status() -> Int {
6218
guard let tccHandle,

src/store.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1914,10 +1914,18 @@ function permissionHostHint(permission: PermissionKind) {
19141914
}
19151915

19161916
function permissionDeniedMessage(permission: PermissionKind) {
1917+
if (permission === "microphone") {
1918+
return `Microphone access is required to record meetings. Allow the app in System Settings > Privacy & Security > Microphone.${permissionHostHint(permission)}`;
1919+
}
1920+
19171921
return `${permissionLabel(permission)} access is off. Enable it in System Settings and try again.${permissionHostHint(permission)}`;
19181922
}
19191923

19201924
function permissionPendingMessage(permission: PermissionKind) {
1925+
if (permission === "microphone") {
1926+
return `Microphone permission request did not finish. Try recording again and allow access when prompted.${permissionHostHint(permission)}`;
1927+
}
1928+
19211929
return `${permissionLabel(permission)} permission request did not finish. Try again and allow access when prompted.${permissionHostHint(permission)}`;
19221930
}
19231931

0 commit comments

Comments
 (0)