From 7a5a1601365b632befdfedc837d3390ecb091b54 Mon Sep 17 00:00:00 2001 From: Hiroshi Horie <548776+hiroshihorie@users.noreply.github.com> Date: Fri, 2 May 2025 19:31:00 +0900 Subject: [PATCH 1/2] macOS --- .../AudioDeviceModuleDelegateAdapter.swift | 16 +++++++-- Sources/LiveKit/LiveKit+DeviceHelpers.swift | 36 +++++++++++++++++++ 2 files changed, 50 insertions(+), 2 deletions(-) diff --git a/Sources/LiveKit/Audio/AudioDeviceModuleDelegateAdapter.swift b/Sources/LiveKit/Audio/AudioDeviceModuleDelegateAdapter.swift index e8146f3d0..5255c43b9 100644 --- a/Sources/LiveKit/Audio/AudioDeviceModuleDelegateAdapter.swift +++ b/Sources/LiveKit/Audio/AudioDeviceModuleDelegateAdapter.swift @@ -23,7 +23,7 @@ internal import LiveKitWebRTC #endif // Invoked on WebRTC's worker thread, do not block. -class AudioDeviceModuleDelegateAdapter: NSObject, LKRTCAudioDeviceModuleDelegate { +class AudioDeviceModuleDelegateAdapter: NSObject, LKRTCAudioDeviceModuleDelegate, Loggable { weak var audioManager: AudioManager? func audioDeviceModule(_: LKRTCAudioDeviceModule, didReceiveSpeechActivityEvent speechActivityEvent: RTCSpeechActivityEvent) { @@ -47,7 +47,19 @@ class AudioDeviceModuleDelegateAdapter: NSObject, LKRTCAudioDeviceModuleDelegate func audioDeviceModule(_: LKRTCAudioDeviceModule, willEnableEngine engine: AVAudioEngine, isPlayoutEnabled: Bool, isRecordingEnabled: Bool) -> Int { guard let audioManager else { return 0 } let entryPoint = audioManager.buildEngineObserverChain() - return entryPoint?.engineWillEnable(engine, isPlayoutEnabled: isPlayoutEnabled, isRecordingEnabled: isRecordingEnabled) ?? 0 + let result = entryPoint?.engineWillEnable(engine, isPlayoutEnabled: isPlayoutEnabled, isRecordingEnabled: isRecordingEnabled) ?? 0 + + if isRecordingEnabled { + // At this point mic perms / session should be configured for recording. + #if os(macOS) + // This will block WebRTC's worker thread, but when instantiating AVAudioInput node it will block by showing a dialog anyways. + // Attempt to acquire mic perms at this point to return an error at SDK level. + let status = LiveKitSDK.ensureDeviceAccessSync(for: [.audio]) + log("Ensure device access result: \(status)") + #endif + } + + return result } func audioDeviceModule(_: LKRTCAudioDeviceModule, willStartEngine engine: AVAudioEngine, isPlayoutEnabled: Bool, isRecordingEnabled: Bool) -> Int { diff --git a/Sources/LiveKit/LiveKit+DeviceHelpers.swift b/Sources/LiveKit/LiveKit+DeviceHelpers.swift index 587e3e42e..359220a1b 100644 --- a/Sources/LiveKit/LiveKit+DeviceHelpers.swift +++ b/Sources/LiveKit/LiveKit+DeviceHelpers.swift @@ -40,4 +40,40 @@ public extension LiveKitSDK { return true } + + /// Blocking version of ensureDeviceAccess that uses DispatchGroup to wait for permissions. + static func ensureDeviceAccessSync(for types: Set) -> Bool { + let group = DispatchGroup() + var result = true + + for type in types { + if ![.video, .audio].contains(type) { + logger.log("types must be .video or .audio", .error, type: LiveKitSDK.self) + } + + let status = AVCaptureDevice.authorizationStatus(for: type) + switch status { + case .notDetermined: + group.enter() + AVCaptureDevice.requestAccess(for: type) { granted in + if !granted { + result = false + } + group.leave() + } + case .restricted, .denied: + return false + case .authorized: + continue // No action needed for authorized status + @unknown default: + logger.error("Unknown AVAuthorizationStatus") + return false + } + } + + // Wait for all permission requests to complete + group.wait() + + return result + } } From f71e7f4b58de0c191c56f84e6666bb4579cd6b69 Mon Sep 17 00:00:00 2001 From: Hiroshi Horie <548776+hiroshihorie@users.noreply.github.com> Date: Sun, 4 May 2025 18:33:12 +0900 Subject: [PATCH 2/2] Update --- LiveKitClient.podspec | 2 +- Package.swift | 2 +- Package@swift-5.9.swift | 2 +- Package@swift-6.0.swift | 2 +- .../AudioDeviceModuleDelegateAdapter.swift | 22 ++++++++++++++----- .../Audio/DefaultAudioSessionObserver.swift | 4 +--- .../LiveKit/Audio/Manager/AudioManager.swift | 10 ++++++++- .../LiveKit/Track/Local/LocalAudioTrack.swift | 9 +------- 8 files changed, 32 insertions(+), 21 deletions(-) diff --git a/LiveKitClient.podspec b/LiveKitClient.podspec index a264de1d7..7eaf5a5f7 100644 --- a/LiveKitClient.podspec +++ b/LiveKitClient.podspec @@ -14,7 +14,7 @@ Pod::Spec.new do |spec| spec.source_files = "Sources/**/*" - spec.dependency("LiveKitWebRTC", "= 125.6422.29") + spec.dependency("LiveKitWebRTC", "= 125.6422.30") spec.dependency("SwiftProtobuf") spec.dependency("Logging", "= 1.5.4") spec.dependency("DequeModule", "= 1.1.4") diff --git a/Package.swift b/Package.swift index 7cee07473..b96aff09d 100644 --- a/Package.swift +++ b/Package.swift @@ -18,7 +18,7 @@ let package = Package( ], dependencies: [ // LK-Prefixed Dynamic WebRTC XCFramework - .package(url: "https://github.com/livekit/webrtc-xcframework.git", exact: "125.6422.29"), + .package(url: "https://github.com/livekit/webrtc-xcframework.git", exact: "125.6422.30"), .package(url: "https://github.com/apple/swift-protobuf.git", from: "1.26.0"), .package(url: "https://github.com/apple/swift-log.git", from: "1.5.4"), .package(url: "https://github.com/apple/swift-collections.git", from: "1.1.0"), diff --git a/Package@swift-5.9.swift b/Package@swift-5.9.swift index b19c62ea6..f22cf157d 100644 --- a/Package@swift-5.9.swift +++ b/Package@swift-5.9.swift @@ -20,7 +20,7 @@ let package = Package( ], dependencies: [ // LK-Prefixed Dynamic WebRTC XCFramework - .package(url: "https://github.com/livekit/webrtc-xcframework.git", exact: "125.6422.29"), + .package(url: "https://github.com/livekit/webrtc-xcframework.git", exact: "125.6422.30"), .package(url: "https://github.com/apple/swift-protobuf.git", from: "1.29.0"), .package(url: "https://github.com/apple/swift-log.git", from: "1.6.2"), // 1.6.x requires Swift >=5.8 .package(url: "https://github.com/apple/swift-collections.git", from: "1.1.0"), diff --git a/Package@swift-6.0.swift b/Package@swift-6.0.swift index ecc2dc827..7f7079859 100644 --- a/Package@swift-6.0.swift +++ b/Package@swift-6.0.swift @@ -20,7 +20,7 @@ let package = Package( ], dependencies: [ // LK-Prefixed Dynamic WebRTC XCFramework - .package(url: "https://github.com/livekit/webrtc-xcframework.git", exact: "125.6422.29"), + .package(url: "https://github.com/livekit/webrtc-xcframework.git", exact: "125.6422.30"), .package(url: "https://github.com/apple/swift-protobuf.git", from: "1.29.0"), .package(url: "https://github.com/apple/swift-log.git", from: "1.6.2"), // 1.6.x requires Swift >=5.8 .package(url: "https://github.com/apple/swift-collections.git", from: "1.1.0"), diff --git a/Sources/LiveKit/Audio/AudioDeviceModuleDelegateAdapter.swift b/Sources/LiveKit/Audio/AudioDeviceModuleDelegateAdapter.swift index 5255c43b9..22f3fa176 100644 --- a/Sources/LiveKit/Audio/AudioDeviceModuleDelegateAdapter.swift +++ b/Sources/LiveKit/Audio/AudioDeviceModuleDelegateAdapter.swift @@ -49,13 +49,25 @@ class AudioDeviceModuleDelegateAdapter: NSObject, LKRTCAudioDeviceModuleDelegate let entryPoint = audioManager.buildEngineObserverChain() let result = entryPoint?.engineWillEnable(engine, isPlayoutEnabled: isPlayoutEnabled, isRecordingEnabled: isRecordingEnabled) ?? 0 - if isRecordingEnabled { - // At this point mic perms / session should be configured for recording. - #if os(macOS) + // At this point mic perms / session should be configured for recording. + if result == 0, isRecordingEnabled { // This will block WebRTC's worker thread, but when instantiating AVAudioInput node it will block by showing a dialog anyways. // Attempt to acquire mic perms at this point to return an error at SDK level. - let status = LiveKitSDK.ensureDeviceAccessSync(for: [.audio]) - log("Ensure device access result: \(status)") + let isAuthorized = LiveKitSDK.ensureDeviceAccessSync(for: [.audio]) + log("AudioEngine pre-enable check, device permission: \(isAuthorized)") + if !isAuthorized { + return kAudioEngineErrorInsufficientDevicePermission + } + + #if os(iOS) || os(visionOS) || os(tvOS) + // Additional check for audio session category. + let session = LKRTCAudioSession.sharedInstance() + log("AudioEngine pre-enable check, audio session: \(session.category)") + if ![AVAudioSession.Category.playAndRecord.rawValue, + AVAudioSession.Category.record.rawValue,].contains(session.category) + { + return kAudioEngineErrorAudioSessionCategoryRecordingRequired + } #endif } diff --git a/Sources/LiveKit/Audio/DefaultAudioSessionObserver.swift b/Sources/LiveKit/Audio/DefaultAudioSessionObserver.swift index d67d10ac9..a0438f2b0 100644 --- a/Sources/LiveKit/Audio/DefaultAudioSessionObserver.swift +++ b/Sources/LiveKit/Audio/DefaultAudioSessionObserver.swift @@ -14,8 +14,6 @@ * limitations under the License. */ -let kFailedToConfigureAudioSessionErrorCode = -4100 - #if os(iOS) || os(visionOS) || os(tvOS) import AVFoundation @@ -83,7 +81,7 @@ public class DefaultAudioSessionObserver: AudioEngineObserver, Loggable, @unchec } catch { log("AudioSession failed to configure with error: \(error)", .error) // Pass error code to audio engine - return kFailedToConfigureAudioSessionErrorCode + return kAudioEngineErrorFailedToConfigureAudioSession } log("AudioSession activationCount: \(session.activationCount), webRTCSessionCount: \(session.webRTCSessionCount)") diff --git a/Sources/LiveKit/Audio/Manager/AudioManager.swift b/Sources/LiveKit/Audio/Manager/AudioManager.swift index 46ec764cd..38aee5e65 100644 --- a/Sources/LiveKit/Audio/Manager/AudioManager.swift +++ b/Sources/LiveKit/Audio/Manager/AudioManager.swift @@ -401,10 +401,18 @@ extension AudioManager { } } +// SDK side AudioEngine error codes +let kAudioEngineErrorFailedToConfigureAudioSession = -4100 + +let kAudioEngineErrorInsufficientDevicePermission = -4101 +let kAudioEngineErrorAudioSessionCategoryRecordingRequired = -4102 + extension AudioManager { func checkAdmResult(code: Int) throws { - if code == kFailedToConfigureAudioSessionErrorCode { + if code == kAudioEngineErrorFailedToConfigureAudioSession { throw LiveKitError(.audioSession, message: "Failed to configure audio session") + } else if code == kAudioEngineErrorInsufficientDevicePermission { + throw LiveKitError(.deviceAccessDenied, message: "Device permissions are not granted") } else if code != 0 { throw LiveKitError(.audioEngine, message: "Audio engine returned error code: \(code)") } diff --git a/Sources/LiveKit/Track/Local/LocalAudioTrack.swift b/Sources/LiveKit/Track/Local/LocalAudioTrack.swift index 34d5d1146..ac3ec9cc2 100644 --- a/Sources/LiveKit/Track/Local/LocalAudioTrack.swift +++ b/Sources/LiveKit/Track/Local/LocalAudioTrack.swift @@ -84,14 +84,7 @@ public class LocalAudioTrack: Track, LocalTrack, AudioTrack, @unchecked Sendable override func startCapture() async throws { // AudioDeviceModule's InitRecording() and StartRecording() automatically get called by WebRTC, but // explicitly init & start it early to detect audio engine failures (mic not accessible for some reason, etc.). - do { - try AudioManager.shared.startLocalRecording() - } catch { - // Make sure internal state is updated to stopped state. (TODO: Remove if ADM reverts state automatically) - try? AudioManager.shared.stopLocalRecording() - // Rethrow - throw error - } + try AudioManager.shared.startLocalRecording() } }