diff --git a/.changes/mic-permission-foreground b/.changes/mic-permission-foreground new file mode 100644 index 000000000..7f733b433 --- /dev/null +++ b/.changes/mic-permission-foreground @@ -0,0 +1 @@ +patch type="fixed" "Request microphone permission in foreground before enabling recording (#815)" diff --git a/Sources/LiveKit/LiveKit+DeviceHelpers.swift b/Sources/LiveKit/LiveKit+DeviceHelpers.swift index d9e488ca4..0c5f87f58 100644 --- a/Sources/LiveKit/LiveKit+DeviceHelpers.swift +++ b/Sources/LiveKit/LiveKit+DeviceHelpers.swift @@ -16,6 +16,10 @@ import AVFoundation +#if canImport(UIKit) && (os(iOS) || os(visionOS) || os(tvOS)) +import UIKit +#endif + public extension LiveKitSDK { /// Helper method to ensure authorization for video(camera) / audio(microphone) permissions in a single call. static func ensureDeviceAccess(for types: Set) async -> Bool { @@ -41,6 +45,19 @@ public extension LiveKitSDK { return true } + /// Requests authorization for the given media types, but only while the app is foregrounded so + /// the system permission dialog can actually appear. A no-op returning `false` on non-active or + /// non-UIKit (extension/background) contexts. Suitable for paths that must never block, where + /// prompting only makes sense when the app can present UI. + static func ensureDeviceAccessIfForegrounded(for types: Set) async -> Bool { + #if canImport(UIKit) && (os(iOS) || os(visionOS) || os(tvOS)) + guard await UIApplication.shared.applicationState == .active else { return false } + return await ensureDeviceAccess(for: types) + #else + return false + #endif + } + /// Blocking version of ensureDeviceAccess that uses DispatchGroup to wait for permissions. static func ensureDeviceAccessSync(for types: Set) -> Bool { let group = DispatchGroup() diff --git a/Sources/LiveKit/Participant/LocalParticipant.swift b/Sources/LiveKit/Participant/LocalParticipant.swift index 146fda51b..5f64a7663 100644 --- a/Sources/LiveKit/Participant/LocalParticipant.swift +++ b/Sources/LiveKit/Participant/LocalParticipant.swift @@ -16,6 +16,7 @@ // swiftlint:disable file_length +import AVFoundation import Combine import Foundation @@ -387,6 +388,10 @@ public extension LocalParticipant { reportStatistics: room._state.roomOptions.reportRemoteTrackStatistics) return try await self._publish(track: localTrack, options: publishOptions) } else if source == .microphone { + // Replace the WebRTC ADM's removed implicit prompt: request mic access while foregrounded (#815). + if AVCaptureDevice.authorizationStatus(for: .audio) == .notDetermined { + _ = await LiveKitSDK.ensureDeviceAccessIfForegrounded(for: [.audio]) + } let localTrack = LocalAudioTrack.createTrack(options: (captureOptions as? AudioCaptureOptions) ?? room._state.roomOptions.defaultAudioCaptureOptions, reportStatistics: room._state.roomOptions.reportRemoteTrackStatistics) return try await self._publish(track: localTrack, options: publishOptions)