Browse Source

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

weicu
lxm 1 year ago
parent
commit
e0c477c885
  1. 3
      devtools_options.yaml
  2. 6
      lib/core/bindings/initial_binding.dart
  3. 5
      lib/core/utils/synchrodata.dart
  4. 19
      lib/data/services/ble_manager.dart
  5. 2
      lib/data/services/music_manager.dart
  6. 535
      lib/modules/agent/controllers/agent_controller.dart
  7. 9
      lib/modules/agent/views/message_bubble.dart
  8. 42
      lib/modules/agent/views/music_card.dart
  9. 10
      lib/modules/meeting/bindings/meeting_connect_binding.dart
  10. 3
      lib/modules/meeting/controllers/meeting_connect_controller.dart
  11. 13
      lib/modules/meeting/views/meeting_connect_view.dart
  12. 66
      lib/modules/meeting/views/meeting_view.dart
  13. 15
      lib/modules/music/bindings/music_binding.dart
  14. 493
      lib/modules/music/controllers/music_controller.dart
  15. 125
      lib/modules/music/services/music_service_facade.dart
  16. 108
      lib/modules/music/views/music_playlist_view.dart
  17. 131
      lib/modules/pairing/controllers/pairing_controller.dart
  18. 9
      lib/routes/app_pages.dart
  19. 1
      lib/routes/app_routes.dart
  20. 26
      local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt
  21. 10
      local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleServicePlugin.kt
  22. 32
      local_plugins/ble_service/lib/ble_service.dart
  23. 146
      local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicService.kt
  24. 8
      local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicServicePlugin.kt
  25. 14
      local_plugins/music_service/lib/music_service.dart
  26. 6
      pubspec.yaml

3
devtools_options.yaml

@ -0,0 +1,3 @@
description: This file stores settings for Dart & Flutter DevTools.
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
extensions:

6
lib/core/bindings/initial_binding.dart

@ -10,8 +10,6 @@ import '../../data/services/bluetooth_manager.dart';
import '../../data/services/ble_manager.dart';
import '../../data/services/account_service.dart';
import '../../data/services/version_update_service.dart';
import '../../modules/music/controllers/music_controller.dart';
import '../../modules/music/services/music_service_facade.dart';
/// 初始绑定,用于管理全局依赖
class InitialBinding extends Bindings {
@ -37,10 +35,8 @@ class InitialBinding extends Bindings {
Get.lazyPut<BleManager>(() => BleManager(), fenix: true);
// MusicManager
Get.lazyPut<MusicManager>(() => MusicManager(), fenix: true);
// 音乐模块
Get.lazyPut<MusicController>(() => MusicController(), fenix: true);
Get.lazyPut<MusicServiceFacade>(() => MusicServiceFacade(), fenix: true);
Get.lazyPut<NavigationManager>(() => NavigationManager(), fenix: true);
Get.lazyPut<LocationManager>(() => LocationManager(), fenix: true);
// 火山AI服务

5
lib/core/utils/synchrodata.dart

@ -113,9 +113,10 @@ class Synchrodata {
// 同步数据
static Future _synchrodata() async {
final db = await DatabaseHelper().database;
var result = await Api.getOperationRecord();
List records = result['records'];
List records = result['records'] ?? [];
if (records.isEmpty) return;
final db = await DatabaseHelper().database;
await db.transaction((txn) async {
for (var element in records) {
final List existList = await txn.query(

19
lib/data/services/ble_manager.dart

@ -767,6 +767,25 @@ class BleManager extends GetxService {
}
}
/// 验证设备是否可以配对
Future<void> startScan() async {
try {
await _bleService.startScan();
} catch (e) {
Logger.error('开始扫描设备: ${e.toString()}');
}
}
Future<Map<String, String>?> getDeviceInfo(String address) async {
try {
final result = _bleService.getDeviceInfo(address);
return result;
} catch (e) {
Logger.error('获取设备信息: ${e.toString()}');
return null;
}
}
/// 验证设备是否可以配对
Future<bool> verifyDeviceCanPair(String macAddress) async {
try {

2
lib/data/services/music_manager.dart

@ -105,6 +105,7 @@ class MusicManager extends GetxService {
required Function() onPrevious,
required Function(int) onSeekTo,
required Function() onStop,
required Function(String, int, int, double) onStop1,
}) async {
try {
await _musicService.setMediaSessionCallbacks(
@ -114,6 +115,7 @@ class MusicManager extends GetxService {
onPrevious: onPrevious,
onSeekTo: onSeekTo,
onStop: onStop,
onStop1: onStop1,
);
} catch (e) {
Logger.d('MusicManager', '设置媒体会话回调失败: $e');

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

@ -25,23 +25,35 @@ import '../../../data/models/message_model.dart';
// 语音输入模式枚举
enum VoiceInputMode {
text, // 文字输入模式
pushToTalk, // 按住说话模式
phoneCall, // 打电话模式
text, // 文字输入模式
pushToTalk, // 按住说话模式
phoneCall, // 打电话模式
}
class AgentController extends GetxController {
static AgentController get to => Get.find(); //单例访问模式
// 聊天消息列表
final messages = <Message>[].obs;
// UI控制器
final TextEditingController textController = TextEditingController();
final ScrollController scrollController = ScrollController();
//音乐播放管理
final _musiceManager = Get.find<MusicManager>();
// 导航管理
final _navigationManager = Get.find<NavigationManager>();
RxBool isPlaying = false.obs;
RxInt currentId = 0.obs;
// 播放进度相关状态
RxInt currentPosition = 0.obs; // 当前播放位置(毫秒)
RxInt duration = 0.obs; // 总时长(毫秒)
RxDouble progress = 0.0.obs; // 播放进度(0.0-1.0)
// 当前播放列表管理
final currentPlaylist = <Map<String, dynamic>>[].obs; // 当前播放列表
final currentPlaylistIndex = 0.obs; // 当前播放歌曲在列表中的索引
// 各种状态标志
final isInitialized = false.obs;
@ -98,6 +110,8 @@ class AgentController extends GetxController {
// BLE事件订阅
StreamSubscription<dynamic>? _bleEventSubscription;
// 进度更新定时器
Timer? _progressTimer;
// 用于跟踪AI回复状态
bool _isNewAssistantResponse = true;
String _currentResponseId = '';
@ -110,17 +124,54 @@ class AgentController extends GetxController {
// 是否已加载聊天记录
final isHistoryLoaded = false.obs;
// 添加加载状态跟踪
final RxBool isLoadingMusic = false.obs;
final RxString loadingMusicId = ''.obs;
// 设置加载状态的方法
void setLoadingMusic(String id, bool loading) {
loadingMusicId.value = loading ? id : '';
isLoadingMusic.value = loading;
}
@override
void onInit() {
super.onInit();
_loadTtsPreference(); //加载TTS偏好设置
_loadChatHistory(); //加载聊天历史记录
_subscribeToEvents(); //订阅代理服务事件
_subscribeToBleEvents(); //订阅BLE事件
_initMusicStatus(); //初始化音乐状态
_setupMediaSessionCallbacks(); //设置媒体会话回调
_setupTextInputListener(); //设置文本输入监听器
}
// 初始化音乐播放状态
Future<void> _initMusicStatus() async {
try {
await musicStatus();
Logger.d('AgentController', '音乐状态初始化完成');
} catch (e) {
Logger.e('AgentController', '音乐状态初始化失败: $e');
}
}
// 设置媒体会话回调
void _setupMediaSessionCallbacks() async {
try {
await _musiceManager.setMediaSessionCallbacks(
onPlay: onMediaSessionPlay,
onPause: onMediaSessionPause,
onNext: onMediaSessionNext,
onPrevious: onMediaSessionPrevious,
onSeekTo: onMediaSessionSeekTo,
onStop: onMediaSessionStop,
onStop1: onMediaSessionSeekTo1,
);
Logger.d('AgentController', '媒体会话回调设置完成');
} catch (e) {
Logger.e('AgentController', '媒体会话回调设置失败: $e');
}
}
@override
void onClose() {
@ -994,7 +1045,8 @@ class AgentController extends GetxController {
_pendingPlusOptionsExpansion = false;
// 添加小延迟确保UI稳定
Future.delayed(const Duration(milliseconds: 50), () {
if (!isKeyboardVisible.value) { // 再次确认键盘状态
if (!isKeyboardVisible.value) {
// 再次确认键盘状态
isPlusOptionsExpanded.value = true;
}
});
@ -1194,7 +1246,8 @@ class AgentController extends GetxController {
}
// 检查文字模式与语音模式的一致性
if (isTextInputMode.value && (isPushToTalkMode.value || isPhoneCallMode.value)) {
if (isTextInputMode.value &&
(isPushToTalkMode.value || isPhoneCallMode.value)) {
Logger.w(TAG, '状态冲突:文字模式与语音模式同时启用,优先保持语音模式');
isTextInputMode.value = false;
}
@ -1268,12 +1321,299 @@ class AgentController extends GetxController {
await stopVoiceInput();
}
// 获取音乐播放状态
Future<void> musicStatus() async {
try {
var status = await _musiceManager.getStatus();
isPlaying.value = status['isPlaying'] ?? false;
currentId.value = status['id'] ?? 0;
Logger.d('AgentController',
'音乐状态更新: playing=${isPlaying.value}, url=${currentId.value}');
print("音乐状态更新: playing=${isPlaying.value}, url=${currentId.value}");
} catch (e) {
Logger.e('AgentController', '获取音乐状态失败: $e');
}
}
// 播放音乐
Future<void> musicPlay(Map<String, dynamic> song) async {
try {
Logger.d(TAG, '开始播放音乐: ${song['title'] ?? song['name']}');
// 立即更新UI状态,提供即时反馈
isPlaying.value = true;
// 修复:确保currentId接收int类型
final songId = song['id'];
currentId.value = songId is int
? songId
: (int.tryParse(songId?.toString() ?? '0') ?? 0);
// 重置进度状态
currentPosition.value = 0;
duration.value = 0;
progress.value = 0.0;
final id = song['id'] ?? "";
final timestampMs = song['ts'] ?? 0;
// 检查URL是否需要刷新(只有当有时间戳时才检查)
if (timestampMs > 0) {
final now = DateTime.now();
final tsTime = DateTime.fromMillisecondsSinceEpoch(timestampMs).add(
const Duration(minutes: 10), // 转换为北京时间
);
// 判断是否过期
if (tsTime.isBefore(now)) {
Logger.d(TAG, '音乐URL已过期,正在获取新URL...');
try {
var response = await Api.getMusicUrl({
"ids": [id]
});
print('getMusicUrl 响应数据: ${response.toString()}'); // 调试输出原始数据
final urldata = response['urls'][0] as Map<String, dynamic>;
song["url"] = urldata['url'] ?? '';
// 修复:确保currentId接收int类型
final newSongId = song['id'];
currentId.value = newSongId is int
? newSongId
: (int.tryParse(newSongId?.toString() ?? '0') ?? 0);
Logger.d(TAG, '获取新URL成功');
} catch (e) {
Logger.e(TAG, '获取新URL失败: $e');
// 如果获取新URL失败,尝试使用原URL
}
}
}
// 转换为MusicManager期望的格式
final musicData = <String, String>{
'id': song['id']?.toString() ?? '',
'url': song['url']?.toString() ?? '',
'title': song['title']?.toString() ?? song['name']?.toString() ?? '',
'artist':
song['artist']?.toString() ?? song['sgener']?.toString() ?? '',
'coverUrl':
song['coverUrl']?.toString() ?? song['image']?.toString() ?? '',
};
// 调用音乐管理器播放
await _musiceManager.play(musicData);
// 更新媒体会话元数据和播放状态
await _updateMediaSessionMetadata(musicData);
await _updateMediaSessionPlaybackState(true);
// 启动进度定时器
_startProgressTimer();
Logger.d(TAG, '音乐播放启动成功');
} catch (e) {
Logger.e(TAG, '播放音乐失败: $e');
// 发生错误时重置状态
isPlaying.value = false;
currentId.value = 0;
}
}
// 暂停音乐
Future<void> musicPause() async {
try {
Logger.d('AgentController', '暂停音乐播放');
await _musiceManager.pause();
isPlaying.value = false;
_stopProgressTimer();
// 更新媒体会话播放状态
await _updateMediaSessionPlaybackState(false);
} catch (e) {
Logger.e('AgentController', '暂停音乐失败: $e');
}
}
// 恢复音乐播放
Future<void> musicResume() async {
try {
Logger.d('AgentController', '恢复播放音乐');
await _musiceManager.resume();
isPlaying.value = true;
_startProgressTimer();
// 更新媒体会话播放状态
await _updateMediaSessionPlaybackState(true);
Logger.d('AgentController', '音乐恢复播放成功');
} catch (e) {
Logger.e('AgentController', '恢复播放音乐失败: $e');
isPlaying.value = false;
}
}
// 启动进度更新定时器
void _startProgressTimer() {
_stopProgressTimer(); // 先停止现有定时器
Logger.d('AgentController', '启动播放进度定时器');
_progressTimer = Timer.periodic(const Duration(milliseconds: 500), (timer) {
if (isPlaying.value && currentId.value != 0) {
updateMusicProgress();
} else if (!isPlaying.value) {
// 如果不在播放状态,停止定时器
timer.cancel();
_progressTimer = null;
Logger.d('AgentController', '播放已停止,取消进度定时器');
}
});
}
// 停止进度更新定时器
void _stopProgressTimer() {
if (_progressTimer != null) {
Logger.d('AgentController', '停止播放进度定时器');
_progressTimer?.cancel();
_progressTimer = null;
}
}
// 获取播放进度
Future<void> updateMusicProgress() async {
try {
if (!isPlaying.value || currentId.value == 0) {
return; // 如果没有在播放,直接返回
}
// var progressInfo = await _musiceManager.getProgress();
// final newPosition = progressInfo['currentPosition'] ?? 0;
// final newDuration = progressInfo['duration'] ?? 0;
// final newProgress = progressInfo['progress'] ?? 0.0;
// bool hasUpdate = false;
// // 只有当值发生变化时才更新,避免不必要的UI刷新
// if (currentPosition.value != newPosition) {
// currentPosition.value = newPosition;
// hasUpdate = true;
// }
// if (duration.value != newDuration) {
// duration.value = newDuration;
// hasUpdate = true;
// }
// if ((progress.value - newProgress).abs() > 0.001) {
// // 避免浮点数精度问题
// progress.value = newProgress;
// hasUpdate = true;
// }
// 如果有进度更新,通知系统媒体会话更新进度信息
// if (hasUpdate) {
// await _updateMediaSessionProgress();
// }
} catch (e) {
Logger.e('AgentController', '获取播放进度失败: $e');
}
}
// 获取播放进度
Future<void> updateMusicProgress1(String currentIdInternal, int newPosition,
int newDuration, double newProgress) async {
try {
if (int.parse(currentIdInternal) != 0) {
isPlaying.value = true;
// return; // 如果没有在播放,直接返回
}
// // var progressInfo = await _musiceManager.getProgress();
// final newPosition = progressInfo['currentPosition'] ?? 0;
// final newDuration = progressInfo['duration'] ?? 0;
// final newProgress = progressInfo['progress'] ?? 0.0;
print(
"updateMusicProgress1:currentIdInternal:$currentIdInternal newPosition:$newPosition newDuration:$newDuration newProgress:$newProgress");
bool hasUpdate = false;
if (currentId.value != int.parse(currentIdInternal)) {
currentId.value = int.parse(currentIdInternal);
hasUpdate = true;
}
// 只有当值发生变化时才更新,避免不必要的UI刷新
if (currentPosition.value != newPosition) {
currentPosition.value = newPosition;
hasUpdate = true;
}
if (duration.value != newDuration) {
duration.value = newDuration;
hasUpdate = true;
}
if ((progress.value - newProgress).abs() > 0.001) {
// 避免浮点数精度问题
progress.value = newProgress;
hasUpdate = true;
}
// // 如果有进度更新,通知系统媒体会话更新进度信息
// if (hasUpdate) {
// await _updateMediaSessionProgress();
// }
} catch (e) {
Logger.e('AgentController', '获取播放进度失败: $e');
}
}
/// 更新系统媒体会话的进度信息
Future<void> _updateMediaSessionProgress() async {
try {
// 这里将调用music_service插件的方法来更新系统媒体会话进度
// 具体实现将在music_service插件中添加
await _musiceManager.updateMediaSessionProgress(
currentPosition.value,
duration.value,
progress.value,
);
} catch (e) {
// 忽略错误,避免影响正常播放
Logger.d('AgentController', '更新媒体会话进度失败: $e');
}
}
/// 更新系统媒体会话的元数据
Future<void> _updateMediaSessionMetadata(Map<String, dynamic> song) async {
try {
await _musiceManager.updateMediaSessionMetadata(song);
} catch (e) {
Logger.d('AgentController', '更新媒体会话元数据失败: $e');
}
}
/// 更新系统媒体会话的播放状态
Future<void> _updateMediaSessionPlaybackState(bool isPlaying) async {
try {
await _musiceManager.updateMediaSessionPlaybackState(isPlaying);
} catch (e) {
Logger.d('AgentController', '更新媒体会话播放状态失败: $e');
}
}
// 跳转到指定位置
Future<void> seekToPosition(int positionMs) async {
try {
await _musiceManager.seekTo(positionMs);
// 立即更新进度状态
// await updateMusicProgress();
} catch (e) {
Logger.e('AgentController', '跳转播放位置失败: $e');
}
}
// 跳转到指定位置
Future<void> seekToPosition1(String currentIdInternal, int newPosition,
int newDuration, double newProgress) async {
try {
//await _musiceManager.seekTo(positionMs);
// 立即更新进度状态
await updateMusicProgress1(
currentIdInternal, newPosition, newDuration, newProgress);
} catch (e) {
Logger.e('AgentController', '跳转播放位置失败: $e');
}
}
// 修改自动停止处理,遵循TTS设置
void _handleAutoStop(int index) {
@ -1313,7 +1653,188 @@ class AgentController extends GetxController {
);
}
/// 播放音乐列表
Future<void> playPlaylist(List<Map<String, dynamic>> playlist) async {
try {
if (playlist.isEmpty) {
Logger.e(TAG, '播放列表为空,无法播放');
return;
}
Logger.d(TAG, '开始播放音乐列表,共${playlist.length}首歌曲');
// 保存当前播放列表
currentPlaylist.assignAll(playlist);
currentPlaylistIndex.value = 0;
// 设置加载状态
isLoadingMusic.value = true;
loadingMusicId.value = playlist[0]['id']?.toString() ?? '';
// 重置播放状态
isPlaying.value = true;
currentPosition.value = 0;
duration.value = 0;
progress.value = 0.0;
// 格式化歌曲列表数据
final formattedPlaylist = playlist.map((song) {
return {
'id': song['id']?.toString() ?? '',
'url': song['url']?.toString() ?? '',
'title': song['title']?.toString() ?? song['name']?.toString() ?? '',
'artist':
song['artist']?.toString() ?? song['sgener']?.toString() ?? '',
'coverUrl':
song['coverUrl']?.toString() ?? song['image']?.toString() ?? '',
};
}).toList();
// 调用音乐管理器播放列表
await _musiceManager.playPlaylist(formattedPlaylist);
// 更新当前播放歌曲ID
if (formattedPlaylist.isNotEmpty) {
currentId.value = int.tryParse(formattedPlaylist[0]['id'] ?? '0') ?? 0;
}
// 启动进度定时器
_startProgressTimer();
Logger.d(TAG, '音乐列表播放启动成功');
} catch (e) {
Logger.e(TAG, '播放音乐列表失败: $e');
isPlaying.value = false;
currentId.value = 0;
// 显示错误提示
Get.snackbar(
'播放失败',
'无法播放音乐列表,请稍后重试',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red[400],
colorText: Colors.white,
duration: const Duration(seconds: 2),
);
} finally {
// 清除加载状态
isLoadingMusic.value = false;
loadingMusicId.value = '';
}
}
/// 播放上一曲
Future<void> playPrevious() async {
if (currentPlaylist.isEmpty) {
Logger.w(TAG, '当前播放列表为空,无法播放上一曲');
return;
}
// 计算上一曲的索引
int previousIndex = currentPlaylistIndex.value - 1;
if (previousIndex < 0) {
previousIndex = currentPlaylist.length - 1; // 循环到最后一首
}
currentPlaylistIndex.value = previousIndex;
final previousSong = currentPlaylist[previousIndex];
Logger.d(TAG, '播放上一曲: ${previousSong['name'] ?? previousSong['title']}');
await musicPlay(previousSong);
}
/// 播放下一曲
Future<void> playNext() async {
if (currentPlaylist.isEmpty) {
Logger.w(TAG, '当前播放列表为空,无法播放下一曲');
return;
}
// 计算下一曲的索引
int nextIndex = currentPlaylistIndex.value + 1;
if (nextIndex >= currentPlaylist.length) {
nextIndex = 0; // 循环到第一首
}
currentPlaylistIndex.value = nextIndex;
final nextSong = currentPlaylist[nextIndex];
Logger.d(TAG, '播放下一曲: ${nextSong['name'] ?? nextSong['title']}');
await musicPlay(nextSong);
}
/// 更新当前播放列表和索引(当单独播放某首歌曲时调用)
void updateCurrentPlaylistContext(
List<Map<String, dynamic>> playlist, int songId) {
currentPlaylist.assignAll(playlist);
// 查找当前歌曲在列表中的索引
int index = -1;
for (int i = 0; i < playlist.length; i++) {
if (int.tryParse(playlist[i]['id']?.toString() ?? '0') == songId) {
index = i;
break;
}
}
if (index != -1) {
currentPlaylistIndex.value = index;
Logger.d(TAG, '更新播放列表上下文,当前歌曲索引: $index');
}
}
// ========== 媒体会话回调处理方法 ==========
/// 处理来自系统通知栏的播放操作
Future<void> onMediaSessionPlay() async {
Logger.d(TAG, '收到媒体会话播放回调');
if (!isPlaying.value) {
await musicResume();
}
}
/// 处理来自系统通知栏的暂停操作
Future<void> onMediaSessionPause() async {
Logger.d(TAG, '收到媒体会话暂停回调');
if (isPlaying.value) {
await musicPause();
}
}
/// 处理来自系统通知栏的下一曲操作
Future<void> onMediaSessionNext() async {
Logger.d(TAG, '收到媒体会话下一曲回调');
await playNext();
}
/// 处理来自系统通知栏的上一曲操作
Future<void> onMediaSessionPrevious() async {
Logger.d(TAG, '收到媒体会话上一曲回调');
await playPrevious();
}
/// 处理来自系统通知栏的跳转操作
Future<void> onMediaSessionSeekTo(int positionMs) async {
Logger.d(TAG, '收到媒体会话跳转回调: ${positionMs}ms');
await seekToPosition(positionMs);
}
/// 处理来自系统通知栏的停止操作
Future<void> onMediaSessionStop() async {
Logger.d(TAG, '收到媒体会话停止回调');
await musicPause();
// 重置播放状态
currentPosition.value = 0;
progress.value = 0.0;
_stopProgressTimer();
}
/// 处理来自系统通知栏的跳转操作
Future<void> onMediaSessionSeekTo1(String currentIdInternal, int newPosition,
int newDuration, double newProgress) async {
print(
"onMediaSessionSeekTo1 currentIdInternal :$currentIdInternal ,newPosition:$newPosition newDuration:$newDuration newProgress:$newProgress");
await seekToPosition1(
currentIdInternal, newPosition, newDuration, newProgress);
}
}

9
lib/modules/agent/views/message_bubble.dart

@ -240,9 +240,11 @@ class MessageBubble extends GetView<AgentController> {
final musicUrl = card_music['url'] as String? ?? '';
final musicSinger = card_music['sgener'] as String? ?? '未知歌手';
final musicImage = card_music['image'] as String? ?? '';
final ts = card_music['ts'] as int? ?? 0;
print("music_play card_music: $card_music");
//音乐卡片
return buildMusicItem(
context, musicId, musicName, musicUrl, musicSinger, musicImage);
return buildMusicItem(context, musicId, musicName, musicUrl,
musicSinger, musicImage, ts);
}
} else if (meta!.keys.contains('music_playlist')) {
final card_musiclist =
@ -281,7 +283,8 @@ class MessageBubble extends GetView<AgentController> {
final end = card_navigation['end'] as String? ?? '';
final destination = card_navigation['destination'] as String? ?? '';
final rawList = card_navigation['items'] as List? ?? [];
final list = rawList.map((e) => Map<String, dynamic>.from(e)).toList();
final list =
rawList.map((e) => Map<String, dynamic>.from(e)).toList();
final rawNavigation = card_navigation['navigation'];
final Map<String, dynamic>? navigation =
rawNavigation is Map<String, dynamic> ? rawNavigation : null;

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

@ -1,22 +1,24 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../music/services/music_service_facade.dart';
import '../../../routes/app_routes.dart';
import '../controllers/agent_controller.dart';
import 'circular_progress_button.dart';
/// 构建音乐项目
Widget buildMusicItem(BuildContext context, int id, String name, String url,
String singer, String image) {
final musicFacade = Get.find<MusicServiceFacade>(); // 获取音乐服务门面
String singer, String image, int ts) {
final controller = Get.find<AgentController>(); // 获取控制器
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
Map<String, dynamic> song = {
'id': id,
'url': url,
'title': name,
'artist': singer,
'coverUrl': image,
'ts': ts,
};
// 音乐状态由MusicController自动管理,无需手动刷新
controller.musicStatus(); //刷新状态
return Container(
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
@ -82,11 +84,11 @@ Widget buildMusicItem(BuildContext context, int id, String name, String url,
),
Obx(() {
final isThisPlaying =
musicFacade.isPlaying.value && musicFacade.currentId.value == id;
controller.isPlaying.value && controller.currentId.value == id;
// 只有当前播放的歌曲才显示进度
final currentProgress =
isThisPlaying ? musicFacade.progress.value : 0.0;
isThisPlaying ? controller.progress.value : 0.0;
return CircularProgressButton(
isPlaying: isThisPlaying,
@ -97,21 +99,21 @@ Widget buildMusicItem(BuildContext context, int id, String name, String url,
strokeWidth: 2.0,
onPlayPause: () async {
try {
if (musicFacade.isPlaying.value) {
if (musicFacade.currentId.value == id) {
if (controller.isPlaying.value) {
if (controller.currentId.value == id) {
// 当前歌曲正在播放,暂停它
await musicFacade.musicPause();
await controller.musicPause();
} else {
// 其他歌曲正在播放,播放这首歌
await musicFacade.musicPlay(song);
await controller.musicPlay(song);
}
} else {
if (musicFacade.currentId.value == id) {
if (controller.currentId.value == id) {
// 当前歌曲已暂停,恢复播放
await musicFacade.musicResume();
await controller.musicResume();
} else {
// 播放新歌曲
await musicFacade.musicPlay(song);
await controller.musicPlay(song);
}
}
} catch (e) {
@ -122,8 +124,8 @@ Widget buildMusicItem(BuildContext context, int id, String name, String url,
? (seekProgress) {
// 只有当前播放的歌曲才允许跳转
final targetPosition =
(musicFacade.duration.value * seekProgress).round();
musicFacade.seekToPosition(targetPosition);
(controller.duration.value * seekProgress).round();
controller.seekToPosition(targetPosition);
}
: null,
);
@ -137,7 +139,8 @@ Widget buildMusicItem(BuildContext context, int id, String name, String url,
Widget buildMusicContent(
BuildContext context, List<Map<String, dynamic>> playlist) {
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
final controller = Get.find<AgentController>(); // 获取控制器
controller.updateCurrentPlaylistContext(playlist, controller.currentId.value);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@ -202,9 +205,10 @@ Widget buildMusicContent(
final url = item["url"] ?? "";
final singer = item["sgener"] ?? "未知歌手";
final image = item["image"] ?? "";
final ts = item["ts"] ?? 0;
return Column(
children: [
buildMusicItem(context, id, name, url, singer, image),
buildMusicItem(context, id, name, url, singer, image, ts),
if (index != playlist.length - 1)
Divider(
height: 1,
@ -221,7 +225,7 @@ Widget buildMusicContent(
child: TextButton(
onPressed: () {
// 跳转到完整歌单页面,传递完整的歌单数据
Get.toNamed('/music/playlist', arguments: playlist);
Get.toNamed(Routes.musicPlaylist, arguments: playlist);
},
style: TextButton.styleFrom(
minimumSize: Size.zero,

10
lib/modules/meeting/bindings/meeting_connect_binding.dart

@ -0,0 +1,10 @@
import 'package:get/get.dart';
import '../controllers/meeting_connect_controller.dart';
class MeetingConnectBinding extends Bindings {
@override
void dependencies() {
// 注册控制器
Get.lazyPut<MeetingConnectController>(() => MeetingConnectController());
}
}

3
lib/modules/meeting/controllers/meeting_connect_controller.dart

@ -0,0 +1,3 @@
import 'package:get/get.dart';
class MeetingConnectController extends GetxController {}

13
lib/modules/meeting/views/meeting_connect_view.dart

@ -0,0 +1,13 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../controllers/meeting_connect_controller.dart';
class MeetingConnectView extends GetView<MeetingConnectController> {
const MeetingConnectView({super.key});
@override
Widget build(BuildContext context) {
return const Placeholder();
}
}

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

@ -23,7 +23,15 @@ class MeetingView extends GetView<MeetingController> {
appBar: _buildAppBar(context, isDarkMode),
body: Stack(
children: [
_buildBody(context, isDarkMode),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildConnect(isDarkMode),
Expanded(
child: _buildBody(context, isDarkMode),
),
],
),
_buildMicButton(context, isDarkMode),
],
),
@ -32,6 +40,62 @@ class MeetingView extends GetView<MeetingController> {
);
}
Widget _buildConnect(bool isDarkMode) {
return GestureDetector(
onTap: () {
Get.toNamed(Routes.meetingConnect);
},
child: Container(
padding: EdgeInsets.all(5.w),
margin: EdgeInsets.symmetric(
horizontal: 12.w,
vertical: 10.w,
),
decoration: BoxDecoration(
color: isDarkMode ? Colors.white.withOpacity(0.05) : Colors.white,
borderRadius: BorderRadius.circular(50.w),
boxShadow: [
BoxShadow(
color: isDarkMode
? Colors.black.withOpacity(0.2)
: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 32.w,
height: 32.w,
decoration: BoxDecoration(
color: Colors.black,
borderRadius: BorderRadius.circular(50.w),
),
child: Icon(
Icons.add,
color: Colors.white,
size: 20.sp,
),
),
Padding(
padding: EdgeInsets.symmetric(horizontal: 5.w),
child: Text(
'连接',
style: TextStyle(
color: isDarkMode ? Colors.white : Colors.black87,
fontSize: 14.sp,
),
),
)
],
),
),
);
}
/// 构建底部中间的录音按钮
Widget _buildMicButton(BuildContext context, bool isDarkMode) {
return Positioned(

15
lib/modules/music/bindings/music_binding.dart

@ -1,15 +0,0 @@
import 'package:get/get.dart';
import '../controllers/music_controller.dart';
/// 音乐模块绑定
/// 负责注册音乐相关的控制器和服务
class MusicBinding extends Bindings {
@override
void dependencies() {
// 注册音乐控制器为单例
Get.lazyPut<MusicController>(
() => MusicController(),
fenix: true, // 允许重新创建
);
}
}

493
lib/modules/music/controllers/music_controller.dart

@ -1,493 +0,0 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../../core/utils/logger.dart';
import '../../../data/services/music_manager.dart';
import '../../../data/services/network/api.dart';
/// 音乐播放控制器
/// 负责管理音乐播放状态、播放列表和播放控制
class MusicController extends GetxController {
static const String TAG = 'MusicController';
// 音乐播放管理器
final _musicManager = Get.find<MusicManager>();
// 播放状态
final isPlaying = false.obs;
final currentId = 0.obs;
// 播放进度相关状态
final currentPosition = 0.obs; // 当前播放位置(毫秒)
final duration = 0.obs; // 总时长(毫秒)
final progress = 0.0.obs; // 播放进度(0.0-1.0)
// 当前播放列表管理
final currentPlaylist = <Map<String, dynamic>>[].obs; // 当前播放列表
final currentPlaylistIndex = 0.obs; // 当前播放歌曲在列表中的索引
// 加载状态跟踪
final isLoadingMusic = false.obs;
final loadingMusicId = ''.obs;
// 进度更新定时器
Timer? _progressTimer;
@override
void onInit() {
super.onInit();
_initMusicStatus();
_setupMediaSessionCallbacks();
}
@override
void onClose() {
_stopProgressTimer();
super.onClose();
}
// 初始化音乐播放状态
Future<void> _initMusicStatus() async {
try {
await musicStatus();
Logger.d(TAG, '音乐状态初始化完成');
} catch (e) {
Logger.e(TAG, '音乐状态初始化失败: $e');
}
}
// 设置媒体会话回调
void _setupMediaSessionCallbacks() async {
try {
await _musicManager.setMediaSessionCallbacks(
onPlay: onMediaSessionPlay,
onPause: onMediaSessionPause,
onNext: onMediaSessionNext,
onPrevious: onMediaSessionPrevious,
onSeekTo: onMediaSessionSeekTo,
onStop: onMediaSessionStop,
);
Logger.d(TAG, '媒体会话回调设置完成');
} catch (e) {
Logger.e(TAG, '媒体会话回调设置失败: $e');
}
}
// 设置加载状态的方法
void setLoadingMusic(String id, bool loading) {
loadingMusicId.value = loading ? id : '';
isLoadingMusic.value = loading;
}
// 获取音乐播放状态
Future<void> musicStatus() async {
try {
var status = await _musicManager.getStatus();
isPlaying.value = status['isPlaying'] ?? false;
currentId.value = status['id'] ?? 0;
Logger.d(TAG, '音乐状态更新: playing=${isPlaying.value}, id=${currentId.value}');
} catch (e) {
Logger.e(TAG, '获取音乐状态失败: $e');
}
}
// 播放音乐
Future<void> musicPlay(Map<String, dynamic> song) async {
try {
Logger.d(TAG, '开始播放音乐: ${song['title'] ?? song['name']}');
// 立即更新UI状态,提供即时反馈
isPlaying.value = true;
// 修复:确保currentId接收int类型
final songId = song['id'];
currentId.value = songId is int
? songId
: (int.tryParse(songId?.toString() ?? '0') ?? 0);
// 重置进度状态
currentPosition.value = 0;
duration.value = 0;
progress.value = 0.0;
final id = song['id'] ?? "";
final timestampMs = song['ts'] ?? 0;
// 检查URL是否需要刷新(只有当有时间戳时才检查)
if (timestampMs > 0) {
final now = DateTime.now();
final tsTime = DateTime.fromMillisecondsSinceEpoch(timestampMs).add(
const Duration(minutes: 10), // 转换为北京时间
);
// 判断是否过期
if (tsTime.isBefore(now)) {
Logger.d(TAG, '音乐URL已过期,正在获取新URL...');
try {
var response = await Api.getMusicUrl({
"ids": [id]
});
print('getMusicUrl 响应数据: ${response.toString()}'); // 调试输出原始数据
final urldata = response['urls'][0] as Map<String, dynamic>;
song["url"] = urldata['url'] ?? '';
// 修复:确保currentId接收int类型
final newSongId = song['id'];
currentId.value = newSongId is int
? newSongId
: (int.tryParse(newSongId?.toString() ?? '0') ?? 0);
Logger.d(TAG, '获取新URL成功');
} catch (e) {
Logger.e(TAG, '获取新URL失败: $e');
// 如果获取新URL失败,尝试使用原URL
}
}
}
// 转换为MusicManager期望的格式
final musicData = <String, String>{
'id': song['id']?.toString() ?? '',
'url': song['url']?.toString() ?? '',
'title': song['title']?.toString() ?? song['name']?.toString() ?? '',
'artist':
song['artist']?.toString() ?? song['sgener']?.toString() ?? '',
'coverUrl':
song['coverUrl']?.toString() ?? song['image']?.toString() ?? '',
};
// 调用音乐管理器播放
await _musicManager.play(musicData);
// 更新媒体会话元数据和播放状态
await _updateMediaSessionMetadata(song);
await _updateMediaSessionPlaybackState(true);
// 启动进度定时器
_startProgressTimer();
Logger.d(TAG, '音乐播放启动成功');
} catch (e) {
Logger.e(TAG, '播放音乐失败: $e');
// 发生错误时重置状态
isPlaying.value = false;
currentId.value = 0;
}
}
// 暂停音乐
Future<void> musicPause() async {
try {
Logger.d(TAG, '暂停音乐播放');
await _musicManager.pause();
isPlaying.value = false;
_stopProgressTimer();
// 更新媒体会话播放状态
await _updateMediaSessionPlaybackState(false);
} catch (e) {
Logger.e(TAG, '暂停音乐失败: $e');
}
}
// 恢复音乐播放
Future<void> musicResume() async {
try {
Logger.d(TAG, '恢复播放音乐');
await _musicManager.resume();
isPlaying.value = true;
_startProgressTimer();
// 更新媒体会话播放状态
await _updateMediaSessionPlaybackState(true);
Logger.d(TAG, '音乐恢复播放成功');
} catch (e) {
Logger.e(TAG, '恢复播放音乐失败: $e');
isPlaying.value = false;
}
}
// 启动进度更新定时器
void _startProgressTimer() {
_stopProgressTimer(); // 先停止现有定时器
Logger.d(TAG, '启动播放进度定时器');
_progressTimer = Timer.periodic(const Duration(milliseconds: 500), (timer) {
if (isPlaying.value && currentId.value != 0) {
updateMusicProgress();
} else if (!isPlaying.value) {
// 如果不在播放状态,停止定时器
timer.cancel();
_progressTimer = null;
Logger.d(TAG, '播放已停止,取消进度定时器');
}
});
}
// 停止进度更新定时器
void _stopProgressTimer() {
if (_progressTimer != null) {
Logger.d(TAG, '停止播放进度定时器');
_progressTimer?.cancel();
_progressTimer = null;
}
}
// 获取播放进度
Future<void> updateMusicProgress() async {
try {
if (!isPlaying.value || currentId.value == 0) {
return; // 如果没有在播放,直接返回
}
var progressInfo = await _musicManager.getProgress();
final newPosition = progressInfo['currentPosition'] ?? 0;
final newDuration = progressInfo['duration'] ?? 0;
final newProgress = progressInfo['progress'] ?? 0.0;
bool hasUpdate = false;
// 只有当值发生变化时才更新,避免不必要的UI刷新
if (currentPosition.value != newPosition) {
currentPosition.value = newPosition;
hasUpdate = true;
}
if (duration.value != newDuration) {
duration.value = newDuration;
hasUpdate = true;
}
if ((progress.value - newProgress).abs() > 0.001) {
// 避免浮点数精度问题
progress.value = newProgress;
hasUpdate = true;
}
// 如果有进度更新,通知系统媒体会话更新进度信息
if (hasUpdate) {
await _updateMediaSessionProgress();
}
} catch (e) {
Logger.e(TAG, '获取播放进度失败: $e');
}
}
// 跳转到指定位置
Future<void> seekToPosition(int positionMs) async {
try {
await _musicManager.seekTo(positionMs);
// 立即更新进度状态
await updateMusicProgress();
} catch (e) {
Logger.e(TAG, '跳转播放位置失败: $e');
}
}
/// 播放音乐列表
Future<void> playPlaylist(List<Map<String, dynamic>> playlist) async {
try {
if (playlist.isEmpty) {
Logger.e(TAG, '播放列表为空,无法播放');
return;
}
Logger.d(TAG, '开始播放音乐列表,共${playlist.length}首歌曲');
// 保存当前播放列表
currentPlaylist.assignAll(playlist);
currentPlaylistIndex.value = 0;
// 设置加载状态
isLoadingMusic.value = true;
loadingMusicId.value = playlist[0]['id']?.toString() ?? '';
// 重置播放状态
isPlaying.value = true;
currentPosition.value = 0;
duration.value = 0;
progress.value = 0.0;
// 格式化歌曲列表数据
final formattedPlaylist = playlist.map((song) {
return {
'id': song['id']?.toString() ?? '',
'url': song['url']?.toString() ?? '',
'title': song['title']?.toString() ?? song['name']?.toString() ?? '',
'artist':
song['artist']?.toString() ?? song['sgener']?.toString() ?? '',
'coverUrl':
song['coverUrl']?.toString() ?? song['image']?.toString() ?? '',
};
}).toList();
// 调用音乐管理器播放列表
await _musicManager.playPlaylist(formattedPlaylist);
// 更新当前播放歌曲ID
if (formattedPlaylist.isNotEmpty) {
currentId.value = int.tryParse(formattedPlaylist[0]['id'] ?? '0') ?? 0;
}
// 启动进度定时器
_startProgressTimer();
Logger.d(TAG, '音乐列表播放启动成功');
} catch (e) {
Logger.e(TAG, '播放音乐列表失败: $e');
isPlaying.value = false;
currentId.value = 0;
// 显示错误提示
Get.snackbar(
'播放失败',
'无法播放音乐列表,请稍后重试',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red[400],
colorText: Colors.white,
duration: const Duration(seconds: 2),
);
} finally {
// 清除加载状态
isLoadingMusic.value = false;
loadingMusicId.value = '';
}
}
/// 播放上一曲
Future<void> playPrevious() async {
if (currentPlaylist.isEmpty) {
Logger.w(TAG, '当前播放列表为空,无法播放上一曲');
return;
}
// 计算上一曲的索引
int previousIndex = currentPlaylistIndex.value - 1;
if (previousIndex < 0) {
previousIndex = currentPlaylist.length - 1; // 循环到最后一首
}
currentPlaylistIndex.value = previousIndex;
final previousSong = currentPlaylist[previousIndex];
Logger.d(TAG, '播放上一曲: ${previousSong['name'] ?? previousSong['title']}');
await musicPlay(previousSong);
}
/// 播放下一曲
Future<void> playNext() async {
if (currentPlaylist.isEmpty) {
Logger.w(TAG, '当前播放列表为空,无法播放下一曲');
return;
}
// 计算下一曲的索引
int nextIndex = currentPlaylistIndex.value + 1;
if (nextIndex >= currentPlaylist.length) {
nextIndex = 0; // 循环到第一首
}
currentPlaylistIndex.value = nextIndex;
final nextSong = currentPlaylist[nextIndex];
Logger.d(TAG, '播放下一曲: ${nextSong['name'] ?? nextSong['title']}');
await musicPlay(nextSong);
}
/// 更新当前播放列表和索引(当单独播放某首歌曲时调用)
void updateCurrentPlaylistContext(
List<Map<String, dynamic>> playlist, int songId) {
currentPlaylist.assignAll(playlist);
// 查找当前歌曲在列表中的索引
int index = -1;
for (int i = 0; i < playlist.length; i++) {
if (int.tryParse(playlist[i]['id']?.toString() ?? '0') == songId) {
index = i;
break;
}
}
if (index != -1) {
currentPlaylistIndex.value = index;
Logger.d(TAG, '更新播放列表上下文,当前歌曲索引: $index');
}
}
// ========== 媒体会话回调处理方法 ==========
/// 处理来自系统通知栏的播放操作
Future<void> onMediaSessionPlay() async {
Logger.d(TAG, '收到媒体会话播放回调');
if (!isPlaying.value) {
await musicResume();
}
}
/// 处理来自系统通知栏的暂停操作
Future<void> onMediaSessionPause() async {
Logger.d(TAG, '收到媒体会话暂停回调');
if (isPlaying.value) {
await musicPause();
}
}
/// 处理来自系统通知栏的下一曲操作
Future<void> onMediaSessionNext() async {
Logger.d(TAG, '收到媒体会话下一曲回调');
await playNext();
}
/// 处理来自系统通知栏的上一曲操作
Future<void> onMediaSessionPrevious() async {
Logger.d(TAG, '收到媒体会话上一曲回调');
await playPrevious();
}
/// 处理来自系统通知栏的跳转操作
Future<void> onMediaSessionSeekTo(int positionMs) async {
Logger.d(TAG, '收到媒体会话跳转回调: ${positionMs}ms');
await seekToPosition(positionMs);
}
/// 处理来自系统通知栏的停止操作
Future<void> onMediaSessionStop() async {
Logger.d(TAG, '收到媒体会话停止回调');
await musicPause();
// 重置播放状态
currentPosition.value = 0;
progress.value = 0.0;
_stopProgressTimer();
}
// ========== 私有辅助方法 ==========
/// 更新系统媒体会话的进度信息
Future<void> _updateMediaSessionProgress() async {
try {
// 具体实现将在music_service插件中添加
await _musicManager.updateMediaSessionProgress(
currentPosition.value,
duration.value,
progress.value,
);
} catch (e) {
// 忽略错误,避免影响正常播放
Logger.d(TAG, '更新媒体会话进度失败: $e');
}
}
/// 更新系统媒体会话的元数据
Future<void> _updateMediaSessionMetadata(Map<String, dynamic> song) async {
try {
await _musicManager.updateMediaSessionMetadata(song);
} catch (e) {
Logger.d(TAG, '更新媒体会话元数据失败: $e');
}
}
/// 更新系统媒体会话的播放状态
Future<void> _updateMediaSessionPlaybackState(bool isPlaying) async {
try {
await _musicManager.updateMediaSessionPlaybackState(isPlaying);
} catch (e) {
Logger.d(TAG, '更新媒体会话播放状态失败: $e');
}
}
}

125
lib/modules/music/services/music_service_facade.dart

@ -1,125 +0,0 @@
import 'package:get/get.dart';
import '../controllers/music_controller.dart';
import '../../../data/services/music_manager.dart';
/// 音乐服务门面
/// 作为音乐功能的统一入口,简化模块间的交互
class MusicServiceFacade {
// 单例实现
static final MusicServiceFacade _instance = MusicServiceFacade._internal();
factory MusicServiceFacade() => _instance;
MusicServiceFacade._internal();
// 获取音乐控制器实例
MusicController get musicController => Get.find<MusicController>();
// 获取音乐管理器实例
MusicManager get musicManager => Get.find<MusicManager>();
/// 播放音乐
Future<void> musicPlay(Map<String, dynamic> song) async {
await musicController.musicPlay(song);
}
/// 暂停音乐
Future<void> musicPause() async {
await musicController.musicPause();
}
/// 恢复播放
Future<void> musicResume() async {
await musicController.musicResume();
}
/// 播放音乐(兼容旧接口)
Future<void> playMusic(Map<String, dynamic> song) async {
await musicPlay(song);
}
/// 暂停音乐(兼容旧接口)
Future<void> pauseMusic() async {
await musicPause();
}
/// 恢复播放(兼容旧接口)
Future<void> resumeMusic() async {
await musicResume();
}
/// 播放/暂停切换
Future<void> togglePlayPause() async {
if (musicController.isPlaying.value) {
await musicPause();
} else {
await musicResume();
}
}
/// 播放上一曲
Future<void> playPrevious() async {
await musicController.playPrevious();
}
/// 播放下一曲
Future<void> playNext() async {
await musicController.playNext();
}
/// 播放音乐列表
Future<void> playPlaylist(List<Map<String, dynamic>> playlist) async {
await musicController.playPlaylist(playlist);
}
/// 跳转到指定位置
Future<void> seekToPosition(int positionMs) async {
await musicController.seekToPosition(positionMs);
}
/// 获取当前播放状态
bool get isPlayingValue => musicController.isPlaying.value;
/// 获取当前播放歌曲ID
int get currentIdValue => musicController.currentId.value;
/// 获取当前播放位置(毫秒)
int get currentPositionValue => musicController.currentPosition.value;
/// 获取当前歌曲总时长(毫秒)
int get durationValue => musicController.duration.value;
/// 获取当前播放进度(0.0-1.0)
double get progressValue => musicController.progress.value;
/// 获取响应式播放状态(用于Obx)
get isPlaying => musicController.isPlaying;
/// 获取响应式当前播放歌曲ID(用于Obx)
get currentId => musicController.currentId;
/// 获取响应式当前播放位置(用于Obx)
get currentPosition => musicController.currentPosition;
/// 获取响应式当前歌曲总时长(用于Obx)
get duration => musicController.duration;
/// 获取响应式当前播放进度(用于Obx)
get progress => musicController.progress;
/// 获取当前播放列表
List<Map<String, dynamic>> get currentPlaylist => musicController.currentPlaylist;
/// 获取当前播放索引
int get currentPlaylistIndex => musicController.currentPlaylistIndex.value;
/// 设置加载状态
void setLoadingMusic(String id, bool loading) {
musicController.setLoadingMusic(id, loading);
}
/// 更新当前播放列表上下文
void updateCurrentPlaylistContext(List<Map<String, dynamic>> playlist, int songId) {
musicController.updateCurrentPlaylistContext(playlist, songId);
}
}

108
lib/modules/music/views/music_playlist_view.dart

@ -3,8 +3,8 @@ import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:flutter/services.dart';
import '../services/music_service_facade.dart';
import '../../../core/utils/logger.dart';
import '../../agent/controllers/agent_controller.dart';
import '../../agent/views/circular_progress_button.dart';
class MusicPlaylistView extends StatelessWidget {
@ -13,7 +13,7 @@ class MusicPlaylistView extends StatelessWidget {
@override
Widget build(BuildContext context) {
ScreenUtil.init(context, designSize: const Size(375, 812)); // 添加设计尺寸参考
final musicFacade = Get.find<MusicServiceFacade>();
final controller = Get.find<AgentController>(); // 获取控制器
final List<Map<String, dynamic>> playlist = Get.arguments ?? [];
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
@ -55,14 +55,14 @@ class MusicPlaylistView extends StatelessWidget {
Container(
height: MediaQuery.of(context).size.height * 0.38, // 增大高度比例
child: _buildCurrentPlayingSection(
musicFacade, playlist, isDarkMode),
controller, playlist, isDarkMode),
),
// 中部:播放全部按钮
_buildPlayAllSection(musicFacade, playlist, isDarkMode),
_buildPlayAllSection(controller, playlist, isDarkMode),
// 下部:歌单列表 - 使用剩余空间
Expanded(
child:
_buildPlaylistSection(musicFacade, playlist, isDarkMode),
_buildPlaylistSection(controller, playlist, isDarkMode),
),
],
),
@ -70,19 +70,19 @@ class MusicPlaylistView extends StatelessWidget {
}
// 构建当前播放音乐详情部分
Widget _buildCurrentPlayingSection(MusicServiceFacade musicFacade,
Widget _buildCurrentPlayingSection(AgentController controller,
List<Map<String, dynamic>> playlist, bool isDarkMode) {
return SingleChildScrollView(
// 添加滚动支持
child: Obx(() {
// 查找当前播放的歌曲
Map<String, dynamic>? currentSong;
if (musicFacade.currentId.value != 0) {
if (controller.currentId.value != 0) {
try {
currentSong = playlist.firstWhere(
(song) =>
int.tryParse(song['id']?.toString() ?? '0') ==
musicFacade.currentId.value,
controller.currentId.value,
);
} catch (e) {
// 如果没找到,使用第一首歌曲
@ -137,10 +137,10 @@ class MusicPlaylistView extends StatelessWidget {
_buildSongInfo(title, artist, isDarkMode),
SizedBox(height: 16.h),
// 进度条
_buildProgressBar(musicFacade, isDarkMode),
_buildProgressBar(controller, isDarkMode),
SizedBox(height: 15.h),
// 播放控制按钮
_buildPlayControls(musicFacade, playlist, isDarkMode),
_buildPlayControls(controller, playlist, isDarkMode),
],
),
);
@ -149,7 +149,7 @@ class MusicPlaylistView extends StatelessWidget {
}
// 构建播放全部按钮部分
Widget _buildPlayAllSection(MusicServiceFacade musicFacade,
Widget _buildPlayAllSection(AgentController controller,
List<Map<String, dynamic>> playlist, bool isDarkMode) {
return Container(
padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 12.h),
@ -159,7 +159,7 @@ class MusicPlaylistView extends StatelessWidget {
child: ElevatedButton.icon(
onPressed: () async {
if (playlist.isNotEmpty) {
await musicFacade.playPlaylist(playlist);
await controller.playPlaylist(playlist);
}
},
icon: Icon(Icons.play_arrow, color: Colors.white, size: 20.w),
@ -185,7 +185,7 @@ class MusicPlaylistView extends StatelessWidget {
}
// 构建歌单列表部分
Widget _buildPlaylistSection(MusicServiceFacade musicFacade,
Widget _buildPlaylistSection(AgentController controller,
List<Map<String, dynamic>> playlist, bool isDarkMode) {
return ListView.builder(
padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 8.h),
@ -200,7 +200,7 @@ class MusicPlaylistView extends StatelessWidget {
return _buildMusicItem(
context,
musicFacade,
controller,
id,
name,
url,
@ -214,7 +214,7 @@ class MusicPlaylistView extends StatelessWidget {
Widget _buildMusicItem(
BuildContext context,
MusicServiceFacade musicFacade,
AgentController controller,
String id,
String name,
String url,
@ -232,7 +232,7 @@ class MusicPlaylistView extends StatelessWidget {
return GestureDetector(
onTap: () {
_playSelectedSong(musicFacade, song, Get.arguments ?? []);
_playSelectedSong(controller, song, Get.arguments ?? []);
},
child: Container(
margin: EdgeInsets.only(bottom: 10.h),
@ -324,10 +324,10 @@ class MusicPlaylistView extends StatelessWidget {
// 播放按钮
Obx(() {
final currentIdInt = int.tryParse(id) ?? 0;
final isThisPlaying = musicFacade.isPlaying.value &&
musicFacade.currentId.value == currentIdInt;
final isThisPlaying = controller.isPlaying.value &&
controller.currentId.value == currentIdInt;
final currentProgress =
isThisPlaying ? musicFacade.progress.value : 0.0;
isThisPlaying ? controller.progress.value : 0.0;
return CircularProgressButton(
isPlaying: isThisPlaying,
@ -339,20 +339,20 @@ class MusicPlaylistView extends StatelessWidget {
onPlayPause: () async {
try {
Logger.d('MusicPlaylistView',
'播放按钮被点击,当前播放状态: ${musicFacade.isPlaying.value}');
'播放按钮被点击,当前播放状态: ${controller.isPlaying.value}');
Logger.d('MusicPlaylistView',
'当前歌曲ID: ${musicFacade.currentId.value}, 点击歌曲ID: $currentIdInt');
if (musicFacade.isPlaying.value) {
if (musicFacade.currentId.value == currentIdInt) {
'当前歌曲ID: ${controller.currentId.value}, 点击歌曲ID: $currentIdInt');
if (controller.isPlaying.value) {
if (controller.currentId.value == currentIdInt) {
Logger.d('MusicPlaylistView', '暂停当前播放的歌曲');
await musicFacade.musicPause();
await controller.musicPause();
} else {
Logger.d('MusicPlaylistView', '播放新歌曲');
await musicFacade.musicPlay(song);
await controller.musicPlay(song);
}
} else {
Logger.d('MusicPlaylistView', '开始播放歌曲');
await musicFacade.musicPlay(song);
await controller.musicPlay(song);
}
} catch (e) {
Logger.e('MusicPlaylistView', '播放音乐时发生错误: $e');
@ -362,8 +362,8 @@ class MusicPlaylistView extends StatelessWidget {
? (seekProgress) {
// 只有当前播放的歌曲才允许跳转
final targetPosition =
(musicFacade.duration.value * seekProgress).round();
musicFacade.seekToPosition(targetPosition);
(controller.duration.value * seekProgress).round();
controller.seekToPosition(targetPosition);
}
: null,
);
@ -449,14 +449,14 @@ class MusicPlaylistView extends StatelessWidget {
}
// 构建进度条
Widget _buildProgressBar(MusicServiceFacade musicFacade, bool isDarkMode) {
Widget _buildProgressBar(AgentController controller, bool isDarkMode) {
return Obx(() {
return Row(
children: [
SizedBox(
width: 40.w,
child: Text(
_formatDuration(musicFacade.currentPosition.value),
_formatDuration(controller.currentPosition.value),
style: TextStyle(
fontSize: 12.sp,
color: isDarkMode ? Colors.grey[400] : Colors.grey[600],
@ -480,11 +480,11 @@ class MusicPlaylistView extends StatelessWidget {
overlayShape: const RoundSliderOverlayShape(overlayRadius: 14),
),
child: Slider(
value: musicFacade.progress.value.clamp(0.0, 1.0),
value: controller.progress.value.clamp(0.0, 1.0),
onChanged: (value) {
final targetPosition =
(musicFacade.duration.value * value).round();
musicFacade.seekToPosition(targetPosition);
(controller.duration.value * value).round();
controller.seekToPosition(targetPosition);
},
),
),
@ -493,7 +493,7 @@ class MusicPlaylistView extends StatelessWidget {
SizedBox(
width: 40.w,
child: Text(
_formatDuration(musicFacade.duration.value),
_formatDuration(controller.duration.value),
style: TextStyle(
fontSize: 12.sp,
color: isDarkMode ? Colors.grey[400] : Colors.grey[600],
@ -508,14 +508,14 @@ class MusicPlaylistView extends StatelessWidget {
}
// 构建播放控制按钮
Widget _buildPlayControls(MusicServiceFacade musicFacade,
Widget _buildPlayControls(AgentController controller,
List<Map<String, dynamic>> playlist, bool isDarkMode) {
return Obx(() {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
IconButton(
onPressed: () => _playPrevious(musicFacade, playlist),
onPressed: () => _playPrevious(controller, playlist),
iconSize: 32.w,
padding: EdgeInsets.all(8.w), // 增大触摸区域
icon: Icon(
@ -540,10 +540,10 @@ class MusicPlaylistView extends StatelessWidget {
],
),
child: IconButton(
onPressed: () => _togglePlayPause(musicFacade),
onPressed: () => _togglePlayPause(controller),
padding: EdgeInsets.zero,
icon: Icon(
musicFacade.isPlaying.value ? Icons.pause : Icons.play_arrow,
controller.isPlaying.value ? Icons.pause : Icons.play_arrow,
size: 28.w,
color: Colors.white,
),
@ -551,7 +551,7 @@ class MusicPlaylistView extends StatelessWidget {
),
SizedBox(width: 16.w),
IconButton(
onPressed: () => _playNext(musicFacade, playlist),
onPressed: () => _playNext(controller, playlist),
iconSize: 32.w,
padding: EdgeInsets.all(8.w), // 增大触摸区域
icon: Icon(
@ -574,35 +574,35 @@ class MusicPlaylistView extends StatelessWidget {
}
// 播放/暂停切换
Future<void> _togglePlayPause(MusicServiceFacade musicFacade) async {
if (musicFacade.isPlaying.value) {
await musicFacade.musicPause();
Future<void> _togglePlayPause(AgentController controller) async {
if (controller.isPlaying.value) {
await controller.musicPause();
} else {
await musicFacade.musicResume();
await controller.musicResume();
}
}
// 播放上一曲
void _playPrevious(
MusicServiceFacade musicFacade, List<Map<String, dynamic>> playlist) {
musicFacade.updateCurrentPlaylistContext(
playlist, musicFacade.currentId.value);
musicFacade.playPrevious();
AgentController controller, List<Map<String, dynamic>> playlist) {
controller.updateCurrentPlaylistContext(
playlist, controller.currentId.value);
controller.playPrevious();
}
// 播放下一曲
void _playNext(
MusicServiceFacade musicFacade, List<Map<String, dynamic>> playlist) {
musicFacade.updateCurrentPlaylistContext(
playlist, musicFacade.currentId.value);
musicFacade.playNext();
AgentController controller, List<Map<String, dynamic>> playlist) {
controller.updateCurrentPlaylistContext(
playlist, controller.currentId.value);
controller.playNext();
}
// 播放选中的歌曲
void _playSelectedSong(MusicServiceFacade musicFacade, Map<String, dynamic> song,
void _playSelectedSong(AgentController controller, Map<String, dynamic> song,
List<Map<String, dynamic>> playlist) {
final songId = int.tryParse(song['id']?.toString() ?? '0') ?? 0;
musicFacade.updateCurrentPlaylistContext(playlist, songId);
musicFacade.musicPlay(song);
controller.updateCurrentPlaylistContext(playlist, songId);
controller.musicPlay(song);
}
}

131
lib/modules/pairing/controllers/pairing_controller.dart

@ -1,3 +1,6 @@
import 'dart:convert';
import 'dart:typed_data';
import 'package:app_settings/app_settings.dart';
import 'package:deep_voice/data/models/user_Info.dart';
import 'package:deep_voice/data/services/network/api.dart';
@ -418,41 +421,32 @@ class PairingController extends GetxController {
// TODO: 实现API验证逻辑
// 暂时模拟验证通过
// await Future.delayed(const Duration(seconds: 2));;
if (await authbindingDevice()) {
// 检查设备名称是否包含 DeepSound 或 DeapSound
final deviceName = device.name ?? '';
if (deviceName.contains('DeepSound') ||
deviceName.contains('DeapSound')) {
Logger.i(_tag, '设备验证通过');
_verifiedDeviceAddress = device.address;
// 检查是否已有BLE关联
final pairedMacAddress = await _bleManager.getPairedMacAddress();
Logger.i(_tag, '已关联的MAC地址: $pairedMacAddress');
if (pairedMacAddress != null &&
pairedMacAddress.toUpperCase() == device.address.toUpperCase()) {
// 已有关联,检查BLE连接状态
Logger.i(_tag, '设备已关联,检查BLE连接状态');
final currentBleState = await _bleManager.getConnectionState();
Logger.i(_tag, '验证时BLE状态: $currentBleState');
if (currentBleState == 2) {
// BleConnectionState.connected
Logger.i(_tag, 'BLE已连接,直接完成配对');
_transition(PairingState.completed);
} else {
Logger.i(_tag, 'BLE未连接,等待BLE连接');
_transition(PairingState.waitingBle);
}
if (await authbindingDevice(device)) {
Logger.i(_tag, '设备验证通过');
_verifiedDeviceAddress = device.address;
// 检查是否已有BLE关联
final pairedMacAddress = await _bleManager.getPairedMacAddress();
Logger.i(_tag, '已关联的MAC地址: $pairedMacAddress');
if (pairedMacAddress != null &&
pairedMacAddress.toUpperCase() == device.address.toUpperCase()) {
// 已有关联,检查BLE连接状态
Logger.i(_tag, '设备已关联,检查BLE连接状态');
final currentBleState = await _bleManager.getConnectionState();
Logger.i(_tag, '验证时BLE状态: $currentBleState');
if (currentBleState == 2) {
// BleConnectionState.connected
Logger.i(_tag, 'BLE已连接,直接完成配对');
_transition(PairingState.completed);
} else {
// 没有关联或关联的是其他设备,需要进行关联
Logger.i(_tag, '需要进行BLE设备关联');
_transition(PairingState.associating);
Logger.i(_tag, 'BLE未连接,等待BLE连接');
_transition(PairingState.waitingBle);
}
} else {
// _setError(ErrorType.deviceNotMatch, '请连接 DeepSound 设备');
_transition(PairingState.completed);
// 没有关联或关联的是其他设备,需要进行关联
Logger.i(_tag, '需要进行BLE设备关联');
_transition(PairingState.associating);
}
} else {
//验证失败 进入手动授权
@ -531,27 +525,76 @@ class PairingController extends GetxController {
}
//自动绑定设备
Future<bool> authbindingDevice() async {
if (User.instance.earphonemac == currentDevice?.address) {
Future<bool> authbindingDevice(BluetoothDevice device) async {
if (User.instance.earphonemac == device.address) {
// 已经绑定
return true;
}
// 调用BLE管理器的自动绑定方法
final response = await Api.bindearphone({
"earphonemac": currentDevice?.address,
});
Logger.i(_tag, '手动授权结果 ${response}');
if (response != null) {
Logger.i(_tag, '设备自动绑定成功');
User.instance.earphonemac = currentDevice?.address ?? '';
return true;
// 检查设备名称是否包含 DeepSound 或 DeapSound
final deviceName = device.name ?? '';
print('获取到 deviceName: $deviceName');
if (deviceName.contains('DeepSound') || deviceName.contains('DeapSound')) {
await _bleManager.startScan();
// 尝试最多读取 3 次设备信息,每次间隔 1 秒
Map<String, String>? deviceinfo;
for (int i = 0; i < 3; i++) {
deviceinfo = await _bleManager.getDeviceInfo(device.address);
print('获取到 deviceinfo: $deviceinfo');
if (deviceinfo != null) {
break;
}
await Future.delayed(Duration(seconds: 1));
}
print('获取到 deviceinfo: $deviceinfo');
if (deviceinfo != null) {
// final code = processHex(deviceinfo["hex"]!, 15, 25);
String code = extractAndReverseBytes(deviceinfo["hex"]!, 15, 24);
print('获取到 code: $code');
// 安全检查长度
if (code != "") {
final response = await Api.bindearphone({
"code": code,
"earphonemac": device.address,
});
Logger.i(_tag, '手动授权结果 ${response}');
if (response != null) {
Logger.i(_tag, '设备自动绑定成功');
User.instance.earphonemac = device.address ?? '';
return true;
} else {
Logger.e(_tag, '手动绑定失败!');
return false;
}
} else {
return false;
}
}
return false;
// 调用BLE管理器的自动绑定方法
} else {
Logger.e(_tag, '手动绑定失败!');
return false;
}
}
int reverseBits(int byte) {
// 这里是 nibble swap 还是 bit 反转?看你需求,先保持 nibble swap
return ((byte & 0x0F) << 4) | ((byte & 0xF0) >> 4);
}
String extractAndReverseBytes(String hex, int start, int end) {
List<int> bytes = [];
for (int i = 0; i < hex.length; i += 2) {
bytes.add(int.parse(hex.substring(i, i + 2), radix: 16));
}
List<int> slice = bytes.sublist(start, end + 1);
List<int> reversed = slice.reversed.toList();
return reversed
.map((b) => b.toRadixString(16).padLeft(2, '0'))
.join()
.toUpperCase();
}
/// 用户点击授权码输入框
void onAutoCodeInputTap() async {
// 调用BLE管理器的自动绑定方法

9
lib/routes/app_pages.dart

@ -6,7 +6,9 @@ import '../modules/home/bindings/home_binding.dart';
import '../modules/explore/views/explore_view.dart';
import '../modules/explore/bindings/explore_binding.dart';
import '../modules/meeting/bindings/meeting_binding.dart';
import '../modules/meeting/bindings/meeting_connect_binding.dart';
import '../modules/meeting/bindings/meeting_details_binding.dart';
import '../modules/meeting/views/meeting_connect_view.dart';
import '../modules/meeting/views/meeting_details_view.dart';
import '../modules/meeting/bindings/meeting_record_binding.dart';
import '../modules/meeting/views/meeting_record_view.dart';
@ -157,6 +159,13 @@ abstract class AppPages {
transition: Transition.rightToLeft,
transitionDuration: Duration(milliseconds: 250),
),
GetPage(
name: Routes.meetingConnect,
page: () => const MeetingConnectView(),
binding: MeetingConnectBinding(),
transition: Transition.rightToLeft,
transitionDuration: Duration(milliseconds: 250),
),
GetPage(
name: Routes.meetingRecord,
page: () => const MeetingRecordView(),

1
lib/routes/app_routes.dart

@ -22,6 +22,7 @@ abstract class Routes {
// 会议相关
static const meeting = '/meeting';
static const meetingDetails = '/meeting/details';
static const meetingConnect = '/meeting/connect';
static const meetingRecord = '/meeting/record';
// 语言功能
static const translation = '/translation'; // 实时翻译

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

@ -298,32 +298,48 @@ object BleService {
}
}
fun getDeviceInfo(address: String): Map<String,Any>? {
// 更新缓存
val idx = scanResults.indexOfFirst { it.device.address == address }
Log.d(TAG, "获取设备信息:${address} idx:$idx")
if (idx >= 0){
val result = scanResults[idx];
return printDeviceInfo(result)
}
return null;
}
/**
* 打印设备信息和服务UUID
*/
private fun printDeviceInfo(result: ScanResult) {
private fun printDeviceInfo(result: ScanResult):Map<String,Any>?{
val device = result.device
val scanRecord = result.scanRecord
val serviceUuids =
scanRecord?.serviceUuids?.joinToString(", ") { it.uuid.toString() } ?: "无"
Log.d(
TAG, "扫描到设备: 名称=${device.name ?: "未知"}, 地址=${device.address}, " +
TAG, "liwei--------- 扫描到设备: 名称=${device.name ?: "未知"}, 地址=${device.address}, " +
"RSSI=${result.rssi}, 服务UUID=[$serviceUuids]"
)
val info = mutableMapOf(
"name" to (device.name ?: "未知"),
"address" to device.address
)
// 打印广播数据的详细信息
val scanData = scanRecord?.bytes
if (scanData != null) {
val hexString = scanData.joinToString("") { String.format("%02X", it) }
Log.d(TAG, "广播数据: $hexString")
Log.d(TAG, "liwei------------ 广播数据: $hexString")
info["hex"] = hexString
// 解析服务数据
scanRecord.serviceData?.forEach { (uuid, data) ->
val dataHex = data.joinToString("") { String.format("%02X", it) }
Log.d(TAG, "服务数据: UUID=${uuid.uuid}, 数据=$dataHex")
}
}
return info
}
// ======================================================================================================

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

@ -294,6 +294,16 @@ class BleServicePlugin : FlutterPlugin, MethodCallHandler, ActivityAware,
result.error("UNSUPPORTED", "设备不支持Companion Device Manager", null)
}
}
"startScan" -> {
val connected = BleService.startScan()
result.success(connected)
}
"getDeviceInfo" -> {
val address = call.argument<String>("address")?:""
val info = BleService.getDeviceInfo(address);
Log.d(TAG, "getDeviceInfo $info")
result.success(info)
}
else -> {
result.notImplemented()
}

32
local_plugins/ble_service/lib/ble_service.dart

@ -281,6 +281,38 @@ class BleService {
}
}
/// 断开连接
Future<bool> startScan() async {
try {
final result = await _methodChannel.invokeMethod<bool>('startScan');
return result ?? false;
} catch (e) {
print('开启扫描ble设备失败: $e');
return false;
}
}
/// 断开连接
Future<Map<String, String>?> getDeviceInfo(String address) async {
try {
final result = await _methodChannel
.invokeMethod<Map>('getDeviceInfo', {'address': address});
print('获取设备信息!: $result');
if (result != null) {
return {
'name': result['name'] ?? "未知",
'address': result['address'] ?? "",
'hex': result['hex'] ?? "",
};
} else {
return null;
}
} catch (e) {
print('获取设备信息失败!: $e');
return null;
}
}
/// 注册BLE后台扫描
///
/// 注册后,即使应用被杀死,系统也会在检测到设备广播时唤醒应用

146
local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicService.kt

@ -1,5 +1,6 @@
package com.yunqiinnovation.music_service
import android.annotation.SuppressLint
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
@ -13,7 +14,9 @@ import android.media.AudioManager
import android.media.MediaPlayer
import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.os.IBinder
import android.os.Looper
import android.support.v4.media.MediaBrowserCompat
import android.support.v4.media.session.MediaSessionCompat
import android.support.v4.media.session.PlaybackStateCompat
@ -35,7 +38,7 @@ class MusicService : MediaBrowserServiceCompat() {
private const val DELETE_ACTION = "com.yunqiinnovation.music_service.DELETE_NOTIFICATION"
private val isRunning = AtomicBoolean(false)
private var currentIdInternal: Int? = null
private var currentIdInternal: String? = null
private var currentStatus = false
private var playlist = mutableListOf<Map<String,Any>>()
private var currentIndex = 0
@ -45,38 +48,38 @@ class MusicService : MediaBrowserServiceCompat() {
fun isServiceRunning() = isRunning.get()
fun isPlaying() = currentStatus
fun getCurrentId(): Int? = currentIdInternal
// 获取播放进度信息
fun getProgress(): Map<String, Any> {
return if (instance != null && instance!!.mediaPlayer != null) {
val player = instance!!.mediaPlayer!!
try {
val currentPosition = if (player.isPlaying || currentStatus) player.currentPosition else 0
val duration = player.duration
val progress = if (duration > 0) currentPosition.toDouble() / duration.toDouble() else 0.0
mapOf(
"currentPosition" to currentPosition,
"duration" to duration,
"progress" to progress
)
} catch (e: Exception) {
Log.e(TAG, "获取播放进度失败: ${e.message}")
mapOf(
"currentPosition" to 0,
"duration" to 0,
"progress" to 0.0
)
}
} else {
mapOf(
"currentPosition" to 0,
"duration" to 0,
"progress" to 0.0
)
}
}
fun getCurrentId(): String? = currentIdInternal
// // 获取播放进度信息
// fun getProgress(): Map<String, Any> {
// return if (instance != null && instance!!.mediaPlayer != null) {
// val player = instance!!.mediaPlayer!!
// try {
// val currentPosition = if (player.isPlaying || currentStatus) player.currentPosition else 0
// val duration = player.duration
// val progress = if (duration > 0) currentPosition.toDouble() / duration.toDouble() else 0.0
// mapOf(
// "currentPosition" to currentPosition,
// "duration" to duration,
// "progress" to progress
// )
// } catch (e: Exception) {
// Log.e(TAG, "获取播放进度失败: ${e.message}")
// mapOf(
// "currentPosition" to 0,
// "duration" to 0,
// "progress" to 0.0
// )
// }
// } else {
// mapOf(
// "currentPosition" to 0,
// "duration" to 0,
// "progress" to 0.0
// )
// }
// }
private var instance: MusicService? = null
private var flutterChannel: io.flutter.plugin.common.MethodChannel? = null
@ -118,7 +121,8 @@ class MusicService : MediaBrowserServiceCompat() {
val title = song["title"] as? String ?: song["name"] as? String ?: "未知标题"
val artist = song["artist"] as? String ?: song["sgener"] as? String ?: "未知歌手"
val coverUrl = song["coverUrl"] as? String ?: song["image"] as? String ?: ""
currentIdInternal = song["id"] as? String ?: song["id"] as? String ?: "0"
Log.i(TAG, "updateMediaSessionMetadata currentIdInternal: $currentIdInternal")
val metadata = android.support.v4.media.MediaMetadataCompat.Builder()
.putString(android.support.v4.media.MediaMetadataCompat.METADATA_KEY_TITLE, title)
.putString(android.support.v4.media.MediaMetadataCompat.METADATA_KEY_ARTIST, artist)
@ -166,6 +170,8 @@ class MusicService : MediaBrowserServiceCompat() {
private lateinit var mediaSession: MediaSessionCompat
private lateinit var audioManager: AudioManager
private var mediaPlayer: MediaPlayer? = null
private val progressHandler = Handler(Looper.getMainLooper())
private lateinit var progressUpdater: Runnable
override fun onCreate() {
super.onCreate()
@ -191,11 +197,35 @@ class MusicService : MediaBrowserServiceCompat() {
registerReceiver(controlReceiver, filter, Context.RECEIVER_NOT_EXPORTED)
} else {
registerReceiver(controlReceiver, filter)
}
// 初始化进度更新任务
progressUpdater = Runnable {
updateProgress()
progressHandler.postDelayed(progressUpdater, 1000) // 每秒更新一次
}
isReceiverRegistered = true
Log.i(TAG, "MediaService 启动完成")
}
@SuppressLint("SuspiciousIndentation")
private fun updateProgress() {
mediaPlayer?.let { player ->
if (player.isPlaying || currentStatus) {
val currentPosition = player.currentPosition
val duration = player.duration
val progress = if (duration > 0) currentPosition.toFloat() / duration else 0f
// 更新媒体会话进度
// 通知Flutter层
flutterChannel?.invokeMethod("onMediaSessionSeekTo1",
mapOf(
"currentIdInternal" to currentIdInternal,
"currentPosition" to currentPosition,
"duration" to duration,
"progress" to progress
))
}
}
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
Log.i(TAG, "onStartCommand: $intent")
intent?.action?.takeIf { it == Intent.ACTION_MEDIA_BUTTON }?.let {
@ -209,8 +239,10 @@ class MusicService : MediaBrowserServiceCompat() {
"play" -> intent.getSerializableExtra("song")?.let { url ->
val songMap = intent.getSerializableExtra("song") as? Map<String, Any>
songMap?.let { song ->
val songId = song["id"] ?: return@let
val songId = song["id"]as? String ?:"0"
Log.i(TAG, "command songId: $songId")
currentIdInternal = songId
val existingIndex = playlist.indexOfFirst { it["id"] == songId }
if (existingIndex == -1) {
playlist.add(song)
@ -323,9 +355,9 @@ class MusicService : MediaBrowserServiceCompat() {
}
override fun onSeekTo(pos: Long) {
Log.i(TAG, "onSeekTo 通知: ${pos}ms")
seekTo(pos.toInt())
// 通知Flutter层
flutterChannel?.invokeMethod("onMediaSessionSeekTo", mapOf("position" to pos.toInt()))
// seekTo(pos.toInt())
// // 通知Flutter层
// flutterChannel?.invokeMethod("onMediaSessionSeekTo", mapOf("position" to pos.toInt()))
}
override fun onStop() {
Log.i(TAG, "onStop 通知!")
@ -518,22 +550,28 @@ class MusicService : MediaBrowserServiceCompat() {
private fun play(song: Map<String,Any>) {
val url = song["url"] as? String ?: return
Log.i(TAG, "开始播放: $url")
val id = song["id"] as? Int ?: 0
val id = song["id"] as? String ?: "0"
Log.i(TAG, "开始播放play id: $id")
val title = song["title"] as? String ?: "未知标题"
val artist = song["artist"] as? String ?: "未知歌手"
val coverUrl = song["coverUrl"] as String
mediaPlayer?.release()
currentStatus = true
currentIdInternal = id
Log.i(TAG, "开始播放play currentIdInternal: $currentIdInternal")
mediaPlayer = MediaPlayer().apply {
setDataSource(url)
setAudioStreamType(AudioManager.STREAM_MUSIC)
setOnPreparedListener {
start()
updateNotification(title,artist,coverUrl)
// 启动进度更新
progressHandler.post(progressUpdater)
Log.i(TAG, "开始播放: $url")
}
setOnCompletionListener { playNextOrStop() }
setOnCompletionListener { // 停止进度更新
progressHandler.removeCallbacks(progressUpdater)
playNextOrStop() }
setOnErrorListener { _, what, extra ->
Log.e(TAG, "播放错误 what=$what extra=$extra")
true
@ -547,17 +585,18 @@ class MusicService : MediaBrowserServiceCompat() {
mediaPlayer?.takeIf { it.isPlaying }?.apply {
pause()
currentStatus = false
mediaSession.setPlaybackState(
PlaybackStateCompat.Builder()
.setState(PlaybackStateCompat.STATE_PAUSED, currentPosition.toLong(), 1.0f)
.build()
)
val song = playlist[currentIndex]
val title = song["title"] as? String ?: "未知标题"
val artist = song["artist"] as? String ?: "未知歌手"
val coverUrl = song["coverUrl"] as String
val id = song["id"] as? String ?: "0"
Log.i(TAG, "pause id: $id")
currentIdInternal = id
updateNotification(title,artist,coverUrl)
setPlaybackState(PlaybackStateCompat.STATE_PAUSED);
// 停止进度更新
progressHandler.removeCallbacks(progressUpdater)
Log.i(TAG, "已暂停")
}
@ -567,17 +606,18 @@ class MusicService : MediaBrowserServiceCompat() {
mediaPlayer?.takeIf { !it.isPlaying }?.apply {
start()
currentStatus = true
mediaSession.setPlaybackState(
PlaybackStateCompat.Builder()
.setState(PlaybackStateCompat.STATE_PLAYING, currentPosition.toLong(), 1.0f)
.build()
)
val song = playlist[currentIndex]
val title = song["title"] as? String ?: "未知标题"
val artist = song["artist"] as? String ?: "未知歌手"
val coverUrl = song["coverUrl"] as String
val id = song["id"] as? String ?: "0"
Log.i(TAG, "resume id: $id")
currentIdInternal = id
updateNotification(title,artist,coverUrl)
setPlaybackState(PlaybackStateCompat.STATE_PLAYING);
// 重启进度更新
progressHandler.post(progressUpdater)
// Log.i(TAG, "继续播放")
}
}
@ -635,6 +675,8 @@ class MusicService : MediaBrowserServiceCompat() {
unregisterReceiver(controlReceiver)
isReceiverRegistered = false
}
// 停止进度更新
progressHandler.removeCallbacks(progressUpdater)
mediaPlayer?.release()
stopForeground(true)
isRunning.set(false)

8
local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicServicePlugin.kt

@ -70,10 +70,10 @@ class MusicServicePlugin: FlutterPlugin, MethodCallHandler {
)
result.success(status)
}
"getProgress" -> {
val progressInfo = MusicService.getProgress()
result.success(progressInfo)
}
// "getProgress" -> {
// // val progressInfo = MusicService.getProgress()
// result.success(progressInfo)
// }
"seekTo" -> {
val position = call.argument<Int>("position")
if (position != null) {

14
local_plugins/music_service/lib/music_service.dart

@ -82,7 +82,10 @@ class MusicService {
/// 更新媒体会话进度信息
Future<void> updateMediaSessionProgress(
int currentPosition, int duration, double progress) async {
int currentPosition,
int duration,
double progress,
) async {
await _methodChannel.invokeMethod('updateMediaSessionProgress', {
'currentPosition': currentPosition,
'duration': duration,
@ -98,6 +101,7 @@ class MusicService {
required Function() onPrevious,
required Function(int) onSeekTo,
required Function() onStop,
required Function(String, int, int, double) onStop1,
}) async {
// 设置方法调用处理器来处理来自原生平台的回调
_methodChannel.setMethodCallHandler((call) async {
@ -121,6 +125,14 @@ class MusicService {
case 'onMediaSessionStop':
onStop();
break;
case 'onMediaSessionSeekTo1':
final currentIdInternal =
call.arguments['currentIdInternal'] as String;
final currentPosition = call.arguments['currentPosition'] as int;
final duration = call.arguments['duration'] as int;
final progress = call.arguments['progress'] as double;
onStop1(currentIdInternal, currentPosition, duration, progress);
break;
}
});

6
pubspec.yaml

@ -23,6 +23,10 @@ dependencies:
audio_session: ^0.1.18
flutter_markdown: ^0.7.7+1
markdown: ^7.1.1
appflowy_editor:
git:
url: https://github.com/AppFlowy-IO/appflowy-editor.git
ref: main
permission_handler: ^10.4.5
web_socket_channel: ^2.4.0
flutter_dotenv: ^5.1.0
@ -47,7 +51,7 @@ dependencies:
path: plugins/window_size
ref: 6c66ad23ee79749f30a8eece542cf54eaf157ed8
flutter_local_notifications: ^18.0.1
device_info_plus: ^10.1.2
device_info_plus: ^11.5.0
uuid: ^4.3.3
record: ^5.2.1
share_plus: ^11.0.0

Loading…
Cancel
Save