Browse Source

fix(agent_service): 修复通话模式结束后音频会话未重置的问题

fix(azure_speech): 适配iOS 18以下版本的音频会话配置和播放逻辑
修复iOS 17及以下版本流式播放可能卡住的问题

refactor(chat_storage): 使用串行队列保护数据库访问
确保所有数据库操作线程安全,避免并发问题
newdev_shunjiawei
liwei1dao 9 months ago
parent
commit
03e0d94b5a
  1. 28
      local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift
  2. 67
      local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureTtsHelper.swift
  3. 68
      local_plugins/chat_storage/ios/chat_storage/Sources/chat_storage/ChatStorageHelper.swift

28
local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift

@ -576,29 +576,43 @@ class AgentServiceImpl: NSObject {
return true return true
} }
// 关键修复:如果正在结束的是通话模式,则立即将会话重置为 normal 状态
if self.currentRecognitionMode == "phone_call" {
os_log("通话模式结束,强制重置音频会话至 'normal' 状态", log: logger, type: .info)
self.currentRecognitionMode = "normal"
// 使用 normal 模式的配置来清理和重置音频会话
self.checkAdnSetAudioSession()
}
os_log("停止语音识别,当前模式: %{public}@", log: logger, type: .info, currentRecognitionMode) os_log("停止语音识别,当前模式: %{public}@", log: logger, type: .info, currentRecognitionMode)
let success = azureAsrHelper?.stopContinuousRecognition() ?? false let success = azureAsrHelper?.stopContinuousRecognition() ?? false
if success {
if !success && isStartingRecognition { // 识别结束后,立即停用 AudioSession,以便其他模块(如TTS)可以接管
// 这是解决“按住说话”后无法播报问题的关键
do {
try AVAudioSession.sharedInstance().setActive(false, options: .notifyOthersOnDeactivation)
os_log("Audio session deactivated successfully after recognition.", log: logger, type: .info)
} catch {
os_log("Failed to deactivate audio session: %{public}@", log: logger, type: .error, error.localizedDescription)
}
} else if isStartingRecognition {
// 启动尚未完成,先记录一次待停止请求,onSessionStarted 到来后立即 stop // 启动尚未完成,先记录一次待停止请求,onSessionStarted 到来后立即 stop
stopRequestedDuringStart = true stopRequestedDuringStart = true
// 即使底层 stop 失败,但因为我们已经记录了停止请求,所以对调用方来说,可以认为是成功的
// 后续的 onSessionStarted 会处理真正的停止逻辑
} }
stopIdleCheck() stopIdleCheck()
// 只有之前在播放时才恢复播放 // 只有之前在播放时才恢复播放
if wasMusicPlayingBeforeRecognition { if wasMusicPlayingBeforeRecognition {
// QPlayAutoManager.shared.play()
wasMusicPlayingBeforeRecognition = false wasMusicPlayingBeforeRecognition = false
MusicService.shared.resume() MusicService.shared.resume()
} }
// 重置识别模式
// currentRecognitionMode = "normal"
// 启动中但已请求停止,也视为已接受停止请求 // 启动中但已请求停止,也视为已接受停止请求
return success || isStartingRecognition return success || stopRequestedDuringStart
} }
func disableBluetoothAudio() { func disableBluetoothAudio() {
azureAsrHelper?.disableBluetoothAudio() azureAsrHelper?.disableBluetoothAudio()

67
local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureTtsHelper.swift

@ -848,6 +848,9 @@ public class AzureTtsHelper: NSObject, ITtsService, AVAudioPlayerDelegate {
*/ */
private func scheduleAvailablePcmBuffers() { private func scheduleAvailablePcmBuffers() {
guard ensureStreamingEngineIfNeeded() else { return } guard ensureStreamingEngineIfNeeded() else { return }
if #available(iOS 18.0, *) {
// iOS 18及以上版本,使用原始逻辑:先调度,后播放
if !hasNotifiedPlaybackStartedForStream, pcmPendingData.count >= streamChunkBytes { if !hasNotifiedPlaybackStartedForStream, pcmPendingData.count >= streamChunkBytes {
// os_log("调用链: scheduleAvailablePcmBuffers 准备调度 pendingPcm=%{public}d chunk=%{public}d active=%{public}d", log: log, type: .info, pcmPendingData.count, streamChunkBytes, activeStreamSynthesisCount) // os_log("调用链: scheduleAvailablePcmBuffers 准备调度 pendingPcm=%{public}d chunk=%{public}d active=%{public}d", log: log, type: .info, pcmPendingData.count, streamChunkBytes, activeStreamSynthesisCount)
} }
@ -894,6 +897,57 @@ public class AzureTtsHelper: NSObject, ITtsService, AVAudioPlayerDelegate {
// os_log("调用链: playerNode.play scheduled=%{public}d pendingPcm=%{public}d", log: log, type: .debug, scheduledBufferCount, pcmPendingData.count) // os_log("调用链: playerNode.play scheduled=%{public}d pendingPcm=%{public}d", log: log, type: .debug, scheduledBufferCount, pcmPendingData.count)
playerNode?.play() playerNode?.play()
} }
} else {
// iOS 17及以下版本,使用修复逻辑:先播放,后调度
if playerNode?.isPlaying == false {
// os_log("调用链: playerNode.play 提前启动播放器", log: log, type: .debug)
playerNode?.play()
}
if !hasNotifiedPlaybackStartedForStream, pcmPendingData.count >= streamChunkBytes {
// os_log("调用链: scheduleAvailablePcmBuffers 准备调度 pendingPcm=%{public}d chunk=%{public}d active=%{public}d", log: log, type: .info, pcmPendingData.count, streamChunkBytes, activeStreamSynthesisCount)
}
while pcmPendingData.count >= streamChunkBytes {
let chunk = pcmPendingData.prefix(streamChunkBytes)
pcmPendingData.removeFirst(streamChunkBytes)
guard let buffer = makePcmBuffer(from: Data(chunk)) else { continue }
scheduledBufferCount += 1
if !hasNotifiedPlaybackStartedForStream {
hasNotifiedPlaybackStartedForStream = true
// os_log("调用链: 流式播放开始 session=%{public}@", log: log, type: .info, sessionid)
notifyEvent(eventType: .playbackStarted)
}
playerNode?.scheduleBuffer(buffer, completionHandler: { [weak self] in
guard let self = self else { return }
self.audioPlaybackQueue.async {
self.scheduledBufferCount = max(0, self.scheduledBufferCount - 1)
self.checkStreamPlaybackCompletedIfNeeded()
}
})
}
if activeStreamSynthesisCount == 0, !pcmPendingData.isEmpty {
if pcmPendingData.count % 2 != 0 {
pcmPendingData.removeLast()
}
if !pcmPendingData.isEmpty, let buffer = makePcmBuffer(from: pcmPendingData) {
pcmPendingData.removeAll(keepingCapacity: true)
scheduledBufferCount += 1
if !hasNotifiedPlaybackStartedForStream {
hasNotifiedPlaybackStartedForStream = true
// os_log("调用链: 流式播放开始 session=%{public}@", log: log, type: .info, sessionid)
notifyEvent(eventType: .playbackStarted)
}
playerNode?.scheduleBuffer(buffer, completionHandler: { [weak self] in
guard let self = self else { return }
self.audioPlaybackQueue.async {
self.scheduledBufferCount = max(0, self.scheduledBufferCount - 1)
self.checkStreamPlaybackCompletedIfNeeded()
}
})
}
}
}
} }
/** /**
@ -1367,7 +1421,11 @@ private func applyAudioSessionForTTS() -> Bool {
desiredCategory = .playAndRecord desiredCategory = .playAndRecord
if isHeadphonesConnected { if isHeadphonesConnected {
desiredMode = .voiceChat desiredMode = .voiceChat
if #available(iOS 18.0, *) {
desiredOptions = [.allowBluetooth, .duckOthers] desiredOptions = [.allowBluetooth, .duckOthers]
} else {
desiredOptions = [.allowBluetooth, .allowBluetoothA2DP]
}
} else { } else {
desiredMode = .videoChat desiredMode = .videoChat
desiredOptions = [.allowBluetooth, .defaultToSpeaker] desiredOptions = [.allowBluetooth, .defaultToSpeaker]
@ -1404,7 +1462,14 @@ private func applyAudioSessionForTTS() -> Bool {
} else { } else {
desiredCategory = .playback desiredCategory = .playback
desiredMode = .default desiredMode = .default
desiredOptions = [.duckOthers] if #available(iOS 18.0, *) {
desiredOptions = []
} else {
desiredOptions = [.defaultToSpeaker]
}
if otherAudioPlaying {
desiredOptions.insert(.duckOthers)
}
} }
do { do {

68
local_plugins/chat_storage/ios/chat_storage/Sources/chat_storage/ChatStorageHelper.swift

@ -15,6 +15,8 @@ public class ChatStorageHelper {
private let dbPath: String private let dbPath: String
private let logger = OSLog(subsystem: "com.yunqiinnovation.chat_storage", category: "ChatStorageHelper") private let logger = OSLog(subsystem: "com.yunqiinnovation.chat_storage", category: "ChatStorageHelper")
private let dbQueue = DispatchQueue(label: "com.yunqiinnovation.chat_storage.dbQueue")
private init() { private init() {
// 获取文档目录路径(不变) // 获取文档目录路径(不变)
let fileURL = try! FileManager.default let fileURL = try! FileManager.default
@ -23,6 +25,7 @@ public class ChatStorageHelper {
dbPath = fileURL.path dbPath = fileURL.path
dbQueue.sync {
// 打开数据库(不变) // 打开数据库(不变)
if sqlite3_open(dbPath, &db) != SQLITE_OK { if sqlite3_open(dbPath, &db) != SQLITE_OK {
let errmsg = String(cString: sqlite3_errmsg(db)!) let errmsg = String(cString: sqlite3_errmsg(db)!)
@ -52,12 +55,15 @@ public class ChatStorageHelper {
os_log("创建表失败: %{public}@", log: logger, type: .error, errmsg) os_log("创建表失败: %{public}@", log: logger, type: .error, errmsg)
} }
} }
}
deinit { deinit {
dbQueue.sync {
if db != nil { if db != nil {
sqlite3_close(db) sqlite3_close(db)
} }
} }
}
/** /**
* 保存消息到数据库 * 保存消息到数据库
@ -68,6 +74,8 @@ public class ChatStorageHelper {
* @return 插入的消息ID,失败则返回-1 * @return 插入的消息ID,失败则返回-1
*/ */
public func saveMessage(agentId: String, sessionId: String, message: String, sender: String, metadata: String?) -> Int64 { public func saveMessage(agentId: String, sessionId: String, message: String, sender: String, metadata: String?) -> Int64 {
var result: Int64 = -1
dbQueue.sync {
// 关键修改:用 INSERT OR REPLACE 替换 INSERT,支持冲突时更新 // 关键修改:用 INSERT OR REPLACE 替换 INSERT,支持冲突时更新
let insertStatementString = """ let insertStatementString = """
INSERT OR REPLACE INTO messages INSERT OR REPLACE INTO messages
@ -95,21 +103,19 @@ public class ChatStorageHelper {
// 执行语句(冲突时会自动替换,返回新的 rowid) // 执行语句(冲突时会自动替换,返回新的 rowid)
if sqlite3_step(insertStatement) == SQLITE_DONE { if sqlite3_step(insertStatement) == SQLITE_DONE {
let id = sqlite3_last_insert_rowid(db) // 替换后返回新的 id(原 id 会被删除) let id = sqlite3_last_insert_rowid(db) // 替换后返回新的 id(原 id 会被删除)
sqlite3_finalize(insertStatement)
os_log("消息保存成功(新增/更新),id: %{public}lld", log: logger, type: .info, id) os_log("消息保存成功(新增/更新),id: %{public}lld", log: logger, type: .info, id)
return id result = id
} else { } else {
let errmsg = String(cString: sqlite3_errmsg(db)!) let errmsg = String(cString: sqlite3_errmsg(db)!)
os_log("插入/更新消息失败: %{public}@", log: logger, type: .error, errmsg) os_log("插入/更新消息失败: %{public}@", log: logger, type: .error, errmsg)
} }
sqlite3_finalize(insertStatement) sqlite3_finalize(insertStatement)
} else { } else {
let errmsg = String(cString: sqlite3_errmsg(db)!) let errmsg = String(cString: sqlite3_errmsg(db)!)
os_log("插入/更新消息语句准备失败: %{public}@", log: logger, type: .error, errmsg) os_log("插入/更新消息语句准备失败: %{public}@", log: logger, type: .error, errmsg)
} }
}
return -1 return result
} }
/** /**
@ -120,6 +126,8 @@ public class ChatStorageHelper {
* @return 消息列表的JSON字符串 * @return 消息列表的JSON字符串
*/ */
public func getMessages(agentId: String, page: Int, pageSize: Int) -> String { public func getMessages(agentId: String, page: Int, pageSize: Int) -> String {
var result = "{\"messages\":[],\"page\":\(page),\"pageSize\":\(pageSize),\"totalCount\":0,\"totalPages\":0}"
dbQueue.sync {
let offset = (page - 1) * pageSize let offset = (page - 1) * pageSize
var messagesArray: [[String: Any]] = [] var messagesArray: [[String: Any]] = []
@ -196,7 +204,7 @@ public class ChatStorageHelper {
sqlite3_finalize(queryStatement) sqlite3_finalize(queryStatement)
// 构建与Android版本一致的返回格式 // 构建与Android版本一致的返回格式
let result: [String: Any] = [ let resultDict: [String: Any] = [
"messages": messagesArray, "messages": messagesArray,
"page": page, "page": page,
"pageSize": pageSize, "pageSize": pageSize,
@ -205,9 +213,9 @@ public class ChatStorageHelper {
] ]
os_log("getMessages: 读取历史记录: %{public}@", log: logger, type: .error, messagesArray) os_log("getMessages: 读取历史记录: %{public}@", log: logger, type: .error, messagesArray)
do { do {
let jsonData = try JSONSerialization.data(withJSONObject: result, options: []) let jsonData = try JSONSerialization.data(withJSONObject: resultDict, options: [])
if let jsonString = String(data: jsonData, encoding: .utf8) { if let jsonString = String(data: jsonData, encoding: .utf8) {
return jsonString result = jsonString
} }
} catch { } catch {
os_log("getMessages: JSON转换失败: %{public}@", log: logger, type: .error, error.localizedDescription) os_log("getMessages: JSON转换失败: %{public}@", log: logger, type: .error, error.localizedDescription)
@ -216,26 +224,8 @@ public class ChatStorageHelper {
let errmsg = String(cString: sqlite3_errmsg(db)!) let errmsg = String(cString: sqlite3_errmsg(db)!)
os_log("getMessages: SQL准备失败: %{public}@", log: logger, type: .error, errmsg) os_log("getMessages: SQL准备失败: %{public}@", log: logger, type: .error, errmsg)
} }
// 返回空结果,但保持格式一致
let emptyResult: [String: Any] = [
"messages": [],
"page": page,
"pageSize": pageSize,
"totalCount": 0,
"totalPages": 0
]
do {
let jsonData = try JSONSerialization.data(withJSONObject: emptyResult, options: [])
if let jsonString = String(data: jsonData, encoding: .utf8) {
return jsonString
}
} catch {
os_log("getMessages: 空结果JSON转换失败: %{public}@", log: logger, type: .error, error.localizedDescription)
} }
return result
return "{\"messages\":[],\"page\":\(page),\"pageSize\":\(pageSize),\"totalCount\":0,\"totalPages\":0}"
} }
/** /**
@ -245,6 +235,8 @@ public class ChatStorageHelper {
* @return 是否删除成功 * @return 是否删除成功
*/ */
public func deleteMessages(agentId: String?, messageIds: [Int]?) -> Bool { public func deleteMessages(agentId: String?, messageIds: [Int]?) -> Bool {
var result = false
dbQueue.sync {
if let agentId = agentId { if let agentId = agentId {
let deleteString = "DELETE FROM messages WHERE agent_id = ?;" let deleteString = "DELETE FROM messages WHERE agent_id = ?;"
var deleteStatement: OpaquePointer? var deleteStatement: OpaquePointer?
@ -254,7 +246,7 @@ public class ChatStorageHelper {
if sqlite3_step(deleteStatement) == SQLITE_DONE { if sqlite3_step(deleteStatement) == SQLITE_DONE {
sqlite3_finalize(deleteStatement) sqlite3_finalize(deleteStatement)
return true result = true
} else { } else {
let errmsg = String(cString: sqlite3_errmsg(db)!) let errmsg = String(cString: sqlite3_errmsg(db)!)
os_log("删除会话消息失败: %{public}@", log: logger, type: .error, errmsg) os_log("删除会话消息失败: %{public}@", log: logger, type: .error, errmsg)
@ -277,13 +269,11 @@ public class ChatStorageHelper {
} }
if sqlite3_step(deleteStatement) == SQLITE_DONE { if sqlite3_step(deleteStatement) == SQLITE_DONE {
sqlite3_finalize(deleteStatement) result = true
return true
} else { } else {
let errmsg = String(cString: sqlite3_errmsg(db)!) let errmsg = String(cString: sqlite3_errmsg(db)!)
os_log("删除特定消息失败: %{public}@", log: logger, type: .error, errmsg) os_log("删除特定消息失败: %{public}@", log: logger, type: .error, errmsg)
} }
sqlite3_finalize(deleteStatement) sqlite3_finalize(deleteStatement)
} else { } else {
let errmsg = String(cString: sqlite3_errmsg(db)!) let errmsg = String(cString: sqlite3_errmsg(db)!)
@ -291,10 +281,9 @@ public class ChatStorageHelper {
} }
} else { } else {
os_log("删除消息参数无效 - agentId和messageIds都为空", log: logger, type: .error) os_log("删除消息参数无效 - agentId和messageIds都为空", log: logger, type: .error)
return false
} }
}
return false return result
} }
/** /**
@ -302,16 +291,19 @@ public class ChatStorageHelper {
* @return 是否清空成功 * @return 是否清空成功
*/ */
public func clearDatabase() -> Bool { public func clearDatabase() -> Bool {
var result = false
dbQueue.sync {
let deleteString = "DELETE FROM messages;" let deleteString = "DELETE FROM messages;"
if sqlite3_exec(db, deleteString, nil, nil, nil) == SQLITE_OK { if sqlite3_exec(db, deleteString, nil, nil, nil) == SQLITE_OK {
return true result = true
} else { } else {
let errmsg = String(cString: sqlite3_errmsg(db)!) let errmsg = String(cString: sqlite3_errmsg(db)!)
os_log("清空数据库失败: %{public}@", log: logger, type: .error, errmsg) os_log("清空数据库失败: %{public}@", log: logger, type: .error, errmsg)
return false
} }
} }
return result
}
/** /**
* 获取指定会话的最近N条消息 * 获取指定会话的最近N条消息
@ -321,7 +313,7 @@ public class ChatStorageHelper {
*/ */
public func getRecentMessages(agentId: String, limit: Int) -> [[String: Any]] { public func getRecentMessages(agentId: String, limit: Int) -> [[String: Any]] {
var messages: [[String: Any]] = [] var messages: [[String: Any]] = []
dbQueue.sync {
// 首先检查数据库中是否有该会话的消息 // 首先检查数据库中是否有该会话的消息
let countQuery = "SELECT COUNT(*) FROM messages WHERE agent_id = ?" let countQuery = "SELECT COUNT(*) FROM messages WHERE agent_id = ?"
var countStatement: OpaquePointer? var countStatement: OpaquePointer?
@ -389,7 +381,7 @@ public class ChatStorageHelper {
let errmsg = String(cString: sqlite3_errmsg(db)!) let errmsg = String(cString: sqlite3_errmsg(db)!)
os_log("查询最近消息失败: %{public}@", log: logger, type: .error, errmsg) os_log("查询最近消息失败: %{public}@", log: logger, type: .error, errmsg)
} }
}
return messages return messages
} }
} }

Loading…
Cancel
Save