Archived
feat: conversation state machine, mic meter, history, wake lock (Phase 3)
This commit is contained in:
1 parent
4986b6d7dc
commit
5483e6ed48
8 files changed
+348
-29
No files matched your search
@@ -1,3 +1,4 @@
|
||||
import UIKit
|
||||
import Foundation
|
||||
import LiveKit
|
||||
import os
|
||||
@@ -24,10 +25,12 @@ final class VoiceSession: ObservableObject {
|
||||
@Published private(set) var state: VoiceSessionState = .idle
|
||||
@Published private(set) var agent: AgentInfo?
|
||||
@Published private(set) var isMuted = false
|
||||
@Published private(set) var micError: String?
|
||||
|
||||
private(set) var room = Room()
|
||||
let controlChannel = ControlChannel()
|
||||
let transcriptHandler = TranscriptHandler()
|
||||
let stateModel = ConversationStateModel()
|
||||
private let config: ServerConfig
|
||||
private let tokenClient: TokenClient
|
||||
private var reconnectAttempted = false
|
||||
@@ -78,6 +81,7 @@ final class VoiceSession: ObservableObject {
|
||||
switch result {
|
||||
case .success:
|
||||
logger.log("room connected")
|
||||
UIApplication.shared.isIdleTimerDisabled = true
|
||||
scanForAgent()
|
||||
startAgentWait()
|
||||
case .failure(let error):
|
||||
@@ -137,6 +141,7 @@ final class VoiceSession: ObservableObject {
|
||||
}
|
||||
agentWaitTask?.cancel()
|
||||
state = .ready
|
||||
stateModel.start()
|
||||
logger.log("state=ready, enabling mic")
|
||||
Task { await enableMicrophone() }
|
||||
}
|
||||
@@ -163,6 +168,7 @@ final class VoiceSession: ObservableObject {
|
||||
do {
|
||||
try await room.localParticipant.setMicrophone(enabled: true)
|
||||
isMuted = false
|
||||
micError = nil
|
||||
logger.log("mic enabled")
|
||||
return
|
||||
} catch {
|
||||
@@ -172,8 +178,9 @@ final class VoiceSession: ObservableObject {
|
||||
try? await Task.sleep(for: .seconds(1))
|
||||
continue
|
||||
}
|
||||
// Non-fatal: keep the session usable (text mode still works).
|
||||
logger.error("mic enable failed after retries: \(message, privacy: .public)")
|
||||
state = .failed("Microphone error: \(error.localizedDescription)")
|
||||
micError = "Microphone unavailable (\(error.localizedDescription)). Text mode still works."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -184,11 +191,22 @@ final class VoiceSession: ObservableObject {
|
||||
try await room.localParticipant.setMicrophone(enabled: !muted)
|
||||
isMuted = muted
|
||||
} catch {
|
||||
state = .failed("Microphone error: \(error.localizedDescription)")
|
||||
micError = "Microphone error: \(error.localizedDescription)"
|
||||
}
|
||||
}
|
||||
|
||||
func setMode(_ mode: String) async {
|
||||
guard isReady else { return }
|
||||
do {
|
||||
try await controlChannel.setMode(mode, room: room)
|
||||
logger.log("set_mode sent: \(mode, privacy: .public)")
|
||||
} catch {
|
||||
logger.error("set_mode failed: \(String(describing: error), privacy: .public)")
|
||||
}
|
||||
}
|
||||
|
||||
func sendText(_ text: String) async throws {
|
||||
logger.log("sendText lk.chat chars=\(text.count)")
|
||||
try await room.localParticipant.sendText(text, for: "lk.chat")
|
||||
}
|
||||
|
||||
@@ -197,10 +215,17 @@ final class VoiceSession: ObservableObject {
|
||||
state = .idle
|
||||
agent = nil
|
||||
isMuted = false
|
||||
micError = nil
|
||||
reconnectAttempted = false
|
||||
stateModel.stop()
|
||||
UIApplication.shared.isIdleTimerDisabled = false
|
||||
await teardownRoom()
|
||||
}
|
||||
|
||||
func tickAudioLevels(local: Float, remote: Float) {
|
||||
stateModel.tick(localAudioLevel: local, remoteAudioLevel: remote)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension VoiceSession: RoomDelegate {
|
||||
@@ -211,6 +236,22 @@ extension VoiceSession: RoomDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
nonisolated func room(_ room: Room, didUpdateSpeakingParticipants participants: [Participant]) {
|
||||
Task { @MainActor [weak self] in
|
||||
guard let self, self.state == .ready else { return }
|
||||
var local = Float(0)
|
||||
var remote = Float(0)
|
||||
for participant in participants {
|
||||
if participant.identity?.stringValue == room.localParticipant.identity?.stringValue {
|
||||
local = max(local, participant.audioLevel)
|
||||
} else {
|
||||
remote = max(remote, participant.audioLevel)
|
||||
}
|
||||
}
|
||||
self.tickAudioLevels(local: local, remote: remote)
|
||||
}
|
||||
}
|
||||
|
||||
nonisolated func room(_ room: Room, participant: RemoteParticipant?, didReceiveData data: Data, forTopic topic: String, encryptionType: EncryptionType) {
|
||||
logger.log("data topic=\(topic, privacy: .public) bytes=\(data.count) payload=\(String(data: data.prefix(300), encoding: .utf8) ?? "<binary>", privacy: .public)")
|
||||
Task { @MainActor [weak self] in
|
||||
|
||||
Reference in new issue
Block a user