@@ -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
0 commit comments