Skip to content

Commit d50939f

Browse files
renal128cursoragent
andcommitted
Claim connecting before voice audio setup so cancel during start is not alreadyStarted.
Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 643837d commit d50939f

1 file changed

Lines changed: 66 additions & 59 deletions

File tree

Sources/ElevenLabs/Internal/Conversation/Conversation.swift

Lines changed: 66 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,12 @@ final class Conversation: ObservableObject {
4444
let agentObserverRegistry = AudioObserverRegistry()
4545
let micObserverRegistry = AudioObserverRegistry()
4646

47+
private let dependencyProvider: any ConversationDependencyProvider
48+
private var activeConnectionManager: (any ConnectionManaging)?
49+
private var activeWebRTCConnectionManager: (any WebRTCConnectionManaging)? {
50+
activeConnectionManager as? any WebRTCConnectionManaging
51+
}
52+
4753
/// Internal LiveKit tracks used to attach ``ConversationAudioObserver``s.
4854
private var inputTrack: (any AudioTrackProtocol)? {
4955
activeWebRTCConnectionManager?.inputTrack
@@ -83,26 +89,30 @@ final class Conversation: ObservableObject {
8389

8490
func startVoiceConversation(_ auth: ConversationAuth.Voice) async throws -> ConversationStartResult {
8591
let manager = dependencyProvider.webRTCConnectionManager
86-
let audioManager = ConversationAudioManager(logger: logger)
87-
self.audioManager = audioManager
92+
let result = try await start(agentId: auth.agentId, isTextOnly: false, using: manager) { config in
93+
let audioManager = ConversationAudioManager(logger: logger)
94+
self.audioManager = audioManager
8895

89-
manager.onRemoteSpeakingChanged = { [weak self] isSpeaking in
90-
Task { @MainActor in
91-
self?.handleRemoteSpeakingUpdate(isSpeaking: isSpeaking)
96+
manager.onRemoteSpeakingChanged = { [weak self] isSpeaking in
97+
Task { @MainActor in
98+
self?.handleRemoteSpeakingUpdate(isSpeaking: isSpeaking)
99+
}
92100
}
93-
}
94-
manager.onTracksChanged = { [weak self] in
95-
Task { @MainActor in
96-
self?.refreshAudioObservers()
101+
manager.onTracksChanged = { [weak self] in
102+
Task { @MainActor in
103+
self?.refreshAudioObservers()
104+
}
105+
}
106+
await audioManager.configure(with: config, callbacks: callbacks)
107+
guard state.isConnecting else {
108+
audioManager.cleanup()
109+
throw CancellationError()
110+
}
111+
if let pendingMuteState {
112+
audioManager.softwareMuteProcessor?.setMuted(pendingMuteState)
97113
}
98-
}
99-
await audioManager.configure(with: config, callbacks: callbacks)
100-
if let pendingMuteState {
101-
audioManager.softwareMuteProcessor?.setMuted(pendingMuteState)
102-
}
103114

104-
let result = try await start(agentId: auth.agentId, isTextOnly: false, using: manager) { config in
105-
try await manager.connect(
115+
return try await manager.connect(
106116
auth: auth,
107117
config: config,
108118
onStartupStateChange: { [weak self] in self?.updateStartupStage($0) }
@@ -112,7 +122,7 @@ final class Conversation: ObservableObject {
112122
if let pendingMute = pendingMuteState {
113123
pendingMuteState = nil
114124
do {
115-
if let softwareMuteProcessor = audioManager.softwareMuteProcessor {
125+
if let softwareMuteProcessor = audioManager?.softwareMuteProcessor {
116126
softwareMuteProcessor.setMuted(pendingMute)
117127
} else {
118128
try await manager.setMicrophoneMuted(pendingMute)
@@ -277,54 +287,16 @@ final class Conversation: ObservableObject {
277287

278288
// MARK: - Private
279289

280-
private let dependencyProvider: any ConversationDependencyProvider
281-
private var activeConnectionManager: (any ConnectionManaging)?
282-
private var activeWebRTCConnectionManager: (any WebRTCConnectionManaging)? {
283-
activeConnectionManager as? any WebRTCConnectionManaging
284-
}
285-
286-
private func updateStartupStage(_ stage: ConversationStartupState) {
287-
guard state.isConnecting, state != .connecting(stage) else { return }
288-
state = .connecting(stage)
289-
}
290-
291-
private func setupAgentStateManager() {
292-
guard let configuration = config.agentStateConfiguration else { return }
293-
let manager = AgentStateManager(configuration: configuration)
294-
manager.onStateChange = { [weak self] state in
295-
self?.agentState = state
296-
self?.callbacks.onAgentStateChange?(state)
297-
}
298-
agentStateManager = manager
299-
}
300-
301-
/// Forward a signal to the event-based state manager, or fall back to directly setting `agentState`.
302-
private func applyStateSignal(_ signal: AgentStateSignal, fallback: AgentState) {
303-
if let manager = agentStateManager {
304-
manager.processSignal(signal)
305-
} else {
306-
agentState = fallback
307-
}
308-
}
309-
310-
private func handleRemoteSpeakingUpdate(isSpeaking: Bool) {
311-
if let manager = agentStateManager {
312-
manager.processSignal(isSpeaking ? .agentStartedSpeaking : .agentStoppedSpeaking)
313-
} else if isSpeaking {
314-
speakingTimer?.cancel()
315-
agentState = .speaking
316-
} else {
317-
scheduleBackToListening(delay: 1.0)
318-
}
319-
}
320-
321290
private func start(
322291
agentId: String,
323292
isTextOnly: Bool,
324293
using manager: any ConnectionManaging,
325294
connect: (ConversationConfig) async throws -> ConversationStartResult
326295
) async throws -> ConversationStartResult {
327-
guard state == .idle else {
296+
if state != .idle {
297+
if state.isEnded, activeConnectionManager == nil {
298+
throw CancellationError()
299+
}
328300
throw ConversationError.alreadyStarted
329301
}
330302

@@ -372,6 +344,41 @@ final class Conversation: ObservableObject {
372344
return result
373345
}
374346

347+
private func updateStartupStage(_ stage: ConversationStartupState) {
348+
guard state.isConnecting, state != .connecting(stage) else { return }
349+
state = .connecting(stage)
350+
}
351+
352+
private func setupAgentStateManager() {
353+
guard let configuration = config.agentStateConfiguration else { return }
354+
let manager = AgentStateManager(configuration: configuration)
355+
manager.onStateChange = { [weak self] state in
356+
self?.agentState = state
357+
self?.callbacks.onAgentStateChange?(state)
358+
}
359+
agentStateManager = manager
360+
}
361+
362+
/// Forward a signal to the event-based state manager, or fall back to directly setting `agentState`.
363+
private func applyStateSignal(_ signal: AgentStateSignal, fallback: AgentState) {
364+
if let manager = agentStateManager {
365+
manager.processSignal(signal)
366+
} else {
367+
agentState = fallback
368+
}
369+
}
370+
371+
private func handleRemoteSpeakingUpdate(isSpeaking: Bool) {
372+
if let manager = agentStateManager {
373+
manager.processSignal(isSpeaking ? .agentStartedSpeaking : .agentStoppedSpeaking)
374+
} else if isSpeaking {
375+
speakingTimer?.cancel()
376+
agentState = .speaking
377+
} else {
378+
scheduleBackToListening(delay: 1.0)
379+
}
380+
}
381+
375382
private func handleIncomingEvent(_ event: IncomingEvent, from connectionManagerID: ObjectIdentifier) async {
376383
guard let activeConnectionManager,
377384
ObjectIdentifier(activeConnectionManager) == connectionManagerID,

0 commit comments

Comments
 (0)