Skip to content

Commit

Permalink
AudioManager adjustments (#486)
Browse files Browse the repository at this point in the history
  • Loading branch information
hiroshihorie authored Sep 18, 2024
1 parent e370855 commit 21d2c4e
Show file tree
Hide file tree
Showing 5 changed files with 250 additions and 109 deletions.
10 changes: 5 additions & 5 deletions Sources/LiveKit/Participant/LocalParticipant.swift
Original file line number Diff line number Diff line change
Expand Up @@ -97,11 +97,6 @@ public class LocalParticipant: Participant {
return await _notifyDidUnpublish()
}

// Wait for track to stop (if required)
if room._state.roomOptions.stopLocalTrackOnUnpublish {
try await track.stop()
}

if let publisher = room._state.publisher, let sender = track._state.rtpSender {
// Remove all simulcast senders...
let simulcastSenders = track._state.read { Array($0.rtpSenderForCodec.values) }
Expand All @@ -114,6 +109,11 @@ public class LocalParticipant: Participant {
try await room.publisherShouldNegotiate()
}

// Wait for track to stop (if required)
if room._state.roomOptions.stopLocalTrackOnUnpublish {
try await track.stop()
}

try await track.onUnpublish()

await _notifyDidUnpublish()
Expand Down
205 changes: 104 additions & 101 deletions Sources/LiveKit/Track/AudioManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -67,18 +67,51 @@ public class AudioManager: Loggable {
public static let shared = AudioManager()
#endif

public typealias DeviceUpdateFunc = (_ audioManager: AudioManager) -> Void

#if os(iOS) || os(visionOS) || os(tvOS)

public typealias ConfigureAudioSessionFunc = (_ newState: State,
_ oldState: State) -> Void

public typealias DeviceUpdateFunc = (_ audioManager: AudioManager) -> Void

/// Use this to provide a custom func to configure the audio session instead of ``defaultConfigureAudioSessionFunc(newState:oldState:)``.
/// This method should not block and is expected to return immediately.
/// Use this to provide a custom function to configure the audio session, overriding the default behavior
/// provided by ``defaultConfigureAudioSessionFunc(newState:oldState:)``.
///
/// - Important: This method should return immediately and must not block.
/// - Note: Once set, the following properties will no longer be effective:
/// - ``sessionConfiguration``
/// - ``isSpeakerOutputPreferred``
///
/// If you want to revert to default behavior, set this to `nil`.
public var customConfigureAudioSessionFunc: ConfigureAudioSessionFunc? {
get { _state.customConfigureFunc }
set { _state.mutate { $0.customConfigureFunc = newValue } }
get { state.customConfigureFunc }
set { state.mutate { $0.customConfigureFunc = newValue } }
}

/// Determines whether the device's built-in speaker or receiver is preferred for audio output.
///
/// - Defaults to `true`, indicating that the speaker is preferred.
/// - Set to `false` if the receiver is preferred instead of the speaker.
/// - Note: This property only applies when the audio output is routed to the built-in speaker or receiver.
///
/// This property is ignored if ``customConfigureAudioSessionFunc`` is set.
public var isSpeakerOutputPreferred: Bool {
get { state.isSpeakerOutputPreferred }
set { state.mutate { $0.isSpeakerOutputPreferred = newValue } }
}

/// Specifies a fixed configuration for the audio session, overriding dynamic adjustments.
///
/// If this property is set, it will take precedence over any dynamic configuration logic, including
/// the value of ``isSpeakerOutputPreferred``.
///
/// This property is ignored if ``customConfigureAudioSessionFunc`` is set.
public var sessionConfiguration: AudioSessionConfiguration? {
get { state.sessionConfiguration }
set { state.mutate { $0.sessionConfiguration = newValue } }
}
#endif

public enum TrackState {
case none
case localOnly
Expand All @@ -89,38 +122,37 @@ public class AudioManager: Loggable {
public struct State: Equatable {
// Only consider State mutated when public vars change
public static func == (lhs: AudioManager.State, rhs: AudioManager.State) -> Bool {
lhs.localTracksCount == rhs.localTracksCount &&
lhs.remoteTracksCount == rhs.remoteTracksCount &&
lhs.isSpeakerOutputPreferred == rhs.isSpeakerOutputPreferred
}
var isEqual = lhs.localTracksCount == rhs.localTracksCount &&
lhs.remoteTracksCount == rhs.remoteTracksCount

// Keep this var within State so it's protected by UnfairLock
var customConfigureFunc: ConfigureAudioSessionFunc?
#if os(iOS) || os(visionOS) || os(tvOS)
isEqual = isEqual &&
lhs.isSpeakerOutputPreferred == rhs.isSpeakerOutputPreferred &&
lhs.sessionConfiguration == rhs.sessionConfiguration
#endif

return isEqual
}

public var localTracksCount: Int = 0
public var remoteTracksCount: Int = 0
public var isSpeakerOutputPreferred: Bool = true
#if os(iOS) || os(visionOS) || os(tvOS)
// Keep this var within State so it's protected by UnfairLock
public var customConfigureFunc: ConfigureAudioSessionFunc?
public var sessionConfiguration: AudioSessionConfiguration?
#endif

public var trackState: TrackState {
if localTracksCount > 0, remoteTracksCount == 0 {
return .localOnly
} else if localTracksCount == 0, remoteTracksCount > 0 {
return .remoteOnly
} else if localTracksCount > 0, remoteTracksCount > 0 {
return .localAndRemote
switch (localTracksCount > 0, remoteTracksCount > 0) {
case (true, false): return .localOnly
case (false, true): return .remoteOnly
case (true, true): return .localAndRemote
default: return .none
}

return .none
}
}

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

// MARK: - AudioProcessingModule

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

// MARK: - Internal

var localTracksCount: Int { _state.localTracksCount }

var remoteTracksCount: Int { _state.remoteTracksCount }

enum `Type` {
case local
case remote
}

// MARK: - Private
let state = StateSync(State())

private var _state = StateSync(State())
// MARK: - Private

// Singleton
private init() {
// trigger events when state mutates
_state.onDidMutate = { [weak self] newState, oldState in
state.onDidMutate = { [weak self] newState, oldState in
guard let self else { return }
// Return if state is equal.
guard newState != oldState else { return }

self.log("\(oldState) -> \(newState)")
#if os(iOS)
#if os(iOS) || os(visionOS) || os(tvOS)
let configureFunc = newState.customConfigureFunc ?? self.defaultConfigureAudioSessionFunc
configureFunc(newState, oldState)
#endif
}
}

func trackDidStart(_ type: Type) {
_state.mutate { state in
state.mutate { state in
if type == .local { state.localTracksCount += 1 }
if type == .remote { state.remoteTracksCount += 1 }
}
}

func trackDidStop(_ type: Type) {
_state.mutate { state in
state.mutate { state in
if type == .local { state.localTracksCount = max(state.localTracksCount - 1, 0) }
if type == .remote { state.remoteTracksCount = max(state.remoteTracksCount - 1, 0) }
}
}

#if os(iOS)
#if os(iOS) || os(visionOS) || os(tvOS)
/// The default implementation when audio session configuration is requested by the SDK.
/// Configure the `RTCAudioSession` of `WebRTC` framework.
///
Expand All @@ -238,76 +266,51 @@ public class AudioManager: Loggable {
/// - configuration: A configured RTCAudioSessionConfiguration
/// - setActive: passing true/false will call `AVAudioSession.setActive` internally
public func defaultConfigureAudioSessionFunc(newState: State, oldState: State) {
DispatchQueue.liveKitWebRTC.async { [weak self] in

guard let self else { return }

// prepare config
let configuration = LKRTCAudioSessionConfiguration.webRTC()

if newState.trackState == .remoteOnly && newState.isSpeakerOutputPreferred {
/* .playback */
configuration.category = AVAudioSession.Category.playback.rawValue
configuration.mode = AVAudioSession.Mode.spokenAudio.rawValue
configuration.categoryOptions = [
.mixWithOthers,
]

} else if [.localOnly, .localAndRemote].contains(newState.trackState) ||
(newState.trackState == .remoteOnly && !newState.isSpeakerOutputPreferred)
{
/* .playAndRecord */
configuration.category = AVAudioSession.Category.playAndRecord.rawValue

if newState.isSpeakerOutputPreferred {
// use .videoChat if speakerOutput is preferred
configuration.mode = AVAudioSession.Mode.videoChat.rawValue
} else {
// use .voiceChat if speakerOutput is not preferred
configuration.mode = AVAudioSession.Mode.voiceChat.rawValue
}

configuration.categoryOptions = [
.allowBluetooth,
.allowBluetoothA2DP,
.allowAirPlay,
]

} else {
/* .soloAmbient */
configuration.category = AVAudioSession.Category.soloAmbient.rawValue
configuration.mode = AVAudioSession.Mode.default.rawValue
configuration.categoryOptions = []
// Lazily computed config
let computeConfiguration: (() -> AudioSessionConfiguration) = {
switch newState.trackState {
case .none:
// Use .soloAmbient configuration
return .soloAmbient
case .remoteOnly where newState.isSpeakerOutputPreferred:
// Use .playback configuration with spoken audio
return .playback
default:
// Use .playAndRecord configuration
return newState.isSpeakerOutputPreferred ? .playAndRecordSpeaker : .playAndRecordReceiver
}
}

var setActive: Bool?

if newState.trackState != .none, oldState.trackState == .none {
// activate audio session when there is any local/remote audio track
setActive = true
} else if newState.trackState == .none, oldState.trackState != .none {
// deactivate audio session when there are no more local/remote audio tracks
setActive = false
}
let configuration = newState.sessionConfiguration ?? computeConfiguration()

// configure session
let session = LKRTCAudioSession.sharedInstance()
session.lockForConfiguration()
// always unlock
defer { session.unlockForConfiguration() }
var setActive: Bool?
if newState.trackState != .none, oldState.trackState == .none {
// activate audio session when there is any local/remote audio track
setActive = true
} else if newState.trackState == .none, oldState.trackState != .none {
// deactivate audio session when there are no more local/remote audio tracks
setActive = false
}

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

if let setActive {
try session.setConfiguration(configuration, active: setActive)
} else {
try session.setConfiguration(configuration)
}
session.lockForConfiguration()
defer { session.unlockForConfiguration() }

} catch {
self.log("Failed to configure audio session with error: \(error)", .error)
do {
log("Configuring audio session: \(String(describing: configuration))")
if let setActive {
try session.setConfiguration(configuration.toRTCType(), active: setActive)
} else {
try session.setConfiguration(configuration.toRTCType())
}
} catch {
log("Failed to configure audio session with error: \(error)", .error)
}
}
#endif
Expand Down
10 changes: 8 additions & 2 deletions Sources/LiveKit/Track/Track.swift
Original file line number Diff line number Diff line change
Expand Up @@ -352,14 +352,20 @@ extension Track {
// LocalTrack only, already muted
guard self is LocalTrack, !isMuted else { return }
try await disable() // Disable track first
try await stop() // Stop track
// Only stop if VideoTrack
if self is LocalVideoTrack {
try await stop()
}
set(muted: true, shouldSendSignal: true)
}

func _unmute() async throws {
// LocalTrack only, already un-muted
guard self is LocalTrack, isMuted else { return }
try await start() // Start track first (Configure session first if local audio)
// Only start if VideoTrack
if self is LocalVideoTrack {
try await start()
}
try await enable() // Enable track
set(muted: false, shouldSendSignal: true)
}
Expand Down
Loading

0 comments on commit 21d2c4e

Please sign in to comment.