import AudioCap's AudioRecordingPermission class

This commit is contained in:
Nick Payne
2025-07-04 09:38:47 +01:00
parent 80d7555b60
commit 4bbdee96ac
2 changed files with 137 additions and 1 deletions
@@ -0,0 +1,131 @@
import OSLog
import Observation
import SwiftUI
// Adapted with a huge debt of gratitude from https://github.com/insidegui/AudioCap/blob/main/AudioCap/ProcessTap/AudioRecordingPermission.swift
/// Uses TCC SPI in order to check/request system audio recording permission.
@Observable
final class AudioRecordingPermission {
// private let logger = Logger(subsystem: kAppSubsystem, category: String(describing: AudioRecordingPermission.self))
enum Status: String {
case unknown
case denied
case authorized
}
private(set) var status: Status = .unknown
init() {
#if ENABLE_TCC_SPI
NotificationCenter.default.addObserver(
forName: NSApplication.didBecomeActiveNotification, object: nil, queue: .main
) { [weak self] _ in
guard let self else { return }
self.updateStatus()
}
updateStatus()
#else
status = .authorized
#endif // ENABLE_TCC_SPI
}
func request() {
#if ENABLE_TCC_SPI
// logger.debug(#function)
guard let request = Self.requestSPI else {
// logger.fault("Request SPI missing")
return
}
request("kTCCServiceAudioCapture" as CFString, nil) { [weak self] granted in
guard let self else { return }
// self.logger.info("Request finished with result: \(granted, privacy: .public)")
DispatchQueue.main.async {
if granted {
self.status = .authorized
} else {
self.status = .denied
}
}
}
#endif // ENABLE_TCC_SPI
}
private func updateStatus() {
#if ENABLE_TCC_SPI
// logger.debug(#function)
guard let preflight = Self.preflightSPI else {
// logger.fault("Preflight SPI missing")
return
}
let result = preflight("kTCCServiceAudioCapture" as CFString, nil)
if result == 1 {
status = .denied
} else if result == 0 {
status = .authorized
} else {
status = .unknown
}
#endif // ENABLE_TCC_SPI
}
#if ENABLE_TCC_SPI
private typealias PreflightFuncType = @convention(c) (CFString, CFDictionary?) -> Int
private typealias RequestFuncType = @convention(c) (
CFString, CFDictionary?, @escaping (Bool) -> Void
) -> Void
/// `dlopen` handle to the TCC framework.
private static let apiHandle: UnsafeMutableRawPointer? = {
let tccPath = "/System/Library/PrivateFrameworks/TCC.framework/Versions/A/TCC"
guard let handle = dlopen(tccPath, RTLD_NOW) else {
assertionFailure("dlopen failed")
return nil
}
return handle
}()
/// `dlsym` function handle for `TCCAccessPreflight`.
private static let preflightSPI: PreflightFuncType? = {
guard let apiHandle else { return nil }
let fnName = "TCCAccessPreflight"
guard let funcSym = dlsym(apiHandle, fnName) else {
assertionFailure("Couldn't find symbol")
return nil
}
let fn = unsafeBitCast(funcSym, to: PreflightFuncType.self)
return fn
}()
/// `dlsym` function handle for `TCCAccessRequest`.
private static let requestSPI: RequestFuncType? = {
guard let apiHandle else { return nil }
let fnName = "TCCAccessRequest"
guard let funcSym = dlsym(apiHandle, fnName) else {
assertionFailure("Couldn't find symbol")
return nil
}
let fn = unsafeBitCast(funcSym, to: RequestFuncType.self)
return fn
}()
#endif // ENABLE_TCC_SPI
}