Skip to content

Commit 4e0ad99

Browse files
authored
iOS Audio configuration improvements (#290)
1 parent 1e39e0f commit 4e0ad99

File tree

4 files changed

+143
-87
lines changed

4 files changed

+143
-87
lines changed

ios/AudioUtils.swift

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,11 @@ public class AudioUtils {
66
case "default_":
77
.default
88
case "voicePrompt":
9-
.voicePrompt
9+
if #available(iOS 12.0, *) {
10+
.voicePrompt
11+
} else {
12+
.default
13+
}
1014
case "videoRecording":
1115
.videoRecording
1216
case "videoChat":
@@ -26,7 +30,7 @@ public class AudioUtils {
2630
}
2731
return retMode
2832
}
29-
33+
3034
public static func audioSessionCategoryFromString(_ category: String) -> AVAudioSession.Category {
3135
let retCategory: AVAudioSession.Category = switch category {
3236
case "ambient":
@@ -42,8 +46,39 @@ public class AudioUtils {
4246
case "multiRoute":
4347
.multiRoute
4448
default:
45-
.ambient
49+
.soloAmbient
4650
}
4751
return retCategory
4852
}
53+
54+
public static func audioSessionCategoryOptionsFromStrings(_ options: [String]) -> AVAudioSession.CategoryOptions {
55+
var categoryOptions: AVAudioSession.CategoryOptions = []
56+
for option in options {
57+
switch option {
58+
case "mixWithOthers":
59+
categoryOptions.insert(.mixWithOthers)
60+
case "duckOthers":
61+
categoryOptions.insert(.duckOthers)
62+
case "allowBluetooth":
63+
categoryOptions.insert(.allowBluetooth)
64+
case "allowBluetoothA2DP":
65+
categoryOptions.insert(.allowBluetoothA2DP)
66+
case "allowAirPlay":
67+
categoryOptions.insert(.allowAirPlay)
68+
case "defaultToSpeaker":
69+
categoryOptions.insert(.defaultToSpeaker)
70+
case "interruptSpokenAudioAndMixWithOthers":
71+
if #available(iOS 13.0, *) {
72+
categoryOptions.insert(.interruptSpokenAudioAndMixWithOthers)
73+
}
74+
case "overrideMutedMicrophoneInterruption":
75+
if #available(iOS 14.5, *) {
76+
categoryOptions.insert(.overrideMutedMicrophoneInterruption)
77+
}
78+
default:
79+
break
80+
}
81+
}
82+
return categoryOptions
83+
}
4984
}

ios/LiveKitReactNativeModule.swift

Lines changed: 76 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -11,30 +11,30 @@ struct LKEvents {
1111

1212
@objc(LivekitReactNativeModule)
1313
public class LivekitReactNativeModule: RCTEventEmitter {
14-
14+
1515
// This cannot be initialized in init as self.bridge is given afterwards.
1616
private var _audioRendererManager: AudioRendererManager? = nil
1717
public var audioRendererManager: AudioRendererManager {
1818
get {
1919
if _audioRendererManager == nil {
2020
_audioRendererManager = AudioRendererManager(bridge: self.bridge)
2121
}
22-
22+
2323
return _audioRendererManager!
2424
}
2525
}
26-
26+
2727
@objc
2828
public override init() {
2929
super.init()
3030
let config = RTCAudioSessionConfiguration()
3131
config.category = AVAudioSession.Category.playAndRecord.rawValue
3232
config.categoryOptions = [.allowAirPlay, .allowBluetooth, .allowBluetoothA2DP, .defaultToSpeaker]
3333
config.mode = AVAudioSession.Mode.videoChat.rawValue
34-
34+
3535
RTCAudioSessionConfiguration.setWebRTC(config)
3636
}
37-
37+
3838
@objc
3939
override public static func requiresMainQueueSetup() -> Bool {
4040
return false
@@ -48,19 +48,19 @@ public class LivekitReactNativeModule: RCTEventEmitter {
4848
options.videoEncoderFactory = simulcastVideoEncoderFactory
4949
options.audioProcessingModule = LKAudioProcessingManager.sharedInstance().audioProcessingModule
5050
}
51-
51+
5252
@objc(configureAudio:)
5353
public func configureAudio(_ config: NSDictionary) {
5454
guard let iOSConfig = config["ios"] as? NSDictionary
5555
else {
5656
return
5757
}
58-
58+
5959
let defaultOutput = iOSConfig["defaultOutput"] as? String ?? "speaker"
60-
60+
6161
let rtcConfig = RTCAudioSessionConfiguration()
6262
rtcConfig.category = AVAudioSession.Category.playAndRecord.rawValue
63-
63+
6464
if (defaultOutput == "earpiece") {
6565
rtcConfig.categoryOptions = [.allowAirPlay, .allowBluetooth, .allowBluetoothA2DP];
6666
rtcConfig.mode = AVAudioSession.Mode.voiceChat.rawValue
@@ -70,17 +70,39 @@ public class LivekitReactNativeModule: RCTEventEmitter {
7070
}
7171
RTCAudioSessionConfiguration.setWebRTC(rtcConfig)
7272
}
73-
74-
@objc(startAudioSession)
75-
public func startAudioSession() {
76-
// intentionally left empty
73+
74+
@objc(startAudioSession:withRejecter:)
75+
public func startAudioSession(resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) {
76+
let session = RTCAudioSession.sharedInstance()
77+
session.lockForConfiguration()
78+
defer {
79+
session.unlockForConfiguration()
80+
}
81+
82+
do {
83+
try session.setActive(true)
84+
resolve(nil)
85+
} catch {
86+
reject("startAudioSession", "Error activating audio session: \(error.localizedDescription)", error)
87+
}
7788
}
78-
79-
@objc(stopAudioSession)
80-
public func stopAudioSession() {
81-
// intentionally left empty
89+
90+
@objc(stopAudioSession:withRejecter:)
91+
public func stopAudioSession(resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) {
92+
let session = RTCAudioSession.sharedInstance()
93+
session.lockForConfiguration()
94+
defer {
95+
session.unlockForConfiguration()
96+
}
97+
98+
do {
99+
try session.setActive(false)
100+
resolve(nil)
101+
} catch {
102+
reject("stopAudioSession", "Error deactivating audio session: \(error.localizedDescription)", error)
103+
}
82104
}
83-
105+
84106
@objc(showAudioRoutePicker)
85107
public func showAudioRoutePicker() {
86108
if #available(iOS 11.0, *) {
@@ -95,12 +117,12 @@ public class LivekitReactNativeModule: RCTEventEmitter {
95117
}
96118
}
97119
}
98-
120+
99121
@objc(getAudioOutputsWithResolver:withRejecter:)
100122
public func getAudioOutputs(resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock){
101123
resolve(["default", "force_speaker"])
102124
}
103-
125+
104126
@objc(selectAudioOutput:withResolver:withRejecter:)
105127
public func selectAudioOutput(_ deviceId: String, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) {
106128
let session = AVAudioSession.sharedInstance()
@@ -114,86 +136,61 @@ public class LivekitReactNativeModule: RCTEventEmitter {
114136
reject("selectAudioOutput error", error.localizedDescription, error)
115137
return
116138
}
117-
139+
118140
resolve(nil)
119141
}
120-
121-
@objc(setAppleAudioConfiguration:)
122-
public func setAppleAudioConfiguration(_ configuration: NSDictionary) {
142+
143+
@objc(setAppleAudioConfiguration:withResolver:withRejecter:)
144+
public func setAppleAudioConfiguration(_ configuration: NSDictionary, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) {
123145
let session = RTCAudioSession.sharedInstance()
124146
let config = RTCAudioSessionConfiguration.webRTC()
125-
147+
126148
let appleAudioCategory = configuration["audioCategory"] as? String
127149
let appleAudioCategoryOptions = configuration["audioCategoryOptions"] as? [String]
128150
let appleAudioMode = configuration["audioMode"] as? String
129-
151+
130152
session.lockForConfiguration()
131-
132-
var categoryChanged = false
133-
134-
if let appleAudioCategoryOptions = appleAudioCategoryOptions {
135-
categoryChanged = true
136-
137-
var newOptions: AVAudioSession.CategoryOptions = []
138-
for option in appleAudioCategoryOptions {
139-
if option == "mixWithOthers" {
140-
newOptions.insert(.mixWithOthers)
141-
} else if option == "duckOthers" {
142-
newOptions.insert(.duckOthers)
143-
} else if option == "allowBluetooth" {
144-
newOptions.insert(.allowBluetooth)
145-
} else if option == "allowBluetoothA2DP" {
146-
newOptions.insert(.allowBluetoothA2DP)
147-
} else if option == "allowAirPlay" {
148-
newOptions.insert(.allowAirPlay)
149-
} else if option == "defaultToSpeaker" {
150-
newOptions.insert(.defaultToSpeaker)
151-
}
152-
}
153-
config.categoryOptions = newOptions
153+
defer {
154+
session.unlockForConfiguration()
154155
}
155-
156+
156157
if let appleAudioCategory = appleAudioCategory {
157-
categoryChanged = true
158158
config.category = AudioUtils.audioSessionCategoryFromString(appleAudioCategory).rawValue
159159
}
160-
161-
if categoryChanged {
162-
do {
163-
try session.setCategory(AVAudioSession.Category(rawValue: config.category), with: config.categoryOptions)
164-
} catch {
165-
NSLog("Error setting category: %@", error.localizedDescription)
166-
}
160+
161+
if let appleAudioCategoryOptions = appleAudioCategoryOptions {
162+
config.categoryOptions = AudioUtils.audioSessionCategoryOptionsFromStrings(appleAudioCategoryOptions)
167163
}
168-
164+
169165
if let appleAudioMode = appleAudioMode {
170-
let mode = AudioUtils.audioSessionModeFromString(appleAudioMode)
171-
config.mode = mode.rawValue
172-
do {
173-
try session.setMode(mode)
174-
} catch {
175-
NSLog("Error setting mode: %@", error.localizedDescription)
176-
}
166+
config.mode = AudioUtils.audioSessionModeFromString(appleAudioMode).rawValue
177167
}
178-
179-
session.unlockForConfiguration()
168+
169+
do {
170+
try session.setConfiguration(config)
171+
resolve(nil)
172+
} catch {
173+
reject("setAppleAudioConfiguration", "Error setting category: \(error.localizedDescription)", error)
174+
return
175+
}
176+
180177
}
181-
178+
182179
@objc(createAudioSinkListener:trackId:)
183180
public func createAudioSinkListener(_ pcId: NSNumber, trackId: String) -> String {
184181
let renderer = AudioSinkRenderer(eventEmitter: self)
185182
let reactTag = self.audioRendererManager.registerRenderer(renderer)
186183
renderer.reactTag = reactTag
187184
self.audioRendererManager.attach(renderer: renderer, pcId: pcId, trackId: trackId)
188-
185+
189186
return reactTag
190187
}
191188

192189
@objc(deleteAudioSinkListener:pcId:trackId:)
193190
public func deleteAudioSinkListener(_ reactTag: String, pcId: NSNumber, trackId: String) -> Any? {
194191
self.audioRendererManager.detach(rendererByTag: reactTag, pcId: pcId, trackId: trackId)
195192
self.audioRendererManager.unregisterRenderer(forReactTag: reactTag)
196-
193+
197194
return nil
198195
}
199196

@@ -203,15 +200,15 @@ public class LivekitReactNativeModule: RCTEventEmitter {
203200
let reactTag = self.audioRendererManager.registerRenderer(renderer)
204201
renderer.reactTag = reactTag
205202
self.audioRendererManager.attach(renderer: renderer, pcId: pcId, trackId: trackId)
206-
203+
207204
return reactTag
208205
}
209206

210207
@objc(deleteVolumeProcessor:pcId:trackId:)
211208
public func deleteVolumeProcessor(_ reactTag: String, pcId: NSNumber, trackId: String) -> Any? {
212209
self.audioRendererManager.detach(rendererByTag: reactTag, pcId: pcId, trackId: trackId)
213210
self.audioRendererManager.unregisterRenderer(forReactTag: reactTag)
214-
211+
215212
return nil
216213
}
217214

@@ -221,7 +218,7 @@ public class LivekitReactNativeModule: RCTEventEmitter {
221218
let minFrequency = (options["minFrequency"] as? NSNumber)?.floatValue ?? 1000
222219
let maxFrequency = (options["maxFrequency"] as? NSNumber)?.floatValue ?? 8000
223220
let intervalMs = (options["updateInterval"] as? NSNumber)?.floatValue ?? 40
224-
221+
225222
let renderer = MultibandVolumeAudioRenderer(
226223
bands: bands,
227224
minFrequency: minFrequency,
@@ -232,26 +229,26 @@ public class LivekitReactNativeModule: RCTEventEmitter {
232229
let reactTag = self.audioRendererManager.registerRenderer(renderer)
233230
renderer.reactTag = reactTag
234231
self.audioRendererManager.attach(renderer: renderer, pcId: pcId, trackId: trackId)
235-
232+
236233
return reactTag
237234
}
238-
235+
239236
@objc(deleteMultibandVolumeProcessor:pcId:trackId:)
240237
public func deleteMultibandVolumeProcessor(_ reactTag: String, pcId: NSNumber, trackId: String) -> Any? {
241238
self.audioRendererManager.detach(rendererByTag: reactTag, pcId: pcId, trackId: trackId)
242239
self.audioRendererManager.unregisterRenderer(forReactTag: reactTag)
243-
240+
244241
return nil
245242
}
246-
243+
247244
@objc(setDefaultAudioTrackVolume:)
248245
public func setDefaultAudioTrackVolume(_ volume: NSNumber) -> Any? {
249246
let options = WebRTCModuleOptions.sharedInstance()
250247
options.defaultTrackVolume = volume.doubleValue
251248

252249
return nil
253250
}
254-
251+
255252
override public func supportedEvents() -> [String]! {
256253
return [
257254
LKEvents.kEventVolumeProcessed,

ios/LivekitReactNativeModule.m

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@
55
@interface RCT_EXTERN_MODULE(LivekitReactNativeModule, RCTEventEmitter)
66

77
RCT_EXTERN_METHOD(configureAudio:(NSDictionary *) config)
8-
RCT_EXTERN_METHOD(startAudioSession)
9-
RCT_EXTERN_METHOD(stopAudioSession)
8+
RCT_EXTERN_METHOD(startAudioSession:(RCTPromiseResolveBlock)resolve
9+
withRejecter:(RCTPromiseRejectBlock)reject)
10+
RCT_EXTERN_METHOD(stopAudioSession:(RCTPromiseResolveBlock)resolve
11+
withRejecter:(RCTPromiseRejectBlock)reject)
1012

1113
RCT_EXTERN_METHOD(setDefaultAudioTrackVolume:(nonnull NSNumber *) volume)
1214

@@ -19,7 +21,9 @@ @interface RCT_EXTERN_MODULE(LivekitReactNativeModule, RCTEventEmitter)
1921

2022

2123
/// Configure audio config for WebRTC
22-
RCT_EXTERN_METHOD(setAppleAudioConfiguration:(NSDictionary *) configuration)
24+
RCT_EXTERN_METHOD(setAppleAudioConfiguration:(NSDictionary *)configuration
25+
withResolver:(RCTPromiseResolveBlock)resolve
26+
withRejecter:(RCTPromiseRejectBlock)reject)
2327

2428
RCT_EXTERN__BLOCKING_SYNCHRONOUS_METHOD(createAudioSinkListener:(nonnull NSNumber *)pcId
2529
trackId:(nonnull NSString *)trackId)

0 commit comments

Comments
 (0)