Skip to content

Commit

Permalink
Fix add(audioRenderer:) (#352)
Browse files Browse the repository at this point in the history
Fixes issue #350
  • Loading branch information
hiroshihorie authored Mar 8, 2024
1 parent a3bab58 commit 212da46
Show file tree
Hide file tree
Showing 3 changed files with 139 additions and 2 deletions.
7 changes: 7 additions & 0 deletions Sources/LiveKit/Support/MulticastDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,13 @@ public class MulticastDelegate<T>: NSObject, Loggable {
_queue = DispatchQueue(label: "LiveKitSDK.Multicast.\(label)", qos: qos, attributes: [])
}

public var allDelegates: [T] {
_queue.sync { [weak self] in
guard let self else { return [] }
return self._set.allObjects.compactMap { $0 as? T }
}
}

/// Add a single delegate.
public func add(delegate: T) {
guard let delegate = delegate as AnyObject? else {
Expand Down
36 changes: 34 additions & 2 deletions Sources/LiveKit/Track/Remote/RemoteAudioTrack.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,22 @@
* limitations under the License.
*/

import CoreMedia
import Foundation

@_implementationOnly import LiveKitWebRTC

@objc
public class RemoteAudioTrack: Track, RemoteTrack, AudioTrack {
// State used to manage AudioRenderers
private struct RendererState {
var didAttacheAudioRendererAdapter: Bool = false
let audioRenderers = MulticastDelegate<AudioRenderer>(label: "AudioRenderer")
}

private lazy var _audioRendererAdapter = AudioRendererAdapter(target: self)
private let _rendererState = StateSync(RendererState())

/// Volume with range 0.0 - 1.0
public var volume: Double {
get {
Expand All @@ -46,12 +56,26 @@ public class RemoteAudioTrack: Track, RemoteTrack, AudioTrack {

public func add(audioRenderer: AudioRenderer) {
guard let audioTrack = mediaTrack as? LKRTCAudioTrack else { return }
audioTrack.add(AudioRendererAdapter(target: audioRenderer))

_rendererState.mutate {
$0.audioRenderers.add(delegate: audioRenderer)
if !$0.didAttacheAudioRendererAdapter {
audioTrack.add(_audioRendererAdapter)
$0.didAttacheAudioRendererAdapter = true
}
}
}

public func remove(audioRenderer: AudioRenderer) {
guard let audioTrack = mediaTrack as? LKRTCAudioTrack else { return }
audioTrack.remove(AudioRendererAdapter(target: audioRenderer))

_rendererState.mutate {
$0.audioRenderers.remove(delegate: audioRenderer)
if $0.audioRenderers.allDelegates.isEmpty {
audioTrack.remove(_audioRendererAdapter)
$0.didAttacheAudioRendererAdapter = false
}
}
}

// MARK: - Internal
Expand All @@ -64,3 +88,11 @@ public class RemoteAudioTrack: Track, RemoteTrack, AudioTrack {
AudioManager.shared.trackDidStop(.remote)
}
}

extension RemoteAudioTrack: AudioRenderer {
public func render(sampleBuffer: CMSampleBuffer) {
_rendererState.audioRenderers.notify { audioRenderer in
audioRenderer.render(sampleBuffer: sampleBuffer)
}
}
}
98 changes: 98 additions & 0 deletions Tests/LiveKitTests/PublishTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -105,4 +105,102 @@ class PublishTests: XCTestCase {
// Reset
await room1.localParticipant.unpublishAll()
}

// Test if possible to receive audio buffer by adding audio renderer to RemoteAudioTrack.
func testAddAudioRenderer() async throws {
// LocalParticipant's identity should not be nil after a sucessful connection
guard let publisherIdentity = room1.localParticipant.identity else {
XCTFail("Publisher's identity is nil")
return
}

// Get publisher's participant
guard let remoteParticipant = room2.remoteParticipants[publisherIdentity] else {
XCTFail("Failed to lookup Publisher (RemoteParticipant)")
return
}

// Set up expectation...
let didSubscribeToRemoteAudioTrack = expectation(description: "Did subscribe to remote audio track")
didSubscribeToRemoteAudioTrack.assertForOverFulfill = false

var remoteAudioTrack: RemoteAudioTrack?

// Start watching RemoteParticipant for audio track...
let watchParticipant = remoteParticipant.objectWillChange.sink { _ in
if let track = remoteParticipant.firstAudioPublication?.track as? RemoteAudioTrack, remoteAudioTrack == nil {
remoteAudioTrack = track
didSubscribeToRemoteAudioTrack.fulfill()
}
}

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

// Wait for track...
print("Waiting for first audio track...")
await fulfillment(of: [didSubscribeToRemoteAudioTrack], timeout: 30)

guard let remoteAudioTrack else {
XCTFail("RemoteAudioTrack is nil")
return
}

// Received RemoteAudioTrack...
print("remoteAudioTrack: \(String(describing: remoteAudioTrack))")

// Set up expectation...
let didReceiveAudioFrame = expectation(description: "Did receive audio frame")
didReceiveAudioFrame.assertForOverFulfill = false

// Start watching for audio frame...
let audioFrameWatcher = AudioFrameWatcher(id: "notifier01") { _ in
didReceiveAudioFrame.fulfill()
}

// Attach audio frame watcher...
remoteAudioTrack.add(audioRenderer: audioFrameWatcher)

// Wait for audio frame...
print("Waiting for first audio frame...")
await fulfillment(of: [didReceiveAudioFrame], timeout: 30)

// Remove audio frame watcher...
remoteAudioTrack.remove(audioRenderer: audioFrameWatcher)

// Clean up
watchParticipant.cancel()
// Reset
await room1.localParticipant.unpublishAll()
}
}

actor AudioFrameWatcher: AudioRenderer {
public let id: String
private let onReceivedFirstFrame: (_ sid: String) -> Void
public private(set) var didReceiveFirstFrame: Bool = false

init(id: String, onReceivedFrame: @escaping (String) -> Void) {
self.id = id
onReceivedFirstFrame = onReceivedFrame
}

public func reset() {
didReceiveFirstFrame = false
}

private func onDidReceiveFirstFrame() {
if !didReceiveFirstFrame {
didReceiveFirstFrame = true
onReceivedFirstFrame(id)
}
}

nonisolated
func render(sampleBuffer: CMSampleBuffer) {
print("did receive first audio frame: \(String(describing: sampleBuffer))")
Task {
await onDidReceiveFirstFrame()
}
}
}

0 comments on commit 212da46

Please sign in to comment.