Browse Source

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

newdev_shunjiawei
tanlongsheng 1 year ago
parent
commit
1c04d078ee
  1. 162
      lib/data/services/meeting/meeting_task_service.dart
  2. 36
      lib/data/services/version_update_service.dart
  3. 1
      lib/modules/agent/controllers/agent_controller.dart
  4. 200
      lib/modules/agent/views/agent_view.dart
  5. 1
      lib/modules/agent/views/music_card.dart
  6. 127
      lib/modules/login/constants/country_codes.dart
  7. 154
      lib/modules/login/controllers/login_controller.dart
  8. 1
      lib/modules/meeting/views/bottomSheet/navigation_bar_bottom_sheet.dart
  9. 26
      lib/modules/meeting/views/meeting_details_view.dart
  10. 1
      lib/modules/meeting/views/meeting_home_view.dart
  11. 1
      lib/modules/meeting/views/meeting_view.dart
  12. 22
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt
  13. 75
      local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureSpeechPlugin.kt
  14. 76
      local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureSpeechPlugin.swift
  15. 696
      local_plugins/music_service/ios/music_service/Sources/music_service/MusicService.swift

162
lib/data/services/meeting/meeting_task_service.dart

@ -10,29 +10,35 @@ import '../db/sqflite_api.dart';
import '../deapsound_ai_service.dart'; import '../deapsound_ai_service.dart';
import 'meeting_upload_service.dart'; import 'meeting_upload_service.dart';
///会议任务服务类,负责管理会议音频的转写任务处理流程,包括任务提交、状态轮询、结果处理和AI分析等功能。
class MeetingTaskService extends GetxService { class MeetingTaskService extends GetxService {
final GetStorage _storage = GetStorage(); final GetStorage _storage = GetStorage();
MeetingUploadService get _uploadService => Get.find<MeetingUploadService>(); MeetingUploadService get _uploadService => Get.find<MeetingUploadService>();
List _taskList = []; List _taskList = []; //待处理的会议任务列表
RxList taskFinishList = [].obs; RxList taskFinishList = [].obs; //已处理的会议任务列表
bool _isTask = false; bool _isTask = false; //是否有正在处理的任务
String _userId = ''; String _userId = '';
//字节跳动(火山引擎)语音服务配置
final String _appid = '2316081933'; final String _appid = '2316081933';
final String _token = 'mcJTh6OelBqU_L78lgGd77Nyrc_-5-En'; final String _token = 'mcJTh6OelBqU_L78lgGd77Nyrc_-5-En';
final String _cluster = 'volc_auc_tele_flash'; final String _cluster = 'volc_auc_video_flash';
final String _serviceUrl = 'https://openspeech.bytedance.com/api/v1/auc'; final String _cluster1 = 'volc.bigasr.auc';
final String _serviceUrl = 'https://openspeech.bytedance.com/api/v1/auc'; //
final String _serviceUrl1 =
'https://openspeech.bytedance.com/api/v3/auc/bigmodel'; //大模型录音文件识别api地址
Map<String, String> _headers = {}; Map<String, String> _headers = {};
Map<String, String> _headers1 = {};
int meetingId = 0; int meetingId = 0;
RxInt tasktype = 1.obs; RxInt tasktype = 1.obs; //任务类型,1-转写中,2-总结中,3-完成
List textList = []; List textList = []; //转写结果列表
String summary = ''; String summary = ''; //AI总结内容
String overview = ''; String overview = ''; //AI概览内容
@override @override
void onInit() { void onInit() {
@ -41,6 +47,14 @@ class MeetingTaskService extends GetxService {
'Authorization': 'Bearer; $_token', 'Authorization': 'Bearer; $_token',
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}; };
// _headers1 = {
// 'X-Api-App-Key': _appid,
// 'X-Api-Access-Key': _token,
// 'X-Api-Resource-Id': _cluster1,
// 'X-Api-Request-Id': DateTime.now().millisecondsSinceEpoch.toString(),
// 'X-Api-Sequence': '-1',
// };
_getAgent(); _getAgent();
_getTaskList(); _getTaskList();
} }
@ -56,6 +70,14 @@ class MeetingTaskService extends GetxService {
_executeTask(); _executeTask();
} }
///添加会议任务
///[id] 会议ID
///[audiourl] 音频URL
///[taskid] 任务ID
///[isSpeaker] 是否为发言人
///[language] 语言
///[prompt] 提示词
///[outline] 大纲
Future<void> addTask(int id, String audiourl, String taskid, bool isSpeaker, Future<void> addTask(int id, String audiourl, String taskid, bool isSpeaker,
String language, String prompt, String outline) async { String language, String prompt, String outline) async {
if (audiourl.isEmpty) { if (audiourl.isEmpty) {
@ -81,6 +103,14 @@ class MeetingTaskService extends GetxService {
} }
} }
///添加会议任务列表
///[id] 会议ID
///[audiourl] 音频URL
///[taskId] 任务ID
///[isSpeaker] 是否为发言人
///[language] 语言
///[prompt] 提示词
///[outline] 大纲
Future<void> addTaskList(int id, String audiourl, String taskId, Future<void> addTaskList(int id, String audiourl, String taskId,
bool isSpeaker, String language, String prompt, String outline) async { bool isSpeaker, String language, String prompt, String outline) async {
if (taskId.isEmpty) { if (taskId.isEmpty) {
@ -103,14 +133,15 @@ class MeetingTaskService extends GetxService {
_executeTask(); _executeTask();
} }
///执行会议任务
void _executeTask() async { void _executeTask() async {
if (_isTask) return; if (_isTask) return;
_isTask = true; _isTask = true;
while (_taskList.isNotEmpty) { while (_taskList.isNotEmpty) {
await Future.delayed(const Duration(seconds: 2)); await Future.delayed(const Duration(seconds: 2));
Logger.info("lxm--轮询---会议任务列表$_taskList");
for (var element in List.from(_taskList)) { for (var task in List.from(_taskList)) {
await _fileRecognize(element); await _fileRecognize(task);
} }
} }
_isTask = false; _isTask = false;
@ -133,9 +164,46 @@ class MeetingTaskService extends GetxService {
} }
} }
//
Future<String> _submitTask( Future<String> _submitTask(
String audiourl, bool isSpeaker, String language) async { String audiourl, bool isSpeaker, String language) async {
String format = _getFileType(audiourl); String format = _getFileType(audiourl);
final taskId = DateTime.now().millisecondsSinceEpoch.toString();
_headers1 = {
'X-Api-App-Key': _appid,
'X-Api-Access-Key': _token,
'X-Api-Resource-Id': _cluster1,
'X-Api-Request-Id': taskId,
'X-Api-Sequence': '-1',
};
print("lxm--大模型asr请求头$_headers1");
final requestBody1 = jsonEncode({
"user": {
"uid": _userId,
},
"audio": {
"url": audiourl,
"language": language,
"format": format,
},
"request": {
"model_name": "bigmodel", // 模型名称,目前只有bigmodel
"model_version": "400", // 模型版本,400模型性能略有提升,且ITN有较大优化
//"enable_itn": true, // 启用文本规范化,默认为true
"enable_punc": true, // 启用标点,默认为false
//"enable_ddc": false, // 启用语义顺滑,默认为false
"enable_speaker_info": isSpeaker, // 启用说话人聚类分离,10人以内效果较好
//"enable_channel_split": false, // 启用双声道识别,默认为false
//"show_utterances": true, // 输出语音停顿、分句、分词信息
//"show_speech_rate": false, // 分句信息携带语速
//"show_volume": false, // 分句信息携带音量
//"enable_lid": false, // 启用语种识别
}
});
print("lxm--大模型asr请求体$requestBody1");
final requestBody = jsonEncode({ final requestBody = jsonEncode({
"app": { "app": {
@ -158,18 +226,23 @@ class MeetingTaskService extends GetxService {
try { try {
final response = await http.post( final response = await http.post(
Uri.parse('$_serviceUrl/submit'), Uri.parse('$_serviceUrl1/submit'),
headers: _headers, headers: _headers1,
body: requestBody, body: requestBody1,
); );
print("lxm--大模型asr响应体${response.body}");
print("lxm--大模型asr上传响应头${response.headers['x-tt-logid']}");
print("lxm--大模型asr上传服务端返回的状态码${response.headers['x-api-status-code']}");
print("lxm--大模型asr上传服务端返回的消息${response.headers['x-api-message']}");
if (response.statusCode != 200) { if (response.statusCode != 200) {
throw Exception('Submit failed with status: ${response.statusCode}'); throw Exception('Submit failed with status: ${response.statusCode}');
} }
final respDic = jsonDecode(response.body); //final respDic = jsonDecode(response.body);
final taskId = respDic['resp']['id'] as String; //final taskId = respDic['resp']['id'] as String;
Logger.debug('taskId: $taskId'); //Logger.debug('taskId: $taskId');
return taskId; return taskId;
} catch (e) { } catch (e) {
Logger.error('Submit error: ${e.toString()}'); Logger.error('Submit error: ${e.toString()}');
@ -182,6 +255,8 @@ class MeetingTaskService extends GetxService {
return array[array.length - 1]; return array[array.length - 1];
} }
///轮询语音转文字任务状态
///[taskId] 任务ID
Future<Map<String, dynamic>> _queryTask(String taskId) async { Future<Map<String, dynamic>> _queryTask(String taskId) async {
final queryBody = jsonEncode({ final queryBody = jsonEncode({
"appid": _appid, "appid": _appid,
@ -192,11 +267,18 @@ class MeetingTaskService extends GetxService {
try { try {
final response = await http.post( final response = await http.post(
Uri.parse('$_serviceUrl/query'), Uri.parse('$_serviceUrl1/query'),
headers: _headers, headers: _headers1,
body: queryBody, body: jsonEncode({}),
); );
// print("lxm---大模型asr响应体${response.body}");
// print("lxm---大模型asr响应头${response.headers}");
// print("lxm---大模型asr响应状态码${response.statusCode}");
// print("lxm---大模型asr响应体logid${response.headers['x-tt-logid']}");
// print("lxm---大模型asr服务端返回的状态码${response.headers['x-api-status-code']}");
// print("lxm---大模型asr服务端返回的消息${response.headers['x-api-message']}");
if (response.statusCode != 200) { if (response.statusCode != 200) {
throw Exception('Query failed with status: ${response.statusCode}'); throw Exception('Query failed with status: ${response.statusCode}');
} }
@ -209,20 +291,24 @@ class MeetingTaskService extends GetxService {
} }
} }
///轮询语音转文字任务状态,若任务完成则处理转写结果
///[item] 包含任务ID、会议ID、AI总结提示词、AI概览提示词的Map
Future<void> _fileRecognize(Map item) async { Future<void> _fileRecognize(Map item) async {
try { try {
var respDic = await _queryTask(item['taskid']); var respDic = await _queryTask(item['taskid']);
var code = respDic['resp']['code'] as int; //var code = respDic['resp']['code'] as int;
print("lxm---大模型asr响应体结果$respDic['result']['text']");
if (code == 1000) { if (respDic['result']['text'] != null) {
await _addTextList( await _addTextList(
item['id'], item['id'], //会议ID
respDic['resp']['utterances'], respDic['result']['utterances'], //语音转文字结果
item['prompt'], item['prompt'], //AI总结提示词
item['outline'], item['outline'], //AI概览提示词
); );
return; return;
} else if (code < 2000) { } else {
Logger.debug("FileRecognize failed"); Logger.debug("FileRecognize failed");
return; return;
} }
@ -231,11 +317,18 @@ class MeetingTaskService extends GetxService {
} }
} }
///处理语音识别完成后的转写结果,包括数据库存储、状态更新和触发AI分析
///[id] 会议ID
///[utterances] 语音转文字结果
///[prompt] AI总结提示词
///[outline] AI概览提示词
Future<void> _addTextList( Future<void> _addTextList(
int id, List utterances, String prompt, String outline) async { int id, List utterances, String prompt, String outline) async {
String text = ''; String text = '';
String personnel = ''; String personnel = '';
List<Map<String, Object?>> list = []; List<Map<String, Object?>> list = [];
// 处理转写结果,将每个句子的开始时间、结束时间、发言人和内容存储到列表中
for (var element in utterances) { for (var element in utterances) {
String speaker = ''; String speaker = '';
if (element['additions'].isNotEmpty) { if (element['additions'].isNotEmpty) {
@ -254,8 +347,10 @@ class MeetingTaskService extends GetxService {
}); });
text += '${element['text']}\n'; text += '${element['text']}\n';
} }
// 存储处理后的转写结果到数据库
var result = await SqfliteApi.insertMeetingSpeaker(list); var result = await SqfliteApi.insertMeetingSpeaker(list);
if (result != null) { if (result != null) {
// 更新会议状态为“总结中”
await SqfliteApi.editMeeting( await SqfliteApi.editMeeting(
id, id,
{'tasktype': 2, 'personnel': personnel}, {'tasktype': 2, 'personnel': personnel},
@ -282,9 +377,16 @@ class MeetingTaskService extends GetxService {
_storage.write("meeting_task_list", _taskList); _storage.write("meeting_task_list", _taskList);
_storage.write("meeting_task_finish_list", taskFinishList); _storage.write("meeting_task_finish_list", taskFinishList);
} }
// 触发AI分析,分别请求总结和概览
aiServiceMessage(id, text, prompt, outline); aiServiceMessage(id, text, prompt, outline);
} }
///触发AI分析,分别请求总结和概览
///[id] 会议ID
///[text] 语音转文字结果
///[prompt] AI总结提示词
///[outline] AI概览提示词
void aiServiceMessage(int id, String text, String prompt, String outline) { void aiServiceMessage(int id, String text, String prompt, String outline) {
int awaitQuantity = 0; int awaitQuantity = 0;
String summaryText = ''; String summaryText = '';
@ -321,6 +423,10 @@ class MeetingTaskService extends GetxService {
}); });
} }
///处理AI分析完成后的结果,包括数据库存储和状态更新
///[id] 会议ID
///[summaryText] 总结结果
///[overviewText] 概览结果
void _aiServiceValue(int id, String summaryText, String overviewText) { void _aiServiceValue(int id, String summaryText, String overviewText) {
SqfliteApi.editMeeting(id, { SqfliteApi.editMeeting(id, {
'tasktype': 3, 'tasktype': 3,

36
lib/data/services/version_update_service.dart

@ -211,6 +211,7 @@ class VersionUpdateService extends GetxService {
/// 获取当前平台信息 /// 获取当前平台信息
Future<PlatformInfo> getCurrentPlatformInfo() async { Future<PlatformInfo> getCurrentPlatformInfo() async {
if (Platform.isIOS) { if (Platform.isIOS) {
await _getIosDeviceBrand();
return const PlatformInfo( return const PlatformInfo(
platform: 'ios', platform: 'ios',
storeType: AppStoreType.appStore, storeType: AppStoreType.appStore,
@ -310,6 +311,41 @@ class VersionUpdateService extends GetxService {
} }
} }
/// 获取iOS设备品牌信息
Future<void> _getIosDeviceBrand() async {
try {
final deviceInfo = DeviceInfoPlugin();
final iosInfo = await deviceInfo.iosInfo;
// 获取设备型号信息
final model =
iosInfo.model.toLowerCase(); // 例如: "iPhone", "iPad", "iPod touch"
//print('lxm---model: $model');
final utsname = iosInfo.utsname; // 包含更详细的设备信息
//print('lxm---utsname: $utsname');
final machineId = utsname.machine.toLowerCase(); // 例如: "iPhone14,2"
//print('lxm---machineId: $machineId');
// 根据设备型号确定品牌
if (model.contains('iphone') || machineId.contains('iphone')) {
_deviceBrand = 'iphone';
} else if (model.contains('ipad') || machineId.contains('ipad')) {
_deviceBrand = 'ipad';
} else if (model.contains('ipod') || machineId.contains('ipod')) {
_deviceBrand = 'ipod';
} else if (model.contains('mac') || machineId.contains('mac')) {
_deviceBrand = 'mac';
} else {
_deviceBrand = 'ios'; // 默认值
}
_logger.d('iOS设备品牌: $_deviceBrand (model: $model, machine: $machineId)');
} catch (e) {
_logger.e('获取iOS设备品牌失败: $e');
_deviceBrand = 'ios'; // 默认值
}
}
/// 判断是否需要强制更新 /// 判断是否需要强制更新
/// 当主版本号(第一位数字)不同时返回true /// 当主版本号(第一位数字)不同时返回true
bool _shouldForceUpdate(String current, String latest) { bool _shouldForceUpdate(String current, String latest) {

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

@ -1002,6 +1002,7 @@ class AgentController extends GetxController with WidgetsBindingObserver {
if (function_name.contains("music")) { if (function_name.contains("music")) {
Future.delayed(Duration(seconds: 1), () { Future.delayed(Duration(seconds: 1), () {
_qqmusicManager.syncCurrentPlayInfo(isOpenPlay: false); _qqmusicManager.syncCurrentPlayInfo(isOpenPlay: false);
musicStatus();
}); });
// } // }
} }

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

@ -25,6 +25,9 @@ class _AgentViewState extends State<AgentView> with WidgetsBindingObserver {
late MessageActionController messageActionController; late MessageActionController messageActionController;
double _keyboardHeight = 0; double _keyboardHeight = 0;
// 👇 新增:用于控制 TextField 焦点
final FocusNode _textFocusNode = FocusNode();
@override @override
void initState() { void initState() {
super.initState(); super.initState();
@ -35,6 +38,7 @@ class _AgentViewState extends State<AgentView> with WidgetsBindingObserver {
@override @override
void dispose() { void dispose() {
_textFocusNode.dispose();
WidgetsBinding.instance.removeObserver(this); WidgetsBinding.instance.removeObserver(this);
super.dispose(); super.dispose();
} }
@ -286,65 +290,102 @@ class _AgentViewState extends State<AgentView> with WidgetsBindingObserver {
children: [ children: [
// 聊天内容 // 聊天内容
Expanded( Expanded(
child: Obx(() { child: GestureDetector(
return Align( onTap: () {
alignment: Alignment.topCenter, // 点击聊天区域时,收起键盘并切换工具显示状态
child: ListView.builder( FocusScope.of(context).unfocus();
controller: controller.scrollController, if (controller.isPlusOptionsExpanded.value) {
shrinkWrap: true, controller.collapsePlusOptions();
reverse: true, }
padding: EdgeInsets.symmetric( },
vertical: 16.h, horizontal: 16.w), child: Obx(() {
itemCount: controller.messages.length + return Align(
(controller.hasMoreMessages.value ? 1 : 0), alignment: Alignment.topCenter,
itemBuilder: (context, index) { child: ListView.builder(
// 如果是最后一项且有更多消息,显示加载指示器 controller: controller.scrollController,
if (index == controller.messages.length && shrinkWrap: true,
controller.hasMoreMessages.value) { reverse: true,
return _buildLoadMoreIndicator(); padding: EdgeInsets.symmetric(
} vertical: 16.h, horizontal: 16.w),
itemCount: controller.messages.length +
(controller.hasMoreMessages.value ? 1 : 0),
itemBuilder: (context, index) {
// 如果是最后一项且有更多消息,显示加载指示器
if (index == controller.messages.length &&
controller.hasMoreMessages.value) {
return _buildLoadMoreIndicator();
}
final message = controller final message = controller
.messages[controller.messages.length - 1 - index]; .messages[controller.messages.length - 1 - index];
return index == 0 return index == 0
? IntroStepBuilder( ? IntroStepBuilder(
group: 'ai', group: 'ai',
order: 4, order: 4,
getOverlayPosition: getOverlayPosition:
IntroOverlayBuilder.overlayPosition, IntroOverlayBuilder.overlayPosition,
overlayBuilder: (params) { overlayBuilder: (params) {
return IntroOverlayBuilder.aiBuilder( return IntroOverlayBuilder.aiBuilder(
params, params,
controller.noviceGuideService.aiStepsList[ controller.noviceGuideService.aiStepsList[
controller controller.noviceGuideService
.noviceGuideService.aiStep]['end'], .aiStep]['end'],
onFinish: () async { onFinish: () async {
if (controller.noviceGuideService.aiStep < if (controller
5) { .noviceGuideService.aiStep <
await Future.delayed( 5) {
const Duration(milliseconds: 300)); await Future.delayed(const Duration(
controller.noviceGuideService.aiStep++; milliseconds: 300));
Intro.of(Get.context!) controller
.start(group: 'ai', reset: true); .noviceGuideService.aiStep++;
} Intro.of(Get.context!)
}, .start(group: 'ai', reset: true);
onTap: () async { }
params.onFinish(); },
if (controller.noviceGuideService.aiStep < onTap: () async {
5) { params.onFinish();
await Future.delayed( if (controller
const Duration(milliseconds: 300)); .noviceGuideService.aiStep <
controller.noviceGuideService.aiStep++; 5) {
Intro.of(Get.context!) await Future.delayed(const Duration(
.start(group: 'ai', reset: true); milliseconds: 300));
} controller
}, .noviceGuideService.aiStep++;
); Intro.of(Get.context!)
}, .start(group: 'ai', reset: true);
builder: }
(BuildContext context, GlobalKey key) => },
Padding( );
key: key, },
builder:
(BuildContext context, GlobalKey key) =>
Padding(
key: key,
padding: EdgeInsets.only(bottom: 8.h),
child: GestureDetector(
onLongPressStart:
(LongPressStartDetails details) {
messageActionController
.onLongPressMessage(context,
isDarkMode, details, message);
},
child: MessageBubble(
sessionid: message.sessionid,
isUser: message.isUser,
message: message.text,
timestamp: message.timestamp,
isRecognizing: message.isRecognizing,
hasImage: message.hasImage,
imagePath: message.imagePath,
meta: message.meta,
functionCalls: message.functionCalls,
isToolCallsExpanded:
message.isToolCallsExpanded,
),
),
),
)
: Padding(
padding: EdgeInsets.only(bottom: 8.h), padding: EdgeInsets.only(bottom: 8.h),
child: GestureDetector( child: GestureDetector(
onLongPressStart: onLongPressStart:
@ -367,35 +408,12 @@ class _AgentViewState extends State<AgentView> with WidgetsBindingObserver {
message.isToolCallsExpanded, message.isToolCallsExpanded,
), ),
), ),
), );
) },
: Padding( ),
padding: EdgeInsets.only(bottom: 8.h), );
child: GestureDetector( }),
onLongPressStart: ),
(LongPressStartDetails details) {
messageActionController.onLongPressMessage(
context, isDarkMode, details, message);
},
child: MessageBubble(
sessionid: message.sessionid,
isUser: message.isUser,
message: message.text,
timestamp: message.timestamp,
isRecognizing: message.isRecognizing,
hasImage: message.hasImage,
imagePath: message.imagePath,
meta: message.meta,
functionCalls: message.functionCalls,
isToolCallsExpanded:
message.isToolCallsExpanded,
),
),
);
},
),
);
}),
), ),
// 输入区域 // 输入区域
@ -553,6 +571,13 @@ class _AgentViewState extends State<AgentView> with WidgetsBindingObserver {
return GestureDetector( return GestureDetector(
onTap: () { onTap: () {
controller.toggleInputMode(); controller.toggleInputMode();
// 切换模式时,确保焦点正确
if (!isTextMode) {
// 切换到语音模式时,请求焦点
Future.delayed(Duration.zero, () {
_textFocusNode.requestFocus();
});
}
}, },
child: Container( child: Container(
width: 44.w, width: 44.w,
@ -652,6 +677,7 @@ class _AgentViewState extends State<AgentView> with WidgetsBindingObserver {
controller.isImageInputActive.value; controller.isImageInputActive.value;
return TextField( return TextField(
focusNode: _textFocusNode, // 绑定焦点节点
controller: controller.textController, controller: controller.textController,
maxLines: isImageActive maxLines: isImageActive
? 3 ? 3

1
lib/modules/agent/views/music_card.dart

@ -17,7 +17,6 @@ Widget buildMusicItem(
'coverUrl': image, 'coverUrl': image,
}; };
controller.musicStatus(); //刷新状态
return Container( return Container(
padding: EdgeInsets.all(14.w), padding: EdgeInsets.all(14.w),
decoration: BoxDecoration( decoration: BoxDecoration(

127
lib/modules/login/constants/country_codes.dart

@ -0,0 +1,127 @@
// Country dialing codes list extracted for maintainability and reuse.
final List<Map<String, String>> kCountryCodes = [
{'name': '邮箱', 'code': '', 'flag': '✉️'}, // 保持首位
{'name': '中国', 'code': '+86', 'flag': '🇨🇳'}, // 保持第二位
// 按国家代码数字升序排列(去除+号后排序)
{'name': '美国', 'code': '+1', 'flag': '🇺🇸'},
{'name': '加拿大', 'code': '+1', 'flag': '🇨🇦'},
{'name': '俄罗斯', 'code': '+7', 'flag': '🇷🇺'},
{'name': '英国', 'code': '+44', 'flag': '🇬🇧'},
{'name': '德国', 'code': '+49', 'flag': '🇩🇪'},
{'name': '意大利', 'code': '+39', 'flag': '🇮🇹'},
{'name': '法国', 'code': '+33', 'flag': '🇫🇷'},
{'name': '西班牙', 'code': '+34', 'flag': '🇪🇸'},
{'name': '南非', 'code': '+27', 'flag': '🇿🇦'},
{'name': '沙特阿拉伯', 'code': '+966', 'flag': '🇸🇦'},
{'name': '土耳其', 'code': '+90', 'flag': '🇹🇷'},
{'name': '日本', 'code': '+81', 'flag': '🇯🇵'},
{'name': '韩国', 'code': '+82', 'flag': '🇰🇷'},
{'name': '新加坡', 'code': '+65', 'flag': '🇸🇬'},
{'name': '马来西亚', 'code': '+60', 'flag': '🇲🇾'},
{'name': '泰国', 'code': '+66', 'flag': '🇹🇭'},
{'name': '越南', 'code': '+84', 'flag': '🇻🇳'},
{'name': '印度', 'code': '+91', 'flag': '🇮🇳'},
{'name': '巴基斯坦', 'code': '+92', 'flag': '🇵🇰'},
{'name': '菲律宾', 'code': '+63', 'flag': '🇵🇭'},
{'name': '印度尼西亚', 'code': '+62', 'flag': '🇮🇩'},
{'name': '阿联酋', 'code': '+971', 'flag': '🇦🇪'},
{'name': '墨西哥', 'code': '+52', 'flag': '🇲🇽'},
{'name': '巴西', 'code': '+55', 'flag': '🇧🇷'},
{'name': '阿根廷', 'code': '+54', 'flag': '🇦🇷'},
{'name': '澳大利亚', 'code': '+61', 'flag': '🇦🇺'},
{'name': '新西兰', 'code': '+64', 'flag': '🇳🇿'},
// 中国地区(保持最后)
{'name': '中国香港', 'code': '+852', 'flag': '🇭🇰'},
{'name': '中国澳门', 'code': '+853', 'flag': '🇲🇴'},
{'name': '中国台湾', 'code': '+886', 'flag': '🇹🇼'},
// 添加的其他主要国家
{'name': '埃及', 'code': '+20', 'flag': '🇪🇬'},
{'name': '希腊', 'code': '+30', 'flag': '🇬🇷'},
{'name': '荷兰', 'code': '+31', 'flag': '🇳🇱'},
{'name': '比利时', 'code': '+32', 'flag': '🇧🇪'},
{'name': '匈牙利', 'code': '+36', 'flag': '🇭🇺'},
{'name': '罗马尼亚', 'code': '+40', 'flag': '🇷🇴'},
{'name': '瑞士', 'code': '+41', 'flag': '🇨🇭'},
{'name': '奥地利', 'code': '+43', 'flag': '🇦🇹'},
{'name': '丹麦', 'code': '+45', 'flag': '🇩🇰'},
{'name': '瑞典', 'code': '+46', 'flag': '🇸🇪'},
{'name': '挪威', 'code': '+47', 'flag': '🇳🇴'},
{'name': '波兰', 'code': '+48', 'flag': '🇵🇱'},
{'name': '秘鲁', 'code': '+51', 'flag': '🇵🇪'},
{'name': '智利', 'code': '+56', 'flag': '🇨🇱'},
{'name': '哥伦比亚', 'code': '+57', 'flag': '🇨🇴'},
{'name': '委内瑞拉', 'code': '+58', 'flag': '🇻🇪'},
{'name': '葡萄牙', 'code': '+351', 'flag': '🇵🇹'},
{'name': '卢森堡', 'code': '+352', 'flag': '🇱🇺'},
{'name': '爱尔兰', 'code': '+353', 'flag': '🇮🇪'},
{'name': '冰岛', 'code': '+354', 'flag': '🇮🇸'},
{'name': '阿尔巴尼亚', 'code': '+355', 'flag': '🇦🇱'},
{'name': '马耳他', 'code': '+356', 'flag': '🇲🇹'},
{'name': '塞浦路斯', 'code': '+357', 'flag': '🇨🇾'},
{'name': '芬兰', 'code': '+358', 'flag': '🇫🇮'},
{'name': '保加利亚', 'code': '+359', 'flag': '🇧🇬'},
{'name': '立陶宛', 'code': '+370', 'flag': '🇱🇹'},
{'name': '拉脱维亚', 'code': '+371', 'flag': '🇱🇻'},
{'name': '爱沙尼亚', 'code': '+372', 'flag': '🇪🇪'},
{'name': '乌克兰', 'code': '+380', 'flag': '🇺🇦'},
{'name': '塞尔维亚', 'code': '+381', 'flag': '🇷🇸'},
{'name': '克罗地亚', 'code': '+385', 'flag': '🇭🇷'},
{'name': '斯洛文尼亚', 'code': '+386', 'flag': '🇸🇮'},
{'name': '捷克', 'code': '+420', 'flag': '🇨🇿'},
{'name': '斯洛伐克', 'code': '+421', 'flag': '🇸🇰'},
{'name': '危地马拉', 'code': '+502', 'flag': '🇬🇹'},
{'name': '萨尔瓦多', 'code': '+503', 'flag': '🇸🇻'},
{'name': '洪都拉斯', 'code': '+504', 'flag': '🇭🇳'},
{'name': '尼加拉瓜', 'code': '+505', 'flag': '🇳🇮'},
{'name': '哥斯达黎加', 'code': '+506', 'flag': '🇨🇷'},
{'name': '巴拿马', 'code': '+507', 'flag': '🇵🇦'},
{'name': '海地', 'code': '+509', 'flag': '🇭🇹'},
{'name': '玻利维亚', 'code': '+591', 'flag': '🇧🇴'},
{'name': '厄瓜多尔', 'code': '+593', 'flag': '🇪🇨'},
{'name': '巴拉圭', 'code': '+595', 'flag': '🇵🇾'},
{'name': '乌拉圭', 'code': '+598', 'flag': '🇺🇾'},
{'name': '摩洛哥', 'code': '+212', 'flag': '🇲🇦'},
{'name': '阿尔及利亚', 'code': '+213', 'flag': '🇩🇿'},
{'name': '突尼斯', 'code': '+216', 'flag': '🇹🇳'},
{'name': '尼日利亚', 'code': '+234', 'flag': '🇳🇬'},
{'name': '加纳', 'code': '+233', 'flag': '🇬🇭'},
{'name': '埃塞俄比亚', 'code': '+251', 'flag': '🇪🇹'},
{'name': '肯尼亚', 'code': '+254', 'flag': '🇰🇪'},
{'name': '坦桑尼亚', 'code': '+255', 'flag': '🇹🇿'},
{'name': '乌干达', 'code': '+256', 'flag': '🇺🇬'},
{'name': '孟加拉国', 'code': '+880', 'flag': '🇧🇩'},
{'name': '以色列', 'code': '+972', 'flag': '🇮🇱'},
{'name': '黎巴嫩', 'code': '+961', 'flag': '🇱🇧'},
{'name': '约旦', 'code': '+962', 'flag': '🇯🇴'},
{'name': '伊拉克', 'code': '+964', 'flag': '🇮🇶'},
{'name': '科威特', 'code': '+965', 'flag': '🇰🇼'},
{'name': '巴林', 'code': '+973', 'flag': '🇧🇭'},
{'name': '卡塔尔', 'code': '+974', 'flag': '🇶🇦'},
{'name': '阿曼', 'code': '+968', 'flag': '🇴🇲'},
{'name': '阿富汗', 'code': '+93', 'flag': '🇦🇫'},
{'name': '斯里兰卡', 'code': '+94', 'flag': '🇱🇰'},
{'name': '缅甸', 'code': '+95', 'flag': '🇲🇲'},
{'name': '尼泊尔', 'code': '+977', 'flag': '🇳🇵'},
{'name': '蒙古', 'code': '+976', 'flag': '🇲🇳'},
{'name': '柬埔寨', 'code': '+855', 'flag': '🇰🇭'},
{'name': '老挝', 'code': '+856', 'flag': '🇱🇦'},
{'name': '文莱', 'code': '+673', 'flag': '🇧🇳'},
{'name': '东帝汶', 'code': '+670', 'flag': '🇹🇱'},
{'name': '斐济', 'code': '+679', 'flag': '🇫🇯'},
{'name': '所罗门群岛', 'code': '+677', 'flag': '🇸🇧'},
{'name': '刚果民主共和国', 'code': '+243', 'flag': '🇨🇩'},
{'name': '安哥拉', 'code': '+244', 'flag': '🇦🇴'},
{'name': '苏丹', 'code': '+249', 'flag': '🇸🇩'},
{'name': '马尔代夫', 'code': '+960', 'flag': '🇲🇻'},
{'name': '不丹', 'code': '+975', 'flag': '🇧🇹'},
{'name': '摩纳哥', 'code': '+377', 'flag': '🇲🇨'},
{'name': '圣马力诺', 'code': '+378', 'flag': '🇸🇲'},
{'name': '牙买加', 'code': '+1-876', 'flag': '🇯🇲'},
{'name': '巴哈马', 'code': '+1-242', 'flag': '🇧🇸'},
{'name': '巴布亚新几内亚', 'code': '+675', 'flag': '🇵🇬'},
{'name': '萨摩亚', 'code': '+685', 'flag': '🇼🇸'},
];

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

@ -7,6 +7,7 @@ import 'package:flutter/foundation.dart';
import 'package:logger/web.dart'; import 'package:logger/web.dart';
import 'package:get_storage/get_storage.dart'; import 'package:get_storage/get_storage.dart';
import 'package:device_info_plus/device_info_plus.dart'; import 'package:device_info_plus/device_info_plus.dart';
import 'package:crypto/crypto.dart'; // 用于生成设备指纹
import 'dart:convert'; import 'dart:convert';
import '../../../data/services/db/sqflite_api.dart'; import '../../../data/services/db/sqflite_api.dart';
import 'facebookLogin.dart'; import 'facebookLogin.dart';
@ -21,6 +22,7 @@ import '../../../../data/models/user_Info.dart';
import '../../../data/models/channel_app_model.dart'; import '../../../data/models/channel_app_model.dart';
import '../../../data/services/version_update_service.dart'; import '../../../data/services/version_update_service.dart';
import '../../../modules/common/widgets/privacy_policy_dialog.dart'; import '../../../modules/common/widgets/privacy_policy_dialog.dart';
import '../constants/country_codes.dart';
enum LoginViewState { enum LoginViewState {
welcome, // 欢迎页面 welcome, // 欢迎页面
@ -42,7 +44,7 @@ class LoginController extends GetxController {
UserChannel _userChannel = UserChannel.OFFICIAL; // 默认渠道 UserChannel _userChannel = UserChannel.OFFICIAL; // 默认渠道
final isChinaMainland = false.obs; //是否中国大陆地区 final isChinaMainland = false.obs; //是否中国大陆地区
final _hasAcceptedPrivacy = false.obs; final _hasAcceptedPrivacy = false.obs; //是否同意隐私政策
bool get hasAcceptedPrivacy => _hasAcceptedPrivacy.value; bool get hasAcceptedPrivacy => _hasAcceptedPrivacy.value;
set hasAcceptedPrivacy(bool value) => _hasAcceptedPrivacy.value = value; set hasAcceptedPrivacy(bool value) => _hasAcceptedPrivacy.value = value;
@ -53,131 +55,7 @@ class LoginController extends GetxController {
String get selectedCountryCode => _selectedCountryCode.value; String get selectedCountryCode => _selectedCountryCode.value;
String get selectedCountryFlag => _selectedCountryFlag.value; String get selectedCountryFlag => _selectedCountryFlag.value;
// 预设国家编码列表 // 预设国家编码列表
final List<Map<String, String>> countryCodes = [ final List<Map<String, String>> countryCodes = kCountryCodes;
{'name': '邮箱', 'code': '', 'flag': '✉️'}, // 保持首位
{'name': '中国', 'code': '+86', 'flag': '🇨🇳'}, // 保持第二位
// 按国家代码数字升序排列(去除+号后排序)
{'name': '美国', 'code': '+1', 'flag': '🇺🇸'},
{'name': '加拿大', 'code': '+1', 'flag': '🇨🇦'},
{'name': '俄罗斯', 'code': '+7', 'flag': '🇷🇺'},
{'name': '英国', 'code': '+44', 'flag': '🇬🇧'},
{'name': '德国', 'code': '+49', 'flag': '🇩🇪'},
{'name': '意大利', 'code': '+39', 'flag': '🇮🇹'},
{'name': '法国', 'code': '+33', 'flag': '🇫🇷'},
{'name': '西班牙', 'code': '+34', 'flag': '🇪🇸'},
{'name': '南非', 'code': '+27', 'flag': '🇿🇦'},
{'name': '沙特阿拉伯', 'code': '+966', 'flag': '🇸🇦'},
{'name': '土耳其', 'code': '+90', 'flag': '🇹🇷'},
{'name': '日本', 'code': '+81', 'flag': '🇯🇵'},
{'name': '韩国', 'code': '+82', 'flag': '🇰🇷'},
{'name': '新加坡', 'code': '+65', 'flag': '🇸🇬'},
{'name': '马来西亚', 'code': '+60', 'flag': '🇲🇾'},
{'name': '泰国', 'code': '+66', 'flag': '🇹🇭'},
{'name': '越南', 'code': '+84', 'flag': '🇻🇳'},
{'name': '印度', 'code': '+91', 'flag': '🇮🇳'},
{'name': '巴基斯坦', 'code': '+92', 'flag': '🇵🇰'},
{'name': '菲律宾', 'code': '+63', 'flag': '🇵🇭'},
{'name': '印度尼西亚', 'code': '+62', 'flag': '🇮🇩'},
{'name': '阿联酋', 'code': '+971', 'flag': '🇦🇪'},
{'name': '墨西哥', 'code': '+52', 'flag': '🇲🇽'},
{'name': '巴西', 'code': '+55', 'flag': '🇧🇷'},
{'name': '阿根廷', 'code': '+54', 'flag': '🇦🇷'},
{'name': '澳大利亚', 'code': '+61', 'flag': '🇦🇺'},
{'name': '新西兰', 'code': '+64', 'flag': '🇳🇿'},
// 中国地区(保持最后)
{'name': '中国香港', 'code': '+852', 'flag': '🇭🇰'},
{'name': '中国澳门', 'code': '+853', 'flag': '🇲🇴'},
{'name': '中国台湾', 'code': '+886', 'flag': '🇹🇼'},
// 添加的其他主要国家
{'name': '埃及', 'code': '+20', 'flag': '🇪🇬'},
{'name': '希腊', 'code': '+30', 'flag': '🇬🇷'},
{'name': '荷兰', 'code': '+31', 'flag': '🇳🇱'},
{'name': '比利时', 'code': '+32', 'flag': '🇧🇪'},
{'name': '匈牙利', 'code': '+36', 'flag': '🇭🇺'},
{'name': '罗马尼亚', 'code': '+40', 'flag': '🇷🇴'},
{'name': '瑞士', 'code': '+41', 'flag': '🇨🇭'},
{'name': '奥地利', 'code': '+43', 'flag': '🇦🇹'},
{'name': '丹麦', 'code': '+45', 'flag': '🇩🇰'},
{'name': '瑞典', 'code': '+46', 'flag': '🇸🇪'},
{'name': '挪威', 'code': '+47', 'flag': '🇳🇴'},
{'name': '波兰', 'code': '+48', 'flag': '🇵🇱'},
{'name': '秘鲁', 'code': '+51', 'flag': '🇵🇪'},
{'name': '智利', 'code': '+56', 'flag': '🇨🇱'},
{'name': '哥伦比亚', 'code': '+57', 'flag': '🇨🇴'},
{'name': '委内瑞拉', 'code': '+58', 'flag': '🇻🇪'},
{'name': '葡萄牙', 'code': '+351', 'flag': '🇵🇹'},
{'name': '卢森堡', 'code': '+352', 'flag': '🇱🇺'},
{'name': '爱尔兰', 'code': '+353', 'flag': '🇮🇪'},
{'name': '冰岛', 'code': '+354', 'flag': '🇮🇸'},
{'name': '阿尔巴尼亚', 'code': '+355', 'flag': '🇦🇱'},
{'name': '马耳他', 'code': '+356', 'flag': '🇲🇹'},
{'name': '塞浦路斯', 'code': '+357', 'flag': '🇨🇾'},
{'name': '芬兰', 'code': '+358', 'flag': '🇫🇮'},
{'name': '保加利亚', 'code': '+359', 'flag': '🇧🇬'},
{'name': '立陶宛', 'code': '+370', 'flag': '🇱🇹'},
{'name': '拉脱维亚', 'code': '+371', 'flag': '🇱🇻'},
{'name': '爱沙尼亚', 'code': '+372', 'flag': '🇪🇪'},
{'name': '乌克兰', 'code': '+380', 'flag': '🇺🇦'},
{'name': '塞尔维亚', 'code': '+381', 'flag': '🇷🇸'},
{'name': '克罗地亚', 'code': '+385', 'flag': '🇭🇷'},
{'name': '斯洛文尼亚', 'code': '+386', 'flag': '🇸🇮'},
{'name': '捷克', 'code': '+420', 'flag': '🇨🇿'},
{'name': '斯洛伐克', 'code': '+421', 'flag': '🇸🇰'},
{'name': '危地马拉', 'code': '+502', 'flag': '🇬🇹'},
{'name': '萨尔瓦多', 'code': '+503', 'flag': '🇸🇻'},
{'name': '洪都拉斯', 'code': '+504', 'flag': '🇭🇳'},
{'name': '尼加拉瓜', 'code': '+505', 'flag': '🇳🇮'},
{'name': '哥斯达黎加', 'code': '+506', 'flag': '🇨🇷'},
{'name': '巴拿马', 'code': '+507', 'flag': '🇵🇦'},
{'name': '海地', 'code': '+509', 'flag': '🇭🇹'},
{'name': '玻利维亚', 'code': '+591', 'flag': '🇧🇴'},
{'name': '厄瓜多尔', 'code': '+593', 'flag': '🇪🇨'},
{'name': '巴拉圭', 'code': '+595', 'flag': '🇵🇾'},
{'name': '乌拉圭', 'code': '+598', 'flag': '🇺🇾'},
{'name': '摩洛哥', 'code': '+212', 'flag': '🇲🇦'},
{'name': '阿尔及利亚', 'code': '+213', 'flag': '🇩🇿'},
{'name': '突尼斯', 'code': '+216', 'flag': '🇹🇳'},
{'name': '尼日利亚', 'code': '+234', 'flag': '🇳🇬'},
{'name': '加纳', 'code': '+233', 'flag': '🇬🇭'},
{'name': '埃塞俄比亚', 'code': '+251', 'flag': '🇪🇹'},
{'name': '肯尼亚', 'code': '+254', 'flag': '🇰🇪'},
{'name': '坦桑尼亚', 'code': '+255', 'flag': '🇹🇿'},
{'name': '乌干达', 'code': '+256', 'flag': '🇺🇬'},
{'name': '孟加拉国', 'code': '+880', 'flag': '🇧🇩'},
{'name': '以色列', 'code': '+972', 'flag': '🇮🇱'},
{'name': '黎巴嫩', 'code': '+961', 'flag': '🇱🇧'},
{'name': '约旦', 'code': '+962', 'flag': '🇯🇴'},
{'name': '伊拉克', 'code': '+964', 'flag': '🇮🇶'},
{'name': '科威特', 'code': '+965', 'flag': '🇰🇼'},
{'name': '巴林', 'code': '+973', 'flag': '🇧🇭'},
{'name': '卡塔尔', 'code': '+974', 'flag': '🇶🇦'},
{'name': '阿曼', 'code': '+968', 'flag': '🇴🇲'},
{'name': '阿富汗', 'code': '+93', 'flag': '🇦🇫'},
{'name': '斯里兰卡', 'code': '+94', 'flag': '🇱🇰'},
{'name': '缅甸', 'code': '+95', 'flag': '🇲🇲'},
{'name': '尼泊尔', 'code': '+977', 'flag': '🇳🇵'},
{'name': '蒙古', 'code': '+976', 'flag': '🇲🇳'},
{'name': '柬埔寨', 'code': '+855', 'flag': '🇰🇭'},
{'name': '老挝', 'code': '+856', 'flag': '🇱🇦'},
{'name': '文莱', 'code': '+673', 'flag': '🇧🇳'},
{'name': '东帝汶', 'code': '+670', 'flag': '🇹🇱'},
{'name': '斐济', 'code': '+679', 'flag': '🇫🇯'},
{'name': '所罗门群岛', 'code': '+677', 'flag': '🇸🇧'},
{'name': '刚果民主共和国', 'code': '+243', 'flag': '🇨🇩'},
{'name': '安哥拉', 'code': '+244', 'flag': '🇦🇴'},
{'name': '苏丹', 'code': '+249', 'flag': '🇸🇩'},
{'name': '马尔代夫', 'code': '+960', 'flag': '🇲🇻'},
{'name': '不丹', 'code': '+975', 'flag': '🇧🇹'},
{'name': '摩纳哥', 'code': '+377', 'flag': '🇲🇨'},
{'name': '圣马力诺', 'code': '+378', 'flag': '🇸🇲'},
{'name': '牙买加', 'code': '+1-876', 'flag': '🇯🇲'},
{'name': '巴哈马', 'code': '+1-242', 'flag': '🇧🇸'},
{'name': '巴布亚新几内亚', 'code': '+675', 'flag': '🇵🇬'},
{'name': '萨摩亚', 'code': '+685', 'flag': '🇼🇸'},
];
// 初始化默认选择的国家代码 // 初始化默认选择的国家代码
void _initDefaultCountryCode() { void _initDefaultCountryCode() {
@ -236,7 +114,7 @@ class LoginController extends GetxController {
_verificationCodeController = TextEditingController(); _verificationCodeController = TextEditingController();
isChinaMainland.value = isChinaMainland.value =
Get.deviceLocale?.countryCode?.toUpperCase() == 'CN'; Get.deviceLocale?.countryCode?.toUpperCase() == 'CN'; //是否中国大陆地区
// 初始化默认国家代码 // 初始化默认国家代码
_initDefaultCountryCode(); _initDefaultCountryCode();
@ -304,9 +182,27 @@ class LoginController extends GetxController {
_deviceId = androidInfo.id; _deviceId = androidInfo.id;
} else if (GetPlatform.isIOS) { } else if (GetPlatform.isIOS) {
IosDeviceInfo iosInfo = await deviceInfo.iosInfo; IosDeviceInfo iosInfo = await deviceInfo.iosInfo;
_deviceId = iosInfo.identifierForVendor ?? 'unknown'; //print('lxm---iosInfo: $iosInfo.toString()');
final utsname = iosInfo.utsname; // 包含更详细的设备信息
//print('lxm---utsname: $utsname');
final machineId = utsname.machine.toLowerCase(); // 例如: "iPhone14,2"
Logger().d('lxm---machineId: $machineId');
final systemVersion = utsname.release; // 例如: "16.4.1"
Logger().d('lxm---systemVersion: $systemVersion');
try {
// 组合 machine 和 systemVersion 生成指纹
final fingerprint = '$machineId|$systemVersion';
final hashedFingerprint =
md5.convert(utf8.encode(fingerprint)).toString();
Logger()
.d('生成设备指纹: $hashedFingerprint (基于 $machineId 和 $systemVersion)');
_deviceId = hashedFingerprint;
} catch (e) {
Logger().e('生成设备指纹失败: $e');
_deviceId = iosInfo.identifierForVendor ?? 'unknown';
}
} }
Logger().d('deviceId: $_deviceId');
} catch (e) { } catch (e) {
Logger().e('获取设备信息失败: $e'); Logger().e('获取设备信息失败: $e');
_deviceId = 'unknown'; _deviceId = 'unknown';

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

@ -5,6 +5,7 @@ import 'package:get/get.dart';
import '../../../../routes/app_routes.dart'; import '../../../../routes/app_routes.dart';
import '../../controllers/meeting_home_controller.dart'; import '../../controllers/meeting_home_controller.dart';
///会议底部导航栏抽屉,提供会议列表、会议详情、会议设置
class NavigationBarBottomSheet extends GetView<MeetingHomeController> { class NavigationBarBottomSheet extends GetView<MeetingHomeController> {
const NavigationBarBottomSheet({super.key}); const NavigationBarBottomSheet({super.key});

26
lib/modules/meeting/views/meeting_details_view.dart

@ -17,6 +17,7 @@ import 'bottomSheet/share_bottom_sheet.dart';
import 'tabs/speech_tab.dart'; import 'tabs/speech_tab.dart';
import 'tabs/summary_tab.dart'; import 'tabs/summary_tab.dart';
///会议音频详情页面,提供会议音频播放、会议音频分享、会议音频更多操作
class MeetingDetailsView extends GetView<MeetingDetailsController> { class MeetingDetailsView extends GetView<MeetingDetailsController> {
const MeetingDetailsView({super.key}); const MeetingDetailsView({super.key});
@ -40,10 +41,9 @@ class MeetingDetailsView extends GetView<MeetingDetailsController> {
preferredSize: const Size.fromHeight(kToolbarHeight), preferredSize: const Size.fromHeight(kToolbarHeight),
child: Obx( child: Obx(
() => AppBar( () => AppBar(
backgroundColor: backgroundColor: isDarkMode
isDarkMode ? (controller.isTop.value ? Colors.black : Colors.grey[900])
? (controller.isTop.value ? Colors.black : Colors.grey[900]) : (controller.isTop.value ? Colors.grey[50] : Colors.grey[200]),
: (controller.isTop.value ? Colors.grey[50] : Colors.grey[200]),
surfaceTintColor: Colors.transparent, surfaceTintColor: Colors.transparent,
elevation: 0.5, elevation: 0.5,
leading: controller.isSpeakerText.value leading: controller.isSpeakerText.value
@ -202,7 +202,8 @@ class MeetingDetailsView extends GetView<MeetingDetailsController> {
Get.bottomSheet( Get.bottomSheet(
const ShareBottomSheet(), const ShareBottomSheet(),
isScrollControlled: true, isScrollControlled: true,
backgroundColor: isDarkMode ? Colors.grey[800] : Colors.grey[100], backgroundColor:
isDarkMode ? Colors.grey[800] : Colors.grey[100],
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.vertical( borderRadius: BorderRadius.vertical(
top: Radius.circular(12.r), top: Radius.circular(12.r),
@ -234,7 +235,8 @@ class MeetingDetailsView extends GetView<MeetingDetailsController> {
Get.bottomSheet( Get.bottomSheet(
const MoreBottomSheet(), const MoreBottomSheet(),
isScrollControlled: true, isScrollControlled: true,
backgroundColor: isDarkMode ? Colors.grey[800] : Colors.grey[100], backgroundColor:
isDarkMode ? Colors.grey[800] : Colors.grey[100],
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.vertical( borderRadius: BorderRadius.vertical(
top: Radius.circular(12.r), top: Radius.circular(12.r),
@ -301,10 +303,14 @@ class MeetingDetailsView extends GetView<MeetingDetailsController> {
child: Obx( child: Obx(
() => Row( () => Row(
children: [ children: [
_iconItem(Icons.summarize, 'overview'.tr, 0), // 对应中文:概览 _iconItem(
_iconItem(Icons.message, 'transcription'.tr, 1), // 对应中文:转写 Icons.summarize, 'overview'.tr, 0), // 对应中文:概览
_iconItem(Icons.article, 'summary'.tr, 2), // 对应中文:总结 _iconItem(Icons.message, 'transcription'.tr,
_iconItem(Icons.lan, 'mindMap'.tr, 3), // 对应中文:思维导图 1), // 对应中文:转写
_iconItem(
Icons.article, 'summary'.tr, 2), // 对应中文:总结
_iconItem(
Icons.lan, 'mindMap'.tr, 3), // 对应中文:思维导图
], ],
), ),
), ),

1
lib/modules/meeting/views/meeting_home_view.dart

@ -11,6 +11,7 @@ import '../model/meeting_model.dart';
import '../utils/meeting_ui_utils.dart'; import '../utils/meeting_ui_utils.dart';
import 'bottomSheet/edit_title_bottom_sheet.dart'; import 'bottomSheet/edit_title_bottom_sheet.dart';
///会议主页面,提供会议列表、会议详情、会议设置
class MeetingHomeView extends StatefulWidget { class MeetingHomeView extends StatefulWidget {
const MeetingHomeView({super.key}); const MeetingHomeView({super.key});

1
lib/modules/meeting/views/meeting_view.dart

@ -9,6 +9,7 @@ import '../utils/meeting_ui_utils.dart';
import 'bottomSheet/filtrate_drawer.dart'; import 'bottomSheet/filtrate_drawer.dart';
import 'bottomSheet/navigation_bar_bottom_sheet.dart'; import 'bottomSheet/navigation_bar_bottom_sheet.dart';
///meeting模块主容器页面,提供底部导航栏、浮动按钮、侧板抽屉
class MeetingView extends GetView<MeetingController> { class MeetingView extends GetView<MeetingController> {
const MeetingView({super.key}); const MeetingView({super.key});

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

@ -480,13 +480,14 @@ object AgentService : CoroutineScope {
"phone_call" -> { "phone_call" -> {
// 通话模式:禁用空闲检测,保持持续激活 // 通话模式:禁用空闲检测,保持持续激活
Log.d(TAG, "通话模式:禁用空闲检测") Log.d(TAG, "通话模式:禁用空闲检测")
startIdleCheck(mode,maxIdleSeconds)
return return
} }
else -> { else -> {
// 未知模式:使用默认的空闲检测 // 未知模式:使用默认的空闲检测
Log.w(TAG, "未知识别模式: $mode,使用默认空闲检测") Log.w(TAG, "未知识别模式: $mode,使用默认空闲检测")
startIdleCheck(maxIdleSeconds) startIdleCheck(mode,maxIdleSeconds)
} }
} }
} }
@ -494,7 +495,7 @@ object AgentService : CoroutineScope {
/** /**
* 启动空闲检测 * 启动空闲检测
*/ */
private fun startIdleCheck(idleSeconds: Int = maxIdleSeconds) { private fun startIdleCheck(mode: String,idleSeconds: Int = maxIdleSeconds) {
stopIdleCheck() // 先停止现有的检查 stopIdleCheck() // 先停止现有的检查
if (!isRecognitionActive) return if (!isRecognitionActive) return
@ -503,8 +504,8 @@ object AgentService : CoroutineScope {
idleCheckJob = launch { idleCheckJob = launch {
try { try {
delay(idleSeconds * 1000L) delay(idleSeconds * 1000L)
Log.d(TAG, "空闲检测触发自动停止,isActive: $isActive, isRecognitionActive: $isRecognitionActive, isTtsSpeaking: $isTtsSpeaking, isAiStreaming: $isAiStreaming, mode: $mode")
if (isActive && isRecognitionActive && !isTtsSpeaking && !isAiStreaming) { if (isActive && isRecognitionActive && !isTtsSpeaking && !isAiStreaming && mode != "phone_call") {
Log.d(TAG, "空闲检测触发自动停止,模式: $currentRecognitionMode") Log.d(TAG, "空闲检测触发自动停止,模式: $currentRecognitionMode")
BleService.closeCodec() BleService.closeCodec()
stopRecognition() stopRecognition()
@ -517,6 +518,10 @@ object AgentService : CoroutineScope {
) )
) )
} }
if (isActive && isRecognitionActive && !isTtsSpeaking && !isAiStreaming && mode == "phone_call") {
Log.d(TAG, "空闲检测触发自动停止,模式: $currentRecognitionMode")
startRecognition(false, mode)
}
} catch (e: CancellationException) { } catch (e: CancellationException) {
// 协程被取消,正常行为 // 协程被取消,正常行为
} catch (e: Exception) { } catch (e: Exception) {
@ -531,6 +536,7 @@ object AgentService : CoroutineScope {
private fun stopIdleCheck() { private fun stopIdleCheck() {
idleCheckJob?.cancel() idleCheckJob?.cancel()
idleCheckJob = null idleCheckJob = null
Log.d(TAG, "空闲检测已停止")
} }
/** /**
@ -643,11 +649,9 @@ object AgentService : CoroutineScope {
override fun onRecognizing(sessionid:String, recognizing: String, detectedLanguage: String) { override fun onRecognizing(sessionid:String, recognizing: String, detectedLanguage: String) {
if (recognizing.isNotEmpty()&&isKeepResult==true) { if (recognizing.isNotEmpty()&&isKeepResult==true) {
// 检测到语音,更新状态 // 检测到语音,更新状态
_hasSpeechDetected.set(true)
_hasSpeechDetected.set(true) Log.d(TAG, "ai识别中重置定时器")
restartIdleCheck()
Log.d(TAG, "ai识别中重置定时器")
restartIdleCheck()
sendEvent( sendEvent(

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

@ -18,6 +18,7 @@ import com.deep_voice.speech.tts.TtsEventListener
import com.deep_voice.speech.tts.TtsEventType import com.deep_voice.speech.tts.TtsEventType
import com.yunqiinnovation.ble_service.BleService import com.yunqiinnovation.ble_service.BleService
import com.deep_voice.speech.tts.AudioOutputDevice import com.deep_voice.speech.tts.AudioOutputDevice
import com.deep_voice.speech.tts.AudioDataListener
import kotlinx.coroutines.* import kotlinx.coroutines.*
import com.yunqiinnovation.azure_speech.tools.RecordFile import com.yunqiinnovation.azure_speech.tools.RecordFile
/** AzureSpeechPlugin */ /** AzureSpeechPlugin */
@ -105,6 +106,70 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin {
} }
} }
} }
/**
* 从WAV格式音频数据中提取纯PCM数据
*
* @param wavData WAV格式的音频数据
* @return 纯PCM音频数据
*/
private fun extractPcmFromWav(wavData: ByteArray): ByteArray {
try {
// WAV文件头最小长度为44字节
if (wavData.size < 44) {
FileLogger.e(tag, "WAV数据长度不足,无法解析文件头")
return ByteArray(0)
}
// 检查RIFF标识符
val riffHeader = String(wavData, 0, 4, Charsets.US_ASCII)
if (riffHeader != "RIFF") {
FileLogger.e(tag, "不是有效的RIFF WAV文件,标识符: $riffHeader")
return ByteArray(0)
}
// 检查WAVE标识符
val waveHeader = String(wavData, 8, 4, Charsets.US_ASCII)
if (waveHeader != "WAVE") {
FileLogger.e(tag, "不是有效的WAVE文件,标识符: $waveHeader")
return ByteArray(0)
}
// 查找data chunk
var offset = 12 // 跳过RIFF头部
while (offset < wavData.size - 8) {
val chunkId = String(wavData, offset, 4, Charsets.US_ASCII)
val chunkSize = (wavData[offset + 4].toInt() and 0xFF) or
((wavData[offset + 5].toInt() and 0xFF) shl 8) or
((wavData[offset + 6].toInt() and 0xFF) shl 16) or
((wavData[offset + 7].toInt() and 0xFF) shl 24)
if (chunkId == "data") {
// 找到data chunk,提取PCM数据
val dataOffset = offset + 8
val dataSize = minOf(chunkSize, wavData.size - dataOffset)
if (dataSize > 0) {
val pcmData = ByteArray(dataSize)
System.arraycopy(wavData, dataOffset, pcmData, 0, dataSize)
FileLogger.d(tag, "成功提取PCM数据: ${pcmData.size}字节")
return pcmData
}
break
}
// 移动到下一个chunk
offset += 8 + chunkSize
}
FileLogger.e(tag, "未找到data chunk")
return ByteArray(0)
} catch (e: Exception) {
FileLogger.e(tag, "解析WAV数据异常: ${e.message}")
return ByteArray(0)
}
}
// 音频数据 事件发送方法 // 音频数据 事件发送方法
private fun sendAudioDataEvent(event: Map<String, Any>) { private fun sendAudioDataEvent(event: Map<String, Any>) {
if (audioDataEventSink == null) { if (audioDataEventSink == null) {
@ -937,7 +1002,15 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin {
} }
override fun onSynthesisAudioGenerated(text: String, audioData: ByteArray) { override fun onSynthesisAudioGenerated(text: String, audioData: ByteArray) {
FileLogger.d(tag, "语音合成音频生成,文本=${text},音频数据大小=${audioData.size}") FileLogger.d(tag, "语音合成音频生成,文本=${text},音频数据大小=${audioData.size}")
BleService.writeExternalAudioData(audioData) // Azure TTS返回的是RIFF WAV格式,需要去除文件头
val pcmData = extractPcmFromWav(audioData)
if (pcmData.isNotEmpty()) {
FileLogger.d(tag, "提取PCM数据,大小=${pcmData.size}字节")
BleService.writeExternalAudioData(pcmData)
}
else {
FileLogger.e(tag, "提取PCM数据失败,音频数据可能不是WAV格式")
}
} }
override fun onSynthesisFailed(text: String, error: String) { override fun onSynthesisFailed(text: String, error: String) {
// sendAstEvent( // sendAstEvent(

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

@ -18,7 +18,7 @@ import ble_service
// 日志标签 // 日志标签
private let tag = "AzureSpeechPlugin" private let tag = "AzureSpeechPlugin"
// 日志对象 // 日志对象
private let log = OSLog(subsystem: "com.azure.speech", category: "AzureSpeechPlugin") internal let log = OSLog(subsystem: "com.azure.speech", category: "AzureSpeechPlugin")
// MARK: - 防抖相关(iOS 原生层) // MARK: - 防抖相关(iOS 原生层)
// 函数级注释:用于限制 startContinuousRecognition/stopContinuousRecognition 在极短时间内的重复触发 // 函数级注释:用于限制 startContinuousRecognition/stopContinuousRecognition 在极短时间内的重复触发
@ -264,6 +264,65 @@ private func sendAstEvent(_ event: [String: Any]) {
sendAudioDataEvent(eventMap) sendAudioDataEvent(eventMap)
} }
/**
* 从WAV格式音频数据中提取纯PCM数据
* @param wavData WAV格式的音频数据
* @return 纯PCM音频数据,如果提取失败则返回空Data
*/
internal func extractPcmFromWav(_ wavData: Data) -> Data {
// WAV文件头最小长度为44字节
guard wavData.count >= 44 else {
os_log("WAV数据长度不足,无法解析文件头", log: log, type: .error)
return Data()
}
// 检查RIFF标识符
let riffHeader = String(data: wavData.subdata(in: 0..<4), encoding: .ascii) ?? ""
guard riffHeader == "RIFF" else {
os_log("不是有效的RIFF WAV文件,标识符: %@", log: log, type: .error, riffHeader)
return Data()
}
// 检查WAVE标识符
let waveHeader = String(data: wavData.subdata(in: 8..<12), encoding: .ascii) ?? ""
guard waveHeader == "WAVE" else {
os_log("不是有效的WAVE文件,标识符: %@", log: log, type: .error, waveHeader)
return Data()
}
// 查找data chunk
var offset = 12 // 跳过RIFF头部
while offset < wavData.count - 8 {
let chunkIdData = wavData.subdata(in: offset..<(offset + 4))
let chunkId = String(data: chunkIdData, encoding: .ascii) ?? ""
// 读取chunk大小(小端序)
let chunkSizeData = wavData.subdata(in: (offset + 4)..<(offset + 8))
let chunkSize = chunkSizeData.withUnsafeBytes { bytes in
return bytes.load(as: UInt32.self).littleEndian
}
if chunkId == "data" {
// 找到data chunk,提取PCM数据
let dataOffset = offset + 8
let dataSize = min(Int(chunkSize), wavData.count - dataOffset)
if dataSize > 0 {
let pcmData = wavData.subdata(in: dataOffset..<(dataOffset + dataSize))
os_log("成功提取PCM数据: %d字节", log: log, type: .info, pcmData.count)
return pcmData
}
break
}
// 移动到下一个chunk
offset += 8 + Int(chunkSize)
}
os_log("未找到data chunk", log: log, type: .error)
return Data()
}
// MARK: - ASR 方法处理 // MARK: - ASR 方法处理
private func handleAsrMethodCall(_ call: FlutterMethodCall, result: @escaping FlutterResult) { private func handleAsrMethodCall(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
@ -950,9 +1009,18 @@ private class AstEventCallback: IntegratedSpeechTranslationService.ServiceEventC
* @param audioData 音频数据 * @param audioData 音频数据
*/ */
public func onSynthesisAudioGenerated(text: String, audioData: Data) { public func onSynthesisAudioGenerated(text: String, audioData: Data) {
print("语音合成音频数据生成回调", text, audioData.count) guard let plugin = plugin else { return }
// 调用BLE服务写入音频数据
BleService.shared.writeExternalAudioData(data: audioData) os_log("语音合成音频生成,文本=%@,音频数据大小=%d", log: plugin.log, type: .info, text, audioData.count)
// Azure TTS返回的是RIFF WAV格式,需要去除文件头提取PCM数据
let pcmData = plugin.extractPcmFromWav(audioData)
if !pcmData.isEmpty {
os_log("提取PCM数据,大小=%d字节", log: plugin.log, type: .info, pcmData.count)
BleService.shared.writeExternalAudioData(data: pcmData)
} else {
os_log("提取PCM数据失败,音频数据可能不是WAV格式", log: plugin.log, type: .error)
}
// plugin?.sendAsrEvent([ // plugin?.sendAsrEvent([
// "type": "synthesisAudioGenerated", // "type": "synthesisAudioGenerated",

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

@ -37,14 +37,9 @@ public class MusicService: NSObject {
private override init() { private override init() {
super.init() super.init()
// 确保应用能接收远程控制事件
UIApplication.shared.beginReceivingRemoteControlEvents()
setupRemoteCommandCenter() setupRemoteCommandCenter()
setupAudioSession() setupAudioSession()
setupNotificationObservers()
print("🎵 [MusicService] 🚀 MusicService初始化完成,远程控制已激活")
} }
public func initialize(serviceUrl: String, userToken: String) { public func initialize(serviceUrl: String, userToken: String) {
@ -54,33 +49,33 @@ public class MusicService: NSObject {
} }
// MARK: - 音频会话设置 // MARK: - 音频会话设置
private func setupAudioSession() { private func setupAudioSession() {
print("🎵 [MusicService] 🚀 开始设置音频会话和通知监听器") do {
// 设置音频会话类别,添加 mixWithOthers 选项以确保通知正常显示
// 先注册通知监听器,不依赖音频会话设置成功 try AVAudioSession.sharedInstance().setCategory(
setupNotificationObservers() .playback,
mode: .default,
// 然后尝试设置音频会话 options: [.allowAirPlay, .allowBluetooth, .mixWithOthers]
setupAudioSessionConfiguration() )
try AVAudioSession.sharedInstance().setActive(true)
// 确保媒体控制中心可以接收到我们的应用 print("🎵 [MusicService] ✅ 音频会话设置成功")
setupRemoteCommandCenter() } catch {
print("🎵 [MusicService] ❌ 设置音频会话失败: \(error)")
print("🎵 [MusicService] 📋 所有通知观察者注册完成") // 尝试备用配置
print("🎵 [MusicService] 🔍 当前MusicService实例: \(Unmanaged.passUnretained(self).toOpaque())") do {
try AVAudioSession.sharedInstance().setCategory(.playback)
try AVAudioSession.sharedInstance().setActive(true)
print("🎵 [MusicService] ✅ 使用备用音频会话配置")
} catch {
print("🎵 [MusicService] ❌ 备用音频会话配置也失败: \(error)")
}
}
} }
private func setupNotificationObservers() { private func setupNotificationObservers() {
// 监听系统音频中断(来电/通话/Siri等)
NotificationCenter.default.addObserver(
self,
selector: #selector(handleAudioInterruption(_:)),
name: AVAudioSession.interruptionNotification,
object: nil
)
print("🎵 [MusicService] ✅ 已注册系统音频中断通知") print("🎵 [MusicService] ✅ 已注册系统音频中断通知")
// 注册AudioSessionHub的自定义中断通知 // 注册AudioSessionHub的自定义中断通知
NotificationCenter.default.addObserver( NotificationCenter.default.addObserver(
self, self,
@ -108,31 +103,6 @@ public class MusicService: NSObject {
print("🎵 [MusicService] ✅ 已注册AudioSessionHub_ResumeMusicPlayback通知") print("🎵 [MusicService] ✅ 已注册AudioSessionHub_ResumeMusicPlayback通知")
} }
private func setupAudioSessionConfiguration() {
do {
let audioSession = AVAudioSession.sharedInstance()
// 使用更简单的配置,避免参数冲突
try audioSession.setCategory(.playback, mode: .default, options: [.allowAirPlay, .allowBluetooth, .allowBluetoothA2DP])
print("🎵 [MusicService] ✅ 音频会话类别设置成功")
// 延迟激活音频会话,避免与其他服务冲突
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
do {
try audioSession.setActive(true)
print("🎵 [MusicService] ✅ 音频会话激活成功")
} catch {
print("🎵 [MusicService] ⚠️ 音频会话激活失败: \(error)")
// 激活失败不影响通知系统工作
}
}
} catch {
print("🎵 [MusicService] ❌ 设置音频会话失败: \(error)")
print("🎵 [MusicService] ℹ️ 音频会话设置失败不影响通知系统工作")
}
}
// MARK: - 基础播放控制 // MARK: - 基础播放控制
func getCurrentUrlString() -> String? { func getCurrentUrlString() -> String? {
guard let urlAsset = player?.currentItem?.asset as? AVURLAsset else { guard let urlAsset = player?.currentItem?.asset as? AVURLAsset else {
@ -429,16 +399,6 @@ public class MusicService: NSObject {
print("开始播放: \(url), ID: \(currentIdInternal ?? 0)") print("开始播放: \(url), ID: \(currentIdInternal ?? 0)")
// 强制设置音频会话为活跃状态
do {
let audioSession = AVAudioSession.sharedInstance()
try audioSession.setCategory(.playback, mode: .default, options: [.mixWithOthers])
try audioSession.setActive(true, options: [])
print("🎵 [StartPlayback] ✅ 音频会话强制激活成功")
} catch {
print("🎵 [StartPlayback] ⚠️ 音频会话设置失败: \(error)")
}
// 检查是否已在播放列表中 // 检查是否已在播放列表中
let existingIndex = playlist.firstIndex { existingSong in let existingIndex = playlist.firstIndex { existingSong in
if let existingId = existingSong["id"] as? NSNumber, if let existingId = existingSong["id"] as? NSNumber,
@ -468,15 +428,6 @@ public class MusicService: NSObject {
currentVolume = 1.0 currentVolume = 1.0
player?.volume = currentVolume player?.volume = currentVolume
// 立即设置媒体信息,即使播放器还未准备就绪
updateNowPlayingInfo(song: song)
// 强制重新设置远程控制中心
setupRemoteCommandCenter()
// 强制激活远程控制
UIApplication.shared.beginReceivingRemoteControlEvents()
print("播放器已创建,等待加载完成...") print("播放器已创建,等待加载完成...")
} }
@ -486,12 +437,6 @@ public class MusicService: NSObject {
isPlaying = false isPlaying = false
updatePlaybackState() updatePlaybackState()
updateMediaSessionPlaybackState(isPlaying: false) updateMediaSessionPlaybackState(isPlaying: false)
// 更新媒体信息,保持通知栏显示但状态为暂停
if currentIndex < playlist.count {
updateNowPlayingInfo(song: playlist[currentIndex])
}
stopProgressTimer() stopProgressTimer()
print("已暂停") print("已暂停")
} }
@ -501,12 +446,6 @@ public class MusicService: NSObject {
isPlaying = true isPlaying = true
updatePlaybackState() updatePlaybackState()
updateMediaSessionPlaybackState(isPlaying: true) updateMediaSessionPlaybackState(isPlaying: true)
// 重新设置媒体信息,确保通知栏显示
if currentIndex < playlist.count {
updateNowPlayingInfo(song: playlist[currentIndex])
}
startProgressTimer() startProgressTimer()
print("继续播放") print("继续播放")
} }
@ -598,10 +537,10 @@ public class MusicService: NSObject {
let progress = (totalDuration > 0 && currentTime >= 0) ? currentTime / duration : 0.0 let progress = (totalDuration > 0 && currentTime >= 0) ? currentTime / duration : 0.0
let result: [String: Any] = [ // Add explicit type annotation here let result: [String: Any] = [ // Add explicit type annotation here
"currentPosition": currentPosition, "currentPosition": currentPosition,
"duration": totalDuration, "duration": totalDuration,
"progress": progress.isNaN || progress.isInfinite ? 0.0 : progress "progress": progress.isNaN || progress.isInfinite ? 0.0 : progress
] ]
// 添加调试日志 // 添加调试日志
@ -624,221 +563,216 @@ public class MusicService: NSObject {
if completed { if completed {
print("跳转到位置: \(positionMs)ms") print("跳转到位置: \(positionMs)ms")
// 如果之前在播放,跳转后继续播放 // 如果之前是播放状态,确保继续播放
if wasPlaying { if wasPlaying {
self?.resume() self?.player?.play()
self?.isPlaying = true
self?.updateMediaSessionPlaybackState(isPlaying: true)
self?.startProgressTimer()
} }
} else {
print("跳转失败") // 立即更新进度信息到系统媒体控制中心
self?.updateProgressInfoImmediately()
// 通知Flutter层
self?.flutterChannel?.invokeMethod("onMediaSessionSeekTo", arguments: ["position": positionMs])
} }
} }
} }
// MARK: - 远程控制中心设置 // MARK: - 媒体会话控制
private func setupRemoteCommandCenter() { private func setupRemoteCommandCenter() {
let commandCenter = MPRemoteCommandCenter.shared() let commandCenter = MPRemoteCommandCenter.shared()
// 清除之前的目标,避免重复注册 // 清除之前的目标,避免重复添加
commandCenter.playCommand.removeTarget(nil) commandCenter.playCommand.removeTarget(nil)
commandCenter.pauseCommand.removeTarget(nil) commandCenter.pauseCommand.removeTarget(nil)
commandCenter.nextTrackCommand.removeTarget(nil) commandCenter.nextTrackCommand.removeTarget(nil)
commandCenter.previousTrackCommand.removeTarget(nil) commandCenter.previousTrackCommand.removeTarget(nil)
commandCenter.changePlaybackPositionCommand.removeTarget(nil) commandCenter.stopCommand.removeTarget(nil)
// 启用命令
commandCenter.playCommand.isEnabled = true
commandCenter.pauseCommand.isEnabled = true
commandCenter.nextTrackCommand.isEnabled = true
commandCenter.previousTrackCommand.isEnabled = true
commandCenter.stopCommand.isEnabled = true
// 播放命令
commandCenter.playCommand.addTarget { [weak self] _ in commandCenter.playCommand.addTarget { [weak self] _ in
print("🎵 [RemoteControl] 播放命令") print("🎵 [RemoteCommand] 播放命令触发")
self?.resume() self?.resume()
return .success return .success
} }
// 暂停命令
commandCenter.pauseCommand.addTarget { [weak self] _ in commandCenter.pauseCommand.addTarget { [weak self] _ in
print("🎵 [RemoteControl] 暂停命令") print("🎵 [RemoteCommand] 暂停命令触发")
self?.pause() self?.pause()
return .success return .success
} }
// 下一首命令
commandCenter.nextTrackCommand.addTarget { [weak self] _ in commandCenter.nextTrackCommand.addTarget { [weak self] _ in
print("🎵 [RemoteControl] 下一首命令") print("🎵 [RemoteCommand] 下一首命令触发")
self?.playNext() self?.next()
return .success return .success
} }
// 上一首命令
commandCenter.previousTrackCommand.addTarget { [weak self] _ in commandCenter.previousTrackCommand.addTarget { [weak self] _ in
print("🎵 [RemoteControl] 上一首命令") print("🎵 [RemoteCommand] 上一首命令触发")
self?.playPrevious() self?.previous()
return .success return .success
} }
// 进度调整命令(iOS 9.1+) commandCenter.stopCommand.addTarget { [weak self] _ in
if #available(iOS 9.1, *) { print("🎵 [RemoteCommand] 停止命令触发")
commandCenter.changePlaybackPositionCommand.addTarget { [weak self] event in self?.stop()
if let positionEvent = event as? MPChangePlaybackPositionCommandEvent { return .success
let positionMs = Int(positionEvent.positionTime * 1000)
print("🎵 [RemoteControl] 跳转到 \(positionMs)ms")
self?.seekTo(positionMs: positionMs)
return .success
}
return .commandFailed
}
commandCenter.changePlaybackPositionCommand.isEnabled = true
}
// 启用远程控制命令
commandCenter.playCommand.isEnabled = true
commandCenter.pauseCommand.isEnabled = true
commandCenter.nextTrackCommand.isEnabled = true
commandCenter.previousTrackCommand.isEnabled = true
// 确保音频会话支持远程控制
do {
let audioSession = AVAudioSession.sharedInstance()
try audioSession.setCategory(.playback, mode: .default, options: [.mixWithOthers])
print("🎵 [RemoteControl] 音频会话配置成功")
} catch {
print("🎵 [RemoteControl] ⚠️ 音频会话配置失败: \(error)")
} }
print("🎵 [RemoteControl] ✅ 远程控制中心设置完成") print("🎵 [MusicService] ✅ 远程控制中心设置完成")
} }
// MARK: - 媒体信息更新
private func updateNowPlayingInfo(song: [String: Any]) { private func updateNowPlayingInfo(song: [String: Any]) {
var nowPlayingInfo = [String: Any]() let title = song["title"] as? String ?? song["name"] as? String ?? "未知标题"
let artist = song["artist"] as? String ?? song["sgener"] as? String ?? "未知歌手"
let coverUrl = song["coverUrl"] as? String ?? song["image"] as? String ?? ""
// 设置歌曲信息 var nowPlayingInfo: [String: Any] = [
nowPlayingInfo[MPMediaItemPropertyTitle] = song["name"] as? String ?? "未知歌曲" MPMediaItemPropertyTitle: title,
nowPlayingInfo[MPMediaItemPropertyArtist] = song["singer"] as? String ?? "未知歌手" MPMediaItemPropertyArtist: artist
]
// 设置专辑信息(如果有的话)
if let album = song["album"] as? String, !album.isEmpty {
nowPlayingInfo[MPMediaItemPropertyAlbumTitle] = album
}
// 设置播放时间信息
if let player = player, let currentItem = player.currentItem {
let currentTime = CMTimeGetSeconds(player.currentTime())
let duration = CMTimeGetSeconds(currentItem.duration)
if !currentTime.isNaN && !currentTime.isInfinite {
nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = currentTime
}
if !duration.isNaN && !duration.isInfinite && duration > 0 { // 异步加载封面图片
nowPlayingInfo[MPMediaItemPropertyPlaybackDuration] = duration if !coverUrl.isEmpty, let url = URL(string: coverUrl) {
DispatchQueue.global(qos: .background).async {
if let data = try? Data(contentsOf: url),
let image = UIImage(data: data) {
DispatchQueue.main.async {
nowPlayingInfo[MPMediaItemPropertyArtwork] = MPMediaItemArtwork(boundsSize: image.size) { _ in image }
MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
}
} else {
DispatchQueue.main.async {
MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
}
}
} }
} } else {
// 设置播放速率
nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = isPlaying ? 1.0 : 0.0
// 设置媒体类型为音频
nowPlayingInfo[MPMediaItemPropertyMediaType] = MPMediaType.music.rawValue
// 强制更新媒体信息
MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
// 确保在主线程上再次设置,解决某些iOS版本的兼容性问题
DispatchQueue.main.async {
MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
} }
print("🎵 [MediaInfo] ✅ 已更新媒体信息: \(song["name"] as? String ?? "未知")")
print("🎵 [MediaInfo] 📱 播放状态: \(isPlaying ? "播放中" : "暂停")")
} }
private func updatePlaybackState() { private func updatePlaybackState() {
guard let flutterChannel = flutterChannel else { return } // iOS manages playback state automatically, but you can update NowPlayingInfo if needed
let progressInfo = getProgress()
flutterChannel.invokeMethod("onPlaybackStateChanged", arguments: [
"isPlaying": isPlaying,
"currentPosition": progressInfo["currentPosition"] ?? 0,
"duration": progressInfo["duration"] ?? 0,
"progress": progressInfo["progress"] ?? 0.0
])
} }
// MARK: - 新增媒体会话功能
/// 更新媒体会话进度信息
public func updateMediaSessionProgress(currentPosition: Int, duration: Int, progress: Float) { public func updateMediaSessionProgress(currentPosition: Int, duration: Int, progress: Float) {
guard let flutterChannel = flutterChannel else { return } var nowPlayingInfo = MPNowPlayingInfoCenter.default().nowPlayingInfo ?? [:]
flutterChannel.invokeMethod("onProgressChanged", arguments: [ // 更新播放时长和当前位置
"currentPosition": currentPosition, nowPlayingInfo[MPMediaItemPropertyPlaybackDuration] = Double(duration) / 1000.0
"duration": duration, nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = Double(currentPosition) / 1000.0
"progress": progress nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = isPlaying ? 1.0 : 0.0
])
// 确保在主线程更新
DispatchQueue.main.async {
MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
print("🎵 [MusicService] ✅ 媒体会话进度已更新: \(currentPosition/1000)s/\(duration/1000)s")
}
} }
/// 启用媒体会话回调
public func enableMediaSessionCallbacks(channel: FlutterMethodChannel) { public func enableMediaSessionCallbacks(channel: FlutterMethodChannel) {
self.flutterChannel = channel flutterChannel = channel
setupEnhancedRemoteCommandCenter()
startProgressTimer()
print("媒体会话回调已启用") print("媒体会话回调已启用")
} }
/// 更新媒体会话元数据
public func updateMediaSessionMetadata(song: [String: Any]) { public func updateMediaSessionMetadata(song: [String: Any]) {
guard let flutterChannel = flutterChannel else { return } let title = song["title"] as? String ?? song["name"] as? String ?? "未知标题"
let artist = song["artist"] as? String ?? song["sgener"] as? String ?? "未知歌手"
// 构建媒体元数据 let coverUrl = song["coverUrl"] as? String ?? song["image"] as? String ?? ""
var metadata: [String: Any] = [:]
// 基本信息 // 更新当前歌曲ID
if let id = song["id"] { if let id = song["id"] as? NSNumber {
metadata["id"] = id currentIdInternal = id.int64Value
}
if let name = song["name"] as? String {
metadata["title"] = name
}
if let singer = song["singer"] as? String {
metadata["artist"] = singer
}
if let image = song["image"] as? String {
metadata["artUri"] = image
} }
// 播放状态 print("updateMediaSessionMetadata currentIdInternal: \(currentIdInternal ?? 0)")
metadata["isPlaying"] = isPlaying
// 进度信息 var nowPlayingInfo: [String: Any] = [
let progressInfo = getProgress() MPMediaItemPropertyTitle: title,
metadata["currentPosition"] = progressInfo["currentPosition"] ?? 0 MPMediaItemPropertyArtist: artist
metadata["duration"] = progressInfo["duration"] ?? 0 ]
metadata["progress"] = progressInfo["progress"] ?? 0.0
// 异步加载封面图片
if !coverUrl.isEmpty, let url = URL(string: coverUrl) {
print("开始加载封面图片: \(coverUrl)")
DispatchQueue.global(qos: .background).async {
// 创建带超时的URLRequest
var request = URLRequest(url: url)
request.timeoutInterval = 10.0
// 播放列表信息 URLSession.shared.dataTask(with: request) { data, response, error in
metadata["playlistSize"] = playlist.count if let error = error {
metadata["currentIndex"] = currentIndex print("图片加载失败: \(error.localizedDescription)")
DispatchQueue.main.async {
MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
}
return
}
// 能力标志 guard let data = data, let image = UIImage(data: data) else {
metadata["canSkipToNext"] = currentIndex < playlist.count - 1 print("图片数据无效或格式不支持")
metadata["canSkipToPrevious"] = currentIndex > 0 DispatchQueue.main.async {
metadata["canSeek"] = true MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
}
return
}
flutterChannel.invokeMethod("onMetadataChanged", arguments: metadata) print("图片加载成功")
DispatchQueue.main.async {
nowPlayingInfo[MPMediaItemPropertyArtwork] = MPMediaItemArtwork(boundsSize: image.size) { _ in image }
MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
}
}.resume()
}
} else {
print("封面图片URL为空")
MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
}
print("已更新媒体元数据: \(metadata)") print("媒体会话元数据已更新: \(title) - \(artist)")
} }
/// 更新媒体会话播放状态
public func updateMediaSessionPlaybackState(isPlaying: Bool) { public func updateMediaSessionPlaybackState(isPlaying: Bool) {
guard let flutterChannel = flutterChannel else { return } self.isPlaying = isPlaying
let progressInfo = getProgress() // 确保在主线程更新
DispatchQueue.main.async {
var nowPlayingInfo = MPNowPlayingInfoCenter.default().nowPlayingInfo ?? [:]
nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = isPlaying ? 1.0 : 0.0
MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
let playbackState: [String: Any] = [ print("🎵 [MusicService] ✅ 媒体会话播放状态已更新: \(isPlaying ? "播放中" : "已暂停")")
"isPlaying": isPlaying, }
"currentPosition": progressInfo["currentPosition"] ?? 0,
"duration": progressInfo["duration"] ?? 0,
"progress": progressInfo["progress"] ?? 0.0,
"playbackSpeed": isPlaying ? 1.0 : 0.0
]
flutterChannel.invokeMethod("onPlaybackStateChanged", arguments: playbackState) if isPlaying {
startProgressTimer()
} else {
stopProgressTimer()
}
print("已更新播放状态: isPlaying=\(isPlaying)") print("媒体会话播放状态已更新: \(isPlaying)")
} }
/// 设置增强的远程控制中心
private func setupEnhancedRemoteCommandCenter() { private func setupEnhancedRemoteCommandCenter() {
let commandCenter = MPRemoteCommandCenter.shared() let commandCenter = MPRemoteCommandCenter.shared()
@ -851,54 +785,54 @@ public class MusicService: NSObject {
// 播放命令 // 播放命令
commandCenter.playCommand.addTarget { [weak self] _ in commandCenter.playCommand.addTarget { [weak self] _ in
print("远程控制: 播放")
self?.resume() self?.resume()
self?.flutterChannel?.invokeMethod("onMediaSessionPlay", arguments: nil)
return .success return .success
} }
// 暂停命令 // 暂停命令
commandCenter.pauseCommand.addTarget { [weak self] _ in commandCenter.pauseCommand.addTarget { [weak self] _ in
print("远程控制: 暂停")
self?.pause() self?.pause()
self?.flutterChannel?.invokeMethod("onMediaSessionPause", arguments: nil)
return .success return .success
} }
// 下一首命令 // 下一曲命令
commandCenter.nextTrackCommand.addTarget { [weak self] _ in commandCenter.nextTrackCommand.addTarget { [weak self] _ in
print("远程控制: 下一首") self?.next()
self?.playNext() self?.flutterChannel?.invokeMethod("onMediaSessionNext", arguments: nil)
return .success return .success
} }
// 上一首命令 // 上一曲命令
commandCenter.previousTrackCommand.addTarget { [weak self] _ in commandCenter.previousTrackCommand.addTarget { [weak self] _ in
print("远程控制: 上一首") self?.previous()
self?.playPrevious() self?.flutterChannel?.invokeMethod("onMediaSessionPrevious", arguments: nil)
return .success return .success
} }
// 进度调整命令 // 跳转命令 - 关键升级点
commandCenter.changePlaybackPositionCommand.isEnabled = true
commandCenter.changePlaybackPositionCommand.addTarget { [weak self] event in commandCenter.changePlaybackPositionCommand.addTarget { [weak self] event in
if let positionEvent = event as? MPChangePlaybackPositionCommandEvent { if let event = event as? MPChangePlaybackPositionCommandEvent {
let positionMs = Int(positionEvent.positionTime * 1000) let positionMs = Int(event.positionTime * 1000)
print("远程控制: 跳转到 \(positionMs)ms")
self?.seekTo(positionMs: positionMs) self?.seekTo(positionMs: positionMs)
return .success return .success
} }
return .commandFailed return .commandFailed
} }
// 启用命令 // 停止命令
commandCenter.playCommand.isEnabled = true commandCenter.stopCommand.addTarget { [weak self] _ in
commandCenter.pauseCommand.isEnabled = true self?.stop()
commandCenter.nextTrackCommand.isEnabled = playlist.count > 1 self?.flutterChannel?.invokeMethod("onMediaSessionStop", arguments: nil)
commandCenter.previousTrackCommand.isEnabled = playlist.count > 1 return .success
commandCenter.changePlaybackPositionCommand.isEnabled = true }
print("增强远程控制中心设置完成") print("增强的远程控制中心已设置,支持进度条滑动")
} }
// MARK: - 进度定时器 /// 启动进度定时器
private func startProgressTimer() { private func startProgressTimer() {
stopProgressTimer() stopProgressTimer()
progressTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in progressTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
@ -906,217 +840,169 @@ public class MusicService: NSObject {
} }
} }
/// 停止进度定时器
private func stopProgressTimer() { private func stopProgressTimer() {
progressTimer?.invalidate() progressTimer?.invalidate()
progressTimer = nil progressTimer = nil
} }
/// 更新进度信息
private func updateProgressInfo() { private func updateProgressInfo() {
guard isPlaying, let player = player, let currentItem = player.currentItem else { guard let player = player, let currentItem = player.currentItem, isPlaying else { return }
return
}
let currentTime = CMTimeGetSeconds(player.currentTime()) let currentTime = CMTimeGetSeconds(player.currentTime())
let duration = CMTimeGetSeconds(currentItem.duration) let duration = CMTimeGetSeconds(currentItem.duration)
if !currentTime.isNaN && !currentTime.isInfinite && if !currentTime.isNaN && !currentTime.isInfinite && !duration.isNaN && !duration.isInfinite {
!duration.isNaN && !duration.isInfinite && duration > 0 { // 更新系统媒体信息 - 关键升级点
updateSystemMediaInfo(currentTime: currentTime, duration: duration)
// 通知Flutter层
let currentPosition = Int(currentTime * 1000) let currentPosition = Int(currentTime * 1000)
let totalDuration = Int(duration * 1000) let totalDuration = Int(duration * 1000)
let progress = Float(currentTime / duration) let progress = totalDuration > 0 ? Float(currentTime / duration) : 0.0
// 更新媒体会话进度
updateMediaSessionProgress(currentPosition: currentPosition,
duration: totalDuration,
progress: progress)
// 更新系统媒体信息 flutterChannel?.invokeMethod("onMediaSessionProgress", arguments: [
updateSystemMediaInfo(currentTime: currentTime, duration: duration) "currentIdInternal": currentIdInternal ?? 0,
"currentPosition": currentPosition,
"duration": totalDuration,
"progress": progress
])
} }
} }
/// 立即更新进度信息(用于seekTo后)
private func updateProgressInfoImmediately() { private func updateProgressInfoImmediately() {
guard let player = player, let currentItem = player.currentItem else { guard let player = player, let currentItem = player.currentItem else { return }
return
}
let currentTime = CMTimeGetSeconds(player.currentTime()) let currentTime = CMTimeGetSeconds(player.currentTime())
let duration = CMTimeGetSeconds(currentItem.duration) let duration = CMTimeGetSeconds(currentItem.duration)
updateSystemMediaInfo(currentTime: currentTime, duration: duration) if !currentTime.isNaN && !currentTime.isInfinite && !duration.isNaN && !duration.isInfinite {
updateSystemMediaInfo(currentTime: currentTime, duration: duration)
}
} }
/// 更新系统媒体信息
private func updateSystemMediaInfo(currentTime: Double, duration: Double) { private func updateSystemMediaInfo(currentTime: Double, duration: Double) {
var nowPlayingInfo = MPNowPlayingInfoCenter.default().nowPlayingInfo ?? [:] var nowPlayingInfo = MPNowPlayingInfoCenter.default().nowPlayingInfo ?? [:]
nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = currentTime
if !currentTime.isNaN && !currentTime.isInfinite { nowPlayingInfo[MPMediaItemPropertyPlaybackDuration] = duration
nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = currentTime
}
if !duration.isNaN && !duration.isInfinite && duration > 0 {
nowPlayingInfo[MPMediaItemPropertyPlaybackDuration] = duration
}
nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = isPlaying ? 1.0 : 0.0 nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = isPlaying ? 1.0 : 0.0
MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
} }
// MARK: - 播放器观察者 // MARK: - 播放完成处理
private func setupPlayerObservers() { private func setupPlayerObservers() {
guard let player = player else { return } guard let player = player else { return }
// 监听播放器状态
player.addObserver(self, forKeyPath: "timeControlStatus", options: [.new], context: nil)
// 监听播放结束通知
NotificationCenter.default.addObserver( NotificationCenter.default.addObserver(
self, forName: .AVPlayerItemDidPlayToEndTime,
selector: #selector(playerDidFinishPlaying), object: player.currentItem,
name: .AVPlayerItemDidPlayToEndTime, queue: .main
object: player.currentItem ) { [weak self] _ in
) self?.stopProgressTimer()
self?.playNextOrStop()
}
} }
// MARK: - 播放项观察者设置
private func setupPlayerItemObservers(playerItem: AVPlayerItem, song: [String: Any]) { private func setupPlayerItemObservers(playerItem: AVPlayerItem, song: [String: Any]) {
// 移除之前的观察者
NotificationCenter.default.removeObserver(self, name: .AVPlayerItemDidPlayToEndTime, object: nil)
NotificationCenter.default.removeObserver(self, name: .AVPlayerItemFailedToPlayToEndTime, object: nil)
// 监听播放项状态 // 监听播放项状态
playerItem.addObserver(self, forKeyPath: "status", options: [.new], context: nil) playerItem.addObserver(self, forKeyPath: "status", options: [.new, .initial], context: nil)
playerItem.addObserver(self, forKeyPath: "duration", options: [.new], context: nil)
playerItem.addObserver(self, forKeyPath: "loadedTimeRanges", options: [.new], context: nil)
// 监听缓冲状态 // 播放完成通知
playerItem.addObserver(self, forKeyPath: "playbackBufferEmpty", options: [.new], context: nil) NotificationCenter.default.addObserver(
playerItem.addObserver(self, forKeyPath: "playbackLikelyToKeepUp", options: [.new], context: nil) forName: .AVPlayerItemDidPlayToEndTime,
} object: playerItem,
queue: .main
) { [weak self] _ in
print("歌曲播放完成")
self?.stopProgressTimer()
self?.playNextOrStop()
}
@objc private func playerDidFinishPlaying() { // 播放失败通知
print("歌曲播放完成") NotificationCenter.default.addObserver(
isPlaying = false forName: .AVPlayerItemFailedToPlayToEndTime,
updatePlaybackState() object: playerItem,
updateMediaSessionPlaybackState(isPlaying: false) queue: .main
stopProgressTimer() ) { [weak self] notification in
print("播放失败: \(notification)")
self?.isPlaying = false
self?.updateMediaSessionPlaybackState(isPlaying: false)
}
// 自动播放下一首或停止 // 存储歌曲信息,用于状态变化时更新
playNextOrStop() objc_setAssociatedObject(playerItem, "songInfo", song, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
} }
// MARK: - KVO 观察者 // MARK: - KVO 观察者
override public func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { override public func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
guard let keyPath = keyPath else { return } guard let playerItem = object as? AVPlayerItem else { return }
switch keyPath { switch keyPath {
case "status": case "status":
if let playerItem = object as? AVPlayerItem { switch playerItem.status {
switch playerItem.status { case .readyToPlay:
case .readyToPlay: print("播放项准备就绪,开始播放")
print("🎵 [Player] 播放器准备就绪,开始播放")
player?.play()
isPlaying = true
updatePlaybackState()
updateMediaSessionPlaybackState(isPlaying: true)
startProgressTimer()
// 强制更新媒体信息,确保通知栏显示
if let currentSong = playlist.isEmpty ? nil : playlist[currentIndex] {
updateNowPlayingInfo(song: currentSong)
updateMediaSessionMetadata(song: currentSong)
print("🎵 [Player] ✅ 播放开始时已更新媒体信息")
}
// 再次确保远程控制激活
DispatchQueue.main.async {
UIApplication.shared.beginReceivingRemoteControlEvents()
print("🎵 [Player] ✅ 远程控制事件接收已重新激活")
}
case .failed:
print("播放器加载失败: \(playerItem.error?.localizedDescription ?? "未知错误")")
isPlaying = false
updatePlaybackState()
updateMediaSessionPlaybackState(isPlaying: false)
case .unknown: // 获取关联的歌曲信息
print("播放器状态未知") if let song = objc_getAssociatedObject(playerItem, "songInfo") as? [String: Any] {
updateNowPlayingInfo(song: song)
@unknown default:
break
} }
}
case "timeControlStatus": // 开始播放
if let player = object as? AVPlayer { player?.play()
switch player.timeControlStatus { isPlaying = true
case .playing: updateMediaSessionPlaybackState(isPlaying: true)
print("播放器正在播放") startProgressTimer()
if !isPlaying {
isPlaying = true
updatePlaybackState()
updateMediaSessionPlaybackState(isPlaying: true)
startProgressTimer()
}
case .paused: case .failed:
print("播放器已暂停") print("播放项加载失败: \(playerItem.error?.localizedDescription ?? "未知错误")")
if isPlaying { isPlaying = false
isPlaying = false updateMediaSessionPlaybackState(isPlaying: false)
updatePlaybackState()
updateMediaSessionPlaybackState(isPlaying: false)
stopProgressTimer()
}
case .waitingToPlayAtSpecifiedRate: case .unknown:
print("播放器等待缓冲") print("播放项状态未知")
@unknown default: @unknown default:
break break
}
} }
case "playbackBufferEmpty": case "duration":
print("播放缓冲区为空") let duration = CMTimeGetSeconds(playerItem.duration)
if !duration.isNaN && !duration.isInfinite && duration > 0 {
case "playbackLikelyToKeepUp": print("获取到音频时长: \(duration) 秒")
if let playerItem = object as? AVPlayerItem, playerItem.isPlaybackLikelyToKeepUp { // 更新系统媒体信息
print("播放缓冲充足,可以继续播放") updateProgressInfoImmediately()
} }
default: case "loadedTimeRanges":
super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context) if let timeRange = playerItem.loadedTimeRanges.first?.timeRangeValue {
} let loadedDuration = CMTimeGetSeconds(CMTimeAdd(timeRange.start, timeRange.duration))
} let totalDuration = CMTimeGetSeconds(playerItem.duration)
if !totalDuration.isNaN && !totalDuration.isInfinite && totalDuration > 0 {
// MARK: - 音频中断处理 let loadedPercentage = loadedDuration / totalDuration * 100
@objc private func handleAudioInterruption(_ notification: Notification) { print("缓冲进度: \(String(format: "%.1f", loadedPercentage))%")
print("🎵 [MusicService] 收到系统音频中断通知") }
guard let info = notification.userInfo,
let typeValue = info[AVAudioSessionInterruptionTypeKey] as? UInt,
let type = AVAudioSession.InterruptionType(rawValue: typeValue) else {
print("🎵 [MusicService] 无法解析中断通知信息")
return
}
switch type {
case .began:
print("🎵 [MusicService] 系统音频中断开始 - 当前播放状态: \(isPlaying)")
// 记录中断发生时是否在播放,并暂停
interruptedDuringPlayback = isPlaying
if isPlaying {
pause()
print("🎵 [MusicService] 已暂停音乐播放")
} }
case .ended: default:
print("🎵 [MusicService] 系统音频中断结束 - 等待AudioSessionHub处理")
// 系统中断结束时不立即处理,等待AudioSessionHub的通知
@unknown default:
print("🎵 [MusicService] 未知的中断类型: \(typeValue)")
break break
} }
} }
// 处理AudioSessionHub发送的中断开始通知 // 处理AudioSessionHub发送的中断开始通知
@objc private func handleAudioSessionHubInterruption(_ notification: Notification) { @objc private func handleAudioSessionHubInterruption(_ notification: Notification) {
interruptedDuringPlayback = isPlaying
pause()
print("🎵 [MusicService] 收到AudioSessionHub中断开始通知") print("🎵 [MusicService] 收到AudioSessionHub中断开始通知")
// AudioSessionHub已经处理了中断开始,我们这里可以做额外的处理 // AudioSessionHub已经处理了中断开始,我们这里可以做额外的处理
} }
@ -1146,9 +1032,9 @@ public class MusicService: NSObject {
do { do {
// 设置音频会话为播放模式 // 设置音频会话为播放模式
let audioSession = AVAudioSession.sharedInstance() // let audioSession = AVAudioSession.sharedInstance()
try audioSession.setCategory(.playback, mode: .default, options: [.mixWithOthers]) // try audioSession.setCategory(.playback, mode: .default, options: [.mixWithOthers])
try audioSession.setActive(true, options: []) // try audioSession.setActive(true, options: [])
print("🎵 [MusicService] 音频会话已设置为播放模式") print("🎵 [MusicService] 音频会话已设置为播放模式")
// 恢复播放 // 恢复播放
@ -1187,9 +1073,9 @@ public class MusicService: NSObject {
do { do {
// 设置音频会话为播放模式 // 设置音频会话为播放模式
let audioSession = AVAudioSession.sharedInstance() // let audioSession = AVAudioSession.sharedInstance()
try audioSession.setCategory(.playback, mode: .default, options: [.mixWithOthers]) // try audioSession.setCategory(.playback, mode: .default, options: [.mixWithOthers])
try audioSession.setActive(true, options: []) // try audioSession.setActive(true, options: [])
print("🎵 [MusicService] 音频会话已设置为播放模式") print("🎵 [MusicService] 音频会话已设置为播放模式")
// 恢复播放 // 恢复播放
@ -1211,12 +1097,6 @@ public class MusicService: NSObject {
} }
deinit { deinit {
NotificationCenter.default.removeObserver(
self,
name: AVAudioSession.interruptionNotification,
object: nil
)
NotificationCenter.default.removeObserver( NotificationCenter.default.removeObserver(
self, self,
name: NSNotification.Name("AudioSessionInterruptionBegan"), name: NSNotification.Name("AudioSessionInterruptionBegan"),

Loading…
Cancel
Save