Skip to content

Ensure audio frame when publishing #690

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
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
1 change: 1 addition & 0 deletions .nanpa/publish-audio-check-frames.kdl
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
patch type="changed" "Ensure audio frames are being generated when publishing audio"
6 changes: 6 additions & 0 deletions Sources/LiveKit/Participant/LocalParticipant.swift
Original file line number Diff line number Diff line change
Expand Up @@ -501,7 +501,7 @@

private extension LocalParticipant {
@discardableResult
internal func _publish(track: LocalTrack, options: TrackPublishOptions? = nil) async throws -> LocalTrackPublication {

Check warning on line 504 in Sources/LiveKit/Participant/LocalParticipant.swift

View workflow job for this annotation

GitHub Actions / Build & Test (macos-14, 15.4, macOS)

'internal' modifier conflicts with extension's default access of 'private'

Check warning on line 504 in Sources/LiveKit/Participant/LocalParticipant.swift

View workflow job for this annotation

GitHub Actions / Build & Test (macos-14, 15.4, macOS)

'internal' modifier conflicts with extension's default access of 'private'

Check warning on line 504 in Sources/LiveKit/Participant/LocalParticipant.swift

View workflow job for this annotation

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

'internal' modifier conflicts with extension's default access of 'private'

Check warning on line 504 in Sources/LiveKit/Participant/LocalParticipant.swift

View workflow job for this annotation

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

'internal' modifier conflicts with extension's default access of 'private'

Check warning on line 504 in Sources/LiveKit/Participant/LocalParticipant.swift

View workflow job for this annotation

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

'internal' modifier conflicts with extension's default access of 'private'

Check warning on line 504 in Sources/LiveKit/Participant/LocalParticipant.swift

View workflow job for this annotation

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

'internal' modifier conflicts with extension's default access of 'private'

Check warning on line 504 in Sources/LiveKit/Participant/LocalParticipant.swift

View workflow job for this annotation

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

'internal' modifier conflicts with extension's default access of 'private'

Check warning on line 504 in Sources/LiveKit/Participant/LocalParticipant.swift

View workflow job for this annotation

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

'internal' modifier conflicts with extension's default access of 'private'

Check warning on line 504 in Sources/LiveKit/Participant/LocalParticipant.swift

View workflow job for this annotation

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

'internal' modifier conflicts with extension's default access of 'private'

Check warning on line 504 in Sources/LiveKit/Participant/LocalParticipant.swift

View workflow job for this annotation

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

'internal' modifier conflicts with extension's default access of 'private'

Check warning on line 504 in Sources/LiveKit/Participant/LocalParticipant.swift

View workflow job for this annotation

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

'internal' modifier conflicts with extension's default access of 'private'

Check warning on line 504 in Sources/LiveKit/Participant/LocalParticipant.swift

View workflow job for this annotation

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

'internal' modifier conflicts with extension's default access of 'private'

Check warning on line 504 in Sources/LiveKit/Participant/LocalParticipant.swift

View workflow job for this annotation

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

'internal' modifier conflicts with extension's default access of 'private'

Check warning on line 504 in Sources/LiveKit/Participant/LocalParticipant.swift

View workflow job for this annotation

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

'internal' modifier conflicts with extension's default access of 'private'

Check warning on line 504 in Sources/LiveKit/Participant/LocalParticipant.swift

View workflow job for this annotation

GitHub Actions / Build & Test (macos-14, 15.4, visionOS Simulator,name=Apple Vision Pro)

'internal' modifier conflicts with extension's default access of 'private'

Check warning on line 504 in Sources/LiveKit/Participant/LocalParticipant.swift

View workflow job for this annotation

GitHub Actions / Build & Test (macos-14, 15.4, visionOS Simulator,name=Apple Vision Pro)

'internal' modifier conflicts with extension's default access of 'private'
log("[publish] \(track) options: \(String(describing: options ?? nil))...", .info)

try checkPermissions(toPublish: track)
Expand Down Expand Up @@ -687,6 +687,12 @@
}
}()

// At this point at least 1 audio frame should be generated to continue
if let track = track as? LocalAudioTrack {
log("[Publish] Waiting for audio frame...")
try await track.frameWatcher.wait()
}

if track is LocalVideoTrack {
if let firstCodecMime = trackInfo.codecs.first?.mimeType,
let firstVideoCodec = VideoCodec.from(mimeType: firstCodecMime)
Expand Down
4 changes: 2 additions & 2 deletions Sources/LiveKit/Support/AsyncCompleter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -132,12 +132,12 @@ final class AsyncCompleter<T: Sendable>: @unchecked Sendable, Loggable {
}

public func resume(returning value: T) {
log("\(label)")
log("\(label)", .trace)
resume(with: .success(value))
}

public func resume(throwing error: Error) {
log("\(label)")
log("\(label)", .error)
resume(with: .failure(error))
}

Expand Down
32 changes: 32 additions & 0 deletions Sources/LiveKit/Track/Local/LocalAudioTrack.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ internal import LiveKitWebRTC
public class LocalAudioTrack: Track, LocalTrack, AudioTrack, @unchecked Sendable {
/// ``AudioCaptureOptions`` used to create this track.
let captureOptions: AudioCaptureOptions
let frameWatcher = AudioFrameWatcher()

init(name: String,
source: Track.Source,
Expand All @@ -41,6 +42,13 @@ public class LocalAudioTrack: Track, LocalTrack, AudioTrack, @unchecked Sendable
source: source,
track: track,
reportStatistics: reportStatistics)

// Listen for frames
add(audioRenderer: frameWatcher)
}

deinit {
remove(audioRenderer: frameWatcher)
}

public static func createTrack(name: String = Track.microphoneName,
Expand Down Expand Up @@ -93,6 +101,10 @@ public class LocalAudioTrack: Track, LocalTrack, AudioTrack, @unchecked Sendable
throw error
}
}

override func stopCapture() async throws {
frameWatcher.reset()
}
}

public extension LocalAudioTrack {
Expand All @@ -109,3 +121,23 @@ public extension LocalAudioTrack {
AudioManager.shared.remove(localAudioRenderer: audioRenderer)
}
}

// MARK: - Internal

final class AudioFrameWatcher: AudioRenderer, Loggable {
private let completer = AsyncCompleter<Void>(label: "Frame watcher", defaultTimeout: 5)

func wait() async throws {
try await completer.wait()
}

func reset() {
completer.reset()
}

// MARK: - AudioRenderer

func render(pcmBuffer _: AVAudioPCMBuffer) {
completer.resume(returning: ())
}
}
Loading