Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 13 additions & 21 deletions Sources/LiveKit/Audio/Manager/AudioManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -101,10 +101,10 @@
// Keep this var within State so it's protected by UnfairLock
public var localTracksCount: Int = 0
public var remoteTracksCount: Int = 0
public var customConfigureFunc: ConfigureAudioSessionFunc?

Check warning on line 104 in Sources/LiveKit/Audio/Manager/AudioManager.swift

View workflow job for this annotation

GitHub Actions / Build & Test (macos-14, 15.4, tvOS Simulator,name=Apple TV,OS=17.5)

'ConfigureAudioSessionFunc' is deprecated

Check warning on line 104 in Sources/LiveKit/Audio/Manager/AudioManager.swift

View workflow job for this annotation

GitHub Actions / Build & Test (macos-14, 15.4, tvOS Simulator,name=Apple TV,OS=17.5)

'ConfigureAudioSessionFunc' is deprecated

Check warning on line 104 in Sources/LiveKit/Audio/Manager/AudioManager.swift

View workflow job for this annotation

GitHub Actions / Build & Test (macos-15, 16.4, tvOS Simulator,name=Apple TV,OS=18.5)

'ConfigureAudioSessionFunc' is deprecated

Check warning on line 104 in Sources/LiveKit/Audio/Manager/AudioManager.swift

View workflow job for this annotation

GitHub Actions / Build & Test (macos-26, latest, macOS,variant=Mac Catalyst)

'ConfigureAudioSessionFunc' is deprecated

Check warning on line 104 in Sources/LiveKit/Audio/Manager/AudioManager.swift

View workflow job for this annotation

GitHub Actions / Build & Test (macos-15, 16.4, macOS,variant=Mac Catalyst)

'ConfigureAudioSessionFunc' is deprecated

Check warning on line 104 in Sources/LiveKit/Audio/Manager/AudioManager.swift

View workflow job for this annotation

GitHub Actions / Build & Test (macos-14, 15.4, macOS,variant=Mac Catalyst)

'ConfigureAudioSessionFunc' is deprecated

Check warning on line 104 in Sources/LiveKit/Audio/Manager/AudioManager.swift

View workflow job for this annotation

GitHub Actions / Build & Test (macos-14, 15.4, macOS,variant=Mac Catalyst)

'ConfigureAudioSessionFunc' is deprecated

Check warning on line 104 in Sources/LiveKit/Audio/Manager/AudioManager.swift

View workflow job for this annotation

GitHub Actions / Build & Test (macos-26, latest, tvOS Simulator,name=Apple TV,OS=26.0)

'ConfigureAudioSessionFunc' is deprecated

Check warning on line 104 in Sources/LiveKit/Audio/Manager/AudioManager.swift

View workflow job for this annotation

GitHub Actions / Build & Test (macos-26, latest, tvOS Simulator,name=Apple TV,OS=26.0)

'ConfigureAudioSessionFunc' is deprecated

Check warning on line 104 in Sources/LiveKit/Audio/Manager/AudioManager.swift

View workflow job for this annotation

GitHub Actions / Build & Test (macos-26, latest, iOS Simulator,name=iPhone 17 Pro,OS=26.0, true)

'ConfigureAudioSessionFunc' is deprecated

Check warning on line 104 in Sources/LiveKit/Audio/Manager/AudioManager.swift

View workflow job for this annotation

GitHub Actions / Build & Test (macos-26, latest, iOS Simulator,name=iPhone 17 Pro,OS=26.0, true)

'ConfigureAudioSessionFunc' is deprecated

Check warning on line 104 in Sources/LiveKit/Audio/Manager/AudioManager.swift

View workflow job for this annotation

GitHub Actions / Build & Test (macos-26, latest, iOS Simulator,name=iPhone 17 Pro,OS=26.0, true)

'ConfigureAudioSessionFunc' is deprecated

Check warning on line 104 in Sources/LiveKit/Audio/Manager/AudioManager.swift

View workflow job for this annotation

GitHub Actions / Build & Test (macos-15, 16.4, iOS Simulator,name=iPhone 16 Pro,OS=18.5)

'ConfigureAudioSessionFunc' is deprecated

Check warning on line 104 in Sources/LiveKit/Audio/Manager/AudioManager.swift

View workflow job for this annotation

GitHub Actions / Build & Test (macos-15, 16.4, iOS Simulator,name=iPhone 16 Pro,OS=18.5)

'ConfigureAudioSessionFunc' is deprecated

Check warning on line 104 in Sources/LiveKit/Audio/Manager/AudioManager.swift

View workflow job for this annotation

GitHub Actions / Build & Test (macos-14, 15.4, iOS Simulator,name=iPhone 15 Pro,OS=17.5)

'ConfigureAudioSessionFunc' is deprecated

Check warning on line 104 in Sources/LiveKit/Audio/Manager/AudioManager.swift

View workflow job for this annotation

GitHub Actions / Build & Test (macos-14, 15.4, iOS Simulator,name=iPhone 15 Pro,OS=17.5)

'ConfigureAudioSessionFunc' is deprecated

Check warning on line 104 in Sources/LiveKit/Audio/Manager/AudioManager.swift

View workflow job for this annotation

GitHub Actions / Build & Test (macos-15, 16.4, visionOS Simulator,name=Apple Vision Pro,OS=2.5)

'ConfigureAudioSessionFunc' is deprecated

Check warning on line 104 in Sources/LiveKit/Audio/Manager/AudioManager.swift

View workflow job for this annotation

GitHub Actions / Build & Test (macos-26, latest, visionOS Simulator,name=Apple Vision Pro,OS=26.0)

'ConfigureAudioSessionFunc' is deprecated

Check warning on line 104 in Sources/LiveKit/Audio/Manager/AudioManager.swift

View workflow job for this annotation

GitHub Actions / Build & Test (macos-26, latest, visionOS Simulator,name=Apple Vision Pro,OS=26.0)

'ConfigureAudioSessionFunc' is deprecated
public var sessionConfiguration: AudioSessionConfiguration?

public var trackState: TrackState {

Check warning on line 107 in Sources/LiveKit/Audio/Manager/AudioManager.swift

View workflow job for this annotation

GitHub Actions / Build & Test (macos-14, 15.4, tvOS Simulator,name=Apple TV,OS=17.5)

'TrackState' is deprecated

Check warning on line 107 in Sources/LiveKit/Audio/Manager/AudioManager.swift

View workflow job for this annotation

GitHub Actions / Build & Test (macos-14, 15.4, tvOS Simulator,name=Apple TV,OS=17.5)

'TrackState' is deprecated

Check warning on line 107 in Sources/LiveKit/Audio/Manager/AudioManager.swift

View workflow job for this annotation

GitHub Actions / Build & Test (macos-15, 16.4, tvOS Simulator,name=Apple TV,OS=18.5)

'TrackState' is deprecated

Check warning on line 107 in Sources/LiveKit/Audio/Manager/AudioManager.swift

View workflow job for this annotation

GitHub Actions / Build & Test (macos-26, latest, macOS,variant=Mac Catalyst)

'TrackState' is deprecated

Check warning on line 107 in Sources/LiveKit/Audio/Manager/AudioManager.swift

View workflow job for this annotation

GitHub Actions / Build & Test (macos-15, 16.4, macOS,variant=Mac Catalyst)

'TrackState' is deprecated

Check warning on line 107 in Sources/LiveKit/Audio/Manager/AudioManager.swift

View workflow job for this annotation

GitHub Actions / Build & Test (macos-14, 15.4, macOS,variant=Mac Catalyst)

'TrackState' is deprecated

Check warning on line 107 in Sources/LiveKit/Audio/Manager/AudioManager.swift

View workflow job for this annotation

GitHub Actions / Build & Test (macos-14, 15.4, macOS,variant=Mac Catalyst)

'TrackState' is deprecated

Check warning on line 107 in Sources/LiveKit/Audio/Manager/AudioManager.swift

View workflow job for this annotation

GitHub Actions / Build & Test (macos-26, latest, tvOS Simulator,name=Apple TV,OS=26.0)

'TrackState' is deprecated

Check warning on line 107 in Sources/LiveKit/Audio/Manager/AudioManager.swift

View workflow job for this annotation

GitHub Actions / Build & Test (macos-26, latest, tvOS Simulator,name=Apple TV,OS=26.0)

'TrackState' is deprecated

Check warning on line 107 in Sources/LiveKit/Audio/Manager/AudioManager.swift

View workflow job for this annotation

GitHub Actions / Build & Test (macos-26, latest, iOS Simulator,name=iPhone 17 Pro,OS=26.0, true)

'TrackState' is deprecated

Check warning on line 107 in Sources/LiveKit/Audio/Manager/AudioManager.swift

View workflow job for this annotation

GitHub Actions / Build & Test (macos-26, latest, iOS Simulator,name=iPhone 17 Pro,OS=26.0, true)

'TrackState' is deprecated

Check warning on line 107 in Sources/LiveKit/Audio/Manager/AudioManager.swift

View workflow job for this annotation

GitHub Actions / Build & Test (macos-15, 16.4, iOS Simulator,name=iPhone 16 Pro,OS=18.5)

'TrackState' is deprecated

Check warning on line 107 in Sources/LiveKit/Audio/Manager/AudioManager.swift

View workflow job for this annotation

GitHub Actions / Build & Test (macos-15, 16.4, iOS Simulator,name=iPhone 16 Pro,OS=18.5)

'TrackState' is deprecated

Check warning on line 107 in Sources/LiveKit/Audio/Manager/AudioManager.swift

View workflow job for this annotation

GitHub Actions / Build & Test (macos-14, 15.4, iOS Simulator,name=iPhone 15 Pro,OS=17.5)

'TrackState' is deprecated

Check warning on line 107 in Sources/LiveKit/Audio/Manager/AudioManager.swift

View workflow job for this annotation

GitHub Actions / Build & Test (macos-14, 15.4, iOS Simulator,name=iPhone 15 Pro,OS=17.5)

'TrackState' is deprecated

Check warning on line 107 in Sources/LiveKit/Audio/Manager/AudioManager.swift

View workflow job for this annotation

GitHub Actions / Build & Test (macos-15, 16.4, visionOS Simulator,name=Apple Vision Pro,OS=2.5)

'TrackState' is deprecated

Check warning on line 107 in Sources/LiveKit/Audio/Manager/AudioManager.swift

View workflow job for this annotation

GitHub Actions / Build & Test (macos-26, latest, visionOS Simulator,name=Apple Vision Pro,OS=26.0)

'TrackState' is deprecated

Check warning on line 107 in Sources/LiveKit/Audio/Manager/AudioManager.swift

View workflow job for this annotation

GitHub Actions / Build & Test (macos-26, latest, visionOS Simulator,name=Apple Vision Pro,OS=26.0)

'TrackState' is deprecated
switch (localTracksCount > 0, remoteTracksCount > 0) {
case (true, false): .localOnly
case (false, true): .remoteOnly
Expand All @@ -117,9 +117,17 @@

// MARK: - AudioProcessingModule

private lazy var capturePostProcessingDelegateAdapter = AudioCustomProcessingDelegateAdapter(label: "capturePost")

private lazy var renderPreProcessingDelegateAdapter = AudioCustomProcessingDelegateAdapter(label: "renderPre")
private lazy var capturePostProcessingDelegateAdapter = AudioCustomProcessingDelegateAdapter(
label: "capturePost",
rtcDelegateGetter: { RTC.audioProcessingModule.capturePostProcessingDelegate },
rtcDelegateSetter: { RTC.audioProcessingModule.capturePostProcessingDelegate = $0 }
)

private lazy var renderPreProcessingDelegateAdapter = AudioCustomProcessingDelegateAdapter(
label: "renderPre",
rtcDelegateGetter: { RTC.audioProcessingModule.renderPreProcessingDelegate },
rtcDelegateSetter: { RTC.audioProcessingModule.renderPreProcessingDelegate = $0 }
)

let capturePostProcessingDelegateSubject = CurrentValueSubject<AudioCustomProcessingDelegate?, Never>(nil)

Expand All @@ -128,15 +136,7 @@
/// - Note: If you only need to observe the buffer (rather than modify it), use ``add(localAudioRenderer:)`` instead
public var capturePostProcessingDelegate: AudioCustomProcessingDelegate? {
didSet {
if let capturePostProcessingDelegate {
// Clear WebRTC delegate first - this triggers audioProcessingRelease() on the old target
RTC.audioProcessingModule.capturePostProcessingDelegate = nil
capturePostProcessingDelegateAdapter.set(target: capturePostProcessingDelegate)
RTC.audioProcessingModule.capturePostProcessingDelegate = capturePostProcessingDelegateAdapter
} else {
RTC.audioProcessingModule.capturePostProcessingDelegate = nil
capturePostProcessingDelegateAdapter.set(target: nil)
}
capturePostProcessingDelegateAdapter.set(target: capturePostProcessingDelegate, oldTarget: oldValue)
capturePostProcessingDelegateSubject.send(capturePostProcessingDelegate)
}
}
Expand All @@ -147,15 +147,7 @@
/// - Note: If you need to observe the buffer for individual tracks, use ``RemoteAudioTrack/add(audioRenderer:)`` instead
public var renderPreProcessingDelegate: AudioCustomProcessingDelegate? {
didSet {
if let renderPreProcessingDelegate {
// Clear WebRTC delegate first - this triggers release() on the old target
RTC.audioProcessingModule.renderPreProcessingDelegate = nil
renderPreProcessingDelegateAdapter.set(target: renderPreProcessingDelegate)
RTC.audioProcessingModule.renderPreProcessingDelegate = renderPreProcessingDelegateAdapter
} else {
RTC.audioProcessingModule.renderPreProcessingDelegate = nil
renderPreProcessingDelegateAdapter.set(target: nil)
}
renderPreProcessingDelegateAdapter.set(target: renderPreProcessingDelegate, oldTarget: oldValue)
}
}

Expand Down
41 changes: 39 additions & 2 deletions Sources/LiveKit/Protocols/AudioCustomProcessingDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -57,16 +57,53 @@ class AudioCustomProcessingDelegateAdapter: MulticastDelegate<AudioRenderer>, @u

private var _state = StateSync(State())

func set(target: AudioCustomProcessingDelegate?) {
private let rtcDelegateGetter: () -> LKRTCAudioCustomProcessingDelegate?
private let rtcDelegateSetter: (LKRTCAudioCustomProcessingDelegate?) -> Void

func set(target: AudioCustomProcessingDelegate?, oldTarget: AudioCustomProcessingDelegate? = nil) {
// Clear WebRTC delegate first if there's an old target - this triggers audioProcessingRelease() on it
if oldTarget != nil {
rtcDelegateSetter(nil)
}
_state.mutate { $0.target = target }
updateRTCConnection()
}

init(label: String) {
init(label: String,
rtcDelegateGetter: @escaping () -> LKRTCAudioCustomProcessingDelegate?,
rtcDelegateSetter: @escaping (LKRTCAudioCustomProcessingDelegate?) -> Void)
{
self.label = label
self.rtcDelegateGetter = rtcDelegateGetter
self.rtcDelegateSetter = rtcDelegateSetter
super.init(label: "AudioCustomProcessingDelegateAdapter.\(label)")
log("label: \(label)")
}

// Override add/remove to manage RTC connection
override func add(delegate: AudioRenderer) {
super.add(delegate: delegate)
updateRTCConnection()
}

override func remove(delegate: AudioRenderer) {
super.remove(delegate: delegate)
updateRTCConnection()
}

private func updateRTCConnection() {
let shouldBeConnected = target != nil || isDelegatesNotEmpty
let isConnected = rtcDelegateGetter() === self

if shouldBeConnected, !isConnected {
// Connect
rtcDelegateSetter(self)
} else if !shouldBeConnected, isConnected {
// Disconnect
rtcDelegateSetter(nil)
}
}

// MARK: - AudioCustomProcessingDelegate

func audioProcessingInitialize(sampleRate sampleRateHz: Int, channels: Int) {
Expand Down
61 changes: 51 additions & 10 deletions Tests/LiveKitAudioTests/AudioProcessingLifecycle.swift
Original file line number Diff line number Diff line change
Expand Up @@ -66,11 +66,7 @@ class AudioProcessingLifecycle: LKTestCase {
let room1 = rooms[0]
// Publish mic
try await room1.localParticipant.setMicrophone(enabled: true)
do {
// 1 secs...
let ns = UInt64(1 * 1_000_000_000)
try await Task.sleep(nanoseconds: ns)
}
await self.sleep(forSeconds: 1)

// Verify processorA was initialized and received audio
let stateA = processorA._state.copy()
Expand All @@ -79,11 +75,7 @@ class AudioProcessingLifecycle: LKTestCase {

// Switch to processorB
AudioManager.shared.capturePostProcessingDelegate = processorB
do {
// 1 secs...
let ns = UInt64(1 * 1_000_000_000)
try await Task.sleep(nanoseconds: ns)
}
await self.sleep(forSeconds: 1)

// Verify processorA was released
let stateA2 = processorA._state.copy()
Expand All @@ -102,4 +94,53 @@ class AudioProcessingLifecycle: LKTestCase {
let stateB2 = processorB._state.copy()
XCTAssertTrue(stateB2.entries.contains(.release), "Processor B should have been released")
}

func testLocalAudioTrackRendererAPI() async throws {
try await withRooms([RoomTestingOptions(canPublish: true)]) { rooms in
let room1 = rooms[0]

// Create a test renderer
let renderer = TestAudioRenderer()

// Publish microphone
try await room1.localParticipant.setMicrophone(enabled: true)

// Get the local audio track
guard let localAudioTrack = room1.localParticipant.audioTracks.first?.track as? LocalAudioTrack else {
XCTFail("No local audio track found")
return
}

// Add renderer via LocalAudioTrack extension method
localAudioTrack.add(audioRenderer: renderer)

// Wait for audio to flow
await self.sleep(forSeconds: 1)

// Verify renderer received audio
let count = renderer.renderCount.copy()
XCTAssertGreaterThan(count, 0, "Renderer should have received audio buffers via LocalAudioTrack.add()")

// Remove renderer
localAudioTrack.remove(audioRenderer: renderer)

// Reset count
renderer.renderCount.mutate { $0 = 0 }

// Wait a bit
await self.sleep(forSeconds: 1)

// Verify no more audio is received
let countAfterRemove = renderer.renderCount.copy()
XCTAssertEqual(countAfterRemove, 0, "Renderer should not receive audio after removal")
}
}
}

private class TestAudioRenderer: AudioRenderer, @unchecked Sendable {
let renderCount = StateSync<Int>(0)

func render(pcmBuffer _: AVAudioPCMBuffer) {
renderCount.mutate { $0 += 1 }
}
}
20 changes: 0 additions & 20 deletions Tests/LiveKitAudioTests/LocalAudioTrackRecorderTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -165,26 +165,6 @@
XCTAssertGreaterThan(dataCount2, 0, "Should have received audio data from recorder2")
}

func testStartingTwice() async throws {
let localTrack = LocalAudioTrack.createTrack(options: .noProcessing)

let recorder = LocalAudioTrackRecorder(
track: localTrack,
format: .pcmFormatInt16,
sampleRate: 48000
)

for await _ in try await recorder.start().prefix(10) {
// swiftformat:disable hoistAwait
await XCTAssertThrowsErrorAsync(try await recorder.start())
recorder.stop()
}

_ = try await recorder.start()

recorder.stop()
}

func testObjCCompatibility() async throws {
let localTrack = LocalAudioTrack.createTrack(options: .noProcessing)

Expand All @@ -198,7 +178,7 @@
let completionExpectation = expectation(description: "Completion called")
dataExpectation.assertForOverFulfill = false

recorder.start(onData: { _ in

Check warning on line 181 in Tests/LiveKitAudioTests/LocalAudioTrackRecorderTests.swift

View workflow job for this annotation

GitHub Actions / Build & Test (macos-26, latest, macOS, true)

'start(maxSize:onData:onCompletion:)' is deprecated: Use for/await instead.

Check warning on line 181 in Tests/LiveKitAudioTests/LocalAudioTrackRecorderTests.swift

View workflow job for this annotation

GitHub Actions / Build & Test (macos-15, 16.4, macOS)

'start(maxSize:onData:onCompletion:)' is deprecated: Use for/await instead.

Check warning on line 181 in Tests/LiveKitAudioTests/LocalAudioTrackRecorderTests.swift

View workflow job for this annotation

GitHub Actions / Build & Test (macos-26, latest, macOS, true)

'start(maxSize:onData:onCompletion:)' is deprecated: Use for/await instead.
dataExpectation.fulfill()
}, onCompletion: { _ in
completionExpectation.fulfill()
Expand Down
2 changes: 2 additions & 0 deletions Tests/LiveKitCoreTests/Token/TokenSourceTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@
*/

@testable import LiveKit
#if canImport(LiveKitTestSupport)
import LiveKitTestSupport
#endif

class TokenSourceTests: LKTestCase {
actor MockValidJWTSource: TokenSourceConfigurable {
Expand Down
Loading