Skip to content

Commit 21d2c4e

Browse files
authored
AudioManager adjustments (#486)
1 parent e370855 commit 21d2c4e

File tree

5 files changed

+250
-109
lines changed

5 files changed

+250
-109
lines changed

Sources/LiveKit/Participant/LocalParticipant.swift

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -97,11 +97,6 @@ public class LocalParticipant: Participant {
9797
return await _notifyDidUnpublish()
9898
}
9999

100-
// Wait for track to stop (if required)
101-
if room._state.roomOptions.stopLocalTrackOnUnpublish {
102-
try await track.stop()
103-
}
104-
105100
if let publisher = room._state.publisher, let sender = track._state.rtpSender {
106101
// Remove all simulcast senders...
107102
let simulcastSenders = track._state.read { Array($0.rtpSenderForCodec.values) }
@@ -114,6 +109,11 @@ public class LocalParticipant: Participant {
114109
try await room.publisherShouldNegotiate()
115110
}
116111

112+
// Wait for track to stop (if required)
113+
if room._state.roomOptions.stopLocalTrackOnUnpublish {
114+
try await track.stop()
115+
}
116+
117117
try await track.onUnpublish()
118118

119119
await _notifyDidUnpublish()

Sources/LiveKit/Track/AudioManager.swift

Lines changed: 104 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -67,18 +67,51 @@ public class AudioManager: Loggable {
6767
public static let shared = AudioManager()
6868
#endif
6969

70+
public typealias DeviceUpdateFunc = (_ audioManager: AudioManager) -> Void
71+
72+
#if os(iOS) || os(visionOS) || os(tvOS)
73+
7074
public typealias ConfigureAudioSessionFunc = (_ newState: State,
7175
_ oldState: State) -> Void
7276

73-
public typealias DeviceUpdateFunc = (_ audioManager: AudioManager) -> Void
74-
75-
/// Use this to provide a custom func to configure the audio session instead of ``defaultConfigureAudioSessionFunc(newState:oldState:)``.
76-
/// This method should not block and is expected to return immediately.
77+
/// Use this to provide a custom function to configure the audio session, overriding the default behavior
78+
/// provided by ``defaultConfigureAudioSessionFunc(newState:oldState:)``.
79+
///
80+
/// - Important: This method should return immediately and must not block.
81+
/// - Note: Once set, the following properties will no longer be effective:
82+
/// - ``sessionConfiguration``
83+
/// - ``isSpeakerOutputPreferred``
84+
///
85+
/// If you want to revert to default behavior, set this to `nil`.
7786
public var customConfigureAudioSessionFunc: ConfigureAudioSessionFunc? {
78-
get { _state.customConfigureFunc }
79-
set { _state.mutate { $0.customConfigureFunc = newValue } }
87+
get { state.customConfigureFunc }
88+
set { state.mutate { $0.customConfigureFunc = newValue } }
8089
}
8190

91+
/// Determines whether the device's built-in speaker or receiver is preferred for audio output.
92+
///
93+
/// - Defaults to `true`, indicating that the speaker is preferred.
94+
/// - Set to `false` if the receiver is preferred instead of the speaker.
95+
/// - Note: This property only applies when the audio output is routed to the built-in speaker or receiver.
96+
///
97+
/// This property is ignored if ``customConfigureAudioSessionFunc`` is set.
98+
public var isSpeakerOutputPreferred: Bool {
99+
get { state.isSpeakerOutputPreferred }
100+
set { state.mutate { $0.isSpeakerOutputPreferred = newValue } }
101+
}
102+
103+
/// Specifies a fixed configuration for the audio session, overriding dynamic adjustments.
104+
///
105+
/// If this property is set, it will take precedence over any dynamic configuration logic, including
106+
/// the value of ``isSpeakerOutputPreferred``.
107+
///
108+
/// This property is ignored if ``customConfigureAudioSessionFunc`` is set.
109+
public var sessionConfiguration: AudioSessionConfiguration? {
110+
get { state.sessionConfiguration }
111+
set { state.mutate { $0.sessionConfiguration = newValue } }
112+
}
113+
#endif
114+
82115
public enum TrackState {
83116
case none
84117
case localOnly
@@ -89,38 +122,37 @@ public class AudioManager: Loggable {
89122
public struct State: Equatable {
90123
// Only consider State mutated when public vars change
91124
public static func == (lhs: AudioManager.State, rhs: AudioManager.State) -> Bool {
92-
lhs.localTracksCount == rhs.localTracksCount &&
93-
lhs.remoteTracksCount == rhs.remoteTracksCount &&
94-
lhs.isSpeakerOutputPreferred == rhs.isSpeakerOutputPreferred
95-
}
125+
var isEqual = lhs.localTracksCount == rhs.localTracksCount &&
126+
lhs.remoteTracksCount == rhs.remoteTracksCount
96127

97-
// Keep this var within State so it's protected by UnfairLock
98-
var customConfigureFunc: ConfigureAudioSessionFunc?
128+
#if os(iOS) || os(visionOS) || os(tvOS)
129+
isEqual = isEqual &&
130+
lhs.isSpeakerOutputPreferred == rhs.isSpeakerOutputPreferred &&
131+
lhs.sessionConfiguration == rhs.sessionConfiguration
132+
#endif
133+
134+
return isEqual
135+
}
99136

100137
public var localTracksCount: Int = 0
101138
public var remoteTracksCount: Int = 0
102139
public var isSpeakerOutputPreferred: Bool = true
140+
#if os(iOS) || os(visionOS) || os(tvOS)
141+
// Keep this var within State so it's protected by UnfairLock
142+
public var customConfigureFunc: ConfigureAudioSessionFunc?
143+
public var sessionConfiguration: AudioSessionConfiguration?
144+
#endif
103145

104146
public var trackState: TrackState {
105-
if localTracksCount > 0, remoteTracksCount == 0 {
106-
return .localOnly
107-
} else if localTracksCount == 0, remoteTracksCount > 0 {
108-
return .remoteOnly
109-
} else if localTracksCount > 0, remoteTracksCount > 0 {
110-
return .localAndRemote
147+
switch (localTracksCount > 0, remoteTracksCount > 0) {
148+
case (true, false): return .localOnly
149+
case (false, true): return .remoteOnly
150+
case (true, true): return .localAndRemote
151+
default: return .none
111152
}
112-
113-
return .none
114153
}
115154
}
116155

117-
/// Set this to false if you prefer using the device's receiver instead of speaker. Defaults to true.
118-
/// This only works when the audio output is set to the built-in speaker / receiver.
119-
public var isSpeakerOutputPreferred: Bool {
120-
get { _state.isSpeakerOutputPreferred }
121-
set { _state.mutate { $0.isSpeakerOutputPreferred = newValue } }
122-
}
123-
124156
// MARK: - AudioProcessingModule
125157

126158
private lazy var capturePostProcessingDelegateAdapter: AudioCustomProcessingDelegateAdapter = {
@@ -185,50 +217,46 @@ public class AudioManager: Loggable {
185217

186218
// MARK: - Internal
187219

188-
var localTracksCount: Int { _state.localTracksCount }
189-
190-
var remoteTracksCount: Int { _state.remoteTracksCount }
191-
192220
enum `Type` {
193221
case local
194222
case remote
195223
}
196224

197-
// MARK: - Private
225+
let state = StateSync(State())
198226

199-
private var _state = StateSync(State())
227+
// MARK: - Private
200228

201229
// Singleton
202230
private init() {
203231
// trigger events when state mutates
204-
_state.onDidMutate = { [weak self] newState, oldState in
232+
state.onDidMutate = { [weak self] newState, oldState in
205233
guard let self else { return }
206234
// Return if state is equal.
207235
guard newState != oldState else { return }
208236

209237
self.log("\(oldState) -> \(newState)")
210-
#if os(iOS)
238+
#if os(iOS) || os(visionOS) || os(tvOS)
211239
let configureFunc = newState.customConfigureFunc ?? self.defaultConfigureAudioSessionFunc
212240
configureFunc(newState, oldState)
213241
#endif
214242
}
215243
}
216244

217245
func trackDidStart(_ type: Type) {
218-
_state.mutate { state in
246+
state.mutate { state in
219247
if type == .local { state.localTracksCount += 1 }
220248
if type == .remote { state.remoteTracksCount += 1 }
221249
}
222250
}
223251

224252
func trackDidStop(_ type: Type) {
225-
_state.mutate { state in
253+
state.mutate { state in
226254
if type == .local { state.localTracksCount = max(state.localTracksCount - 1, 0) }
227255
if type == .remote { state.remoteTracksCount = max(state.remoteTracksCount - 1, 0) }
228256
}
229257
}
230258

231-
#if os(iOS)
259+
#if os(iOS) || os(visionOS) || os(tvOS)
232260
/// The default implementation when audio session configuration is requested by the SDK.
233261
/// Configure the `RTCAudioSession` of `WebRTC` framework.
234262
///
@@ -238,76 +266,51 @@ public class AudioManager: Loggable {
238266
/// - configuration: A configured RTCAudioSessionConfiguration
239267
/// - setActive: passing true/false will call `AVAudioSession.setActive` internally
240268
public func defaultConfigureAudioSessionFunc(newState: State, oldState: State) {
241-
DispatchQueue.liveKitWebRTC.async { [weak self] in
242-
243-
guard let self else { return }
244-
245-
// prepare config
246-
let configuration = LKRTCAudioSessionConfiguration.webRTC()
247-
248-
if newState.trackState == .remoteOnly && newState.isSpeakerOutputPreferred {
249-
/* .playback */
250-
configuration.category = AVAudioSession.Category.playback.rawValue
251-
configuration.mode = AVAudioSession.Mode.spokenAudio.rawValue
252-
configuration.categoryOptions = [
253-
.mixWithOthers,
254-
]
255-
256-
} else if [.localOnly, .localAndRemote].contains(newState.trackState) ||
257-
(newState.trackState == .remoteOnly && !newState.isSpeakerOutputPreferred)
258-
{
259-
/* .playAndRecord */
260-
configuration.category = AVAudioSession.Category.playAndRecord.rawValue
261-
262-
if newState.isSpeakerOutputPreferred {
263-
// use .videoChat if speakerOutput is preferred
264-
configuration.mode = AVAudioSession.Mode.videoChat.rawValue
265-
} else {
266-
// use .voiceChat if speakerOutput is not preferred
267-
configuration.mode = AVAudioSession.Mode.voiceChat.rawValue
268-
}
269-
270-
configuration.categoryOptions = [
271-
.allowBluetooth,
272-
.allowBluetoothA2DP,
273-
.allowAirPlay,
274-
]
275-
276-
} else {
277-
/* .soloAmbient */
278-
configuration.category = AVAudioSession.Category.soloAmbient.rawValue
279-
configuration.mode = AVAudioSession.Mode.default.rawValue
280-
configuration.categoryOptions = []
269+
// Lazily computed config
270+
let computeConfiguration: (() -> AudioSessionConfiguration) = {
271+
switch newState.trackState {
272+
case .none:
273+
// Use .soloAmbient configuration
274+
return .soloAmbient
275+
case .remoteOnly where newState.isSpeakerOutputPreferred:
276+
// Use .playback configuration with spoken audio
277+
return .playback
278+
default:
279+
// Use .playAndRecord configuration
280+
return newState.isSpeakerOutputPreferred ? .playAndRecordSpeaker : .playAndRecordReceiver
281281
}
282+
}
282283

283-
var setActive: Bool?
284-
285-
if newState.trackState != .none, oldState.trackState == .none {
286-
// activate audio session when there is any local/remote audio track
287-
setActive = true
288-
} else if newState.trackState == .none, oldState.trackState != .none {
289-
// deactivate audio session when there are no more local/remote audio tracks
290-
setActive = false
291-
}
284+
let configuration = newState.sessionConfiguration ?? computeConfiguration()
292285

293-
// configure session
294-
let session = LKRTCAudioSession.sharedInstance()
295-
session.lockForConfiguration()
296-
// always unlock
297-
defer { session.unlockForConfiguration() }
286+
var setActive: Bool?
287+
if newState.trackState != .none, oldState.trackState == .none {
288+
// activate audio session when there is any local/remote audio track
289+
setActive = true
290+
} else if newState.trackState == .none, oldState.trackState != .none {
291+
// deactivate audio session when there are no more local/remote audio tracks
292+
setActive = false
293+
}
298294

299-
do {
300-
self.log("configuring audio session category: \(configuration.category), mode: \(configuration.mode), setActive: \(String(describing: setActive))")
295+
let session = LKRTCAudioSession.sharedInstance()
296+
// Check if needs setConfiguration
297+
guard configuration != session.toAudioSessionConfiguration() else {
298+
log("Skipping configure audio session, no changes")
299+
return
300+
}
301301

302-
if let setActive {
303-
try session.setConfiguration(configuration, active: setActive)
304-
} else {
305-
try session.setConfiguration(configuration)
306-
}
302+
session.lockForConfiguration()
303+
defer { session.unlockForConfiguration() }
307304

308-
} catch {
309-
self.log("Failed to configure audio session with error: \(error)", .error)
305+
do {
306+
log("Configuring audio session: \(String(describing: configuration))")
307+
if let setActive {
308+
try session.setConfiguration(configuration.toRTCType(), active: setActive)
309+
} else {
310+
try session.setConfiguration(configuration.toRTCType())
310311
}
312+
} catch {
313+
log("Failed to configure audio session with error: \(error)", .error)
311314
}
312315
}
313316
#endif

Sources/LiveKit/Track/Track.swift

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -352,14 +352,20 @@ extension Track {
352352
// LocalTrack only, already muted
353353
guard self is LocalTrack, !isMuted else { return }
354354
try await disable() // Disable track first
355-
try await stop() // Stop track
355+
// Only stop if VideoTrack
356+
if self is LocalVideoTrack {
357+
try await stop()
358+
}
356359
set(muted: true, shouldSendSignal: true)
357360
}
358361

359362
func _unmute() async throws {
360363
// LocalTrack only, already un-muted
361364
guard self is LocalTrack, isMuted else { return }
362-
try await start() // Start track first (Configure session first if local audio)
365+
// Only start if VideoTrack
366+
if self is LocalVideoTrack {
367+
try await start()
368+
}
363369
try await enable() // Enable track
364370
set(muted: false, shouldSendSignal: true)
365371
}

0 commit comments

Comments
 (0)