Browse Source

优化按住说话,修复带耳机外放问题

weicu
fdp 1 year ago
parent
commit
81d77eab6b
  1. 19
      lib/modules/agent/views/agent_view.dart
  2. 78
      local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift
  3. 40
      local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServicePlugin.swift
  4. 26
      local_plugins/agent_service/ios/agent_service/Sources/agent_service/AudioSessionHub.swift
  5. 24
      local_plugins/agent_service/lib/agent_service.dart
  6. 5
      local_plugins/azure_speech/ios/azure_speech/Package.swift
  7. 237
      local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureAsrHelper.swift
  8. 40
      local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureSpeechPlugin.swift
  9. 79
      local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureTtsHelper.swift
  10. 195
      local_plugins/azure_speech/ios/azure_speech/Sources/tools/MicrophoneCapture.swift
  11. 18
      local_plugins/azure_speech/ios/azure_speech/Sources/tools/SimpleAudioReceiver.swift
  12. 2
      local_plugins/classic_bluetooth/ios/Classes/ClassicBluetoothPlugin.h
  13. 12
      local_plugins/classic_bluetooth/ios/Classes/ClassicBluetoothPlugin.m
  14. 23
      local_plugins/classic_bluetooth/ios/classic_bluetooth/Package.swift
  15. 164
      local_plugins/classic_bluetooth/ios/classic_bluetooth/Sources/classic_bluetooth/ClassicBluetoothHelper.swift
  16. 2
      local_plugins/classic_bluetooth/ios/classic_bluetooth/Sources/classic_bluetooth/SwiftClassicBluetoothPlugin.swift
  17. 2
      local_plugins/classic_bluetooth/pubspec.yaml

19
lib/modules/agent/views/agent_view.dart

@ -613,31 +613,12 @@ class _AgentViewState extends State<AgentView> with WidgetsBindingObserver {
// 语音输入模式 - 按住说话模式
return Listener(
onPointerDown: (_) {
// 设置300ms延迟防止误触
_holdTimer = Timer(
const Duration(milliseconds: 100),
() {
Haptics.vibrate(HapticsType.success);
controller.startPushToTalk();
// if (activeSpeaker == 0) {
// if (speakerId == 1) {
// controller
// .restoreOriginalAudioState();
// } else {
// controller
// .disableBluetoothAudio();
// }
// controller
// .setActiveSpeaker(speakerId);
// }
});
},
onPointerUp: (_) {
_holdTimer?.cancel();
controller.endPushToTalk();
},
onPointerCancel: (_) {
_holdTimer?.cancel();
controller.endPushToTalk();
},
// onLongPressStart: (_) {

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

@ -33,6 +33,9 @@ class AgentServiceImpl: NSObject {
private var azureSpeechRegion: String = ""
private var config: [String: Any] = [:]
// 添加音乐播放状态记录
private var wasMusicPlayingBeforeRecognition: Bool = false
private var isSpeaking: Bool = false
private var azureAsrHelper: AzureAsrHelper?
@ -62,6 +65,11 @@ class AgentServiceImpl: NSObject {
private var hasSpeechDetected: Bool = false
internal var isAiStreaming: Bool = false
// 新增:识别"启动中"与"启动中收到停止请求"状态
// 用途:解决 start 异步、stop 先于 onSessionStarted 到来时被忽略的问题
private var isStartingRecognition: Bool = false
private var stopRequestedDuringStart: Bool = false
var currsessionId: String = ""
var language: String = ""
private var serverurl = ""
@ -69,7 +77,7 @@ class AgentServiceImpl: NSObject {
private var idleTimer: DispatchSourceTimer?
private let maxIdleSeconds: TimeInterval = 10
var micCapture: MicrophoneCapture!
// 语音识别模式
private var currentRecognitionMode: String = "normal"
@ -81,8 +89,6 @@ class AgentServiceImpl: NSObject {
private override init() {
super.init()
audioPlayer = AudioPlayer()
azureAsrHelper = AzureAsrHelper()
azureTtsHelper = AzureTtsHelper()
@ -92,6 +98,9 @@ class AgentServiceImpl: NSObject {
UserPortrait.shared.initialize()
BleService.shared.setDelegate(self)
AudioSessionHub.initialize()
micCapture = MicrophoneCapture.shared
audioPlayer = AudioPlayer(agentService: self)
}
func addListener(_ listener: AgentServiceListener) {
@ -384,6 +393,15 @@ class AgentServiceImpl: NSObject {
stopRecognition()
}
// 检查当前音乐播放状态并记录
let currentPlayState = QPlayAutoManager.shared.currentPlayState
wasMusicPlayingBeforeRecognition = (currentPlayState == .playing)
// 如果音乐正在播放,则暂停
if wasMusicPlayingBeforeRecognition {
// QPlayAutoManager.shared.pause()
}
// 设置当前识别模式
currentRecognitionMode = mode
os_log("设置语音识别模式: %{public}@", log: logger, type: .info, mode)
@ -395,16 +413,22 @@ class AgentServiceImpl: NSObject {
} else {
//AudioSessionHub.shared.begin(.voice)
}
// 新增:标记为"启动中",用于允许 stop 在启动未完成时也能生效
isStartingRecognition = true
print("ai启动语音=\(audioSourceType)=\(useBle)")
guard let success = azureAsrHelper?.startContinuousRecognition(
// callback: self,
audioSourceType: audioSourceType
), success else {
// 新增:启动失败时复位"启动中"状态
isStartingRecognition = false
sendError("启动语音识别失败", code: "RECOGNITION_START_ERROR")
return false
}
// 根据模式启动相应的空闲检测
// 根据模式启动相应的空闲检测(此处 guard isRecognizing,会在 onSessionStarted 中再次启动)
startIdleCheckForMode(mode: mode)
return true
@ -421,22 +445,38 @@ class AgentServiceImpl: NSObject {
}
func stopRecognition() -> Bool {
if !isRecognizing {
/// 函数功能:停止语音识别
/// 特性:
/// 1) 即便处于"启动中"(isStartingRecognition)也会打印日志并尝试停止
/// 2) 如果底层尚未完全进入可停止状态,记录一次待停止请求,待会话启动后立即停止
if !(isRecognizing || isStartingRecognition) {
// 既不在识别中,也不在启动中,视为幂等处理
return true
}
os_log("停止语音识别,当前模式: %{public}@", log: logger, type: .info, currentRecognitionMode)
guard let success = azureAsrHelper?.stopContinuousRecognition(), success else {
return false
let success = azureAsrHelper?.stopContinuousRecognition() ?? false
if !success && isStartingRecognition {
// 启动尚未完成,先记录一次待停止请求,onSessionStarted 到来后立即 stop
stopRequestedDuringStart = true
}
stopIdleCheck()
// 只有之前在播放时才恢复播放
if wasMusicPlayingBeforeRecognition {
// QPlayAutoManager.shared.play()
wasMusicPlayingBeforeRecognition = false
}
BleService.shared.closeCodec()
// 重置识别模式
currentRecognitionMode = "normal"
return true
// 启动中但已请求停止,也视为已接受停止请求
return success || isStartingRecognition
}
func disableBluetoothAudio() {
azureAsrHelper?.disableBluetoothAudio()
@ -1493,9 +1533,16 @@ class AudioPlayer {
private let logger = OSLog(subsystem: "com.yunqiinnovation.agent_service", category: "AudioPlayer")
private let audioQueue = DispatchQueue(label: "agent.audio.player", qos: .userInitiated)
// 添加对AgentServiceImpl的弱引用
private weak var agentService: AgentServiceImpl?
// 等待音效超时时间(秒)- 改为 var 以支持动态修改
private var awaitTimeoutSeconds: TimeInterval = 30.0
// 修改初始化方法,接受AgentServiceImpl引用
init(agentService: AgentServiceImpl) {
self.agentService = agentService
}
private var resourcesBundle: Bundle {
#if SWIFT_PACKAGE
// 在 Swift Package 中使用 Bundle.module
@ -1548,6 +1595,8 @@ class AudioPlayer {
awaitPlayer?.stop()
awaitPlayer = nil
// 停止超时定时器
stopAwaitTimeout()
@ -1711,12 +1760,19 @@ extension AgentServiceImpl: AzureAsrHelper.ContinuousRecognizeCallback {
sendEvent(name: "recognition_started", data: ["status": "started"])
isRecognizing = true
hasSpeechDetected = false
// 新增:会话真正启动,复位"启动中"状态
isStartingRecognition = false
// 使用模式感知的空闲检测启动方法
startIdleCheckForMode(mode: currentRecognitionMode)
if (currentRecognitionMode == "ble_wakeup") {
audioPlayer?.playStartSound()
}
// 新增:若在启动期间已经收到停止请求,则立即执行停止
if stopRequestedDuringStart {
stopRequestedDuringStart = false
_ = stopRecognition()
}
}
func onSessionStopped() {
@ -1740,6 +1796,9 @@ extension AgentServiceImpl: AzureAsrHelper.ContinuousRecognizeCallback {
sendEvent(name: "recognition_canceled", data: data)
isRecognizing = false
// 新增:取消时复位"启动中/待停止"状态
isStartingRecognition = false
stopRequestedDuringStart = false
stopIdleCheck()
}
@ -1747,6 +1806,9 @@ extension AgentServiceImpl: AzureAsrHelper.ContinuousRecognizeCallback {
let data: [String: Any] = ["message": error.isEmpty ? "未知错误" : error]
sendEvent(name: "error", data: data)
isRecognizing = false
// 新增:出错时复位"启动中/待停止"状态
isStartingRecognition = false
stopRequestedDuringStart = false
stopIdleCheck()
}
}

40
local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServicePlugin.swift

@ -24,6 +24,34 @@ public class AgentServicePlugin: NSObject, FlutterPlugin {
// MARK: - FlutterPlugin 实现
/// 处理Flutter发来的方法调用
// MARK: - 防抖相关(iOS 原生层)
// 函数级注释:用于限制 startConversation/stopConversation 在极短时间内的重复触发
private var lastStartConversationAt: TimeInterval = 0
private var lastStopConversationAt: TimeInterval = 0
private let conversationDebounceInterval: TimeInterval = 0.6
/// 判断是否允许处理“开始对话”的调用(防抖)
/// 返回值:true 允许执行,false 表示在防抖冷却时间内被拦截
private func shouldHandleStartConversation() -> Bool {
let now = Date().timeIntervalSince1970
if now - lastStartConversationAt < conversationDebounceInterval {
return false
}
lastStartConversationAt = now
return true
}
/// 判断是否允许处理“停止对话”的调用(防抖)
/// 返回值:true 允许执行,false 表示在防抖冷却时间内被拦截
private func shouldHandleStopConversation() -> Bool {
let now = Date().timeIntervalSince1970
if now - lastStopConversationAt < conversationDebounceInterval {
return false
}
lastStopConversationAt = now
return true
}
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
switch call.method {
case "startAgentService":
@ -41,6 +69,12 @@ public class AgentServicePlugin: NSObject, FlutterPlugin {
result(success)
case "startConversation":
// 防抖:短时间重复调用直接忽略
guard shouldHandleStartConversation() else {
result(true)
return
}
print("startConversation")
let mode = (call.arguments as? [String: Any])?["mode"] as? String ?? "normal"
impl.startRecognition(mode: mode)
result(true)
@ -50,6 +84,12 @@ public class AgentServicePlugin: NSObject, FlutterPlugin {
result(true)
case "stopConversation":
// 防抖:短时间重复调用直接忽略
guard shouldHandleStopConversation() else {
result(true)
return
}
print("stopConversation")
impl.stopRecognition()
result(true)

26
local_plugins/agent_service/ios/agent_service/Sources/agent_service/AudioSessionHub.swift

@ -323,19 +323,19 @@ public class AudioSessionHub {
public func appDidEnterBackground() {
os_log("📱 应用进入后台,准备释放音频会话", log: logger, type: .info)
do {
try audioSession.setActive(false, options: .notifyOthersOnDeactivation)
os_log("✓ 音频会话已释放(应用进入后台)", log: logger, type: .info)
} catch let error as NSError {
let osStatusCode = error.code
let domain = error.domain
let description = error.localizedDescription
os_log("❌ 释放音频会话失败", log: logger, type: .error)
os_log(" - 错误域: %{public}@", log: logger, type: .error, domain)
os_log(" - OSStatus代码: %d (0x%X)", log: logger, type: .error, osStatusCode, osStatusCode)
os_log(" - 错误描述: %{public}@", log: logger, type: .error, description)
}
// do {
// try audioSession.setActive(false, options: .notifyOthersOnDeactivation)
// os_log("✓ 音频会话已释放(应用进入后台)", log: logger, type: .info)
// } catch let error as NSError {
// let osStatusCode = error.code
// let domain = error.domain
// let description = error.localizedDescription
// os_log("❌ 释放音频会话失败", log: logger, type: .error)
// os_log(" - 错误域: %{public}@", log: logger, type: .error, domain)
// os_log(" - OSStatus代码: %d (0x%X)", log: logger, type: .error, osStatusCode, osStatusCode)
// os_log(" - 错误描述: %{public}@", log: logger, type: .error, description)
// }
}
/// 获取当前音频路由信息

24
local_plugins/agent_service/lib/agent_service.dart

@ -240,7 +240,22 @@ class AgentService {
/// 启动语音识别,开始监听用户语音输入
/// [mode] 语音识别模式:'ble_wakeup'(BLE唤醒), 'phone_call'(通话模式), 'push_to_talk'(按住说话), 'normal'(普通模式)
/// 返回是否成功开始对话
// 防抖相关静态字段(全局统一防抖)
// 说明:用于限制 startConversation/stopConversation 在极短时间内的重复触发
static DateTime? _lastStartConversationAt;
static DateTime? _lastStopConversationAt;
static const Duration _conversationDebounce = Duration(milliseconds: 600);
static Future<bool> startConversation({String mode = 'normal'}) async {
// 防抖:短时间内重复点击直接忽略(冷却式防抖)
final now = DateTime.now();
if (_lastStartConversationAt != null &&
now.difference(_lastStartConversationAt!) < _conversationDebounce) {
// 函数级注释:当在冷却时间内再次调用时,直接返回 false 表示此次调用被防抖拦截
return false;
}
_lastStartConversationAt = now;
try {
final bool result = await _channel.invokeMethod('startConversation', {
'mode': mode,
@ -296,6 +311,15 @@ class AgentService {
/// 停止语音识别
/// 返回是否成功停止对话
static Future<bool> stopConversation() async {
// 防抖:短时间内重复点击直接忽略(冷却式防抖)
final now = DateTime.now();
if (_lastStopConversationAt != null &&
now.difference(_lastStopConversationAt!) < _conversationDebounce) {
// 函数级注释:当在冷却时间内再次调用时,直接返回 false 表示此次调用被防抖拦截
return false;
}
_lastStopConversationAt = now;
try {
final bool result = await _channel.invokeMethod('stopConversation');
return result;

5
local_plugins/azure_speech/ios/azure_speech/Package.swift

@ -11,7 +11,9 @@ let package = Package(
// 依赖speech接口模块
.package(path: "../../../speech/ios/speech"),
// 添加对ble_service的依赖
.package(path: "../../../ble_service/ios/ble_service")
.package(path: "../../../ble_service/ios/ble_service"),
// 添加对classic_bluetooth的依赖
.package(path: "../../../classic_bluetooth/ios/classic_bluetooth")
],
targets: [
.target(
@ -19,6 +21,7 @@ let package = Package(
dependencies: [
.product(name: "speech", package: "speech"),
.product(name: "ble-service", package: "ble_service"),
.product(name: "classic-bluetooth", package: "classic_bluetooth"),
"MicrosoftCognitiveServicesSpeech"
],
path: "Sources", // 修改为包含整个 Sources 目录

237
local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureAsrHelper.swift

@ -37,6 +37,11 @@ public class AzureAsrHelper: NSObject {
private var lastNetworkLostTime: Int64 = 0
private let networkLostDebounceInterval: Int64 = 3000 // 3秒防抖间隔
// 防抖机制相关变量 - 只保留开始操作的防抖
private var lastAudioStartTime: Int64 = 0
private let audioOperationDebounceInterval: Int64 = 500 // 500毫秒防抖间隔
private var audioStartWorkItem: DispatchWorkItem?
// 状态管理
private var _isContinuousRecognitionActive = false
@ -216,11 +221,137 @@ public class AzureAsrHelper: NSObject {
/**
* 开始连续语音识别
* 异步启动音频录制(带防抖与状态二次校验)
*
* @param audioSourceType 音频源类型
* @param audioDataCallback 音频数据回调
* @param completion 完成回调(true/false)
*/
/// 异步启动音频录制(带防抖与状态二次校验)
/// - 参数:
/// - audioSourceType: 音频源类型
/// - audioDataCallback: 音频数据回调
/// - completion: 完成回调(true/false)
///
/// 修复点:
/// 1) 新任务创建前,取消旧的未执行任务,确保只有“最后一次启动请求”会生效
/// 2) 任务执行前二次校验:必须是当前挂起任务且 _isContinuousRecognitionActive 仍为 true 才执行启动
private func startAudioRecordAsync(
audioSourceType: AudioSourceType,
audioDataCallback: SimpleAudioReceiver.AudioDataCallback? = nil,
completion: @escaping (Bool) -> Void
) {
let currentTime = Int64(Date().timeIntervalSince1970 * 1000)
// 防抖:距离上次启动过短,取消旧任务
if currentTime - lastAudioStartTime < audioOperationDebounceInterval {
audioStartWorkItem?.cancel()
os_log("音频启动操作被防抖过滤,距离上次启动仅%{public}dms", log: log, type: .debug, currentTime - lastAudioStartTime)
} else {
// 即使未触发防抖,也保证旧任务不会残留
audioStartWorkItem?.cancel()
}
lastAudioStartTime = currentTime
// 创建新的启动任务
let workItem = DispatchWorkItem { [weak self] in
guard let self = self else {
completion(false)
return
}
print("startAudioRecordAsync\(_isContinuousRecognitionActive)")
// 二次校验:stop 可能已发生,确认仍需启动
guard self._isContinuousRecognitionActive else {
os_log("已收到停止请求,取消启动音频录制", log: self.log, type: .info)
completion(false)
return
}
os_log("开始异步启动音频录制", log: self.log, type: .info)
// 在后台队列执行耗时的音频启动操作
DispatchQueue.global(qos: .userInitiated).async {
let success = self.performAudioStart(audioSourceType: audioSourceType, audioDataCallback: audioDataCallback)
// 回到主队列执行完成回调
DispatchQueue.main.async {
completion(success)
}
}
}
// 保存"当前挂起任务"
audioStartWorkItem = workItem
// 延迟执行以实现防抖
DispatchQueue.main.asyncAfter(
deadline: .now() + .milliseconds(Int(audioOperationDebounceInterval)),
execute: workItem
)
}
/**
* 执行音频启动操作
*/
private func performAudioStart(
audioSourceType: AudioSourceType,
audioDataCallback: SimpleAudioReceiver.AudioDataCallback?
) -> Bool {
do {
audioStream?.startAudioRecord(
audioSourceType: audioSourceType == .microphone ? .microphone : .external,
audioDataCallback: audioDataCallback
)
os_log("音频录制启动成功", log: log, type: .info)
return true
} catch {
os_log("音频录制启动失败: %{public}@", log: log, type: .error, error.localizedDescription)
return false
}
}
/**
* 异步停止音频录制(移除防抖机制,立即执行)
*
* @param completion 完成回调
*/
private func stopAudioRecordAsync(completion: @escaping (Bool) -> Void) {
os_log("开始立即停止音频录制", log: log, type: .info)
// 在后台队列执行耗时的音频停止操作
DispatchQueue.global(qos: .userInitiated).async {
let success = self.performAudioStop()
// 回到主队列执行完成回调
DispatchQueue.main.async {
completion(success)
}
}
}
/**
* 执行音频停止操作
*/
private func performAudioStop() -> Bool {
do {
audioStream?.stopMicrophoneCapture()
os_log("音频录制停止成功", log: log, type: .info)
return true
} catch {
os_log("音频录制停止失败: %{public}@", log: log, type: .error, error.localizedDescription)
return false
}
}
/**
* 开始连续语音识别(优化:在音频启动成功后再次校验活跃状态)
*
* @param callback 连续识别结果回调
* @param audioSourceType 音频源类型
* @return 是否成功开始识别
* @param audioDataCallback 音频数据回调
* @return 是否成功开始识别(调度成功即返回 true)
*
* 修复点:在音频启动成功后的闭包中,先判断 _isContinuousRecognitionActive 是否仍然为 true,
* 若 stop 已发生则直接跳过 recognizer 的启动,避免“已停止但仍启动识别”的情况。
*/
public func startContinuousRecognition(
audioSourceType: AudioSourceType = .microphone,
@ -241,8 +372,12 @@ public class AzureAsrHelper: NSObject {
return true
}
self.audioSourceType = audioSourceType
// 先取消任何挂起的启动任务
cancelPendingAudioStart()
// 延迟设置状态,直到音频启动任务真正开始执行
self.audioSourceType = audioSourceType
print("startContinuousRecognition:准备启动")
do {
if recognizer == nil {
@ -253,33 +388,45 @@ public class AzureAsrHelper: NSObject {
}
print("startContinuousRecognition:\(audioSourceType)")
// 启动音频处理
audioStream?.startAudioRecord(audioSourceType: audioSourceType == .microphone ? .microphone : .external, audioDataCallback: audioDataCallback)
// 只有在音频启动成功后才设置状态为 true
self._isContinuousRecognitionActive = true
// 异步启动音频处理
startAudioRecordAsync(audioSourceType: audioSourceType, audioDataCallback: audioDataCallback) { [weak self] (success: Bool) in
guard let self = self else { return }
// 开始连续识别
try recognizer?.startContinuousRecognition()
_isContinuousRecognitionActive = true
if success {
// 只有在音频启动成功后才设置状态为 true
self._isContinuousRecognitionActive = true
print("startContinuousRecognition:\(self._isContinuousRecognitionActive)")
os_log("连续识别已启动,音频源: %{public}@", log: log, type: .info, audioSourceType == .microphone ? "麦克风" : "外部")
// 启动语音识别
do {
try self.recognizer?.startContinuousRecognition()
os_log("连续识别已启动,音频源: %{public}@", log: self.log, type: .info, audioSourceType == .microphone ? "麦克风" : "外部")
} catch {
os_log("启动连续识别失败: %{public}@", log: self.log, type: .error, error.localizedDescription)
self.stopContinuousRecognition()
self._isContinuousRecognitionActive = false
}
} else {
os_log("音频启动失败,无法开始识别", log: self.log, type: .error)
self._isContinuousRecognitionActive = false
}
}
return true
} catch {
stopContinuousRecognition()
_isContinuousRecognitionActive = false
//callback.onError("启动连续识别失败: \(error.localizedDescription)")
return false
}
}
/**
* 停止连续语音识别
*
* @return 停止操作是否成功
*/
/**
* 停止连续语音识别
* @return 停止操作是否成功
* 停止连续语音识别(优化:确保取消未执行的启动任务)
* - 返回: 停止操作是否成功
*/
public func stopContinuousRecognition() -> Bool {
guard speechConfig != nil else {
@ -288,17 +435,29 @@ public class AzureAsrHelper: NSObject {
}
guard let recognizer = recognizer else {
os_log("识别器为空,重置状态", log: log, type: .info)
return true
}
if !_isContinuousRecognitionActive {
// 仍需取消潜在的挂起启动任务,避免稍后被触发
cancelPendingAudioStart()
return true
}
do {
_isContinuousRecognitionActive = false
audioStream?.stopMicrophoneCapture()
// 关键:取消尚未执行的启动任务,防止 stop 后音频又被启动
cancelPendingAudioStart()
// 异步停止音频处理
stopAudioRecordAsync { [weak self] (success: Bool) in
guard let self = self else { return }
if success {
// 标记不再活跃,供启动任务二次校验
self._isContinuousRecognitionActive = false
os_log("音频停止成功", log: self.log, type: .info)
} else {
os_log("音频停止失败", log: self.log, type: .error)
}
}
// 停止连续识别
try recognizer.stopContinuousRecognition()
self.recognizer = nil
@ -307,17 +466,23 @@ public class AzureAsrHelper: NSObject {
} catch {
// 强制重置状态
_isContinuousRecognitionActive = false
// 关键:即使异常,也取消未执行的启动任务
cancelPendingAudioStart()
os_log("强制停止识别失败", log: log, type: .error)
// 停止音频处理
audioStream?.stopMicrophoneCapture()
// 异步停止音频处理
stopAudioRecordAsync { [weak self] (_: Bool) in
guard let self = self else { return }
os_log("强制停止音频处理完成", log: self.log, type: .info)
}
// 【修复】在 catch 块中安全地停止识别器,避免再次抛出异常
// 尝试安全地停止识别器
do {
try recognizer.stopContinuousRecognition()
self.recognizer = nil
} catch {
// 如果强制停止也失败,记录错误但不再抛出异常
os_log("强制停止识别器失败: %@", log: log, type: .error, error.localizedDescription)
}
@ -333,7 +498,7 @@ public class AzureAsrHelper: NSObject {
}
/**
* 释放所有资源
* 释放资源
*/
public func dispose() {
do {
@ -344,7 +509,8 @@ public class AzureAsrHelper: NSObject {
try? recognizer?.stopContinuousRecognition()
_isContinuousRecognitionActive = false
}
// 取消防抖任务 - 只取消开始操作的防抖
audioStartWorkItem?.cancel()
// 停止音频处理
audioStream?.releaseAudioResources()
// 释放网络监听资源
@ -853,6 +1019,15 @@ extension AzureAsrHelper: NetworkStateMonitor.NetworkStateListener {
self.handleNetworkLost()
}
// .MARK: - 启动任务管理
/// 取消尚未执行的音频启动任务,防止 stop 后仍然触发启动
private func cancelPendingAudioStart() {
audioStartWorkItem?.cancel()
audioStartWorkItem = nil
}
}

40
local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureSpeechPlugin.swift

@ -20,6 +20,34 @@ import ble_service
// 日志对象
private let log = OSLog(subsystem: "com.azure.speech", category: "AzureSpeechPlugin")
// MARK: - 防抖相关(iOS 原生层)
// 函数级注释:用于限制 startContinuousRecognition/stopContinuousRecognition 在极短时间内的重复触发
private var lastStartRecognitionAt: TimeInterval = 0
private var lastStopRecognitionAt: TimeInterval = 0
private let recognitionDebounceInterval: TimeInterval = 0.6
/// 判断是否允许处理"开始连续识别"的调用(防抖)
/// 返回值:true 允许执行,false 表示在防抖冷却时间内被拦截
private func shouldHandleStartRecognition() -> Bool {
let now = Date().timeIntervalSince1970
if now - lastStartRecognitionAt < recognitionDebounceInterval {
return false
}
lastStartRecognitionAt = now
return true
}
/// 判断是否允许处理"停止连续识别"的调用(防抖)
/// 返回值:true 允许执行,false 表示在防抖冷却时间内被拦截
private func shouldHandleStopRecognition() -> Bool {
let now = Date().timeIntervalSince1970
if now - lastStopRecognitionAt < recognitionDebounceInterval {
return false
}
lastStopRecognitionAt = now
return true
}
// 替换原有的 StreamHandler 类
private typealias AsrEventStreamHandler = BaseEventStreamHandler
private typealias TtsEventStreamHandler = BaseEventStreamHandler
@ -263,6 +291,12 @@ private func sendAstEvent(_ event: [String: Any]) {
result(FlutterError(code: "NOT_SUPPORTED", message: "iOS版本不支持recognizeOnce", details: nil))
case "startContinuousRecognition":
// 防抖:短时间重复调用直接忽略
guard shouldHandleStartRecognition() else {
result(true)
return
}
// 确保事件通道已准备好
guard asrEventSink != nil else {
result(FlutterError(code: "EVENT_CHANNEL_NOT_READY", message: "事件通道未准备好,无法开始连续识别", details: nil))
@ -311,7 +345,13 @@ private func sendAstEvent(_ event: [String: Any]) {
return
case "stopContinuousRecognition":
// 防抖:短时间重复调用直接忽略
guard shouldHandleStopRecognition() else {
result(true)
return
}
print("stopContinuousRecognition")
let success = azureAsrHelper.stopContinuousRecognition()
currentAsrCallback = nil
result(success)

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

@ -33,6 +33,11 @@ public class AzureTtsHelper: NSObject, ITtsService {
private var streamBuffer = ""
private var lastSpeakTime: TimeInterval = 0
// 添加文本计数统计变量
private var totalTextCount = 0
private var currentSessionTextCount = 0
private var pendingTextCount = 0
// 内部异步处理队列
private let synthesisQueue = DispatchQueue(label: "com.azure.tts.synthesis", qos: .userInitiated)
private var isProcessing = false // 添加处理状态标志
@ -43,6 +48,7 @@ public class AzureTtsHelper: NSObject, ITtsService {
// 自定义音频输出流
private var customAudioOutputStream: SPXPushAudioOutputStream?
private var useInternalPlayer = true
private var micCapture: MicrophoneCapture!
// 记录最后播放的文本
private var lastSpokenText: String?
/**
@ -50,6 +56,7 @@ public class AzureTtsHelper: NSObject, ITtsService {
*/
public func initialize(ttsAppId: String, ttsAppToken: String, ttsResource: String, language: String) -> Bool {
do {
micCapture = MicrophoneCapture.shared
// 创建语音配置
speechConfig = try SPXSpeechConfiguration(subscription: ttsAppToken, region: ttsResource)
speechConfig?.speechSynthesisLanguage = language
@ -110,10 +117,20 @@ public class AzureTtsHelper: NSObject, ITtsService {
}
}
/**
* 开始语音合成会话
* @param sessionid 会话ID
* @return 是否成功启动
*/
public func startspeak(sessionid sessionid: String)-> Bool {
self.sessionid = sessionid
// 重置当前会话的文本计数
self.currentSessionTextCount = 0
self.pendingTextCount = 0
return true
}
/**
* 单次合成并播放
*/
@ -131,6 +148,7 @@ public class AzureTtsHelper: NSObject, ITtsService {
return false
}
// 清理文本
let cleanedText = cleanTextForTTS(text)
if cleanedText.isEmpty {
@ -165,6 +183,12 @@ public class AzureTtsHelper: NSObject, ITtsService {
return true
}
// 增加文本计数统计
totalTextCount += text.count
currentSessionTextCount += text.count
print("流式合成文本\(text.count),总计文本数量:\(totalTextCount),当前会话:\(currentSessionTextCount)")
// 清理文本并添加到缓冲区
let cleanedText = cleanTextForTTS(text)
streamBuffer.append(cleanedText)
@ -234,6 +258,8 @@ public class AzureTtsHelper: NSObject, ITtsService {
sessionid = ""
streamBuffer = ""
lastSpokenText = ""
// 重置计数器
pendingTextCount = 0
taskLock.lock()
pendingTasks.removeAll()
taskLock.unlock()
@ -242,6 +268,13 @@ public class AzureTtsHelper: NSObject, ITtsService {
print("liwei-------------停止语音合成播放")
try self.synthesizer?.stopSpeaking()
self.notifyEvent(eventType: .synthesisCanceled)
if !micCapture.isCapturing{
// 停止时设置音频会话为非活跃状态
try AVAudioSession.sharedInstance().setActive(false)
}
print("音频会话已设置为非活跃状态")
} catch {
os_log("停止语音合成失败: %{public}@", log: self.log, type: .error, error.localizedDescription)
self.notifyEvent(eventType: .error, params: [
@ -429,6 +462,8 @@ public class AzureTtsHelper: NSObject, ITtsService {
}
} catch {
speaking = false
// 减少待处理文本计数
pendingTextCount = max(0, pendingTextCount - text.count)
os_log("语音合成失败: %{public}@", log: log, type: .error, error.localizedDescription)
notifyEvent(eventType: .error, params: [
"errorCode": "SYNTHESIS_FAILED",
@ -481,14 +516,31 @@ public class AzureTtsHelper: NSObject, ITtsService {
synthesizer?.addSynthesisCompletedEventHandler { [weak self] _, _ in
guard let self = self else { return }
self.speaking = false
// 减少待处理文本计数
self.pendingTextCount = max(0, self.pendingTextCount - 1)
self.notifyEvent(eventType: .synthesisCompleted)
self.notifyEvent(eventType: .playbackCompleted)
// 检查是否为最后一个文本播放完毕
if self.pendingTextCount == 0 && self.pendingTasks.isEmpty && !micCapture.isCapturing{
do {
try AVAudioSession.sharedInstance().setActive(false)
print("最后一个文本播放完毕,音频会话已设置为非活跃状态")
} catch {
print("设置音频会话为非活跃状态失败: \(error.localizedDescription)")
}
}
}
synthesizer?.addSynthesisCanceledEventHandler { [weak self] _, event in
guard let self = self else { return }
self.speaking = false
// 减少待处理文本计数
self.pendingTextCount = max(0, self.pendingTextCount - 1)
var params: [String: Any] = [:]
if let details = try? SPXSpeechSynthesisCancellationDetails(fromCanceledSynthesisResult: event.result) {
params["errorCode"] = details.errorCode
@ -604,4 +656,31 @@ public class AzureTtsHelper: NSObject, ITtsService {
public func isSpeaking() -> Bool {
return speaking
}
/**
* 获取总计文本数量
*/
public func getTotalTextCount() -> Int {
return totalTextCount
}
/**
* 获取当前会话文本数量
*/
public func getCurrentSessionTextCount() -> Int {
return currentSessionTextCount
}
/**
* 重置文本计数统计
*/
public func resetTextCount() {
totalTextCount = 0
currentSessionTextCount = 0
pendingTextCount = 0
}
}

195
local_plugins/azure_speech/ios/azure_speech/Sources/tools/MicrophoneCapture.swift

@ -1,21 +1,23 @@
import AVFoundation
import classic_bluetooth // 添加这行导入语句
class MicrophoneCapture: NSObject {
// 将 class 改为 public class
public class MicrophoneCapture: NSObject {
// MARK: - 单例实例
/**
* 单例实例,确保整个应用中只有一个音频捕获对象
* 避免多个实例同时访问麦克风资源导致冲突
*/
static let shared = MicrophoneCapture()
public static let shared = MicrophoneCapture()
private var audioEngine: AVAudioEngine!
private var audioSession: AVAudioSession!
private var audioFormat: AVAudioFormat?
private var audioInputNode: AVAudioInputNode!
private var sampleRate: Double = 16000 // 修改为16000采样率,符合Microsoft要求
private var isCapturing = false
public var isCapturing = false
public var hasBluetoothDevices = false
// 音频数据回调
var audioDataHandler: ((Data) -> Void)?
/**
@ -25,6 +27,41 @@ class MicrophoneCapture: NSObject {
private override init() {
super.init()
setupAudioSession()
// 监听音频路由变化
NotificationCenter.default.addObserver(
self,
selector: #selector(audioRouteChanged),
name: AVAudioSession.routeChangeNotification,
object: nil
)
}
/**
* 音频路由变化处理
* 当用户插拔耳机时自动调整音频路由
*/
@objc private func audioRouteChanged(notification: Notification) {
guard let userInfo = notification.userInfo,
let reasonValue = userInfo[AVAudioSessionRouteChangeReasonKey] as? UInt,
let reason = AVAudioSession.RouteChangeReason(rawValue: reasonValue) else {
return
}
switch reason {
case .newDeviceAvailable, .oldDeviceUnavailable:
// 设备连接或断开时重新配置音频路由
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
self.configureAudioRoute()
}
default:
break
}
}
deinit {
stopCapture()
NotificationCenter.default.removeObserver(self)
}
/// 获取最佳音频格式 (iOS 通常支持标准采样率)
@ -48,23 +85,12 @@ class MicrophoneCapture: NSObject {
audioSession = AVAudioSession.sharedInstance()
do {
// 设置音频会话参数
try audioSession.setCategory(.playAndRecord,
mode: .videoChat,
options: [//.allowBluetooth, // 允许蓝牙耳机,占用hfp链路
// .allowAirPlay,
.allowBluetoothA2DP,// 允许蓝牙耳机,不占用hfp链路
.mixWithOthers,
.defaultToSpeaker
])
try audioSession.setPreferredSampleRate(sampleRate)
try audioSession.setPreferredIOBufferDuration(0.005) // 5ms缓冲
// 设置蓝牙设备优先级
if let bluetoothInput = audioSession.availableInputs?.first(where: { $0.portType == .bluetoothHFP }) {
try audioSession.setPreferredInput(bluetoothInput)
}
// 优先设置蓝牙音频路由
configureAudioRoute()
try audioSession.setActive(true)
print("音频会话配置成功")
@ -73,14 +99,107 @@ class MicrophoneCapture: NSObject {
}
}
/**
* 配置音频输入输出路由
* 根据蓝牙连接状态自动配置音频路由
*/
private func configureAudioRoute() {
// 检查是否有蓝牙音频设备连接
let bluetoothHelper = ClassicBluetoothHelper.shared
self.hasBluetoothDevices = !bluetoothHelper.getConnectedAudioDevices().isEmpty
if self.hasBluetoothDevices {
// 有蓝牙连接:末端在手机,输出端在耳机
configureAudioForBluetoothMode()
} else {
// 无蓝牙连接:末端和输出端都在手机
configureAudioForPhoneMode()
}
}
/**
* 配置蓝牙模式的音频路由:末端在手机,输出端在耳机
*/
private func configureAudioForBluetoothMode() {
guard let availableInputs = audioSession.availableInputs else { return }
// 强制使用内置麦克风作为输入(手机)
if let builtInMic = availableInputs.first(where: { $0.portType == .builtInMic }) {
do {
try audioSession.setPreferredInput(builtInMic)
print("蓝牙模式:音频输入设置为内置麦克风(手机)")
} catch {
print("设置内置麦克风失败: \(error.localizedDescription)")
}
}
// 检查当前音频输出路由
let currentRoute = audioSession.currentRoute
print("当前音频输出路由: \(currentRoute.outputs.map { $0.portType.rawValue })")
// 确保音频输出通过蓝牙耳机
let hasBluetoothOutput = currentRoute.outputs.contains { output in
[.bluetoothHFP, .bluetoothLE].contains(output.portType)
}
if hasBluetoothOutput {
// 取消扬声器强制输出,让音频通过蓝牙耳机输出
do {
// 设置音频会话参数
try audioSession.setCategory(.playAndRecord,
mode: .videoChat,
options: [.allowBluetoothA2DP, // 允许蓝牙耳机,不占用hfp链路
.mixWithOthers,
.allowBluetooth]) // 添加音频优先级控制
try audioSession.overrideOutputAudioPort(.none)
print("蓝牙模式:音频输出设置为蓝牙耳机")
} catch {
print("设置蓝牙音频输出失败: \(error.localizedDescription)")
}
}
}
/**
* 配置手机模式的音频路由:末端和输出端都在手机
*/
private func configureAudioForPhoneMode() {
guard let availableInputs = audioSession.availableInputs else { return }
// 使用内置麦克风作为输入(手机)
if let builtInMic = availableInputs.first(where: { $0.portType == .builtInMic }) {
do {
// 设置音频会话参数
try audioSession.setCategory(.playAndRecord,
mode: .videoChat,
options: [.allowBluetoothA2DP, // 允许蓝牙耳机,不占用hfp链路
.mixWithOthers,
.defaultToSpeaker]) // 添加音频优先级控制
try audioSession.setPreferredInput(builtInMic)
print("手机模式:音频输入设置为内置麦克风(手机)")
} catch {
print("设置内置麦克风失败: \(error.localizedDescription)")
}
}
// 强制音频输出通过扬声器(手机)
do {
try audioSession.overrideOutputAudioPort(.speaker)
print("手机模式:音频输出设置为扬声器(手机)")
} catch {
print("设置扬声器输出失败: \(error.localizedDescription)")
}
}
// MARK: - 开始捕获
/**
* 开始音频捕获
* 检查麦克风权限并启动音频引擎
*/
public func startCapture() throws {
print("开始捕获\(isCapturing)")
guard !isCapturing else { return }
print("开始捕获")
// 检查麦克风权限
switch audioSession.recordPermission {
case .granted:
@ -149,6 +268,7 @@ class MicrophoneCapture: NSObject {
try audioInputNode.setVoiceProcessingEnabled(true)
}
// 检查输入格式是否符合Microsoft要求
let needsConversion = !isMicrosoftCompatibleFormat(inputFormat)
@ -295,11 +415,38 @@ class MicrophoneCapture: NSObject {
*/
func stopCapture() {
guard isCapturing else { return }
audioEngine.stop()
audioInputNode.removeTap(onBus: 0)
// try? audioSession.setActive(false)
isCapturing = false
// 安全地停止音频引擎
if let engine = audioEngine {
engine.stop()
}
// 安全地移除音频处理块
if let inputNode = audioInputNode {
inputNode.removeTap(onBus: 0)
}
try? audioSession.setActive(false)
if self.hasBluetoothDevices {
// 设置音频会话参数
try? audioSession.setCategory(.playback,
mode: .videoChat,
options: [
.allowBluetoothA2DP,
.allowBluetooth
]) // 添加音频优先级控制
} else {
try? audioSession.setCategory(.playback,
mode: .videoChat,
options: [
.mixWithOthers,
.defaultToSpeaker])
}
try? audioSession.overrideOutputAudioPort(.none)
try? audioSession.setActive(true)
}
/**
@ -311,7 +458,5 @@ class MicrophoneCapture: NSObject {
audioDataHandler = nil
}
deinit {
stopCapture()
}
}

18
local_plugins/azure_speech/ios/azure_speech/Sources/tools/SimpleAudioReceiver.swift

@ -183,6 +183,13 @@ public class SimpleAudioReceiver: NSObject {
do {
// 确保正在写入状态
guard self._isWriting else { return }
// 设置音频会话参数
try audioSession.setCategory(.playAndRecord,
mode: .videoChat,
options: [.allowBluetoothA2DP, // 允许蓝牙耳机,不占用hfp链路
.mixWithOthers,
.defaultToSpeaker]) // 添加音频优先级控制
try audioSession.setActive(true)
try micCapture.startCapture()
try micCapture.audioDataHandler = { data in
self.writeQueue.put(data)
@ -203,6 +210,17 @@ public class SimpleAudioReceiver: NSObject {
private func runExternalCapture() {
if audioSourceType == .external {
do {
try audioSession.setCategory(.playback,
mode: .videoChat,
options: [ .mixWithOthers,.allowBluetoothA2DP // 允许蓝牙耳机,不占用hfp链路
]) // 添加音频优先级控制
try audioSession.overrideOutputAudioPort(.none)
try audioSession.setActive(true)
} catch {
print("设置音频会话失败: \(error.localizedDescription)")
}
//pushAudioData(data: Data())
micCapture.stopCapture()

2
local_plugins/classic_bluetooth/ios/Classes/ClassicBluetoothPlugin.h

@ -1,4 +1,4 @@
#import <Flutter/Flutter.h>
@interface ClassicBluetoothPlugin : NSObject<FlutterPlugin>
@interface SwiftClassicBluetoothPlugin : NSObject<FlutterPlugin>
@end

12
local_plugins/classic_bluetooth/ios/Classes/ClassicBluetoothPlugin.m

@ -1,12 +0,0 @@
#import "ClassicBluetoothPlugin.h"
#if __has_include(<classic_bluetooth/classic_bluetooth-Swift.h>)
#import <classic_bluetooth/classic_bluetooth-Swift.h>
#else
#import "classic_bluetooth-Swift.h"
#endif
@implementation ClassicBluetoothPlugin
+ (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar>*)registrar {
[SwiftClassicBluetoothPlugin registerWithRegistrar:registrar];
}
@end

23
local_plugins/classic_bluetooth/ios/classic_bluetooth/Package.swift

@ -0,0 +1,23 @@
// swift-tools-version: 5.9
import PackageDescription
let package = Package(
name: "classic_bluetooth",
platforms: [.iOS("16.0")],
products: [
.library(
name: "classic-bluetooth",
targets: ["classic_bluetooth"]
)
],
dependencies: [
// 如果需要其他依赖,在这里添加
],
targets: [
.target(
name: "classic_bluetooth",
dependencies: [],
path: "Sources"
)
]
)

164
local_plugins/classic_bluetooth/ios/Classes/ClassicBluetoothHelper.swift → local_plugins/classic_bluetooth/ios/classic_bluetooth/Sources/classic_bluetooth/ClassicBluetoothHelper.swift

@ -3,14 +3,14 @@ import AVFoundation
import CoreBluetooth
/// 蓝牙事件类型
enum BluetoothEventType {
public enum BluetoothEventType {
case stateChanged
case deviceConnected
case deviceDisconnected
}
/// 蓝牙状态枚举
enum BluetoothState: String {
public enum BluetoothState: String {
case unknown = "unknown"
case off = "off"
case on = "on"
@ -19,7 +19,10 @@ enum BluetoothState: String {
}
/// 简化的蓝牙助手类,专注于iOS上的蓝牙耳机检测
class ClassicBluetoothHelper: NSObject, CBCentralManagerDelegate {
public class ClassicBluetoothHelper: NSObject, CBCentralManagerDelegate {
/// 单例实例
public static let shared = ClassicBluetoothHelper()
private var centralManager: CBCentralManager?
private var bluetoothState: CBManagerState = .unknown
@ -30,16 +33,132 @@ class ClassicBluetoothHelper: NSObject, CBCentralManagerDelegate {
// 音频路由变化观察者
private var routeChangeObserver: NSObjectProtocol?
override init() {
// 添加自动启动标志
private var autoStartEnabled: Bool = false
/// 私有初始化方法,防止外部创建实例
private override init() {
super.init()
centralManager = CBCentralManager(delegate: self, queue: nil)
// 应用启动时自动开始监听
enableAutoStart()
}
/**
* 启用自动启动蓝牙监听功能
* 应用启动时自动开始监听蓝牙状态和音频路由变化
*/
func enableAutoStart() {
autoStartEnabled = true
// 自动开始蓝牙状态监听
startBluetoothStateMonitoring { [weak self] eventType, data in
self?.handleBluetoothEvent(eventType: eventType, data: data)
}
print("蓝牙监听服务已自动启动")
}
/**
* 处理蓝牙事件并自动配置音频路由
*/
private func handleBluetoothEvent(eventType: BluetoothEventType, data: [String: Any]) {
switch eventType {
case .stateChanged:
if let state = data["state"] as? String {
print("蓝牙状态变化: \(state)")
configureAudioRouteBasedOnBluetoothState()
}
case .deviceConnected:
print("蓝牙设备已连接")
// 蓝牙设备连接时:末端在手机,输出端在耳机
configureAudioForBluetoothConnected()
case .deviceDisconnected:
print("蓝牙设备已断开")
// 蓝牙设备断开时:末端和输出端都在手机
configureAudioForBluetoothDisconnected()
}
}
/**
* 根据蓝牙状态配置音频路由
*/
private func configureAudioRouteBasedOnBluetoothState() {
let hasBluetoothDevices = !getConnectedAudioDevices().isEmpty
if hasBluetoothDevices {
configureAudioForBluetoothConnected()
} else {
configureAudioForBluetoothDisconnected()
}
}
/**
* 配置蓝牙连接时的音频路由:末端在手机,输出端在耳机
*/
private func configureAudioForBluetoothConnected() {
let audioSession = AVAudioSession.sharedInstance()
do {
// 设置音频会话类别,允许蓝牙音频
try audioSession.setCategory(.playAndRecord,
mode: .videoChat,
options: [.allowBluetoothA2DP])
// 设置音频输入为内置麦克风(手机)
if let availableInputs = audioSession.availableInputs {
if let builtInMic = availableInputs.first(where: { $0.portType == .builtInMic }) {
try audioSession.setPreferredInput(builtInMic)
print("音频输入设置为内置麦克风(手机)")
}
}
// 音频输出自动路由到蓝牙耳机
try audioSession.overrideOutputAudioPort(.none) // 不强制扬声器输出,让系统自动选择蓝牙
print("音频输出设置为蓝牙耳机")
try audioSession.setActive(true)
} catch {
print("配置蓝牙连接音频路由失败: \(error.localizedDescription)")
}
}
/**
* 配置蓝牙断开时的音频路由:末端和输出端都在手机
*/
private func configureAudioForBluetoothDisconnected() {
let audioSession = AVAudioSession.sharedInstance()
do {
// 设置音频会话类别,不使用蓝牙
try audioSession.setCategory(.playAndRecord,
mode: .videoChat,
options: [.mixWithOthers])
// 设置音频输入为内置麦克风(手机)
if let availableInputs = audioSession.availableInputs {
if let builtInMic = availableInputs.first(where: { $0.portType == .builtInMic }) {
try audioSession.setPreferredInput(builtInMic)
print("音频输入设置为内置麦克风(手机)")
}
}
// 设置音频输出为扬声器(手机)
try audioSession.overrideOutputAudioPort(.speaker)
print("音频输出设置为扬声器(手机)")
try audioSession.setActive(true)
} catch {
print("配置蓝牙断开音频路由失败: \(error.localizedDescription)")
}
}
deinit {
stopBluetoothStateMonitoring()
}
func centralManagerDidUpdateState(_ central: CBCentralManager) {
/// 蓝牙状态更新回调
public func centralManagerDidUpdateState(_ central: CBCentralManager) {
bluetoothState = central.state
print("lxm---蓝牙状态更新: \(bluetoothState.rawValue)")
@ -50,7 +169,8 @@ class ClassicBluetoothHelper: NSObject, CBCentralManagerDelegate {
}
}
func isBluetoothEnabled() -> Bool {
/// 检查蓝牙是否可用
public func isBluetoothEnabled() -> Bool {
switch bluetoothState {
case .poweredOn:
return true
@ -83,7 +203,7 @@ class ClassicBluetoothHelper: NSObject, CBCentralManagerDelegate {
}
// 获取连接的蓝牙音频设备
private func getConnectedAudioDevices() -> [[String: String]] {
public func getConnectedAudioDevices() -> [[String: String]] {
var result: [[String: String]] = []
// 获取当前音频路由
@ -103,7 +223,7 @@ class ClassicBluetoothHelper: NSObject, CBCentralManagerDelegate {
}
// 检查是否为蓝牙输出
private func isBluetoothOutput(_ portType: AVAudioSession.Port) -> Bool {
public func isBluetoothOutput(_ portType: AVAudioSession.Port) -> Bool {
return portType == .bluetoothA2DP || portType == .bluetoothHFP || portType == .bluetoothLE
}
@ -116,8 +236,10 @@ class ClassicBluetoothHelper: NSObject, CBCentralManagerDelegate {
return address
}
/// 开始监听蓝牙状态变化
func startBluetoothStateMonitoring(callback: @escaping (BluetoothEventType, [String: Any]) -> Void) {
/**
* 开始监听蓝牙状态变化
*/
public func startBluetoothStateMonitoring(callback: @escaping (BluetoothEventType, [String: Any]) -> Void) {
self.stateCallback = callback
if !isMonitoring {
@ -133,9 +255,6 @@ class ClassicBluetoothHelper: NSObject, CBCentralManagerDelegate {
self?.handleAudioRouteChange(notification)
}
// 激活音频会话
activateAudioSession()
// 报告当前状态
reportCurrentBluetoothState()
}
@ -186,19 +305,10 @@ class ClassicBluetoothHelper: NSObject, CBCentralManagerDelegate {
}
}
// 激活音频会话
private func activateAudioSession() {
do {
let audioSession = AVAudioSession.sharedInstance()
try audioSession.setCategory(.playback, mode: .default)
try audioSession.setActive(true)
} catch {
print("设置音频会话失败: \(error.localizedDescription)")
}
}
/// 停止监听蓝牙状态变化
func stopBluetoothStateMonitoring() {
/**
* 停止监听蓝牙状态变化
*/
public func stopBluetoothStateMonitoring() {
if isMonitoring {
isMonitoring = false
@ -226,7 +336,7 @@ class ClassicBluetoothHelper: NSObject, CBCentralManagerDelegate {
}
/// 获取蓝牙可用性状态(优化后的统一状态判断方法)
func getBluetoothAvailability() -> BluetoothState {
public func getBluetoothAvailability() -> BluetoothState {
switch bluetoothState {
case .poweredOn:
return .on

2
local_plugins/classic_bluetooth/ios/Classes/SwiftClassicBluetoothPlugin.swift → local_plugins/classic_bluetooth/ios/classic_bluetooth/Sources/classic_bluetooth/SwiftClassicBluetoothPlugin.swift

@ -10,7 +10,7 @@ public class SwiftClassicBluetoothPlugin: NSObject, FlutterPlugin {
private var methodChannel: FlutterMethodChannel?
private var eventChannel: FlutterEventChannel?
private var eventSink: FlutterEventSink?
private let bluetoothHelper = ClassicBluetoothHelper()
private let bluetoothHelper = ClassicBluetoothHelper.shared
public static func register(with registrar: FlutterPluginRegistrar) {
let channel = FlutterMethodChannel(name: "com.yunqiinnovation.classic_bluetooth/methods", binaryMessenger: registrar.messenger())

2
local_plugins/classic_bluetooth/pubspec.yaml

@ -17,4 +17,4 @@ flutter:
package: com.yunqiinnovation.classic_bluetooth
pluginClass: ClassicBluetoothPlugin
ios:
pluginClass: ClassicBluetoothPlugin
pluginClass: SwiftClassicBluetoothPlugin
Loading…
Cancel
Save