diff --git a/ios/AudioUtils.swift b/ios/AudioUtils.swift index e06dbe67..a84fcb2c 100644 --- a/ios/AudioUtils.swift +++ b/ios/AudioUtils.swift @@ -6,7 +6,11 @@ public class AudioUtils { case "default_": .default case "voicePrompt": - .voicePrompt + if #available(iOS 12.0, *) { + .voicePrompt + } else { + .default + } case "videoRecording": .videoRecording case "videoChat": @@ -26,7 +30,7 @@ public class AudioUtils { } return retMode } - + public static func audioSessionCategoryFromString(_ category: String) -> AVAudioSession.Category { let retCategory: AVAudioSession.Category = switch category { case "ambient": @@ -42,8 +46,39 @@ public class AudioUtils { case "multiRoute": .multiRoute default: - .ambient + .soloAmbient } return retCategory } + + public static func audioSessionCategoryOptionsFromStrings(_ options: [String]) -> AVAudioSession.CategoryOptions { + var categoryOptions: AVAudioSession.CategoryOptions = [] + for option in options { + switch option { + case "mixWithOthers": + categoryOptions.insert(.mixWithOthers) + case "duckOthers": + categoryOptions.insert(.duckOthers) + case "allowBluetooth": + categoryOptions.insert(.allowBluetooth) + case "allowBluetoothA2DP": + categoryOptions.insert(.allowBluetoothA2DP) + case "allowAirPlay": + categoryOptions.insert(.allowAirPlay) + case "defaultToSpeaker": + categoryOptions.insert(.defaultToSpeaker) + case "interruptSpokenAudioAndMixWithOthers": + if #available(iOS 13.0, *) { + categoryOptions.insert(.interruptSpokenAudioAndMixWithOthers) + } + case "overrideMutedMicrophoneInterruption": + if #available(iOS 14.5, *) { + categoryOptions.insert(.overrideMutedMicrophoneInterruption) + } + default: + break + } + } + return categoryOptions + } } diff --git a/ios/LiveKitReactNativeModule.swift b/ios/LiveKitReactNativeModule.swift index 2f116992..b3c3ef47 100644 --- a/ios/LiveKitReactNativeModule.swift +++ b/ios/LiveKitReactNativeModule.swift @@ -11,7 +11,7 @@ struct LKEvents { @objc(LivekitReactNativeModule) public class LivekitReactNativeModule: RCTEventEmitter { - + // This cannot be initialized in init as self.bridge is given afterwards. private var _audioRendererManager: AudioRendererManager? = nil public var audioRendererManager: AudioRendererManager { @@ -19,11 +19,11 @@ public class LivekitReactNativeModule: RCTEventEmitter { if _audioRendererManager == nil { _audioRendererManager = AudioRendererManager(bridge: self.bridge) } - + return _audioRendererManager! } } - + @objc public override init() { super.init() @@ -31,10 +31,10 @@ public class LivekitReactNativeModule: RCTEventEmitter { config.category = AVAudioSession.Category.playAndRecord.rawValue config.categoryOptions = [.allowAirPlay, .allowBluetooth, .allowBluetoothA2DP, .defaultToSpeaker] config.mode = AVAudioSession.Mode.videoChat.rawValue - + RTCAudioSessionConfiguration.setWebRTC(config) } - + @objc override public static func requiresMainQueueSetup() -> Bool { return false @@ -48,19 +48,19 @@ public class LivekitReactNativeModule: RCTEventEmitter { options.videoEncoderFactory = simulcastVideoEncoderFactory options.audioProcessingModule = LKAudioProcessingManager.sharedInstance().audioProcessingModule } - + @objc(configureAudio:) public func configureAudio(_ config: NSDictionary) { guard let iOSConfig = config["ios"] as? NSDictionary else { return } - + let defaultOutput = iOSConfig["defaultOutput"] as? String ?? "speaker" - + let rtcConfig = RTCAudioSessionConfiguration() rtcConfig.category = AVAudioSession.Category.playAndRecord.rawValue - + if (defaultOutput == "earpiece") { rtcConfig.categoryOptions = [.allowAirPlay, .allowBluetooth, .allowBluetoothA2DP]; rtcConfig.mode = AVAudioSession.Mode.voiceChat.rawValue @@ -70,17 +70,39 @@ public class LivekitReactNativeModule: RCTEventEmitter { } RTCAudioSessionConfiguration.setWebRTC(rtcConfig) } - - @objc(startAudioSession) - public func startAudioSession() { - // intentionally left empty + + @objc(startAudioSession:withRejecter:) + public func startAudioSession(resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) { + let session = RTCAudioSession.sharedInstance() + session.lockForConfiguration() + defer { + session.unlockForConfiguration() + } + + do { + try session.setActive(true) + resolve(nil) + } catch { + reject("startAudioSession", "Error activating audio session: \(error.localizedDescription)", error) + } } - - @objc(stopAudioSession) - public func stopAudioSession() { - // intentionally left empty + + @objc(stopAudioSession:withRejecter:) + public func stopAudioSession(resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) { + let session = RTCAudioSession.sharedInstance() + session.lockForConfiguration() + defer { + session.unlockForConfiguration() + } + + do { + try session.setActive(false) + resolve(nil) + } catch { + reject("stopAudioSession", "Error deactivating audio session: \(error.localizedDescription)", error) + } } - + @objc(showAudioRoutePicker) public func showAudioRoutePicker() { if #available(iOS 11.0, *) { @@ -95,12 +117,12 @@ public class LivekitReactNativeModule: RCTEventEmitter { } } } - + @objc(getAudioOutputsWithResolver:withRejecter:) public func getAudioOutputs(resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock){ resolve(["default", "force_speaker"]) } - + @objc(selectAudioOutput:withResolver:withRejecter:) public func selectAudioOutput(_ deviceId: String, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) { let session = AVAudioSession.sharedInstance() @@ -114,78 +136,53 @@ public class LivekitReactNativeModule: RCTEventEmitter { reject("selectAudioOutput error", error.localizedDescription, error) return } - + resolve(nil) } - - @objc(setAppleAudioConfiguration:) - public func setAppleAudioConfiguration(_ configuration: NSDictionary) { + + @objc(setAppleAudioConfiguration:withResolver:withRejecter:) + public func setAppleAudioConfiguration(_ configuration: NSDictionary, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) { let session = RTCAudioSession.sharedInstance() let config = RTCAudioSessionConfiguration.webRTC() - + let appleAudioCategory = configuration["audioCategory"] as? String let appleAudioCategoryOptions = configuration["audioCategoryOptions"] as? [String] let appleAudioMode = configuration["audioMode"] as? String - + session.lockForConfiguration() - - var categoryChanged = false - - if let appleAudioCategoryOptions = appleAudioCategoryOptions { - categoryChanged = true - - var newOptions: AVAudioSession.CategoryOptions = [] - for option in appleAudioCategoryOptions { - if option == "mixWithOthers" { - newOptions.insert(.mixWithOthers) - } else if option == "duckOthers" { - newOptions.insert(.duckOthers) - } else if option == "allowBluetooth" { - newOptions.insert(.allowBluetooth) - } else if option == "allowBluetoothA2DP" { - newOptions.insert(.allowBluetoothA2DP) - } else if option == "allowAirPlay" { - newOptions.insert(.allowAirPlay) - } else if option == "defaultToSpeaker" { - newOptions.insert(.defaultToSpeaker) - } - } - config.categoryOptions = newOptions + defer { + session.unlockForConfiguration() } - + if let appleAudioCategory = appleAudioCategory { - categoryChanged = true config.category = AudioUtils.audioSessionCategoryFromString(appleAudioCategory).rawValue } - - if categoryChanged { - do { - try session.setCategory(AVAudioSession.Category(rawValue: config.category), with: config.categoryOptions) - } catch { - NSLog("Error setting category: %@", error.localizedDescription) - } + + if let appleAudioCategoryOptions = appleAudioCategoryOptions { + config.categoryOptions = AudioUtils.audioSessionCategoryOptionsFromStrings(appleAudioCategoryOptions) } - + if let appleAudioMode = appleAudioMode { - let mode = AudioUtils.audioSessionModeFromString(appleAudioMode) - config.mode = mode.rawValue - do { - try session.setMode(mode) - } catch { - NSLog("Error setting mode: %@", error.localizedDescription) - } + config.mode = AudioUtils.audioSessionModeFromString(appleAudioMode).rawValue } - - session.unlockForConfiguration() + + do { + try session.setConfiguration(config) + resolve(nil) + } catch { + reject("setAppleAudioConfiguration", "Error setting category: \(error.localizedDescription)", error) + return + } + } - + @objc(createAudioSinkListener:trackId:) public func createAudioSinkListener(_ pcId: NSNumber, trackId: String) -> String { let renderer = AudioSinkRenderer(eventEmitter: self) let reactTag = self.audioRendererManager.registerRenderer(renderer) renderer.reactTag = reactTag self.audioRendererManager.attach(renderer: renderer, pcId: pcId, trackId: trackId) - + return reactTag } @@ -193,7 +190,7 @@ public class LivekitReactNativeModule: RCTEventEmitter { public func deleteAudioSinkListener(_ reactTag: String, pcId: NSNumber, trackId: String) -> Any? { self.audioRendererManager.detach(rendererByTag: reactTag, pcId: pcId, trackId: trackId) self.audioRendererManager.unregisterRenderer(forReactTag: reactTag) - + return nil } @@ -203,7 +200,7 @@ public class LivekitReactNativeModule: RCTEventEmitter { let reactTag = self.audioRendererManager.registerRenderer(renderer) renderer.reactTag = reactTag self.audioRendererManager.attach(renderer: renderer, pcId: pcId, trackId: trackId) - + return reactTag } @@ -211,7 +208,7 @@ public class LivekitReactNativeModule: RCTEventEmitter { public func deleteVolumeProcessor(_ reactTag: String, pcId: NSNumber, trackId: String) -> Any? { self.audioRendererManager.detach(rendererByTag: reactTag, pcId: pcId, trackId: trackId) self.audioRendererManager.unregisterRenderer(forReactTag: reactTag) - + return nil } @@ -221,7 +218,7 @@ public class LivekitReactNativeModule: RCTEventEmitter { let minFrequency = (options["minFrequency"] as? NSNumber)?.floatValue ?? 1000 let maxFrequency = (options["maxFrequency"] as? NSNumber)?.floatValue ?? 8000 let intervalMs = (options["updateInterval"] as? NSNumber)?.floatValue ?? 40 - + let renderer = MultibandVolumeAudioRenderer( bands: bands, minFrequency: minFrequency, @@ -232,18 +229,18 @@ public class LivekitReactNativeModule: RCTEventEmitter { let reactTag = self.audioRendererManager.registerRenderer(renderer) renderer.reactTag = reactTag self.audioRendererManager.attach(renderer: renderer, pcId: pcId, trackId: trackId) - + return reactTag } - + @objc(deleteMultibandVolumeProcessor:pcId:trackId:) public func deleteMultibandVolumeProcessor(_ reactTag: String, pcId: NSNumber, trackId: String) -> Any? { self.audioRendererManager.detach(rendererByTag: reactTag, pcId: pcId, trackId: trackId) self.audioRendererManager.unregisterRenderer(forReactTag: reactTag) - + return nil } - + @objc(setDefaultAudioTrackVolume:) public func setDefaultAudioTrackVolume(_ volume: NSNumber) -> Any? { let options = WebRTCModuleOptions.sharedInstance() @@ -251,7 +248,7 @@ public class LivekitReactNativeModule: RCTEventEmitter { return nil } - + override public func supportedEvents() -> [String]! { return [ LKEvents.kEventVolumeProcessed, diff --git a/ios/LivekitReactNativeModule.m b/ios/LivekitReactNativeModule.m index 27a86bec..dfe83d6c 100644 --- a/ios/LivekitReactNativeModule.m +++ b/ios/LivekitReactNativeModule.m @@ -5,8 +5,10 @@ @interface RCT_EXTERN_MODULE(LivekitReactNativeModule, RCTEventEmitter) RCT_EXTERN_METHOD(configureAudio:(NSDictionary *) config) -RCT_EXTERN_METHOD(startAudioSession) -RCT_EXTERN_METHOD(stopAudioSession) +RCT_EXTERN_METHOD(startAudioSession:(RCTPromiseResolveBlock)resolve + withRejecter:(RCTPromiseRejectBlock)reject) +RCT_EXTERN_METHOD(stopAudioSession:(RCTPromiseResolveBlock)resolve + withRejecter:(RCTPromiseRejectBlock)reject) RCT_EXTERN_METHOD(setDefaultAudioTrackVolume:(nonnull NSNumber *) volume) @@ -19,7 +21,9 @@ @interface RCT_EXTERN_MODULE(LivekitReactNativeModule, RCTEventEmitter) /// Configure audio config for WebRTC -RCT_EXTERN_METHOD(setAppleAudioConfiguration:(NSDictionary *) configuration) +RCT_EXTERN_METHOD(setAppleAudioConfiguration:(NSDictionary *)configuration + withResolver:(RCTPromiseResolveBlock)resolve + withRejecter:(RCTPromiseRejectBlock)reject) RCT_EXTERN__BLOCKING_SYNCHRONOUS_METHOD(createAudioSinkListener:(nonnull NSNumber *)pcId trackId:(nonnull NSString *)trackId) diff --git a/src/index.tsx b/src/index.tsx index 44a34f80..76cf2ffa 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -24,14 +24,34 @@ import RNKeyProvider, { type RNKeyProviderOptions } from './e2ee/RNKeyProvider'; import { setupNativeEvents } from './events/EventEmitter'; import { ReadableStream, WritableStream } from 'web-streams-polyfill'; +export interface RegisterGlobalsOptions { + /** + * Automatically configure audio session before accessing microphone. + * When enabled, sets the iOS audio category to 'playAndRecord' before getUserMedia. + * + * @default true + * @platform ios + */ + autoConfigureAudioSession?: boolean; +} + /** * Registers the required globals needed for LiveKit to work. * * Must be called before using LiveKit. + * + * @param options Optional configuration for global registration */ -export function registerGlobals() { +export function registerGlobals(options?: RegisterGlobalsOptions) { + const opts = { + autoConfigureAudioSession: true, + ...options, + }; + webrtcRegisterGlobals(); - iosCategoryEnforce(); + if (opts.autoConfigureAudioSession) { + iosCategoryEnforce(); + } livekitRegisterGlobals(); setupURLPolyfill(); fixWebrtcAdapter();