Skip to content

Commit c79fdbc

Browse files
authored
Ensure audio frame when publishing (#690)
Ensure at least one audio frame was generated before returning publish().
1 parent ba652f1 commit c79fdbc

File tree

15 files changed

+74
-17
lines changed

15 files changed

+74
-17
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
patch type="changed" "Ensure audio frames are being generated when publishing audio"

Sources/LiveKit/Core/Room+Engine.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -367,7 +367,6 @@ extension Room {
367367
self.log("[Connect] Retry cycle waiting for \(String(format: "%.2f", delay)) seconds before attempt \(attempt + 1)")
368368
return delay
369369
}) { currentAttempt, totalAttempts in
370-
371370
// Not reconnecting state anymore
372371
guard let currentMode = self._state.isReconnectingWithMode else {
373372
self.log("[Connect] Not in reconnect state anymore, exiting retry cycle.")

Sources/LiveKit/Core/Room+EngineDelegate.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,6 @@ extension Room {
103103

104104
func engine(_ engine: Room, didUpdateSpeakers speakers: [Livekit_SpeakerInfo]) {
105105
let activeSpeakers = _state.mutate { state -> [Participant] in
106-
107106
var activeSpeakers: [Participant] = []
108107
var seenParticipantSids = [Participant.Sid: Bool]()
109108
for speaker in speakers {

Sources/LiveKit/Core/Room+SignalClientDelegate.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,6 @@ extension Room: SignalClientDelegate {
143143
log("speakers: \(speakers)", .trace)
144144

145145
let activeSpeakers = _state.mutate { state -> [Participant] in
146-
147146
var lastSpeakers = state.activeSpeakers.reduce(into: [Sid: Participant]()) { $0[$1.sid] = $1 }
148147
for speaker in speakers {
149148
let participantSid = Participant.Sid(from: speaker.sid)

Sources/LiveKit/Core/Room.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,6 @@ public class Room: NSObject, @unchecked Sendable, ObservableObject, Loggable {
242242

243243
// trigger events when state mutates
244244
_state.onDidMutate = { [weak self] newState, oldState in
245-
246245
guard let self else { return }
247246

248247
// sid updated

Sources/LiveKit/DataStream/Outgoing/StreamWriterDestination.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,6 @@ import Foundation
1818

1919
protocol StreamWriterDestination: Sendable {
2020
var isOpen: Bool { get async }
21-
func write<T: StreamData>(_ data: T) async throws
21+
func write(_ data: some StreamData) async throws
2222
func close(reason: String?) async throws
2323
}

Sources/LiveKit/Participant/LocalParticipant.swift

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -568,7 +568,6 @@ extension LocalParticipant {
568568
let videoLayers = dimensions.videoLayers(for: encodings)
569569

570570
populatorFunc = { populator in
571-
572571
self.log("[publish] using layers: \(videoLayers.map { String(describing: $0) }.joined(separator: ", "))")
573572

574573
var simulcastCodecs: [Livekit_SimulcastCodec] = [
@@ -696,6 +695,12 @@ extension LocalParticipant {
696695
}
697696
}()
698697

698+
// At this point at least 1 audio frame should be generated to continue
699+
if let track = track as? LocalAudioTrack {
700+
log("[Publish] Waiting for audio frame...")
701+
try await track.startWaitingForFrames()
702+
}
703+
699704
if track is LocalVideoTrack {
700705
if let firstCodecMime = trackInfo.codecs.first?.mimeType,
701706
let firstVideoCodec = VideoCodec.from(mimeType: firstCodecMime)

Sources/LiveKit/Participant/Participant.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,6 @@ public class Participant: NSObject, @unchecked Sendable, ObservableObject, Logga
122122

123123
// trigger events when state mutates
124124
_state.onDidMutate = { [weak self] newState, oldState in
125-
126125
guard let self, let room = self._room else { return }
127126

128127
if newState.isSpeaking != oldState.isSpeaking {

Sources/LiveKit/Support/AsyncCompleter.swift

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -132,12 +132,12 @@ final class AsyncCompleter<T: Sendable>: @unchecked Sendable, Loggable {
132132
}
133133

134134
public func resume(returning value: T) {
135-
log("\(label)")
135+
log("\(label)", .trace)
136136
resume(with: .success(value))
137137
}
138138

139139
public func resume(throwing error: Error) {
140-
log("\(label)")
140+
log("\(label)", .error)
141141
resume(with: .failure(error))
142142
}
143143

@@ -161,7 +161,6 @@ final class AsyncCompleter<T: Sendable>: @unchecked Sendable, Loggable {
161161
// Create a cancel-aware timed continuation
162162
return try await withTaskCancellationHandler {
163163
try await withUnsafeThrowingContinuation { continuation in
164-
165164
// Create time-out block
166165
let timeoutBlock = DispatchWorkItem { [weak self] in
167166
guard let self else { return }

Sources/LiveKit/Track/Local/LocalAudioTrack.swift

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
* limitations under the License.
1515
*/
1616

17+
import AVFAudio
1718
import Combine
1819
import Foundation
1920

@@ -26,7 +27,15 @@ internal import LiveKitWebRTC
2627
@objc
2728
public class LocalAudioTrack: Track, LocalTrack, AudioTrack, @unchecked Sendable {
2829
/// ``AudioCaptureOptions`` used to create this track.
29-
let captureOptions: AudioCaptureOptions
30+
public let captureOptions: AudioCaptureOptions
31+
32+
// MARK: - Internal
33+
34+
struct FrameWatcherState {
35+
var frameWatcher: AudioFrameWatcher?
36+
}
37+
38+
let _frameWatcherState = StateSync(FrameWatcherState())
3039

3140
init(name: String,
3241
source: Track.Source,
@@ -43,6 +52,10 @@ public class LocalAudioTrack: Track, LocalTrack, AudioTrack, @unchecked Sendable
4352
reportStatistics: reportStatistics)
4453
}
4554

55+
deinit {
56+
cleanUpFrameWatcher()
57+
}
58+
4659
public static func createTrack(name: String = Track.microphoneName,
4760
options: AudioCaptureOptions? = nil,
4861
reportStatistics: Bool = false) -> LocalAudioTrack
@@ -93,6 +106,10 @@ public class LocalAudioTrack: Track, LocalTrack, AudioTrack, @unchecked Sendable
93106
throw error
94107
}
95108
}
109+
110+
override func stopCapture() async throws {
111+
cleanUpFrameWatcher()
112+
}
96113
}
97114

98115
public extension LocalAudioTrack {
@@ -109,3 +126,49 @@ public extension LocalAudioTrack {
109126
AudioManager.shared.remove(localAudioRenderer: audioRenderer)
110127
}
111128
}
129+
130+
// MARK: - Internal frame waiting
131+
132+
extension LocalAudioTrack {
133+
final class AudioFrameWatcher: AudioRenderer, Loggable {
134+
private let completer = AsyncCompleter<Void>(label: "Frame watcher", defaultTimeout: 5)
135+
136+
func wait() async throws {
137+
try await completer.wait()
138+
}
139+
140+
func reset() {
141+
completer.reset()
142+
}
143+
144+
// MARK: - AudioRenderer
145+
146+
func render(pcmBuffer _: AVAudioPCMBuffer) {
147+
completer.resume(returning: ())
148+
}
149+
}
150+
151+
func startWaitingForFrames() async throws {
152+
let frameWatcher = _frameWatcherState.mutate {
153+
$0.frameWatcher?.reset()
154+
let watcher = AudioFrameWatcher()
155+
add(audioRenderer: watcher)
156+
$0.frameWatcher = watcher
157+
return watcher
158+
}
159+
160+
try await frameWatcher.wait()
161+
// Detach after wait is complete
162+
cleanUpFrameWatcher()
163+
}
164+
165+
func cleanUpFrameWatcher() {
166+
_frameWatcherState.mutate {
167+
if let watcher = $0.frameWatcher {
168+
watcher.reset()
169+
remove(audioRenderer: watcher)
170+
$0.frameWatcher = nil
171+
}
172+
}
173+
}
174+
}

0 commit comments

Comments
 (0)