Browse Source

Merge commit '87c4ecb867f8643d88922e77b4d5f88b04f8b5cd' into new_dev

weicu
lxm 1 year ago
parent
commit
ff9025cf95
  1. 41
      ios/Runner/Info.plist
  2. 4
      lib/data/services/asr_service.dart
  3. 10
      lib/data/services/ble_manager.dart
  4. 3
      lib/data/services/speech_impl/azure_asr_service.dart
  5. 3
      lib/data/services/speech_impl/volcano_asr_api_service.dart
  6. 3
      lib/data/services/speech_impl/volcano_asr_service.dart
  7. 3
      lib/data/services/speech_impl/xunfei_asr_service.dart
  8. 4
      lib/modules/agent/controllers/agent_controller.dart
  9. 14
      lib/modules/meeting/controllers/meeting_record_controller.dart
  10. 64
      lib/modules/meeting/views/bottomSheet/navigation_bar_bottom_sheet.dart
  11. 73
      lib/modules/translation/controllers/translation_controller.dart
  12. 26
      lib/modules/translation/views/translation_view.dart
  13. 5
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleAgent.kt
  14. 21
      local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift
  15. 197
      local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt
  16. 153
      local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureSpeechPlugin.kt
  17. 47
      local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/tools/RecordFile.kt
  18. 32
      local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/tools/SimpleAudioReceiver.kt
  19. 128
      local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureAsrHelper.swift
  20. 175
      local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureSpeechPlugin.swift
  21. 5
      local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/IntegratedSpeechTranslationService.swift
  22. 226
      local_plugins/azure_speech/ios/azure_speech/Sources/tools/MicrophoneCapture.swift
  23. 66
      local_plugins/azure_speech/ios/azure_speech/Sources/tools/RecordFile.swift
  24. 22
      local_plugins/azure_speech/ios/azure_speech/Sources/tools/SimpleAudioReceiver.swift
  25. 4
      local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleConst.kt
  26. 73
      local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt
  27. 10
      local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleServicePlugin.kt
  28. 2
      local_plugins/ble_service/ios/ble_service/Sources/ble_service/BleConst.swift
  29. 30
      local_plugins/ble_service/ios/ble_service/Sources/ble_service/BleService.swift
  30. 13
      local_plugins/ble_service/ios/ble_service/Sources/ble_service/SwiftBleServicePlugin.swift
  31. 93
      local_plugins/ble_service/ios/ble_service/Sources/ble_service/SwiftOpusAudioProcessor.swift
  32. 12
      local_plugins/ble_service/lib/ble_service.dart
  33. 120
      local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/QQMusicSystemFunctionHandler.kt
  34. 306
      local_plugins/music_service/ios/music_service/Sources/music_service/MusicService.swift
  35. 5
      local_plugins/ota/android/src/main/kotlin/com/example/ota/OtaPlugin.kt
  36. 35
      local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/QQMusicLoginManager.kt
  37. 106
      local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/QQMusicManager.kt
  38. 438
      local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/QQMusicSingleton.kt
  39. 16
      local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/QqMusicPlugin.kt
  40. 2
      pubspec.yaml

41
ios/Runner/Info.plist

@ -102,6 +102,47 @@
<true/>
<key>NSAllowsArbitraryLoadsInWebContent</key>
<true/>
<!-- 为网易云音乐图片域名添加例外 -->
<key>NSExceptionDomains</key>
<dict>
<key>music.126.net</key>
<dict>
<key>NSExceptionAllowsInsecureHTTPLoads</key>
<true/>
<key>NSExceptionMinimumTLSVersion</key>
<string>TLSv1.0</string>
<key>NSIncludesSubdomains</key>
<true/>
</dict>
<key>p1.music.126.net</key>
<dict>
<key>NSExceptionAllowsInsecureHTTPLoads</key>
<true/>
<key>NSExceptionMinimumTLSVersion</key>
<string>TLSv1.0</string>
</dict>
<key>p2.music.126.net</key>
<dict>
<key>NSExceptionAllowsInsecureHTTPLoads</key>
<true/>
<key>NSExceptionMinimumTLSVersion</key>
<string>TLSv1.0</string>
</dict>
<key>p3.music.126.net</key>
<dict>
<key>NSExceptionAllowsInsecureHTTPLoads</key>
<true/>
<key>NSExceptionMinimumTLSVersion</key>
<string>TLSv1.0</string>
</dict>
<key>p4.music.126.net</key>
<dict>
<key>NSExceptionAllowsInsecureHTTPLoads</key>
<true/>
<key>NSExceptionMinimumTLSVersion</key>
<string>TLSv1.0</string>
</dict>
</dict>
</dict>
<key>NSBluetoothAlwaysUsageDescription</key>
<string>Bluetooth permission is required to connect and manage audio devices</string>

4
lib/data/services/asr_service.dart

@ -43,8 +43,10 @@ abstract class AsrService {
/// 开始录音
///
/// [filePath] 录音文件路径
/// [audioSourceType] 音频源类型
/// [acceptAudioData] 是否接受音频数据回调,默认为 false
Future<bool> enableRecord(String filePath, {bool acceptAudioData = false});
Future<bool> enableRecord(bool audioSourceType, String filePath,
{bool acceptAudioData = false});
/// 获取音频数据流(如果支持)
Stream<Uint8List>? getAudioDataStream() => null;

10
lib/data/services/ble_manager.dart

@ -857,6 +857,16 @@ class BleManager extends GetxService {
}
}
/// 打开解码器
Future<bool> openCallRecordDecoder() async {
try {
return await _bleService.openCallRecordDecoder();
} catch (e) {
Logger.error('打开解码器失败: ${e.toString()}');
return false;
}
}
/// 关闭编解码器
Future<bool> closeCodec() async {
try {

3
lib/data/services/speech_impl/azure_asr_service.dart

@ -465,10 +465,11 @@ class AzureAsrService extends GetxService implements AsrService {
}
@override
Future<bool> enableRecord(String filePath,
Future<bool> enableRecord(bool audioSourceType, String filePath,
{bool acceptAudioData = false}) async {
try {
final bool result = await _channel.invokeMethod('enableRecord', {
'audioSourceType': audioSourceType,
'filePath': filePath,
'acceptAudioData': acceptAudioData, // 新增参数
});

3
lib/data/services/speech_impl/volcano_asr_api_service.dart

@ -813,7 +813,8 @@ class VolcanoAsrApiService implements AsrService {
}
@override
Future<bool> enableRecord(String filePath, {bool acceptAudioData = false}) {
Future<bool> enableRecord(bool audioSourceType, String filePath,
{bool acceptAudioData = false}) {
// TODO: implement enableRecord
throw UnimplementedError();
}

3
lib/data/services/speech_impl/volcano_asr_service.dart

@ -348,7 +348,8 @@ class VolcanoAsrService extends GetxService implements AsrService {
}
@override
Future<bool> enableRecord(String filePath, {bool acceptAudioData = false}) {
Future<bool> enableRecord(bool audioSourceType, String filePath,
{bool acceptAudioData = false}) {
// TODO: implement enableRecord
throw UnimplementedError();
}

3
lib/data/services/speech_impl/xunfei_asr_service.dart

@ -272,7 +272,8 @@ class XunfeiAsrService extends GetxService implements AsrService {
}
@override
Future<bool> enableRecord(String filePath, {bool acceptAudioData = false}) {
Future<bool> enableRecord(bool audioSourceType, String filePath,
{bool acceptAudioData = false}) {
// TODO: implement enableRecord
throw UnimplementedError();
}

4
lib/modules/agent/controllers/agent_controller.dart

@ -498,7 +498,7 @@ class AgentController extends GetxController with WidgetsBindingObserver {
switch (event.type) {
case AgentServiceEventType.recognitionStarted:
isListening.value = true;
isTextInputMode.value = false;
//isTextInputMode.value = false;
Logger.i(TAG, '订阅代理服务事件识别开始');
// isTextInputMode.refresh();
// 只标记识别开始,不添加消息
@ -937,7 +937,7 @@ class AgentController extends GetxController with WidgetsBindingObserver {
// 注意:这里不需要调用startVoiceInput(),因为原生层已经启动了语音识别
// 我们只需要同步Flutter层的状态
isTextInputMode.value = false;
//isTextInputMode.value = false;
Logger.i(TAG, 'BLE唤醒状态已设置,等待原生层语音识别结果');
}

14
lib/modules/meeting/controllers/meeting_record_controller.dart

@ -70,7 +70,7 @@ class MeetingRecordController extends GetxController
// 性能优化:音频处理采样控制
int _audioProcessCounter = 0;
static const int _audioProcessInterval = 3; // 每3个音频包处理1次
static const int _audioProcessInterval = 1; // 每1个音频包处理1次
// 波形流动动画控制器
late AnimationController waveformAnimationController;
@ -306,9 +306,13 @@ class MeetingRecordController extends GetxController
final formattedTime = DateFormat('yyyyMMdd_HHmmss').format(DateTime.now());
final fullFileName = "${fileName.value}_$formattedTime";
// Start audio recording
await _asrService.enableRecord("${dir.path}$fullFileName.wav",
acceptAudioData: true);
if (fileName.value == "liveRecording".tr) {
await _asrService.enableRecord(false, "${dir.path}$fullFileName.wav",
acceptAudioData: true);
} else {
await _asrService.enableRecord(true, "${dir.path}$fullFileName.wav",
acceptAudioData: true);
}
// 通过 asrService 获取音频数据流
_audioDataSubscription =
@ -350,7 +354,7 @@ class MeetingRecordController extends GetxController
break;
case 2:
_asrService.setAudioConfig(sampleRate: 16000, channels: 2);
_bleManager.openA2DPDecoder();
_bleManager.openCallRecordDecoder();
break;
}
}

64
lib/modules/meeting/views/bottomSheet/navigation_bar_bottom_sheet.dart

@ -35,38 +35,38 @@ class NavigationBarBottomSheet extends GetView<MeetingHomeController> {
controller.addMeetingAudio();
},
),
// if (controller.meetingController.type == 1)
// _buildItem(
// context, // 传递 context 参数
// 'audioVideoRecording'.tr, // 对应中文:音、视频录音
// Icons.videocam,
// Colors.green,
// onTap: controller.bleManager.isConnected
// ? () async {
// Get.back();
// await Get.toNamed(Routes.meetingRecord, arguments: {
// 'audioTypes': 1,
// });
// controller.addMeetingAudio();
// }
// : null,
// ),
// if (controller.meetingController.type == 1)
// _buildItem(
// context, // 传递 context 参数
// 'callRecording'.tr, // 对应中文:通话录音
// Icons.phone,
// Colors.orange,
// onTap: controller.bleManager.isConnected
// ? () async {
// Get.back();
// await Get.toNamed(Routes.meetingRecord, arguments: {
// 'audioTypes': 2,
// });
// controller.addMeetingAudio();
// }
// : null,
// ),
if (controller.meetingController.type == 1)
_buildItem(
context, // 传递 context 参数
'audioVideoRecording'.tr, // 对应中文:音、视频录音
Icons.videocam,
Colors.green,
onTap: controller.bleManager.isConnected
? () async {
Get.back();
await Get.toNamed(Routes.meetingRecord, arguments: {
'audioTypes': 1,
});
controller.addMeetingAudio();
}
: null,
),
if (controller.meetingController.type == 1)
_buildItem(
context, // 传递 context 参数
'callRecording'.tr, // 对应中文:通话录音
Icons.phone,
Colors.orange,
onTap: controller.bleManager.isConnected
? () async {
Get.back();
await Get.toNamed(Routes.meetingRecord, arguments: {
'audioTypes': 2,
});
controller.addMeetingAudio();
}
: null,
),
_buildItem(
context, // 传递 context 参数
'importAudio'.tr, // 对应中文:导入音频

73
lib/modules/translation/controllers/translation_controller.dart

@ -74,7 +74,7 @@ class TranslationController extends GetxController with WidgetsBindingObserver {
final isTranslating = false.obs;
final isTtsEnabled = true.obs;
final isRecording = false.obs;
final lasyIsRecording = false.obs;
final isCreateRecord = false.obs;
final hasRecordPermission = false.obs;
// ==================== 语言相关 ====================
@ -128,6 +128,8 @@ class TranslationController extends GetxController with WidgetsBindingObserver {
if (mode == 'call') {
await _initializeCallModeTranslationService();
} else {
_reinitializeAsrService();
}
}
@ -186,8 +188,16 @@ class TranslationController extends GetxController with WidgetsBindingObserver {
/// [speakerId] 说话者ID
Future<void> _startFaceToFaceRecognition(int speakerId) async {
try {
await _asrService.startContinuousRecognition(_audioSourceType);
await continueRecording();
await _asrService.startContinuousRecognition(_audioSourceType); //开启识别
_startAsrActiveTracking(); //开启识别活动跟踪
if (isRecording.value) {
if (isCreateRecord.value) {
//已经创建文件
await continueRecording();
} else {
await startRecording();
}
}
Logger.info('面对面翻译:激活说话者 $speakerId');
} catch (e) {
Logger.error('启动面对面翻译失败: ${e.toString()}');
@ -198,13 +208,15 @@ class TranslationController extends GetxController with WidgetsBindingObserver {
/// 停止面对面翻译的语音识别
Future<void> _stopFaceToFaceRecognition() async {
try {
await pauseRecording(); //暂停
if (isRecording.value) {
await pauseRecording(); //暂停
}
if (faceToFaceRecognitionText.value.isNotEmpty) {
handleFinalResult(faceToFaceRecognitionText.value);
faceToFaceRecognitionText.value = '';
}
await _asrService.stopContinuousRecognition();
_stopAsrActiveTracking();
_stopAsrActiveTracking(); //停止识别活动跟踪
isRecognizing.value = false;
currentSessionId = null;
@ -444,6 +456,19 @@ class TranslationController extends GetxController with WidgetsBindingObserver {
Future<void> _initializeCallModeTranslationService() async {
try {
Logger.info('开始初始化通话模式语音翻译服务');
// 初始化 ASR 服务时,明确指定需要支持的语言
final List<String> asrSupportedLanguages = [targetLanguageCode.value];
Logger.info('初始化ASR服务,支持语言: $asrSupportedLanguages');
await _asrService.initialize(supportedLanguages: asrSupportedLanguages);
// 获取识别事件流
var recognitionStream = await _asrService.recognizeCallback();
_recognitionSubscription?.cancel();
_recognitionSubscription =
recognitionStream.listen(_handleRecognitionEvent);
final List<String> callModeLanguages = [
sourceLanguageCode.value,
targetLanguageCode.value
@ -498,7 +523,9 @@ class TranslationController extends GetxController with WidgetsBindingObserver {
/// 切换录音功能开关
Future<void> toggleisRecord() async {
isRecording.toggle();
if (isRecording.value && isRecognizing.value) {
if (isRecording.value &&
isRecognizing.value &&
currentMode.value != 'faceToFace') {
startRecording();
} else if (!isRecording.value) {
stopRecording();
@ -508,21 +535,27 @@ class TranslationController extends GetxController with WidgetsBindingObserver {
/// 开始录音
Future<void> startRecording() async {
_timerManager.startTimer();
final formattedTime = DateFormat('yyyyMMdd_HHmmss').format(DateTime.now());
await _asrService.enableRecord(
"${dir.path}/${currentModeTitle.value.tr}_$formattedTime.wav");
if (currentMode.value == 'call') {
await _astService.enableRecord(
"${dir.path}/${currentModeTitle.value.tr}_${formattedTime}_mic.wav");
if (currentMode.value == 'call' || currentMode.value == 'audioVideo') {
await _asrService.enableRecord(
true, "${dir.path}/${currentModeTitle.value.tr}_$formattedTime.wav");
} else {
await _asrService.enableRecord(
false, "${dir.path}/${currentModeTitle.value.tr}_$formattedTime.wav");
}
// if (currentMode.value == 'call') {
// await _astService.enableRecord(
// "${dir.path}/${currentModeTitle.value.tr}_${formattedTime}_mic.wav");
// }
isCreateRecord.value = true;
}
/// 停止录音
Future<void> stopRecording() async {
_timerManager.stopTimer();
_asrService.stopRecord(true);
lasyIsRecording.value = false;
isCreateRecord.value = false;
Logger.info('录音已停止,计时器已重置');
}
@ -530,7 +563,7 @@ class TranslationController extends GetxController with WidgetsBindingObserver {
Future<void> pauseRecording() async {
_timerManager.pauseTimer();
_asrService.pauseRecord();
lasyIsRecording.value = false;
Logger.info(' 录音已暂停');
}
@ -538,7 +571,7 @@ class TranslationController extends GetxController with WidgetsBindingObserver {
Future<void> continueRecording() async {
_timerManager.resumeTimer();
_asrService.resumeRecord();
lasyIsRecording.value = true;
Logger.info(' 录音已继续');
}
@ -552,8 +585,10 @@ class TranslationController extends GetxController with WidgetsBindingObserver {
try {
await _initializeSession();
await _configureAudioMode();
await _startAsrService();
await _finalizeRecognitionStart();
if (currentMode.value != 'faceToFace') {
await _startAsrService();
await _finalizeRecognitionStart();
}
} catch (e) {
isRecognizing.value = false;
Logger.error('启动语音识别失败: ${e.toString()}');
@ -622,9 +657,7 @@ class TranslationController extends GetxController with WidgetsBindingObserver {
/// 启动语音识别
Future<void> _startAsrService() async {
if (currentMode.value != 'faceToFace') {
await _asrService.startContinuousRecognition(_audioSourceType);
}
await _asrService.startContinuousRecognition(_audioSourceType);
}
/// 完成识别启动

26
lib/modules/translation/views/translation_view.dart

@ -1087,19 +1087,19 @@ class TranslationView extends GetView<TranslationController> {
await controller.startRecognition();
},
),
// _buildModeItem(
// context,
// 'audioVideoTranslation'.tr,
// Icons.videocam,
// Colors.orange,
// onTap: controller.bleManager.isConnected
// ? () async {
// Get.back();
// await controller.changeTranslationMode('audioVideo');
// await controller.startRecognition();
// }
// : null,
// ),
_buildModeItem(
context,
'audioVideoTranslation'.tr,
Icons.videocam,
Colors.orange,
onTap: controller.bleManager.isConnected
? () async {
Get.back();
await controller.changeTranslationMode('audioVideo');
await controller.startRecognition();
}
: null,
),
_buildModeItem(
context,
'callTranslation'.tr,

5
local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleAgent.kt

@ -130,13 +130,10 @@ Log.d(TAG, "手动启动语音识别: ")
}
}
override fun onAudioDataReceived(data: ByteArray) {
override fun onAudioDataReceived(data: ByteArray, channel: Int) {
// Log.d(TAG, "onAudioDataReceived, data: ${data.size}")
AgentService.pushAudioData(data)
// 可选:处理音频数据
}
override fun onAudioDataReceivedCall(data: ByteArray) {
}
/**
* 处理唤醒信号

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

@ -917,19 +917,19 @@ private func jsonToString(_ json: [String: Any]) -> String? {
// 处理音乐卡片
private func processMusicCard(_ cardMusic: [String: Any]) {
let id = cardMusic["id"] as? Int ?? 0
let url = cardMusic["url"] as? String ?? ""
// let url = cardMusic["url"] as? String ?? ""
let title = cardMusic["name"] as? String ?? ""
let artist = cardMusic["sgener"] as? String ?? ""
let coverUrl = cardMusic["image"] as? String ?? ""
let ts = cardMusic["ts"] as? Int ?? 0
// let ts = cardMusic["ts"] as? Int ?? 0
processMusicPlay([
"id": id,
"url": url,
// "url": url,
"title": title,
"artist": artist,
"coverUrl": coverUrl,
"ts": ts
// "ts": ts
])
}
@ -939,19 +939,19 @@ private func jsonToString(_ json: [String: Any]) -> String? {
for item in musics {
let id = item["id"] as? Int ?? 0
let url = item["url"] as? String ?? ""
// let url = item["url"] as? String ?? ""
let title = item["name"] as? String ?? ""
let artist = item["sgener"] as? String ?? ""
let coverUrl = item["image"] as? String ?? ""
let ts = item["ts"] as? Int ?? 0
// let ts = item["ts"] as? Int ?? 0
playlist.append([
"id": id,
"url": url,
// "url": url,
"title": title,
"artist": artist,
"coverUrl": coverUrl,
"ts": ts
// "ts": ts
])
}
@ -1507,13 +1507,10 @@ extension AgentServiceImpl: BleService.Callback {
func onConnectionStateChanged(state: Int) {
}
func onAudioDataReceived(data: Data) {
func onAudioDataReceived(data: Data, channel: Int32) {
pushAudioData(data)
}
func onAudioDataReceivedCall(data: Data) {
}
func onWakeupSignalReceived() {
stopTts()

197
local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt

@ -31,9 +31,6 @@ class AzureAsrHelper(private val context: Context) {
private var audioConfig: AudioConfig? = null
private var continuousCallback: ContinuousRecognizeCallback? = null
// 状态管理
private var isContinuousRecognitionActive = false
// 配置参数
private var currentLanguage = "zh-CN"
private var supportedLanguages = arrayOf("zh-CN")
@ -44,8 +41,8 @@ class AzureAsrHelper(private val context: Context) {
// 音频源配置
var audioSourceType = AudioSourceType.MICROPHONE
// 音频处理
var audioStream: SimpleAudioReceiver? = null
// 音频处理 - Initialize immediately to avoid null checks
var audioStream: SimpleAudioReceiver = SimpleAudioReceiver(context)
// 网络状态监听
private var networkMonitor: NetworkStateMonitor = NetworkStateMonitor(context)
@ -181,10 +178,10 @@ class AzureAsrHelper(private val context: Context) {
private fun setupRecognizer(): Boolean {
try {
// 如果正在进行连续识别,先停止
if (isContinuousRecognitionActive) {
if (audioStream.isContinuousRecognitionActive) {
// 直接停止,不等待结果
recognizer?.stopContinuousRecognitionAsync()
isContinuousRecognitionActive = false
audioStream.isContinuousRecognitionActive = false
}
setupMicrophoneStream()
@ -214,18 +211,15 @@ class AzureAsrHelper(private val context: Context) {
try {
Log.d(tag, "设置麦克风流 - 使用拉流方式: }")
if (audioStream == null) {
// 创建外部音频拉流对象
audioStream = SimpleAudioReceiver(context)
audioStream!!.initAudioRecord()
// Initialize if not already done
if (!audioStream.isInitialized()) {
audioStream.initAudioRecord()
}
audioConfig = AudioConfig.fromStreamInput(audioStream!!.pushAudioStream)
audioConfig = AudioConfig.fromStreamInput(audioStream.pushAudioStream)
} catch (e: Exception) {
Log.e(tag, "设置麦克风流失败: ${e.message}")
//microphoneStream = null
}
}
@ -241,7 +235,7 @@ class AzureAsrHelper(private val context: Context) {
audioDataCallback: SimpleAudioReceiver.AudioDataCallback? = null
): Boolean {
Log.d(tag, "startContinuousRecognition:$isContinuousRecognitionActive ")
Log.d(tag, "startContinuousRecognition:${audioStream.isContinuousRecognitionActive} ")
// 检查网络状态
if (!checkNetworkStatus()) {
@ -250,7 +244,7 @@ class AzureAsrHelper(private val context: Context) {
return false
}
if (isContinuousRecognitionActive) {
if (audioStream.isContinuousRecognitionActive) {
return true
}
if (!isRecognizerValid()) {
@ -266,13 +260,12 @@ class AzureAsrHelper(private val context: Context) {
this.audioSourceType = audioSourceType
try {
Log.d(tag, "startContinuousRecognition: ${audioStream!!.isWriting()}")
Log.d(tag, "startContinuousRecognition: ${audioStream.isWriting()}")
// 启动音频处理(若已启动则跳过)
if (!audioStream!!.isWriting()) {
if (!audioStream.isContinuousRecognitionActive) {
Log.d(tag, "startAudioRecord:$audioSourceType")
audioStream!!.startAudioRecord(
audioStream.startAudioRecord(
when (audioSourceType) {
AudioSourceType.MICROPHONE -> SimpleAudioReceiver.AudioSourceType.MICROPHONE
AudioSourceType.EXTERNAL -> SimpleAudioReceiver.AudioSourceType.EXTERNAL
},
@ -283,12 +276,12 @@ class AzureAsrHelper(private val context: Context) {
}
recognizer?.startContinuousRecognitionAsync()
isContinuousRecognitionActive = true
audioStream.isContinuousRecognitionActive = true
return true
} catch (e: Exception) {
Log.e(tag, "开始连续识别失败: ${e.message}")
stopContinuousRecognition()
isContinuousRecognitionActive = false
stopContinuousRecognition()
audioStream.isContinuousRecognitionActive = false
return false
}
}
@ -297,7 +290,7 @@ class AzureAsrHelper(private val context: Context) {
* 设置事件监听器
*/
fun setupEventListeners(callback: ContinuousRecognizeCallback): Boolean {
Log.d(tag, "设置ssssss监听器:${speechConfig} ")
Log.d(tag, "设置ssssss监听器:${speechConfig ?: "null"} ")
if (speechConfig == null) {
callback.onError(1002,"语音服务未初始化")
return false
@ -357,7 +350,7 @@ class AzureAsrHelper(private val context: Context) {
Log.d(tag, "会话结束事件")
if (audioSourceType == AudioSourceType.EXTERNAL) {
callback.onSessionStopped()
isContinuousRecognitionActive = false
audioStream?.isContinuousRecognitionActive = false
}
}
)
@ -392,26 +385,25 @@ class AzureAsrHelper(private val context: Context) {
}
try {
Log.d(tag, "停止连续语音识别: ")
// 停止音频处理
audioStream!!.stopMicrophoneCapture()
// 停止音频处理
audioStream.stopMicrophoneCapture()
// 直接停止连续识别(SDK内部已是异步操作)
recognizer?.stopContinuousRecognitionAsync()?.get(1000, TimeUnit.MILLISECONDS)
recognizer?.close()
recognizer = null
isContinuousRecognitionActive = false
audioStream.isContinuousRecognitionActive = false
return true
} catch (e: Exception) {
// 强制重置状态
isContinuousRecognitionActive = false
audioStream.isContinuousRecognitionActive = false
Log.e(tag, "停止连续识别失败: ${e.message}")
// 停止音频处理
audioStream!!.stopMicrophoneCapture()
// 停止音频处理
audioStream.stopMicrophoneCapture()
recognizer?.stopContinuousRecognitionAsync()
recognizer?.close()
recognizer?.close()
recognizer = null
isContinuousRecognitionActive = false
audioStream.isContinuousRecognitionActive = false
return false
}
}
@ -419,7 +411,7 @@ class AzureAsrHelper(private val context: Context) {
/**
* 检查连续识别是否活跃
*/
fun isContinuousRecognitionActive(): Boolean = isContinuousRecognitionActive
fun isContinuousRecognitionActive(): Boolean = audioStream.isContinuousRecognitionActive
/**
* 释放所有资源
@ -427,19 +419,18 @@ class AzureAsrHelper(private val context: Context) {
fun dispose() {
try {
// 如果正在进行连续识别,先停止
if (isContinuousRecognitionActive) {
if (audioStream.isContinuousRecognitionActive) {
// 直接停止,不等待结果
recognizer?.stopContinuousRecognitionAsync()
isContinuousRecognitionActive = false
audioStream.isContinuousRecognitionActive = false
}
// 停止音频处理
stopAudioProcessing()
// 清理网络监听
clearNetworkDetection()
// 停止录音
if (audioStream!!.recordfile != null) {
audioStream!!.recordfile!!.closeFile(true)
}
audioStream.recordfile?.closeFile(true)
// 释放recognizer
recognizer?.close()
recognizer = null
@ -452,11 +443,11 @@ class AzureAsrHelper(private val context: Context) {
audioConfig?.close()
audioConfig = null
// 确保状态被重置
isContinuousRecognitionActive = false
audioStream.isContinuousRecognitionActive = false
} catch (e: Exception) {
// 确保状态被重置
isContinuousRecognitionActive = false
audioStream.isContinuousRecognitionActive = false
audioConfig = null
recognizer = null
speechConfig = null
@ -468,20 +459,14 @@ class AzureAsrHelper(private val context: Context) {
* 停止音频处理
*/
private fun stopAudioProcessing() {
audioStream?.let {
try {
Log.e(tag, "停止音频处理: }")
it.releaseAudioResources()
it.pushAudioStream?.close()
audioStream = null
} catch (e: Exception) {
Log.e(tag, "关闭麦克风流失败: ${e.message}")
e.printStackTrace()
}
} // 关闭音频流(根据实际实现可能需要)
try {
Log.e(tag, "停止音频处理: }")
audioStream.releaseAudioResources()
audioStream.pushAudioStream?.close()
} catch (e: Exception) {
Log.e(tag, "关闭麦克风流失败: ${e.message}")
e.printStackTrace()
}
}
@ -492,47 +477,61 @@ class AzureAsrHelper(private val context: Context) {
/**
* 开启录音
*/
fun enableRecord(filePath: String, audioDataCallback: SimpleAudioReceiver.AudioDataCallback? = null) {
fun enableRecord(audioSourceType: AudioSourceType = AudioSourceType.MICROPHONE,filePath: String, audioDataCallback: SimpleAudioReceiver.AudioDataCallback? = null) {
Log.i(tag, "开启录音:")
if (audioStream == null) {
// 创建外部音频拉流对象
audioStream = SimpleAudioReceiver(context)
audioStream!!.initAudioRecord()
}
if (audioStream!!.recordfile == null) {
audioStream!!.recordfile = RecordFile()
this.audioSourceType = audioSourceType
if(audioSourceType == AudioSourceType.EXTERNAL){
Log.i(tag, "外部音频源不在这里录音")
return
}
// 启动音频处理
audioStream!!.startAudioRecord(SimpleAudioReceiver.AudioSourceType.MICROPHONE
,audioDataCallback)
audioStream!!.recordfile!!.closeFile(true)
audioStream!!.recordfile!!.creatingFiles(filePath)
// Initialize if not already done
if (!audioStream.isInitialized()) {
audioStream.initAudioRecord()
}
if (audioStream.recordfile == null) {
audioStream.recordfile = RecordFile()
}
if (!audioStream.isContinuousRecognitionActive) {
// 启动音频处理
Log.i(tag, "开启录音:audioSourceType${audioSourceType}")
audioStream.startAudioRecord( when (audioSourceType) {
AudioSourceType.MICROPHONE -> SimpleAudioReceiver.AudioSourceType.MICROPHONE
AudioSourceType.EXTERNAL -> SimpleAudioReceiver.AudioSourceType.EXTERNAL
},
audioDataCallback
)
}
audioStream.isRecord = true
audioStream.recordfile?.closeFile(true)
audioStream.recordfile?.creatingFiles(filePath)
}
/**
* 移动文件到新路径
*/
fun moveFile(sourcePath: String, destPath: String): Boolean {
if(audioSourceType == AudioSourceType.EXTERNAL){
Log.i(tag, "外部音频源不在这里移动文件")
return false
}
Log.i(tag, "移动文件到新路径:")
if (audioStream!!.recordfile == null)return false
audioStream!!.recordfile!!.moveFile(sourcePath, destPath)
audioStream.recordfile?.moveFile(sourcePath, destPath) ?: return false
return true
}
/**
* 重命名指定路径的音频文件
*/
fun renameFile(filePath: String, newName: String): Boolean {
if(audioSourceType == AudioSourceType.EXTERNAL){
Log.i(tag, "外部音频源不在这里重命名文件")
return false
}
Log.i(tag, "重命名指定路径的音频文件:")
if (audioStream!!.recordfile == null)return false
audioStream!!.recordfile!!.renameFile(filePath, newName)
audioStream.recordfile?.renameFile(filePath, newName) ?: return false
return true
}
@ -540,35 +539,45 @@ class AzureAsrHelper(private val context: Context) {
* 停止录音
*/
fun pauseRecord() {
if(audioSourceType == AudioSourceType.EXTERNAL){
Log.i(tag, "外部音频源不在这里暂停录音")
return
}
Log.i(tag, "停止连续录音:")
if (audioStream!!.recordfile == null)return
audioStream!!.stopMicrophoneCapture()
audioStream.recordfile ?: return
audioStream.isRecord = false
}
/**
* 继续录音
*/
fun resumeRecord() {
Log.i(tag, "继续录音:")
if (audioStream!!.recordfile == null)return
audioStream!!.resumeRecord()
if(audioSourceType == AudioSourceType.EXTERNAL){
Log.i(tag, "外部音频源不在这里继续录音")
return
}
Log.i(tag, "继续录音:")
audioStream.recordfile ?: return
audioStream.isRecord = true
}
/**
* 关闭录音
*/
fun stopRecord(isSave: Boolean) {
Log.i(tag, "关闭录音:")
if (audioStream == null) {
if(audioSourceType == AudioSourceType.EXTERNAL){
Log.i(tag, "外部音频源不在这里关闭录音")
return
}
if (audioStream!!.recordfile == null)return
audioStream!!.stopMicrophoneCapture()
audioStream!!.recordfile!!.closeFile(isSave)
Log.i(tag, "关闭录音:")
audioStream.recordfile ?: return
if (!audioStream.isContinuousRecognitionActive) {
audioStream.stopMicrophoneCapture()
}
audioStream.isRecord = false
audioStream.recordfile?.closeFile(isSave)
}

153
local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureSpeechPlugin.kt

@ -19,7 +19,7 @@ import com.deep_voice.speech.tts.TtsEventType
import com.yunqiinnovation.ble_service.BleService
import com.deep_voice.speech.tts.AudioOutputDevice
import kotlinx.coroutines.*
import com.yunqiinnovation.azure_speech.tools.RecordFile
/** AzureSpeechPlugin */
class AzureSpeechPlugin : BleService.Callback, FlutterPlugin {
private val tag = "AzureSpeechPlugin"
@ -49,7 +49,8 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin {
private lateinit var audioDataEventChannel: EventChannel
private var audioDataEventSink: EventChannel.EventSink? = null
private var recordfile: RecordFile? = null
private var isRecord = false
// 是否已添加TTS事件监听器
private var isTtsListenerAdded = false
@ -446,7 +447,12 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin {
val newName = call.argument<String>("newName") ?: ""
try {
FileLogger.d(tag, "音频文件名称为: ${filePath}") //
azureAsrHelper.renameFile(filePath, newName)
if(azureAsrHelper.audioSourceType == AzureAsrHelper.AudioSourceType.EXTERNAL){
isRecord = false
recordfile?.renameFile(filePath, newName)
}else{
azureAsrHelper.renameFile(filePath, newName)
}
result.success(true)
} catch (e: Exception) {
result.error("ENABLERECORD_ERROR", e.message, null)
@ -458,8 +464,12 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin {
val destPath = call.argument<String>("destPath") ?: ""
try {
FileLogger.d(tag, "音频文件名称为: ${sourcePath}") //
azureAsrHelper.moveFile(sourcePath, destPath)
if(azureAsrHelper.audioSourceType == AzureAsrHelper.AudioSourceType.EXTERNAL){
isRecord = false
recordfile?.moveFile(sourcePath, destPath)
}else{
azureAsrHelper.moveFile(sourcePath, destPath)
}
result.success(true)
} catch (e: Exception) {
result.error("ENABLERECORD_ERROR", e.message, null)
@ -470,11 +480,28 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin {
val filePath = call.argument<String>("filePath") ?: ""
// 是否接受音频数据
val acceptAudioData = call.argument<Boolean>("acceptAudioData") ?: false
val useExternalAudio = call.argument<Boolean>("audioSourceType") ?: false
try {
FileLogger.d(tag, "选择音频源类型: ${useExternalAudio}") //
// 选择音频源类型
val audioSourceType = if (useExternalAudio) {
if(recordfile == null){
recordfile = RecordFile()
}
recordfile?.closeFile(true)
recordfile?.creatingFiles(filePath)
isRecord = true
AzureAsrHelper.AudioSourceType.EXTERNAL
} else {
AzureAsrHelper.AudioSourceType.MICROPHONE
// 启用录音,并根据 acceptAudioData 参数决定是否设置音频数据回调
}
FileLogger.d(tag, "音频文件名称为: ${filePath}")
// 启用录音,并根据 acceptAudioData 参数决定是否设置音频数据回调
azureAsrHelper.enableRecord(filePath, if(acceptAudioData) {
azureAsrHelper.enableRecord(audioSourceType, filePath, if(acceptAudioData) {
// 创建音频数据回调,将音频数据发送到 Flutter 层
object : SimpleAudioReceiver.AudioDataCallback {
override fun onAudio(audioData: ByteArray) {
@ -499,18 +526,36 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin {
}
"pauseRecord" -> {
if(azureAsrHelper.audioSourceType == AzureAsrHelper.AudioSourceType.EXTERNAL){
isRecord = false
}else{
azureAsrHelper.pauseRecord()
azureAsrHelper.pauseRecord()
}
result.success(true)
}
"resumeRecord" -> {
azureAsrHelper.resumeRecord()
if(azureAsrHelper.audioSourceType == AzureAsrHelper.AudioSourceType.EXTERNAL){
isRecord = true
}else{
azureAsrHelper.resumeRecord()
}
result.success(true)
}
"stopRecord" -> {
val isSave = call.argument<Boolean>("isSave") ?: false
try {
azureAsrHelper.stopRecord(isSave)
if(azureAsrHelper.audioSourceType == AzureAsrHelper.AudioSourceType.EXTERNAL){
isRecord = false
recordfile?.closeFile(true)
}
else
{
azureAsrHelper.stopRecord(isSave)
}
result.success(true)
} catch (e: Exception) {
result.error("ENABLERECORD_ERROR", e.message, null)
@ -521,7 +566,7 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin {
val sampleRate = call.argument<Int>("sampleRate") ?: 16000
val channels = call.argument<Int>("channels") ?: 1
try {
azureAsrHelper.audioStream?.recordfile?.setAudioConfig(sampleRate, channels)
// azureAsrHelper.audioStream?.recordfile?.setAudioConfig(sampleRate, channels)
result.success(true)
} catch (e: Exception) {
result.error("SET_AUDIO_CONFIG_ERROR", e.message, null)
@ -688,17 +733,17 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin {
}
override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) {
when (call.method) {
"enableRecord" -> {
val filePath = call.argument<String>("filePath") ?: ""
try {
FileLogger.d(tag, "音频文件名称为: ${filePath}") //
// "enableRecord" -> {
// val filePath = call.argument<String>("filePath") ?: ""
// try {
// FileLogger.d(tag, "音频文件名称为: ${filePath}") //
azureAstHelper.enableRecord(filePath)
result.success(true)
} catch (e: Exception) {
result.error("ENABLERECORD_ERROR", e.message, null)
}
}
// //azureAstHelper.enableRecord(filePath)
// result.success(true)
// } catch (e: Exception) {
// result.error("ENABLERECORD_ERROR", e.message, null)
// }
// }
"startContinuousTranslation" -> {
try {
FileLogger.d(tag, "开启翻译")
@ -996,20 +1041,70 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin {
override fun onConnectionStateChanged(state: Int) {
}
override fun onAudioDataReceived(data: ByteArray) {
if (azureAsrHelper.audioSourceType == AzureAsrHelper.AudioSourceType.EXTERNAL) {
override fun onAudioDataReceived(data: ByteArray,channel:Int) {
if (channel == 0) {
return
}
else if (channel == 1) {
if (isRecord) {
recordfile?.setAudioConfig(16000,1)
recordfile?.saveAudioDataToWav(data)
// 构建音频数据事件映射
val audioEvent = mapOf(
"type" to "audioData",
"data" to data,
"timestamp" to System.currentTimeMillis(),
"size" to data.size
)
// 发送音频数据事件到 Flutter 层
sendAudioDataEvent(audioEvent)
}
if (azureAsrHelper.audioSourceType == AzureAsrHelper.AudioSourceType.EXTERNAL) {
azureAsrHelper.audioStream?.saveAudioDataTo(data)
}
// 可选:处理音频数据
}
override fun onAudioDataReceivedCall(data: ByteArray) {
}
else if (channel == 2) {
if (isRecord) {
recordfile?.setAudioConfig(16000,2)
recordfile?.saveAudioDataToWav(data)
// 构建音频数据事件映射
val audioEvent = mapOf(
"type" to "audioData",
"data" to data,
"timestamp" to System.currentTimeMillis(),
"size" to data.size
)
// 发送音频数据事件到 Flutter 层
sendAudioDataEvent(audioEvent)
}
val sampleCount = data.size / 4 // 每个样本4字节(左右声道各2字节)
val leftBuffer = ByteArray(sampleCount * 2) // 左声道缓冲区
val rightBuffer = ByteArray(sampleCount * 2) // 右声道缓冲区
// 拆分交错的左右声道数据
for (i in 0 until sampleCount) {
val stereoIndex = i * 4
val monoIndex = i * 2
// 左声道(低位字节在前,高位字节在后)
leftBuffer[monoIndex] = data[stereoIndex]
leftBuffer[monoIndex + 1] = data[stereoIndex + 1]
// 右声道
rightBuffer[monoIndex] = data[stereoIndex + 2]
rightBuffer[monoIndex + 1] = data[stereoIndex + 3]
}
// FileLogger.d(tag, "onAudioDataReceivedCall: ${data.size}")
azureAstHelper.pushAudioData(data)
if (azureAsrHelper.audioSourceType == AzureAsrHelper.AudioSourceType.EXTERNAL) {
azureAsrHelper.audioStream?.saveAudioDataTo(rightBuffer)
}
azureAstHelper?.pushAudioData(leftBuffer)
}
// 可选:处理音频数据
}
/**
* 处理唤醒信号
* 在收到唤醒信号时启动语音识别

47
local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/tools/RecordFile.kt

@ -117,10 +117,10 @@ class RecordFile {
fos = FileOutputStream(currentAudioFile, true)
startWriteThread()
// 如果是双声道,创建左右声道文件
if (channels == 2) {
createStereoChannelFiles(filePath)
}
// // 如果是双声道,创建左右声道文件
// if (channels == 2) {
// createStereoChannelFiles(filePath)
// }
}
/**
@ -242,14 +242,14 @@ class RecordFile {
internal fun saveAudioDataToWav(buffer: ByteArray) {
if (fos == null || currentAudioFile == null) return
if (channels == 2) {
// 双声道:拆分左右声道
splitStereoToMono(buffer)
} else {
// if (channels == 2) {
// // 双声道:拆分左右声道
// splitStereoToMono(buffer)
// } else {
// 单声道:直接保存
writeQueue.offer(buffer.copyOf())
totalBytesWritten += buffer.size
}
// }
}
/**
@ -454,21 +454,22 @@ class RecordFile {
}
}
// 处理左右声道文件(如果是双声道)
if (channels == 2) {
leftChannelSuccess = closeStereoChannelFile(leftChannelFile, leftChannelFos, leftChannelBytesWritten, "左声道", isSave)
rightChannelSuccess = closeStereoChannelFile(rightChannelFile, rightChannelFos, rightChannelBytesWritten, "右声道", isSave)
// // 处理左右声道文件(如果是双声道)
// if (channels == 2) {
// leftChannelSuccess = closeStereoChannelFile(leftChannelFile, leftChannelFos, leftChannelBytesWritten, "左声道", isSave)
// rightChannelSuccess = closeStereoChannelFile(rightChannelFile, rightChannelFos, rightChannelBytesWritten, "右声道", isSave)
// 等待左右声道写入线程结束
leftChannelWriteThread?.join(500)
rightChannelWriteThread?.join(500)
}
return if (channels == 2) {
leftChannelSuccess && rightChannelSuccess
} else {
mainFileSuccess
}
// // 等待左右声道写入线程结束
// leftChannelWriteThread?.join(500)
// rightChannelWriteThread?.join(500)
// }
// return if (channels == 2) {
// leftChannelSuccess && rightChannelSuccess
// } else {
// mainFileSuccess
// }
return mainFileSuccess
} catch (e: Exception) {
Log.e("tag", "更新WAV文件头失败: ${e.message}")
return false

32
local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/tools/SimpleAudioReceiver.kt

@ -49,7 +49,7 @@ class SimpleAudioReceiver(private val context: Context) {
private val isRunning = AtomicBoolean(false)// 控制线程是否继续存在
private val isWriting = AtomicBoolean(false) // 控制是否应该写入数据
private var writeThread: Thread? = null
private var isInitialized = false
// 音频配置
private val channelConfig = AudioFormat.CHANNEL_IN_MONO
private val audioFormat = AudioFormat.ENCODING_PCM_16BIT
@ -59,6 +59,8 @@ class SimpleAudioReceiver(private val context: Context) {
// 录音文件处理
var recordfile: RecordFile? = null
var isRecord =false
var isContinuousRecognitionActive = false
/**
* 获取最优音频格式
*/
@ -94,8 +96,9 @@ class SimpleAudioReceiver(private val context: Context) {
// 初始化音频管理器
audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
originalAudioMode = audioManager?.mode ?: AudioManager.MODE_NORMAL
isInitialized = true
}
fun isInitialized(): Boolean = isInitialized
/**
* 启动写入线程
*/
@ -140,14 +143,14 @@ class SimpleAudioReceiver(private val context: Context) {
if (bytesToWrite < data.size) data.copyOf(bytesToWrite) else data
try {
// Log.d(TAG, "写入数据大小: ${finalData.size}")
Log.d(TAG, "写入数据大小: ${finalData.size},isContinuousRecognitionActive:${isContinuousRecognitionActive},isRecord${isRecord}")
if(audioDataCallback!=null){
audioDataCallback?.onAudio(finalData)
}
if(pushAudioStream!=null&&audioDataCallback==null){
if(pushAudioStream!=null&&isContinuousRecognitionActive){
pushAudioStream?.write(finalData)
}
if(recordfile!=null){
if(recordfile!=null&&isRecord){
recordfile?.saveAudioDataToWav(finalData)
}
@ -342,7 +345,23 @@ class SimpleAudioReceiver(private val context: Context) {
Log.e(TAG, "继续录音失败: ${e.message}")
}
}
/**
* 暂停麦克风捕获
*/
fun pauseMicrophoneCapture() {
try {
isWriting.set(false)
// 停止录音
audioRecord?.stop()
// 恢复音频模式 这里会导致关闭麦克风卡住待优化
//restoreCommunicationAudioMode()
} catch (e: Exception) {
Log.e(TAG, "暂停麦克风捕获失败: ${e.message}")
}
}
/**
* 停止麦克风捕获
*/
@ -376,6 +395,7 @@ class SimpleAudioReceiver(private val context: Context) {
// 释放录音实例
audioRecord?.release()
audioRecord = null
isInitialized = false
} catch (e: Exception) {
Log.e(TAG, "释放音频资源失败: ${e.message}")
e.printStackTrace()

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

@ -39,7 +39,7 @@ public class AzureAsrHelper: NSObject {
// 防抖机制相关变量 - 只保留开始操作的防抖
private var lastAudioStartTime: Int64 = 0
private let audioOperationDebounceInterval: Int64 = 500 // 500毫秒防抖间隔
private let audioOperationDebounceInterval: Int64 = 0 // 500毫秒防抖间隔
private var audioStartWorkItem: DispatchWorkItem?
// 串行队列和状态标记,用于顺序控制音频启动/停止,避免竞态
private let audioOpQueue = DispatchQueue(label: "com.azure.speech.audio.operations")
@ -47,8 +47,7 @@ public class AzureAsrHelper: NSObject {
private var isAudioStarted = false
private var pendingStopRequest = false
// 状态管理
private var _isContinuousRecognitionActive = false
// 配置参数
private var currentLanguage = "zh-CN"
@ -66,7 +65,7 @@ public class AzureAsrHelper: NSObject {
case external
}
private var audioSourceType = AudioSourceType.microphone
public var audioSourceType = AudioSourceType.microphone
// 音频处理
// private var externalAudioStream: ExternalAudioPullStream?
@ -240,7 +239,7 @@ public class AzureAsrHelper: NSObject {
///
/// 修复点:
/// 1) 新任务创建前,取消旧的未执行任务,确保只有“最后一次启动请求”会生效
/// 2) 任务执行前二次校验:必须是当前挂起任务且 _isContinuousRecognitionActive 仍为 true 才执行启动
/// 2) 任务执行前二次校验:必须是当前挂起任务且 audioStream?.isContinuousRecognitionActive 仍为 true 才执行启动
private func startAudioRecordAsync(
audioSourceType: AudioSourceType,
audioDataCallback: SimpleAudioReceiver.AudioDataCallback? = nil,
@ -264,9 +263,9 @@ public class AzureAsrHelper: NSObject {
completion(false)
return
}
// print("startAudioRecordAsync\(_isContinuousRecognitionActive)")
// print("startAudioRecordAsync\(audioStream?.isContinuousRecognitionActive)")
// // 二次校验:stop 可能已发生,确认仍需启动
// guard self._isContinuousRecognitionActive else {
// guard self.audioStream?.isContinuousRecognitionActive else {
// os_log("已收到停止请求,取消启动音频录制", log: self.log, type: .info)
// completion(false)
// return
@ -321,10 +320,12 @@ public class AzureAsrHelper: NSObject {
audioDataCallback: SimpleAudioReceiver.AudioDataCallback?
) -> Bool {
do {
audioStream?.startAudioRecord(
audioSourceType: audioSourceType == .microphone ? .microphone : .external,
audioDataCallback: audioDataCallback
)
if(!audioStream!.isContinuousRecognitionActive){
audioStream?.startAudioRecord(
audioSourceType: audioSourceType == .microphone ? .microphone : .external,
audioDataCallback: audioDataCallback
)
}
os_log("音频录制启动成功", log: log, type: .info)
return true
} catch {
@ -382,7 +383,7 @@ public class AzureAsrHelper: NSObject {
* @param audioDataCallback 音频数据回调
* @return 是否成功开始识别(调度成功即返回 true)
*
* 修复点:在音频启动成功后的闭包中,先判断 _isContinuousRecognitionActive 是否仍然为 true,
* 修复点:在音频启动成功后的闭包中,先判断 audioStream?.isContinuousRecognitionActive 是否仍然为 true,
* 若 stop 已发生则直接跳过 recognizer 的启动,避免“已停止但仍启动识别”的情况。
*/
public func startContinuousRecognition(
@ -399,7 +400,7 @@ public class AzureAsrHelper: NSObject {
return false
}
if _isContinuousRecognitionActive {
if audioStream?.isContinuousRecognitionActive == true {
print("连续识别已在进行中")
return true
}
@ -421,7 +422,7 @@ public class AzureAsrHelper: NSObject {
print("startContinuousRecognition:\(audioSourceType)")
// 移除这里的状态设置,等音频启动成功后再设置
// self._isContinuousRecognitionActive = true
// self.audioStream?.isContinuousRecognitionActive = true
// 异步启动音频处理
startAudioRecordAsync(audioSourceType: audioSourceType, audioDataCallback: audioDataCallback) { [weak self] (success: Bool) in
@ -429,8 +430,8 @@ public class AzureAsrHelper: NSObject {
if success {
// 只有在音频启动成功后才设置状态为 true
self._isContinuousRecognitionActive = true
print("startContinuousRecognition:\(self._isContinuousRecognitionActive)")
self.audioStream?.isContinuousRecognitionActive = true
print("startContinuousRecognition:\(self.audioStream?.isContinuousRecognitionActive)")
// 启动语音识别
do {
@ -439,18 +440,18 @@ public class AzureAsrHelper: NSObject {
} catch {
os_log("启动连续识别失败: %{public}@", log: self.log, type: .error, error.localizedDescription)
self.stopContinuousRecognition()
self._isContinuousRecognitionActive = false
self.audioStream?.isContinuousRecognitionActive = false
}
} else {
os_log("音频启动失败,无法开始识别", log: self.log, type: .error)
self._isContinuousRecognitionActive = false
self.audioStream?.isContinuousRecognitionActive = false
}
}
return true
} catch {
stopContinuousRecognition()
_isContinuousRecognitionActive = false
audioStream?.isContinuousRecognitionActive = false
return false
}
}
@ -478,7 +479,7 @@ public class AzureAsrHelper: NSObject {
guard let self = self else { return }
if success {
// 标记不再活跃,供启动任务二次校验
self._isContinuousRecognitionActive = false
self.audioStream?.isContinuousRecognitionActive = false
os_log("音频停止成功", log: self.log, type: .info)
} else {
os_log("音频停止失败", log: self.log, type: .error)
@ -497,7 +498,7 @@ public class AzureAsrHelper: NSObject {
return true
} catch {
// 强制重置状态
_isContinuousRecognitionActive = false
audioStream?.isContinuousRecognitionActive = false
// 关键:即使异常,也取消未执行的启动任务
cancelPendingAudioStart()
@ -532,7 +533,7 @@ public class AzureAsrHelper: NSObject {
* 检查连续识别是否活跃
*/
public func isContinuousRecognitionActive() -> Bool {
return self._isContinuousRecognitionActive
return self.audioStream?.isContinuousRecognitionActive == true
}
/**
@ -542,10 +543,10 @@ public class AzureAsrHelper: NSObject {
do {
print("释放所有资源:")
// 如果正在进行连续识别,先停止
if _isContinuousRecognitionActive {
if audioStream?.isContinuousRecognitionActive == true {
// 直接停止,不等待结果
try? recognizer?.stopContinuousRecognition()
_isContinuousRecognitionActive = false
audioStream?.isContinuousRecognitionActive = false
}
// 取消防抖任务 - 只取消开始操作的防抖
audioStartWorkItem?.cancel()
@ -559,11 +560,11 @@ public class AzureAsrHelper: NSObject {
audioConfig = nil
audioStream = nil
// 确保状态被重置
_isContinuousRecognitionActive = false
audioStream?.isContinuousRecognitionActive = false
//externalAudioStream = nil
} catch {
// 确保状态被重置
_isContinuousRecognitionActive = false
audioStream?.isContinuousRecognitionActive = false
//externalAudioStream = nil
audioConfig = nil
recognizer = nil
@ -599,13 +600,13 @@ public class AzureAsrHelper: NSObject {
*/
private func setupRecognizer() -> Bool {
do {
print("设置识别器\(_isContinuousRecognitionActive)")
print("设置识别器\(audioStream?.isContinuousRecognitionActive ?? false)")
// 如果正在进行连续识别,先停止
if _isContinuousRecognitionActive {
try? recognizer?.stopContinuousRecognition()
_isContinuousRecognitionActive = false
}
if audioStream?.isContinuousRecognitionActive == true {
try? recognizer?.stopContinuousRecognition()
audioStream?.isContinuousRecognitionActive = false
}
// 【优化】只有在音频流未初始化时才创建,避免重复初始化
if audioStream == nil || audioConfig == nil {
@ -741,7 +742,7 @@ public class AzureAsrHelper: NSObject {
self.isAutoDetectLanguage = (validatedSourceLang != validatedTargetLang)
// 如果正在识别,需要重新设置识别器
if _isContinuousRecognitionActive {
if audioStream?.isContinuousRecognitionActive == true {
let wasActive = stopContinuousRecognition()
if wasActive {
return setupRecognizer()
@ -815,7 +816,7 @@ public class AzureAsrHelper: NSObject {
print("会话结束事件:")
// 直接在当前线程调用回调
callback.onSessionStopped()
self._isContinuousRecognitionActive = false
self.audioStream?.isContinuousRecognitionActive = false
//self.stopAudioProcessing()
}
@ -865,12 +866,17 @@ public class AzureAsrHelper: NSObject {
/**
* 开启录音
* @param audioSourceType 音频源类型
* @param filePath 录音文件路径
* @param audioDataCallback 音频数据回调接口
*/
public func enableRecord(filePath: String, audioDataCallback: SimpleAudioReceiver.AudioDataCallback? = nil) {
public func enableRecord( audioSourceType: AudioSourceType = .microphone,filePath: String, audioDataCallback: SimpleAudioReceiver.AudioDataCallback? = nil) {
os_log("开启录音: %{public}@", log: log, type: .info, filePath)
self.audioSourceType = audioSourceType
if(audioSourceType == .external){
os_log("外部音频源不在这里录音", log: log, type: .info)
return
}
if audioStream == nil {
// 创建外部音频拉流对象
audioStream = SimpleAudioReceiver()
@ -881,21 +887,17 @@ public class AzureAsrHelper: NSObject {
audioStream?.recordfile = RecordFile()
}
self.audioSourceType = .microphone
// 修复:移除多余的 audioDataCallback 参数
// 修复:添加类型转换
audioStream?.startAudioRecord(
audioSourceType: {
switch audioSourceType {
case .microphone:
return SimpleAudioReceiver.AudioSourceType.microphone
case .external:
return SimpleAudioReceiver.AudioSourceType.external
}
}(),
audioDataCallback: audioDataCallback
)
if(!audioStream!.isContinuousRecognitionActive){
// 异步启动音频处理
startAudioRecordAsync(audioSourceType: audioSourceType, audioDataCallback: audioDataCallback) { [weak self] (success: Bool) in
guard let self = self else { return }
}
}
audioStream?.isRecord = true
audioStream?.recordfile?.closeFile(isSave: true)
audioStream?.recordfile?.creatingFiles(atPath: filePath)
}
@ -905,7 +907,10 @@ public class AzureAsrHelper: NSObject {
*/
public func moveFile(sourcePath: String, destPath: String)-> Bool {
if(audioSourceType == .external){
os_log(" 外部音频源不在这里移动文件", log: log, type: .info)
return true
}
audioStream?.recordfile?.moveFile(from: sourcePath, to: destPath) // Fixed method call
return true
}
@ -915,6 +920,10 @@ public class AzureAsrHelper: NSObject {
* 重命名指定路径的音频文件
*/
public func renameFile(filePath: String, newName: String)-> Bool {
if(audioSourceType == .external){
os_log(" 外部音频源不在这里重命名文件", log: log, type: .info)
return true
}
audioStream?.recordfile?.renameFile(at: filePath, to: newName) // Fixed method call
return true
}
@ -925,32 +934,47 @@ public class AzureAsrHelper: NSObject {
* 暂停录音
*/
public func pauseRecord() {
if(audioSourceType == .external){
os_log(" 外部音频源不在这里暂停录音", log: log, type: .info)
return
}
os_log("暂停录音", log: log, type: .info)
guard let audioStream = audioStream, audioStream.recordfile != nil else {
return
}
audioStream.stopMicrophoneCapture()
audioStream.isRecord = false
}
/**
* 继续录音
*/
public func resumeRecord() {
if(audioSourceType == .external){
os_log(" 外部音频源不在这里继续录音", log: log, type: .info)
return
}
os_log("继续录音", log: log, type: .info)
guard let audioStream = audioStream, audioStream.recordfile != nil else {
return
}
audioStream.resumeRecord()
audioStream.isRecord = true
}
/**
* 关闭录音
*/
public func stopRecord(isSave: Bool) {
if(audioSourceType == .external){
os_log(" 外部音频源不在这里关闭录音", log: log, type: .info)
return
}
guard let audioStream = audioStream, audioStream.recordfile != nil else {
return
}
audioStream.stopMicrophoneCapture()
audioStream.isRecord = false
if (!audioStream.isContinuousRecognitionActive) {
audioStream.stopMicrophoneCapture()
}
audioStream.recordfile?.isPause = false //
audioStream.recordfile?.closeFile(isSave: true) //
}

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

@ -49,10 +49,6 @@ import ble_service
}
// 替换原有的 StreamHandler 类
private typealias AsrEventStreamHandler = BaseEventStreamHandler
private typealias TtsEventStreamHandler = BaseEventStreamHandler
private typealias AudioDataEventStreamHandler = BaseEventStreamHandler
private typealias AstEventStreamHandler = BaseEventStreamHandler
// 事件类型枚举
@ -72,6 +68,11 @@ private typealias AstEventStreamHandler = BaseEventStreamHandler
let methodHandler: (FlutterMethodCall, @escaping FlutterResult) -> Void
}
private typealias AsrEventStreamHandler = BaseEventStreamHandler
private typealias TtsEventStreamHandler = BaseEventStreamHandler
private typealias AudioDataEventStreamHandler = BaseEventStreamHandler
private typealias AstEventStreamHandler = BaseEventStreamHandler
// ASR相关
private var asrChannel: FlutterMethodChannel?
private var asrEventChannel: FlutterEventChannel?
@ -96,8 +97,9 @@ private typealias AstEventStreamHandler = BaseEventStreamHandler
private var audioDataEventChannel: FlutterEventChannel?
internal var audioDataEventSink: FlutterEventSink?
// 录音相关
public var recordfile: RecordFile?
private var isRecord = false
// 创建事件回调
private var astEventCallback: AstEventCallback?
// 是否已添加AST事件监听器
@ -391,8 +393,14 @@ private func sendAstEvent(_ event: [String: Any]) {
return
}
do {
print("音频文件名称为: (filePath)")
try azureAsrHelper.renameFile(filePath: filePath, newName: newName)
print("renameFile音频文件名称为: \(filePath),新名称为: \(newName)")
if(azureAsrHelper.audioSourceType == .external){
try recordfile?.renameFile(at: filePath, to: newName)
}
else
{
try azureAsrHelper.renameFile(filePath: filePath, newName: filePath)
}
result(true)
} catch {
result(FlutterError(code: "RENAMEFILE_ERROR", message: error.localizedDescription, details: nil))
@ -406,8 +414,13 @@ private func sendAstEvent(_ event: [String: Any]) {
return
}
do {
print("音频文件名称为: (sourcePath)")
try azureAsrHelper.moveFile(sourcePath: sourcePath, destPath: destPath)
print("moveFile音频文件名称为: \(sourcePath),新名称为: \(destPath)")
if(azureAsrHelper.audioSourceType == .external){
try recordfile?.moveFile(from: sourcePath, to: destPath)
}
else{
try azureAsrHelper.moveFile(sourcePath: sourcePath, destPath: destPath)
}
result(true)
} catch {
result(FlutterError(code: "MOVEFILE_ERROR", message: error.localizedDescription, details: nil))
@ -422,13 +435,28 @@ private func sendAstEvent(_ event: [String: Any]) {
// 检查是否需要接受音频数据
let acceptAudioData = args["acceptAudioData"] as? Bool ?? false
// 检查是否需要接受音频数据
let isExternalActive = args["audioSourceType"] as? Bool ?? false
if isExternalActive {
if(recordfile == nil){
recordfile = RecordFile()
}
recordfile?.closeFile(isSave: true)
recordfile?.creatingFiles(atPath: filePath)
isRecord = true
}
// 根据参数确定音频源类型
let audioSourceType = isExternalActive ?
AzureAsrHelper.AudioSourceType.external :
AzureAsrHelper.AudioSourceType.microphone
print("startContinuousRecognition: \(audioSourceType)")
do {
if acceptAudioData {
// 使用复用的音频数据回调实例
try azureAsrHelper.enableRecord(filePath: filePath, audioDataCallback: audioDataCallback)
try azureAsrHelper.enableRecord(audioSourceType: audioSourceType, filePath: filePath, audioDataCallback: audioDataCallback)
} else {
try azureAsrHelper.enableRecord(filePath: filePath)
try azureAsrHelper.enableRecord(audioSourceType: audioSourceType, filePath: filePath)
}
result(true)
} catch {
@ -438,22 +466,34 @@ private func sendAstEvent(_ event: [String: Any]) {
case "pauseRecord":
isRecord = false
azureAsrHelper.pauseRecord()
result(true)
case "resumeRecord":
isRecord = true
azureAsrHelper.resumeRecord()
result(true)
case "stopRecord":
guard let args = call.arguments as? [String: Any],
let isSave = args["isSave"] as? Bool else {
result(FlutterError(code: "INVALID_ARGUMENTS", message: "isSave 参数不能为空", details: nil))
return
}
do {
try azureAsrHelper.stopRecord(isSave: isSave)
print("stopRecord : \(azureAsrHelper.audioSourceType)")
if(azureAsrHelper.audioSourceType == .external){
print("stopRecord音频文件名称为:)")
isRecord = false
recordfile?.closeFile(isSave: true)
}
else
{
try azureAsrHelper.stopRecord(isSave: isSave)
}
result(true)
} catch {
result(FlutterError(code: "STOPRECORD_ERROR", message: error.localizedDescription, details: nil))
@ -595,20 +635,20 @@ private func sendAstEvent(_ event: [String: Any]) {
// MARK: - AST 方法处理
private func handleAstMethodCall(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
switch call.method {
case "enableRecord":
guard let args = call.arguments as? [String: Any],
let filePath = args["filePath"] as? String else {
result(FlutterError(code: "INVALID_ARGUMENTS", message: "Missing filePath", details: nil))
return
}
do {
os_log("音频文件名称为: %@", log: log, type: .info, filePath)
try azureAstHelper.enableRecord(filePath: filePath)
result(true)
} catch {
result(FlutterError(code: "ENABLERECORD_ERROR", message: error.localizedDescription, details: nil))
}
// case "enableRecord":
// guard let args = call.arguments as? [String: Any],
// let filePath = args["filePath"] as? String else {
// result(FlutterError(code: "INVALID_ARGUMENTS", message: "Missing filePath", details: nil))
// return
// }
// do {
// os_log("音频文件名称为: %@", log: log, type: .info, filePath)
// try azureAstHelper.enableRecord(filePath: filePath)
// result(true)
// } catch {
// result(FlutterError(code: "ENABLERECORD_ERROR", message: error.localizedDescription, details: nil))
// }
case "startContinuousTranslation":
do {
@ -935,28 +975,79 @@ extension AzureSpeechPlugin: BleService.Callback {
/// 接收到音频数据回调
/// - Parameter data: 音频数据
public func onAudioDataReceived(data: Data) {
// 安全解包版本
public func onAudioDataReceived(data: Data, channel: Int32) {
if channel == 0 {
return
} else if channel == 1 {
if (isRecord) {
recordfile?.setAudioConfig(sampleRate: 16000, channels: 1)
recordfile?.saveAudioDataToWav(data)
handleAudioData(data)
}
// 安全解包版本
guard let audioStream = azureAsrHelper.audioStream else {
os_log("音频流未初始化", type: .error)
return
}
// print("liwei--------------接收到音频数据回调")
audioStream.saveAudioDataTo(data: data)
}
/// 接收到音频数据回调1
/// - Parameter data: 音频数据
public func onAudioDataReceivedCall(data: Data) {
// 安全解包版本
guard let audioStream = azureAstHelper.audioProcessor else {
azureAsrHelper.audioStream?.saveAudioDataTo(data: data)
}
else if channel == 2 {
if (isRecord) {
recordfile?.setAudioConfig(sampleRate: 16000, channels: 2)
recordfile?.saveAudioDataToWav(data)
handleAudioData(data)
}
let sampleCount = data.count / 4 // 每个样本4字节(左右声道各2字节)
guard sampleCount > 0 else {
print("双声道数据样本数为0")
return
}
var leftBuffer = Data()
var rightBuffer = Data()
leftBuffer.reserveCapacity(sampleCount * 2)
rightBuffer.reserveCapacity(sampleCount * 2)
data.withUnsafeBytes { bytes in
let uint8Ptr = bytes.bindMemory(to: UInt8.self)
for i in 0..<sampleCount {
let stereoIndex = i * 4
// 左声道(低位字节在前,高位字节在后)
leftBuffer.append(uint8Ptr[stereoIndex])
leftBuffer.append(uint8Ptr[stereoIndex + 1])
// 右声道
rightBuffer.append(uint8Ptr[stereoIndex + 2])
rightBuffer.append(uint8Ptr[stereoIndex + 3])
}
}
print("分离音频数据 - 左声道: \(leftBuffer.count) bytes, 右声道: \(rightBuffer.count) bytes")
// 安全解包版本
guard let audioStream = azureAsrHelper.audioStream else {
os_log("音频流未初始化", type: .error)
return
}
azureAsrHelper.audioStream?.saveAudioDataTo(data: rightBuffer)
// // 安全解包版本
// guard let audioProcessor = azureAstHelper.audioProcessor else {
// os_log("音频流未初始化", type: .error)
// return
// }
// print("liwei--------------接收到音频数据回调1")
audioStream.saveAudioDataTo(data: data)
azureAstHelper.pushAudioData(audioData: leftBuffer)
}
}
/// 接收到编码数据回调
/// - Parameter data: 编码数据
public func onEncodedDataReceived(data: Data) {

5
local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/IntegratedSpeechTranslationService.swift

@ -331,6 +331,7 @@ import os.log
private func initializeAudioProcessor() {
audioProcessor = SimpleAudioReceiver()
audioProcessor?.initAudioRecord()
// 检查音频配置是否已存在
if audioConfig == nil, let pushStream = audioProcessor?.pushAudioStream {
audioConfig = SPXAudioConfiguration(streamInput: pushStream)
@ -547,6 +548,7 @@ import os.log
* 开始连续语音翻译
*/
public func startContinuousTranslation() -> Bool {
audioProcessor?.isContinuousRecognitionActive = true
os_log("尝试启动连续翻译,当前状态:初始化=%@, 识别中=%@", log: log, type: .info,
serviceState.isInitialized ? "true" : "false",
serviceState.isRecognizing ? "true" : "false")
@ -597,7 +599,8 @@ import os.log
*/
public func stopContinuousTranslation() {
do {
self.serviceState.isRecognizing = false
audioProcessor?.isContinuousRecognitionActive = false
self.serviceState.isRecognizing = false
try recognizer?.stopContinuousRecognition()
audioProcessor?.stopMicrophoneCapture()
recordFile?.closeFile(isSave: true)

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

@ -10,10 +10,11 @@ public class MicrophoneCapture: NSObject {
*/
public static let shared = MicrophoneCapture()
private var audioEngine: AVAudioEngine!
private var audioSession: AVAudioSession!
// 修复:将强制解包改为可选类型,避免内存访问错误
private var audioEngine: AVAudioEngine?
private var audioSession: AVAudioSession?
private var audioFormat: AVAudioFormat?
private var audioInputNode: AVAudioInputNode!
private var audioInputNode: AVAudioInputNode?
private var sampleRate: Double = 16000 // 修改为16000采样率,符合Microsoft要求
public var isCapturing = false
public var hasBluetoothDevices = false
@ -84,8 +85,13 @@ public class MicrophoneCapture: NSObject {
audioFormat = getOptimalAudioFormat()
audioSession = AVAudioSession.sharedInstance()
// 修复:添加安全检查
guard let audioSession = audioSession else {
print("音频会话初始化失败")
return
}
do {
try audioSession.setPreferredSampleRate(sampleRate)
try audioSession.setPreferredIOBufferDuration(0.005) // 5ms缓冲
@ -121,10 +127,11 @@ public class MicrophoneCapture: NSObject {
* 配置蓝牙模式的音频路由:末端在手机,输出端在耳机
*/
private func configureAudioForBluetoothMode() {
guard let availableInputs = audioSession.availableInputs else { return }
guard let audioSession = audioSession,
let availableInputs = audioSession.availableInputs else { return }
// 强制使用内置麦克风作为输入(手机)
if let builtInMic = availableInputs.first(where: { $0.portType == .builtInMic }) {
if let builtInMic = availableInputs.first(where: { $0.portType == AVAudioSession.Port.builtInMic }) {
do {
try audioSession.setPreferredInput(builtInMic)
print("蓝牙模式:音频输入设置为内置麦克风(手机)")
@ -139,7 +146,7 @@ public class MicrophoneCapture: NSObject {
// 确保音频输出通过蓝牙耳机
let hasBluetoothOutput = currentRoute.outputs.contains { output in
[.bluetoothHFP, .bluetoothLE].contains(output.portType)
[AVAudioSession.Port.bluetoothHFP, AVAudioSession.Port.bluetoothLE].contains(where: { $0 == output.portType })
}
if hasBluetoothOutput {
@ -164,10 +171,11 @@ public class MicrophoneCapture: NSObject {
* 配置手机模式的音频路由:末端和输出端都在手机
*/
private func configureAudioForPhoneMode() {
guard let availableInputs = audioSession.availableInputs else { return }
guard let audioSession = audioSession,
let availableInputs = audioSession.availableInputs else { return }
// 使用内置麦克风作为输入(手机)
if let builtInMic = availableInputs.first(where: { $0.portType == .builtInMic }) {
if let builtInMic = availableInputs.first(where: { $0.portType == AVAudioSession.Port.builtInMic }) {
do {
// 设置音频会话参数
try audioSession.setCategory(.playAndRecord,
@ -199,6 +207,9 @@ public class MicrophoneCapture: NSObject {
public func startCapture() throws {
print("开始捕获\(isCapturing)")
guard !isCapturing else { return }
guard let audioSession = audioSession else {
throw NSError(domain: "音频会话未初始化", code: -1)
}
// 检查麦克风权限
switch audioSession.recordPermission {
@ -250,20 +261,24 @@ public class MicrophoneCapture: NSObject {
* 根据Microsoft音频堆栈要求优化格式转换
*/
private func setupAudioEngine() throws {
// 修复:先安全清理现有资源
cleanupAudioEngine()
audioEngine = AVAudioEngine()
guard let audioEngine = audioEngine else {
throw NSError(domain: "AudioSetup", code: 1, userInfo: [NSLocalizedDescriptionKey: "音频引擎初始化失败"])
}
audioInputNode = audioEngine.inputNode
// 获取实际硬件采样率
sampleRate = audioSession.sampleRate
// 安全地停止音频引擎
if let engine = audioEngine {
engine.stop()
guard let audioInputNode = audioInputNode else {
throw NSError(domain: "AudioSetup", code: 1, userInfo: [NSLocalizedDescriptionKey: "音频输入节点获取失败"])
}
// 安全地移除音频处理块
if let inputNode = audioInputNode {
inputNode.removeTap(onBus: 0)
// 获取实际硬件采样率
if let audioSession = audioSession {
sampleRate = audioSession.sampleRate
}
// 设置音频格式
@ -274,72 +289,56 @@ public class MicrophoneCapture: NSObject {
try audioInputNode.setVoiceProcessingEnabled(true)
}
// 检查输入格式是否符合Microsoft要求
let needsConversion = !isMicrosoftCompatibleFormat(inputFormat)
// if needsConversion {
// 需要格式转换
guard let targetFormat = audioFormat,
let converter = AVAudioConverter(from: inputFormat, to: targetFormat) else {
throw NSError(domain: "AudioSetup", code: 2)
}
// 需要格式转换
guard let targetFormat = audioFormat,
let converter = AVAudioConverter(from: inputFormat, to: targetFormat) else {
throw NSError(domain: "AudioSetup", code: 2, userInfo: [NSLocalizedDescriptionKey: "音频格式转换器创建失败"])
}
print("输入格式不符合Microsoft要求,进行格式转换")
print("输入格式: \(inputFormat.sampleRate)Hz, \(inputFormat.commonFormat.rawValue)")
print("目标格式: \(targetFormat.sampleRate)Hz, \(targetFormat.commonFormat.rawValue)")
// 添加tap进行格式转换
audioInputNode.installTap(onBus: 0,
bufferSize: 1024,
format: inputFormat) { [weak self] buffer, when in
print("输入格式不符合Microsoft要求,进行格式转换")
print("输入格式: \(inputFormat.sampleRate)Hz, \(inputFormat.commonFormat.rawValue)")
print("目标格式: \(targetFormat.sampleRate)Hz, \(targetFormat.commonFormat.rawValue)")
guard let strongSelf = self else { return }
// 添加tap进行格式转换
audioInputNode.installTap(onBus: 0,
bufferSize: 1024,
format: inputFormat) { [weak self] buffer, when in
guard let strongSelf = self else { return }
// 创建目标格式的音频缓冲区
let convertedBuffer = AVAudioPCMBuffer(
pcmFormat: targetFormat,
frameCapacity: AVAudioFrameCount(
targetFormat.sampleRate * Double(buffer.frameLength) / buffer.format.sampleRate
)
)!
var error: NSError?
// 执行音频格式转换
let status = converter.convert(
to: convertedBuffer,
error: &error,
withInputFrom: { inNumPackets, outStatus in
outStatus.pointee = .haveData
return buffer
}
// 修复:添加安全检查,避免强制解包崩溃
guard let convertedBuffer = AVAudioPCMBuffer(
pcmFormat: targetFormat,
frameCapacity: AVAudioFrameCount(
targetFormat.sampleRate * Double(buffer.frameLength) / buffer.format.sampleRate
)
// 转换成功且无错误
if status == .haveData, error == nil {
let data = strongSelf.audioBufferToData(convertedBuffer)
//print("转换后数据长度: \(data.count)")
strongSelf.audioDataHandler?(data)
}
) else {
print("创建转换缓冲区失败")
return
}
// } else {
// // 格式已符合Microsoft要求,直接使用
// print("输入格式符合Microsoft要求,无需转换")
// print("格式: \(inputFormat.sampleRate)Hz, \(inputFormat.commonFormat.rawValue)")
// audioInputNode.installTap(onBus: 0,
// bufferSize: 1024,
// format: inputFormat) { [weak self] buffer, when in
// guard let strongSelf = self else { return }
//strongSelf.processAudioBuffer(buffer)
// // 直接处理原始音频数据
// let data = strongSelf.audioBufferToData(convertedBuffer)
// print("转换后数据长度: \(data.count)")
// strongSelf.audioDataHandler?(data)
// }
// }
var error: NSError?
// 执行音频格式转换
let status = converter.convert(
to: convertedBuffer,
error: &error,
withInputFrom: { inNumPackets, outStatus in
outStatus.pointee = .haveData
return buffer
}
)
// 转换成功且无错误
if status == .haveData, error == nil {
let data = strongSelf.audioBufferToData(convertedBuffer)
strongSelf.audioDataHandler?(data)
} else if let error = error {
print("音频格式转换失败: \(error.localizedDescription)")
}
}
// 启动引擎
do {
@ -347,9 +346,26 @@ public class MicrophoneCapture: NSObject {
isCapturing = true
} catch {
print("音频引擎启动失败: \(error.localizedDescription)")
throw error
}
}
/**
* 安全清理音频引擎资源
* 防止内存访问错误
*/
private func cleanupAudioEngine() {
// 安全地停止音频引擎
audioEngine?.stop()
// 安全地移除音频处理块
audioInputNode?.removeTap(onBus: 0)
// 重置引用
audioInputNode = nil
audioEngine = nil
}
/**
* 将音频缓冲区转换为Data格式
* 支持16位整数和32位浮点格式
@ -423,36 +439,36 @@ public class MicrophoneCapture: NSObject {
guard isCapturing else { return }
isCapturing = false
// 安全地停止音频引擎
if let engine = audioEngine {
engine.stop()
}
// 修复:使用安全的清理方法
cleanupAudioEngine()
// 安全地移除音频处理块
if let inputNode = audioInputNode {
inputNode.removeTap(onBus: 0)
}
// 安全地处理音频会话
guard let audioSession = audioSession else { return }
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])
do {
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)
} catch {
print("音频会话配置失败: \(error.localizedDescription)")
}
try? audioSession.overrideOutputAudioPort(.none)
try? audioSession.setActive(true)
}
/**
@ -462,6 +478,10 @@ public class MicrophoneCapture: NSObject {
func reset() {
stopCapture()
audioDataHandler = nil
// 修复:完全清理资源
cleanupAudioEngine()
audioSession = nil
audioFormat = nil
}

66
local_plugins/azure_speech/ios/azure_speech/Sources/tools/RecordFile.swift

@ -10,11 +10,47 @@ public class RecordFile {
private var isWriting = false
private var dataBuffer = Data()
private var totalBytesWritten = 0
private let sampleRate: UInt32 = 16000
// 音频配置 - 可配置的采样率和声道数
private var sampleRate: UInt32 = 16000
private var channels: UInt32 = 1 // 1=单声道, 2=立体声
private var lastSavedFile: URL?
public var isPause = false
var fileName = ""
public init() {}
public init() {}
/**
* 设置音频配置
* @param sampleRate 采样率 (8000, 16000, 24000, 32000, 44100, 48000)
* @param channels 声道数 (1=单声道, 2=立体声)
*/
public func setAudioConfig(sampleRate: UInt32, channels: UInt32) {
self.sampleRate = sampleRate
self.channels = channels
print("音频配置已更新: 采样率=\(sampleRate)Hz, 声道数=\(channels)")
}
/**
* 获取当前音频配置信息
* @return Dictionary包含采样率和声道数信息
*/
public func getAudioConfig() -> [String: UInt32] {
return [
"sampleRate": sampleRate,
"channels": channels
]
}
/**
* 检查是否为双声道模式
* @return true表示双声道,false表示单声道
*/
public func isStereoMode() -> Bool {
return channels == 2
}
// 创建音频文件并初始化 WAV 头
public func creatingFiles(atPath path: String) {
guard fileHandle == nil else { return }
@ -87,10 +123,16 @@ public class RecordFile {
}
}
// 生成 WAV 文件头
/**
* 生成WAV文件头,支持可配置的采样率和声道数
* @param dataLength 音频数据长度
* @return WAV文件头数据
*/
private func generateWavHeader(dataLength: Int) -> Data {
let totalLength = 36 + dataLength
let byteRate = sampleRate * 2
let totalLength = 36 + dataLength // RIFF块总长度 = 头部36字节 + 音频数据
let bytesPerSample: UInt32 = 2 // 16位PCM
let byteRate = sampleRate * channels * bytesPerSample // 采样率 * 声道数 * 字节/样本
let blockAlign = channels * bytesPerSample // 声道数 * 字节/样本
var header = Data()
header.append("RIFF".data(using: .ascii)!)
@ -104,20 +146,26 @@ public class RecordFile {
header.append("fmt ".data(using: .ascii)!)
header.append(Data(bytes: [16, 0, 0, 0])) // PCM 头长度
header.append(Data(bytes: [1, 0])) // PCM 格式
header.append(Data(bytes: [1, 0])) // 单声道
header.append(Data(bytes: [
UInt8(channels & 0xFF),
UInt8((channels >> 8) & 0xFF)
])) // 声道数
header.append(Data(bytes: [
UInt8(sampleRate & 0xFF),
UInt8((sampleRate >> 8) & 0xFF),
UInt8((sampleRate >> 16) & 0xFF),
UInt8((sampleRate >> 24) & 0xFF)
]))
])) // 采样率
header.append(Data(bytes: [
UInt8(byteRate & 0xFF),
UInt8((byteRate >> 8) & 0xFF),
UInt8((byteRate >> 16) & 0xFF),
UInt8((byteRate >> 24) & 0xFF)
]))
header.append(Data(bytes: [2, 0])) // 块对齐
])) // 字节率
header.append(Data(bytes: [
UInt8(blockAlign & 0xFF),
UInt8((blockAlign >> 8) & 0xFF)
])) // 块对齐
header.append(Data(bytes: [16, 0])) // 样本位数
header.append("data".data(using: .ascii)!)
header.append(Data(bytes: [

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

@ -42,6 +42,9 @@ public class SimpleAudioReceiver: NSObject {
private var audioDataCallback: AudioDataCallback?
private var isRunning = false
public var _isWriting = false
public var isRecord = false
public var isContinuousRecognitionActive = false
private var isInitialized = false
private let bufferSize: Int = 4096
private var writeThread: DispatchQueue?
private var currentRoute: AudioOutputRoute?
@ -63,7 +66,6 @@ public class SimpleAudioReceiver: NSObject {
// .audioDataHandler = { data in
// self.writeQueue.offer(data)
// }
print("初始化了")
pushAudioStream = SPXPushAudioInputStream()
@ -74,8 +76,18 @@ public class SimpleAudioReceiver: NSObject {
// 创建新的写线程
writeThread = DispatchQueue(label: "audio.stream.writer")
startWriteThread()
isInitialized = true
}
/**
* 检查音频接收器是否已初始化
* @return 如果已初始化则返回true,否则返回false
*/
public func getIsInitialized() -> Bool {
print("getIsInitialized=\(isInitialized)")
return isInitialized
}
/**
* 设置音频配置参数
@ -144,11 +156,11 @@ public class SimpleAudioReceiver: NSObject {
if self.audioDataCallback != nil {
self.audioDataCallback!.onAudio(dataToWrite)
}
if self.pushAudioStream != nil && self.audioDataCallback == nil {
if self.pushAudioStream != nil && isContinuousRecognitionActive {
try self.pushAudioStream?.write(dataToWrite)
}
if self.recordfile != nil {
if self.recordfile != nil && isRecord{
// print("写入recordfile数据长度: \(dataToWrite.count)")
recordfile?.saveAudioDataToWav(dataToWrite)
}
@ -330,6 +342,10 @@ public func restoreOriginalAudioState() {
print("stopMicrophoneCapture")
}
/**
* 释放音频资源
* 安全地清理音频引擎和相关资源
*/
public func releaseAudioResources() {
print("释放了")
stopMicrophoneCapture()

4
local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleConst.kt

@ -90,8 +90,10 @@ object BleConst {
const val CODEC_CONTROL_CLOSE = 0x00
/** 音乐或者通话远端声音 */
const val CODEC_CONTROL_DECODE_ON = 0xA1
/** mic和dac(音乐或者通话远端)声音 */
/** mic和dac(音乐或者通话远端)声音+翻译后重新编码 */
const val CODEC_CONTROL_A2DP_PLAY = 0xA2
/** mic和dac(音乐或者通话远端)声音 */
const val CODEC_CONTROL_CALL_RECORD_PLAY = 0xA3
/** 打开编码指令 */
const val CODEC_CONTROL_ENCODE_ON = 0xB1
/** 左声道 */

73
local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt

@ -53,9 +53,7 @@ object BleService {
fun onConnectionStateChanged(state: Int)
// 数据相关回调
fun onAudioDataReceived(data: ByteArray)
// 数据相关回调
fun onAudioDataReceivedCall(data: ByteArray)
fun onAudioDataReceived(data: ByteArray,channel:Int)
// 唤醒信号相关回调
fun onWakeupSignalReceived()
@ -1054,11 +1052,13 @@ startBytesStatistics()
BleConst.CODEC_CONTROL_CLOSE -> "已关闭编解码"
BleConst.CODEC_CONTROL_DECODE_ON -> "已打开解码"
BleConst.CODEC_CONTROL_A2DP_PLAY -> "A2DP播放模式"
BleConst.CODEC_CONTROL_CALL_RECORD_PLAY -> "通话记录播放模式"
BleConst.CODEC_CONTROL_ENCODE_ON -> "已打开编码"
else -> "未知状态($codecStatus)"
}
if (codecStatus == BleConst.CODEC_CONTROL_DECODE_ON ||
codecStatus == BleConst.CODEC_CONTROL_A2DP_PLAY ||
codecStatus == BleConst.CODEC_CONTROL_CALL_RECORD_PLAY ||
codecStatus == BleConst.CODEC_CONTROL_ENCODE_ON
) {
recordfile1!!.closeFile()
@ -1276,38 +1276,8 @@ startBytesStatistics()
opusManager?.startDecodeStream(option, object : OnDecodeStreamCallback {
override fun onDecodeStream(data: ByteArray?) {
if (data != null) {
// Log.d(TAG, "Opus解码数据: ${data.size} bytes")
if (option!!.getChannel() == 2) {
//解码数据再重新编码回去
val sampleCount = data.size / 4 // 每个样本4字节(左右声道各2字节)
val leftBuffer = ByteArray(sampleCount * 2) // 左声道缓冲区
val rightBuffer = ByteArray(sampleCount * 2) // 右声道缓冲区
// 拆分交错的左右声道数据
for (i in 0 until sampleCount) {
val stereoIndex = i * 4
val monoIndex = i * 2
// 左声道(低位字节在前,高位字节在后)
leftBuffer[monoIndex] = data[stereoIndex]
leftBuffer[monoIndex + 1] = data[stereoIndex + 1]
// 右声道
rightBuffer[monoIndex] = data[stereoIndex + 2]
rightBuffer[monoIndex + 1] = data[stereoIndex + 3]
}
// //重新编码
// opusManager?.writeEncodeStream(rightBuffer)
//notifyAudioDataReceived1(leftBuffer)
//回调
notifyAudioDataReceived(rightBuffer)//对方的
notifyAudioDataReceived1(leftBuffer)//自己的
} else if (option!!.getChannel() == 1) {
notifyAudioDataReceived(data)
if (option!!.getChannel() >= 1) {
notifyAudioDataReceived(data,option!!.getChannel())
} else {
Log.e(TAG, "Opus解码数据错误: ${data.size} bytes")
}
@ -1707,6 +1677,22 @@ startBytesStatistics()
)
)
}
/**
* 控制编解码 - 打开解码
*/
fun openCallRecordDecoder(): Boolean {
Log.i(TAG, "打开编码 0xA3")
//双声道 80字节
startOpusStreamDecoding(false, 2, 16000, 80)
// Log.i(TAG, "打开解码...")
return sendCommand(
BleConst.CMD_CONTROL_CODEC.toByte(), byteArrayOf(
BleConst.CODEC_CONTROL_CALL_RECORD_PLAY.toByte(),
BleConst.AUDIO_CHANNEL_RIGHT.toByte()
)
)
}
/**
* 控制编解码 - A2DP播放
@ -1980,28 +1966,17 @@ startBytesStatistics()
/**
* 向所有回调监听器分发音频数据
*/
private fun notifyAudioDataReceived(data: ByteArray) {
for (callback in callbacks) {
try {
callback.onAudioDataReceived(data)
} catch (e: Exception) {
Log.e(TAG, "分发音频数据回调异常", e)
}
}
}
/**
* 向所有回调监听器分发音频数据
*/
private fun notifyAudioDataReceived1(data: ByteArray) {
private fun notifyAudioDataReceived(data: ByteArray,channel:Int) {
for (callback in callbacks) {
try {
callback.onAudioDataReceivedCall(data)
callback.onAudioDataReceived(data,channel)
} catch (e: Exception) {
Log.e(TAG, "分发音频数据回调异常", e)
}
}
}
/**
* 向所有回调监听器分发唤醒信号
*/

10
local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleServicePlugin.kt

@ -209,6 +209,10 @@ class BleServicePlugin : FlutterPlugin, MethodCallHandler, ActivityAware,
val success = BleService.openA2DPDecoder()
result.success(success)
}
"openCallRecordDecoder" -> {
val success = BleService.openCallRecordDecoder()
result.success(success)
}
"closeCodec" -> {
val success = BleService.closeCodec()
result.success(success)
@ -357,11 +361,7 @@ class BleServicePlugin : FlutterPlugin, MethodCallHandler, ActivityAware,
sendEvent(statusEventSink, stateMap, "发送连接状态异常")
}
override fun onAudioDataReceived(data: ByteArray) {
// sendEvent(dataEventSink, mapOf("type" to "audioData", "data" to data), "发送音频数据异常")
}
override fun onAudioDataReceivedCall(data: ByteArray) {
override fun onAudioDataReceived(data: ByteArray,channel:Int) {
// sendEvent(dataEventSink, mapOf("type" to "audioData", "data" to data), "发送音频数据异常")
}

2
local_plugins/ble_service/ios/ble_service/Sources/ble_service/BleConst.swift

@ -93,6 +93,8 @@ class BleConst {
static let CODEC_CONTROL_DECODE_ON: UInt8 = 0xA1
/** mic和dac(音乐或者通话远端)声音 */
static let CODEC_CONTROL_A2DP_PLAY: UInt8 = 0xA2
/** mic和dac(音乐或者通话远端)声音 */
static let CODEC_CONTROL_CALL_RECORD_PLAY: UInt8 = 0xA3
/** 打开编码指令 */
static let CODEC_CONTROL_ENCODE_ON: UInt8 = 0xB1
/** 左声道 */

30
local_plugins/ble_service/ios/ble_service/Sources/ble_service/BleService.swift

@ -27,8 +27,8 @@ public class BleService: NSObject {
func onConnectionStateChanged(state: Int)
// 数据相关回调
func onAudioDataReceived(data: Data)
func onAudioDataReceivedCall(data: Data)
func onAudioDataReceived(data: Data, channel: Int32)
// 唤醒信号回调
func onWakeupSignalReceived()
@ -524,15 +524,9 @@ private var cmdReplyType: UInt8 = 0
}
/// 通知所有回调接收到音频数据
private func notifyAudioDataReceived(data: Data) {
for callback in callbacks {
callback.onAudioDataReceived(data: data)
}
}
/// 通知所有回调接收到音频数据
private func notifyAudioDataReceived1(data: Data) {
private func notifyAudioDataReceived(data: Data, channel: Int32) {
for callback in callbacks {
callback.onAudioDataReceivedCall(data: data)
callback.onAudioDataReceived(data: data, channel: channel)
}
}
@ -642,6 +636,12 @@ private var cmdReplyType: UInt8 = 0
let paramData = Data([BleConst.CODEC_CONTROL_A2DP_PLAY, BleConst.AUDIO_CHANNEL_RIGHT])
return sendCommand(BleConst.CMD_CONTROL_CODEC, data: paramData)
}
/// 打开通话记录解码器并开始录制
func openCallRecordDecoder() -> Bool {
os_log("打开通话记录解码并开始录制...", log: logger, type: .info)
let paramData = Data([BleConst.CODEC_CONTROL_CALL_RECORD_PLAY, BleConst.AUDIO_CHANNEL_RIGHT])
return sendCommand(BleConst.CMD_CONTROL_CODEC, data: paramData)
}
/// 关闭编解码器并停止录制
/// - Returns: 操作是否成功
@ -983,6 +983,8 @@ private var cmdReplyType: UInt8 = 0
statusDesc = "已打开解码"
case Int(BleConst.CODEC_CONTROL_A2DP_PLAY):
statusDesc = "A2DP播放模式"
case Int(BleConst.CODEC_CONTROL_CALL_RECORD_PLAY):
statusDesc = "通话录音播放模式"
case Int(BleConst.CODEC_CONTROL_ENCODE_ON):
statusDesc = "已打开编码"
default:
@ -1184,14 +1186,10 @@ private var cmdReplyType: UInt8 = 0
@available(iOS 13.0, *)
extension BleService: SwiftOpusAudioProcessor.AudioDataCallback {
/// 接收到解码后的PCM音频数据
func onAudioDataReceived(data: Data) {
notifyAudioDataReceived(data: data)
func onAudioDataReceived(data: Data, channel: Int32) {
notifyAudioDataReceived(data: data, channel: channel)
}
/// 接收到解码后的PCM音频数据
func onAudioDataReceivedCall(data: Data) {
notifyAudioDataReceived1(data: data)
}
/// 接收到编码后的音频数据
/// - Parameter data: 编码后的音频数据

13
local_plugins/ble_service/ios/ble_service/Sources/ble_service/SwiftBleServicePlugin.swift

@ -100,6 +100,9 @@ public class SwiftBleServicePlugin: NSObject, FlutterPlugin {
case "openA2DPDecoder":
result(BleService.shared.openA2DPDecoder())
case "openCallRecordDecoder":
result(BleService.shared.openCallRecordDecoder())
case "closeCodec":
result(BleService.shared.closeCodec())
@ -225,21 +228,19 @@ extension SwiftBleServicePlugin: BleService.Callback {
}
}
public func onAudioDataReceived(data: Data) {
public func onAudioDataReceived(data: Data, channel: Int32) {
if let eventSink = dataEventSink {
DispatchQueue.main.async {
eventSink([
"type": "audioData",
"data": FlutterStandardTypedData(bytes: data)
"data": FlutterStandardTypedData(bytes: data),
"channel": channel
])
}
}
}
public func onAudioDataReceivedCall(data: Data) {
}
public func onWakeupSignalReceived() {
if let eventSink = statusEventSink {
DispatchQueue.main.async {

93
local_plugins/ble_service/ios/ble_service/Sources/ble_service/SwiftOpusAudioProcessor.swift

@ -8,8 +8,7 @@ class SwiftOpusAudioProcessor: NSObject {
/// 音频数据回调协议
protocol AudioDataCallback: AnyObject {
func onAudioDataReceived(data: Data)
func onAudioDataReceivedCall(data: Data)
func onAudioDataReceived(data: Data, channel: Int32)
func onEncodedDataReceived(data: Data) // 编码数据回调
}
@ -284,61 +283,61 @@ class SwiftOpusAudioProcessor: NSObject {
}
print("Opus解码数据: \(data.count) bytes, 声道数: \(channels)")
if channels == 2 {
processStereoAudioData(data)
} else if channels == 1 {
DispatchQueue.main.async { [weak self] in
self?.callback?.onAudioDataReceived(data: data)
DispatchQueue.main.async { [weak self] in
self?.callback?.onAudioDataReceived(data: data, channel: self?.channels ?? 0)
}
} else {
print("不支持的声道数: \(channels)")
}
// if channels == 2 {
// processStereoAudioData(data)
// } else if channels == 1 {
// } else {
// print("不支持的声道数: \(channels)")
// }
}
/// 处理双声道音频数据,分离左右声道
/// - Parameter stereoData: 交错的双声道PCM数据
private func processStereoAudioData(_ stereoData: Data) {
let sampleCount = stereoData.count / 4 // 每个样本4字节(左右声道各2字节)
// /// 处理双声道音频数据,分离左右声道
// /// - Parameter stereoData: 交错的双声道PCM数据
// private func processStereoAudioData(_ stereoData: Data) {
// let sampleCount = stereoData.count / 4 // 每个样本4字节(左右声道各2字节)
guard sampleCount > 0 else {
print("双声道数据样本数为0")
return
}
// guard sampleCount > 0 else {
// print("双声道数据样本数为0")
// return
// }
var leftBuffer = Data()
var rightBuffer = Data()
// var leftBuffer = Data()
// var rightBuffer = Data()
leftBuffer.reserveCapacity(sampleCount * 2)
rightBuffer.reserveCapacity(sampleCount * 2)
// leftBuffer.reserveCapacity(sampleCount * 2)
// rightBuffer.reserveCapacity(sampleCount * 2)
stereoData.withUnsafeBytes { bytes in
let uint8Ptr = bytes.bindMemory(to: UInt8.self)
// stereoData.withUnsafeBytes { bytes in
// let uint8Ptr = bytes.bindMemory(to: UInt8.self)
for i in 0..<sampleCount {
let stereoIndex = i * 4
// for i in 0..<sampleCount {
// let stereoIndex = i * 4
// 左声道(低位字节在前,高位字节在后)
leftBuffer.append(uint8Ptr[stereoIndex])
leftBuffer.append(uint8Ptr[stereoIndex + 1])
// // 左声道(低位字节在前,高位字节在后)
// leftBuffer.append(uint8Ptr[stereoIndex])
// leftBuffer.append(uint8Ptr[stereoIndex + 1])
// 右声道
rightBuffer.append(uint8Ptr[stereoIndex + 2])
rightBuffer.append(uint8Ptr[stereoIndex + 3])
}
}
print("分离音频数据 - 左声道: \(leftBuffer.count) bytes, 右声道: \(rightBuffer.count) bytes")
// 使用SpeexKit编码右声道数据
//encodePCMData(rightBuffer)
// 回调分离后的音频数据
DispatchQueue.main.async { [weak self] in
self?.callback?.onAudioDataReceived(data: rightBuffer)
self?.callback?.onAudioDataReceivedCall(data: leftBuffer)
}
}
// // 右声道
// rightBuffer.append(uint8Ptr[stereoIndex + 2])
// rightBuffer.append(uint8Ptr[stereoIndex + 3])
// }
// }
// print("分离音频数据 - 左声道: \(leftBuffer.count) bytes, 右声道: \(rightBuffer.count) bytes")
// // 使用SpeexKit编码右声道数据
// //encodePCMData(rightBuffer)
// // 回调分离后的音频数据
// DispatchQueue.main.async { [weak self] in
// self?.callback?.onAudioDataReceived(data: rightBuffer)
// self?.callback?.onAudioDataReceivedCall(data: leftBuffer)
// }
// }
// MARK: - 状态查询方法

12
local_plugins/ble_service/lib/ble_service.dart

@ -138,6 +138,18 @@ class BleService {
}
}
/// 打开解码器
Future<bool> openCallRecordDecoder() async {
try {
final result =
await _methodChannel.invokeMethod<bool>('openCallRecordDecoder');
return result ?? false;
} catch (e) {
print('打开解码器失败: $e');
return false;
}
}
/// 关闭编解码器
Future<bool> closeCodec() async {
try {

120
local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/QQMusicSystemFunctionHandler.kt

@ -48,41 +48,7 @@ class QQMusicSystemFunctionHandler(private val context: Context? = null) {
),
QQMusicSearchHandler(context)
)
// QQ音乐播放歌曲
// client.registerLocalFunction(
// "qqmusic_play_songs",
// "播放QQ音乐歌曲列表",
// mapOf(
// "type" to "object",
// "properties" to mapOf(
// "songMids" to mapOf(
// "type" to "array",
// "description" to "歌曲ID列表",
// "items" to mapOf("type" to "string")
// ),
// "index" to mapOf(
// "type" to "integer",
// "description" to "开始播放的歌曲索引,默认为0"
// )
// ),
// "required" to listOf("songMids")
// ),
// QQMusicPlaySongsHandler(context)
// )
//QQ音乐播放控制
// client.registerLocalFunction(
// "qqmusic_play",
// "播放QQ音乐",
// mapOf(
// "type" to "object",
// "properties" to emptyMap<String, Any>(),
// "required" to emptyList<String>()
// ),
// QQMusicPlayHandler(context)
// )
client.registerLocalFunction(
"qqmusic_pause",
"暂停QQ音乐",
@ -137,40 +103,7 @@ class QQMusicSystemFunctionHandler(private val context: Context? = null) {
),
QQMusicPreviousHandler(context)
)
// QQ音乐状态查询
client.registerLocalFunction(
"qqmusic_get_playback_state",
"获取QQ音乐播放状态",
mapOf(
"type" to "object",
"properties" to emptyMap<String, Any>(),
"required" to emptyList<String>()
),
QQMusicGetPlaybackStateHandler(context)
)
client.registerLocalFunction(
"qqmusic_get_current_song",
"获取QQ音乐当前播放歌曲信息",
mapOf(
"type" to "object",
"properties" to emptyMap<String, Any>(),
"required" to emptyList<String>()
),
QQMusicGetCurrentSongHandler(context)
)
client.registerLocalFunction(
"qqmusic_get_play_time",
"获取QQ音乐播放时间信息",
mapOf(
"type" to "object",
"properties" to emptyMap<String, Any>(),
"required" to emptyList<String>()
),
QQMusicGetPlayTimeHandler(context)
)
Log.d(TAG, "QQ音乐所有功能注册完成")
} catch (e: Exception) {
Log.e(TAG, "注册QQ音乐功能失败: ${e.message}", e)
@ -179,31 +112,6 @@ class QQMusicSystemFunctionHandler(private val context: Context? = null) {
}
private class QQMusicLoginHandler(private val context: Context?) : FunctionHandler {
override suspend fun handle(arguments: Map<String, Any>): String {
return try {
val qqMusicSingleton = QQMusicSingleton.getInstance()
var result = ""
qqMusicSingleton.login { loginResult ->
result = if (loginResult.isSuccess) {
val loginData = loginResult.getOrNull()
"{\"success\": true, \"message\": \"登录成功\", \"data\": {\"openId\": \"${loginData?.openId}\", \"openToken\": \"${loginData?.openToken}\", \"expireTime\": \"${loginData?.expireTime}\"}}"
} else {
val error = loginResult.exceptionOrNull()
"{\"success\": false, \"message\": \"登录失败: ${error?.message}\"}"
}
}
// 等待回调完成
delay(3000)
result.ifEmpty { "{\"success\": false, \"message\": \"登录超时\"}" }
} catch (e: Exception) {
Log.e("QQMusicLoginHandler", "QQ音乐登录失败", e)
"{\"success\": false, \"message\": \"登录异常: ${e.message}\"}"
}
}
}
private class QQMusicSearchHandler(private val context: Context?) : FunctionHandler {
override suspend fun handle(arguments: Map<String, Any>): String {
return try {
@ -537,27 +445,3 @@ private class QQMusicGetCurrentSongHandler(private val context: Context?) : Func
}
}
private class QQMusicGetPlayTimeHandler(private val context: Context?) : FunctionHandler {
override suspend fun handle(arguments: Map<String, Any>): String {
return try {
val qqMusicSingleton = QQMusicSingleton.getInstance()
var result = ""
qqMusicSingleton.getPlayTime { timeResult ->
result = if (timeResult.isSuccess) {
val timeInfo = timeResult.getOrNull()
"{\"success\": true, \"message\": \"获取播放时间成功\", \"data\": {\"totalTime\": ${timeInfo?.totalTime}, \"currentTime\": ${timeInfo?.currentTime}}}"
} else {
val error = timeResult.exceptionOrNull()
"{\"success\": false, \"message\": \"获取播放时间失败: ${error?.message}\"}"
}
}
delay(2000)
result.ifEmpty { "{\"success\": false, \"message\": \"获取播放时间超时\"}" }
} catch (e: Exception) {
Log.e("QQMusicLoginHandler", "获取QQ音乐播放时间失败", e)
"{\"success\": false, \"message\": \"获取播放时间异常: ${e.message}\"}"
}
}
}

306
local_plugins/music_service/ios/music_service/Sources/music_service/MusicService.swift

@ -16,6 +16,7 @@ public class MusicService: NSObject {
private var player: AVPlayer?
private var playlist: [[String: Any]] = []
private var currentIndex = 0
private var currentsongid = Int64(0)
public var isPlaying = false
private var flutterChannel: FlutterMethodChannel?
private var progressTimer: Timer?
@ -34,7 +35,7 @@ public class MusicService: NSObject {
private override init() {
super.init()
setupRemoteCommandCenter()
//setupAudioSession()
setupAudioSession()
}
public func initialize(serviceUrl: String, userToken: String) {
@ -44,15 +45,21 @@ public class MusicService: NSObject {
}
// // MARK: - 音频会话设置
// private func setupAudioSession() {
// do {
// try AVAudioSession.sharedInstance().setCategory(.playback, mode: .default, options: [.allowAirPlay, .allowBluetooth])
// try AVAudioSession.sharedInstance().setActive(true)
// } catch {
// print("设置音频会话失败: \(error)")
// }
// }
// MARK: - 音频会话设置
private func setupAudioSession() {
do {
let audioSession = AVAudioSession.sharedInstance()
try audioSession.setCategory(.playback, mode: .default, options: [.allowAirPlay, .allowBluetooth, .allowBluetoothA2DP])
try audioSession.setActive(true)
// 确保媒体控制中心可以接收到我们的应用
setupRemoteCommandCenter()
print("音频会话和媒体控制设置成功")
} catch {
print("设置音频会话失败: \(error)")
}
}
// MARK: - 基础播放控制
func getCurrentUrlString() -> String? {
@ -144,9 +151,7 @@ public class MusicService: NSObject {
"id": songObj["id"] as? Int64,
"name": songObj["name"] as? String,
"singer": songObj["singer"] as? String,
"url": songObj["url"] as? String,
"image": songObj["image"] as? String,
"ts": songObj["ts"] as? Int64
])
}
@ -154,17 +159,203 @@ public class MusicService: NSObject {
}
public func play(song: [String: Any]) {
guard let urlString = song["url"] as? String, let url = URL(string: urlString) else { return }
// MARK: - 获取歌曲URL
public func getSongUrl(id: Int64,curragnt:Int,allagent:Int, completion: @escaping (Result<[String: Any?], Error>) -> Void) {
guard !serviceUrl.isEmpty, let url = URL(string: "\(serviceUrl)/api/home/music_url") else {
completion(.failure(NSError(domain: "MusicService", code: -1, userInfo: [NSLocalizedDescriptionKey: "Service URL is not configured"])))
return
}
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.addValue(userToken, forHTTPHeaderField: "Authorization")
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
request.timeoutInterval = 30.0
let body: [String: Any] = [
"id": id
]
do {
request.httpBody = try JSONSerialization.data(withJSONObject: body, options: [])
} catch {
if (allagent > 0 && curragnt < allagent){
getSongUrl(id: id,curragnt: curragnt+1,allagent: allagent,completion: completion)
}else{
completion(.failure(error))
}
return
}
print("发送获取URL请求: id=\(id)")
let task = URLSession.shared.dataTask(with: request) { data, response, error in
if let error = error {
print("获取URL请求失败: \(error.localizedDescription)")
if (allagent > 0 && curragnt < allagent){
self.getSongUrl(id: id,curragnt: curragnt+1,allagent: allagent,completion: completion)
}else{
completion(.failure(error))
}
return
}
guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else {
let statusCode = (response as? HTTPURLResponse)?.statusCode ?? -1
print("获取URL失败,HTTP状态码: \(statusCode)")
if (allagent > 0 && curragnt < allagent){
self.getSongUrl(id: id,curragnt: curragnt+1,allagent: allagent,completion: completion)
}else{
completion(.failure(NSError(domain: "MusicService", code: statusCode, userInfo: [NSLocalizedDescriptionKey: "获取URL失败,HTTP状态码: \(statusCode)"])))
}
return
}
guard let data = data else {
print("获取URL响应数据为空")
if (allagent > 0 && curragnt < allagent){
self.getSongUrl(id: id,curragnt: curragnt+1,allagent: allagent,completion: completion)
}else{
completion(.failure(NSError(domain: "MusicService", code: -2, userInfo: [NSLocalizedDescriptionKey: "响应数据为空"])))
}
return
}
do {
if let jsonResponse = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] {
let urlData = self.parseUrlFromResponse(jsonResponse: jsonResponse)
print("获取URL成功: \(urlData)")
if (!urlData.isEmpty){
completion(.success(urlData))
}else{
if (allagent > 0 && curragnt < allagent){
self.getSongUrl(id: id,curragnt: curragnt+1,allagent: allagent,completion: completion)
}else{
completion(.failure(NSError(domain: "MusicService", code: -3, userInfo: [NSLocalizedDescriptionKey: "获取连接失败!"])))
}
}
} else {
if (allagent > 0 && curragnt < allagent){
self.getSongUrl(id: id,curragnt: curragnt+1,allagent: allagent,completion: completion)
}else{
completion(.failure(NSError(domain: "MusicService", code: -3, userInfo: [NSLocalizedDescriptionKey: "解析JSON响应失败"])))
}
}
} catch {
print("解析获取URL响应时发生异常: \(error.localizedDescription)")
if (allagent > 0 && curragnt < allagent){
self.getSongUrl(id: id,curragnt: curragnt+1,allagent: allagent,completion: completion)
}else{
completion(.failure(error))
}
}
}
task.resume()
}
private func parseUrlFromResponse(jsonResponse: [String: Any]) -> [String: Any?] {
var urlData: [String: Any?] = [:]
guard let data = jsonResponse["data"] as? [String: Any] else {
return urlData
}
urlData["id"] = data["id"] as? Int64
urlData["url"] = data["url"] as? String
urlData["ts"] = data["ts"] as? Int64
return urlData
}
public func playPlaylist(songs: [[String: Any]]) {
playlist = songs
currentIndex = 0
if !playlist.isEmpty {
play(song: playlist[currentIndex])
}
}
public func play(song: [String: Any]) {
// 获取歌曲ID
if let id = song["id"] as? NSNumber {
currentIdInternal = id.int64Value
let id: Int64
if let songId = song["id"] as? NSNumber {
id = songId.int64Value
currentIdInternal = id
} else {
id = 0
currentIdInternal = 0
}
currentsongid = id
print("开始播放play id: \(id)")
// 检查 URL 和时间戳
let url = song["url"] as? String
let ts = song["ts"] as? Int64
let currentTime = Int64(Date().timeIntervalSince1970) // 当前时间戳(秒)
// 判断是否需要重新获取 URL
let needNewUrl = url?.isEmpty != false || ts == nil || ts! <= currentTime
if needNewUrl {
print("URL 不存在或已过期,重新获取 URL")
// 调用 getSongUrl 获取新的 URL
getSongUrl(id: id,curragnt: 0,allagent: 3) { [weak self] result in
DispatchQueue.main.async {
switch result {
case .success(let urlData):
if let newUrl = urlData["url"] as? String, !newUrl.isEmpty {
// 更新 song 数据
var updatedSong = song
updatedSong["url"] = newUrl
updatedSong["ts"] = urlData["ts"] ?? 0
// 更新 playlist 中对应的歌曲数据
self?.updatePlaylistSong(songId: id, updatedSong: updatedSong)
if (self?.currentsongid == id) {
// 使用新 URL 开始播放
self?.startPlayback(song: updatedSong, url: newUrl)
}
} else {
print("获取到的 URL 为空")
}
case .failure(let error):
print("获取 URL 失败: \(error.localizedDescription)")
}
}
}
} else {
// URL 有效,直接播放
startPlayback(song: song, url: url!)
}
}
// 新增方法:更新 playlist 中指定 ID 的歌曲数据
private func updatePlaylistSong(songId: Int64, updatedSong: [String: Any]) {
for i in 0..<playlist.count {
let playlistSong = playlist[i]
if let playlistSongId = playlistSong["id"] as? NSNumber,
playlistSongId.int64Value == songId {
// 找到匹配的歌曲,更新数据
playlist[i] = updatedSong
print("已更新 playlist 中 ID 为 \(songId) 的歌曲数据")
break
}
}
}
// 新增方法:实际的播放逻辑
private func startPlayback(song: [String: Any], url: String) {
guard let playUrl = URL(string: url) else {
print("无效的播放URL: \(url)")
return
}
print("开始播放: \(urlString), ID: \(currentIdInternal ?? 0)")
print("开始播放: \(url), ID: \(currentIdInternal ?? 0)")
// 检查是否已在播放列表中
let existingIndex = playlist.firstIndex { existingSong in
@ -183,7 +374,7 @@ public class MusicService: NSObject {
}
// 创建播放项
let playerItem = AVPlayerItem(url: url)
let playerItem = AVPlayerItem(url: playUrl)
player = AVPlayer(playerItem: playerItem)
// 设置播放器观察者
@ -195,16 +386,15 @@ public class MusicService: NSObject {
currentVolume = 1.0
player?.volume = currentVolume
// 立即设置媒体信息,即使播放器还未准备就绪
updateNowPlayingInfo(song: song)
// 确保远程控制中心已设置
setupRemoteCommandCenter()
print("播放器已创建,等待加载完成...")
}
public func playPlaylist(songs: [[String: Any]]) {
playlist = songs
currentIndex = 0
if !playlist.isEmpty {
play(song: playlist[currentIndex])
}
}
public func pause() {
player?.pause()
@ -396,20 +586,39 @@ public class MusicService: NSObject {
// 异步加载封面图片
if !coverUrl.isEmpty, let url = URL(string: coverUrl) {
print("开始加载封面图片: \(coverUrl)")
DispatchQueue.global(qos: .background).async {
if let data = try? Data(contentsOf: url),
let image = UIImage(data: data) {
DispatchQueue.main.async {
nowPlayingInfo[MPMediaItemPropertyArtwork] = MPMediaItemArtwork(boundsSize: image.size) { _ in image }
MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
// 创建带超时的URLRequest
var request = URLRequest(url: url)
request.timeoutInterval = 10.0
URLSession.shared.dataTask(with: request) { data, response, error in
if let error = error {
print("图片加载失败: \(error.localizedDescription)")
DispatchQueue.main.async {
MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
}
return
}
} else {
guard let data = data, let image = UIImage(data: data) else {
print("图片数据无效或格式不支持")
DispatchQueue.main.async {
MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
}
return
}
print("图片加载成功")
DispatchQueue.main.async {
nowPlayingInfo[MPMediaItemPropertyArtwork] = MPMediaItemArtwork(boundsSize: image.size) { _ in image }
MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
}
}
}.resume()
}
} else {
print("封面图片URL为空")
MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
}
}
@ -460,20 +669,39 @@ public class MusicService: NSObject {
// 异步加载封面图片
if !coverUrl.isEmpty, let url = URL(string: coverUrl) {
print("开始加载封面图片: \(coverUrl)")
DispatchQueue.global(qos: .background).async {
if let data = try? Data(contentsOf: url),
let image = UIImage(data: data) {
DispatchQueue.main.async {
nowPlayingInfo[MPMediaItemPropertyArtwork] = MPMediaItemArtwork(boundsSize: image.size) { _ in image }
MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
// 创建带超时的URLRequest
var request = URLRequest(url: url)
request.timeoutInterval = 10.0
URLSession.shared.dataTask(with: request) { data, response, error in
if let error = error {
print("图片加载失败: \(error.localizedDescription)")
DispatchQueue.main.async {
MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
}
return
}
} else {
guard let data = data, let image = UIImage(data: data) else {
print("图片数据无效或格式不支持")
DispatchQueue.main.async {
MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
}
return
}
print("图片加载成功")
DispatchQueue.main.async {
nowPlayingInfo[MPMediaItemPropertyArtwork] = MPMediaItemArtwork(boundsSize: image.size) { _ in image }
MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
}
}
}.resume()
}
} else {
print("封面图片URL为空")
MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
}

5
local_plugins/ota/android/src/main/kotlin/com/example/ota/OtaPlugin.kt

@ -378,14 +378,11 @@ class OtaPlugin : BleService.Callback, FlutterPlugin, MethodCallHandler {
}
}
override fun onAudioDataReceived(data: ByteArray) {
override fun onAudioDataReceived(data: ByteArray,channel:Int) {
// 可选:处理音频数据
}
override fun onAudioDataReceivedCall(data: ByteArray) {
}
/**
* 处理唤醒信号
* 在收到唤醒信号时启动语音识别

35
local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/QQMusicLoginManager.kt

@ -27,7 +27,7 @@ class QQMusicLoginManager(private val context: Context) {
// 定义错误码常量,替代 LoginExampleActivity 中的常量
const val LOGIN_FAILED = 1
}
private var isBindQQMusicService = false
private lateinit var qPlayBindHelper: QPlayBindHelper
private var qqMusicApi: IQQMusicApi? = null
private lateinit var qqMusicApiWrapper: QQMusicApiWrapper
@ -37,10 +37,15 @@ class QQMusicLoginManager(private val context: Context) {
* 登录结果回调接口
*/
interface LoginCallback {
fun onSuccess(openId: String, openToken: String, expireTime: String)
fun onSuccess()
fun onError(errorCode: String, errorMessage: String)
}
//是否连接
public val isLogined: Boolean
get() = isBindQQMusicService && qPlayBindHelper != null
/**
* 开始登录流程
*/
@ -101,8 +106,8 @@ class QQMusicLoginManager(private val context: Context) {
ErrorCodes.ERROR_OK -> {
// 已授权,直接请求登录信息
Log.d(TAG, "已授权,直接获取登录信息")
callback.onSuccess("","","")
// requestAuth(callback)
isBindQQMusicService = true
callback.onSuccess()
}
ErrorCodes.ERROR_API_NO_PERMISSION -> {
// 需要授权
@ -110,12 +115,14 @@ class QQMusicLoginManager(private val context: Context) {
startAuthProcess(callback)
}
else -> {
isBindQQMusicService = false
Log.e(TAG, "检查授权状态失败: $code")
callback.onError("AUTH_CHECK_FAILED", "检查授权状态失败")
}
}
} catch (e: Exception) {
Log.e(TAG, "检查授权状态异常", e)
isBindQQMusicService = false
callback.onError("AUTH_CHECK_ERROR", "检查授权状态异常: ${e.message}")
}
}
@ -172,7 +179,7 @@ class QQMusicLoginManager(private val context: Context) {
},
failed = { errorCode, errorMsg ->
Log.e(TAG, "授权失败: code=$errorCode, msg=$errorMsg")
isBindQQMusicService = false
val errorMessage = when (errorCode) {
LOGIN_FAILED -> "登录失败: $errorMsg"
else -> "授权失败: $errorMsg"
@ -197,22 +204,23 @@ class QQMusicLoginManager(private val context: Context) {
// 检查签名
if (OpenIDHelper.checkQMSign(sign, returnNonce)) {
val openId = appDecryptJson.getString(Keys.API_RETURN_KEY_OPEN_ID)
val openToken = appDecryptJson.getString(Keys.API_RETURN_KEY_OPEN_TOKEN)
val expireTime = appDecryptJson.getString(Keys.API_PARAM_KEY_SDK_EXPIRETIME)
Log.d(TAG, "登录成功: openId=$openId")
// val openId = appDecryptJson.getString(Keys.API_RETURN_KEY_OPEN_ID)
// val openToken = appDecryptJson.getString(Keys.API_RETURN_KEY_OPEN_TOKEN)
// val expireTime = appDecryptJson.getString(Keys.API_PARAM_KEY_SDK_EXPIRETIME)
// Log.d(TAG, "登录成功: openId=$openId")
isBindQQMusicService = true
handler.post {
callback.onSuccess(openId, openToken, expireTime)
callback.onSuccess()
}
} else {
isBindQQMusicService = false
Log.e(TAG, "签名验证失败")
handler.post {
callback.onError("SIGN_ERROR", "签名验证失败")
}
}
} else {
isBindQQMusicService = false
Log.e(TAG, "解密失败")
handler.post {
callback.onError("DECRYPT_ERROR", "解密失败")
@ -220,6 +228,7 @@ class QQMusicLoginManager(private val context: Context) {
}
} catch (e: Exception) {
Log.e(TAG, "解析登录结果失败", e)
isBindQQMusicService = false
handler.post {
callback.onError("PARSE_ERROR", "解析登录结果失败: ${e.message}")
}

106
local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/QQMusicManager.kt

@ -39,38 +39,27 @@ class QQMusicManager(private val context: Context) {
fun onSuccess(data: Map<String, Any?>)
fun onError(errorCode: Int, errorMessage: String)
}
private val serviceConnection = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
Log.d(TAG, "QQ音乐服务连接成功")
qqmusicApi = IQQMusicApi.Stub.asInterface(service)
isBindQQMusicService = true
// 初始化SDK
CommonCmd.init(Config.BIND_PLATFORM)
// 延迟发送hi消息,确保Binder连接稳定
Handler(Looper.getMainLooper()).postDelayed({
sayHi()
}, 500) // 延迟500ms
}
override fun onServiceDisconnected(name: ComponentName?) {
Log.d(TAG, "QQ音乐服务连接断开")
qqmusicApi = null
isBindQQMusicService = false
//重连绑定
bindQQMusicApiService();
}
// 新增:绑定服务的回调接口
interface BindCallback {
fun onSuccess()
fun onError(errorCode: Int, errorMessage: String)
}
init {
bindQQMusicApiService()
// bindQQMusicApiService()
}
//是否连接
public val isConnected: Boolean
get() = isBindQQMusicService && qqmusicApi != null
private fun sayHi() {
if (!isBindQQMusicService || qqmusicApi == null) {
Log.w(TAG, "QQ音乐服务未连接,无法发送hi消息")
Log.w(TAG, "连接QQ音乐失败,可能是QQ音乐帐号状态异常,请打开QQ音乐检查帐号状态是否异常?")
return
}
@ -97,9 +86,9 @@ class QQMusicManager(private val context: Context) {
}
}
fun bindQQMusicApiService() {
fun bindQQMusicApiService(callback: BindCallback? = null) {
Log.d(TAG, "开始绑定QQ音乐服务,平台类型: ${Config.BIND_PLATFORM}")
val intent = when (Config.BIND_PLATFORM) {
CommonCmd.AIDL_PLATFORM_TYPE_PHONE -> {
Intent("com.tencent.qqmusic.third.api.QQMusicApiService").apply {
@ -118,6 +107,7 @@ class QQMusicManager(private val context: Context) {
}
else -> {
Log.e(TAG, "不支持的平台类型: ${Config.BIND_PLATFORM}")
callback?.onError(-1, "不支持的平台类型: ${Config.BIND_PLATFORM}")
return
}
}
@ -125,22 +115,71 @@ class QQMusicManager(private val context: Context) {
Log.d(TAG, "绑定Intent: action=${intent.action}, package=${intent.`package`}")
try {
val bindResult = context.bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE)
val bindResult = context.bindService(intent, object : ServiceConnection {
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
Log.d(TAG, "QQ音乐服务连接成功")
qqmusicApi = IQQMusicApi.Stub.asInterface(service)
isBindQQMusicService = true
// 初始化SDK
CommonCmd.init(Config.BIND_PLATFORM)
// 延迟发送hi消息,确保Binder连接稳定
Handler(Looper.getMainLooper()).postDelayed({
sayHi()
// 绑定成功后调用回调
callback?.onSuccess()
}, 500) // 延迟500ms
}
override fun onServiceDisconnected(name: ComponentName?) {
Log.d(TAG, "QQ音乐服务连接断开")
qqmusicApi = null
isBindQQMusicService = false
// 服务断开时通知错误
callback?.onError(-2, "QQ音乐服务连接断开")
//重连绑定
bindQQMusicApiService(callback)
}
}, Context.BIND_AUTO_CREATE)
Log.d(TAG, "bindService结果: $bindResult")
if (!bindResult) {
Log.e(TAG, "绑定QQ音乐服务失败,尝试启动QQ音乐进程")
// 尝试启动QQ音乐进程
CommonCmd.startQQMusicProcess(context, context.packageName)
// 延迟重试绑定
Handler(Looper.getMainLooper()).postDelayed({
Log.d(TAG, "重试绑定QQ音乐服务")
context.bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE)
val retryResult = context.bindService(intent, object : ServiceConnection {
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
Log.d(TAG, "QQ音乐服务连接成功")
qqmusicApi = IQQMusicApi.Stub.asInterface(service)
isBindQQMusicService = true
// 初始化SDK
CommonCmd.init(Config.BIND_PLATFORM)
// 延迟发送hi消息,确保Binder连接稳定
Handler(Looper.getMainLooper()).postDelayed({
sayHi()
callback?.onSuccess()
}, 500) // 延迟500ms
}
override fun onServiceDisconnected(name: ComponentName?) {
Log.d(TAG, "QQ音乐服务连接断开")
qqmusicApi = null
isBindQQMusicService = false
}
}, Context.BIND_AUTO_CREATE)
Log.d(TAG, "重试绑定QQ音乐服务 $retryResult")
if (!retryResult) {
callback?.onError(-3, "重试绑定QQ音乐服务失败")
}
}, 2000)
}
} catch (e: Exception) {
Log.e(TAG, "绑定QQ音乐服务异常", e)
callback?.onError(-4, "绑定QQ音乐服务异常: ${e.message}")
}
}
@ -659,7 +698,6 @@ class QQMusicManager(private val context: Context) {
fun cleanup() {
try {
if (isBindQQMusicService) {
context.unbindService(serviceConnection)
isBindQQMusicService = false
Log.d(TAG, "QQ音乐服务已解绑")
}

438
local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/QQMusicSingleton.kt

@ -8,8 +8,6 @@ import android.os.Handler
import android.os.Looper
import android.util.Log
import com.tencent.qqmusic.third.api.contract.*
import java.text.SimpleDateFormat
import java.util.*
/**
* QQ音乐单例管理器
@ -23,12 +21,7 @@ class QQMusicSingleton private constructor() {
private const val KEY_OPEN_ID = "open_id"
private const val KEY_OPEN_TOKEN = "open_token"
private const val KEY_EXPIRE_TIME = "expire_time"
private const val LOGIN_WAIT_DELAY_MS = 30000L
// 添加搜索重试相关常量
private const val SEARCH_TIMEOUT_MS = 10000L
private const val MAX_RETRY_COUNT = 1
private const val RETRY_DELAY_MS = 1000L
@Volatile
private var INSTANCE: QQMusicSingleton? = null
@ -53,9 +46,9 @@ class QQMusicSingleton private constructor() {
*/
fun initialize(context: Context) {
this.context = context
this.sharedPreferences = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE)
loadSavedLoginInfo()
Log.d(TAG, "QQ音乐单例初始化完成")
// this.sharedPreferences = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE)
// loadSavedLoginInfo()
// Log.d(TAG, "QQ音乐单例初始化完成")
}
/**
* 检查QQ音乐App是否已安装
@ -87,58 +80,54 @@ class QQMusicSingleton private constructor() {
* 检查是否已登录且未过期
*/
fun isLoggedIn(): Boolean {
if (currentLoginResult == null){
loadSavedLoginInfo()
if (currentLoginResult == null){
return false
}
return true
}else{
return true
}
return loginManager != null && loginManager!!.isLogined
}
/**
* 获取当前登录信息
*/
fun getCurrentLoginInfo(): LoginResult? {
return if (isLoggedIn()) currentLoginResult else null
}
/**
* 登录接口
*/
fun login(callback: (Result<LoginResult>) -> Unit) {
fun login(callback: (Result<Unit>) -> Unit) {
if (!isQQMusicInstalled()){
callback(Result.failure(Exception("未安装QQ音乐软件,请先安装QQ音乐软件")))
return
}
val currentActivity = context
val currentContext = context
// 先检查是否已经登录且未过期
if (isLoggedIn()) {
Log.d(TAG, "用户已登录且未过期,直接返回登录信息")
ensureMusicManager(currentActivity!!)
callback(Result.success(currentLoginResult!!))
ensureMusicManager(currentContext!!,object: QQMusicManager. BindCallback{
override fun onSuccess() {
callback(Result.success(Unit))
}
override fun onError(errorCode: Int, errorMessage: String) {
callback(Result.failure(Exception("no logined $errorMessage")))
}
})
return
}
if (currentActivity == null) {
if (currentContext == null) {
callback(Result.failure(Exception("Activity not available")))
return
}
Log.d(TAG, "开始新的登录流程")
loginManager = QQMusicLoginManager(currentActivity)
loginManager = QQMusicLoginManager(currentContext)
loginManager?.startLogin(object : QQMusicLoginManager.LoginCallback {
override fun onSuccess(openId: String, openToken: String, expireTime: String) {
override fun onSuccess() {
handler.post {
ensureMusicManager(currentActivity!!)
val result = LoginResult(openId, openToken, expireTime)
currentLoginResult = result
saveLoginInfo(result)
Log.d(TAG, "登录成功并已保存: $result")
callback(Result.success(result))
ensureMusicManager(currentContext,object: QQMusicManager. BindCallback{
override fun onSuccess() {
callback(Result.success(Unit))
}
override fun onError(errorCode: Int, errorMessage: String) {
callback(Result.failure(Exception("no logined $errorMessage")))
}
})
}
}
@ -164,8 +153,16 @@ class QQMusicSingleton private constructor() {
// 先检查是否已经登录且未过期
if (isLoggedIn()) {
Log.d(TAG, "用户已登录且未过期,直接返回登录信息")
ensureMusicManager(currentActivity!!)
callback(Result.success(currentLoginResult!!))
ensureMusicManager(currentActivity!!,object: QQMusicManager. BindCallback{
override fun onSuccess() {
callback(Result.success(currentLoginResult!!))
}
override fun onError(errorCode: Int, errorMessage: String) {
callback(Result.failure(Exception("no logined $errorMessage")))
}
})
return
}else{
callback(Result.failure(Exception("no logined")))
@ -190,129 +187,12 @@ class QQMusicSingleton private constructor() {
searchType: Int = Data.SearchType.SEARCH_TYPE_SONG,
callback: (Result<List<Map<String, Any?>>>) -> Unit
) {
if (!isQQMusicInstalled()){
callback(Result.failure(Exception("未安装QQ音乐软件,请先安装QQ音乐软件")))
return
}
searchWithRetry(keyword, searchType, 1, callback)
}
/**
* 带重试的搜索实现
*/
private fun searchWithRetry(
keyword: String,
searchType: Int,
retryCount: Int,
callback: (Result<List<Map<String, Any?>>>) -> Unit
) {
val currentActivity = context
if (currentActivity == null) {
callback(Result.failure(Exception("Activity not available")))
return
}
// 检查登录状态
if (!isLoggedIn()) {
Log.d(TAG, "搜索前检测到未登录,开始自动登录")
login { loginResult ->
if (loginResult.isSuccess) {
Log.d(TAG, "自动登录成功,等待QQ音乐服务连接...")
handler.postDelayed({
Log.d(TAG, "等待完成,开始搜索")
performSearchWithTimeout(keyword, searchType, retryCount, callback)
}, LOGIN_WAIT_DELAY_MS)
} else {
callback(Result.failure(Exception("自动登录失败: ${loginResult.exceptionOrNull()?.message}")))
}
}
} else {
performSearchWithTimeout(keyword, searchType, retryCount, callback)
}
}
/**
* 执行带超时的搜索操作
*/
private fun performSearchWithTimeout(
keyword: String,
searchType: Int,
retryCount: Int,
callback: (Result<List<Map<String, Any?>>>) -> Unit
) {
val currentActivity = context ?: return
val searchKey = "${keyword}_${System.currentTimeMillis()}"
var completed = false
Log.d(TAG, "开始搜索: $keyword (第${retryCount + 1}次)")
// 设置超时
val timeoutRunnable = Runnable {
if (!completed) {
completed = true
searchTimeouts.remove(searchKey)
Log.w(TAG, "搜索超时: $keyword")
if (retryCount < MAX_RETRY_COUNT) {
Log.d(TAG, "准备重试: ${retryCount + 1}/$MAX_RETRY_COUNT")
handler.postDelayed({
searchWithRetry(keyword, searchType, retryCount + 1, callback)
}, RETRY_DELAY_MS)
} else {
callback(Result.failure(Exception("搜索超时,已重试${MAX_RETRY_COUNT}次")))
}
}
}
searchTimeouts[searchKey] = timeoutRunnable
handler.postDelayed(timeoutRunnable, SEARCH_TIMEOUT_MS)
ensureMusicManager(currentActivity)
musicManager?.searchSongs(keyword, searchType, object : QQMusicManager.SearchCallback {
override fun onSuccess(songs: List<Map<String, Any?>>) {
handler.post {
if (!completed) {
completed = true
searchTimeouts.remove(searchKey)?.let { handler.removeCallbacks(it) }
Log.d(TAG, "搜索成功: 找到 ${songs.size} 首歌曲")
callback(Result.success(songs))
}
}
}
override fun onError(errorCode: Int, errorMessage: String) {
handler.post {
if (!completed) {
completed = true
searchTimeouts.remove(searchKey)?.let { handler.removeCallbacks(it) }
Log.e(TAG, "搜索失败: code=$errorCode, message=$errorMessage")
// 如果是登录相关错误,清除本地登录信息
if (errorMessage.contains("登录") || errorMessage.contains("授权")) {
Log.d(TAG, "检测到登录相关错误,清除本地登录信息")
logout()
}
// 网络相关错误才重试
if (retryCount < MAX_RETRY_COUNT &&
(errorMessage.contains("网络") || errorMessage.contains("超时") ||
errorMessage.contains("连接") || errorCode == -1)) {
Log.d(TAG, "网络错误,准备重试: ${retryCount + 1}/$MAX_RETRY_COUNT")
if (errorCode == -1){
musicManager?.bindQQMusicApiService()
}
handler.postDelayed({
searchWithRetry(keyword, searchType, retryCount + 1, callback)
}, RETRY_DELAY_MS)
} else {
callback(Result.failure(Exception("搜索失败: $errorMessage")))
}
}
}
}
})
executeSearchbackAction("搜索歌曲",0,2) { musicManager, searchCallback ->
musicManager.searchSongs(keyword,searchType,searchCallback)
}(callback)
}
/**
* 播放歌曲列表(带自动登录检查)
*/
@ -321,8 +201,8 @@ class QQMusicSingleton private constructor() {
index: Int = 0,
callback: (Result<Unit>) -> Unit
) {
Log.e(TAG, "播放音乐列表: $songMids")
executePlaybackAction("播放列表") { musicManager, playCallback ->
executePlaybackAction("播放列表",0,2) { musicManager, playCallback ->
Log.d(TAG, "播放歌曲:${songMids[index]}")
musicManager.playSongs(songMids, index,playCallback)
}(callback)
}
@ -331,7 +211,7 @@ class QQMusicSingleton private constructor() {
* 播放音乐
*/
fun playMusic(callback: (Result<Unit>) -> Unit) {
executePlaybackAction("播放") { musicManager, playCallback ->
executePlaybackAction("播放",0,2) { musicManager, playCallback ->
musicManager.playMusic(playCallback)
}(callback)
}
@ -386,11 +266,12 @@ class QQMusicSingleton private constructor() {
}
})
}
/**
* 恢复播放
*/
fun resumeMusic(callback: (Result<Unit>) -> Unit) {
executePlaybackAction("恢复播放") { musicManager, playCallback ->
executePlaybackAction("恢复播放",0,0) { musicManager, playCallback ->
musicManager.resumeMusic(playCallback)
}(callback)
}
@ -399,7 +280,7 @@ class QQMusicSingleton private constructor() {
* 停止音乐
*/
fun stopMusic(callback: (Result<Unit>) -> Unit) {
executePlaybackAction("停止") { musicManager, playCallback ->
executePlaybackAction("停止",0,0) { musicManager, playCallback ->
musicManager.stopMusic(playCallback)
}(callback)
}
@ -408,7 +289,7 @@ class QQMusicSingleton private constructor() {
* 下一首
*/
fun skipToNext(callback: (Result<Unit>) -> Unit) {
executePlaybackAction("下一首") { musicManager, playCallback ->
executePlaybackAction("下一首",0,0) { musicManager, playCallback ->
musicManager.skipToNext(playCallback)
}(callback)
}
@ -417,7 +298,7 @@ class QQMusicSingleton private constructor() {
* 上一首
*/
fun skipToPrevious(callback: (Result<Unit>) -> Unit) {
executePlaybackAction("上一首") { musicManager, playCallback ->
executePlaybackAction("上一首",0,0) { musicManager, playCallback ->
musicManager.skipToPrevious(playCallback)
}(callback)
}
@ -443,60 +324,20 @@ class QQMusicSingleton private constructor() {
/**
* 获取播放时间
*/
fun getPlayTime(callback: (Result<PlayTimeInfo>) -> Unit) {
val currentActivity = context
if (currentActivity == null) {
callback(Result.failure(Exception("Activity not available")))
return
}
ensureMusicManager(currentActivity)
musicManager?.getPlayTime(object : QQMusicManager.InfoCallback {
override fun onSuccess(data: Map<String, Any?>) {
handler.post {
val totalTime = data["totalTime"] as? Long ?: 0L
val currentTime = data["currentTime"] as? Long ?: 0L
val playTimeInfo = PlayTimeInfo(totalTime, currentTime)
Log.d(TAG, "获取播放时间成功: $playTimeInfo")
callback(Result.success(playTimeInfo))
}
}
override fun onError(errorCode: Int, errorMessage: String) {
handler.post {
Log.e(TAG, "获取播放时间失败: $errorMessage")
callback(Result.failure(Exception("获取播放时间失败: $errorMessage")))
}
}
})
fun getPlayTime(callback: (Result<Map<String, Any?>>) -> Unit) {
executeInfoAction("获取播放时间") { musicManager, infoCallback ->
musicManager.getPlayTime(infoCallback)
}(callback)
}
/**
* 同步当前播放信息 - 统一接口
*/
fun syncCurrentPlayInfo(callback: (Result<Map<String, Any?>>) -> Unit) {
val currentcontext = context
if (currentcontext == null) {
callback(Result.failure(Exception("Activity not available")))
return
}
ensureMusicManager(currentcontext)
musicManager?.syncCurrentPlayInfo(object : QQMusicManager.InfoCallback {
override fun onSuccess(data: Map<String, Any?>) {
handler.post {
Log.d(TAG, "同步播放信息成功")
callback(Result.success(data))
}
}
override fun onError(errorCode: Int, errorMessage: String) {
handler.post {
Log.e(TAG, "同步播放信息失败: $errorMessage")
callback(Result.failure(Exception("同步播放信息失败: $errorMessage")))
}
}
})
executeInfoAction("同步当前播放信息") { musicManager, infoCallback ->
musicManager.syncCurrentPlayInfo(infoCallback)
}(callback)
}
/**
@ -561,14 +402,81 @@ class QQMusicSingleton private constructor() {
}
// 私有辅助方法
private fun ensureMusicManager(context: Context) {
private fun ensureMusicManager(context: Context,callback: QQMusicManager. BindCallback) {
if (musicManager == null) {
musicManager = QQMusicManager(context)
musicManager!!.bindQQMusicApiService(callback)
}else{
if(!musicManager!!.isConnected) { //未连接
musicManager?.bindQQMusicApiService(callback)
}else{
callback.onSuccess()
}
}
}
private fun executeSearchbackAction(
actionName: String,
curragain:Int,
allagain:Int,
action: (QQMusicManager, QQMusicManager.SearchCallback) -> Unit
): (callback: (Result<List<Map<String, Any?>>>) -> Unit) -> Unit {
return lambda@{ callback ->
if (!isQQMusicInstalled()){
callback(Result.failure(Exception("未安装QQ音乐软件,请先安装QQ音乐软件")))
return@lambda
}
val currentActivity = context
if (currentActivity == null) {
callback(Result.failure(Exception("Activity not available")))
return@lambda
}
Log.d(TAG, "开始 ${actionName}")
ensureMusicManager(currentActivity,object: QQMusicManager. BindCallback{
override fun onSuccess(){
action(musicManager!!, object : QQMusicManager.SearchCallback {
override fun onSuccess(songs: List<Map<String, Any?>>) {
Log.d(TAG, "${actionName} 成功")
handler.post {
callback(Result.success(songs))
}
}
override fun onError(errorCode: Int, errorMessage: String) {
Log.d(TAG, "失败 ${actionName} 成功")
if (allagain > 0 && curragain < allagain){ //重试机制
executeSearchbackAction(actionName,curragain+1,allagain,action)
}else{
handler.post {
callback(Result.failure(Exception("$actionName 失败: $errorMessage")))
}
}
}
})
}
override fun onError(errorCode: Int, errorMessage: String) {
if (allagain > 0 && curragain < allagain){ //重试机制
executeSearchbackAction(actionName,curragain+1,allagain,action)
}else{
handler.post {
callback(Result.failure(Exception("$actionName 失败: $errorMessage")))
}
}
}
})
}
}
private fun executePlaybackAction(
actionName: String,
curragain:Int,
allagain:Int,
action: (QQMusicManager, QQMusicManager.PlayCallback) -> Unit
): (callback: (Result<Unit>) -> Unit) -> Unit {
return lambda@{ callback ->
@ -583,30 +491,42 @@ class QQMusicSingleton private constructor() {
callback(Result.failure(Exception("Activity not available")))
return@lambda
}
ensureMusicManager(currentActivity)
action(musicManager!!, object : QQMusicManager.PlayCallback {
override fun onSuccess() {
handler.post {
Log.d(TAG, "${actionName}成功")
callback(Result.success(Unit))
}
}
Log.d(TAG, "开始 ${actionName}")
ensureMusicManager(currentActivity,object: QQMusicManager. BindCallback{
override fun onSuccess(){
action(musicManager!!, object : QQMusicManager.PlayCallback {
override fun onSuccess() {
Log.d(TAG, "${actionName}成功")
handler.post {
callback(Result.success(Unit))
}
}
override fun onError(errorCode: Int, errorMessage: String) {
if (allagain > 0 && curragain < allagain){ //重试机制
executePlaybackAction(actionName,curragain+1,allagain,action)
}else{
handler.post {
Log.e(TAG, "${actionName}失败: $errorCode $errorMessage")
if (errorCode == -1){
musicManager?.bindQQMusicApiService()
callback(Result.failure(Exception("播放失败: $errorMessage")))
} else if (errorCode == 1030) {
callback(Result.success(Unit))
}else{
callback(Result.failure(Exception("播放失败: $errorMessage")))
}
}
}
override fun onError(errorCode: Int, errorMessage: String) {
handler.post {
Log.e(TAG, "播放失败: $errorCode $errorMessage")
if (errorCode == -1){
musicManager?.bindQQMusicApiService()
callback(Result.failure(Exception("播放失败: $errorMessage")))
} else if (errorCode == 1030) {
callback(Result.success(Unit))
}else{
callback(Result.failure(Exception("播放失败: $errorMessage")))
}
}
})
}
override fun onError(errorCode: Int, errorMessage: String) {
callback(Result.failure(Exception("$actionName 失败: $errorMessage")))
}
})
}
}
@ -615,26 +535,36 @@ class QQMusicSingleton private constructor() {
action: (QQMusicManager, QQMusicManager.InfoCallback) -> Unit
): (callback: (Result<Map<String, Any?>>) -> Unit) -> Unit {
return lambda@{ callback ->
if (!isQQMusicInstalled()){
callback(Result.failure(Exception("未安装QQ音乐软件,请先安装QQ音乐软件")))
return@lambda
}
val currentcontext = context
if (currentcontext == null) {
callback(Result.failure(Exception("Activity not available")))
return@lambda
}
ensureMusicManager(currentcontext)
action(musicManager!!, object : QQMusicManager.InfoCallback {
override fun onSuccess(data: Map<String, Any?>) {
handler.post {
Log.d(TAG, "${actionName}成功")
callback(Result.success(data))
}
ensureMusicManager(currentcontext,object: QQMusicManager. BindCallback{
override fun onSuccess(){
action(musicManager!!, object : QQMusicManager.InfoCallback {
override fun onSuccess(data: Map<String, Any?>) {
handler.post {
Log.d(TAG, "${actionName}成功")
callback(Result.success(data))
}
}
override fun onError(errorCode: Int, errorMessage: String) {
handler.post {
Log.e(TAG, "${actionName}失败: $errorMessage")
callback(Result.failure(Exception("${actionName}失败: $errorMessage")))
}
}
})
}
override fun onError(errorCode: Int, errorMessage: String) {
handler.post {
Log.e(TAG, "${actionName}失败: $errorMessage")
callback(Result.failure(Exception("${actionName}失败: $errorMessage")))
}
callback(Result.failure(Exception("$actionName 失败: $errorMessage")))
}
})
}

16
local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/QqMusicPlugin.kt

@ -203,22 +203,6 @@ class QqMusicPlugin: FlutterPlugin, MethodCallHandler, ActivityAware {
)
}
}
"getPlayTime" -> {
qqMusicSingleton.getPlayTime { kotlinResult ->
kotlinResult.fold(
onSuccess = { playTimeInfo ->
result.success(mapOf(
"success" to true,
"totalTime" to playTimeInfo.totalTime,
"currentTime" to playTimeInfo.currentTime
))
},
onFailure = { error ->
result.error("GET_TIME_FAILED", error.message, null)
}
)
}
}
"syncCurrentPlayInfo" -> {
qqMusicSingleton.syncCurrentPlayInfo { kotlinResult ->
kotlinResult.fold(

2
pubspec.yaml

@ -1,7 +1,7 @@
name: voitrans
description: "Voitrans - AI Voice Assistant."
publish_to: "none"
version: 1.0.12+29
version: 1.0.13+34
environment:
sdk: ">=3.3.0 <4.0.0"

Loading…
Cancel
Save