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
}
// 关键修复:如果正在结束的是通话模式,则立即将会话重置为 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)
let success = azureAsrHelper?.stopContinuousRecognition() ?? false
if !success && isStartingRecognition {
if success {
// 识别结束后,立即停用 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
stopRequestedDuringStart = true
// 即使底层 stop 失败,但因为我们已经记录了停止请求,所以对调用方来说,可以认为是成功的
// 后续的 onSessionStarted 会处理真正的停止逻辑
}
stopIdleCheck()
// 只有之前在播放时才恢复播放
if wasMusicPlayingBeforeRecognition {
// QPlayAutoManager.shared.play()
wasMusicPlayingBeforeRecognition = false
MusicService.shared.resume()
}
// 重置识别模式
// currentRecognitionMode = "normal"
// 启动中但已请求停止,也视为已接受停止请求
return success || isStartingRecognition
return success || stopRequestedDuringStart
}
func 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() {
guard ensureStreamingEngineIfNeeded() else { return }
if #available(iOS 18.0, *) {
// iOS 18及以上版本,使用原始逻辑:先调度,后播放
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)
}
@ -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)
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
if isHeadphonesConnected {
desiredMode = .voiceChat
if #available(iOS 18.0, *) {
desiredOptions = [.allowBluetooth, .duckOthers]
} else {
desiredOptions = [.allowBluetooth, .allowBluetoothA2DP]
}
} else {
desiredMode = .videoChat
desiredOptions = [.allowBluetooth, .defaultToSpeaker]
@ -1404,7 +1462,14 @@ private func applyAudioSessionForTTS() -> Bool {
} else {
desiredCategory = .playback
desiredMode = .default
desiredOptions = [.duckOthers]
if #available(iOS 18.0, *) {
desiredOptions = []
} else {
desiredOptions = [.defaultToSpeaker]
}
if otherAudioPlaying {
desiredOptions.insert(.duckOthers)
}
}
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 logger = OSLog(subsystem: "com.yunqiinnovation.chat_storage", category: "ChatStorageHelper")
private let dbQueue = DispatchQueue(label: "com.yunqiinnovation.chat_storage.dbQueue")
private init() {
// 获取文档目录路径(不变)
let fileURL = try! FileManager.default
@ -23,6 +25,7 @@ public class ChatStorageHelper {
dbPath = fileURL.path
dbQueue.sync {
// 打开数据库(不变)
if sqlite3_open(dbPath, &db) != SQLITE_OK {
let errmsg = String(cString: sqlite3_errmsg(db)!)
@ -52,12 +55,15 @@ public class ChatStorageHelper {
os_log("创建表失败: %{public}@", log: logger, type: .error, errmsg)
}
}
}
deinit {
dbQueue.sync {
if db != nil {
sqlite3_close(db)
}
}
}
/**
* 保存消息到数据库
@ -68,6 +74,8 @@ public class ChatStorageHelper {
* @return 插入的消息ID,失败则返回-1
*/
public func saveMessage(agentId: String, sessionId: String, message: String, sender: String, metadata: String?) -> Int64 {
var result: Int64 = -1
dbQueue.sync {
// 关键修改:用 INSERT OR REPLACE 替换 INSERT,支持冲突时更新
let insertStatementString = """
INSERT OR REPLACE INTO messages
@ -95,21 +103,19 @@ public class ChatStorageHelper {
// 执行语句(冲突时会自动替换,返回新的 rowid)
if sqlite3_step(insertStatement) == SQLITE_DONE {
let id = sqlite3_last_insert_rowid(db) // 替换后返回新的 id(原 id 会被删除)
sqlite3_finalize(insertStatement)
os_log("消息保存成功(新增/更新),id: %{public}lld", log: logger, type: .info, id)
return id
result = id
} else {
let errmsg = String(cString: sqlite3_errmsg(db)!)
os_log("插入/更新消息失败: %{public}@", log: logger, type: .error, errmsg)
}
sqlite3_finalize(insertStatement)
} else {
let errmsg = String(cString: sqlite3_errmsg(db)!)
os_log("插入/更新消息语句准备失败: %{public}@", log: logger, type: .error, errmsg)
}
return -1
}
return result
}
/**
@ -120,6 +126,8 @@ public class ChatStorageHelper {
* @return 消息列表的JSON字符串
*/
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
var messagesArray: [[String: Any]] = []
@ -196,7 +204,7 @@ public class ChatStorageHelper {
sqlite3_finalize(queryStatement)
// 构建与Android版本一致的返回格式
let result: [String: Any] = [
let resultDict: [String: Any] = [
"messages": messagesArray,
"page": page,
"pageSize": pageSize,
@ -205,9 +213,9 @@ public class ChatStorageHelper {
]
os_log("getMessages: 读取历史记录: %{public}@", log: logger, type: .error, messagesArray)
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) {
return jsonString
result = jsonString
}
} catch {
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)!)
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 "{\"messages\":[],\"page\":\(page),\"pageSize\":\(pageSize),\"totalCount\":0,\"totalPages\":0}"
return result
}
/**
@ -245,6 +235,8 @@ public class ChatStorageHelper {
* @return 是否删除成功
*/
public func deleteMessages(agentId: String?, messageIds: [Int]?) -> Bool {
var result = false
dbQueue.sync {
if let agentId = agentId {
let deleteString = "DELETE FROM messages WHERE agent_id = ?;"
var deleteStatement: OpaquePointer?
@ -254,7 +246,7 @@ public class ChatStorageHelper {
if sqlite3_step(deleteStatement) == SQLITE_DONE {
sqlite3_finalize(deleteStatement)
return true
result = true
} else {
let errmsg = String(cString: sqlite3_errmsg(db)!)
os_log("删除会话消息失败: %{public}@", log: logger, type: .error, errmsg)
@ -277,13 +269,11 @@ public class ChatStorageHelper {
}
if sqlite3_step(deleteStatement) == SQLITE_DONE {
sqlite3_finalize(deleteStatement)
return true
result = true
} else {
let errmsg = String(cString: sqlite3_errmsg(db)!)
os_log("删除特定消息失败: %{public}@", log: logger, type: .error, errmsg)
}
sqlite3_finalize(deleteStatement)
} else {
let errmsg = String(cString: sqlite3_errmsg(db)!)
@ -291,10 +281,9 @@ public class ChatStorageHelper {
}
} else {
os_log("删除消息参数无效 - agentId和messageIds都为空", log: logger, type: .error)
return false
}
return false
}
return result
}
/**
@ -302,16 +291,19 @@ public class ChatStorageHelper {
* @return 是否清空成功
*/
public func clearDatabase() -> Bool {
var result = false
dbQueue.sync {
let deleteString = "DELETE FROM messages;"
if sqlite3_exec(db, deleteString, nil, nil, nil) == SQLITE_OK {
return true
result = true
} else {
let errmsg = String(cString: sqlite3_errmsg(db)!)
os_log("清空数据库失败: %{public}@", log: logger, type: .error, errmsg)
return false
}
}
return result
}
/**
* 获取指定会话的最近N条消息
@ -321,7 +313,7 @@ public class ChatStorageHelper {
*/
public func getRecentMessages(agentId: String, limit: Int) -> [[String: Any]] {
var messages: [[String: Any]] = []
dbQueue.sync {
// 首先检查数据库中是否有该会话的消息
let countQuery = "SELECT COUNT(*) FROM messages WHERE agent_id = ?"
var countStatement: OpaquePointer?
@ -389,7 +381,7 @@ public class ChatStorageHelper {
let errmsg = String(cString: sqlite3_errmsg(db)!)
os_log("查询最近消息失败: %{public}@", log: logger, type: .error, errmsg)
}
}
return messages
}
}

Loading…
Cancel
Save