Browse Source

Merge branch 'new_dev' of https://github.com/deepcloud2048/deep_voice into liwei

newdev_shunjiawei
liwei1dao 1 year ago
parent
commit
1a35b05879
  1. 2
      android/app/src/main/AndroidManifest.xml
  2. 6
      lib/core/bindings/initial_binding.dart
  3. 6
      lib/core/translations/language/en_us.dart
  4. 4
      lib/core/translations/language/zh_cn.dart
  5. 156
      lib/core/widgets/intro_overlay_builder.dart
  6. 23
      lib/data/services/asr_service.dart
  7. 113
      lib/data/services/ast_service.dart
  8. 29
      lib/data/services/audio_service.dart
  9. 123
      lib/data/services/speech_impl/azure_asr_service.dart
  10. 305
      lib/data/services/speech_impl/azure_ast_service.dart
  11. 20
      lib/data/services/speech_impl/volcano_asr_api_service.dart
  12. 20
      lib/data/services/speech_impl/volcano_asr_service.dart
  13. 20
      lib/data/services/speech_impl/xunfei_asr_service.dart
  14. 4
      lib/modules/agent/controllers/agent_controller.dart
  15. 5
      lib/modules/login/bindings/login_binding.dart
  16. 38
      lib/modules/login/controllers/login_controller.dart
  17. 40
      lib/modules/login/views/login_view.dart
  18. 28
      lib/modules/meeting/controllers/meeting_record_controller.dart
  19. 163
      lib/modules/opus_test/controllers/opus_test_controller.dart
  20. 4
      lib/modules/opus_test/views/opus_test_view.dart
  21. 16
      lib/modules/settings/controllers/settings_controller.dart
  22. 44
      lib/modules/settings/views/permissions_view.dart
  23. 34
      lib/modules/settings/views/settings_view.dart
  24. 13
      lib/modules/speech_test/controllers/speech_test_controller.dart
  25. 294
      lib/modules/translation/controllers/translation_controller.dart
  26. 180
      lib/modules/translation/views/translation_view.dart
  27. 4
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt
  28. 4
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleAgent.kt
  29. 2
      local_plugins/agent_service/ios/agent_service/Package.swift
  30. 39
      local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift
  31. 1
      local_plugins/azure_speech/android/build.gradle.kts
  32. 517
      local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt
  33. 1311
      local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrToAsr.kt
  34. 461
      local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureSpeechPlugin.kt
  35. 2
      local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureTtsHelper.kt
  36. 310
      local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/tools/RecordFile.kt
  37. 4
      local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/tools/SimpleAudioPlayer.kt
  38. 426
      local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/tools/SimpleAudioReceiver.kt
  39. 50
      local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureAsrHelper.swift
  40. 19
      local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureSpeechPlugin.swift
  41. 22
      local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleConst.kt
  42. 674
      local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt
  43. 4
      local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleServicePlugin.kt
  44. 2
      local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/ChatApiService.kt
  45. 57
      local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/CustomSseClientTransport.kt
  46. 48
      local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/MCPClient.kt
  47. 175
      local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/MCPSubClient.kt
  48. 66
      local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/SystemFunctionHandler.kt
  49. 4
      local_plugins/chat_api/ios/chat_api/Sources/chat_api/SystemFunctionHandler.swift
  50. 7
      local_plugins/location_service/android/src/main/AndroidManifest.xml
  51. 424
      local_plugins/location_service/android/src/main/kotlin/com/yunqiinnovation/location_service/LocationService.kt
  52. 38
      local_plugins/location_service/android/src/main/kotlin/com/yunqiinnovation/location_service/LocationServicePlugin.kt
  53. 499
      local_plugins/location_service/android/src/main/kotlin/com/yunqiinnovation/location_service/NativeLocationService.kt
  54. 10
      local_plugins/location_service/ios/location_service/Sources/location_service/LocationData.swift
  55. 278
      local_plugins/location_service/ios/location_service/Sources/location_service/LocationService.swift
  56. 3
      local_plugins/ota/android/src/main/kotlin/com/example/ota/OtaPlugin.kt
  57. 2
      pubspec.yaml

2
android/app/src/main/AndroidManifest.xml

@ -50,6 +50,8 @@
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
<!--用于申请调用A-GPS模块,卫星定位加速-->
<uses-permission android:name="android.permission.ACCESS_LOCATION_EXTRA_COMMANDS" />
<!--用于申请调用精确闹钟权限-->
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
<!--允许写设备缓存,用于问题排查-->
<uses-permission android:name="android.permission.WRITE_SETTINGS" />
<uses-permission android:name="android.permission.VIBRATE"/>

6
lib/core/bindings/initial_binding.dart

@ -5,6 +5,8 @@ import '../../../data/services/user_portrait.dart';
import '../../../data/services/location_manager.dart';
import '../../../data/services/music_manager.dart';
import '../../../data/services/navigation_manager.dart';
import '../../../data/services/ast_service.dart';
import '../../../data/services/speech_impl/azure_ast_service.dart';
import 'package:get/get.dart';
import '../../data/services/meeting/meeting_task_service.dart';
import '../../data/services/meeting/meeting_upload_service.dart';
@ -25,7 +27,6 @@ class InitialBinding extends Bindings {
void dependencies() {
Logger.warning('InitialBinding dependencies');
// 语言管理器(需要最先初始化)
// 语言管理器(需要最先初始化)
Get.lazyPut<LanguageManager>(() => LanguageManager(), fenix: true);
@ -33,6 +34,9 @@ class InitialBinding extends Bindings {
Get.lazyPut<SpeechFactory>(() => SpeechFactory(), fenix: true);
Get.find<SpeechFactory>().initialize(initialType: SpeechServiceType.azure);
// 注册 AST 服务
Get.lazyPut<AstService>(() => AzureAstService(), fenix: true);
// 火山翻译服务
Get.lazyPut<VolcanoTranslationService>(() => VolcanoTranslationService(),
fenix: true);

6
lib/core/translations/language/en_us.dart

@ -487,15 +487,19 @@ const Map<String, String> enUS = {
// 权限管理页面
'getPermissionStatusFailed': 'Failed to get permission status',
'communicationPermissions': 'Communication Permissions',
'alarmPermissions': 'Alarm Permissions',
'calendarPermissions': 'Calendar Permissions',
'locationPermissions': 'Location Permissions',
'phone': 'Phone',
'sms': 'SMS',
'alarm': 'Alarm',
'calendarWrite': 'Calendar Write',
'calendarFullAccess': 'Calendar Full Access',
'locationInfo': 'Location Info',
'phonePermissionDesc': 'Used for phone call functionality',
'smsPermissionDesc': 'Used for SMS functionality',
'alarmPermissionDesc':
'Used to set exact alarms to ensure reminders trigger on time',
'calendarWritePermissionDesc':
'Used for creating calendar events and reminders',
'calendarFullAccessPermissionDesc':
@ -512,6 +516,8 @@ const Map<String, String> enUS = {
'phonePermissionDetailDesc':
'Phone call functionality requires phone permission',
'smsPermissionDetailDesc': 'SMS functionality requires SMS permission',
'alarmPermissionDetailDesc':
'This permission is required to set exact alarms and ensure reminders trigger on time',
'calendarWritePermissionDetailDesc':
'Creating calendar events requires calendar write permission',
'calendarFullAccessPermissionDetailDesc':

4
lib/core/translations/language/zh_cn.dart

@ -455,15 +455,18 @@ const Map<String, String> zhCN = {
// 权限管理页面
'getPermissionStatusFailed': '获取权限状态失败',
'communicationPermissions': '通讯权限',
'alarmPermissions': '闹钟权限',
'calendarPermissions': '日历权限',
'locationPermissions': '位置权限',
'phone': '电话',
'sms': '短信',
'alarm': '闹钟',
'calendarWrite': '日历写入',
'calendarFullAccess': '日历完整访问',
'locationInfo': '位置信息',
'phonePermissionDesc': '用于拨打电话功能',
'smsPermissionDesc': '用于发送短信功能',
'alarmPermissionDesc': '用于设置精确闹钟以确保提醒按时触发',
'calendarWritePermissionDesc': '用于创建日历事件和提醒',
'calendarFullAccessPermissionDesc': '用于读取和管理日历事件及提醒',
'locationPermissionDesc': '用于获取当前位置信息',
@ -477,6 +480,7 @@ const Map<String, String> zhCN = {
'permissionRequestError': '请求权限时出错',
'phonePermissionDetailDesc': '拨打电话功能需要电话权限',
'smsPermissionDetailDesc': '发送短信功能需要短信权限',
'alarmPermissionDetailDesc': '设置精确闹钟需要此权限,以确保提醒按时触发',
'calendarWritePermissionDetailDesc': '创建日历事件需要日历写入权限',
'calendarFullAccessPermissionDetailDesc': '管理日历事件需要完整日历权限',
'locationPermissionDetailDesc': '获取位置信息需要位置权限',

156
lib/core/widgets/intro_overlay_builder.dart

@ -89,22 +89,9 @@ class IntroOverlayBuilder {
}
static Widget _button(String text, Function() onTap) {
return GestureDetector(
return _AnimatedBorderButton(
text: text,
onTap: onTap,
child: Container(
padding: EdgeInsets.symmetric(horizontal: 10.w, vertical: 5.w),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20.r),
border: Border.all(color: Colors.white),
),
child: Text(
text,
style: TextStyle(
fontSize: 14.sp,
color: Colors.white,
),
),
),
);
}
}
@ -166,3 +153,142 @@ class _AnimatedLottieWidgetState extends State<_AnimatedLottieWidget>
);
}
}
// 新增的带边框流动动画的按钮组件
class _AnimatedBorderButton extends StatefulWidget {
final String text;
final Function() onTap;
const _AnimatedBorderButton({
required this.text,
required this.onTap,
});
@override
_AnimatedBorderButtonState createState() => _AnimatedBorderButtonState();
}
class _AnimatedBorderButtonState extends State<_AnimatedBorderButton>
with SingleTickerProviderStateMixin {
late AnimationController _animationController;
late Animation<double> _animation;
@override
void initState() {
super.initState();
_animationController = AnimationController(
duration: const Duration(seconds: 2),
vsync: this,
);
_animation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(_animationController);
_animationController.repeat();
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: widget.onTap,
child: AnimatedBuilder(
animation: _animation,
builder: (context, child) {
return CustomPaint(
painter: _FlowingBorderPainter(
progress: _animation.value,
borderRadius: 20.r,
),
child: Container(
padding: EdgeInsets.symmetric(horizontal: 10.w, vertical: 5.w),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20.r),
border: Border.all(color: Colors.transparent),
),
child: Text(
widget.text,
style: TextStyle(
fontSize: 14.sp,
color: Colors.white,
),
),
),
);
},
),
);
}
}
// 自定义绘制器,用于绘制流动的边框
class _FlowingBorderPainter extends CustomPainter {
final double progress;
final double borderRadius;
_FlowingBorderPainter({
required this.progress,
required this.borderRadius,
});
@override
void paint(Canvas canvas, Size size) {
final rect = Rect.fromLTWH(0, 0, size.width, size.height);
final rrect = RRect.fromRectAndRadius(rect, Radius.circular(borderRadius));
// 计算路径总长度
final path = Path()..addRRect(rrect);
final pathMetrics = path.computeMetrics().first;
final totalLength = pathMetrics.length;
// 流动效果:创建一个渐变的虚线效果
final dashLength = totalLength * 0.3; // 虚线长度
final startOffset = (progress * totalLength) % totalLength;
// 绘制流动的边框段
for (int i = 0; i < 3; i++) {
final offset = (startOffset + i * totalLength / 3) % totalLength;
final endOffset = (offset + dashLength) % totalLength;
Path extractedPath;
if (endOffset > offset) {
extractedPath = pathMetrics.extractPath(offset, endOffset);
} else {
// 处理跨越路径起点的情况
final path1 = pathMetrics.extractPath(offset, totalLength);
final path2 = pathMetrics.extractPath(0, endOffset);
extractedPath = Path()
..addPath(path1, Offset.zero)
..addPath(path2, Offset.zero);
}
// 创建渐变效果
const gradient = LinearGradient(
colors: [
Colors.green,
Colors.yellow,
Colors.red,
],
stops: [0.0, 0.5, 1.0],
);
final gradientPaint = Paint()
..shader = gradient.createShader(rect)
..style = PaintingStyle.stroke
..strokeWidth = 1.5;
canvas.drawPath(extractedPath, gradientPaint);
}
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return true;
}
}

23
lib/data/services/asr_service.dart

@ -41,11 +41,26 @@ abstract class AsrService {
bool isContinuousRecognitionActive();
/// 开始录音
Future<bool> enableRecord(String filePath);
///
/// [filePath] 录音文件路径
/// [acceptAudioData] 是否接受音频数据回调,默认为 false
Future<bool> enableRecord(String filePath, {bool acceptAudioData = false});
/// 获取音频数据流(如果支持)
Stream<Uint8List>? getAudioDataStream() => null;
/// 暂停录音
Future<bool> pauseRecord();
/// 继续录音
Future<bool> resumeRecord();
/// 设置音频配置
Future<bool> setAudioConfig({
int sampleRate = 16000,
int channels = 1,
});
// /// 移动文件到新路径
Future<bool> moveFile(String sourcePath, String destPath);
@ -64,12 +79,12 @@ enum RecognitionEventType {
/// 最终识别结果
finalResult,
/// 麦识别结果
finalResult1,
/// 中间识别结果(实时反馈)
intermediateResult,
/// 音频
onAudio,
/// 会话开始
sessionStarted,

113
lib/data/services/ast_service.dart

@ -0,0 +1,113 @@
import 'dart:async';
import 'dart:typed_data';
/// 语音识别服务接口
abstract class AstService {
/// 支持的语言
List<String> get supportedLanguages;
/// 初始化语音识别服务
Future<bool> initialize({required List<String> supportedLanguages});
/// 开始录音
Future<bool> enableRecord(String filePath);
///
Future<bool> startContinuousTranslation();
///
Future<bool> stopContinuousTranslation();
/// 返回一个包含识别事件的流
Future<Stream<RecognitionEvent1>> recognizeCallback();
/// 开始录音
Future<bool> path(String filePath);
/// 释放资源
Future<void> dispose();
}
// 识别事件类型
enum RecognitionEventType1 {
/// 最终识别结果
finalResult,
/// 中间识别结果(实时反馈)
intermediateResult,
/// 会话开始
sessionStarted,
/// 会话结束
sessionStopped,
/// 识别取消
canceled,
/// 识别错误
error,
}
// 识别事件
class RecognitionEvent1 {
/// 事件类型
final RecognitionEventType1 type;
/// 识别文本(仅在 finalResult 和 intermediateResult 类型中有效)
final String text;
/// 检测到的语言
final String detectedLanguage;
/// 角色
final String role;
/// 原始音频
final Uint8List? audio;
/// 错误信息(仅在 error 和 canceled 类型中有效)
final String error;
RecognitionEvent1({
required this.type,
this.text = '',
this.detectedLanguage = '',
this.role = '',
this.audio,
this.error = '',
});
/// 创建最终结果事件的快捷构造函数
factory RecognitionEvent1.finalResult({
required String text,
String detectedLanguage = '',
}) {
return RecognitionEvent1(
type: RecognitionEventType1.finalResult,
text: text,
detectedLanguage: detectedLanguage,
);
}
/// 创建错误事件的快捷构造函数
factory RecognitionEvent1.error(String errorMessage) {
return RecognitionEvent1(
type: RecognitionEventType1.error,
error: errorMessage,
);
}
/// 检查是否为最终结果
bool get isFinalResult => type == RecognitionEventType1.finalResult;
/// 检查是否为错误
bool get isError =>
type == RecognitionEventType1.error ||
type == RecognitionEventType1.canceled;
@override
String toString() {
return 'RecognitionEvent{type: $type, text: $text, detectedLanguage: $detectedLanguage, error: $error}';
}
}

29
lib/data/services/audio_service.dart

@ -0,0 +1,29 @@
import 'dart:async';
import 'dart:typed_data';
/// 语音识别服务接口
abstract class AudioService {
/// 开始录音
Future<bool> enableRecord(String filePath);
/// 暂停录音
Future<bool> pauseRecord();
/// 继续录音
Future<bool> resumeRecord();
/// 设置音频配置
Future<bool> setAudioConfig({
int sampleRate = 16000,
int channels = 1,
});
// /// 移动文件到新路径
Future<bool> moveFile(String sourcePath, String destPath);
// /// 重命名指定路径的音频文件
Future<bool> renameFile(String filePath, String newName);
/// 结束录音
Future<bool> stopRecord(bool isSave);
}

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

@ -21,6 +21,10 @@ class AzureAsrService extends GetxService implements AsrService {
static const MethodChannel _channel = MethodChannel('azure_speech/asr');
static const EventChannel _eventChannel =
EventChannel('azure_speech/asr_events');
// 音频数据事件通道
static const EventChannel _audioDataEventChannel =
EventChannel('azure_speech/audio_data_events');
// final GetStorage _storage = GetStorage();
bool _isInitialized = false;
late final String _subscriptionKey;
@ -43,6 +47,11 @@ class AzureAsrService extends GetxService implements AsrService {
String _latestDetectedLanguage = '';
String get latestDetectedLanguage => _latestDetectedLanguage;
// 音频数据事件订阅
StreamSubscription? _audioDataEventSubscription;
// 音频数据流控制器
StreamController<Uint8List>? _audioDataStreamController;
// 当前音频源类型
AudioSourceType _audioSourceType = AudioSourceType.microphone;
@ -72,6 +81,19 @@ class AzureAsrService extends GetxService implements AsrService {
}, onError: _handleRecognitionError);
}
/// 设置音频数据事件通道
void _setupAudioDataEventChannel() {
_audioDataEventSubscription?.cancel();
_audioDataEventSubscription =
_audioDataEventChannel.receiveBroadcastStream().listen((event) {
if (event is Map) {
_handleAudioDataEvent(event);
}
}, onError: (error) {
Logger.error('音频数据事件流错误: ${error.toString()}');
});
}
@override
Future<bool> initialize({
List<String>? supportedLanguages,
@ -220,6 +242,36 @@ class AzureAsrService extends GetxService implements AsrService {
return _isContinuousRecognitionActive;
}
/// 处理音频数据事件
void _handleAudioDataEvent(dynamic event) {
if (event is! Map) return;
final Map<dynamic, dynamic> eventMap = event;
final String eventType = eventMap['type'] as String? ?? '';
switch (eventType) {
case 'audioData':
final Uint8List data = eventMap['data'] as Uint8List? ?? Uint8List(0);
final double timestamp =
(eventMap['timestamp'] as num?)?.toDouble() ?? 0.0;
final int size = eventMap['size'] as int? ?? 0;
print('接收到音频数据: 大小=${size}字节, 时间戳=${timestamp}');
Logger.debug('接收到音频数据: 大小=${size}字节, 时间戳=${timestamp}');
// 发送到音频数据流
_audioDataStreamController?.add(data);
break;
}
}
/// 获取音频数据流
Stream<Uint8List>? getAudioDataStream() {
print("getAudioDataStream");
_audioDataStreamController ??= StreamController<Uint8List>.broadcast();
return _audioDataStreamController?.stream;
}
/// 处理来自原生端的识别事件
void _handleRecognitionEvent(dynamic event) {
if (event is! Map || _eventStreamController == null) return;
@ -242,22 +294,27 @@ class AzureAsrService extends GetxService implements AsrService {
detectedLanguage: detectedLanguage,
));
break;
case 'recognizing':
case 'result1':
final String text = eventMap['text'] as String? ?? '';
final String detectedLanguage =
eventMap['detectedLanguage'] as String? ?? '';
_latestRecognizedText = text;
_latestDetectedLanguage = detectedLanguage;
_eventStreamController?.add(RecognitionEvent(
type: RecognitionEventType.intermediateResult,
type: RecognitionEventType.finalResult1,
text: text,
detectedLanguage: detectedLanguage,
));
break;
case 'onAudio':
final Uint8List data = eventMap['data'] as Uint8List? ?? Uint8List(0);
case 'recognizing':
final String text = eventMap['text'] as String? ?? '';
final String detectedLanguage =
eventMap['detectedLanguage'] as String? ?? '';
_eventStreamController?.add(RecognitionEvent(
type: RecognitionEventType.onAudio,
audio: data,
type: RecognitionEventType.intermediateResult,
text: text,
detectedLanguage: detectedLanguage,
));
break;
case 'sessionStarted':
@ -324,6 +381,14 @@ class AzureAsrService extends GetxService implements AsrService {
await _eventSubscription?.cancel();
_eventSubscription = null;
// 取消音频数据事件订阅
await _audioDataEventSubscription?.cancel();
_audioDataEventSubscription = null;
// 关闭音频数据流
await _audioDataStreamController?.close();
_audioDataStreamController = null;
// 通知原生端释放资源
await _channel.invokeMethod('dispose');
_isInitialized = false;
@ -364,11 +429,16 @@ class AzureAsrService extends GetxService implements AsrService {
}
@override
Future<bool> enableRecord(String filePath) async {
Future<bool> enableRecord(String filePath,
{bool acceptAudioData = false}) async {
try {
final bool result = await _channel.invokeMethod('enableRecord', {
'filePath': filePath,
'acceptAudioData': acceptAudioData, // 新增参数
});
if (acceptAudioData) {
_setupAudioDataEventChannel();
}
return result;
} catch (e) {
@ -398,7 +468,30 @@ class AzureAsrService extends GetxService implements AsrService {
return result;
} catch (e) {
Logger.error('停止录音: ${e.toString()}');
Logger.error('暂停录音: ${e.toString()}');
rethrow;
}
}
/// 设置音频配置
///
/// [sampleRate] 采样率,默认16000
/// [channels] 声道数,默认1(单声道)
Future<bool> setAudioConfig({
int sampleRate = 16000,
int channels = 1,
}) async {
try {
final bool result = await _channel.invokeMethod('setAudioConfig', {
'sampleRate': sampleRate,
'channels': channels,
});
Logger.info(
'音频配置设置${result ? '成功' : '失败'}: 采样率=$sampleRate, 声道数=$channels');
return result;
} catch (e) {
Logger.error('设置音频配置失败: ${e.toString()}');
rethrow;
}
}
@ -459,4 +552,16 @@ class AzureAsrService extends GetxService implements AsrService {
rethrow;
}
}
@override
Future<bool> resumeRecord() async {
try {
final bool result = await _channel.invokeMethod('resumeRecord');
return result;
} catch (e) {
Logger.error('继续录音: ${e.toString()}');
rethrow;
}
}
}

305
lib/data/services/speech_impl/azure_ast_service.dart

@ -0,0 +1,305 @@
import 'dart:async';
import '../../../data/models/appconfig.dart';
import 'package:flutter/services.dart';
import '../../../core/utils/logger.dart';
import 'package:get/get.dart';
import '../ast_service.dart';
/// 音频源类型
enum AudioSourceType {
microphone, // 使用设备麦克风
external // 使用外部提供的音频数据
}
/// Azure 语音识别服务
///
/// 该服务提供了通过平台通道与原生 Microsoft Speech SDK 交互的接口
class AzureAstService extends GetxService implements AstService {
static final AzureAstService to = Get.put(AzureAstService());
static const MethodChannel _channel = MethodChannel('azure_speech/ast');
static const EventChannel _eventChannel =
EventChannel('azure_speech/ast_events');
// final GetStorage _storage = GetStorage();
bool _isInitialized = false;
late final String _subscriptionKey;
late final String _serviceRegion;
late String _baseUrl;
final String _endpoint = '/'; // 修改为根路径
late final String _accessKey;
late final String _secretKey;
late final String _region;
late final String _service;
final List<String> _defaultSupportedLanguages = ['zh-CN', 'en-US'];
@override
List<String> get supportedLanguages => _defaultSupportedLanguages;
//连续识别相关
bool _isContinuousRecognitionActive = false;
StreamController<RecognitionEvent1>? _eventStreamController;
StreamSubscription? _eventSubscription;
// 最新的识别结果
String _latestRecognizedText = '';
String get latestRecognizedText => _latestRecognizedText;
// 最新检测到的语言
String _latestDetectedLanguage = '';
String get latestDetectedLanguage => _latestDetectedLanguage;
// 当前音频源类型
AudioSourceType _audioSourceType = AudioSourceType.microphone;
AzureAstService() {
_loadConfig();
}
/// 设置事件通道
void _setupEventChannel() {
_eventSubscription?.cancel();
_eventSubscription = _eventChannel.receiveBroadcastStream().listen((event) {
if (event is Map) {
_handleRecognitionEvent(event);
}
}, onError: _handleRecognitionError);
}
/// 处理来自原生端的识别事件
void _handleRecognitionEvent(dynamic event) {
if (event is! Map || _eventStreamController == null) return;
final Map<dynamic, dynamic> eventMap = event;
final String eventType = eventMap['type'] as String? ?? '';
// 添加日志帮助调试
switch (eventType) {
case 'result':
final String text = eventMap['text'] as String? ?? '';
final String detectedLanguage =
eventMap['detectedLanguage'] as String? ?? '';
_latestRecognizedText = text;
_latestDetectedLanguage = detectedLanguage;
_eventStreamController?.add(RecognitionEvent1(
type: RecognitionEventType1.finalResult,
text: text,
detectedLanguage: detectedLanguage,
));
break;
case 'recognizing':
final String text = eventMap['text'] as String? ?? '';
final String detectedLanguage =
eventMap['detectedLanguage'] as String? ?? '';
_eventStreamController?.add(RecognitionEvent1(
type: RecognitionEventType1.intermediateResult,
text: text,
detectedLanguage: detectedLanguage,
));
break;
case 'sessionStarted':
_eventStreamController?.add(RecognitionEvent1(
type: RecognitionEventType1.sessionStarted,
));
break;
case 'sessionStopped':
_isContinuousRecognitionActive = false;
_eventStreamController?.add(RecognitionEvent1(
type: RecognitionEventType1.sessionStopped,
));
break;
case 'canceled':
_isContinuousRecognitionActive = false;
final String reason = eventMap['reason'] as String? ?? '';
final String errorDetails = eventMap['errorDetails'] as String? ?? '';
if (reason.isNotEmpty || errorDetails.isNotEmpty) {
Logger.error('识别取消: $reason - ${errorDetails.toString()}');
}
_eventStreamController?.add(RecognitionEvent1(
type: RecognitionEventType1.canceled,
error: '$reason: $errorDetails',
));
break;
case 'error':
final String error = eventMap['message'] as String? ?? '';
Logger.error('识别错误: ${error.toString()}');
_eventStreamController?.add(RecognitionEvent1(
type: RecognitionEventType1.error,
error: error,
));
break;
}
}
/// 处理识别事件流错误
void _handleRecognitionError(Object error) {
Logger.error('识别事件流错误: ${error.toString()}');
_eventStreamController?.addError(error);
_cleanupEventStream();
}
/// 清理事件流资源
void _cleanupEventStream() {
_eventStreamController?.close();
_eventStreamController = null;
_isContinuousRecognitionActive = false;
}
/// 从环境变量加载配置
void _loadConfig() {
// final _env = _storage.read("ENV") as Map<String, String>;
_subscriptionKey = AppConfig.env('AZURE_SPEECH_KEY') ?? '';
_serviceRegion = AppConfig.env('AZURE_SPEECH_REGION') ?? '';
_accessKey = AppConfig.env('VOLCANO_TRANSLATION_ACCESS_KEY') ?? '';
_secretKey = AppConfig.env('VOLCANO_TRANSLATION_SECRET_KEY') ?? '';
_region = AppConfig.env('VOLCANO_TRANSLATION_REGION') ?? 'cn-north-1';
_service = 'translate';
_baseUrl = 'https://translate.volcengineapi.com';
if (_subscriptionKey.isEmpty || _serviceRegion.isEmpty) {
throw Exception(
'未找到 Azure 语音服务配置。请在 .env 文件中设置 AZURE_SPEECH_KEY 和 AZURE_SPEECH_REGION');
}
}
@override
Future<Stream<RecognitionEvent1>> recognizeCallback() async {
if (!_isInitialized) {
await initialize();
}
try {
_eventStreamController = StreamController<RecognitionEvent1>.broadcast();
// 开始连续识别
final bool result = await _channel.invokeMethod('recognizeCallback');
if (!result) {
_cleanupEventStream();
}
return _eventStreamController!.stream;
} catch (e) {
Logger.error('开始连续语音识别失败: ${e.toString()}');
rethrow;
}
}
@override
Future<bool> enableRecord(String filePath) async {
try {
final bool result = await _channel.invokeMethod('enableRecord', {
'filePath': filePath,
});
return result;
} catch (e) {
Logger.error('开始录音: ${e.toString()}');
rethrow;
}
}
@override
Future<bool> path(String filePath) async {
try {
final bool result = await _channel.invokeMethod('path', {
'filePath': filePath,
});
return result;
} catch (e) {
Logger.error('开始录音: ${e.toString()}');
rethrow;
}
}
@override
Future<bool> startContinuousTranslation() async {
try {
final bool result =
await _channel.invokeMethod('startContinuousTranslation');
return result;
} catch (e) {
Logger.error('停止录音: ${e.toString()}');
rethrow;
}
}
@override
Future<bool> stopContinuousTranslation() async {
try {
final bool result =
await _channel.invokeMethod('stopContinuousTranslation');
return result;
} catch (e) {
Logger.error('停止录音: ${e.toString()}');
rethrow;
}
}
@override
Future<void> dispose() async {
try {
final bool result = await _channel.invokeMethod('dispose');
return;
} catch (e) {
Logger.error('停止录音: ${e.toString()}');
rethrow;
}
}
@override
Future<bool> initialize({
List<String>? supportedLanguages,
bool useExternalAudio = false,
bool useEchoCancellation = false,
}) async {
try {
final List<String> languages =
supportedLanguages ?? _defaultSupportedLanguages;
//底层会初始化前释放
// // 检查是否需要重新初始化
// if (_isInitialized) {
// await dispose();
// }
// 设置音频源类型
_audioSourceType = useExternalAudio
? AudioSourceType.external
: AudioSourceType.microphone;
// Future<bool> initialize({
// required String subscriptionKey,
// required String region,
// required List<String> supportedLanguages,
// required String audioSourceType,
// required String translationAccessKey,
// required String translationSecretKey,
// String translationRegion = 'cn-north-1',
// });
final bool result = await _channel.invokeMethod('initialize', {
'subscriptionKey': _subscriptionKey,
'region': _serviceRegion,
'supportedLanguages': languages,
'audioSourceType': _audioSourceType.toString().split('.').last,
'translationAccessKey': _accessKey,
'translationSecretKey': _secretKey,
'translationRegion': _region,
});
_isInitialized = result;
_setupEventChannel();
Logger.info('Azure 语音识别服务初始化${result ? '成功' : '失败'}');
return result;
} catch (e) {
Logger.error('Azure 语音识别服务初始化失败: ${e.toString()}');
_isInitialized = false;
rethrow;
}
}
}

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

@ -813,7 +813,7 @@ class VolcanoAsrApiService implements AsrService {
}
@override
Future<bool> enableRecord(String filePath) {
Future<bool> enableRecord(String filePath, {bool acceptAudioData = false}) {
// TODO: implement enableRecord
throw UnimplementedError();
}
@ -865,4 +865,22 @@ class VolcanoAsrApiService implements AsrService {
// TODO: implement startContinuousRecognition
throw UnimplementedError();
}
@override
Future<bool> setAudioConfig({int sampleRate = 16000, int channels = 1}) {
// TODO: implement setAudioConfig
throw UnimplementedError();
}
@override
Future<bool> resumeRecord() {
// TODO: implement resumeRecord
throw UnimplementedError();
}
@override
Stream<Uint8List>? getAudioDataStream() {
// TODO: implement getAudioDataStream
throw UnimplementedError();
}
}

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

@ -348,7 +348,7 @@ class VolcanoAsrService extends GetxService implements AsrService {
}
@override
Future<bool> enableRecord(String filePath) {
Future<bool> enableRecord(String filePath, {bool acceptAudioData = false}) {
// TODO: implement enableRecord
throw UnimplementedError();
}
@ -400,4 +400,22 @@ class VolcanoAsrService extends GetxService implements AsrService {
// TODO: implement startContinuousRecognition
throw UnimplementedError();
}
@override
Future<bool> setAudioConfig({int sampleRate = 16000, int channels = 1}) {
// TODO: implement setAudioConfig
throw UnimplementedError();
}
@override
Future<bool> resumeRecord() {
// TODO: implement resumeRecord
throw UnimplementedError();
}
@override
Stream<Uint8List>? getAudioDataStream() {
// TODO: implement getAudioDataStream
throw UnimplementedError();
}
}

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

@ -272,7 +272,7 @@ class XunfeiAsrService extends GetxService implements AsrService {
}
@override
Future<bool> enableRecord(String filePath) {
Future<bool> enableRecord(String filePath, {bool acceptAudioData = false}) {
// TODO: implement enableRecord
throw UnimplementedError();
}
@ -324,4 +324,22 @@ class XunfeiAsrService extends GetxService implements AsrService {
// TODO: implement startContinuousRecognition
throw UnimplementedError();
}
@override
Future<bool> setAudioConfig({int sampleRate = 16000, int channels = 1}) {
// TODO: implement setAudioConfig
throw UnimplementedError();
}
@override
Future<bool> resumeRecord() {
// TODO: implement resumeRecord
throw UnimplementedError();
}
@override
Stream<Uint8List>? getAudioDataStream() {
// TODO: implement getAudioDataStream
throw UnimplementedError();
}
}

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

@ -1070,9 +1070,9 @@ class AgentController extends GetxController {
try {
await AgentService.recognizeCallback();
if (_bluetoothManager.currentDeviceRx.value != null) {
await AgentService.restoreOriginalAudioState();
// await AgentService.restoreOriginalAudioState();
} else {
await AgentService.disableBluetoothAudio();
// await AgentService.disableBluetoothAudio();
}
} catch (e) {
Logger.e(TAG, '启动识别器失败: $e');

5
lib/modules/login/bindings/login_binding.dart

@ -4,6 +4,7 @@ import '../controllers/login_controller.dart';
class LoginBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut<LoginController>(() => LoginController());
// 使用 put 而不是 lazyPut,确保控制器立即创建
Get.put<LoginController>(LoginController(), permanent: false);
}
}
}

38
lib/modules/login/controllers/login_controller.dart

@ -214,15 +214,30 @@ class LoginController extends GetxController {
List<String> get verificationCode => _verificationCode;
final versionService = VersionUpdateService.to;
// 文本控制器
TextEditingController contactController = TextEditingController();
TextEditingController verificationCodeController = TextEditingController();
// 文本控制器 - 延迟初始化
TextEditingController? _contactController;
TextEditingController? _verificationCodeController;
// Getter 方法,确保控制器存在
TextEditingController get contactController {
_contactController ??= TextEditingController();
return _contactController!;
}
TextEditingController get verificationCodeController {
_verificationCodeController ??= TextEditingController();
return _verificationCodeController!;
}
@override
void onInit() {
super.onInit();
_viewState.value = LoginViewState.welcome;
// 初始化控制器
_contactController = TextEditingController();
_verificationCodeController = TextEditingController();
isChinaMainland.value =
Get.deviceLocale?.countryCode?.toUpperCase() == 'CN';
@ -239,8 +254,21 @@ class LoginController extends GetxController {
@override
void onClose() {
// contactController.dispose();
// verificationCodeController.dispose();
// 安全地销毁控制器
try {
_contactController?.dispose();
_contactController = null;
} catch (e) {
Logger().e('销毁 contactController 失败: $e');
}
try {
_verificationCodeController?.dispose();
_verificationCodeController = null;
} catch (e) {
Logger().e('销毁 verificationCodeController 失败: $e');
}
super.onClose();
}

40
lib/modules/login/views/login_view.dart

@ -371,27 +371,29 @@ class LoginView extends GetView<LoginController> {
),
SizedBox(width: 12.w),
Expanded(
child: TextField(
controller: controller.verificationCodeController,
keyboardType: TextInputType.number,
maxLength: 6,
decoration: InputDecoration(
hintText: 'verificationCode'.tr, // 验证码
hintStyle: TextStyle(
child: GetBuilder<LoginController>(
builder: (controller) => TextField(
controller: controller.verificationCodeController,
keyboardType: TextInputType.number,
maxLength: 6,
decoration: InputDecoration(
hintText: 'verificationCode'.tr, // 验证码
hintStyle: TextStyle(
fontSize: 14.sp,
color: isDarkMode
? Colors.grey[500]
: Colors.black38, // 调整提示文字颜色
),
border: InputBorder.none,
counterText: '',
fillColor: Colors.transparent,
filled: true,
focusedBorder: InputBorder.none,
),
style: TextStyle(
fontSize: 14.sp,
color: isDarkMode
? Colors.grey[500]
: Colors.black38, // 调整提示文字颜色
color: isDarkMode ? Colors.white : Colors.black,
),
border: InputBorder.none,
counterText: '',
fillColor: Colors.transparent,
filled: true,
focusedBorder: InputBorder.none,
),
style: TextStyle(
fontSize: 14.sp,
color: isDarkMode ? Colors.white : Colors.black,
),
),
),

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

@ -60,6 +60,7 @@ class MeetingRecordController extends GetxController
bool _audioSourceType = false;
late TabController tabController;
StreamSubscription<Uint8List>? _audioDataSubscription;
// 添加波形放大倍数变量
final double waveAmplifyFactor = 100.0; // 增加波动幅度
final RxDouble ursorPosition = 0.0.obs; // 当前播放位置
@ -123,7 +124,7 @@ class MeetingRecordController extends GetxController
/// Initializes UI controllers and focus nodes
void _initializeComponents() {
tabController = TabController(length: 2, vsync: this);
tabController = TabController(length: 1, vsync: this);
titleEditingController = TextEditingController(text: fileName.value);
titleFocusNode = FocusNode();
titleFocusNode.addListener(_handleTitleFocusChange);
@ -255,7 +256,22 @@ class MeetingRecordController extends GetxController
final fullFileName = "${fileName.value}_$formattedTime";
// Start audio recording
await _asrService.enableRecord("${dir.path}/$fullFileName.wav");
await _asrService.enableRecord("${dir.path}/$fullFileName.wav",
acceptAudioData: true);
// 监听音频数据流
_audioDataSubscription =
_asrService.getAudioDataStream()?.listen((audioData) {
// 处理音频数据,例如:
// 1. 实时音频可视化
// 2. 音频质量检测
// 3. 发送到其他服务
print('接收到音频数据: ${audioData.length} 字节');
// 处理音频数据的逻辑
_processAudioData(audioData);
});
isRecording.value = true;
// Update state
fileName.value = fullFileName;
@ -274,9 +290,11 @@ class MeetingRecordController extends GetxController
// _bleManager.openEncoder();
break;
case 1:
_asrService.setAudioConfig(sampleRate: 16000, channels: 1);
_bleManager.openDecoder();
break;
case 2:
_asrService.setAudioConfig(sampleRate: 16000, channels: 2);
_bleManager.openA2DPDecoder();
break;
}
@ -291,7 +309,7 @@ class MeetingRecordController extends GetxController
/// Resumes paused recording session
Future<void> _resumeRecording() async {
await _asrService.startContinuousRecognition(_audioSourceType);
await _asrService.resumeRecord();
startTimer();
}
@ -302,10 +320,6 @@ class MeetingRecordController extends GetxController
case RecognitionEventType.intermediateResult:
intermediateContent.value = event.text;
break;
case RecognitionEventType.onAudio:
// 添加新音频数据到波形
_processAudioData(event.audio!);
break;
case RecognitionEventType.finalResult:
finalContent.value += _formatTranscript(event);
intermediateContent.value = '';

163
lib/modules/opus_test/controllers/opus_test_controller.dart

@ -9,6 +9,7 @@ import 'package:path_provider/path_provider.dart';
import 'package:just_audio/just_audio.dart';
import 'package:jl_opus/jl_opus.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:share_plus/share_plus.dart'; // 添加分享插件导入
class OpusTestController extends GetxController {
// 选中的文件
@ -231,7 +232,7 @@ class OpusTestController extends GetxController {
isDecoding.value = false;
return;
}
print("pcmPath: $pcmPath");
tempPcmPath = pcmPath;
// 将PCM文件转换为WAV文件
@ -346,6 +347,166 @@ class OpusTestController extends GetxController {
// 数据大小
data.setUint32(40, pcmLength, Endian.little);
}
/// 删除外部文件
Future<void> deleteExternalFile(PlatformFile file) async {
try {
final fileToDelete = File(file.path!);
if (await fileToDelete.exists()) {
await fileToDelete.delete();
externalFiles.removeWhere((f) => f.path == file.path);
// 如果删除的是当前选中的文件,清空选择
if (selectedFiles.isNotEmpty && selectedFiles.first.path == file.path) {
selectedFiles.clear();
}
statusMessage.value = '文件已删除: ${file.name}';
} else {
statusMessage.value = '文件不存在: ${file.name}';
}
} catch (e) {
statusMessage.value = '删除文件失败: $e';
}
}
/// 分享外部文件
Future<void> shareExternalFile(PlatformFile file) async {
try {
if (file.path != null) {
final fileToShare = File(file.path!);
if (await fileToShare.exists()) {
await Share.shareXFiles(
[XFile(file.path!)],
text: '分享Opus音频文件: ${file.name}',
);
statusMessage.value = '正在分享文件: ${file.name}';
} else {
statusMessage.value = '文件不存在: ${file.name}';
}
}
} catch (e) {
statusMessage.value = '分享文件失败: $e';
}
}
/// 显示文件操作底部弹窗
void showFileActionSheet(BuildContext context, PlatformFile file) {
showModalBottomSheet(
context: context,
backgroundColor: Get.isDarkMode ? Colors.grey[900] : Colors.white,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
),
builder: (context) => Container(
padding: const EdgeInsets.symmetric(vertical: 20),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// 文件信息
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Row(
children: [
Icon(
Icons.audiotrack,
color: Get.isDarkMode ? Colors.white70 : Colors.black87,
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
file.name,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
color: Get.isDarkMode ? Colors.white : Colors.black,
),
overflow: TextOverflow.ellipsis,
),
Text(
'${(file.size / 1024).toStringAsFixed(2)} KB',
style: TextStyle(
fontSize: 14,
color: Get.isDarkMode
? Colors.white60
: Colors.black54,
),
),
],
),
),
],
),
),
const Divider(),
// 分享选项
ListTile(
leading: const Icon(Icons.share, color: Colors.blue),
title: const Text('分享文件'),
onTap: () {
Navigator.pop(context);
shareExternalFile(file);
},
),
// 删除选项
ListTile(
leading: const Icon(Icons.delete, color: Colors.red),
title: const Text('删除文件', style: TextStyle(color: Colors.red)),
onTap: () {
Navigator.pop(context);
_showDeleteConfirmDialog(context, file);
},
),
// 取消选项
ListTile(
leading: const Icon(Icons.cancel),
title: const Text('取消'),
onTap: () => Navigator.pop(context),
),
],
),
),
);
}
/// 显示删除确认对话框
void _showDeleteConfirmDialog(BuildContext context, PlatformFile file) {
showDialog(
context: context,
builder: (context) => AlertDialog(
backgroundColor: Get.isDarkMode ? Colors.grey[900] : Colors.white,
title: Text(
'确认删除',
style: TextStyle(
color: Get.isDarkMode ? Colors.white : Colors.black,
),
),
content: Text(
'确定要删除文件 "${file.name}" 吗?\n此操作不可撤销。',
style: TextStyle(
color: Get.isDarkMode ? Colors.white70 : Colors.black87,
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('取消'),
),
TextButton(
onPressed: () {
Navigator.pop(context);
deleteExternalFile(file);
},
style: TextButton.styleFrom(foregroundColor: Colors.red),
child: const Text('删除'),
),
],
),
);
}
}
// PCM音频源

4
lib/modules/opus_test/views/opus_test_view.dart

@ -170,6 +170,10 @@ class OpusTestView extends GetView<OpusTestController> {
onTap: () {
controller.selectExternalFile(file);
},
onLongPress: () {
// 长按显示操作菜单
controller.showFileActionSheet(context, file);
},
trailing: Icon(
Icons.arrow_forward,
color:

16
lib/modules/settings/controllers/settings_controller.dart

@ -192,7 +192,18 @@ class SettingsController extends GetxController {
},
{'type': Permission.bluetooth, 'name': '蓝牙', 'description': '用于连接蓝牙设备'}
]
}
},
{
'name': '闹钟权限',
'icon': Icons.alarm,
'permissions': [
{
'type': Permission.scheduleExactAlarm,
'name': '闹钟',
'description': '用于设置精确闹钟'
}
]
},
];
// 检查单个权限状态
@ -453,7 +464,8 @@ class SettingsController extends GetxController {
Logger().i('更新agent服务语音识别语言: ${supportedLanguages.join(', ')}');
// 调用agent服务更新语言设置
final success = await AgentService.setSupportedLanguages(supportedLanguages);
final success =
await AgentService.setSupportedLanguages(supportedLanguages);
if (success) {
Logger().i('agent服务语言设置更新成功');
} else {

44
lib/modules/settings/views/permissions_view.dart

@ -4,6 +4,8 @@ import 'package:permission_handler/permission_handler.dart';
import '../controllers/settings_controller.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'dart:ui';
import 'dart:io' show Platform;
import '../../../core/utils/permission_util.dart'; // 引入PermissionUtil
class PermissionsView extends GetView<SettingsController> {
@ -37,7 +39,7 @@ class PermissionsView extends GetView<SettingsController> {
centerTitle: true,
),
body: FutureBuilder<Map<Permission, PermissionStatus>>(
future: controller.checkAllPermissions(),
future: controller.checkAllPermissions(), //检查权限当前状态
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
@ -45,8 +47,8 @@ class PermissionsView extends GetView<SettingsController> {
if (snapshot.hasError) {
return Center(
child: Text('getPermissionStatusFailed'.tr +
': ${snapshot.error}')); // 获取权限状态失败
child: Text(
'${'getPermissionStatusFailed'.tr}: ${snapshot.error}')); // 获取权限状态失败
}
final permissionStatuses = snapshot.data ?? {};
@ -54,15 +56,27 @@ class PermissionsView extends GetView<SettingsController> {
return ListView(
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 16.h),
children: [
// 通讯权限组
_buildPermissionSection(
'communicationPermissions'.tr, // 通讯权限
Icons.phone_android,
[Permission.phone, Permission.sms],
// 通讯权限组- iOS平台没有通讯权限
if (Platform.isAndroid) ...[
_buildPermissionSection(
'communicationPermissions'.tr, // 通讯权限
Icons.phone_android,
[Permission.phone, Permission.sms],
permissionStatuses,
isDarkMode),
SizedBox(height: 20.h),
],
// 闹钟权限组
if (Platform.isAndroid) ...[
_buildPermissionSection(
'alarmPermissions'.tr, // 闹钟权限
Icons.alarm,
[Permission.scheduleExactAlarm],
permissionStatuses,
isDarkMode),
SizedBox(height: 20.h),
isDarkMode,
),
SizedBox(height: 20.h),
],
// 日历权限组
_buildPermissionSection(
@ -162,6 +176,8 @@ class PermissionsView extends GetView<SettingsController> {
return 'calendarFullAccess'.tr; // 日历完整访问
case Permission.location:
return 'locationInfo'.tr; // 位置信息
case Permission.scheduleExactAlarm:
return 'alarm'.tr; // 闹钟
default:
return permission.toString();
}
@ -180,6 +196,8 @@ class PermissionsView extends GetView<SettingsController> {
return 'calendarFullAccessPermissionDesc'.tr; // 用于读取和管理日历事件及提醒
case Permission.location:
return 'locationPermissionDesc'.tr; // 用于获取当前位置信息
case Permission.scheduleExactAlarm:
return 'alarmPermissionDesc'.tr; // 设置精确闹钟需要此权限
default:
return 'appFunctionNeeds'.tr; // 应用功能需要
}
@ -355,6 +373,8 @@ class PermissionsView extends GetView<SettingsController> {
return 'calendarFullAccess'.tr; // 日历完整访问
case Permission.location:
return 'locationInfo'.tr; // 位置信息
case Permission.scheduleExactAlarm:
return 'alarm'.tr; // 闹钟
default:
return permission.toString();
}
@ -373,6 +393,8 @@ class PermissionsView extends GetView<SettingsController> {
return 'calendarFullAccessPermissionDetailDesc'.tr; // 管理日历事件需要完整日历权限
case Permission.location:
return 'locationPermissionDetailDesc'.tr; // 获取位置信息需要位置权限
case Permission.scheduleExactAlarm:
return 'alarmPermissionDetailDesc'.tr; // 设置精确闹钟需要此权限
default:
return 'appNeedsPermissionForFullFunction'.tr; // 应用需要此权限以提供完整功能
}

34
lib/modules/settings/views/settings_view.dart

@ -514,23 +514,23 @@ class SettingsView extends GetView<SettingsController> {
// color: isDarkMode
// ? Colors.white.withOpacity(0.1)
// : Colors.grey[200]),
// BLE测试
// _buildSimpleNavigationSetting(
// title: 'opus解码测试',
// subtitle: '测试opus解码',
// icon: Icons.bluetooth_searching,
// iconBgColor: isDarkMode
// ? Colors.green[900]!.withOpacity(0.3)
// : Colors.green[100]!,
// iconColor:
// isDarkMode ? Colors.green[300]! : Colors.green[600]!,
// titleColor: isDarkMode ? Colors.white : null,
// subtitleColor: isDarkMode ? Colors.white70 : null,
// onTap: () {
// Get.toNamed(Routes.opusTest);
// },
// isDarkMode: isDarkMode,
// ),
//BLE测试
_buildSimpleNavigationSetting(
title: 'opus解码测试',
subtitle: '测试opus解码',
icon: Icons.bluetooth_searching,
iconBgColor: isDarkMode
? Colors.green[900]!.withOpacity(0.3)
: Colors.green[100]!,
iconColor:
isDarkMode ? Colors.green[300]! : Colors.green[600]!,
titleColor: isDarkMode ? Colors.white : null,
subtitleColor: isDarkMode ? Colors.white70 : null,
onTap: () {
Get.toNamed(Routes.opusTest);
},
isDarkMode: isDarkMode,
),
// Divider(
// height: 1,
// color: isDarkMode

13
lib/modules/speech_test/controllers/speech_test_controller.dart

@ -164,7 +164,15 @@ class SpeechTestController extends GetxController {
}
recognitionStatus.value = '识别完成';
break;
case RecognitionEventType.finalResult1:
// 更新最终结果,显示为正常文本
finalResult.value = event.text;
intermediateResult.value = ''; // 清空中间结果
if (event.detectedLanguage.isNotEmpty) {
detectedLanguage.value = event.detectedLanguage;
}
recognitionStatus.value = '识别完成';
break;
case RecognitionEventType.error:
isListening.value = false;
recognitionStatus.value = '识别错误: ${event.error}';
@ -176,9 +184,6 @@ class SpeechTestController extends GetxController {
// 清空中间结果
intermediateResult.value = '';
break;
case RecognitionEventType.onAudio:
// TODO: Handle this case.
break;
}
}, onError: (error) {
isListening.value = false;

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

@ -9,6 +9,7 @@ import 'package:get_storage/get_storage.dart';
import 'package:intl/intl.dart';
import 'package:path_provider/path_provider.dart';
import 'package:permission_handler/permission_handler.dart';
import '../../../data/services/ast_service.dart';
import '../../../data/services/bluetooth_manager.dart';
import '../../../data/services/music_manager.dart';
import '../../../data/services/volcano_translation_service.dart';
@ -37,6 +38,8 @@ class TranslationController extends GetxController {
final VolcanoTranslationService _translationService =
Get.find<VolcanoTranslationService>();
final TtsService _ttsService = Get.find<TtsService>();
final AstService _astService = Get.find<AstService>();
final LanguageManager _languageManager = Get.find<LanguageManager>();
final GetStorage _storage = GetStorage();
// 蓝牙服务
@ -62,6 +65,8 @@ class TranslationController extends GetxController {
Timer? _recordTimer;
//秒
int _seconds = 0;
//暂停状态记录计时器是否暂停
bool _isTimerPaused = false;
// 存储相关配置
static const String _historyKey = 'translation_history';
static const String _sourceLanguageKey = 'translation_source_language';
@ -89,6 +94,8 @@ class TranslationController extends GetxController {
final isTranslating = false.obs;
final isTtsEnabled = true.obs;
final isRecording = false.obs;
final lasyIsRecording = false.obs;
final hasRecordPermission = false.obs;
// 语言相关
final sourceLanguage = '中文(简体)'.obs;
final targetLanguage = '英语'.obs;
@ -107,11 +114,15 @@ class TranslationController extends GetxController {
var appDir;
var dir;
// 最新的语音识别文本
final latestRecognitionText = ''.obs;
// 检测到的语言代码
final detectedLanguageCode1 = ''.obs;
//int num = 0;
Future<void> changeTranslationMode(String mode) async {
currentMode.value = mode;
currentModeTitle.value = _getModeTitle(mode);
stopAll();
}
@ -129,12 +140,57 @@ class TranslationController extends GetxController {
}
}
/// 暂停计时器
void pauseRecordTimer() {
if (_recordTimer != null && !_isTimerPaused) {
_recordTimer?.cancel();
_recordTimer = null;
_isTimerPaused = true;
Logger.info('录音计时器已暂停');
}
}
/// 恢复计时器
void resumeRecordTimer() {
if (_isTimerPaused) {
_recordTimer = Timer.periodic(Duration(seconds: 1), (_) {
_seconds++;
final minutes = (_seconds ~/ 60).toString().padLeft(2, '0');
final seconds = (_seconds % 60).toString().padLeft(2, '0');
recordDurationText.value = '$minutes:$seconds';
});
_isTimerPaused = false;
Logger.info('录音计时器已恢复');
}
}
/// 重置计时器
void resetRecordTimer() {
_recordTimer?.cancel();
_recordTimer = null;
_seconds = 0;
_isTimerPaused = false;
recordDurationText.value = '00:00';
Logger.info('录音计时器已重置');
}
Future<void> setActiveSpeaker(int id) async {
activeSpeaker.value = id;
await _asrService.startContinuousRecognition(_audioSourceType);
// 监听识别结果
if (activeSpeaker.value != 0) {
isPlayback = false;
await _asrService.startContinuousRecognition(_audioSourceType);
isRecognizing.value = true;
if (isRecording.value && isRecognizing.value) {
if (lasyIsRecording.value != isRecording.value) {
lasyIsRecording.value = isRecording.value;
await startRecording();
} else {
await _asrService.resumeRecord();
// 恢复计时器
resumeRecordTimer();
}
}
// 开始ASR活跃时长跟踪(面对面模式)
_startAsrActiveTracking();
@ -143,7 +199,11 @@ class TranslationController extends GetxController {
} else {
// 停止ASR活跃时长跟踪(面对面模式)
_stopAsrActiveTracking();
if (isRecording.value && isRecognizing.value) {
// 暂停计时器
pauseRecordTimer();
await _asrService.pauseRecord();
}
if (fTFTranslationResult != '' && !isPlayback) {
isPlayback = true;
await _ttsService.setAudioOutputDevice(lastActiveSpeaker.value);
@ -181,7 +241,6 @@ class TranslationController extends GetxController {
StreamSubscription? _recognitionSubscription;
StreamSubscription? _voiceInteractionSubscription;
Timer? _translationDebounceTimer;
// 获取支持的语言列表
Map<String, String> get supportedLanguages =>
_languageManager.getChineseNameToAsrCodeMap();
@ -191,6 +250,9 @@ class TranslationController extends GetxController {
super.onInit();
//_setupEventListeners();
// 预先检查权限,避免在用户交互时阻塞
_preCheckPermissions();
// 初始化统计服务
_initUsageService();
@ -215,11 +277,21 @@ class TranslationController extends GetxController {
// 初始化服务
_initServices();
// 初始化语音翻译服务
_initializeCallModeTranslationService();
// 🔑 reverse模式下不需要手动滚动,ListView会自然显示最新内容
// 参考Agent模块的实现模式
}
/// 预先检查权限
Future<void> _preCheckPermissions() async {
try {
await requestRecordPermission();
} catch (e) {
Logger.e("Permission", "预检查权限失败: $e");
}
}
// 初始化统计服务
void _initUsageService() {
try {
@ -256,16 +328,17 @@ class TranslationController extends GetxController {
_storage.write(_targetLanguageKey, targetLanguage.value);
}
Future<bool> _requestRecordPermission() async {
Future<bool> requestRecordPermission() async {
try {
final bool granted = await PermissionUtil.instance.requestPermission(
hasRecordPermission.value =
await PermissionUtil.instance.requestPermission(
permissionType: Permission.microphone,
permissionName: 'microphonePermission'.tr,
explanationText: 'microphonePermissionExplanation'.tr,
permanentDenialText: 'microphonePermissionPermanentDenial'.tr,
);
if (granted) {
if (hasRecordPermission.value) {
Logger.d("Permission", "用户授予了录音权限");
return true;
} else {
@ -349,6 +422,7 @@ class TranslationController extends GetxController {
_recognitionSubscription?.cancel();
_recognitionSubscription =
recognitionStream.listen(_handleRecognitionEvent);
final bool storage = await _requestStoragePermission();
if (!storage) {
Logger.d("Permission", "未授予存储权限,无法提供录音功能");
@ -365,6 +439,144 @@ class TranslationController extends GetxController {
}
}
// 初始化通话模式的语音翻译服务
Future<void> _initializeCallModeTranslationService() async {
try {
Logger.info('开始初始化通话模式语音翻译服务');
// 为通话模式配置特殊的ASR设置
final List<String> callModeLanguages = [
sourceLanguageCode.value,
targetLanguageCode.value
];
// Future<bool> initialize({
// required String subscriptionKey,
// required String region,
// required List<String> supportedLanguages,
// required String audioSourceType,
// required String translationAccessKey,
// required String translationSecretKey,
// String translationRegion = 'cn-north-1',
// });
// 重新初始化ASR服务以支持通话音频源
await _astService.initialize(supportedLanguages: callModeLanguages);
// 配置实时翻译参数
await _configureCallModeTranslation();
Logger.info('通话模式语音翻译服务初始化完成');
return;
} catch (e) {
Logger.error('通话模式语音翻译服务初始化失败: ${e.toString()}');
}
}
// 配置通话模式的翻译参数
Future<void> _configureCallModeTranslation() async {
try {
// 设置通话模式的特殊配置
// 1. 更短的识别超时时间,适应通话场景
// 2. 更高的识别敏感度
// 3. 噪声抑制优化
// 这里可以调用ASR服务的特殊配置方法
// await _asrService.configureForCallMode(
// endSilenceTimeout: 200, // 更短的静音超时
// noiseReduction: true, // 启用噪声抑制
// echoCancellation: true, // 启用回声消除
// );
Logger.info('通话模式翻译参数配置完成');
} catch (e) {
Logger.error('通话模式翻译参数配置失败: ${e.toString()}');
}
}
// // 处理通话模式的翻译结果
// Future<void> _handleCallModeTranslation(String sourceText) async {
// try {
// // 在通话模式下,翻译结果可能需要特殊处理
// // 例如:发送到蓝牙设备、显示在特定UI等
// final translationResult = await _translationService.translateText(
// text: sourceText,
// sourceLanguageCode: sourceLanguageCode.value,
// targetLanguageCode: targetLanguageCode.value,
// );
// if (translationResult != null && translationResult.isNotEmpty) {
// // 通话模式下的特殊处理
// await _processCallModeTranslationResult(sourceText, translationResult);
// }
// } catch (e) {
// Logger.error('通话模式翻译处理失败: ${e.toString()}');
// }
// }
// // 处理通话模式的翻译结果
// Future<void> _processCallModeTranslationResult(String sourceText, String translatedText) async {
// try {
// // 1. 更新UI显示
// final newItem = TranslationItem(
// sourceText: sourceText,
// translatedText: translatedText,
// sourceLanguageCode: sourceLanguageCode.value,
// targetLanguageCode: targetLanguageCode.value,
// timestamp: DateTime.now(),
// sessionId: currentSessionId ?? DateTime.now().millisecondsSinceEpoch.toString(),
// isFirstInSession: translationHistory.isEmpty,
// isIntermediate: false,
// );
// translationHistory.add(newItem);
// translationHistory.refresh();
// _scrollToBottom();
// saveTranslationHistory();
// // 2. 通话模式下可能需要将翻译结果发送到蓝牙设备
// // 或者通过其他方式传输给通话对方
// await _sendTranslationToCallParty(translatedText);
// // 3. 记录统计信息
// _recordCallModeUsageStats(sourceText, translatedText);
// Logger.info('通话模式翻译结果处理完成: $sourceText -> $translatedText');
// } catch (e) {
// Logger.error('通话模式翻译结果处理失败: ${e.toString()}');
// }
// }
// // 将翻译结果发送给通话对方
// Future<void> _sendTranslationToCallParty(String translatedText) async {
// try {
// // 这里可以实现将翻译结果发送给通话对方的逻辑
// // 例如:通过蓝牙、网络等方式
// // 示例:通过蓝牙发送
// await bleManager.sendTranslationResult(translatedText);
// Logger.info('翻译结果已发送给通话对方: $translatedText');
// } catch (e) {
// Logger.error('发送翻译结果失败: ${e.toString()}');
// }
// }
// // 记录通话模式的使用统计
// void _recordCallModeUsageStats(String sourceText, String translatedText) {
// try {
// _usageService.recordTranslationApiCall(
// sourceText: sourceText,
// targetText: translatedText,
// sourceLanguage: _languageManager.getChineseNameByAsrCode(sourceLanguageCode.value) ?? '未知',
// targetLanguage: _languageManager.getChineseNameByAsrCode(targetLanguageCode.value) ?? '未知',
// mode: 'call', // 明确标记为通话模式
// );
// Logger.info('通话模式使用统计已记录');
// } catch (e) {
// Logger.error('记录通话模式统计失败: ${e.toString()}');
// }
// }
@override
void onClose() {
stopAll();
@ -392,11 +604,6 @@ class TranslationController extends GetxController {
}
Future<void> startRecording() async {
final bool storage = await _requestStoragePermission();
if (!storage) {
Logger.d("Permission", "未授予存储权限,无法提供录音功能");
return;
}
//isRecording.value = true;
_seconds = 0;
recordDurationText.value = '00:00';
@ -412,23 +619,29 @@ class TranslationController extends GetxController {
final formattedTime = DateFormat('yyyyMMdd_HHmmss').format(DateTime.now());
await _asrService.enableRecord(
"${dir.path}/${currentModeTitle.value.tr}_$formattedTime.wav");
// await _astService.enableRecord(
// "${dir.path}/${currentModeTitle.value.tr}_${formattedTime}_mic.wav");
return;
}
Future<void> stopRecording() async {
_recordTimer?.cancel();
_recordTimer = null;
isRecording.value = false;
_isTimerPaused = false;
recordDurationText.value = '00:00';
await _asrService.stopRecord(true);
// 停止实际录音逻辑
// ...
_seconds = 0;
_asrService.stopRecord(true);
lasyIsRecording.value = false;
Logger.info('录音已停止,计时器已重置');
}
// 开始语音识别
Future<void> startRecognition() async {
Logger.info('开始语音识别');
// 等待权限请求完成并获取结果
final bool record = await _requestRecordPermission();
final bool record = await requestRecordPermission();
// 如果权限被拒绝,直接返回
if (!record) {
@ -476,6 +689,8 @@ class TranslationController extends GetxController {
bleManager.openA2DPDecoder();
_audioSourceType = true;
isTtsEnabled.value = false;
await _astService.startContinuousTranslation();
// await _astService.path("${dir.path}/8_mic.wav");
Logger.info('发送ble系统mic和dac(音乐或者通话远端)声音');
} else {
// 开始连续语音识别
@ -483,18 +698,14 @@ class TranslationController extends GetxController {
isTtsEnabled.value = true;
}
if (currentMode.value == 'faceToFace') {
_asrService.stopContinuousRecognition();
if (_bluetoothManager.currentDeviceRx.value != null) {
restoreOriginalAudioState();
} else {
if (_bluetoothManager.currentDeviceRx.value != null) {
restoreOriginalAudioState();
} else {
disableBluetoothAudio();
}
await _asrService.startContinuousRecognition(_audioSourceType);
// 开始ASR活跃时长计时
_startAsrActiveTracking();
disableBluetoothAudio();
}
await _asrService.startContinuousRecognition(_audioSourceType);
// 开始ASR活跃时长计时
_startAsrActiveTracking();
isRecognizing.value = true;
if (isRecording.value && isRecognizing.value) {
startRecording();
@ -510,13 +721,22 @@ class TranslationController extends GetxController {
if (event.type == RecognitionEventType.finalResult &&
event.text.isNotEmpty) {
detectedLanguageCode = event.detectedLanguage;
print("处理识别事件:${event.text}");
handleFinalResult(event.text);
} else if (event.type == RecognitionEventType.intermediateResult &&
event.text.isNotEmpty) {
if (event.detectedLanguage.isNotEmpty) {
detectedLanguageCode = event.detectedLanguage;
}
print("处理识别事件:${event.text}");
handleIntermediateResult(event.text);
} else if (event.type == RecognitionEventType.finalResult1 &&
event.text.isNotEmpty) {
if (event.detectedLanguage.isNotEmpty) {
detectedLanguageCode1.value = event.detectedLanguage;
}
latestRecognitionText.value = event.text;
print("处理麦:${event.text}");
} else if (event.type == RecognitionEventType.error) {
isRecognizing.value = false;
Logger.error('语音识别错误: ${event.error}');
@ -531,8 +751,9 @@ class TranslationController extends GetxController {
if (!isRecognizing.value) return;
try {
await stopRecording();
await _asrService.stopContinuousRecognition();
await _astService.stopContinuousTranslation();
// 停止ASR活跃时长计时
_stopAsrActiveTracking();
@ -555,7 +776,7 @@ class TranslationController extends GetxController {
saveTranslationHistory();
}
stopRecording();
currentSessionId = null;
} catch (e) {
Logger.error('停止语音识别失败: ${e.toString()}');
@ -833,7 +1054,6 @@ class TranslationController extends GetxController {
}
// 确保停止ASR活跃时长跟踪
_stopAsrActiveTracking();
stopRecording();
_translationDebounceTimer?.cancel();
await _ttsService.stop();
} catch (e) {
@ -979,6 +1199,9 @@ class TranslationController extends GetxController {
// 重新初始化ASR服务以支持新的语言
Future<void> _reinitializeAsrService() async {
try {
// 停止当前识别
await stopRecognition();
// 更新ASR支持的语言
final List<String> asrSupportedLanguages = [
sourceLanguageCode.value,
@ -991,6 +1214,17 @@ class TranslationController extends GetxController {
await _asrService.initialize(
supportedLanguages: asrSupportedLanguages,
);
// Future<bool> initialize({
// required String subscriptionKey,
// required String region,
// required List<String> supportedLanguages,
// required String audioSourceType,
// required String translationAccessKey,
// required String translationSecretKey,
// String translationRegion = 'cn-north-1',
// });
// 重新初始化ASR服务以支持通话音频源
await _astService.initialize(supportedLanguages: asrSupportedLanguages);
// 获取识别事件流 - 这启动了异步识别过程
var recognitionStream = await _asrService.recognizeCallback();
// 监听识别结果

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

@ -459,67 +459,128 @@ class TranslationView extends GetView<TranslationController> {
],
),
child: Obx(() {
// 同时模式显示双按钮
if (controller.currentMode.value == 'faceToFace') {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// 耳机图标按钮 (说话者1)
_buildHoldToTalkButton(
isDarkMode: isDarkMode,
speakerId: 1,
activeSpeaker: controller.activeSpeaker.value,
icon: Icons.headset, // 耳机图标
label: "headsetUser".tr, // 耳机用户
return Column(
mainAxisSize: MainAxisSize.min,
children: [
// 原有的按钮区域
if (controller.currentMode.value == 'faceToFace')
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// 耳机图标按钮 (说话者1)
_buildHoldToTalkButton(
isDarkMode: isDarkMode,
speakerId: 1,
activeSpeaker: controller.activeSpeaker.value,
icon: Icons.headset,
label: "headsetUser".tr,
),
SizedBox(width: 40.w),
// 手机图标按钮 (说话者2)
_buildHoldToTalkButton(
isDarkMode: isDarkMode,
speakerId: 2,
activeSpeaker: controller.activeSpeaker.value,
icon: Icons.phone_android,
label: "phoneUser".tr,
),
],
)
else
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// 主要控制按钮 - 开始/暂停语音识别
Obx(() {
final isRecognizing = controller.isRecognizing.value;
return GestureDetector(
onTap: () {
if (isRecognizing) {
controller.stopAll();
} else {
controller.startRecognition();
}
},
child: Container(
width: 60.w,
height: 60.w,
decoration: BoxDecoration(
color: isRecognizing
? (isDarkMode ? Colors.red[700] : Colors.red)
: (isDarkMode ? AppColors.primary : Colors.blue),
shape: BoxShape.circle,
),
child: Icon(
isRecognizing ? Icons.pause : Icons.play_arrow,
color: Colors.white,
size: 30.sp,
),
),
);
}),
],
),
SizedBox(width: 40.w),
// 手机图标按钮 (说话者2)
_buildHoldToTalkButton(
isDarkMode: isDarkMode,
speakerId: 2,
activeSpeaker: controller.activeSpeaker.value,
icon: Icons.phone_android, // 手机图标
label: "phoneUser".tr, // 手机用户
// 添加间距
SizedBox(height: 16.h),
// 语音识别结果显示框
Container(
width: double.infinity,
padding: EdgeInsets.all(12.w),
decoration: BoxDecoration(
color: isDarkMode ? Colors.grey[800] : Colors.grey[100],
borderRadius: BorderRadius.circular(8.r),
border: Border.all(
color: isDarkMode ? Colors.grey[600]! : Colors.grey[300]!,
width: 1,
),
),
],
);
}
// 普通模式显示单按钮
else {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// 主要控制按钮 - 开始/暂停语音识别
Obx(() {
final isRecognizing = controller.isRecognizing.value;
return GestureDetector(
onTap: () {
if (isRecognizing) {
controller.stopAll();
} else {
controller.startRecognition();
}
},
child: Container(
width: 60.w,
height: 60.w,
decoration: BoxDecoration(
color: isRecognizing
? (isDarkMode ? Colors.red[700] : Colors.red)
: (isDarkMode ? AppColors.primary : Colors.blue),
shape: BoxShape.circle,
child: Obx(() {
// 显示最新的语音识别结果
final recognitionText = controller.latestRecognitionText.value;
final detectedLanguage = controller.detectedLanguageCode1.value;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 标题
Text(
'麦头语音识别结果',
style: TextStyle(
fontSize: 12.sp,
color: isDarkMode ? Colors.grey[400] : Colors.grey[600],
fontWeight: FontWeight.w500,
),
),
child: Icon(
isRecognizing ? Icons.pause : Icons.play_arrow,
color: Colors.white,
size: 30.sp,
SizedBox(height: 4.h),
// 识别文本 - 这里显示 finalResult1 的内容
Text(
recognitionText.isEmpty ? '等待语音输入...' : recognitionText,
style: TextStyle(
fontSize: 14.sp,
color: isDarkMode ? Colors.white : Colors.black87,
height: 1.3,
),
),
),
// 检测到的语言(如果有)
if (detectedLanguage.isNotEmpty) ...[
SizedBox(height: 4.h),
Text(
'检测语言: $detectedLanguage',
style: TextStyle(
fontSize: 10.sp,
color:
isDarkMode ? Colors.grey[500] : Colors.grey[500],
),
),
],
],
);
}),
],
);
}
),
],
);
}),
);
}
@ -587,8 +648,13 @@ class TranslationView extends GetView<TranslationController> {
bool isDisabled = activeSpeaker != 0 && activeSpeaker != speakerId;
Timer? _holdTimer; // 增加延迟触发的计时器
return Listener(
onPointerDown: (_) {
// 设置300ms延迟防止误触
onPointerDown: (_) async {
// 同步检查权限状态,不使用 await
if (!controller.hasRecordPermission.value) {
Get.snackbar('权限提示', '需要麦克风权限才能使用录音功能');
return;
}
_holdTimer = Timer(const Duration(milliseconds: 200), () {
if (activeSpeaker == 0) {
controller.setActiveSpeaker(speakerId);

4
local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt

@ -594,9 +594,7 @@ object AgentService : CoroutineScope {
}
override fun onAudio(data: ByteArray) {
}
override fun onSessionStopped() {
sendEvent("recognition_stopped", mapOf("status" to "stopped"))

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

@ -135,7 +135,9 @@ Log.d(TAG, "手动启动语音识别: ")
AgentService.pushAudioData(data)
// 可选:处理音频数据
}
override fun onAudioDataReceived1(data: ByteArray) {
}
/**
* 处理唤醒信号
* 在收到唤醒信号时启动语音识别

2
local_plugins/agent_service/ios/agent_service/Package.swift

@ -14,6 +14,7 @@ let package = Package(
.package(path: "../../../azure_speech/ios/azure_speech"),
.package(path: "../../../chat_api/ios/chat_api"),
.package(path: "../../../chat_storage/ios/chat_storage"),
.package(path: "../../../location_service/ios/location_service"),
.package(path: "../../../music_service/ios/music_service"),
.package(path: "../../../music_service/ios/user_portrait")
@ -28,6 +29,7 @@ let package = Package(
.product(name: "azure-speech", package: "azure_speech"),
.product(name: "chat-api", package: "chat_api"),
.product(name: "chat-storage", package: "chat_storage"),
.product(name: "location-service", package: "location_service"),
.product(name: "music-service", package: "music_service"),
.product(name: "user-portrait", package: "user_portrait"),
],

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

@ -1,6 +1,7 @@
import Foundation
import AVFoundation
import ble_service
import location_service
import speech
import azure_speech
import os
@ -65,7 +66,7 @@ class AgentServiceImpl: NSObject {
private let maxIdleSeconds: TimeInterval = 10
internal var audioPlayer: AudioPlayer?
private var locationInfo:[String: String]?
// private var locationInfo:[String: String]?
internal let logger = OSLog(subsystem: "com.yunqiinnovation.agent_service", category: "AgentServiceImpl")
@ -182,7 +183,7 @@ class AgentServiceImpl: NSObject {
}
//启动定位服务
NotificationCenter.default.post(name: .locationstartEvent, object: nil)
addlocationmonitor()
// addlocationmonitor()
if let mcpServer = config["mcpServer"] as? String {
self.mcpServer = mcpServer
}
@ -204,7 +205,9 @@ class AgentServiceImpl: NSObject {
if isInitialized {
loadChatHistory()
}
LocationService.shared.getCurrentLocation() { location in
// self.locationInfo = location
}
return isInitialized
}
@ -841,19 +844,19 @@ private func jsonToString(_ json: [String: Any]) -> String? {
}
// 添加定位监听
private func addlocationmonitor(){
// 通常放在启动后或模块初始化时
NotificationCenter.default.addObserver(
forName: .updatelocationEvent,
object: nil,
queue: .main
) { notification in
if let info = notification.userInfo as? [String: String] {
print("📤 地址信息:\(info)")
self.locationInfo = info
}
}
}
// private func addlocationmonitor(){
// // 通常放在启动后或模块初始化时
// NotificationCenter.default.addObserver(
// forName: .updatelocationEvent,
// object: nil,
// queue: .main
// ) { notification in
// if let info = notification.userInfo as? [String: String] {
// print("📤 地址信息:\(info)")
// self.locationInfo = info
// }
// }
// }
/**
* 填充模板字符串,使用正则表达式匹配 {key} 格式的占位符
* 对应 Kotlin 版本的 fillTemplate 方法
@ -1379,6 +1382,10 @@ extension AgentServiceImpl: BleService.Callback {
pushAudioData(data)
}
func onAudioDataReceived1(data: Data) {
}
func onWakeupSignalReceived() {
stopTts()

1
local_plugins/azure_speech/android/build.gradle.kts

@ -54,6 +54,7 @@ dependencies {
// 添加Microsoft语音SDK
implementation("com.microsoft.cognitiveservices.speech:client-sdk:1.43.0")
implementation(project(":speech"))
implementation("com.squareup.okhttp3:okhttp:4.12.0")
add("compileOnly", project(":ble_service"))
}

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

@ -11,7 +11,7 @@ import android.media.audiofx.AutomaticGainControl
import android.util.Log
import com.microsoft.cognitiveservices.speech.*
import com.microsoft.cognitiveservices.speech.util.EventHandler
import com.yunqiinnovation.azure_speech.tools.SimpleAudioReceiver
import android.media.AudioManager
import android.media.AudioAttributes
import android.media.AudioFocusRequest
@ -58,14 +58,11 @@ class AzureAsrHelper(private val context: Context) {
var audioSourceType = AudioSourceType.MICROPHONE
// 音频处理
var audioStream: AudioStream? = null
var audioStream: SimpleAudioReceiver? = null
// 音频模式管理
private var audioManager: AudioManager? = null
private var originalAudioMode = AudioManager.MODE_NORMAL
// 录音文件处理
var recordfile: RecordFile? = null
/**
* 音频来源类型
@ -103,10 +100,6 @@ class AzureAsrHelper(private val context: Context) {
// 释放之前的资源
dispose()
// 初始化音频管理器
audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
originalAudioMode = audioManager?.mode ?: AudioManager.MODE_NORMAL
// 保存配置
this.subscriptionKey = subscriptionKey
@ -147,9 +140,7 @@ class AzureAsrHelper(private val context: Context) {
// 设置分段策略为时间模式
setProperty("Speech_SegmentationStrategy", "Time")
}
// 录音文件类
recordfile = RecordFile;
return true
} catch (e: Exception) {
@ -158,6 +149,21 @@ class AzureAsrHelper(private val context: Context) {
}
}
// /**
// * 设置音频配置
// * @param sampleRate 采样率 (8000, 16000, 24000, 32000, 44100, 48000)
// * @param channels 声道数 (1=单声道, 2=立体声)
// */
// fun setAudioConfig(sampleRate: Int, channels: Int) {
// try {
// // 设置RecordFile的音频配置
// recordfile?.setAudioConfig(sampleRate, channels)
// Log.d(tag, "音频配置已设置: 采样率=${sampleRate}Hz, 声道数=${channels}")
// } catch (e: Exception) {
// Log.e(tag, "设置音频配置失败: ${e.message}")
// }
// }
private fun isRecognizerValid(): Boolean {
return recognizer != null
}
@ -203,7 +209,7 @@ class AzureAsrHelper(private val context: Context) {
if (audioStream == null) {
// 创建外部音频拉流对象
audioStream = AudioStream()
audioStream = SimpleAudioReceiver(context)
audioStream!!.initAudioRecord()
}
@ -237,11 +243,19 @@ class AzureAsrHelper(private val context: Context) {
stopContinuousRecognition()
}
try {
// 启动音频处理
audioStream!!.startAudioRecord()
// 启动音频处理(若已启动则跳过)
if (!audioStream!!.isWriting()) {
audioStream!!.startAudioRecord(
when (audioSourceType) {
AudioSourceType.MICROPHONE -> SimpleAudioReceiver.AudioSourceType.MICROPHONE
AudioSourceType.EXTERNAL -> SimpleAudioReceiver.AudioSourceType.EXTERNAL
},
null
)
} else {
Log.d(tag, "音频已启动,跳过重复启动")
}
// 执行同步识别
val result = recognizer?.recognizeOnceAsync()?.get()
@ -264,13 +278,13 @@ class AzureAsrHelper(private val context: Context) {
/**
* 开始连续语音识别
*
* @param callback 连续识别结果回调
* @return 是否成功开始识别
* @param audioSourceType 音频源类型
* @param audioDataCallback 音频数据回调接口
* @return 是否成功启动
*/
fun startContinuousRecognition(
audioSourceType: AudioSourceType = AudioSourceType.MICROPHONE
audioSourceType: AudioSourceType = AudioSourceType.MICROPHONE,
audioDataCallback: SimpleAudioReceiver.AudioDataCallback? = null
): Boolean {
Log.d(tag, "startContinuousRecognition:$isContinuousRecognitionActive ")
@ -286,26 +300,29 @@ class AzureAsrHelper(private val context: Context) {
}
this.audioSourceType = audioSourceType
try {
Log.d(tag, "startContinuousRecognition: ")
// 启动音频处理
//startAudioProcessing()
// 开始连续识别
// 启动音频处理(若已启动则跳过)
if (!audioStream!!.isWriting()) {
audioStream!!.startAudioRecord(
when (audioSourceType) {
AudioSourceType.MICROPHONE -> SimpleAudioReceiver.AudioSourceType.MICROPHONE
AudioSourceType.EXTERNAL -> SimpleAudioReceiver.AudioSourceType.EXTERNAL
},
audioDataCallback
)
} else {
Log.d(tag, "音频已启动,跳过重复启动")
}
// 启动音频处理
audioStream!!.startAudioRecord()
recognizer?.startContinuousRecognitionAsync()
isContinuousRecognitionActive = true
return true
} catch (e: Exception) {
audioStream!!.stopMicrophoneCapture()
isContinuousRecognitionActive = false
return false
}
}
@ -468,7 +485,9 @@ class AzureAsrHelper(private val context: Context) {
// 停止音频处理
stopAudioProcessing()
// 停止录音
recordfile!!.closeFile(true)
if (audioStream!!.recordfile != null) {
audioStream!!.recordfile!!.closeFile(true)
}
// 释放recognizer
recognizer?.close()
recognizer = null
@ -523,53 +542,33 @@ class AzureAsrHelper(private val context: Context) {
}
/**
* 禁用蓝牙音频功能,切换回正常音频模式
*/
fun disableBluetoothAudio() {
try {
// 关闭蓝牙SCO(Synchronous Connection Oriented)音频路由
audioManager?.isBluetoothScoOn = false
// 停止蓝牙SCO连接
audioManager?.stopBluetoothSco()
// 切换回正常音频模式(非通话模式)
audioManager?.mode = AudioManager.MODE_IN_COMMUNICATION
} catch (e: Exception) {
Log.e("AudioConfig", "Failed to disable Bluetooth: ${e.message}")
}
}
/**
* 恢复原始音频设备状态(通常是重新启用蓝牙)
*/
fun restoreOriginalAudioState() {
try {
// 恢复应用启动时的原始音频模式
audioManager?.mode = AudioManager.MODE_NORMAL
// 重新启用蓝牙SCO
audioManager?.isBluetoothScoOn = true
// 启动蓝牙SCO连接(通常在需要蓝牙通话时使用)
audioManager?.startBluetoothSco()
} catch (e: Exception) {
Log.e("AudioConfig", "Failed to restore audio state: ${e.message}")
}
}
//录音音频文件
/**
* 开启录音
*/
fun enableRecord(filePath: String) {
fun enableRecord(filePath: String, audioDataCallback: SimpleAudioReceiver.AudioDataCallback? = null) {
Log.i(tag, "开启录音:")
if (audioStream == null) {
// 创建外部音频拉流对象
audioStream = SimpleAudioReceiver(context)
audioStream!!.initAudioRecord()
}
if (audioStream!!.recordfile == null) {
audioStream!!.recordfile = RecordFile()
}
recordfile!!.closeFile(true)
// 启动音频处理
audioStream!!.startAudioRecord( when(audioSourceType) {
AudioSourceType.MICROPHONE -> SimpleAudioReceiver.AudioSourceType.MICROPHONE
AudioSourceType.EXTERNAL -> SimpleAudioReceiver.AudioSourceType.EXTERNAL
},audioDataCallback)
audioStream!!.recordfile!!.closeFile(true)
recordfile!!.creatingFiles(filePath)
audioStream!!.recordfile!!.creatingFiles(filePath)
}
@ -578,8 +577,8 @@ class AzureAsrHelper(private val context: Context) {
*/
fun moveFile(sourcePath: String, destPath: String): Boolean {
Log.i(tag, "移动文件到新路径:")
recordfile!!.moveFile(sourcePath, destPath)
if (audioStream!!.recordfile == null)return false
audioStream!!.recordfile!!.moveFile(sourcePath, destPath)
return true
}
@ -590,8 +589,8 @@ class AzureAsrHelper(private val context: Context) {
fun renameFile(filePath: String, newName: String): Boolean {
Log.i(tag, "重命名指定路径的音频文件:")
recordfile!!.renameFile(filePath, newName)
if (audioStream!!.recordfile == null)return false
audioStream!!.recordfile!!.renameFile(filePath, newName)
return true
}
@ -600,307 +599,37 @@ class AzureAsrHelper(private val context: Context) {
*/
fun pauseRecord() {
Log.i(tag, "停止连续录音:")
if (audioStream!!.recordfile == null)return
audioStream!!.stopMicrophoneCapture()
recordfile!!.isPause = true
}
/**
* 关闭录音
* 继续录音
*/
fun stopRecord(isSave: Boolean) {
Log.i(tag, "关闭录音:")
recordfile!!.isPause = false
recordfile!!.closeFile(isSave)
fun resumeRecord() {
Log.i(tag, "继续录音:")
if (audioStream!!.recordfile == null)return
audioStream!!.resumeRecord()
}
/**
* 麦克风流 - 拉流模式
* 实现PullAudioInputStreamCallback,为Azure SDK提供音频数据
* 关闭录音
*/
inner class AudioStream {
private val bufferSize = 4096 // 可根据需要调整
var audioRecord: AudioRecord? = null
var pushAudioStream: PushAudioInputStream? = null
// 新增:用于异步写入的队列和线程
private val writeQueue = LinkedBlockingQueue<ByteArray>()
private val isRunning = AtomicBoolean(false)// 控制线程是否继续存在
private val isWriting = AtomicBoolean(false) // 控制是否应该写入数据
private var writeThread: Thread? = null
// 音频配置
private val channelConfig = AudioFormat.CHANNEL_IN_MONO
private val audioFormat = AudioFormat.ENCODING_PCM_16BIT
/**
* 获取设备支持的最佳音频格式
* 优先选择16000Hz,若不支持则降级到8000Hz
*/
private fun getOptimalAudioFormat(): AudioStreamFormat {
// 支持的采样率列表(按优先级排序)
val supportedSampleRates = intArrayOf(16000, 8000, 11025, 22050, 44100)
// 查找设备支持的最佳采样率
val sampleRate = supportedSampleRates.firstOrNull { rate ->
val bufferSize = AudioRecord.getMinBufferSize(
rate,
AudioFormat.CHANNEL_IN_MONO,
AudioFormat.ENCODING_PCM_16BIT
)
bufferSize > 0 // 返回正值表示支持
} ?: 16000 // 默认回退值
Log.i(tag, "使用采样率: ${sampleRate}Hz")
// 创建对应的音频格式
return AudioStreamFormat.getWaveFormatPCM(sampleRate.toLong(), 16, 1)
}
/**
* 初始化
*/
fun initAudioRecord() {
val format = getOptimalAudioFormat()
pushAudioStream = AudioInputStream.createPushStream(format)
isRunning.set(true)
startWriteThread() // 再启动数据读取线程
}
/**
* 开启音频写入线程
*/
private fun startWriteThread() {
writeThread = Thread {
try {
while (isRunning.get()) {
// 等待录音信号
if (!isWriting.get()) {
Thread.sleep(10) // 短暂休眠避免空转
continue
}
var data: ByteArray? = null
var bytesToWrite = 0
// 情况1:正在录制中 -> 直接从AudioRecord读取
if (audioRecord?.recordingState == AudioRecord.RECORDSTATE_RECORDING) {
data = ByteArray(bufferSize)
val bytesRead = audioRecord?.read(data, 0, bufferSize) ?: -1
when {
bytesRead < 0 -> {
Log.e("tag", "读取音频失败,错误码: $bytesRead")
continue
}
bytesRead == 0 -> continue // 无数据可读
else -> bytesToWrite = bytesRead // 有效数据
}
}
// 情况2:不在录制但队列有数据 -> 从队列获取
else if (writeQueue.isNotEmpty()) {
data = writeQueue.poll()
Log.d("tag", "写入数据: ${data?.size}")
bytesToWrite = data?.size ?: 0
}
// 确保有有效数据再写入
if (data != null && bytesToWrite > 0) {
// 处理实际读取长度 < bufferSize 的情况
val finalData =
if (bytesToWrite < data.size) data.copyOf(bytesToWrite) else data
try {
//continuousCallback?.onAudio(finalData)
pushAudioStream?.write(finalData)
recordfile?.saveAudioDataToWav(finalData)
} catch (e: Exception) {
Log.e("tag", "写入失败: ${e.message}")
}
} else {
Thread.yield() // 避免空转消耗CPU
}
}
} catch (e: Exception) {
Log.e("tag", "写入线程异常: ${e.stackTraceToString()}")
} finally {
Log.d("tag", "音频写入线程退出")
writeQueue.clear()
}
}.apply {
name = "AudioWriteThread"
start()
}
}
/**
* 外部音频输入
*/
fun saveAudioDataTo(buffer: ByteArray) {
if (audioSourceType == AudioSourceType.MICROPHONE||!isContinuousRecognitionActive) return
// 放入队列,由写线程写入
writeQueue.offer(buffer.copyOf())
}
/**
* 开始音频输入
*/
fun startAudioRecord() {
isWriting.set(true)
when (audioSourceType) {
AudioSourceType.MICROPHONE -> runMicrophoneCapture()
AudioSourceType.EXTERNAL -> runExternalCapture()
}
}
private fun runMicrophoneCapture() {
try {
// 首先设置通话音频模式
setupCommunicationAudioMode()
val supportedSampleRates = intArrayOf(16000, 8000, 11025, 22050, 44100)
val sampleRate = supportedSampleRates.firstOrNull { rate ->
val bufferSize = AudioRecord.getMinBufferSize(rate, channelConfig, audioFormat)
bufferSize > 0
} ?: 16000
val minBufferSize = AudioRecord.getMinBufferSize(sampleRate, channelConfig, audioFormat)
// 使用VOICE_COMMUNICATION音频源(专为VoIP优化)
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
val format = AudioFormat.Builder()
.setSampleRate(sampleRate)
.setEncoding(audioFormat)
.setChannelMask(AudioFormat.CHANNEL_IN_MONO)
.build()
audioRecord = AudioRecord.Builder()
.setAudioSource(MediaRecorder.AudioSource.VOICE_COMMUNICATION)
.setAudioFormat(format)
.setBufferSizeInBytes(minBufferSize * 2)
.build()
} else {
audioRecord = AudioRecord(
MediaRecorder.AudioSource.VOICE_COMMUNICATION,
sampleRate,
channelConfig,
audioFormat,
minBufferSize * 2
)
}
if (audioRecord?.state != AudioRecord.STATE_INITIALIZED) {
throw IllegalStateException("AudioRecord初始化失败")
}
audioRecord?.startRecording()
Log.d("TAG", "通话模式录音开始,采样率: $sampleRate Hz")
} catch (e: Exception) {
Log.e("TAG", "音频捕获异常: ${e.message}")
// 出错时恢复音频模式
restoreCommunicationAudioMode()
}
}
private fun runExternalCapture() {
Log.d("TAG", "外部音频捕获启动")
try { // TODO: 实现外部音频源捕获逻辑
// 停止录音
if (audioRecord?.recordingState == AudioRecord.STATE_INITIALIZED) {
Log.d("TAG", "外部音频捕获启动 释放audioRecord")
audioRecord?.stop()
// 释放录音实例
audioRecord?.release()
audioRecord = null
}
} catch (e: Exception) {
Log.e("TAG", "外部音频捕获异常: ${e.message}")
} finally {
}
}
/**
* 停止麦克风捕获并释放所有相关资源
*/
fun stopMicrophoneCapture() {
try {
if (!isWriting.get()) return
isWriting.set(false)
// 停止录音
if (audioRecord?.recordingState == AudioRecord.RECORDSTATE_RECORDING) {
audioRecord?.stop()
}
// 停止录音并释放AudioRecord资源
// 释放录音实例
audioRecord?.release()
audioRecord = null
} catch (e: Exception) {
Log.e("AudioConfig", "Error releasing resources: ${e.message}")
} finally {
writeQueue.clear()
// 确保恢复原始音频状态
// restoreOriginalAudioState()
}
fun stopRecord(isSave: Boolean) {
Log.i(tag, "关闭录音:")
if (audioStream == null) {
return
}
if (audioStream!!.recordfile == null)return
audioStream!!.stopMicrophoneCapture()
audioStream!!.recordfile!!.closeFile(isSave)
}
/**
* 释放音频资源
*/
fun releaseAudioResources() {
try {
if (!isWriting.get()) return
isWriting.set(false)
isRunning.set(false)
// 停止录音
if (audioRecord?.recordingState == AudioRecord.RECORDSTATE_RECORDING) {
audioRecord?.stop()
}
// 中断并等待捕获线程结束
writeThread?.interrupt()
writeThread?.join(300) // 最多等待300ms
// 恢复音频模式
this@AzureAsrHelper.audioManager?.mode = this@AzureAsrHelper.originalAudioMode
// 释放录音实例
audioRecord?.release()
audioRecord = null
} catch (e: Exception) {
Log.e(tag, "释放音频资源失败: ${e.message}")
e.printStackTrace()
} finally {
writeQueue.clear()
writeThread = null
// 确保恢复原始音频状态
// restoreOriginalAudioState()
}
}
}
/**
* 一次性识别回调接口
@ -952,13 +681,7 @@ class AzureAsrHelper(private val context: Context) {
*/
fun onSessionStopped()
/**
* 返回音频
*
* @param data 识别的音频
*/
fun onAudio(data: ByteArray)
/**
* 识别取消时调用
*
@ -975,55 +698,7 @@ class AzureAsrHelper(private val context: Context) {
fun onError(error: String)
}
/**
* 配置通话音频模式以优化回声消除
*/
private fun setupCommunicationAudioMode() {
try {
audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
// 保存原始音频模式
originalAudioMode = audioManager?.mode ?: AudioManager.MODE_NORMAL
// 设置通话模式 - 这是关键!
audioManager?.mode = AudioManager.MODE_IN_COMMUNICATION
// 启用扬声器(如果需要外放)
audioManager?.isSpeakerphoneOn = true
// 请求音频焦点
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
val focusRequest = AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE)
.setAudioAttributes(
AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION)
.setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
.build()
)
.build()
audioManager?.requestAudioFocus(focusRequest)
}
Log.d("AudioMode", "通话音频模式已设置: MODE_IN_COMMUNICATION")
} catch (e: Exception) {
Log.e("AudioMode", "设置通话音频模式失败: ${e.message}")
}
}
/**
* 恢复原始音频模式
*/
private fun restoreCommunicationAudioMode() {
try {
audioManager?.mode = originalAudioMode
audioManager?.isSpeakerphoneOn = false
// 释放音频焦点
Log.d("AudioMode", "音频模式已恢复")
} catch (e: Exception) {
Log.e("AudioMode", "恢复音频模式失败: ${e.message}")
}
}
}

1311
local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrToAsr.kt

File diff suppressed because it is too large

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

@ -5,6 +5,8 @@ import android.os.Handler
import android.os.Looper
import androidx.annotation.NonNull
import com.yunqiinnovation.azure_speech.utils.FileLogger
import com.yunqiinnovation.azure_speech.tools.SimpleAudioReceiver
import com.yunqiinnovation.azure_speech.tools.SimpleAudioPlayer
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
@ -16,6 +18,7 @@ import com.deep_voice.speech.tts.TtsEventListener
import com.deep_voice.speech.tts.TtsEventType
import com.yunqiinnovation.ble_service.BleService
import com.deep_voice.speech.tts.AudioOutputDevice
import kotlinx.coroutines.*
/** AzureSpeechPlugin */
class AzureSpeechPlugin : BleService.Callback, FlutterPlugin {
@ -35,6 +38,18 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin {
private var ttsEventSink: EventChannel.EventSink? = null
private lateinit var azureTtsHelper: AzureTtsHelper
// AST相关
private lateinit var astChannel: MethodChannel
private lateinit var astEventChannel: EventChannel
private var astEventSink: EventChannel.EventSink? = null
private lateinit var azureAstHelper: IntegratedSpeechTranslationService
// 音频数据相关
private lateinit var audioDataChannel: MethodChannel
private lateinit var audioDataEventChannel: EventChannel
private var audioDataEventSink: EventChannel.EventSink? = null
// 是否已添加TTS事件监听器
private var isTtsListenerAdded = false
@ -74,6 +89,36 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin {
}
}
// AST 事件发送方法
private fun sendAstEvent(event: Map<String, Any>) {
if (astEventSink == null) {
FileLogger.w(tag, "无法发送AST事件:事件通道未准备好")
return
}
mainHandler.post {
try {
astEventSink?.success(event)
} catch (e: Exception) {
FileLogger.e(tag, "发送AST事件失败: ${e.message}")
}
}
}
// 音频数据 事件发送方法
private fun sendAudioDataEvent(event: Map<String, Any>) {
if (audioDataEventSink == null) {
FileLogger.w(tag, "无法发送音频数据事件:事件通道未准备好")
return
}
mainHandler.post {
try {
audioDataEventSink?.success(event)
} catch (e: Exception) {
FileLogger.e(tag, "发送音频数据事件失败: ${e.message}")
}
}
}
override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
context = flutterPluginBinding.applicationContext
@ -84,7 +129,9 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin {
// 初始化TTS通道
ttsChannel = MethodChannel(flutterPluginBinding.binaryMessenger, "azure_speech/tts")
ttsChannel.setMethodCallHandler(TtsMethodHandler())
// 初始化AST通道
astChannel = MethodChannel(flutterPluginBinding.binaryMessenger, "azure_speech/ast")
astChannel.setMethodCallHandler(AsTMethodHandler())
// 初始化ASR事件通道
asrEventChannel =
EventChannel(flutterPluginBinding.binaryMessenger, "azure_speech/asr_events")
@ -111,11 +158,35 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin {
ttsEventSink = null
}
})
// 初始化AST事件通道
astEventChannel =
EventChannel(flutterPluginBinding.binaryMessenger, "azure_speech/ast_events")
astEventChannel.setStreamHandler(object : EventChannel.StreamHandler {
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
astEventSink = events
//setupTtsEventListener() // 在事件通道准备好时设置TTS事件监听器
}
override fun onCancel(arguments: Any?) {
astEventSink = null
}
})
// 初始化音频数据事件通道
audioDataEventChannel =
EventChannel(flutterPluginBinding.binaryMessenger, "azure_speech/audio_data_events")
audioDataEventChannel.setStreamHandler(object : EventChannel.StreamHandler {
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
audioDataEventSink = events
}
override fun onCancel(arguments: Any?) {
audioDataEventSink = null
}
})
// 初始化Azure语音服务
azureTtsHelper = AzureTtsHelper(context)
azureAsrHelper = AzureAsrHelper(context)
azureAstHelper = IntegratedSpeechTranslationService(context)
// 2. 初始化BleService并注册回调
if (BleService.initialize(context)) {
@ -171,14 +242,6 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin {
sendAsrEvent(mapOf("type" to "sessionStopped"))
}
override fun onAudio(data: ByteArray) {
sendAsrEvent(
mapOf(
"type" to "onAudio",
"data" to data
)
)
}
override fun onCanceled(reason: String, errorDetails: String) {
@ -334,12 +397,12 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin {
"disableBluetoothAudio" -> {
azureAsrHelper.disableBluetoothAudio();
azureAsrHelper.audioStream?.disableBluetoothAudio();
return
}
"restoreOriginalAudioState" -> {
azureAsrHelper.restoreOriginalAudioState();
azureAsrHelper.audioStream?.restoreOriginalAudioState();
return
}
@ -435,10 +498,30 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin {
"enableRecord" -> {
val filePath = call.argument<String>("filePath") ?: ""
// 是否接受音频数据
val acceptAudioData = call.argument<Boolean>("acceptAudioData") ?: false
try {
FileLogger.d(tag, "音频文件名称为: ${filePath}") //
azureAsrHelper.enableRecord(filePath)
FileLogger.d(tag, "音频文件名称为: ${filePath}")
// 启用录音,并根据 acceptAudioData 参数决定是否设置音频数据回调
azureAsrHelper.enableRecord(filePath, if(acceptAudioData) {
// 创建音频数据回调,将音频数据发送到 Flutter 层
object : SimpleAudioReceiver.AudioDataCallback {
override fun onAudio(audioData: ByteArray) {
// 构建音频数据事件映射
val audioEvent = mapOf(
"type" to "audioData",
"data" to audioData,
"timestamp" to System.currentTimeMillis(),
"size" to audioData.size
)
// 发送音频数据事件到 Flutter 层
sendAudioDataEvent(audioEvent)
}
}
} else {
null
})
result.success(true)
} catch (e: Exception) {
result.error("ENABLERECORD_ERROR", e.message, null)
@ -449,6 +532,10 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin {
azureAsrHelper.pauseRecord()
result.success(true)
}
"resumeRecord" -> {
azureAsrHelper.resumeRecord()
result.success(true)
}
"stopRecord" -> {
val isSave = call.argument<Boolean>("isSave") ?: false
@ -460,6 +547,17 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin {
}
}
"setAudioConfig" -> {
val sampleRate = call.argument<Int>("sampleRate") ?: 16000
val channels = call.argument<Int>("channels") ?: 1
try {
azureAsrHelper.audioStream?.recordfile?.setAudioConfig(sampleRate, channels)
result.success(true)
} catch (e: Exception) {
result.error("SET_AUDIO_CONFIG_ERROR", e.message, null)
}
}
else -> {
result.notImplemented()
}
@ -495,6 +593,8 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin {
val outputStream = customPlayer.getAudioOutputStream()
if (outputStream != null) {
azureTtsHelper.setCustomAudioOutputStream(outputStream)
} else {
FileLogger.w(tag, "无法获取自定义音频输出流")
}
}
@ -573,6 +673,326 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin {
}
}
// ASt方法处理器
inner class AsTMethodHandler : MethodCallHandler {
private suspend fun streamAudioFile(file: java.io.File) {
try {
val inputStream = file.inputStream()
val buffer = ByteArray(1024) // 每次读取1KB
// WAV文件参数(假设16kHz, 16bit, 单声道)
val sampleRate = 16000 // 采样率
val bytesPerSample = 2 // 16bit = 2字节
val channels = 1 // 单声道
// 计算每秒需要的字节数
val bytesPerSecond = sampleRate * bytesPerSample * channels
// 计算每个缓冲区对应的播放时间(毫秒)
val bufferDurationMs = (buffer.size * 1000L) / bytesPerSecond
FileLogger.d("TAG", "开始流式读取音频文件,缓冲区大小: ${buffer.size}, 播放间隔: ${bufferDurationMs}ms")
var bytesRead: Int
while (inputStream.read(buffer).also { bytesRead = it } != -1) {
// 只发送实际读取的字节数
val audioChunk = if (bytesRead < buffer.size) {
buffer.copyOf(bytesRead)
} else {
buffer
}
// 推送音频数据块
withContext(Dispatchers.Main) {
azureAstHelper.pushAudioData(audioChunk)
FileLogger.d("TAG", "推送音频数据块: ${audioChunk.size} 字节")
}
}
inputStream.close()
FileLogger.d("TAG", "音频文件流式读取完成")
} catch (e: Exception) {
FileLogger.e("TAG", "流式读取音频文件失败: ${e.message}")
}
}
override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) {
when (call.method) {
"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)
}
}
"startContinuousTranslation" -> {
try {
FileLogger.d(tag, "开启翻译")
azureAstHelper.startContinuousTranslation()
result.success(true)
} catch (e: Exception) {
result.error("STOP_CONTINUOUS_TRANSLATION_ERROR", e.message, null)
}
}
"stopContinuousTranslation" -> {
try {
FileLogger.d(tag, "停止翻译")
azureAstHelper.stopContinuousTranslation()
result.success(true)
} catch (e: Exception) {
result.error("STOP_CONTINUOUS_TRANSLATION_ERROR", e.message, null)
}
}
"dispose" -> {
try {
FileLogger.d(tag, "释放AST资源")
azureAstHelper.dispose()
result.success(true)
} catch (e: Exception) {
result.error("STOP_CONTINUOUS_TRANSLATION_ERROR", e.message, null)
}
}
"recognizeCallback" -> {
FileLogger.d(tag, "recognizeCallback:") //
result.success(true)
}
"path" -> {
val filePath = call.argument<String>("filePath") ?: ""
try {
// 读取这个wav音频文件,取里面的音频数据进行播放
val file = java.io.File(filePath)
if (file.exists()) {
// 启动协程来按播放速度读取音频文件
CoroutineScope(Dispatchers.IO).launch {
streamAudioFile(file)
}
result.success(true)
} else {
result.error("FILE_NOT_FOUND", "音频文件不存在: $filePath", null)
}
} catch (e: Exception) {
result.error("READ_FILE_ERROR", "读取音频文件失败: ${e.message}", null)
}
}
"initialize" -> {
val subscriptionKey = call.argument<String>("subscriptionKey") ?: ""
val region = call.argument<String>("region") ?: ""
val supportedLanguages =
call.argument<List<String>>("supportedLanguages") ?: listOf("zh-CN")
val useExternalAudio = call.argument<Boolean>("useExternalAudio") ?: false
// 获取翻译服务配置参数(需要从Flutter端传递)
val translationAccessKey = call.argument<String>("translationAccessKey") ?: ""
val translationSecretKey = call.argument<String>("translationSecretKey") ?: ""
val translationRegion =
call.argument<String>("translationRegion") ?: "cn-north-1"
FileLogger.d(tag, "初始化AST服务")
// 创建Azure配置
val azureConfig = AzureConfiguration(
subscriptionKey = subscriptionKey,
region = region
)
// 创建翻译配置
val translationConfig =
TranslationConfiguration(
accessKey = translationAccessKey,
secretKey = translationSecretKey,
region = translationRegion
)
// 创建服务配置
val serviceConfig = IntegratedSpeechTranslationService.ServiceConfiguration(
sourceLanguage = if (supportedLanguages.isNotEmpty()) supportedLanguages[0] else "zh-CN",
targetLanguage = if (supportedLanguages.size > 1) supportedLanguages[1] else "en-US"
)
// 创建事件回调
val callback = object : IntegratedSpeechTranslationService.ServiceEventCallback {
override fun onServiceInitialized() {
// sendAstEvent(
// mapOf(
// "type" to "serviceInitialized"
// )
// )
}
override fun onRecognizing(text: String, language: String, confidence: Float) {
// sendAstEvent(
// mapOf(
// "type" to "recognizing",
// "text" to text,
// "language" to language,
// "confidence" to confidence
// )
// )
}
override fun onRecognized(text: String, language: String, confidence: Float) {
FileLogger.d(tag, "识别到文本: $text, 语言: $language, 置信度: $confidence")
sendAsrEvent(
mapOf(
"type" to "result1",
"text" to text,
"detectedLanguage" to language
)
)
// sendAstEvent(
// mapOf(
// "type" to "recognized",
// "text" to text,
// "language" to language,
// "confidence" to confidence
// )
// )
}
override fun onTranslated(
originalText: String,
translatedText: String,
targetLanguage: String
) {
// sendAstEvent(
// mapOf(
// "type" to "translated",
// "originalText" to originalText,
// "translatedText" to translatedText,
// "targetLanguage" to targetLanguage
// )
// )
}
override fun onTranslationStarted(text: String) {
// sendAstEvent(
// mapOf(
// "type" to "translationStarted",
// "text" to text
// )
// )
}
override fun onTranslationFailed(text: String, error: String) {
// sendAstEvent(
// mapOf(
// "type" to "translationFailed",
// "text" to text,
// "error" to error
// )
// )
}
override fun onSynthesisStarted(text: String) {
// sendAstEvent(
// mapOf(
// "type" to "synthesisStarted",
// "text" to text
// )
// )
}
override fun onSynthesisCompleted(text: String) {
FileLogger.d(tag, "语音合成完成,文本=${text}")
// sendAstEvent(
// mapOf(
// "type" to "synthesisCompleted",
// "text" to text
// )
// )
}
override fun onSynthesisAudioGenerated(text: String, audioData: ByteArray) {
FileLogger.d(tag, "语音合成音频生成,文本=${text},音频数据大小=${audioData.size}")
BleService.writeExternalAudioData(audioData)
}
override fun onSynthesisFailed(text: String, error: String) {
// sendAstEvent(
// mapOf(
// "type" to "synthesisFailed",
// "text" to text,
// "error" to error
// )
// )
}
override fun onSynthesisProgress(text: String, progress: Float) {
// sendAstEvent(
// mapOf(
// "type" to "synthesisProgress",
// "text" to text,
// "progress" to progress
// )
// )
}
override fun onRecognitionStarted() {
// sendAstEvent(
// mapOf(
// "type" to "recognitionStarted"
// )
// )
}
override fun onRecognitionStopped() {
// sendAstEvent(
// mapOf(
// "type" to "recognitionStopped"
// )
// )
}
override fun onStateChanged(component: String, isActive: Boolean) {
// sendAstEvent(
// mapOf(
// "type" to "stateChanged",
// "component" to component,
// "isActive" to isActive
// )
// )
}
override fun onError(component: String, error: String) {
// sendAstEvent(
// mapOf(
// "type" to "error",
// "component" to component,
// "error" to error
// )
// )
}
}
// 使用协程调用异步初始化方法
GlobalScope.launch(Dispatchers.Main) {
azureAstHelper.initialize(
azureConfig = azureConfig,
translationConfig = translationConfig,
serviceConfig = serviceConfig,
callback = callback
)
}
result.success(true)
}
}
}
}
override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {
asrChannel.setMethodCallHandler(null)
ttsChannel.setMethodCallHandler(null)
@ -613,6 +1033,12 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin {
// 可选:处理音频数据
}
override fun onAudioDataReceived1(data: ByteArray) {
azureAstHelper.pushAudioData(data)
// 可选:处理音频数据
}
/**
* 处理唤醒信号
* 在收到唤醒信号时启动语音识别
@ -624,4 +1050,7 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin {
override fun onDeviceInfoReceived(infoType: Int, infoData: Map<String, Any>) {
// 不处理设备信息
}
}
}

2
local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureTtsHelper.kt

@ -14,7 +14,7 @@ import com.deep_voice.speech.tts.TtsEventType
import kotlinx.coroutines.*
import java.io.ByteArrayInputStream
import java.io.InputStream
import com.yunqiinnovation.azure_speech.tools.SimpleAudioPlayer
/**
* Azure TTS Helper
*

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

@ -9,11 +9,15 @@ import java.io.FileInputStream
import java.util.concurrent.LinkedBlockingQueue
import java.util.concurrent.atomic.AtomicBoolean
object RecordFile {
/**
* 音频录制文件管理类
* 支持单声道和双声道录制,提供WAV文件格式输出
*/
class RecordFile {
private var currentAudioFile: File? = null
private var fos: FileOutputStream? = null
// 新增:用于异步写入的队列和线程
// 用于异步写入的队列和线程
private val writeQueue = LinkedBlockingQueue<ByteArray>()
private val isWriting = AtomicBoolean(false)
private var writeThread: Thread? = null
@ -22,15 +26,73 @@ object RecordFile {
private val dataBuffer = mutableListOf<ByteArray>()
private var totalBytesWritten = 0
// 音频配置
private val sampleRate = 16000
// 音频配置 - 可配置的采样率和声道数
private var sampleRate = 16000
private var channels = 1 // 1=单声道, 2=立体声
// 新增:用于存储最后成功保存的文件
// 用于存储最后成功保存的文件
private var lastSavedFile: File? = null
private var lastFileTimestamp: String = ""
var isPause = false
var fileName = ""
// 双声道分离相关变量
private var leftChannelFile: File? = null
private var rightChannelFile: File? = null
private var leftChannelFos: FileOutputStream? = null
private var rightChannelFos: FileOutputStream? = null
private val leftChannelQueue = LinkedBlockingQueue<ByteArray>()
private val rightChannelQueue = LinkedBlockingQueue<ByteArray>()
private var leftChannelWriteThread: Thread? = null
private var rightChannelWriteThread: Thread? = null
private var leftChannelBytesWritten = 0
private var rightChannelBytesWritten = 0
/**
* 设置音频配置
* @param sampleRate 采样率 (8000, 16000, 24000, 32000, 44100, 48000)
* @param channels 声道数 (1=单声道, 2=立体声)
*/
fun setAudioConfig(sampleRate: Int, channels: Int) {
this.sampleRate = sampleRate
this.channels = channels
Log.d("RecordFile", "音频配置已更新: 采样率=${sampleRate}Hz, 声道数=${channels}")
}
/**
* 获取左声道文件路径
* @return 左声道文件的绝对路径,如果不存在则返回null
*/
internal fun getLeftChannelFilePath(): String? {
return leftChannelFile?.absolutePath
}
/**
* 获取右声道文件路径
* @return 右声道文件的绝对路径,如果不存在则返回null
*/
internal fun getRightChannelFilePath(): String? {
return rightChannelFile?.absolutePath
}
/**
* 获取当前音频配置信息
* @return Map包含采样率和声道数信息
*/
internal fun getAudioConfig(): Map<String, Int> {
return mapOf(
"sampleRate" to sampleRate,
"channels" to channels
)
}
/**
* 检查是否为双声道模式
* @return true表示双声道,false表示单声道
*/
internal fun isStereoMode(): Boolean {
return channels == 2
}
// 修改:添加 filePath 参数
internal fun creatingFiles(filePath: String) {
@ -54,6 +116,105 @@ object RecordFile {
fos = FileOutputStream(currentAudioFile, true)
startWriteThread()
// 如果是双声道,创建左右声道文件
if (channels == 2) {
createStereoChannelFiles(filePath)
}
}
/**
* 创建左右声道文件
*/
private fun createStereoChannelFiles(originalFilePath: String) {
val originalFile = File(originalFilePath)
val parentDir = originalFile.parentFile
val nameWithoutExt = originalFile.nameWithoutExtension
// 创建左声道文件
leftChannelFile = File(parentDir, "${nameWithoutExt}_left.wav")
leftChannelFile?.createNewFile()
leftChannelFos = FileOutputStream(leftChannelFile).apply {
write(generateMonoWavHeader(0))
close()
}
leftChannelFos = FileOutputStream(leftChannelFile, true)
// 创建右声道文件
rightChannelFile = File(parentDir, "${nameWithoutExt}_right.wav")
rightChannelFile?.createNewFile()
rightChannelFos = FileOutputStream(rightChannelFile).apply {
write(generateMonoWavHeader(0))
close()
}
rightChannelFos = FileOutputStream(rightChannelFile, true)
// 启动左右声道写入线程
startStereoWriteThreads()
Log.d("tag", "双声道文件创建完成: ${leftChannelFile?.name}, ${rightChannelFile?.name}")
}
/**
* 生成单声道WAV文件头
*/
private fun generateMonoWavHeader(dataLength: Int): ByteArray {
val totalLength = 36 + dataLength
val bytesPerSample = 2
val monoChannels = 1
val byteRate = sampleRate * monoChannels * bytesPerSample
val blockAlign = monoChannels * bytesPerSample
return byteArrayOf(
'R'.code.toByte(), 'I'.code.toByte(), 'F'.code.toByte(), 'F'.code.toByte(),
(totalLength and 0xFF).toByte(), ((totalLength shr 8) and 0xFF).toByte(),
((totalLength shr 16) and 0xFF).toByte(), ((totalLength shr 24) and 0xFF).toByte(),
'W'.code.toByte(), 'A'.code.toByte(), 'V'.code.toByte(), 'E'.code.toByte(),
'f'.code.toByte(), 'm'.code.toByte(), 't'.code.toByte(), ' '.code.toByte(),
16, 0, 0, 0,
1, 0,
(monoChannels and 0xFF).toByte(), ((monoChannels shr 8) and 0xFF).toByte(),
(sampleRate and 0xFF).toByte(), ((sampleRate shr 8) and 0xFF).toByte(),
((sampleRate shr 16) and 0xFF).toByte(), ((sampleRate shr 24) and 0xFF).toByte(),
(byteRate and 0xFF).toByte(), ((byteRate shr 8) and 0xFF).toByte(),
((byteRate shr 16) and 0xFF).toByte(), ((byteRate shr 24) and 0xFF).toByte(),
(blockAlign and 0xFF).toByte(), ((blockAlign shr 8) and 0xFF).toByte(),
16, 0,
'd'.code.toByte(), 'a'.code.toByte(), 't'.code.toByte(), 'a'.code.toByte(),
(dataLength and 0xFF).toByte(), ((dataLength shr 8) and 0xFF).toByte(),
((dataLength shr 16) and 0xFF).toByte(), ((dataLength shr 24) and 0xFF).toByte()
)
}
/**
* 启动左右声道写入线程
*/
private fun startStereoWriteThreads() {
// 左声道写入线程
leftChannelWriteThread = Thread {
try {
while (isWriting.get() || leftChannelQueue.isNotEmpty()) {
val data = leftChannelQueue.poll() ?: continue
leftChannelFos?.write(data)
}
} catch (e: Exception) {
Log.e("tag", "左声道异步写入失败: ${e.message}")
}
}
leftChannelWriteThread?.start()
// 右声道写入线程
rightChannelWriteThread = Thread {
try {
while (isWriting.get() || rightChannelQueue.isNotEmpty()) {
val data = rightChannelQueue.poll() ?: continue
rightChannelFos?.write(data)
}
} catch (e: Exception) {
Log.e("tag", "右声道异步写入失败: ${e.message}")
}
}
rightChannelWriteThread?.start()
}
@ -76,19 +237,65 @@ object RecordFile {
/**
* 保存音频数据到 WAV 文件(异步)
* 如果是双声道,会自动拆分成左右声道分别保存
*/
internal fun saveAudioDataToWav(buffer: ByteArray) {
if (fos == null || currentAudioFile == null) return
// 放入队列,由写线程写入
writeQueue.offer(buffer.copyOf())
totalBytesWritten += buffer.size
if (channels == 2) {
// 双声道:拆分左右声道
splitStereoToMono(buffer)
} else {
// 单声道:直接保存
writeQueue.offer(buffer.copyOf())
totalBytesWritten += buffer.size
}
}
/**
* 将双声道音频数据拆分成左右声道
* @param stereoBuffer 双声道音频数据(16位PCM,交错格式:L1R1L2R2...)
*/
private fun splitStereoToMono(stereoBuffer: ByteArray) {
if (stereoBuffer.size % 4 != 0) {
Log.w("tag", "双声道音频数据长度不正确: ${stereoBuffer.size}")
return
}
val sampleCount = stereoBuffer.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] = stereoBuffer[stereoIndex]
leftBuffer[monoIndex + 1] = stereoBuffer[stereoIndex + 1]
// 右声道
rightBuffer[monoIndex] = stereoBuffer[stereoIndex + 2]
rightBuffer[monoIndex + 1] = stereoBuffer[stereoIndex + 3]
}
// 将左右声道数据分别放入队列
leftChannelQueue.offer(leftBuffer)
rightChannelQueue.offer(rightBuffer)
leftChannelBytesWritten += leftBuffer.size
rightChannelBytesWritten += rightBuffer.size
Log.d("tag", "双声道拆分完成: 左声道${leftBuffer.size}字节, 右声道${rightBuffer.size}字节")
}
// 更新文件头生成(修正RIFF长度计算)
// 更新文件头生成(支持可配置的采样率和声道数)
private fun generateWavHeader(dataLength: Int): ByteArray {
val totalLength = 36 + dataLength // RIFF块总长度 = 头部36字节 + 音频数据
val byteRate = sampleRate * 2 * 1 // 采样率 * 字节/样本 * 通道数
val bytesPerSample = 2 // 16位PCM
val byteRate = sampleRate * channels * bytesPerSample // 采样率 * 声道数 * 字节/样本
val blockAlign = channels * bytesPerSample // 声道数 * 字节/样本
return byteArrayOf(
'R'.code.toByte(), 'I'.code.toByte(), 'F'.code.toByte(), 'F'.code.toByte(),
@ -98,10 +305,12 @@ object RecordFile {
'f'.code.toByte(), 'm'.code.toByte(), 't'.code.toByte(), ' '.code.toByte(),
16, 0, 0, 0, // PCM头长度
1, 0, // PCM格式
1, 0, // 单声道
(sampleRate and 0xFF).toByte(), ((sampleRate shr 8) and 0xFF).toByte(), 0, 0, // 采样率
(byteRate and 0xFF).toByte(), ((byteRate shr 8) and 0xFF).toByte(), 0, 0, // 字节率
2, 0, // 块对齐 (通道数 * 样本位数/8)
(channels and 0xFF).toByte(), ((channels shr 8) and 0xFF).toByte(), // 声道数
(sampleRate and 0xFF).toByte(), ((sampleRate shr 8) and 0xFF).toByte(),
((sampleRate shr 16) and 0xFF).toByte(), ((sampleRate shr 24) and 0xFF).toByte(), // 采样率
(byteRate and 0xFF).toByte(), ((byteRate shr 8) and 0xFF).toByte(),
((byteRate shr 16) and 0xFF).toByte(), ((byteRate shr 24) and 0xFF).toByte(), // 字节率
(blockAlign and 0xFF).toByte(), ((blockAlign shr 8) and 0xFF).toByte(), // 块对齐
16, 0, // 样本位数
'd'.code.toByte(), 'a'.code.toByte(), 't'.code.toByte(), 'a'.code.toByte(),
(dataLength and 0xFF).toByte(), ((dataLength shr 8) and 0xFF).toByte(),
@ -225,31 +434,92 @@ object RecordFile {
writeThread?.join(500)
fos?.close()
var mainFileSuccess = false
var leftChannelSuccess = false
var rightChannelSuccess = false
// 处理主文件
currentAudioFile?.let { file ->
if (file.length() <= 44 || isSave == false) {
file.delete()
Log.d("tag", "音频文件过小已删除: ${file.absolutePath}")
return false
} else {
RandomAccessFile(file, "rw").use { raf ->
raf.seek(0)
raf.write(generateWavHeader(totalBytesWritten))
}
Log.d("tag", "音频文件保存完成: ${file.absolutePath}")
// 保存成功文件引用
lastSavedFile = file
return true
mainFileSuccess = true
}
}
// 处理左右声道文件(如果是双声道)
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
}
} catch (e: Exception) {
Log.e("tag", "更新WAV文件头失败: ${e.message}")
return false
} finally {
// 清理资源
fos = null
currentAudioFile = null
writeThread = null
// 清理双声道资源
leftChannelFos = null
rightChannelFos = null
leftChannelFile = null
rightChannelFile = null
leftChannelWriteThread = null
rightChannelWriteThread = null
leftChannelBytesWritten = 0
rightChannelBytesWritten = 0
totalBytesWritten = 0
}
}
/**
* 关闭单个声道文件
*/
private fun closeStereoChannelFile(
channelFile: File?,
channelFos: FileOutputStream?,
bytesWritten: Int,
channelName: String,
isSave: Boolean
): Boolean {
return try {
channelFos?.close()
channelFile?.let { file ->
if (file.length() <= 44 || isSave == false) {
file.delete()
Log.d("tag", "${channelName}文件过小已删除: ${file.absolutePath}")
false
} else {
RandomAccessFile(file, "rw").use { raf ->
raf.seek(0)
raf.write(generateMonoWavHeader(bytesWritten))
}
Log.d("tag", "${channelName}文件保存完成: ${file.absolutePath}")
true
}
} ?: false
} catch (e: Exception) {
Log.e("tag", "${channelName}文件关闭失败: ${e.message}")
false
}
return false
}
}

4
local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/SimpleAudioPlayer.kt → local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/tools/SimpleAudioPlayer.kt

@ -1,4 +1,4 @@
package com.yunqiinnovation.azure_speech
package com.yunqiinnovation.azure_speech.tools
import android.content.Context
import android.media.AudioAttributes
@ -49,7 +49,7 @@ class SimpleAudioPlayer(private val context: Context? = null) {
// 推送流
private var pushOutputStream: PushAudioOutputStream? = null
// 自定义推送流回调
private inner class AudioOutputCallback : PushAudioOutputStreamCallback() {
override fun write(dataBuffer: ByteArray): Int {

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

@ -0,0 +1,426 @@
package com.yunqiinnovation.azure_speech.tools
import android.content.Context
import android.media.AudioFormat
import android.media.AudioRecord
import android.media.MediaRecorder
import android.util.Log
import com.microsoft.cognitiveservices.speech.*
import com.microsoft.cognitiveservices.speech.audio.AudioStreamFormat
import java.util.concurrent.LinkedBlockingQueue
import com.microsoft.cognitiveservices.speech.audio.AudioInputStream
import com.microsoft.cognitiveservices.speech.audio.PushAudioInputStream
import java.util.concurrent.atomic.AtomicBoolean
import android.media.AudioManager
// 添加缺失的导入
import android.media.AudioFocusRequest
import android.media.AudioAttributes
import com.yunqiinnovation.azure_speech.tools.RecordFile
/**
* 简单音频接收器类,用于处理音频录制和流传输
* @param azureAsrHelper AzureAsrHelper实例的引用,用于访问共享状态
*/
class SimpleAudioReceiver(private val context: Context) {
companion object {
private const val TAG = "SimpleAudioReceiver"
}
/**
* 音频来源类型(使用 AzureAsrHelper 中定义的枚举)
*/
enum class AudioSourceType {
/** 使用设备麦克风 */
MICROPHONE,
/** 使用外部提供的音频数据 */
EXTERNAL
}
private val bufferSize = 4096 // 可根据需要调整
private var audioDataCallback: AudioDataCallback? = null
var audioRecord: AudioRecord? = null
var pushAudioStream: PushAudioInputStream? = null
// 音频源配置
var audioSourceType = AudioSourceType.MICROPHONE
// 新增:用于异步写入的队列和线程
private val writeQueue = LinkedBlockingQueue<ByteArray>()
private val isRunning = AtomicBoolean(false)// 控制线程是否继续存在
private val isWriting = AtomicBoolean(false) // 控制是否应该写入数据
private var writeThread: Thread? = null
// 音频配置
private val channelConfig = AudioFormat.CHANNEL_IN_MONO
private val audioFormat = AudioFormat.ENCODING_PCM_16BIT
// 音频模式管理
private var audioManager: AudioManager? = null
private var originalAudioMode = AudioManager.MODE_NORMAL
// 录音文件处理
var recordfile: RecordFile? = null
/**
* 获取最优音频格式
*/
private fun getOptimalAudioFormat(): AudioStreamFormat {
// 支持的采样率列表(按优先级排序)
val supportedSampleRates = intArrayOf(16000, 8000, 11025, 22050, 44100)
// 查找设备支持的最佳采样率
val sampleRate = supportedSampleRates.firstOrNull { rate ->
val bufferSize = AudioRecord.getMinBufferSize(
rate,
AudioFormat.CHANNEL_IN_MONO,
AudioFormat.ENCODING_PCM_16BIT
)
bufferSize > 0 // 返回正值表示支持
} ?: 16000 // 默认回退值
Log.i(TAG, "使用采样率: ${sampleRate}Hz")
// 创建对应的音频格式
return AudioStreamFormat.getWaveFormatPCM(sampleRate.toLong(), 16, 1)
}
/**
* 初始化音频录制
*/
fun initAudioRecord() {
val format = getOptimalAudioFormat()
pushAudioStream = AudioInputStream.createPushStream(format)
isRunning.set(true)
Log.d(TAG, "initAudioRecord: ${isRunning}")
startWriteThread() // 再启动数据读取线程
// 初始化音频管理器
audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
originalAudioMode = audioManager?.mode ?: AudioManager.MODE_NORMAL
}
/**
* 启动写入线程
*/
private fun startWriteThread() {
writeThread = Thread {
try {
while (isRunning.get()) {
// Log.d(TAG, "startWriteThread:isWriting= ${isWriting}")
if (!isWriting.get()) {
Thread.sleep(10) // 短暂休眠避免空转
continue
}
var data: ByteArray? = null
var bytesToWrite = 0
//Log.d(TAG, "startWriteThread: ${audioRecord?.recordingState}")
// 情况1:正在录制中 -> 直接从AudioRecord读取
if (audioRecord?.recordingState == AudioRecord.RECORDSTATE_RECORDING) {
data = ByteArray(bufferSize)
val bytesRead = audioRecord?.read(data, 0, bufferSize) ?: -1
when {
bytesRead < 0 -> {
Log.e("tag", "读取音频失败,错误码: $bytesRead")
continue
}
bytesRead == 0 -> continue // 无数据可读
else -> bytesToWrite = bytesRead // 有效数据
}
}
// 情况2:不在录制但队列有数据 -> 从队列获取
else if (writeQueue.isNotEmpty()) {
data = writeQueue.poll()
Log.d("tag", "写入数据: ${data?.size}")
bytesToWrite = data?.size ?: 0
}
// 确保有有效数据再写入
if (data != null && bytesToWrite > 0) {
// 处理实际读取长度 < bufferSize 的情况
val finalData =
if (bytesToWrite < data.size) data.copyOf(bytesToWrite) else data
try {
Log.d(TAG, "写入数据大小: ${finalData.size}")
if(audioDataCallback!=null){
audioDataCallback?.onAudio(finalData)
}
pushAudioStream?.write(finalData)
recordfile?.saveAudioDataToWav(finalData)
} catch (e: Exception) {
Log.e(TAG, "写入失败: ${e.message}")
}
} else {
Thread.yield() // 避免空转消耗CPU
}
}
} catch (e: Exception) {
Log.e(TAG, "写入线程异常: ${e.stackTraceToString()}")
} finally {
Log.d(TAG, "音频写入线程退出")
writeQueue.clear()
}
}.apply {
name = "AudioWriteThread"
start()
}
}
/**
* 外部音频输入
*/
fun saveAudioDataTo(buffer: ByteArray) {
if (audioSourceType == AudioSourceType.MICROPHONE) return
// 放入队列,由写线程写入
writeQueue.offer(buffer.copyOf())
}
/**
* 开始音频输入
* @param audioSourceType 音频源类型
* @param callback 音频数据回调,可为空
*/
fun startAudioRecord(audioSourceType: AudioSourceType, callback: AudioDataCallback?) {
this.audioSourceType = audioSourceType
this.audioDataCallback = callback
writeQueue.clear()
isWriting.set(true)
when (audioSourceType) {
AudioSourceType.MICROPHONE -> runMicrophoneCapture()
AudioSourceType.EXTERNAL -> runExternalCapture()
}
}
/**
* 配置通话音频模式以优化回声消除
*/
private fun setupCommunicationAudioMode() {
try {
audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
// 保存原始音频模式
originalAudioMode = audioManager?.mode ?: AudioManager.MODE_NORMAL
// 设置通话模式 - 这是关键!
audioManager?.mode = AudioManager.MODE_IN_COMMUNICATION
// 启用扬声器(如果需要外放)
audioManager?.isSpeakerphoneOn = true
// 请求音频焦点
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
val focusRequest = AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE)
.setAudioAttributes(
AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION)
.setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
.build()
)
.build()
audioManager?.requestAudioFocus(focusRequest)
}
Log.d("AudioMode", "通话音频模式已设置: MODE_IN_COMMUNICATION")
} catch (e: Exception) {
Log.e("AudioMode", "设置通话音频模式失败: ${e.message}")
}
}
/**
* 运行麦克风捕获
*/
private fun runMicrophoneCapture() {
try {
// 首先设置通话音频模式
setupCommunicationAudioMode()
val supportedSampleRates = intArrayOf(16000, 8000, 11025, 22050, 44100)
val sampleRate = supportedSampleRates.firstOrNull { rate ->
val bufferSize = AudioRecord.getMinBufferSize(rate, channelConfig, audioFormat)
bufferSize > 0
} ?: 16000
val minBufferSize = AudioRecord.getMinBufferSize(sampleRate, channelConfig, audioFormat)
// 使用VOICE_COMMUNICATION音频源(专为VoIP优化)
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
val format = AudioFormat.Builder()
.setSampleRate(sampleRate)
.setEncoding(audioFormat)
.setChannelMask(AudioFormat.CHANNEL_IN_MONO)
.build()
audioRecord = AudioRecord.Builder()
.setAudioSource(MediaRecorder.AudioSource.VOICE_COMMUNICATION)
.setAudioFormat(format)
.setBufferSizeInBytes(minBufferSize * 2)
.build()
} else {
audioRecord = AudioRecord(
MediaRecorder.AudioSource.VOICE_COMMUNICATION,
sampleRate,
channelConfig,
audioFormat,
minBufferSize * 2
)
}
if (audioRecord?.state != AudioRecord.STATE_INITIALIZED) {
throw IllegalStateException("AudioRecord初始化失败")
}
audioRecord?.startRecording()
Log.d("TAG", "通话模式录音开始,采样率: $sampleRate Hz")
} catch (e: Exception) {
Log.e("TAG", "麦克风捕获失败: ${e.message}")
}
}
/**
* 运行外部捕获
*/
private fun runExternalCapture() {
Log.d("TAG", "外部音频捕获启动")
try { // TODO: 实现外部音频源捕获逻辑
// 停止录音
if (audioRecord?.recordingState == AudioRecord.STATE_INITIALIZED) {
Log.d("TAG", "外部音频捕获启动 释放audioRecord")
audioRecord?.stop()
// 释放录音实例
audioRecord?.release()
audioRecord = null
}
} catch (e: Exception) {
Log.e("TAG", "外部音频捕获异常: ${e.message}")
} finally {
}
}
/**
* 继续麦克风捕获
*/
fun resumeRecord() {
try {
isWriting.set(true)
// 首先设置通话音频模式
setupCommunicationAudioMode()
// 停止录音
audioRecord?.startRecording()
} catch (e: Exception) {
Log.e(TAG, "继续录音失败: ${e.message}")
}
}
/**
* 停止麦克风捕获
*/
fun stopMicrophoneCapture() {
try {
isWriting.set(false)
// 停止录音
audioRecord?.stop()
// 恢复音频模式
restoreCommunicationAudioMode()
} catch (e: Exception) {
Log.e(TAG, "停止麦克风捕获失败: ${e.message}")
}
}
/**
* 释放音频资源
*/
fun releaseAudioResources() {
try {
isRunning.set(false)
isWriting.set(false)
// 中断并等待捕获线程结束
writeThread?.interrupt()
writeThread?.join(300) // 最多等待300ms
// 释放录音实例
audioRecord?.release()
audioRecord = null
} catch (e: Exception) {
Log.e(TAG, "释放音频资源失败: ${e.message}")
e.printStackTrace()
} finally {
writeQueue.clear()
writeThread = null
}
}
/**
* 禁用蓝牙音频功能,切换回正常音频模式
*/
fun disableBluetoothAudio() {
try {
// 关闭蓝牙SCO(Synchronous Connection Oriented)音频路由
audioManager?.isBluetoothScoOn = false
// 停止蓝牙SCO连接
audioManager?.stopBluetoothSco()
// 切换回正常音频模式(非通话模式)
audioManager?.mode = AudioManager.MODE_IN_COMMUNICATION
} catch (e: Exception) {
Log.e("AudioConfig", "Failed to disable Bluetooth: ${e.message}")
}
}
/**
* 恢复原始音频设备状态(通常是重新启用蓝牙)
*/
fun restoreOriginalAudioState() {
try {
// 恢复应用启动时的原始音频模式
audioManager?.mode = AudioManager.MODE_NORMAL
// 重新启用蓝牙SCO
audioManager?.isBluetoothScoOn = true
// 启动蓝牙SCO连接(通常在需要蓝牙通话时使用)
audioManager?.startBluetoothSco()
} catch (e: Exception) {
Log.e("AudioConfig", "Failed to restore audio state: ${e.message}")
}
}
/**
* 恢复原始音频模式
*/
private fun restoreCommunicationAudioMode() {
try {
audioManager?.mode = originalAudioMode
audioManager?.isSpeakerphoneOn = false
// 释放音频焦点
Log.d("AudioMode", "音频模式已恢复")
} catch (e: Exception) {
Log.e("AudioMode", "恢复音频模式失败: ${e.message}")
}
}
fun isWriting(): Boolean {
return isWriting.get()
}
/**
* 连续识别回调接口
*/
interface AudioDataCallback {
/**
* 音频数据回调
* @param data 音频数据字节数组
*/
fun onAudio(data: ByteArray)
}
}

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

@ -285,6 +285,30 @@ public class AzureAsrHelper: NSObject {
}
}
/**
* 设置音频配置
* @param sampleRate 采样率,默认16000
* @param channels 声道数,默认1
* @return 设置是否成功
*/
public func setAudioConfig(sampleRate: Int, channels: Int) -> Bool {
do {
print("设置音频配置 - 采样率: \(sampleRate), 声道: \(channels)")
// 设置音频流的配置
audioStream?.setAudioConfig(sampleRate: sampleRate, channels: channels)
// 设置录音文件的配置
recordfile?.setAudioConfig(sampleRate: sampleRate, channels: channels)
print("音频配置设置成功")
return true
} catch {
print("设置音频配置失败: \(error.localizedDescription)")
return false
}
}
/**
* 设置识别器
@ -678,6 +702,32 @@ public class AzureAsrHelper: NSObject {
interleaved: true
)
}
/**
* 设置音频配置
* @param sampleRate 采样率,默认16000
* @param channels 声道数,默认1
*/
public func setAudioConfig(sampleRate: Int, channels: Int) {
print("AudioStream设置音频配置 - 采样率: \(sampleRate), 声道: \(channels)")
// 更新音频格式
audioFormat = AVAudioFormat(
commonFormat: .pcmFormatInt16,
sampleRate: Double(sampleRate),
channels: AVAudioChannelCount(channels),
interleaved: true
)
// 如果已经有推流,重新创建
if pushAudioStream != nil {
let audioStreamFormat = SPXAudioStreamFormat()
audioStreamFormat?.initUsingPCM(withSampleRate: UInt(sampleRate), bitsPerSample: 16, channels: UInt(channels))
pushAudioStream = SPXPushAudioInputStream(audioFormat: audioStreamFormat)
}
print("AudioStream音频配置设置完成")
}
/**
* 开始音频输入
*/

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

@ -262,7 +262,7 @@ import os.log
return
}
do {
print("音频文件名称为: (filePath)")
print("音频文件名称为: \(filePath)")
try azureAsrHelper.enableRecord(filePath: filePath)
result(true)
} catch {
@ -272,6 +272,9 @@ import os.log
case "pauseRecord":
azureAsrHelper.pauseRecord()
result(true)
case "resumeRecord":
azureAsrHelper.resumeRecord()
result(true)
case "stopRecord":
guard let args = call.arguments as? [String: Any],
@ -299,6 +302,18 @@ import os.log
azureTtsHelper.stop()
result(true)
case "setAudioConfig":
guard let args = call.arguments as? [String: Any] else {
result(FlutterError(code: "INVALID_ARGUMENTS", message: "参数不能为空", details: nil))
return
}
let sampleRate = args["sampleRate"] as? Int ?? 16000
let channels = args["channels"] as? Int ?? 1
let success = azureAsrHelper.setAudioConfig(sampleRate: sampleRate, channels: channels)
result(success)
default:
result(FlutterMethodNotImplemented)
}
@ -558,4 +573,4 @@ extension AzureSpeechPlugin: AudioDataListener {
sendTtsEvent(eventMap)
}
}
}

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

@ -10,12 +10,17 @@ object BleConst {
/** 主服务UUID - 文档中定义为0000ABC0-0000-1111-2222-123456789ABC */
val PRIMARY_SERVICE_UUID: UUID = UUID.fromString("0000abc0-0000-1111-2222-123456789abc")
/** 写入特征UUID - 文档中定义为0000ABC1-0000-1111-2222-123456789ABC */
val WRITE_CHAR_UUID: UUID = UUID.fromString("0000abc1-0000-1111-2222-123456789abc")
/** 音频服务UUID - 文档中定义为0000ABC0-0001-1111-2222-123456789ABC */
val AUDIO_SERVICE_UUID1: UUID = UUID.fromString("0000ABC0-0001-1111-2222-123456789ABC")
/** 通知特征UUID - 文档中定义为0000ABC2-0000-1111-2222-123456789ABC */
val NOTIFY_CHAR_UUID: UUID = UUID.fromString("0000abc2-0000-1111-2222-123456789abc")
/** 通话音频服务UUID - 文档中定义为0000ABC0-0001-1111-2222-123456789ABC */
val CALL_AUDIO_SERVICE_UUID: UUID = UUID.fromString("0000ABC0-0001-1111-2222-123456789ABC")
/** 接收音频特征UUID - 文档中定义为0000ABC2-0001-1111-2222-123456789ABC */
val RECEIVE_AUDIO_CHAR_UUID1: UUID = UUID.fromString("0000ABC2-0001-1111-2222-123456789ABC")
/** 通话写入音频特征UUID - 文档中定义为0000ABC1-0001-1111-2222-123456789ABC */
val CALL_WRITE_AUDIO_CHAR_UUID: UUID = UUID.fromString("0000ABC1-0001-1111-2222-123456789ABC")
/** 音频服务UUID - 文档中定义为00001801-0000-1000-8000-00805f9b34fb */
val AUDIO_SERVICE_UUID: UUID = UUID.fromString("0000ae00-0000-1000-8000-00805f9b34fb")
@ -23,15 +28,6 @@ object BleConst {
/** 接收音频特征UUID - 文档中定义为0000ABC2-0001-1111-2222-123456789ABC */
val RECEIVE_AUDIO_CHAR_UUID: UUID = UUID.fromString("0000ae02-0000-1000-8000-00805f9b34fb")
/** 写入特征UUID - 文档中定义为0000ABC1-0000-1111-2222-123456789ABC */
val WRITE_CHAR_UUID: UUID = UUID.fromString("0000abc1-0000-1111-2222-123456789abc")
/** 音频写入特征UUID - 文档中定义为0000ABC1-0000-1111-2222-123456789ABC */
val AUDIO_WRITE_CHAR_UUID: UUID = UUID.fromString("0000ABC1-0001-1111-2222-123456789ABC")
/** 通知特征UUID - 文档中定义为0000ABC2-0000-1111-2222-123456789ABC */
val NOTIFY_CHAR_UUID: UUID = UUID.fromString("0000abc2-0000-1111-2222-123456789abc")
/** 客户端特征配置描述符UUID */
val CLIENT_CHAR_CONFIG_UUID: UUID = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb")

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

@ -12,6 +12,7 @@ import java.util.concurrent.CopyOnWriteArrayList
// Import Jieli Opus SDK classes
import com.jieli.jl_audio_decode.opus.model.OpusOption
import com.jieli.jl_audio_decode.callback.OnDecodeStreamCallback
import com.jieli.jl_audio_decode.callback.OnEncodeStreamCallback
import com.jieli.jl_audio_decode.opus.OpusManager
import com.jieli.jl_audio_decode.exceptions.OpusException
@ -25,6 +26,7 @@ import android.os.Handler
import java.util.concurrent.atomic.AtomicBoolean
import android.util.Log
import androidx.annotation.RequiresPermission
import java.util.concurrent.TimeUnit
/**
* BLE服务类:提供蓝牙低功耗设备的扫描、连接和通信功能
@ -52,6 +54,10 @@ object BleService {
// 数据相关回调
fun onAudioDataReceived(data: ByteArray)
// 数据相关回调
fun onAudioDataReceived1(data: ByteArray)
// 唤醒信号相关回调
fun onWakeupSignalReceived()
// 设备信息相关回调 - 统一回调接口
@ -76,8 +82,9 @@ object BleService {
private var notifyChar: BluetoothGattCharacteristic? = null
private var writeChar: BluetoothGattCharacteristic? = null
private var audioChar: BluetoothGattCharacteristic? = null
private var audioChar1: BluetoothGattCharacteristic? = null
private var callWriteChar: BluetoothGattCharacteristic? = null
var recordfile: RecordingFile? = null
var recordfile1: RecordingFile? = null
// 扫描相关
private lateinit var scanHandler: Handler
@ -104,11 +111,58 @@ object BleService {
// Opus解码器实例
private var opusManager: OpusManager? = null
private var option: OpusOption? = null
private val mainHandler = Handler(Looper.getMainLooper())
// 解码音频数据队列和处理线程
private val audioDataQueue = LinkedBlockingQueue<ByteArray>()
private var audioQueueProcessorThread: Thread? = null
// 音频数据缓存
private val audioDataBuffer = mutableListOf<Byte>()
//private val AUDIO_BUFFER_SIZE = 1280// 1280字节缓存阈值
// 音频数据发送相关
private val audioSendQueue = LinkedBlockingQueue<ByteArray>()
private var audioSendThread: Thread? = null
private val audioSendHandler = Handler(Looper.getMainLooper())
private val isAudioSending = AtomicBoolean(false)
// 音频数据分块发送的常量
private val AUDIO_CHUNK_SIZE = 120 // 每次发送80字节
private val AUDIO_SEND_INTERVAL = 60L // 发送间隔20ms
// 音频数据缓冲区,用于累积数据到80字节再发送
private val audioBuffer = mutableListOf<Byte>()
// 重发机制相关常量
private val MAX_RETRY_COUNT = 1 // 最大重试次数
private val RETRY_DELAY = 30L // 重试延迟时间(毫秒)
// 初始化状态
private var isInitialized = false
// 音频数据统计相关变量
private var bytesReceivedInCurrentSecond = 0
// 添加发送数据统计变量
private var bytesSentInCurrentSecond = 0
private var lastStatisticsTime = System.currentTimeMillis()
private val statisticsHandler = Handler(Looper.getMainLooper())
private val statisticsRunnable = object : Runnable {
override fun run() {
val currentTime = System.currentTimeMillis()
val timeDiff = currentTime - lastStatisticsTime
if (timeDiff >= 1000) { // 每秒统计一次
Log.i(TAG, "每秒接收音频数据: $bytesReceivedInCurrentSecond 字节")
Log.i(TAG, "每秒发送音频数据: $bytesSentInCurrentSecond 字节")
bytesReceivedInCurrentSecond = 0
bytesSentInCurrentSecond = 0
lastStatisticsTime = currentTime
}
statisticsHandler.postDelayed(this, 1000) // 每秒执行一次
}
}
private val replyTimeoutHandler = Handler(Looper.getMainLooper())
private val replyTimeoutRunnable = Runnable {
if (!isReply) {
@ -128,20 +182,6 @@ object BleService {
try {
this.context = appContext.applicationContext
// otaManager = OTAManager(this.context).apply {
// // 设置数据回调
// setDataCallback(object : OTAManager.DataCallback {
// override fun onDataReceived(device: BluetoothDevice?, data: ByteArray?) {
// data?.let {
// Log.i("BleService", "收到数据:${it.size} 字节")
// Log.i("BleService", "认证交互数据${it.contentToString()}")
// // 移除次数限制,除非明确需要
// otaManager?.onReceiveDeviceData(device, it)
// }
// }
// })
// }
// 初始化蓝牙管理器和适配器
bluetoothManager =
context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
@ -151,16 +191,23 @@ object BleService {
// 初始化Handler
scanHandler = Handler(Looper.getMainLooper())
// 初始化OpusManager和OTAManager
// 启动队列处理
startAudioQueueProcessing()
// 初始化OpusManager和OpusOption
try {
opusManager = OpusManager()
startOpusStreamDecoding()
option = OpusOption()
Log.d(TAG, "OpusManager初始化成功")
} catch (e: OpusException) {
Log.e(TAG, "OpusManager初始化失败: ${e.message}", e)
// 根据需要决定是否因为Opus初始化失败而返回false
}
recordfile = RecordingFile(this.context)
recordfile!!.fileName = "不拆分"
recordfile1 = RecordingFile(this.context)
recordfile1!!.fileName = "重新压缩"
isInitialized = true
Log.d(TAG, "BLE服务初始化成功")
return true
@ -205,6 +252,25 @@ object BleService {
Log.d(TAG, "已清除所有BLE回调")
}
fun writeExternalAudioData(data: ByteArray) {
// 检查OpusManager是否已初始化
if (opusManager == null) {
Log.e(TAG, "opusManager 未初始化")
return
}
// 如果已经在编码流中,直接写入数据
if (opusManager?.isEncodeStream == true) {
Log.d(TAG, "正在进行Opus编码流,写入音频数据")
// 将外部音频数据写入编码流,每次处理1280字节
opusManager?.writeEncodeStream(data)
} else {
Log.w(TAG, "Opus编码流未启动,无法写入音频数据")
// 可选:自动启动编码流
// startOpusEncodeStream()
}
}
// ======================================================================================================
// 扫描功能
// ======================================================================================================
@ -360,15 +426,16 @@ object BleService {
}
return null;
}
fun getDeviceInfos(deviceName: String): List<Map<String, Any>> {
// 更新缓存
val matchedResults = scanResults.filter {
it.device.name == deviceName
}
Log.d(TAG, "找到 ${matchedResults.size} 个名称为 $deviceName 的设备")
val ret = mutableListOf<Map<String, Any>>()
// 如果找到匹配设备名的结果,返回匹配设备的信息
if (matchedResults.isNotEmpty()) {
for (result in matchedResults) {
@ -386,9 +453,10 @@ object BleService {
}
}
}
return ret
}
/**
* 打印设备信息和服务UUID
*/
@ -517,7 +585,7 @@ object BleService {
notifyChar = null
writeChar = null
audioChar = null
audioChar1 = null
callWriteChar = null
}
}
@ -530,42 +598,56 @@ object BleService {
notifyConnectionStateChanged(state)
}
// 在连接成功时启动统计(可以在onConnectionStateChange的连接成功分支中添加)
private fun startBytesStatistics() {
bytesReceivedInCurrentSecond = 0
lastStatisticsTime = System.currentTimeMillis()
statisticsHandler.post(statisticsRunnable)
Log.i(TAG, "开始统计每秒接收字节数")
}
// 在断开连接时停止统计
private fun stopBytesStatistics() {
statisticsHandler.removeCallbacks(statisticsRunnable)
Log.i(TAG, "停止统计每秒接收字节数")
}
/**
* GATT回调
*/
private val gattCallback = object : BluetoothGattCallback() {
// override fun onMtuChanged(gatt: BluetoothGatt, mtu: Int, status: Int) {
// if (status == BluetoothGatt.GATT_SUCCESS) {
// Log.i(TAG, "MTU 更新成功: $mtu")
// configureOTA()
// startOTA()
// } else {
// Log.e(TAG, "MTU 更新失败: status=$status")
// }
// }
override fun onConnectionStateChange(g: BluetoothGatt, status: Int, newState: Int) {
when {
status == BluetoothGatt.GATT_SUCCESS && newState == BluetoothProfile.STATE_CONNECTED -> {
updateConnectionState(BleConst.STATE_CONNECTED)
// 设置PHY值为2M以提高传输速度
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
try {
// 首先尝试读取当前PHY状态
g.readPhy()
// 然后请求设置2M PHY
g.setPreferredPhy(
BluetoothDevice.PHY_LE_2M_MASK, // TX PHY: 2M
BluetoothDevice.PHY_LE_2M_MASK, // RX PHY: 2M
BluetoothDevice.PHY_OPTION_NO_PREFERRED
)
Log.d(TAG, "已请求设置PHY为2M")
} catch (e: Exception) {
Log.w(TAG, "设置PHY失败: ${e.message},将使用默认1M PHY")
}
} else {
Log.w(TAG, "当前Android版本不支持PHY设置(需要API 26+),使用默认1M PHY")
}
g.discoverServices()
// Log.i(TAG, "连接成功,开始 MTU 协商")
// otaManager?.onBtDeviceConnection(g.device, StateCode.CONNECTION_OK)
// g.requestMtu(512) // 触发 MTU 修改流程
}
newState == BluetoothProfile.STATE_DISCONNECTED -> {
updateConnectionState(BleConst.STATE_DISCONNECTED)
disconnectGatt()
// otaManager?.onBtDeviceConnection(g.device, StateCode.CONNECTION_CONNECTING)
// g.close()
// otaManager?.release();
}
else -> {
@ -574,6 +656,45 @@ object BleService {
}
}
}
/**
* PHY读取回调 - 获取当前PHY状态
*/
override fun onPhyRead(gatt: BluetoothGatt, txPhy: Int, rxPhy: Int, status: Int) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
if (status == BluetoothGatt.GATT_SUCCESS) {
val txPhyStr = getPhyString(txPhy)
val rxPhyStr = getPhyString(rxPhy)
Log.i(TAG, "当前PHY状态: TX=$txPhyStr, RX=$rxPhyStr")
} else {
Log.w(TAG, "读取PHY状态失败: status=$status")
}
}
}
/**
* PHY更新回调 - 监控PHY设置结果
*/
override fun onPhyUpdate(gatt: BluetoothGatt, txPhy: Int, rxPhy: Int, status: Int) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
when (status) {
BluetoothGatt.GATT_SUCCESS -> {
val txPhyStr = getPhyString(txPhy)
val rxPhyStr = getPhyString(rxPhy)
Log.i(TAG, "PHY更新成功: TX=$txPhyStr, RX=$rxPhyStr")
}
6 -> { // GATT_REQUEST_NOT_SUPPORTED
Log.w(TAG, "设备不支持PHY更新,将继续使用1M PHY进行通信")
}
133 -> { // GATT_ERROR
Log.w(TAG, "PHY更新出现一般性错误,可能是设备兼容性问题")
}
else -> {
Log.w(TAG, "PHY更新失败: status=$status")
}
}
}
}
override fun onServicesDiscovered(g: BluetoothGatt, status: Int) {
if (status != BluetoothGatt.GATT_SUCCESS) {
@ -592,16 +713,18 @@ object BleService {
audioChar = audioSvc?.getCharacteristic(BleConst.RECEIVE_AUDIO_CHAR_UUID)
// 获取音频服务1特征
val audioSvc1 = g.getService(BleConst.AUDIO_SERVICE_UUID1)
audioChar1 = audioSvc1?.getCharacteristic(BleConst.RECEIVE_AUDIO_CHAR_UUID1)
if (notifyChar == null || writeChar == null) {
Log.e(TAG, "未找到主服务所需特征")
// 获取通话音频服务特征
val callAudioSvc = g.getService(BleConst.CALL_AUDIO_SERVICE_UUID)
callWriteChar = callAudioSvc?.getCharacteristic(BleConst.CALL_WRITE_AUDIO_CHAR_UUID)
if (callWriteChar == null) {
Log.e(TAG, "未找到通话音频主服务所需特征")
updateConnectionState(BleConst.STATE_ERROR)
return
}
Log.d(TAG, "找到通话音频服务所需特征")
startBytesStatistics()
writeChar?.writeType = BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE
callWriteChar?.writeType = BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE
//要先设置音频服务的通知,否则接收不到
// 设置音频服务的通知(如果存在)
if (audioChar != null) {
@ -610,15 +733,9 @@ object BleService {
} else {
Log.w(TAG, "音频服务特征未找到")
}
if (audioChar1 != null) {
setupNotifications(g, audioChar1)
Log.i(TAG, "音频服务特征找到并设置通知")
} else {
Log.w(TAG, "音频服务特征未找到")
}
// 设置主服务的通知
setupNotifications(g, notifyChar)
//开启解码
}
@ -628,15 +745,11 @@ object BleService {
// 根据特征UUID区分处理
when (c.uuid) {
// 音频特征数据
// BleConst.RECEIVE_AUDIO_CHAR_UUID1 -> {
// Log.i(TAG, "RECEIVE_AUDIO_CHAR_UUID1")
// processAudioData(data)
// }
// 音频特征数据
BleConst.RECEIVE_AUDIO_CHAR_UUID -> {
// 统计接收到的字节数
bytesReceivedInCurrentSecond += data.size
processAudioData(data)
}
// 通知特征数据(命令和控制)
@ -713,12 +826,30 @@ object BleService {
*/
private fun processAudioData(data: ByteArray) {
try {
if (opusManager?.isDecodeStream == true) {
recordfile?.saveAudioDataToWav(data)
opusManager?.writeAudioStream(data)
// 将完整的帧加入队列,由专门的线程处理
audioDataQueue.offer(data)
// // 将接收到的数据添加到缓存中
// synchronized(audioDataBuffer) {
// audioDataBuffer.addAll(data.toList())
// // 当缓存达到阈值时,提取完整的帧进行解码
// while (audioDataBuffer.size >= AUDIO_BUFFER_SIZE) {
// // 提取一个完整的帧(1280字节)
// val frameData = ByteArray(AUDIO_BUFFER_SIZE)
// for (i in 0 until AUDIO_BUFFER_SIZE) {
// frameData[i] = audioDataBuffer.removeAt(0)
// }
// Log.d(TAG, "缓存达到阈值,提取 ${frameData.size} 字节帧进行解码")
// }
// }
} else {
// Log.d(TAG, "Opus解码流未启动,忽略音频数据")
// Log.d(TAG, "Opus解码流未启动,忽略音频数据")
}
} catch (e: Exception) {
Log.e(TAG, "处理音频数据异常: ${e.message}", e)
@ -922,11 +1053,13 @@ object BleService {
codecStatus == BleConst.CODEC_CONTROL_A2DP_PLAY ||
codecStatus == BleConst.CODEC_CONTROL_ENCODE_ON
) {
recordfile1!!.closeFile()
recordfile1!!.creatingFiles()
recordfile!!.closeFile()
recordfile!!.creatingFiles()
} else if (codecStatus == BleConst.CODEC_CONTROL_CLOSE) {
recordfile!!.closeFile()
recordfile1!!.closeFile()
}
// 声道模式描述
val channelDesc = when (channelMode) {
@ -1102,11 +1235,16 @@ object BleService {
Log.d(TAG, "已停止正在进行的Opus解码流")
}
val option = OpusOption()
.setHasHead(hasHeader)
.setChannel(channel)
.setSampleRate(sampleRate)
.setPacketSize(packetSize)
// 清理音频数据缓存,确保开始时是干净的状态
synchronized(audioDataBuffer) {
audioDataBuffer.clear()
Log.d(TAG, "开始解码前已清理音频数据缓存")
}
option!!.setHasHead(hasHeader)
option!!.setChannel(channel)
option!!.setSampleRate(sampleRate)
option!!.setPacketSize(packetSize)
Log.d(TAG, "准备开始Opus数据流解码, 参数: $option")
@ -1115,7 +1253,40 @@ object BleService {
override fun onDecodeStream(data: ByteArray?) {
if (data != null) {
// Log.d(TAG, "Opus解码数据: ${data.size} bytes")
notifyAudioDataReceived(data)
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)
} else {
Log.e(TAG, "Opus解码数据错误: ${data.size} bytes")
}
}
}
@ -1143,6 +1314,13 @@ object BleService {
private fun stopOpusStreamDecoding(): Boolean {
if (opusManager?.isDecodeStream == true) {
opusManager?.stopDecodeStream()
// 清理音频数据缓存
synchronized(audioDataBuffer) {
audioDataBuffer.clear()
Log.d(TAG, "已清理音频数据缓存")
}
Log.i(TAG, "已停止Opus数据流解码")
return true
}
@ -1150,7 +1328,268 @@ object BleService {
return false
}
// ======================================================================================================
// Opus 编码相关
// ======================================================================================================
private fun startOpusEncodeStream(): Boolean {
if (opusManager == null) {
Log.e(TAG, "OpusManager未初始化,无法开始编码")
return false
}
// 如果已经在编码流,先停止
if (opusManager?.isEncodeStream == true) {
Log.d(TAG, "Opus编码流已在运行")
return true
}
// 启动音频发送线程
startAudioSendThread()
var streamStartedSuccessfully = false
opusManager?.startEncodeStream(object : OnEncodeStreamCallback {
override fun onEncodeStream(data: ByteArray?) {
if (data != null) {
// 编码完成的数据处理:
// 1. 保存到WAV文件
recordfile1?.saveAudioDataToWav(data)
// 2. 将编码后的数据加入发送队列进行分块发送
addAudioDataToSendQueue(data)
} else {
Log.w(TAG, "编码回调收到空数据")
}
}
override fun onStart() {
streamStartedSuccessfully = true
Log.i(TAG, "Opus数据流编码已开始")
}
override fun onComplete(outPath: String?) {
Log.i(TAG, "Opus数据流编码完成: $outPath")
}
override fun onError(code: Int, message: String?) {
Log.e(TAG, "Opus数据流编码错误: [$code] $message")
}
})
return true
}
private fun stopOpusEncodeStream(): Boolean {
if (opusManager?.isEncodeStream == true) {
opusManager?.stopEncodeStream()
stopAudioSendThread() // 停止音频发送线程
Log.i(TAG, "已停止Opus数据流编码")
return true
}
Log.d(TAG, "Opus数据流未在编码或OpusManager未初始化")
return false
}
/**
* 将音频数据添加到发送队列
* 只有当缓冲区达到80字节时才发送数据
* @param data 音频数据字节数组
*/
private fun addAudioDataToSendQueue(data: ByteArray) {
try {
// 将新数据添加到缓冲区
audioBuffer.addAll(data.toList())
// 当缓冲区达到80字节时,发送数据
while (audioBuffer.size >= AUDIO_CHUNK_SIZE) {
// 取出80字节数据
val chunk = ByteArray(AUDIO_CHUNK_SIZE)
for (i in 0 until AUDIO_CHUNK_SIZE) {
chunk[i] = audioBuffer.removeAt(0)
}
// 将数据加入发送队列
if (!audioSendQueue.offer(chunk)) {
Log.w(TAG, "音频发送队列已满,丢弃数据块")
break
}
}
// Log.d(TAG, "音频数据已加入缓冲区,当前缓冲区大小: ${audioBuffer.size} 字节")
} catch (e: Exception) {
Log.e(TAG, "处理音频数据异常: ${e.message}", e)
}
}
/**
* 启动音频数据发送线程
*/
private fun startAudioSendThread() {
if (audioSendThread?.isAlive == true) {
Log.d(TAG, "音频发送线程已在运行")
return
}
isAudioSending.set(true)
audioSendThread = Thread {
Log.i(TAG, "音频发送线程已启动")
while (isAudioSending.get() && !Thread.currentThread().isInterrupted) {
try {
// 从队列中取出音频数据块
val audioChunk = audioSendQueue.poll(100, TimeUnit.MILLISECONDS)
if (audioChunk != null) {
// 发送音频数据块
sendAudioChunk(audioChunk)
// 控制发送频率,避免蓝牙缓冲区溢出
Thread.sleep(AUDIO_SEND_INTERVAL)
}
} catch (e: InterruptedException) {
Log.d(TAG, "音频发送线程被中断")
break
} catch (e: Exception) {
Log.e(TAG, "音频发送线程异常: ${e.message}", e)
}
}
Log.i(TAG, "音频发送线程已停止")
}.apply {
name = "AudioSendThread"
start()
}
}
/**
* 停止音频数据发送线程
*/
private fun stopAudioSendThread() {
isAudioSending.set(false)
audioSendThread?.interrupt()
audioSendQueue.clear()
audioBuffer.clear() // 清空音频缓冲区
Log.i(TAG, "音频发送线程已停止,队列和缓冲区已清空")
}
/**
* 发送音频数据块到设备(带重发机制)
* @param chunk 要发送的音频数据块
* @param retryCount 当前重试次数
*/
private fun sendAudioChunk(chunk: ByteArray, retryCount: Int = 0) {
try {
if (callWriteChar == null || bluetoothGatt == null) {
Log.e(TAG, "蓝牙连接或特征值未准备就绪")
return
}
// 在主线程中执行蓝牙写入操作
audioSendHandler.post {
try {
callWriteChar?.value = chunk
val isSuccess = bluetoothGatt?.writeCharacteristic(callWriteChar)
if (isSuccess == true) {
// 累加发送成功的字节数
bytesSentInCurrentSecond += chunk.size
Log.d(TAG, "成功发送音频数据块,大小: ${chunk.size} 字节")
} else {
Log.e(TAG, "发送音频数据块失败,大小: ${chunk.size} 字节")
// 详细的失败原因分析
val failureReason = StringBuilder("发送音频数据块失败,大小: ${chunk.size} 字节")
// 检查蓝牙连接状态
if (bluetoothGatt == null) {
failureReason.append(" - 原因: BluetoothGatt为空")
} else {
// 检查连接状态
val connectionState = bluetoothManager.getConnectionState(
bluetoothGatt!!.device,
BluetoothProfile.GATT
)
when (connectionState) {
BluetoothProfile.STATE_DISCONNECTED -> {
failureReason.append(" - 原因: 设备已断开连接")
}
BluetoothProfile.STATE_CONNECTING -> {
failureReason.append(" - 原因: 设备正在连接中")
}
BluetoothProfile.STATE_DISCONNECTING -> {
failureReason.append(" - 原因: 设备正在断开连接")
}
BluetoothProfile.STATE_CONNECTED -> {
// 连接正常,检查其他原因
if (callWriteChar == null) {
failureReason.append(" - 原因: 写入特征值为空")
} else {
// 检查特征值属性
val properties = callWriteChar!!.properties
if ((properties and BluetoothGattCharacteristic.PROPERTY_WRITE) == 0 &&
(properties and BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE) == 0) {
failureReason.append(" - 原因: 特征值不支持写入操作")
} else if (chunk.size > 512) { // BLE MTU通常限制
failureReason.append(" - 原因: 数据块过大 (${chunk.size} > 512字节)")
} else {
failureReason.append(" - 原因: 未知错误,可能是设备忙碌或缓冲区满")
}
}
}
else -> {
failureReason.append(" - 原因: 未知连接状态($connectionState)")
}
}
}
// 检查蓝牙适配器状态
if (bluetoothAdapter?.isEnabled != true) {
failureReason.append(" - 蓝牙适配器未启用")
}
Log.e(TAG, failureReason.toString())
// // 实现重发机制
// if (retryCount < MAX_RETRY_COUNT) {
// Log.w(TAG, "准备重发音频数据块,重试次数: ${retryCount + 1}/$MAX_RETRY_COUNT")
// // 延迟后重发
// audioSendHandler.postDelayed({
// val isSuccess = bluetoothGatt?.writeCharacteristic(callWriteChar)
// if (isSuccess == true) {
// Log.d(TAG, "重发音频数据块成功")
// } else {
// Log.e(TAG, "重发音频数据块失败")
// }
// }, RETRY_DELAY)
// } else {
// Log.e(TAG, "音频数据块发送失败,已达到最大重试次数($MAX_RETRY_COUNT),丢弃数据块")
// // 可以选择将失败的数据块记录到日志或进行其他处理
// }
}
} catch (e: Exception) {
Log.e(TAG, "发送音频数据块异常: ${e.message}", e)
// // 异常情况下也进行重发
// if (retryCount < MAX_RETRY_COUNT) {
// Log.w(TAG, "发送异常,准备重发音频数据块,重试次数: ${retryCount + 1}/$MAX_RETRY_COUNT")
// audioSendHandler.postDelayed({
// val isSuccess = bluetoothGatt?.writeCharacteristic(callWriteChar)
// if (isSuccess == true) {
// Log.d(TAG, "重发音频数据块成功")
// } else {
// Log.e(TAG, "重发音频数据块失败")
// }
// }, RETRY_DELAY)
// } else {
// Log.e(TAG, "音频数据块发送异常,已达到最大重试次数($MAX_RETRY_COUNT),丢弃数据块")
// }
}
}
} catch (e: Exception) {
Log.e(TAG, "发送音频数据块异常: ${e.message}", e)
}
}
// ======================================================================================================
// 公开的命令接口
// ======================================================================================================
@ -1217,7 +1656,7 @@ object BleService {
// )
startOpusStreamDecoding(false, 1, 16000, 40)
Log.i(TAG, "打开解码0xA2")
Log.i(TAG, "打开解码0xA1")
return sendCommand(
BleConst.CMD_CONTROL_CODEC.toByte(), byteArrayOf(
BleConst.CODEC_CONTROL_DECODE_ON.toByte(),
@ -1231,8 +1670,10 @@ object BleService {
* 控制编解码 - 打开解码
*/
fun openA2DPDecoder(): Boolean {
Log.i(TAG, "打开编码 0xA1")
startOpusStreamDecoding(false, 1, 16000, 80)
Log.i(TAG, "打开编码 0xA2")
startOpusEncodeStream()
//双声道 80字节
startOpusStreamDecoding(false, 2, 16000, 80)
// Log.i(TAG, "打开解码...")
return sendCommand(
BleConst.CMD_CONTROL_CODEC.toByte(), byteArrayOf(
@ -1323,7 +1764,7 @@ object BleService {
* 连接状态检查
*/
private fun checkConn(): Boolean =
bluetoothGatt != null && writeChar != null &&
bluetoothGatt != null && writeChar != null && callWriteChar != null &&
connectionState.value == BleConst.STATE_CONNECTED
/**
@ -1351,14 +1792,28 @@ object BleService {
stopScan()
scanHandler.removeCallbacksAndMessages(null)
disconnectGatt()
// 释放OpusManager
opusManager?.let {
if (it.isDecodeStream) {
it.stopDecodeStream()
}
it.release()
// 停止音频队列处理线程
audioQueueProcessorThread?.interrupt()
audioQueueProcessorThread = null
audioDataQueue.clear()
// 停止音频发送线程
stopAudioSendThread()
// 清理音频数据缓存
synchronized(audioDataBuffer) {
audioDataBuffer.clear()
Log.d(TAG, "已清理音频数据缓存")
}
Log.d(TAG, "音频解码线程和队列处理线程已清理")
// 释放OpusManager
stopOpusEncodeStream()
stopOpusStreamDecoding()
opusManager?.release()
opusManager = null
option = null
Log.d(TAG, "OpusManager已释放")
isInitialized = false // 标记为未初始化
}
@ -1424,8 +1879,7 @@ object BleService {
isReply = false
replyTimeoutHandler.postDelayed(replyTimeoutRunnable, 1000) // 设置1秒超时
return true
} else
{
} else {
commandQueue.poll()
return false
}
@ -1511,6 +1965,18 @@ object BleService {
}
}
}
/**
* 向所有回调监听器分发音频数据
*/
private fun notifyAudioDataReceived1(data: ByteArray) {
for (callback in callbacks) {
try {
callback.onAudioDataReceived1(data)
} catch (e: Exception) {
Log.e(TAG, "分发音频数据回调异常", e)
}
}
}
/**
* 向所有回调监听器分发唤醒信号
@ -1544,6 +2010,46 @@ object BleService {
}
}
/**
* 获取PHY类型的字符串描述
*/
private fun getPhyString(phy: Int): String {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
when (phy) {
BluetoothDevice.PHY_LE_1M -> "1M"
BluetoothDevice.PHY_LE_2M -> "2M"
BluetoothDevice.PHY_LE_CODED -> "Coded"
else -> "Unknown($phy)"
}
} else {
"Unknown($phy)"
}
}
/**
* 启动音频队列处理
*/
private fun startAudioQueueProcessing() {
audioQueueProcessorThread = Thread {
while (!Thread.currentThread().isInterrupted) {
try {
// 从队列中取出音频数据进行解码
val audioData = audioDataQueue.take() // 阻塞等待数据
opusManager?.writeAudioStream(audioData)
} catch (e: InterruptedException) {
Log.d(TAG, "音频队列处理线程被中断")
break
} catch (e: Exception) {
Log.e(TAG, "音频队列处理异常: ${e.message}", e)
}
}
}.apply {
name = "AudioQueueProcessor"
start()
}
}
}

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

@ -361,6 +361,10 @@ class BleServicePlugin : FlutterPlugin, MethodCallHandler, ActivityAware,
// sendEvent(dataEventSink, mapOf("type" to "audioData", "data" to data), "发送音频数据异常")
}
override fun onAudioDataReceived1(data: ByteArray) {
// sendEvent(dataEventSink, mapOf("type" to "audioData", "data" to data), "发送音频数据异常")
}
override fun onWakeupSignalReceived() {
// 将唤醒事件发送到Flutter
sendEvent(statusEventSink, mapOf("type" to "wakeup"), "发送唤醒信号异常")

2
local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/ChatApiService.kt

@ -403,7 +403,7 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor
Log.e("ChatApiService", "openAI对象为null")
throw IllegalStateException("OpenAI客户端未初始化")
}
// Log.e("ChatApiService", "liwei---------,发送AI请求 model:${currentModel} messages:${chatMessages}")
Log.e("ChatApiService", "liwei---------,发送AI请求 model:${currentModel} messages:${chatMessages}")
val flow = openAI!!.chatCompletions(chatCompletionRequest)
// Log.d(TAG, "liwei-------------------------开始AI 对话 6")
flow

57
local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/CustomSseClientTransport.kt

@ -22,7 +22,8 @@ import java.util.concurrent.atomic.AtomicBoolean
*/
class CustomSseClientTransport(
private val client: HttpClient,
private val urlString: String?,
private val serviceidString: String?,
public val urlString: String?,
private val reconnectionTime: Duration? = null,
private val requestBuilder: HttpRequestBuilder.() -> Unit = {},
private val onConnectionLost: (() -> Unit)? = null
@ -88,7 +89,7 @@ class CustomSseClientTransport(
Triple(hostUrl, path, params)
} catch (e: Exception) {
Log.e(TAG, "解析URL失败: $url, ${e.message}")
Log.e(TAG, "$serviceidString 解析URL失败: $url, ${e.message}")
Triple(url, "", emptyMap())
}
}
@ -102,9 +103,9 @@ class CustomSseClientTransport(
session.incoming.collect { event ->
when (event.event) {
"error" -> {
Log.e(TAG, "SSE错误: ${event.data}")
Log.e(TAG, "$serviceidString SSE错误: ${event.data}")
isConnected.set(false)
val exception = Exception("SSE Error: ${event.data}")
val exception = Exception("$serviceidString SSE Error: ${event.data}")
_onError(exception)
onConnectionLost?.invoke()
throw exception
@ -112,7 +113,7 @@ class CustomSseClientTransport(
"open" -> {
// SSE连接已打开
Log.d(TAG, "SSE连接已打开")
Log.d(TAG, "$serviceidString SSE连接已打开")
isConnected.set(true)
}
"ping" -> {
@ -146,7 +147,7 @@ class CustomSseClientTransport(
endpoint.complete(endpointWithParams)
} catch (e: Exception) {
Log.e(TAG, "处理endpoint事件失败: ${e.message}", e)
Log.e(TAG, "$serviceidString 处理endpoint事件失败: ${e.message}", e)
_onError(e)
close()
error(e)
@ -161,22 +162,22 @@ class CustomSseClientTransport(
val message = json.decodeFromString<JSONRPCMessage>(data)
_onMessage(message)
} catch (e: Exception) {
Log.e(TAG, "解析JSON-RPC消息失败: ${e.message}", e)
Log.e(TAG, "$serviceidString 解析JSON-RPC消息失败: ${e.message}", e)
_onError(e)
}
}
} catch (e: Exception) {
Log.e(TAG, "处理事件失败: ${e.message}", e)
Log.e(TAG, "$serviceidString 处理事件失败: ${e.message}", e)
_onError(e)
}
}
}
}
} catch (e: CancellationException) {
Log.d(TAG, "SSE事件收集被取消")
Log.d(TAG, "$serviceidString SSE事件收集被取消")
throw e
} catch (e: Exception) {
Log.e(TAG, "SSE连接异常断开: ${e.message}", e)
Log.e(TAG, "$serviceidString SSE连接异常断开: ${e.message}", e)
isConnected.set(false)
_onError(e)
onConnectionLost?.invoke()
@ -199,13 +200,13 @@ class CustomSseClientTransport(
// 检查session是否仍然活跃
if (session.coroutineContext[Job]?.isCancelled == true) {
Log.w(TAG, "检测到SSE会话已取消")
Log.w(TAG, "$serviceidString 检测到SSE会话已取消")
isConnected.set(false)
onConnectionLost?.invoke()
break
}
} catch (e: Exception) {
Log.e(TAG, "连接监控异常: ${e.message}", e)
Log.e(TAG, "$serviceidString 连接监控异常: ${e.message}", e)
isConnected.set(false)
onConnectionLost?.invoke()
break
@ -219,7 +220,7 @@ class CustomSseClientTransport(
*/
override suspend fun start() {
if (!initialized.compareAndSet(false, true)) {
Log.e(TAG, "传输层已经启动,不能重复启动")
Log.e(TAG, "$serviceidString 传输层已经启动,不能重复启动")
error("CustomSseClientTransport already started!")
}
@ -283,12 +284,30 @@ class CustomSseClientTransport(
setBody(jsonString)
}
if (!response.status.isSuccess()) {
val text = response.bodyAsText()
// Log.e(TAG, "发送消息失败:URL:${urlString} HTTP ${response.status}, $text")
// 优化处理百度API的202状态码
when {
response.status.isSuccess() -> {
// 2xx状态码都视为成功
Log.d(TAG, "$serviceidString 消息发送成功: HTTP ${response.status}")
}
response.status == HttpStatusCode.Accepted -> {
// 202 Accepted - 百度API异步处理中,这是正常状态
Log.d(TAG, "$serviceidString 消息已被接受,正在异步处理: HTTP ${response.status}")
}
else -> {
// 其他非成功状态码才记录为错误
val text = response.bodyAsText()
Log.w(TAG, "$serviceidString 发送消息收到非成功状态码: URL:${messageEndpoint} HTTP ${response.status}, $text")
// 根据具体状态码决定是否抛出异常
if (response.status.value >= 400) {
// 4xx和5xx错误才抛出异常
throw Exception("HTTP ${response.status}: $text")
}
}
}
} catch (e: Exception) {
Log.e(TAG, "发送消息异常: ${e.message}", e)
Log.e(TAG, "$serviceidString 发送消息异常: ${e.message}", e)
_onError(e)
throw e
}
@ -306,7 +325,7 @@ class CustomSseClientTransport(
*/
override suspend fun close() {
if (!initialized.get()) {
Log.e(TAG, "关闭失败: 传输层未初始化")
Log.e(TAG, "$serviceidString 关闭失败: 传输层未初始化")
error("CustomSseClientTransport is not initialized!")
}
@ -317,6 +336,6 @@ class CustomSseClientTransport(
job?.cancelAndJoin()
connectionMonitorJob?.cancelAndJoin()
Log.d(TAG, "CustomSseClientTransport已关闭")
Log.d(TAG, "$serviceidString CustomSseClientTransport已关闭")
}
}

48
local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/MCPClient.kt

@ -68,55 +68,53 @@ class MCPClient(private val context: Context? = null) : AutoCloseable {
Log.w(TAG, "Failed to initialize system functions", e)
}
}
/**
* 初始化音乐函数
*/
private fun initializeQQMusiceFunctions() {
try {
val handler = QQMusicSystemFunctionHandler(context)
handler.registerAllFunctions(this)
} catch (e: Exception) {
Log.w(TAG, "Failed to initialize system functions", e)
}
}
/**
* 连接到SSE服务器
* 直接接收完整的JSON配置字符串
*
*
* @param mcpConfigJson 包含mcpServers字段的JSON配置字符串
* @return 是否连接成功
*/
suspend fun connectToSSE(mcpConfigJson: String): Boolean {
// 清除现有连接
closeAllConnections()
return try {
val config = JSONObject(mcpConfigJson)
val mcpServers = config.optJSONObject("mcpServers") ?: return false
var connectedCount = 0
val serverIds = mcpServers.keys()
while (serverIds.hasNext()) {
val serverId = serverIds.next()
val serverConfig = mcpServers.optJSONObject(serverId) ?: continue
val url = serverConfig.optString("url", "")
val tools = serverConfig.optString("tools", "")
if (url.isEmpty()) continue
val subClient = MCPSubClient(serverId, url,tools, sharedHttpClient)
if (subClient.connect()) {
subClients[serverId] = subClient
connectedCount++
} else {
Log.w(TAG, "Failed to connect to MCP server: $serverId")
Log.d(TAG, "开始连接MCP服务器: $serverId")
val subClient = MCPSubClient(serverId, url, tools, sharedHttpClient)
// 使用协程并发连接,但每个服务器都会进行重试
try {
if (subClient.connect()) {
subClients[serverId] = subClient
connectedCount++
Log.d(TAG, "MCP服务器连接成功: $serverId")
} else {
Log.w(TAG, "MCP服务器连接失败: $serverId")
}
} catch (e: Exception) {
Log.e(TAG, "MCP服务器连接异常: $serverId, ${e.message}", e)
}
}
isConnectedFlag = connectedCount > 0
Log.d(TAG, "MCP连接完成,成功连接 $connectedCount 个服务器")
connectedCount > 0
} catch (e: Exception) {
Log.e(TAG, "Failed to connect to SSE", e)
false

175
local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/MCPSubClient.kt

@ -33,7 +33,7 @@ class MCPSubClient(
private var heartbeatJob: Job? = null
private val heartbeatInterval = 30000L // 30秒
// 重连配置
private val maxRetryAttempts = 3
private val maxRetryAttempts = 10
private val initialReconnectDelay = 1000L // 初始重连延迟1秒
private val maxReconnectDelay = 30000L // 最大重连延迟30秒
private var currentReconnectDelay = initialReconnectDelay
@ -45,95 +45,122 @@ class MCPSubClient(
private var isConnected = false
private var availableTools = mutableListOf<Tool>()
private var transport: CustomSseClientTransport? = null
/**
* 连接到MCP服务器
*/
suspend fun connect(): Boolean = connectionMutex.withLock {
// Log.e(TAG, "liwei---------Mcp [$serverId] 连接 1")
if (isConnected) return true
Log.e(TAG, "[$serverId] 开始连接mcp服务器: $serverUrl")
return try {
// 创建MCP客户端实例
val client = Client(
clientInfo = Implementation(
name = "deep-voice-chat-api",
version = "1.0.0"
// 重试连接逻辑
for (attempt in 0 until maxRetryAttempts) {
try {
Log.d(TAG, "[$serverId] 连接尝试 ${attempt + 1}/$maxRetryAttempts")
// 创建MCP客户端实例
val client = Client(
clientInfo = Implementation(
name = "deep-voice-chat-api",
version = "1.0.0"
)
)
)
// Log.e(TAG, "liwei---------Mcp [$serverId] 连接 2")
// 根据URL类型选择传输方式
val newTransport = when {
serverUrl.startsWith("http://") || serverUrl.startsWith("https://") -> {
// SSE传输 - 使用自定义的CustomSseClientTransport
val mcpHttpClient = httpClient ?: createMcpHttpClient()
CustomSseClientTransport(
client = mcpHttpClient,
urlString = serverUrl,
onConnectionLost = {
// 连接断开回调
Log.w(TAG, "[$serverId] 检测到连接断开")
scope.launch {
handleConnectionLost()
// 根据URL类型选择传输方式
val newTransport = when {
serverUrl.startsWith("http://") || serverUrl.startsWith("https://") -> {
// SSE传输 - 使用自定义的CustomSseClientTransport
val mcpHttpClient = httpClient ?: createMcpHttpClient()
CustomSseClientTransport(
client = mcpHttpClient,
serviceidString = serverId,
urlString = serverUrl,
onConnectionLost = {
// 连接断开回调
Log.w(TAG, "[$serverId] 检测到连接断开")
scope.launch {
handleConnectionLost()
}
}
}
)
}
else -> {
Log.e(TAG, "[$serverId] 不支持的服务器URL格式: $serverUrl")
return false
)
}
else -> {
Log.e(TAG, "[$serverId] 不支持的服务器URL格式: $serverUrl")
return false
}
}
}
// Log.e(TAG, "liwei---------Mcp [$serverId] 连接 3")
transport = newTransport
try {
// 连接到服务器
withTimeout(10000) { // 10秒超时
client.connect(newTransport)
transport = newTransport
// 连接到服务器 - 增加超时时间
try {
Log.d(TAG, "[$serverId] 尝试建立连接 ${transport?.urlString}")
withTimeout(30000) { // 30秒超时
client.connect(newTransport)
}
Log.d(TAG, "[$serverId] 连接建立成功")
} catch (e: TimeoutCancellationException) {
Log.w(TAG, "[$serverId] 连接超时 (尝试 ${attempt + 1}/$maxRetryAttempts)")
if (attempt < maxRetryAttempts - 1) {
delay(currentReconnectDelay)
currentReconnectDelay = (currentReconnectDelay * 2).coerceAtMost(maxReconnectDelay)
continue // 继续下一次重试
} else {
Log.e(TAG, "[$serverId] 所有连接尝试都超时,连接失败")
return false
}
} catch (e: Exception) {
Log.w(TAG, "[$serverId] 连接异常 (尝试 ${attempt + 1}/$maxRetryAttempts): ${e.message}")
if (attempt < maxRetryAttempts - 1) {
delay(currentReconnectDelay)
currentReconnectDelay = (currentReconnectDelay * 2).coerceAtMost(maxReconnectDelay)
continue // 继续下一次重试
} else {
Log.e(TAG, "[$serverId] 所有连接尝试都失败: ${e.message}", e)
return false
}
}
// Log.e(TAG, "liwei---------Mcp [$serverId] 连接 4")
} catch (e: TimeoutCancellationException) {
Log.e(TAG, "liwei---------Mcp [$serverId] 连接超时")
return false
} catch (e: Exception) {
Log.e(TAG, "liwei---------Mcp [$serverId] 连接异常: ${e.message}", e)
return false
}
// 获取可用工具列表
try {
val toolsResult = client.listTools()
// Log.e(TAG, "liwei---------Mcp [$serverId] 连接 5")
if (toolsResult != null) {
availableTools.clear()
val filtered = toolsResult.tools.filter { tool ->
filterTools.isEmpty() || filterTools.contains(tool.name)
// 获取可用工具列表
try {
val toolsResult = client.listTools()
if (toolsResult != null) {
availableTools.clear()
val allToolNames = toolsResult.tools.map { it.name }
Log.d(TAG, "$serverId:所有工具名称列表: $allToolNames")
val filtered = toolsResult.tools.filter { tool ->
filterTools.isEmpty() || filterTools.contains(tool.name)
}
val filteredToolNames = filtered.map { it.name }
Log.d(TAG, "$serverId: 过滤后的工具: $filteredToolNames")
availableTools.addAll(filtered)
}
Log.w(TAG, "[$serverId] [${filterTools}] 获取工具列表: ${filtered} 原始列表:${toolsResult.tools}")
availableTools.addAll(filtered)
// Log.e(TAG, "liwei---------Mcp [$serverId] 连接 6")
} catch (e: Exception) {
Log.w(TAG, "[$serverId] 获取工具列表失败: ${e.message}")
// 即使获取工具失败,连接也可能是成功的
}
mcpClient = client
isConnected = true
retryCount = 0
currentReconnectDelay = initialReconnectDelay
Log.e(TAG, "[$serverId] 连接mcp服务器成功: $serverUrl")
return true
} catch (e: Exception) {
Log.w(TAG, "[$serverId] 获取工具列表失败: ${e.message}")
// 即使获取工具失败,连接也可能是成功的
Log.w(TAG, "[$serverId] 连接尝试 ${attempt + 1} 失败: ${e.message}")
if (attempt < maxRetryAttempts - 1) {
delay(currentReconnectDelay)
currentReconnectDelay = (currentReconnectDelay * 2).coerceAtMost(maxReconnectDelay)
}
}
// Log.e(TAG, "liwei---------Mcp [$serverId] 连接 7")
mcpClient = client
isConnected = true
retryCount = 0
currentReconnectDelay = initialReconnectDelay
// Log.e(TAG, "liwei---------Mcp [$serverId] 连接 8")
// 启动心跳检测
// startHeartbeat()
Log.e(TAG, "[$serverId] 连接mcp服务器成功: $serverUrl")
true
} catch (e: Exception) {
Log.e(TAG, "[$serverId] MCP连接失败: ${e.message}", e)
false
}
Log.e(TAG, "[$serverId] 所有重试尝试都失败,MCP连接失败")
return false
}
/**
* 检查是否包含指定工具
*/

66
local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/SystemFunctionHandler.kt

@ -19,7 +19,6 @@ import android.telephony.SmsManager
import android.util.Log
import androidx.core.content.ContextCompat
import com.yunqiinnovation.location_service.LocationService
import androidx.core.content.ContextCompat.RECEIVER_NOT_EXPORTED
import org.json.JSONObject
import java.text.SimpleDateFormat
import java.util.*
@ -1134,7 +1133,7 @@ private class GetCurrentTimeHandler : FunctionHandler {
* 获取当前位置处理器
*/
private class GetCurrentLocationHandler(private val context: Context?) : FunctionHandler {
override suspend fun handle(arguments: Map<String, Any>): String {
if (context == null) {
return JSONObject().apply {
@ -1144,75 +1143,28 @@ private class GetCurrentLocationHandler(private val context: Context?) : Functio
})
}.toString()
}
val handler = context.let { SystemFunctionHandler(it) }
// 检查位置权限
if (!handler.checkPermissionAndGuide(Manifest.permission.ACCESS_FINE_LOCATION, "位置信息", "获取位置")) {
return JSONObject().apply {
put("error", JSONObject().apply {
put("code", "permission_denied")
put("message", "需要位置权限才能获取当前位置,请在权限管理中开启位置权限")
})
}.toString()
}
// 获取缓存的位置数据
val cachedLocation = LocationService.getCachedLocationData()
// 异步触发最新定位获取(不等待结果)
LocationService.requestLocationInBackground(object : LocationService.LocationCallback {
override fun onLocationSuccess(latitude: Double, longitude: Double, address: String) {
Log.d(SystemFunctionHandler.TAG, "后台定位成功,缓存已更新: $latitude, $longitude, $address")
}
override fun onLocationError(errorCode: Int, errorMessage: String) {
Log.w(SystemFunctionHandler.TAG, "后台定位失败: $errorCode - $errorMessage")
}
})
// 立即返回缓存数据(如果存在)
if (cachedLocation != null) {
val cacheAge = System.currentTimeMillis() - cachedLocation.timestamp
val cacheAgeMinutes = cacheAge / (60 * 1000)
return JSONObject().apply {
put("result", JSONObject().apply {
put("latitude", cachedLocation.latitude)
put("longitude", cachedLocation.longitude)
put("address", cachedLocation.address)
put("source", "cache")
put("timestamp", cachedLocation.timestamp)
put("cacheAgeMinutes", cacheAgeMinutes)
put("note", "正在后台更新位置信息")
})
}.toString()
}
// 如果没有缓存,则同步获取一次
return try {
kotlinx.coroutines.suspendCancellableCoroutine { continuation ->
LocationService.requestLocation(object : LocationService.LocationCallback {
override fun onLocationSuccess(latitude: Double, longitude: Double, address: String) {
Log.d(SystemFunctionHandler.TAG, "首次定位成功: $latitude, $longitude, $address")
override fun onLocationSuccess(latitude: Double, longitude: Double) {
Log.d(SystemFunctionHandler.TAG, "首次定位成功: $latitude, $longitude")
val result = JSONObject().apply {
put("result", JSONObject().apply {
put("latitude", latitude)
put("longitude", longitude)
put("address", address)
put("source", "gps")
put("latitude", latitude)
put("timestamp", System.currentTimeMillis())
put("note", "首次获取位置信息")
})
}.toString()
continuation.resumeWith(Result.success(result))
}
override fun onLocationError(errorCode: Int, errorMessage: String) {
Log.e(SystemFunctionHandler.TAG, "首次定位失败: $errorCode - $errorMessage")
val result = JSONObject().apply {
put("error", JSONObject().apply {
put("code", "location_error")
@ -1220,7 +1172,7 @@ private class GetCurrentLocationHandler(private val context: Context?) : Functio
put("errorCode", errorCode)
})
}.toString()
continuation.resumeWith(Result.success(result))
}
})

4
local_plugins/chat_api/ios/chat_api/Sources/chat_api/SystemFunctionHandler.swift

@ -461,6 +461,7 @@ private class GetCurrentLocationHandler: FunctionHandler {
// 解析位置数据
guard let latStr = locationInfo["user_lat"] as? String,
let lngStr = locationInfo["user_lng"] as? String,
let latitude = Double(latStr),
let longitude = Double(lngStr) else {
print("[GetCurrentLocationHandler] 位置数据格式错误")
@ -489,8 +490,9 @@ private class GetCurrentLocationHandler: FunctionHandler {
let result: [String: Any] = [
"success": true,
"data": [
"latitude": latitude,
"longitude": longitude,
"latitude": latitude,
"location": "\(longitude),\(latitude)",
"timestamp": Date().timeIntervalSince1970 * 1000
]
]

7
local_plugins/location_service/android/src/main/AndroidManifest.xml

@ -1,7 +1,12 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.yunqiinnovation.location_service">
<!-- 必须:精确定位权限(包含GPS) -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<!-- 可选:粗略定位权限(网络定位兜底) -->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<!-- Android 10+ 后台定位需额外声明(若需要后台持续定位) -->
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
<!-- 辅助:获取Wi-Fi/基站信息提升网络定位精度 -->
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
</manifest>

424
local_plugins/location_service/android/src/main/kotlin/com/yunqiinnovation/location_service/LocationService.kt

@ -16,30 +16,34 @@ import io.flutter.plugin.common.MethodChannel
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.json.JSONObject
object LocationService {
private const val TAG = "LocationService"
private const val PERMISSION_REQUEST_CODE = 1001
// 只接受高精度定位
private const val HIGH_ACCURACY_THRESHOLD = 50.0 // 只接受50米以内的高精度定位
private const val LOCATION_TIMEOUT_MS = 60000L // 增加到60秒超时,给更多时间获取高精度定位
private const val CACHE_PREFS_NAME = "location_cache"
private var appcontext: Context? = null
private var locationClient: AMapLocationClient? = null
private var initialized = false
private var permissionRetryHandler: Handler? = null
private var permissionRetryRunnable: Runnable? = null
// 存储当前的定位请求回调(Flutter用)
private var currentLocationCallback: MethodChannel.Result? = null
// 存储权限请求回调(Flutter用)
private var permissionRequestCallback: MethodChannel.Result? = null
// 存储服务间调用的回调
private val serviceCallbacks = mutableListOf<LocationCallback>()
private var isHighAccuracyMode = true
private var cachedLocationData: LocationData? = null
private var locationTimeoutHandler: Handler? = null
private var locationStartTime: Long = 0
/**
* 定位回调接口,供其他Android服务使用
*/
// 修正接口,包含address参数
interface LocationCallback {
fun onLocationSuccess(latitude: Double, longitude: Double, address: String)
fun onLocationSuccess(latitude: Double, longitude: Double)
fun onLocationError(errorCode: Int, errorMessage: String)
}
@ -65,147 +69,165 @@ object LocationService {
}
fun initialize(context: Context) {
if (initialized) return
appcontext = context
if (!checkPermissions(context)) {
Log.w(TAG, "没有定位权限,等待用户授权中...")
startPermissionMonitor()
if (initialized) {
Log.d(TAG, "定位服务已初始化")
return
}
appcontext = context.applicationContext
try {
AMapLocationClient.updatePrivacyShow(context, true, true)
AMapLocationClient.updatePrivacyAgree(context, true)
locationClient = AMapLocationClient(context.applicationContext)
val option = AMapLocationClientOption().apply {
locationMode = AMapLocationClientOption.AMapLocationMode.Hight_Accuracy
isOnceLocation = true // 单次定位
isNeedAddress = true // 需要地址信息
httpTimeOut = 20000 // 设置超时时间
}
// 优化后的定位参数配置
val option = AMapLocationClientOption()
// 高精度定位模式(融合GPS、网络、传感器)
option.setLocationMode(AMapLocationClientOption.AMapLocationMode.Hight_Accuracy)
// 设置定位场景
option.setLocationPurpose(AMapLocationClientOption.AMapLocationPurpose.SignIn) // 改为签到模式,更适合高精度定位
// 定位间隔(首次定位建议设置较短间隔)
option.setInterval(2000) // 改为2秒,更快响应
// 超时时间
option.setHttpTimeOut(30000) // 30秒足够
// 启用所有辅助定位技术
option.setGpsFirst(false) // 不强制GPS优先,让SDK智能选择
option.setWifiScan(true) // 启用WiFi辅助定位
option.setSensorEnable(true) // 启用传感器辅助
option.setLocationCacheEnable(true) // 启用位置缓存
// 单次定位设置
option.setOnceLocation(true) // 改为单次定位,避免持续定位的干扰
option.setOnceLocationLatest(true) // 获取最新的定位结果
// 其他设置
option.setMockEnable(false) // 禁用模拟定位
option.setNeedAddress(true) // 启用地址解析,有助于定位精度
locationClient?.setLocationOption(option)
// 设置定位监听器
locationClient?.setLocationListener { location ->
handleLocationResult(location)
}
initialized = true
Log.d(TAG, "定位服务初始化完成")
//首次请求一次定位
requestLocation(object : LocationCallback {
override fun onLocationSuccess(latitude: Double, longitude: Double) {
}
override fun onLocationError(errorCode: Int, errorMessage: String) {
}
})
Log.d(TAG, "定位服务初始化完成 - 优化高精度模式")
} catch (e: Exception) {
Log.e(TAG, "高德定位初始化失败: ${e.message}", e)
}
}
// 在LocationService object中添加缓存相关的属性和方法
private var cachedLocationData: LocationData? = null
private const val CACHE_PREFS_NAME = "location_cache"
/**
* 后台异步获取位置信息(不阻塞调用者)
* @param callback 定位结果回调
*/
fun requestLocationInBackground(callback: LocationCallback) {
fun requestLocation(callback: LocationCallback) {
val ctx = appcontext
if (ctx == null) {
callback.onLocationError(-1, "定位服务未初始化")
return
}
if (!checkPermissions(ctx)) {
callback.onLocationError(-2, "没有定位权限")
return
}
if (!initialized) {
callback.onLocationError(-3, "定位服务未初始化")
return
}
// 优先返回内存中的缓存
if (cachedLocationData != null) {
callback.onLocationSuccess(cachedLocationData!!.latitude,cachedLocationData!!.longitude)
}else{
// 添加到回调列表
serviceCallbacks.add(callback)
}
// 记录定位开始时间
locationStartTime = System.currentTimeMillis()
// 启动超时计时器
startLocationTimeout()
// 使用协程在后台执行
CoroutineScope(Dispatchers.IO).launch {
try {
// 添加到回调列表
serviceCallbacks.add(callback)
// 如果没有正在进行的定位请求,开始定位
if (currentLocationCallback == null && serviceCallbacks.size == 1) {
locationClient?.startLocation()
}
locationClient?.startLocation()
} catch (e: Exception) {
stopLocationTimeout()
callback.onLocationError(-4, "启动后台定位失败: ${e.message}")
}
}
}
// 修改handleLocationResult方法,添加缓存逻辑
// 统一的handleLocationResult方法(整合优化逻辑)
private fun handleLocationResult(location: AMapLocation?) {
// 处理Flutter回调
val flutterCallback = currentLocationCallback
currentLocationCallback = null
if (location != null && location.errorCode == 0) {
Log.d(TAG, "定位成功: ${location.latitude}, ${location.longitude}")
// 缓存位置数据
val locationData = LocationData(
latitude = location.latitude,
longitude = location.longitude,
address = location.address ?: "未知地址",
timestamp = System.currentTimeMillis()
)
cachedLocationData = locationData
saveCachedLocationToPrefs(locationData)
// 通知Flutter
if (flutterCallback != null) {
val result = mapOf(
"user_location" to (location.address ?: "未知地址"),
"user_lat" to location.latitude.toString(),
"user_lng" to location.longitude.toString()
)
flutterCallback.success(result)
}
if (location == null) {
Log.e(TAG, "定位结果为空")
return
}
Log.d(TAG, "定位结果: 来源=${location.provider}, 精度=${location.accuracy}米, 坐标=(${location.latitude}, ${location.longitude})")
if (location.errorCode == AMapLocation.LOCATION_SUCCESS) {
val accuracy = location.accuracy
// Log.d(TAG, "GPS定位成功: 精度=${accuracy}米")
// 通知其他Android服务
serviceCallbacks.forEach { callback ->
// 只接受高精度GPS定位
// if (accuracy <= HIGH_ACCURACY_THRESHOLD) {
Log.d(TAG, "接受高精度GPS定位: ${accuracy}米")
// 停止超时计时器
stopLocationTimeout()
// 停止定位
try {
callback.onLocationSuccess(
location.latitude,
location.longitude,
location.address ?: "未知地址"
)
locationClient?.stopLocation()
} catch (e: Exception) {
Log.e(TAG, "服务回调失败: ${e.message}", e)
Log.e(TAG, "停止定位失败: ${e.message}", e)
}
}
} else {
Log.w(TAG, "定位失败: ${location?.errorCode} - ${location?.errorInfo}")
// 通知Flutter
if (flutterCallback != null) {
flutterCallback.error("LOCATION_ERROR", "定位失败: ${location?.errorInfo ?: "未知错误"}", null)
}
// 通知其他Android服务
serviceCallbacks.forEach { callback ->
try {
callback.onLocationError(
location?.errorCode ?: -1,
location?.errorInfo ?: "未知错误"
)
} catch (e: Exception) {
Log.e(TAG, "服务回调失败: ${e.message}", e)
// 缓存位置数据
val locationData = LocationData(
latitude = location.latitude,
longitude = location.longitude,
timestamp = System.currentTimeMillis()
)
cachedLocationData = locationData
// 处理成功的定位结果
serviceCallbacks.forEach { callback ->
try {
callback.onLocationSuccess(
location.latitude,
location.longitude,
)
} catch (e: Exception) {
Log.e(TAG, "服务回调失败: ${e.message}", e)
}
}
}
// 清空服务回调列表(单次定位)
serviceCallbacks.clear()
// } else {
// Log.w(TAG, "GPS精度不足(${accuracy}米),继续等待更高精度定位")
// }
} else {
Log.e(TAG, "GPS定位失败: ${location.errorCode} - ${location.errorInfo}")
}
// 清空服务回调列表(单次定位)
serviceCallbacks.clear()
}
private fun startPermissionMonitor() {
@ -315,182 +337,56 @@ object LocationService {
}
}
/**
* 处理权限请求结果(Flutter用)
*/
fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
): Boolean {
if (requestCode != PERMISSION_REQUEST_CODE) {
return false
}
val callback = permissionRequestCallback
permissionRequestCallback = null
if (callback == null) {
return true
}
val ctx = appcontext
if (ctx == null) {
callback.error("NO_CONTEXT", "上下文丢失", null)
return true
}
val hasPermission = checkPermissions(ctx)
if (hasPermission) {
// 权限获取成功,重新初始化定位服务
if (!initialized) {
initialize(ctx)
}
callback.success(mapOf("granted" to true))
} else {
callback.success(mapOf("granted" to false))
}
return true
}
/**
* 按需获取当前位置(Flutter用)
* 位置数据类
*/
fun getCurrentLocation(result: MethodChannel.Result) {
val ctx = appcontext
if (ctx == null) {
result.error("NOT_INITIALIZED", "定位服务未初始化", null)
return
}
if (!checkPermissions(ctx)) {
result.error("NO_PERMISSION", "需要定位权限", null)
return
}
if (!initialized) {
result.error("NOT_INITIALIZED", "定位服务未初始化", null)
return
}
if (currentLocationCallback != null) {
result.error("LOCATION_IN_PROGRESS", "正在定位中,请稍后再试", null)
return
}
// 保存回调,开始定位
currentLocationCallback = result
locationClient?.startLocation()
}
data class LocationData(
val latitude: Double,
val longitude: Double,
val timestamp: Long
)
// ========== 以下是供其他Android服务使用的接口 ==========
/**
* 检查是否有定位权限(供其他服务使用)
*/
fun hasLocationPermission(): Boolean {
val ctx = appcontext ?: return false
return checkPermissions(ctx)
}
/**
* 获取当前位置(供其他Android服务使用)
* @param callback 定位结果回调
*/
fun requestLocation(callback: LocationCallback) {
val ctx = appcontext
if (ctx == null) {
callback.onLocationError(-1, "定位服务未初始化")
return
}
if (!checkPermissions(ctx)) {
callback.onLocationError(-2, "没有定位权限")
return
}
if (!initialized) {
callback.onLocationError(-3, "定位服务未初始化")
return
}
// 添加到回调列表
serviceCallbacks.add(callback)
// 如果没有正在进行的定位请求,开始定位
if (currentLocationCallback == null && serviceCallbacks.size == 1) {
locationClient?.startLocation()
}
}
/**
* 同步获取缓存的位置信息(供其他服务使用)
* 注意:这个方法返回的可能是过期的位置信息
* 启动定位超时计时器
*/
fun getCachedLocationData(): LocationData? {
// 优先返回内存中的缓存
if (cachedLocationData != null) {
return cachedLocationData
}
private fun startLocationTimeout() {
// 如果已经有超时计时器在运行,先停止它
stopLocationTimeout()
// 从SharedPreferences中读取缓存
val ctx = appcontext ?: return null
return loadCachedLocationFromPrefs(ctx)
}
private fun saveCachedLocationToPrefs(locationData: LocationData) {
val ctx = appcontext ?: return
try {
val prefs = ctx.getSharedPreferences(CACHE_PREFS_NAME, Context.MODE_PRIVATE)
prefs.edit().apply {
putString("latitude", locationData.latitude.toString())
putString("longitude", locationData.longitude.toString())
putString("address", locationData.address)
putLong("timestamp", locationData.timestamp)
apply()
locationTimeoutHandler = Handler(Looper.getMainLooper())
locationTimeoutHandler?.postDelayed({
Log.w(TAG, "定位超时,停止定位请求")
// 停止定位
try {
locationClient?.stopLocation()
} catch (e: Exception) {
Log.e(TAG, "停止定位失败: ${e.message}", e)
}
} catch (e: Exception) {
Log.e(TAG, "保存位置缓存失败: ${e.message}", e)
}
}
private fun loadCachedLocationFromPrefs(context: Context): LocationData? {
return try {
val prefs = context.getSharedPreferences(CACHE_PREFS_NAME, Context.MODE_PRIVATE)
val latitude = prefs.getString("latitude", null)?.toDoubleOrNull() ?: return null
val longitude = prefs.getString("longitude", null)?.toDoubleOrNull() ?: return null
val address = prefs.getString("address", null) ?: return null
val timestamp = prefs.getLong("timestamp", 0L)
if (timestamp == 0L) return null
// 通知所有等待的回调超时错误
serviceCallbacks.forEach { callback ->
try {
callback.onLocationError(-5, "定位超时,未能在${LOCATION_TIMEOUT_MS/1000}秒内获取到高精度位置")
} catch (e: Exception) {
Log.e(TAG, "超时回调失败: ${e.message}", e)
}
}
LocationData(latitude, longitude, address, timestamp)
} catch (e: Exception) {
Log.e(TAG, "读取位置缓存失败: ${e.message}", e)
null
}
// 清空回调列表
serviceCallbacks.clear()
// 清理超时处理器
locationTimeoutHandler = null
}, LOCATION_TIMEOUT_MS)
}
/**
* 位置数据类
* 停止定位超时计时器
*/
data class LocationData(
val latitude: Double,
val longitude: Double,
val address: String,
val timestamp: Long
)
fun stop() {
locationClient?.stopLocation()
locationClient?.onDestroy()
locationClient = null
currentLocationCallback = null
permissionRequestCallback = null
serviceCallbacks.clear()
initialized = false
stopPermissionMonitor()
private fun stopLocationTimeout() {
locationTimeoutHandler?.removeCallbacksAndMessages(null)
locationTimeoutHandler = null
}
}

38
local_plugins/location_service/android/src/main/kotlin/com/yunqiinnovation/location_service/LocationServicePlugin.kt

@ -12,11 +12,10 @@ import io.flutter.plugin.common.MethodChannel.Result
import io.flutter.plugin.common.PluginRegistry
/** LocationServicePlugin */
class LocationServicePlugin: FlutterPlugin, MethodCallHandler, ActivityAware, PluginRegistry.RequestPermissionsResultListener {
class LocationServicePlugin: FlutterPlugin, MethodCallHandler {
private val TAG = "LocationServicePlugin"
private lateinit var context: Context
private lateinit var channel : MethodChannel
private var activityBinding: ActivityPluginBinding? = null
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
context = flutterPluginBinding.applicationContext
@ -25,43 +24,12 @@ class LocationServicePlugin: FlutterPlugin, MethodCallHandler, ActivityAware, Pl
}
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
// Log.d(TAG, "Method call: ${call.method}")
when (call.method) {
"location" -> LocationService.getCurrentLocation(result)
"requestPermission" -> LocationService.requestLocationPermission(activityBinding?.activity, result)
"checkPermission" -> LocationService.checkLocationPermission(context, result)
else -> result.notImplemented()
}
Log.d(TAG, "Method call: ${call.method}")
}
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
channel.setMethodCallHandler(null)
}
override fun onAttachedToActivity(binding: ActivityPluginBinding) {
activityBinding = binding
binding.addRequestPermissionsResultListener(this)
LocationService.initialize(context)
}
override fun onDetachedFromActivityForConfigChanges() {
onDetachedFromActivity()
}
override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {
onAttachedToActivity(binding)
}
override fun onDetachedFromActivity() {
activityBinding?.removeRequestPermissionsResultListener(this)
activityBinding = null
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
): Boolean {
return LocationService.onRequestPermissionsResult(requestCode, permissions, grantResults)
}
}

499
local_plugins/location_service/android/src/main/kotlin/com/yunqiinnovation/location_service/NativeLocationService.kt

@ -0,0 +1,499 @@
package com.yunqiinnovation.location_service
import android.Manifest
import android.app.Activity
import android.content.Context
import android.content.pm.PackageManager
import android.location.*
import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.util.Log
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.util.*
object NativeLocationService {
private const val TAG = "NativeLocationService"
private const val PERMISSION_REQUEST_CODE = 1002
// 高精度定位阈值
private const val HIGH_ACCURACY_THRESHOLD = 50.0f // 只接受50米以内的高精度定位
private const val LOCATION_TIMEOUT_MS = 60000L // 60秒超时
private const val MIN_TIME_BETWEEN_UPDATES = 5000L // 5秒最小更新间隔
private const val MIN_DISTANCE_CHANGE_FOR_UPDATES = 10f // 10米最小距离变化
private var appContext: Context? = null
private var locationManager: LocationManager? = null
private var initialized = false
private val serviceCallbacks = mutableListOf<LocationCallback>()
private var cachedLocationData: LocationData? = null
private var locationTimeoutHandler: Handler? = null
private var locationStartTime: Long = 0
private var gpsLocationListener: LocationListener? = null
private var networkLocationListener: LocationListener? = null
// 定位回调接口
interface LocationCallback {
fun onLocationSuccess(latitude: Double, longitude: Double)
fun onLocationError(errorCode: Int, errorMessage: String)
}
/**
* 初始化原生定位服务
*/
fun initialize(context: Context) {
if (initialized) {
Log.d(TAG, "原生定位服务已初始化")
return
}
appContext = context.applicationContext
locationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
if (locationManager == null) {
Log.e(TAG, "无法获取LocationManager")
return
}
initialized = true
Log.d(TAG, "原生定位服务初始化完成")
}
/**
* 检查GPS是否可用
*/
private fun isGpsEnabled(): Boolean {
return locationManager?.isProviderEnabled(LocationManager.GPS_PROVIDER) ?: false
}
/**
* 检查网络定位是否可用
*/
private fun isNetworkEnabled(): Boolean {
return locationManager?.isProviderEnabled(LocationManager.NETWORK_PROVIDER) ?: false
}
/**
* 获取位置信息(异步)
*/
fun requestLocation(callback: LocationCallback) {
val ctx = appContext
if (ctx == null) {
callback.onLocationError(-1, "定位服务未初始化")
return
}
if (!checkPermissions(ctx)) {
callback.onLocationError(-2, "没有定位权限")
return
}
if (!initialized) {
callback.onLocationError(-3, "定位服务未初始化")
return
}
// 检查定位服务是否可用
if (!isGpsEnabled() && !isNetworkEnabled()) {
callback.onLocationError(-4, "GPS和网络定位都不可用,请检查设置")
return
}
// 优先返回缓存的位置
cachedLocationData?.let { cached ->
val currentTime = System.currentTimeMillis()
// 如果缓存时间在5分钟内,直接返回
if (currentTime - cached.timestamp < 5 * 60 * 1000) {
callback.onLocationSuccess(cached.latitude, cached.longitude)
return
}
}
// 添加到回调列表
serviceCallbacks.add(callback)
// 记录定位开始时间
locationStartTime = System.currentTimeMillis()
// 启动超时计时器
startLocationTimeout()
// 使用协程在后台执行定位
CoroutineScope(Dispatchers.IO).launch {
try {
startLocationUpdates()
} catch (e: Exception) {
stopLocationTimeout()
callback.onLocationError(-5, "启动定位失败: ${e.message}")
}
}
}
/**
* 开始位置更新
*/
private fun startLocationUpdates() {
val ctx = appContext ?: return
val locManager = locationManager ?: return
// 检查权限
if (ActivityCompat.checkSelfPermission(
ctx,
Manifest.permission.ACCESS_FINE_LOCATION
) != PackageManager.PERMISSION_GRANTED &&
ActivityCompat.checkSelfPermission(
ctx,
Manifest.permission.ACCESS_COARSE_LOCATION
) != PackageManager.PERMISSION_GRANTED
) {
Log.e(TAG, "没有定位权限")
return
}
Log.d(TAG, "=== 启动混合定位模式 ===")
Log.d(TAG, "网络定位可用: ${isNetworkEnabled()}")
Log.d(TAG, "GPS定位可用: ${isGpsEnabled()}")
// 优先启动网络定位
if (isNetworkEnabled()) {
Log.d(TAG, "🌐 启动网络定位(优先)")
networkLocationListener = object : LocationListener {
override fun onLocationChanged(location: Location) {
Log.d(TAG, "🌐 网络定位更新: 精度=${location.accuracy}米")
handleLocationResult(location, "NETWORK")
}
override fun onProviderEnabled(provider: String) {
Log.d(TAG, "网络提供者已启用: $provider")
}
override fun onProviderDisabled(provider: String) {
Log.d(TAG, "网络提供者已禁用: $provider")
}
override fun onStatusChanged(provider: String?, status: Int, extras: Bundle?) {
Log.d(TAG, "网络状态变化: $provider, status: $status")
}
}
locManager.requestLocationUpdates(
LocationManager.NETWORK_PROVIDER,
MIN_TIME_BETWEEN_UPDATES,
MIN_DISTANCE_CHANGE_FOR_UPDATES,
networkLocationListener!!,
Looper.getMainLooper()
)
Log.d(TAG, "✅ 网络定位已启动")
}
// GPS作为补充定位
if (isGpsEnabled()) {
Log.d(TAG, "🛰️ 启动GPS定位(补充)")
gpsLocationListener = object : LocationListener {
override fun onLocationChanged(location: Location) {
Log.d(TAG, "🛰️ GPS定位更新: 精度=${location.accuracy}米")
handleLocationResult(location, "GPS")
}
override fun onProviderEnabled(provider: String) {
Log.d(TAG, "GPS提供者已启用: $provider")
}
override fun onProviderDisabled(provider: String) {
Log.d(TAG, "GPS提供者已禁用: $provider")
}
override fun onStatusChanged(provider: String?, status: Int, extras: Bundle?) {
when (status) {
LocationProvider.AVAILABLE -> Log.d(TAG, "GPS状态: 可用")
LocationProvider.OUT_OF_SERVICE -> Log.d(TAG, "GPS状态: 服务中断")
LocationProvider.TEMPORARILY_UNAVAILABLE -> Log.d(TAG, "GPS状态: 暂时不可用")
}
}
}
locManager.requestLocationUpdates(
LocationManager.GPS_PROVIDER,
MIN_TIME_BETWEEN_UPDATES,
MIN_DISTANCE_CHANGE_FOR_UPDATES,
gpsLocationListener!!,
Looper.getMainLooper()
)
Log.d(TAG, "✅ GPS定位已启动")
} else {
Log.w(TAG, "GPS未启用,仅使用网络定位")
}
if (!isNetworkEnabled() && !isGpsEnabled()) {
Log.e(TAG, "❌ 所有定位服务都不可用")
serviceCallbacks.forEach { callback ->
callback.onLocationError(-6, "定位服务不可用")
}
}
}
/**
* 处理定位结果
*/
// 修改handleLocationResult方法,对网络定位进行更细致的处理
private fun handleLocationResult(location: Location, provider: String) {
Log.d(TAG, "定位结果: 来源=$provider, 精度=${location.accuracy}米, 坐标=(${location.latitude}, ${location.longitude})")
// 混合定位策略:优先接受可用的定位结果
when (provider) {
"NETWORK" -> {
// 网络定位优先策略
when {
location.accuracy <= 200f -> {
Log.d(TAG, "✅ 接受网络定位: ${location.accuracy}米")
processLocationSuccess(location)
}
location.accuracy <= 1000f -> {
Log.d(TAG, "⚠️ 接受中等精度网络定位: ${location.accuracy}米")
// 短暂等待,看是否有更好的定位
val elapsedTime = System.currentTimeMillis() - locationStartTime
if (elapsedTime > 5000) { // 5秒后接受
processLocationSuccess(location)
} else {
Log.d(TAG, "等待更高精度定位中...")
}
}
location.accuracy <= 3000f -> {
Log.w(TAG, "⚠️ 网络定位精度较低: ${location.accuracy}米")
// 10秒后接受低精度定位
val elapsedTime = System.currentTimeMillis() - locationStartTime
if (elapsedTime > 10000) {
Log.d(TAG, "接受低精度网络定位")
processLocationSuccess(location)
}
}
else -> {
Log.w(TAG, "网络定位精度过低(${location.accuracy}米),继续等待")
}
}
}
"GPS" -> {
// GPS作为高精度补充
if (location.accuracy <= HIGH_ACCURACY_THRESHOLD) {
Log.d(TAG, "✅ 接受高精度GPS定位: ${location.accuracy}米")
processLocationSuccess(location)
} else if (location.accuracy <= HIGH_ACCURACY_THRESHOLD * 2) {
Log.d(TAG, "✅ 接受GPS定位: ${location.accuracy}米")
processLocationSuccess(location)
} else {
Log.w(TAG, "GPS定位精度不足(${location.accuracy}米),继续等待")
}
}
}
}
/**
* 处理定位成功
*/
private fun processLocationSuccess(location: Location) {
// 停止超时计时器
stopLocationTimeout()
// 停止定位更新
stopLocationUpdates()
// 获取地址信息(可选)
val address = getAddressFromLocation(location)
// 缓存位置数据
val locationData = LocationData(
latitude = location.latitude,
longitude = location.longitude,
timestamp = System.currentTimeMillis()
)
cachedLocationData = locationData
// 处理成功的定位结果
serviceCallbacks.forEach { callback ->
try {
callback.onLocationSuccess(
location.latitude,
location.longitude
)
} catch (e: Exception) {
Log.e(TAG, "服务回调失败: ${e.message}", e)
}
}
// 清空服务回调列表
serviceCallbacks.clear()
}
/**
* 停止定位更新
*/
private fun stopLocationUpdates() {
val locManager = locationManager ?: return
gpsLocationListener?.let {
locManager.removeUpdates(it)
gpsLocationListener = null
}
networkLocationListener?.let {
locManager.removeUpdates(it)
networkLocationListener = null
}
Log.d(TAG, "已停止定位更新")
}
/**
* 从位置获取地址信息
*/
private fun getAddressFromLocation(location: Location): String {
return try {
val ctx = appContext ?: return ""
val geocoder = Geocoder(ctx, Locale.getDefault())
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
// Android 13+ 使用新的异步API
var addressResult = ""
geocoder.getFromLocation(location.latitude, location.longitude, 1) { addresses ->
if (addresses.isNotEmpty()) {
addressResult = addresses[0].getAddressLine(0) ?: ""
}
}
addressResult
} else {
// 旧版本使用同步API
@Suppress("DEPRECATION")
val addresses = geocoder.getFromLocation(location.latitude, location.longitude, 1)
if (!addresses.isNullOrEmpty()) {
addresses[0].getAddressLine(0) ?: ""
} else {
""
}
}
} catch (e: Exception) {
Log.w(TAG, "获取地址失败: ${e.message}")
""
}
}
/**
* 检查定位权限
*/
private fun checkPermissions(context: Context): Boolean {
return ContextCompat.checkSelfPermission(
context,
Manifest.permission.ACCESS_FINE_LOCATION
) == PackageManager.PERMISSION_GRANTED ||
ContextCompat.checkSelfPermission(
context,
Manifest.permission.ACCESS_COARSE_LOCATION
) == PackageManager.PERMISSION_GRANTED
}
/**
* 检查精确定位权限
*/
private fun hasFineLocationPermission(context: Context): Boolean {
return ContextCompat.checkSelfPermission(
context,
Manifest.permission.ACCESS_FINE_LOCATION
) == PackageManager.PERMISSION_GRANTED
}
/**
* 检查粗略定位权限
*/
private fun hasCoarseLocationPermission(context: Context): Boolean {
return ContextCompat.checkSelfPermission(
context,
Manifest.permission.ACCESS_COARSE_LOCATION
) == PackageManager.PERMISSION_GRANTED
}
/**
* 检查后台定位权限
*/
private fun hasBackgroundLocationPermission(context: Context): Boolean {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
ContextCompat.checkSelfPermission(
context,
Manifest.permission.ACCESS_BACKGROUND_LOCATION
) == PackageManager.PERMISSION_GRANTED
} else {
true // Android 9 及以下没有这个权限
}
}
/**
* 位置数据类
*/
data class LocationData(
val latitude: Double,
val longitude: Double,
val timestamp: Long
)
/**
* 启动定位超时计时器
*/
private fun startLocationTimeout() {
// 如果已经有超时计时器在运行,先停止它
stopLocationTimeout()
locationTimeoutHandler = Handler(Looper.getMainLooper())
locationTimeoutHandler?.postDelayed({
Log.w(TAG, "定位超时,停止定位请求")
// 停止定位
stopLocationUpdates()
// 通知所有等待的回调超时错误
serviceCallbacks.forEach { callback ->
try {
callback.onLocationError(-6, "定位超时,未能在${LOCATION_TIMEOUT_MS/1000}秒内获取到位置")
} catch (e: Exception) {
Log.e(TAG, "超时回调失败: ${e.message}", e)
}
}
// 清空回调列表
serviceCallbacks.clear()
// 清理超时处理器
locationTimeoutHandler = null
}, LOCATION_TIMEOUT_MS)
}
/**
* 停止定位超时计时器
*/
private fun stopLocationTimeout() {
locationTimeoutHandler?.removeCallbacksAndMessages(null)
locationTimeoutHandler = null
}
/**
* 清理资源
*/
fun cleanup() {
stopLocationUpdates()
stopLocationTimeout()
serviceCallbacks.clear()
cachedLocationData = null
initialized = false
Log.d(TAG, "原生定位服务已清理")
}
}
// 调整精度阈值,更适合混合定位
private const val HIGH_ACCURACY_THRESHOLD = 100.0f // 放宽到100米
private const val LOCATION_TIMEOUT_MS = 30000L // 缩短到30秒
private const val MIN_TIME_BETWEEN_UPDATES = 2000L // 2秒更新
private const val MIN_DISTANCE_CHANGE_FOR_UPDATES = 5f // 5米距离变化

10
local_plugins/location_service/ios/location_service/Sources/location_service/LocationData.swift

@ -2,15 +2,15 @@ import Foundation
/// 位置数据结构
public struct LocationData: Codable {
public let latitude: Double
public let longitude: Double
public let latitude: Double
public let timestamp: TimeInterval
public let latitudeandlongitude: String
public let location: String
public init(latitude: Double, longitude: Double,timestamp: TimeInterval) {
self.latitude = latitude
public init( longitude: Double,latitude: Double,timestamp: TimeInterval) {
self.longitude = longitude
self.latitude = latitude
self.timestamp = timestamp
self.latitudeandlongitude = "\(latitude),\(longitude)"
self.location = "\(longitude),\(latitude)"
}
}

278
local_plugins/location_service/ios/location_service/Sources/location_service/LocationService.swift

@ -23,37 +23,33 @@ public class LocationService: NSObject {
private var authorizationStatus: CLAuthorizationStatus = .notDetermined
private var isLocationServicesEnabled: Bool = false
// 高精度定位属性
private var locationAttempts: Int = 0
private let maxLocationAttempts: Int = 3
private let requiredAccuracy: CLLocationAccuracy = 10.0 // 要求精度在10米以内
private var bestLocation: CLLocation?
private var locationTimer: Timer?
// MARK: - 权限回调
private var permissionCallback: ((Result<Bool, Error>) -> Void)?
private override init() {
super.init()
setupLocationManager()
}
private func setupLocationManager() {
DispatchQueue.main.async { [weak self] in
self?.locationManager = CLLocationManager()
self?.locationManager?.delegate = self
self?.locationManager?.desiredAccuracy = kCLLocationAccuracyBest
self?.locationManager?.distanceFilter = 10
// 不需要手动获取状态,delegate 方法会自动被调用
// 高精度定位设置
self?.locationManager?.desiredAccuracy = kCLLocationAccuracyBestForNavigation // 最高精度
self?.locationManager?.distanceFilter = 1 // 降低距离过滤器,更敏感
print("[LocationService] LocationManager 初始化完成,等待权限状态回调")
}
}
// private func setupLocationManager() {
// DispatchQueue.main.async { [weak self] in
// self?.locationManager = CLLocationManager()
// self?.locationManager?.delegate = self
// self?.locationManager?.desiredAccuracy = kCLLocationAccuracyBest
// self?.locationManager?.distanceFilter = 10
// // 初始化时获取当前状态
// self?.authorizationStatus = CLLocationManager.authorizationStatus()
// self?.isLocationServicesEnabled = CLLocationManager.locationServicesEnabled()
// print("[LocationService] LocationManager 初始化完成")
// print("[LocationService] 初始权限状态: \(self?.authorizationStatus.rawValue ?? -1)")
// print("[LocationService] 位置服务启用: \(self?.isLocationServicesEnabled ?? false)")
// }
// }
/// 获取当前位置 - 主要接口
public func getCurrentLocation(completion: @escaping (Result<[String: Any], Error>) -> Void) {
@ -98,20 +94,14 @@ public class LocationService: NSObject {
/// 异步更新位置缓存
private func asyncUpdateLocation() {
// 检查权限
guard checkLocationPermission() else {
print("[LocationService] 异步更新失败:无位置权限")
return
}
// 异步请求位置更新
DispatchQueue.global(qos: .background).async { [weak self] in
self?.requestLocationUpdate { result in
if checkLocationPermission() {
requestHighAccuracyLocation { [weak self] result in
switch result {
case .success(let locationData):
print("[LocationService] 异步更新位置缓存成功")
self?.cachedLocationData = locationData
print("[LocationService] 异步更新缓存成功")
case .failure(let error):
print("[LocationService] 异步更新位置缓存失败: \(error.localizedDescription)")
print("[LocationService] 异步更新缓存失败: \(error.localizedDescription)")
}
}
}
@ -119,14 +109,22 @@ public class LocationService: NSObject {
/// 同步获取位置
private func syncGetLocation(completion: @escaping (Result<[String: Any], Error>) -> Void) {
// 检查权限
guard checkLocationPermission() else {
print("[LocationService] 无位置权限,尝试请求权限")
if checkLocationPermission() {
requestHighAccuracyLocation { [weak self] result in
switch result {
case .success(let locationData):
self?.cachedLocationData = locationData
let locationInfo = self?.formatLocationData(locationData) ?? [:]
completion(.success(locationInfo))
case .failure(let error):
completion(.failure(error))
}
}
} else {
requestLocationPermission { [weak self] result in
switch result {
case .success(let granted):
if granted {
print("[LocationService] 权限获取成功,重新获取位置")
self?.syncGetLocation(completion: completion)
} else {
completion(.failure(NSError(domain: "LocationService", code: -1, userInfo: [NSLocalizedDescriptionKey: "位置权限被拒绝"])))
@ -135,164 +133,152 @@ public class LocationService: NSObject {
completion(.failure(error))
}
}
return
}
// 有权限,直接获取位置
requestLocationUpdate { [weak self] result in
switch result {
case .success(let locationData):
print("[LocationService] 同步获取位置成功")
self?.cachedLocationData = locationData
let locationInfo = self?.formatLocationData(locationData) ?? [:]
completion(.success(locationInfo))
case .failure(let error):
print("[LocationService] 同步获取位置失败: \(error.localizedDescription)")
completion(.failure(error))
}
}
}
/// 检查位置权限 - 使用缓存的状态
/// 检查位置权限
private func checkLocationPermission() -> Bool {
return authorizationStatus == .authorizedWhenInUse || authorizationStatus == .authorizedAlways
}
/// 请求位置权限 - 异步处理
/// 请求位置权限
private func requestLocationPermission(completion: @escaping (Result<Bool, Error>) -> Void) {
print("[LocationService] 当前缓存的权限状态: \(authorizationStatus.rawValue)")
switch authorizationStatus {
case .notDetermined:
print("[LocationService] 请求位置权限")
// 保存回调,等待权限结果
permissionCallback = completion
DispatchQueue.main.async { [weak self] in
self?.locationManager?.requestWhenInUseAuthorization()
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
guard let locationManager = self.locationManager else {
completion(.failure(NSError(domain: "LocationService", code: -3, userInfo: [NSLocalizedDescriptionKey: "LocationManager 未初始化"])))
return
}
// 设置权限回调
self.permissionCallback = completion
// 请求权限
switch self.authorizationStatus {
case .notDetermined:
locationManager.requestWhenInUseAuthorization()
case .denied, .restricted:
completion(.success(false))
case .authorizedWhenInUse, .authorizedAlways:
completion(.success(true))
@unknown default:
completion(.failure(NSError(domain: "LocationService", code: -2, userInfo: [NSLocalizedDescriptionKey: "未知权限状态"])))
}
case .denied, .restricted:
print("[LocationService] 位置权限被拒绝或受限")
completion(.success(false))
case .authorizedWhenInUse, .authorizedAlways:
print("[LocationService] 已有位置权限")
completion(.success(true))
@unknown default:
completion(.failure(NSError(domain: "LocationService", code: -2, userInfo: [NSLocalizedDescriptionKey: "未知权限状态"])))
}
}
/// 请求位置更新 - 优化版本
private func requestLocationUpdate(completion: @escaping (Result<LocationData, Error>) -> Void) {
/// 高精度定位方法
private func requestHighAccuracyLocation(completion: @escaping (Result<LocationData, Error>) -> Void) {
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
guard let locationManager = self.locationManager else {
print("[LocationService] 错误:LocationManager 未初始化")
completion(.failure(NSError(domain: "LocationService", code: -3, userInfo: [NSLocalizedDescriptionKey: "LocationManager 未初始化"])))
return
}
// 检查是否已有请求在进行
if self.currentLocationCallback != nil {
print("[LocationService] 错误:位置请求正在进行中,拒绝新请求")
completion(.failure(NSError(domain: "LocationService", code: -4, userInfo: [NSLocalizedDescriptionKey: "位置请求正在进行中"])))
return
}
// 使用缓存的状态信息
// print("[LocationService] 开始位置请求")
// print("[LocationService] 缓存的权限状态: \(self.authorizationStatus.rawValue)")
// print("[LocationService] 缓存的位置服务状态: \(self.isLocationServicesEnabled)")
// print("[LocationService] 当前线程: \(Thread.current)")
// print("[LocationService] 是否主线程: \(Thread.isMainThread)")
// 验证权限状态
// 检查权限
if !self.checkLocationPermission() {
print("[LocationService] 错误:无位置权限")
completion(.failure(NSError(domain: "LocationService", code: -5, userInfo: [NSLocalizedDescriptionKey: "无位置权限"])))
return
}
if !self.isLocationServicesEnabled {
print("[LocationService] 错误:位置服务未启用")
completion(.failure(NSError(domain: "LocationService", code: -6, userInfo: [NSLocalizedDescriptionKey: "位置服务未启用"])))
return
}
// 强制重新设置 delegate
if locationManager.delegate == nil {
print("[LocationService] 警告:Delegate 为空,重新设置")
locationManager.delegate = self
}
// 验证 delegate
if locationManager.delegate === self {
print("[LocationService] Delegate 验证成功")
} else {
print("[LocationService] 错误:Delegate 设置失败")
completion(.failure(NSError(domain: "LocationService", code: -8, userInfo: [NSLocalizedDescriptionKey: "Delegate 设置失败"])))
return
}
// 重置状态
self.locationAttempts = 0
self.bestLocation = nil
// 设置回调
self.currentLocationCallback = { result in
switch result {
case .success(let locationInfo):
print("[LocationService] 位置回调成功,开始解析数据")
if let latStr = locationInfo["user_lat"] as? String,
let lngStr = locationInfo["user_lng"] as? String,
let latitude = Double(latStr),
let longitude = Double(lngStr) {
print("[LocationService] 位置数据解析成功: lat=\(latitude), lng=\(longitude)")
let locationData = LocationData(
latitude: latitude,
longitude: longitude,
latitude: latitude,
timestamp: Date().timeIntervalSince1970 * 1000
)
completion(.success(locationData))
} else {
print("[LocationService] 错误:位置数据格式错误,原始数据: \(locationInfo)")
completion(.failure(NSError(domain: "LocationService", code: -7, userInfo: [NSLocalizedDescriptionKey: "位置数据格式错误"])))
}
case .failure(let error):
print("[LocationService] 位置回调失败: \(error.localizedDescription)")
completion(.failure(error))
}
}
print("[LocationService] 调用 locationManager.requestLocation()")
locationManager.requestLocation()
print("[LocationService] requestLocation() 调用完成,等待回调")
print("[LocationService] 开始高精度连续定位")
// 开始连续定位
locationManager.startUpdatingLocation()
// 30秒超时机制
DispatchQueue.main.asyncAfter(deadline: .now() + 30) {
if self.currentLocationCallback != nil {
print("[LocationService] 位置请求超时")
// 设置超时定时器(15秒)
self.locationTimer = Timer.scheduledTimer(withTimeInterval: 15.0, repeats: false) { _ in
self.stopLocationUpdates()
if let bestLocation = self.bestLocation {
self.processLocation(bestLocation)
} else {
let callback = self.currentLocationCallback
self.currentLocationCallback = nil
callback?(.failure(NSError(domain: "LocationService", code: -9, userInfo: [NSLocalizedDescriptionKey: "位置请求超时"])))
callback?(.failure(NSError(domain: "LocationService", code: -9, userInfo: [NSLocalizedDescriptionKey: "无法获取满足精度要求的位置"])))
}
}
}
}
/// 停止位置更新
private func stopLocationUpdates() {
locationManager?.stopUpdatingLocation()
locationTimer?.invalidate()
locationTimer = nil
}
/// 处理位置数据
private func processLocation(_ location: CLLocation) {
let locationInfo: [String: Any] = [
"user_lng": String(location.coordinate.longitude),
"user_lat": String(location.coordinate.latitude),
"timestamp": Date().timeIntervalSince1970 * 1000,
"accuracy": location.horizontalAccuracy // 添加精度信息
]
print("[LocationService] 处理最终位置,精度: \(location.horizontalAccuracy)m")
// 更新缓存
cachedLocationData = LocationData(
longitude: location.coordinate.longitude,
latitude: location.coordinate.latitude,
timestamp: Date().timeIntervalSince1970 * 1000
)
let callback = currentLocationCallback
currentLocationCallback = nil
callback?(.success(locationInfo))
}
/// 格式化位置数据
private func formatLocationData(_ locationData: LocationData) -> [String: Any] {
return [
"user_lat": String(locationData.latitude),
"user_lng": String(locationData.longitude),
"user_lat": String(locationData.latitude),
"location": "\(locationData.longitude),\(locationData.latitude)",
"timestamp": locationData.timestamp
]
}
// MARK: - 权限回调
private var permissionCallback: ((Result<Bool, Error>) -> Void)?
}
// MARK: - CLLocationManagerDelegate
extension LocationService: CLLocationManagerDelegate {
// 替换 didUpdateLocations 方法中的逆地理编码部分
public func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
print("[LocationService] didUpdateLocations 被调用,位置数量: \(locations.count)")
@ -301,22 +287,47 @@ extension LocationService: CLLocationManagerDelegate {
return
}
print("[LocationService] 位置更新成功: lat=\(location.coordinate.latitude), lng=\(location.coordinate.longitude)")
print("[LocationService] 位置精度: \(location.horizontalAccuracy)m")
print("[LocationService] 位置更新: lat=\(location.coordinate.latitude), lng=\(location.coordinate.longitude)")
print("[LocationService] 水平精度: \(location.horizontalAccuracy)m, 垂直精度: \(location.verticalAccuracy)m")
print("[LocationService] 位置时间: \(location.timestamp)")
// 直接使用经纬度,不进行逆地理编码
let locationInfo: [String: Any] = [
"user_lat": String(location.coordinate.latitude),
"user_lng": String(location.coordinate.longitude),
"timestamp": Date().timeIntervalSince1970 * 1000
]
// 过滤无效位置
guard location.horizontalAccuracy >= 0 else {
print("[LocationService] 位置精度无效,忽略此次更新")
return
}
print("[LocationService] 准备调用位置回调")
// 调用回调
let callback = currentLocationCallback
currentLocationCallback = nil
callback?(.success(locationInfo))
print("[LocationService] 位置回调已调用")
// 过滤过旧的位置(超过5秒)
let locationAge = -location.timestamp.timeIntervalSinceNow
guard locationAge < 5.0 else {
print("[LocationService] 位置数据过旧(\(locationAge)秒),忽略")
return
}
locationAttempts += 1
// 检查是否满足精度要求
if location.horizontalAccuracy <= requiredAccuracy {
print("[LocationService] 获得高精度位置,精度: \(location.horizontalAccuracy)m")
stopLocationUpdates()
processLocation(location)
return
}
// 保存最佳位置
if bestLocation == nil || location.horizontalAccuracy < bestLocation!.horizontalAccuracy {
bestLocation = location
print("[LocationService] 更新最佳位置,精度: \(location.horizontalAccuracy)m")
}
// 达到最大尝试次数,使用最佳位置
if locationAttempts >= maxLocationAttempts {
print("[LocationService] 达到最大尝试次数,使用最佳位置")
stopLocationUpdates()
if let bestLocation = bestLocation {
processLocation(bestLocation)
}
}
}
public func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
@ -325,11 +336,10 @@ extension LocationService: CLLocationManagerDelegate {
print("[LocationService] 错误代码: \((error as NSError).code)")
print("[LocationService] 错误域: \((error as NSError).domain)")
stopLocationUpdates()
let callback = currentLocationCallback
currentLocationCallback = nil
print("[LocationService] 准备调用错误回调")
callback?(.failure(error))
print("[LocationService] 错误回调已调用")
}
public func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {

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

@ -383,6 +383,9 @@ class OtaPlugin : BleService.Callback, FlutterPlugin, MethodCallHandler {
// 可选:处理音频数据
}
override fun onAudioDataReceived1(data: ByteArray) {
}
/**
* 处理唤醒信号
* 在收到唤醒信号时启动语音识别

2
pubspec.yaml

@ -1,7 +1,7 @@
name: voitrans
description: "Voitrans - AI Voice Assistant."
publish_to: "none"
version: 1.0.6+20
version: 1.0.7+20
environment:
sdk: ">=3.3.0 <4.0.0"

Loading…
Cancel
Save