-
Notifications
You must be signed in to change notification settings - Fork 114
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge remote-tracking branch 'upstream/main' into broadcast-ipc
- Loading branch information
Showing
26 changed files
with
1,118 additions
and
209 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
88 changes: 88 additions & 0 deletions
88
Sources/LiveKit/Audio/AudioDeviceModuleDelegateAdapter.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,88 @@ | ||
/* | ||
* Copyright 2025 LiveKit | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
|
||
import Foundation | ||
|
||
#if swift(>=5.9) | ||
internal import LiveKitWebRTC | ||
#else | ||
@_implementationOnly import LiveKitWebRTC | ||
#endif | ||
|
||
// Invoked on WebRTC's worker thread, do not block. | ||
class AudioDeviceModuleDelegateAdapter: NSObject, LKRTCAudioDeviceModuleDelegate { | ||
weak var audioManager: AudioManager? | ||
|
||
func audioDeviceModule(_: LKRTCAudioDeviceModule, didReceiveSpeechActivityEvent speechActivityEvent: RTCSpeechActivityEvent) { | ||
guard let audioManager else { return } | ||
audioManager._state.onMutedSpeechActivity?(audioManager, speechActivityEvent.toLKType()) | ||
} | ||
|
||
func audioDeviceModuleDidUpdateDevices(_: LKRTCAudioDeviceModule) { | ||
guard let audioManager else { return } | ||
audioManager._state.onDevicesDidUpdate?(audioManager) | ||
} | ||
|
||
// Engine events | ||
|
||
func audioDeviceModule(_: LKRTCAudioDeviceModule, didCreateEngine engine: AVAudioEngine) { | ||
guard let audioManager else { return } | ||
let entryPoint = audioManager._state.engineObservers.buildChain() | ||
entryPoint?.engineDidCreate(engine) | ||
} | ||
|
||
func audioDeviceModule(_: LKRTCAudioDeviceModule, willEnableEngine engine: AVAudioEngine, isPlayoutEnabled: Bool, isRecordingEnabled: Bool) { | ||
guard let audioManager else { return } | ||
let entryPoint = audioManager._state.engineObservers.buildChain() | ||
entryPoint?.engineWillEnable(engine, isPlayoutEnabled: isPlayoutEnabled, isRecordingEnabled: isRecordingEnabled) | ||
} | ||
|
||
func audioDeviceModule(_: LKRTCAudioDeviceModule, willStartEngine engine: AVAudioEngine, isPlayoutEnabled: Bool, isRecordingEnabled: Bool) { | ||
guard let audioManager else { return } | ||
let entryPoint = audioManager._state.engineObservers.buildChain() | ||
entryPoint?.engineWillStart(engine, isPlayoutEnabled: isPlayoutEnabled, isRecordingEnabled: isRecordingEnabled) | ||
} | ||
|
||
func audioDeviceModule(_: LKRTCAudioDeviceModule, didStopEngine engine: AVAudioEngine, isPlayoutEnabled: Bool, isRecordingEnabled: Bool) { | ||
guard let audioManager else { return } | ||
let entryPoint = audioManager._state.engineObservers.buildChain() | ||
entryPoint?.engineDidStop(engine, isPlayoutEnabled: isPlayoutEnabled, isRecordingEnabled: isRecordingEnabled) | ||
} | ||
|
||
func audioDeviceModule(_: LKRTCAudioDeviceModule, didDisableEngine engine: AVAudioEngine, isPlayoutEnabled: Bool, isRecordingEnabled: Bool) { | ||
guard let audioManager else { return } | ||
let entryPoint = audioManager._state.engineObservers.buildChain() | ||
entryPoint?.engineDidDisable(engine, isPlayoutEnabled: isPlayoutEnabled, isRecordingEnabled: isRecordingEnabled) | ||
} | ||
|
||
func audioDeviceModule(_: LKRTCAudioDeviceModule, willReleaseEngine engine: AVAudioEngine) { | ||
guard let audioManager else { return } | ||
let entryPoint = audioManager._state.engineObservers.buildChain() | ||
entryPoint?.engineWillRelease(engine) | ||
} | ||
|
||
func audioDeviceModule(_: LKRTCAudioDeviceModule, engine: AVAudioEngine, configureInputFromSource src: AVAudioNode?, toDestination dst: AVAudioNode, format: AVAudioFormat) -> Bool { | ||
guard let audioManager else { return false } | ||
let entryPoint = audioManager._state.engineObservers.buildChain() | ||
return entryPoint?.engineWillConnectInput(engine, src: src, dst: dst, format: format) ?? false | ||
} | ||
|
||
func audioDeviceModule(_: LKRTCAudioDeviceModule, engine: AVAudioEngine, configureOutputFromSource src: AVAudioNode, toDestination dst: AVAudioNode?, format: AVAudioFormat) -> Bool { | ||
guard let audioManager else { return false } | ||
let entryPoint = audioManager._state.engineObservers.buildChain() | ||
return entryPoint?.engineWillConnectOutput(engine, src: src, dst: dst, format: format) ?? false | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
/* | ||
* Copyright 2025 LiveKit | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
|
||
import AVFAudio | ||
|
||
/// Do not retain the engine object. | ||
public protocol AudioEngineObserver: NextInvokable, Sendable { | ||
func setNext(_ handler: any AudioEngineObserver) | ||
|
||
func engineDidCreate(_ engine: AVAudioEngine) | ||
func engineWillEnable(_ engine: AVAudioEngine, isPlayoutEnabled: Bool, isRecordingEnabled: Bool) | ||
func engineWillStart(_ engine: AVAudioEngine, isPlayoutEnabled: Bool, isRecordingEnabled: Bool) | ||
func engineDidStop(_ engine: AVAudioEngine, isPlayoutEnabled: Bool, isRecordingEnabled: Bool) | ||
func engineDidDisable(_ engine: AVAudioEngine, isPlayoutEnabled: Bool, isRecordingEnabled: Bool) | ||
func engineWillRelease(_ engine: AVAudioEngine) | ||
|
||
/// Provide custom implementation for internal AVAudioEngine's output configuration. | ||
/// Buffers flow from `src` to `dst`. Preferred format to connect node is provided as `format`. | ||
/// Return true if custom implementation is provided, otherwise default implementation will be used. | ||
func engineWillConnectOutput(_ engine: AVAudioEngine, src: AVAudioNode, dst: AVAudioNode?, format: AVAudioFormat) -> Bool | ||
/// Provide custom implementation for internal AVAudioEngine's input configuration. | ||
/// Buffers flow from `src` to `dst`. Preferred format to connect node is provided as `format`. | ||
/// Return true if custom implementation is provided, otherwise default implementation will be used. | ||
func engineWillConnectInput(_ engine: AVAudioEngine, src: AVAudioNode?, dst: AVAudioNode, format: AVAudioFormat) -> Bool | ||
} | ||
|
||
/// Default implementation to make it optional. | ||
public extension AudioEngineObserver { | ||
func engineDidCreate(_: AVAudioEngine) {} | ||
func engineWillEnable(_: AVAudioEngine, isPlayoutEnabled _: Bool, isRecordingEnabled _: Bool) {} | ||
func engineWillStart(_: AVAudioEngine, isPlayoutEnabled _: Bool, isRecordingEnabled _: Bool) {} | ||
func engineDidStop(_: AVAudioEngine, isPlayoutEnabled _: Bool, isRecordingEnabled _: Bool) {} | ||
func engineDidDisable(_: AVAudioEngine, isPlayoutEnabled _: Bool, isRecordingEnabled _: Bool) {} | ||
func engineWillRelease(_: AVAudioEngine) {} | ||
|
||
func engineWillConnectOutput(_: AVAudioEngine, src _: AVAudioNode, dst _: AVAudioNode?, format _: AVAudioFormat) -> Bool { false } | ||
func engineWillConnectInput(_: AVAudioEngine, src _: AVAudioNode?, dst _: AVAudioNode, format _: AVAudioFormat) -> Bool { false } | ||
} | ||
|
||
extension [any AudioEngineObserver] { | ||
func buildChain() -> Element? { | ||
guard let first else { return nil } | ||
|
||
for i in 0 ..< count - 1 { | ||
self[i].setNext(self[i + 1]) | ||
} | ||
|
||
return first | ||
} | ||
} |
127 changes: 127 additions & 0 deletions
127
Sources/LiveKit/Audio/DefaultAudioSessionObserver.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,127 @@ | ||
/* | ||
* Copyright 2025 LiveKit | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
|
||
#if os(iOS) || os(visionOS) || os(tvOS) | ||
|
||
import AVFoundation | ||
|
||
#if swift(>=5.9) | ||
internal import LiveKitWebRTC | ||
#else | ||
@_implementationOnly import LiveKitWebRTC | ||
#endif | ||
|
||
public final class DefaultAudioSessionObserver: AudioEngineObserver, Loggable { | ||
struct State { | ||
var isSessionActive = false | ||
var next: (any AudioEngineObserver)? | ||
|
||
// Used for backward compatibility with `customConfigureAudioSessionFunc`. | ||
var isPlayoutEnabled: Bool = false | ||
var isRecordingEnabled: Bool = false | ||
} | ||
|
||
let _state = StateSync(State()) | ||
|
||
init() { | ||
// Backward compatibility with `customConfigureAudioSessionFunc`. | ||
_state.onDidMutate = { new_, old_ in | ||
if let config_func = AudioManager.shared._state.customConfigureFunc, | ||
new_.isPlayoutEnabled != old_.isPlayoutEnabled || | ||
new_.isRecordingEnabled != old_.isRecordingEnabled | ||
{ | ||
// Simulate state and invoke custom config func. | ||
let old_state = AudioManager.State(localTracksCount: old_.isRecordingEnabled ? 1 : 0, remoteTracksCount: old_.isPlayoutEnabled ? 1 : 0) | ||
let new_state = AudioManager.State(localTracksCount: new_.isRecordingEnabled ? 1 : 0, remoteTracksCount: new_.isPlayoutEnabled ? 1 : 0) | ||
config_func(new_state, old_state) | ||
} | ||
} | ||
} | ||
|
||
public func setNext(_ nextHandler: any AudioEngineObserver) { | ||
_state.mutate { $0.next = nextHandler } | ||
} | ||
|
||
public func engineWillEnable(_ engine: AVAudioEngine, isPlayoutEnabled: Bool, isRecordingEnabled: Bool) { | ||
if AudioManager.shared._state.customConfigureFunc == nil { | ||
log("Configuring audio session...") | ||
let session = LKRTCAudioSession.sharedInstance() | ||
session.lockForConfiguration() | ||
defer { session.unlockForConfiguration() } | ||
|
||
let config: AudioSessionConfiguration = isRecordingEnabled ? .playAndRecordSpeaker : .playback | ||
do { | ||
if _state.isSessionActive { | ||
log("AudioSession deactivating due to category switch") | ||
try session.setActive(false) // Deactivate first | ||
_state.mutate { $0.isSessionActive = false } | ||
} | ||
|
||
log("AudioSession activating category to: \(config.category)") | ||
try session.setConfiguration(config.toRTCType(), active: true) | ||
_state.mutate { $0.isSessionActive = true } | ||
} catch { | ||
log("AudioSession failed to configure with error: \(error)", .error) | ||
} | ||
|
||
log("AudioSession activationCount: \(session.activationCount), webRTCSessionCount: \(session.webRTCSessionCount)") | ||
} | ||
|
||
_state.mutate { | ||
$0.isPlayoutEnabled = isPlayoutEnabled | ||
$0.isRecordingEnabled = isRecordingEnabled | ||
} | ||
|
||
// Call next last | ||
_state.next?.engineWillEnable(engine, isPlayoutEnabled: isPlayoutEnabled, isRecordingEnabled: isRecordingEnabled) | ||
} | ||
|
||
public func engineDidDisable(_ engine: AVAudioEngine, isPlayoutEnabled: Bool, isRecordingEnabled: Bool) { | ||
// Call next first | ||
_state.next?.engineDidDisable(engine, isPlayoutEnabled: isPlayoutEnabled, isRecordingEnabled: isRecordingEnabled) | ||
|
||
_state.mutate { | ||
$0.isPlayoutEnabled = isPlayoutEnabled | ||
$0.isRecordingEnabled = isRecordingEnabled | ||
} | ||
|
||
if AudioManager.shared._state.customConfigureFunc == nil { | ||
log("Configuring audio session...") | ||
let session = LKRTCAudioSession.sharedInstance() | ||
session.lockForConfiguration() | ||
defer { session.unlockForConfiguration() } | ||
|
||
do { | ||
if isPlayoutEnabled, !isRecordingEnabled { | ||
let config: AudioSessionConfiguration = .playback | ||
log("AudioSession switching category to: \(config.category)") | ||
try session.setConfiguration(config.toRTCType()) | ||
} | ||
if !isPlayoutEnabled, !isRecordingEnabled { | ||
log("AudioSession deactivating") | ||
try session.setActive(false) | ||
_state.mutate { $0.isSessionActive = false } | ||
} | ||
} catch { | ||
log("AudioSession failed to configure with error: \(error)", .error) | ||
} | ||
|
||
log("AudioSession activationCount: \(session.activationCount), webRTCSessionCount: \(session.webRTCSessionCount)") | ||
} | ||
} | ||
} | ||
|
||
#endif |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.