Skip to content

Commit f607b25

Browse files
authored
Fix AudioManager deadlock (#121)
* Update AudioManager.swift * simplify
1 parent d4cb800 commit f607b25

File tree

1 file changed

+61
-72
lines changed

1 file changed

+61
-72
lines changed

Sources/LiveKit/Track/AudioManager.swift

Lines changed: 61 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,9 @@ public class AudioManager: Loggable {
2727
public typealias ConfigureAudioSessionFunc = (_ newState: State,
2828
_ oldState: State) -> Void
2929

30-
public var customConfigureFunc: ConfigureAudioSessionFunc?
30+
/// Use this to provide a custom func to configure the audio session instead of ``defaultConfigureAudioSessionFunc(newState:oldState:)``.
31+
/// This method should not block and is expected to return immediately.
32+
public var customConfigureAudioSessionFunc: ConfigureAudioSessionFunc?
3133

3234
public enum TrackState {
3335
case none
@@ -59,7 +61,6 @@ public class AudioManager: Loggable {
5961
// MARK: - Private
6062

6163
private var _state = StateSync(State())
62-
private let configureQueue = DispatchQueue(label: "LiveKitSDK.AudioManager.configure", qos: .default)
6364

6465
#if os(iOS)
6566
private let notificationQueue = OperationQueue()
@@ -93,9 +94,7 @@ public class AudioManager: Loggable {
9394
// trigger events when state mutates
9495
_state.onMutate = { [weak self] newState, oldState in
9596
guard let self = self else { return }
96-
self.configureQueue.async {
97-
self.configureAudioSession(newState: newState, oldState: oldState)
98-
}
97+
self.configureAudioSession(newState: newState, oldState: oldState)
9998
}
10099
}
101100

@@ -124,106 +123,96 @@ public class AudioManager: Loggable {
124123
}
125124
}
126125

127-
private func configureAudioSession(newState: State,
128-
oldState: State) {
126+
private func configureAudioSession(newState: State, oldState: State) {
127+
129128
log("\(oldState) -> \(newState)")
130129

131130
#if os(iOS)
132131
if let _deprecatedFunc = LiveKit.onShouldConfigureAudioSession {
133132
_deprecatedFunc(newState.trackState, oldState.trackState)
134-
} else if let customConfigureFunc = customConfigureFunc {
135-
customConfigureFunc(newState, oldState)
133+
} else if let customConfigureAudioSessionFunc = customConfigureAudioSessionFunc {
134+
customConfigureAudioSessionFunc(newState, oldState)
136135
} else {
137-
defaultShouldConfigureAudioSessionFunc(newState: newState,
138-
oldState: oldState)
136+
defaultConfigureAudioSessionFunc(newState: newState, oldState: oldState)
139137
}
140138
#endif
141139
}
142140

143141
#if os(iOS)
142+
/// The default implementation when audio session configuration is requested by the SDK.
144143
/// Configure the `RTCAudioSession` of `WebRTC` framework.
145144
///
146145
/// > Note: It is recommended to use `RTCAudioSessionConfiguration.webRTC()` to obtain an instance of `RTCAudioSessionConfiguration` instead of instantiating directly.
147146
///
148-
/// View ``defaultShouldConfigureAudioSessionFunc(newState:oldState:)`` for usage of this method.
149-
///
150147
/// - Parameters:
151148
/// - configuration: A configured RTCAudioSessionConfiguration
152149
/// - setActive: passing true/false will call `AVAudioSession.setActive` internally
153-
public func configureAudioSession(_ configuration: RTCAudioSessionConfiguration,
154-
setActive: Bool? = nil,
155-
preferSpeakerOutput: Bool = true) {
156-
157-
let session: RTCAudioSession = DispatchQueue.webRTC.sync {
158-
let result = RTCAudioSession.sharedInstance()
159-
result.lockForConfiguration()
160-
return result
161-
}
150+
public func defaultConfigureAudioSessionFunc(newState: State, oldState: State) {
162151

163-
defer { DispatchQueue.webRTC.sync { session.unlockForConfiguration() } }
152+
DispatchQueue.webRTC.async { [weak self] in
164153

165-
do {
166-
logger.log("configuring audio session with category: \(configuration.category), mode: \(configuration.mode), setActive: \(String(describing: setActive))", type: AudioManager.self)
154+
guard let self = self else { return }
167155

168-
if let setActive = setActive {
169-
try DispatchQueue.webRTC.sync { try session.setConfiguration(configuration, active: setActive) }
170-
} else {
171-
try DispatchQueue.webRTC.sync { try session.setConfiguration(configuration) }
172-
}
156+
// prepare config
157+
let configuration = RTCAudioSessionConfiguration.webRTC()
158+
var categoryOptions: AVAudioSession.CategoryOptions = []
173159

174-
} catch let error {
175-
logger.log("Failed to configureAudioSession with error: \(error)", .error, type: AudioManager.self)
176-
}
160+
switch newState.trackState {
161+
case .remoteOnly:
162+
configuration.category = AVAudioSession.Category.playback.rawValue
163+
configuration.mode = AVAudioSession.Mode.spokenAudio.rawValue
164+
case .localOnly, .localAndRemote:
165+
configuration.category = AVAudioSession.Category.playAndRecord.rawValue
166+
configuration.mode = AVAudioSession.Mode.videoChat.rawValue
177167

178-
do {
179-
logger.log("preferSpeakerOutput: \(preferSpeakerOutput)", type: AudioManager.self)
180-
try DispatchQueue.webRTC.sync { try session.overrideOutputAudioPort(preferSpeakerOutput ? .speaker : .none) }
181-
} catch let error {
182-
logger.log("Failed to overrideOutputAudioPort with error: \(error)", .error, type: AudioManager.self)
183-
}
184-
}
168+
categoryOptions = [.allowBluetooth, .allowBluetoothA2DP]
185169

186-
/// The default implementation when audio session configuration is requested by the SDK.
187-
public func defaultShouldConfigureAudioSessionFunc(newState: State,
188-
oldState: State) {
170+
if newState.preferSpeakerOutput {
171+
categoryOptions.insert(.defaultToSpeaker)
172+
}
189173

190-
let config = DispatchQueue.webRTC.sync { RTCAudioSessionConfiguration.webRTC() }
174+
default:
175+
configuration.category = AVAudioSession.Category.soloAmbient.rawValue
176+
configuration.mode = AVAudioSession.Mode.default.rawValue
177+
}
191178

192-
var categoryOptions: AVAudioSession.CategoryOptions = []
179+
configuration.categoryOptions = categoryOptions
193180

194-
switch newState.trackState {
195-
case .remoteOnly:
196-
config.category = AVAudioSession.Category.playback.rawValue
197-
config.mode = AVAudioSession.Mode.spokenAudio.rawValue
198-
case .localOnly, .localAndRemote:
199-
config.category = AVAudioSession.Category.playAndRecord.rawValue
200-
config.mode = AVAudioSession.Mode.videoChat.rawValue
181+
var setActive: Bool?
182+
if newState.trackState != .none, oldState.trackState == .none {
183+
// activate audio session when there is any local/remote audio track
184+
setActive = true
185+
} else if newState.trackState == .none, oldState.trackState != .none {
186+
// deactivate audio session when there are no more local/remote audio tracks
187+
setActive = false
188+
}
201189

202-
categoryOptions = [.allowBluetooth, .allowBluetoothA2DP]
190+
// configure session
191+
let session = RTCAudioSession.sharedInstance()
192+
session.lockForConfiguration()
193+
// always unlock
194+
defer { session.unlockForConfiguration() }
203195

204-
if newState.preferSpeakerOutput {
205-
categoryOptions.insert(.defaultToSpeaker)
206-
}
196+
do {
197+
self.log("configuring audio session with category: \(configuration.category), mode: \(configuration.mode), setActive: \(String(describing: setActive))")
207198

208-
default:
209-
config.category = AVAudioSession.Category.soloAmbient.rawValue
210-
config.mode = AVAudioSession.Mode.default.rawValue
211-
}
199+
if let setActive = setActive {
200+
try session.setConfiguration(configuration, active: setActive)
201+
} else {
202+
try session.setConfiguration(configuration)
203+
}
212204

213-
config.categoryOptions = categoryOptions
205+
} catch let error {
206+
self.log("Failed to configureAudioSession with error: \(error)", .error)
207+
}
214208

215-
var setActive: Bool?
216-
if newState.trackState != .none, oldState.trackState == .none {
217-
// activate audio session when there is any local/remote audio track
218-
setActive = true
219-
} else if newState.trackState == .none, oldState.trackState != .none {
220-
// deactivate audio session when there are no more local/remote audio tracks
221-
setActive = false
209+
do {
210+
self.log("preferSpeakerOutput: \(newState.preferSpeakerOutput)")
211+
try session.overrideOutputAudioPort(newState.preferSpeakerOutput ? .speaker : .none)
212+
} catch let error {
213+
self.log("Failed to overrideOutputAudioPort with error: \(error)", .error)
214+
}
222215
}
223-
224-
configureAudioSession(config,
225-
setActive: setActive,
226-
preferSpeakerOutput: newState.preferSpeakerOutput)
227216
}
228217
#endif
229218
}

0 commit comments

Comments
 (0)