From d69a3bb4c6efb3089cd7becdfadfd0696c84cda3 Mon Sep 17 00:00:00 2001 From: lxm <1274844347@qq.com> Date: Tue, 24 Jun 2025 21:20:18 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E9=9F=B3=E4=B9=90=E6=9C=8D=E5=8A=A1):=20?= =?UTF-8?q?=E5=AE=9E=E7=8E=B0=E5=AA=92=E4=BD=93=E4=BC=9A=E8=AF=9D=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=E5=B9=B6=E4=BC=98=E5=8C=96=E6=92=AD=E6=94=BE=E5=88=97?= =?UTF-8?q?=E8=A1=A8=E7=95=8C=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 在音乐服务插件中新增媒体会话功能,支持锁屏控制和通知栏显示 2. 为AgentController添加播放列表管理和媒体会话回调处理 3. 重构音乐播放列表界面,优化布局和交互体验 4. 修复播放进度同步和ID类型转换问题 5. 简化圆形进度按钮的点击交互逻辑 --- lib/data/services/music_manager.dart | 52 ++ .../agent/controllers/agent_controller.dart | 221 +++++- .../agent/views/circular_progress_button.dart | 32 +- .../login/controllers/login_controller.dart | 12 +- .../music_playlist_controller.dart | 5 +- .../music/views/music_playlist_view.dart | 672 ++++++++++++++---- .../music_service/MusicService.kt | 122 +++- .../music_service/MusicServicePlugin.kt | 25 + .../Sources/music_service/MusicService.swift | 156 ++++ .../music_service/lib/music_service.dart | 62 ++ 10 files changed, 1179 insertions(+), 180 deletions(-) diff --git a/lib/data/services/music_manager.dart b/lib/data/services/music_manager.dart index 26e7aef91..9bfae0e7a 100644 --- a/lib/data/services/music_manager.dart +++ b/lib/data/services/music_manager.dart @@ -85,4 +85,56 @@ class MusicManager extends GetxService { Future playPlaylist(List> playlist) async { await _musicService.playMusicPlaylist(playlist); } + + /// 更新媒体会话进度信息 + Future updateMediaSessionProgress( + int currentPosition, int duration, double progress) async { + try { + await _musicService.updateMediaSessionProgress( + currentPosition, duration, progress); + } catch (e) { + Logger.d('MusicManager', '更新媒体会话进度失败: $e'); + } + } + + /// 设置媒体会话回调 + Future setMediaSessionCallbacks({ + required Function() onPlay, + required Function() onPause, + required Function() onNext, + required Function() onPrevious, + required Function(int) onSeekTo, + required Function() onStop, + }) async { + try { + await _musicService.setMediaSessionCallbacks( + onPlay: onPlay, + onPause: onPause, + onNext: onNext, + onPrevious: onPrevious, + onSeekTo: onSeekTo, + onStop: onStop, + ); + } catch (e) { + Logger.d('MusicManager', '设置媒体会话回调失败: $e'); + } + } + + /// 更新媒体会话元数据 + Future updateMediaSessionMetadata(Map song) async { + try { + await _musicService.updateMediaSessionMetadata(song); + } catch (e) { + Logger.d('MusicManager', '更新媒体会话元数据失败: $e'); + } + } + + /// 更新媒体会话播放状态 + Future updateMediaSessionPlaybackState(bool isPlaying) async { + try { + await _musicService.updateMediaSessionPlaybackState(isPlaying); + } catch (e) { + Logger.d('MusicManager', '更新媒体会话播放状态失败: $e'); + } + } } diff --git a/lib/modules/agent/controllers/agent_controller.dart b/lib/modules/agent/controllers/agent_controller.dart index 5d394178c..79d3e64c3 100644 --- a/lib/modules/agent/controllers/agent_controller.dart +++ b/lib/modules/agent/controllers/agent_controller.dart @@ -43,6 +43,10 @@ class AgentController extends GetxController { var duration = 0.obs; // 总时长(毫秒) var progress = 0.0.obs; // 播放进度(0.0-1.0) + // 当前播放列表管理 + final currentPlaylist = >[].obs; // 当前播放列表 + final currentPlaylistIndex = 0.obs; // 当前播放歌曲在列表中的索引 + // 各种状态标志 final isInitialized = false.obs; final isListening = false.obs; //是否正在语音识别 @@ -102,6 +106,7 @@ class AgentController extends GetxController { _loadChatHistory(); //加载聊天历史记录 _subscribeToEvents(); //订阅代理服务事件 _initMusicStatus(); //初始化音乐状态 + _setupMediaSessionCallbacks(); //设置媒体会话回调 } // 初始化音乐播放状态 @@ -114,6 +119,23 @@ class AgentController extends GetxController { } } + // 设置媒体会话回调 + void _setupMediaSessionCallbacks() async { + try { + await _musiceManager.setMediaSessionCallbacks( + onPlay: onMediaSessionPlay, + onPause: onMediaSessionPause, + onNext: onMediaSessionNext, + onPrevious: onMediaSessionPrevious, + onSeekTo: onMediaSessionSeekTo, + onStop: onMediaSessionStop, + ); + Logger.d('AgentController', '媒体会话回调设置完成'); + } catch (e) { + Logger.e('AgentController', '媒体会话回调设置失败: $e'); + } + } + @override void onClose() { AgentService.stopConversation(); // 停止当前会话 @@ -871,7 +893,11 @@ class AgentController extends GetxController { // 立即更新UI状态,提供即时反馈 isPlaying.value = true; - currentId.value = song['id'] ?? 0; + // 修复:确保currentId接收int类型 + final songId = song['id']; + currentId.value = songId is int + ? songId + : (int.tryParse(songId?.toString() ?? '0') ?? 0); // 重置进度状态 currentPosition.value = 0; @@ -898,7 +924,11 @@ class AgentController extends GetxController { print('getMusicUrl 响应数据: ${response.toString()}'); // 调试输出原始数据 final urldata = response['urls'][0] as Map; song["url"] = urldata['url'] ?? ''; - currentId.value = song['id'] ?? 0; + // 修复:确保currentId接收int类型 + final newSongId = song['id']; + currentId.value = newSongId is int + ? newSongId + : (int.tryParse(newSongId?.toString() ?? '0') ?? 0); Logger.d('AgentController', '获取新URL成功'); } catch (e) { Logger.e('AgentController', '获取新URL失败: $e'); @@ -921,6 +951,10 @@ class AgentController extends GetxController { // 调用音乐管理器播放 await _musiceManager.play(musicData); + // 更新媒体会话元数据和播放状态 + await _updateMediaSessionMetadata(song); + await _updateMediaSessionPlaybackState(true); + // 启动进度定时器 _startProgressTimer(); @@ -940,6 +974,9 @@ class AgentController extends GetxController { await _musiceManager.pause(); isPlaying.value = false; _stopProgressTimer(); + + // 更新媒体会话播放状态 + await _updateMediaSessionPlaybackState(false); } catch (e) { Logger.e('AgentController', '暂停音乐失败: $e'); } @@ -952,6 +989,10 @@ class AgentController extends GetxController { await _musiceManager.resume(); isPlaying.value = true; _startProgressTimer(); + + // 更新媒体会话播放状态 + await _updateMediaSessionPlaybackState(true); + Logger.d('AgentController', '音乐恢复播放成功'); } catch (e) { Logger.e('AgentController', '恢复播放音乐失败: $e'); @@ -997,22 +1038,66 @@ class AgentController extends GetxController { 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 _updateMediaSessionProgress() async { + try { + // 这里将调用music_service插件的方法来更新系统媒体会话进度 + // 具体实现将在music_service插件中添加 + await _musiceManager.updateMediaSessionProgress( + currentPosition.value, + duration.value, + progress.value, + ); + } catch (e) { + // 忽略错误,避免影响正常播放 + Logger.d('AgentController', '更新媒体会话进度失败: $e'); + } + } + + /// 更新系统媒体会话的元数据 + Future _updateMediaSessionMetadata(Map song) async { + try { + await _musiceManager.updateMediaSessionMetadata(song); + } catch (e) { + Logger.d('AgentController', '更新媒体会话元数据失败: $e'); + } + } + + /// 更新系统媒体会话的播放状态 + Future _updateMediaSessionPlaybackState(bool isPlaying) async { + try { + await _musiceManager.updateMediaSessionPlaybackState(isPlaying); + } catch (e) { + Logger.d('AgentController', '更新媒体会话播放状态失败: $e'); + } + } + // 跳转到指定位置 Future seekToPosition(int positionMs) async { try { @@ -1071,45 +1156,51 @@ class AgentController extends GetxController { } 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() ?? '', + '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( '播放失败', @@ -1125,4 +1216,110 @@ class AgentController extends GetxController { loadingMusicId.value = ''; } } + + /// 播放上一曲 + Future 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 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> 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 onMediaSessionPlay() async { + Logger.d(TAG, '收到媒体会话播放回调'); + if (!isPlaying.value) { + await musicResume(); + } + } + + /// 处理来自系统通知栏的暂停操作 + Future onMediaSessionPause() async { + Logger.d(TAG, '收到媒体会话暂停回调'); + if (isPlaying.value) { + await musicPause(); + } + } + + /// 处理来自系统通知栏的下一曲操作 + Future onMediaSessionNext() async { + Logger.d(TAG, '收到媒体会话下一曲回调'); + await playNext(); + } + + /// 处理来自系统通知栏的上一曲操作 + Future onMediaSessionPrevious() async { + Logger.d(TAG, '收到媒体会话上一曲回调'); + await playPrevious(); + } + + /// 处理来自系统通知栏的跳转操作 + Future onMediaSessionSeekTo(int positionMs) async { + Logger.d(TAG, '收到媒体会话跳转回调: ${positionMs}ms'); + await seekToPosition(positionMs); + } + + /// 处理来自系统通知栏的停止操作 + Future onMediaSessionStop() async { + Logger.d(TAG, '收到媒体会话停止回调'); + await musicPause(); + // 重置播放状态 + currentPosition.value = 0; + progress.value = 0.0; + _stopProgressTimer(); + } } diff --git a/lib/modules/agent/views/circular_progress_button.dart b/lib/modules/agent/views/circular_progress_button.dart index 510bf95a4..28729d231 100644 --- a/lib/modules/agent/views/circular_progress_button.dart +++ b/lib/modules/agent/views/circular_progress_button.dart @@ -27,35 +27,9 @@ class CircularProgressButton extends StatelessWidget { @override Widget build(BuildContext context) { return GestureDetector( - onTapDown: (details) { - if (onSeek != null) { - // 计算点击位置对应的进度 - final center = Offset(size / 2, size / 2); - final tapPosition = details.localPosition; - final dx = tapPosition.dx - center.dx; - final dy = tapPosition.dy - center.dy; - - // 计算角度 (从顶部开始,顺时针) - double angle = math.atan2(dy, dx) + math.pi / 2; - if (angle < 0) angle += 2 * math.pi; - - // 转换为进度值 (0.0 - 1.0) - final seekProgress = angle / (2 * math.pi); - - // 检查点击是否在圆环区域内 - final distance = math.sqrt(dx * dx + dy * dy); - final innerRadius = (size / 2) - strokeWidth - 4; - final outerRadius = (size / 2) + 4; - - if (distance >= innerRadius && distance <= outerRadius) { - onSeek!(seekProgress); - } else if (distance < innerRadius) { - // 点击中心区域,播放/暂停 - onPlayPause(); - } - } else { - onPlayPause(); - } + onTap: () { + // 只保留播放/暂停功能,移除进度条点击交互 + onPlayPause(); }, child: SizedBox( width: size, diff --git a/lib/modules/login/controllers/login_controller.dart b/lib/modules/login/controllers/login_controller.dart index d6cc76ca1..a06a2cd1f 100644 --- a/lib/modules/login/controllers/login_controller.dart +++ b/lib/modules/login/controllers/login_controller.dart @@ -656,7 +656,7 @@ class LoginController extends GetxController { // }); try { switch (loginType) { - case 0: + case 0: //邮箱登录 { if (!isVerificationCodeComplete()) { Get.snackbar( @@ -670,7 +670,7 @@ class LoginController extends GetxController { } requestLogin({ "mail": contactController.text, - "phone": selectedCountryCode + contactController.text, + "phone": "", "stype": loginType, "vcode": verificationCodeController.text, "ttoken": "", @@ -681,7 +681,7 @@ class LoginController extends GetxController { }); break; } - case 1: + case 1: //手机号登录 { if (!isVerificationCodeComplete()) { Get.snackbar( @@ -706,7 +706,7 @@ class LoginController extends GetxController { }); break; } - case 4: + case 4: //Facebook登录 { print("数据获取Facebook"); final facebook = Facebook(); @@ -736,7 +736,7 @@ class LoginController extends GetxController { } break; } - case 3: + case 3: //Google登录 { print("数据获取_Google()"); final google = Google(); @@ -766,7 +766,7 @@ class LoginController extends GetxController { break; } - case 2: + case 2: //微信登录 { print("数据获取_FlWX()"); diff --git a/lib/modules/music/controllers/music_playlist_controller.dart b/lib/modules/music/controllers/music_playlist_controller.dart index ac0c8d2ee..367d2b560 100644 --- a/lib/modules/music/controllers/music_playlist_controller.dart +++ b/lib/modules/music/controllers/music_playlist_controller.dart @@ -12,8 +12,5 @@ class MusicPlaylistController extends GetxController { playlist.assignAll(playlistData); } - @override - void onClose() { - super.onClose(); - } + } diff --git a/lib/modules/music/views/music_playlist_view.dart b/lib/modules/music/views/music_playlist_view.dart index a66d228a3..eaffa21c2 100644 --- a/lib/modules/music/views/music_playlist_view.dart +++ b/lib/modules/music/views/music_playlist_view.dart @@ -1,7 +1,9 @@ import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:get/get.dart'; import '../../agent/controllers/agent_controller.dart'; import '../../agent/views/circular_progress_button.dart'; +import '../../../core/utils/logger.dart'; class MusicPlaylistView extends StatelessWidget { const MusicPlaylistView({Key? key}) : super(key: key); @@ -18,7 +20,6 @@ class MusicPlaylistView extends StatelessWidget { icon: const Icon(Icons.arrow_back_ios, size: 20), // iOS风格箭头 onPressed: () => Navigator.pop(context), ), - //title: const Text('音乐播放列表'), backgroundColor: isDarkMode ? Colors.grey[900] : Colors.white, foregroundColor: isDarkMode ? Colors.white : Colors.black, elevation: 0, @@ -45,32 +46,177 @@ class MusicPlaylistView extends StatelessWidget { ], ), ) - : ListView.builder( - padding: const EdgeInsets.all(16), - itemCount: playlist.length, - itemBuilder: (context, index) { - final item = playlist[index]; - final id = item["id"]?.toString() ?? ""; - final name = item["name"] ?? "未知歌曲"; - final url = item["url"] ?? ""; - final singer = item["singer"] ?? item["sgener"] ?? "未知歌手"; - final image = item["image"] ?? ""; - - return _buildMusicItem( - context, - controller, - id, - name, - url, - singer, - image, - isDarkMode, - ); - }, + : Column( + children: [ + // 上部:当前播放音乐详情 - 进一步优化高度占比 + SizedBox( + height: MediaQuery.of(context).size.height * + 0.35, // 从35%减少到32%,为进度条优化腾出空间 + child: _buildCurrentPlayingSection( + controller, playlist, isDarkMode), + ), + // 中部:播放全部按钮 + _buildPlayAllSection(controller, playlist, isDarkMode), + // 下部:歌单列表 - 使用剩余空间 + Expanded( + child: + _buildPlaylistSection(controller, playlist, isDarkMode), + ), + ], ), ); } + // 构建当前播放音乐详情部分 + Widget _buildCurrentPlayingSection(AgentController controller, + List> playlist, bool isDarkMode) { + return Obx(() { + // 查找当前播放的歌曲 - 修复自动切换时不更新的问题 + Map? currentSong; + if (controller.currentId.value != 0) { + try { + currentSong = playlist.firstWhere( + (song) => + int.tryParse(song['id']?.toString() ?? '0') == + controller.currentId.value, + ); + } catch (e) { + // 如果没找到,使用第一首歌曲 + currentSong = playlist.isNotEmpty ? playlist.first : null; + } + } else { + currentSong = playlist.isNotEmpty ? playlist.first : null; + } + + if (currentSong == null) { + return Container( + padding: const EdgeInsets.all(16), // 减少padding + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.music_note, + size: 60, // 减小图标尺寸 + color: Colors.grey[400], + ), + const SizedBox(height: 12), // 减少间距 + Text( + '暂无播放音乐', + style: TextStyle( + fontSize: 16, // 减小字体 + color: isDarkMode ? Colors.grey[400] : Colors.grey[600], + ), + ), + ], + ), + ), + ); + } + + final title = currentSong['name'] ?? currentSong['title'] ?? '未知歌曲'; + final artist = currentSong['singer'] ?? + currentSong['sgener'] ?? + currentSong['artist'] ?? + '未知歌手'; + final coverUrl = currentSong['image'] ?? currentSong['coverUrl'] ?? ''; + + return SingleChildScrollView( + // 添加滚动支持 + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 20, vertical: 12), // 优化padding:水平增加,垂直减少 + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // 封面图片 - 进一步优化尺寸 + _buildCoverImage(coverUrl, isDarkMode, + size: 110), // 从120减小到110,为进度条优化腾出空间 + const SizedBox(height: 12), // 减少间距从16到14 + // 歌曲信息 + _buildSongInfo(title, artist, isDarkMode), + const SizedBox(height: 16), // 增加歌曲信息和进度条之间的间距,提升视觉层次 + // 进度条 - 现在是水平布局,需要更多关注 + _buildProgressBar(controller, isDarkMode), + const SizedBox(height: 15), // 增加进度条和控制按钮之间的间距 + // 播放控制按钮 + _buildPlayControls(controller, playlist, isDarkMode), + ], + ), + ), + ); + }); + } + + // 构建播放全部按钮部分 + Widget _buildPlayAllSection(AgentController controller, + List> playlist, bool isDarkMode) { + return Container( + padding: const EdgeInsets.symmetric( + horizontal: 20, vertical: 12), // 优化padding,与音乐详情区域保持一致 + child: SizedBox( + width: double.infinity, + height: 48, // 从50减小到48,更紧凑 + child: ElevatedButton.icon( + onPressed: () async { + if (playlist.isNotEmpty) { + await controller.playPlaylist(playlist); + } + }, + icon: const Icon(Icons.play_arrow, + color: Colors.white, size: 20), // 调整图标大小 + label: const Text( + '播放全部', + style: TextStyle( + fontSize: 15, // 从16减小到15 + fontWeight: FontWeight.w600, + color: Colors.white, + ), + ), + style: ElevatedButton.styleFrom( + backgroundColor: Theme.of(Get.context!).primaryColor, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(14), // 从25减小到14,更协调 + ), + elevation: 3, // 从2增加到3,增强视觉层次 + shadowColor: Theme.of(Get.context!) + .primaryColor + .withValues(alpha: 0.4), // 添加阴影颜色 + ), + ), + ), + ); + } + + // 构建歌单列表部分 + Widget _buildPlaylistSection(AgentController controller, + List> playlist, bool isDarkMode) { + return ListView.builder( + padding: const EdgeInsets.symmetric( + horizontal: 20, vertical: 8), // 优化padding,与其他区域保持一致 + itemCount: playlist.length, + itemBuilder: (context, index) { + final item = playlist[index]; + final id = item["id"]?.toString() ?? ""; + final name = item["name"] ?? "未知歌曲"; + final url = item["url"] ?? ""; + final singer = item["singer"] ?? item["sgener"] ?? "未知歌手"; + final image = item["image"] ?? ""; + + return _buildMusicItem( + context, + controller, + id, + name, + url, + singer, + image, + isDarkMode, + ); + }, + ); + } + Widget _buildMusicItem( BuildContext context, AgentController controller, @@ -89,123 +235,399 @@ class MusicPlaylistView extends StatelessWidget { 'coverUrl': image, }; + return GestureDetector( + onTap: () { + // 点击音乐卡片直接播放对应歌曲 + _playSelectedSong(controller, song, Get.arguments ?? []); + }, + child: Container( + margin: const EdgeInsets.only(bottom: 10), // 从12减小到10,更紧凑 + padding: const EdgeInsets.all(14), // 从12增加到14,增加内部空间 + decoration: BoxDecoration( + color: isDarkMode ? Colors.grey[800] : Colors.white, + borderRadius: BorderRadius.circular(14), // 从12增加到14,更现代的圆角 + boxShadow: [ + BoxShadow( + color: Colors.black + .withValues(alpha: isDarkMode ? 0.25 : 0.05), // 增强阴影效果 + blurRadius: 4, // 从3增加到4 + spreadRadius: 0, // 从1减小到0,更自然的阴影 + offset: const Offset(0, 2), // 从(0,1)调整到(0,2),更明显的深度感 + ), + ], + ), + child: Row( + children: [ + // 封面图片 - 优化设计 + Container( + width: 52, // 从50增加到52,稍微大一点 + height: 52, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), // 从8增加到10,更现代的圆角 + color: isDarkMode + ? Colors.grey[700] + : Colors.grey[200], // 根据主题调整背景色 + boxShadow: [ + BoxShadow( + color: + Colors.black.withValues(alpha: isDarkMode ? 0.2 : 0.08), + blurRadius: 2, + offset: const Offset(0, 1), + ), + ], + ), + child: image.isNotEmpty + ? ClipRRect( + borderRadius: BorderRadius.circular(10), + child: Image.network( + image, + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) { + return Icon( + Icons.music_note, + color: isDarkMode + ? Colors.grey[500] + : Colors.grey[600], + size: 24, // 指定图标大小 + ); + }, + ), + ) + : Icon( + Icons.music_note, + color: isDarkMode ? Colors.grey[500] : Colors.grey[600], + size: 24, // 指定图标大小 + ), + ), + const SizedBox(width: 14), // 从12增加到14,增加间距 + // 歌曲信息 - 优化文本样式 + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, // 垂直居中对齐 + children: [ + Text( + name, + style: TextStyle( + fontSize: 15.5, // 从16减小到15.5,更协调 + fontWeight: FontWeight.w600, // 从w500增加到w600,更突出 + color: isDarkMode ? Colors.white : Colors.black87, + height: 1.2, // 添加行高控制 + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 3), // 从4减小到3,更紧凑 + Text( + singer, + style: TextStyle( + fontSize: 13, // 从14减小到13 + color: isDarkMode ? Colors.grey[400] : Colors.grey[600], + height: 1.1, // 添加行高控制 + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + const SizedBox(width: 14), // 从12增加到14,与封面图片间距保持一致 + // 播放按钮 + Obx(() { + // 修复类型比较问题:将id转换为int进行比较 + final currentIdInt = int.tryParse(id) ?? 0; + final isThisPlaying = controller.isPlaying.value && + controller.currentId.value == currentIdInt; + + // 只有当前播放的歌曲才显示进度 + final currentProgress = + isThisPlaying ? controller.progress.value : 0.0; + + return CircularProgressButton( + isPlaying: isThisPlaying, + progress: currentProgress, + primaryColor: Theme.of(context).primaryColor, + backgroundColor: Colors.grey, + size: 40.0, + strokeWidth: 2.5, + onPlayPause: () async { + try { + Logger.d('MusicPlaylistView', + '播放按钮被点击,当前播放状态: ${controller.isPlaying.value}'); + Logger.d('MusicPlaylistView', + '当前歌曲ID: ${controller.currentId.value}, 点击歌曲ID: $currentIdInt'); + + if (controller.isPlaying.value) { + if (controller.currentId.value == currentIdInt) { + Logger.d('MusicPlaylistView', '暂停当前播放的歌曲'); + await controller.musicPause(); + } else { + Logger.d('MusicPlaylistView', '播放新歌曲'); + await controller.musicPlay(song); + } + } else { + Logger.d('MusicPlaylistView', '开始播放歌曲'); + await controller.musicPlay(song); + } + } catch (e) { + Logger.e('MusicPlaylistView', '播放音乐时发生错误: $e'); + } + }, + onSeek: isThisPlaying + ? (seekProgress) { + // 只有当前播放的歌曲才允许跳转 + final targetPosition = + (controller.duration.value * seekProgress).round(); + controller.seekToPosition(targetPosition); + } + : null, + ); + }), + ], + ), + ), + ); + } + + // 构建封面图片 + Widget _buildCoverImage(String coverUrl, bool isDarkMode, + {double size = 200}) { return Container( - margin: const EdgeInsets.only(bottom: 12), - padding: const EdgeInsets.all(12), + width: size, + height: size, decoration: BoxDecoration( - color: isDarkMode ? Colors.grey[800] : Colors.white, - borderRadius: BorderRadius.circular(12), + borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( - color: Colors.black.withValues(alpha: isDarkMode ? 0.2 : 0.03), - blurRadius: 3, - spreadRadius: 1, - offset: const Offset(0, 1), + color: Colors.black.withValues(alpha: isDarkMode ? 0.3 : 0.1), + blurRadius: 15, + spreadRadius: 2, + offset: const Offset(0, 6), ), ], ), - child: Row( + child: ClipRRect( + borderRadius: BorderRadius.circular(16), + child: coverUrl.isNotEmpty + ? Image.network( + coverUrl, + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) { + return _buildDefaultCover(isDarkMode); + }, + ) + : _buildDefaultCover(isDarkMode), + ), + ); + } + + // 构建默认封面 + Widget _buildDefaultCover(bool isDarkMode) { + return Container( + color: isDarkMode ? Colors.grey[800] : Colors.grey[300], + child: Icon( + Icons.music_note, + size: 60, // 从20改为60,更合适的大小 + color: isDarkMode ? Colors.grey[600] : Colors.grey[500], + ), + ); + } + + // 构建歌曲信息 + Widget _buildSongInfo(String title, String artist, bool isDarkMode) { + return Column( + children: [ + Text( + title, + style: TextStyle( + fontSize: 18, // 从20减小到18 + fontWeight: FontWeight.bold, + color: isDarkMode ? Colors.white : Colors.black87, + ), + textAlign: TextAlign.center, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 6), // 从8减小到6 + Text( + artist, + style: TextStyle( + fontSize: 14, // 从16减小到14 + color: isDarkMode ? Colors.grey[400] : Colors.grey[600], + ), + textAlign: TextAlign.center, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], + ); + } + + // 构建进度条 - 优化布局:将进度条和时间显示合并到同一水平线 + Widget _buildProgressBar(AgentController controller, bool isDarkMode) { + return Obx(() { + return Row( children: [ - // 封面图片 - Container( - width: 50, - height: 50, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(8), - color: Colors.grey[300], + // 左侧:当前播放时间 + SizedBox( + width: 35.w, // 固定宽度确保对齐,足够显示 "99:59" 格式 + + child: Text( + _formatDuration(controller.currentPosition.value), + style: TextStyle( + fontSize: 12, + color: isDarkMode ? Colors.grey[400] : Colors.grey[600], + fontFeatures: const [FontFeature.tabularFigures()], // 使用等宽数字 + ), + textAlign: TextAlign.left, ), - child: image.isNotEmpty - ? ClipRRect( - borderRadius: BorderRadius.circular(8), - child: Image.network( - image, - fit: BoxFit.cover, - errorBuilder: (context, error, stackTrace) { - return Icon( - Icons.music_note, - color: Colors.grey[600], - ); - }, - ), - ) - : Icon( - Icons.music_note, - color: Colors.grey[600], - ), ), - const SizedBox(width: 12), - // 歌曲信息 + SizedBox(width: 6.w), // 左侧时间和进度条之间的间距 + // 中间:可交互的进度条 Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - name, - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w500, - color: isDarkMode ? Colors.white : Colors.black87, - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - const SizedBox(height: 4), - Text( - singer, - style: TextStyle( - fontSize: 14, - color: isDarkMode ? Colors.grey[400] : Colors.grey[600], - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, + child: SliderTheme( + data: SliderTheme.of(Get.context!).copyWith( + activeTrackColor: Theme.of(Get.context!).primaryColor, + inactiveTrackColor: + isDarkMode ? Colors.grey[700] : Colors.grey[300], + thumbColor: Theme.of(Get.context!).primaryColor, + overlayColor: + Theme.of(Get.context!).primaryColor.withValues(alpha: 0.2), + trackHeight: 4.h, + thumbShape: const RoundSliderThumbShape(enabledThumbRadius: 0), + overlayShape: const RoundSliderOverlayShape(overlayRadius: 0), + ), + child: Slider( + value: controller.progress.value.clamp(0.0, 1.0), + onChanged: (value) { + final targetPosition = + (controller.duration.value * value).round(); + controller.seekToPosition(targetPosition); + }, + ), + ), + ), + SizedBox(width: 6.w), // 进度条和右侧时间之间的间距 + // 右侧:音乐总时长 + SizedBox( + width: 35.w, // 固定宽度确保对齐,足够显示 "99:59" 格式 + child: Text( + _formatDuration(controller.duration.value), + style: TextStyle( + fontSize: 12, + color: isDarkMode ? Colors.grey[400] : Colors.grey[600], + fontFeatures: const [FontFeature.tabularFigures()], // 使用等宽数字 + ), + textAlign: TextAlign.right, + ), + ), + ], + ); + }); + } + + // 格式化时间显示 + String _formatDuration(int milliseconds) { + final duration = Duration(milliseconds: milliseconds); + final minutes = duration.inMinutes; + final seconds = duration.inSeconds % 60; + return '${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}'; + } + + // 构建播放控制按钮 + Widget _buildPlayControls(AgentController controller, + List> playlist, bool isDarkMode) { + return Obx(() { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // 上一曲按钮 + IconButton( + onPressed: () => _playPrevious(controller, playlist), + icon: Icon( + Icons.skip_previous, + size: 32, // 从36减小到32 + color: isDarkMode ? Colors.white70 : Colors.black54, + ), + ), + const SizedBox(width: 16), // 从20减小到16 + // 播放/暂停按钮 + Container( + width: 56, // 从64减小到56 + height: 56, + decoration: BoxDecoration( + color: Theme.of(Get.context!).primaryColor, + shape: BoxShape.circle, + boxShadow: [ + BoxShadow( + color: Theme.of(Get.context!) + .primaryColor + .withValues(alpha: 0.3), + blurRadius: 10, // 从12减小到10 + spreadRadius: 1, // 从2减小到1 + offset: const Offset(0, 3), // 从4减小到3 ), ], ), + child: IconButton( + onPressed: () => _togglePlayPause(controller), + icon: Icon( + controller.isPlaying.value ? Icons.pause : Icons.play_arrow, + size: 28, // 从32减小到28 + color: Colors.white, + ), + ), + ), + const SizedBox(width: 16), // 从20减小到16 + // 下一曲按钮 + IconButton( + onPressed: () => _playNext(controller, playlist), + icon: Icon( + Icons.skip_next, + size: 32, // 从36减小到32 + color: isDarkMode ? Colors.white70 : Colors.black54, + ), ), - const SizedBox(width: 12), - // 播放按钮 - Obx(() { - final isThisPlaying = - controller.isPlaying.value && controller.currentId.value == id; - - // 只有当前播放的歌曲才显示进度 - final currentProgress = - isThisPlaying ? controller.progress.value : 0.0; - - return CircularProgressButton( - isPlaying: isThisPlaying, - progress: currentProgress, - primaryColor: Theme.of(context).primaryColor, - backgroundColor: Colors.grey, - size: 40.0, - strokeWidth: 2.5, - onPlayPause: () async { - try { - if (controller.isPlaying.value) { - if (controller.currentId.value == id) { - await controller.musicPause(); - } else { - await controller.musicPlay(song); - } - } else { - controller.musicPlay(song); - } - } catch (e) { - // 静默处理错误 - } - }, - onSeek: isThisPlaying - ? (seekProgress) { - // 只有当前播放的歌曲才允许跳转 - final targetPosition = - (controller.duration.value * seekProgress).round(); - controller.seekToPosition(targetPosition); - } - : null, - ); - }), ], - ), - ); + ); + }); + } + + // 播放/暂停切换 + Future _togglePlayPause(AgentController controller) async { + if (controller.isPlaying.value) { + await controller.musicPause(); + } else { + await controller.musicResume(); + } + } + + // 播放上一曲 + void _playPrevious( + AgentController controller, List> playlist) { + // 确保播放列表上下文是最新的 + controller.updateCurrentPlaylistContext( + playlist, controller.currentId.value); + controller.playPrevious(); + } + + // 播放下一曲 + void _playNext( + AgentController controller, List> playlist) { + // 确保播放列表上下文是最新的 + controller.updateCurrentPlaylistContext( + playlist, controller.currentId.value); + controller.playNext(); + } + + // 播放选中的歌曲 + void _playSelectedSong(AgentController controller, Map song, + List> playlist) { + // 更新播放列表上下文 + final songId = int.tryParse(song['id']?.toString() ?? '0') ?? 0; + controller.updateCurrentPlaylistContext(playlist, songId); + + // 播放选中的歌曲 + controller.musicPlay(song); } } diff --git a/local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicService.kt b/local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicService.kt index 682098255..b807dbc03 100644 --- a/local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicService.kt +++ b/local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicService.kt @@ -76,6 +76,88 @@ class MusicService : MediaBrowserServiceCompat() { } private var instance: MusicService? = null + private var flutterChannel: io.flutter.plugin.common.MethodChannel? = null + + // 更新媒体会话进度信息 + fun updateMediaSessionProgress(currentPosition: Int, duration: Int, progress: Float) { + instance?.let { service -> + try { + val state = if (currentStatus) PlaybackStateCompat.STATE_PLAYING else PlaybackStateCompat.STATE_PAUSED + val playbackState = PlaybackStateCompat.Builder() + .setActions( + PlaybackStateCompat.ACTION_PLAY or + PlaybackStateCompat.ACTION_PAUSE or + PlaybackStateCompat.ACTION_PLAY_PAUSE or + PlaybackStateCompat.ACTION_SKIP_TO_NEXT or + PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS or + PlaybackStateCompat.ACTION_STOP or + PlaybackStateCompat.ACTION_SEEK_TO + ) + .setState(state, currentPosition.toLong(), 1.0f, System.currentTimeMillis()) + .build() + service.mediaSession.setPlaybackState(playbackState) + } catch (e: Exception) { + Log.e(TAG, "更新媒体会话进度失败: ${e.message}") + } + } + } + + // 启用媒体会话回调 + fun enableMediaSessionCallbacks(channel: io.flutter.plugin.common.MethodChannel) { + flutterChannel = channel + Log.d(TAG, "媒体会话回调已启用") + } + + // 更新媒体会话元数据 + fun updateMediaSessionMetadata(song: Map) { + instance?.let { service -> + try { + 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 ?: "" + + 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) + .putString(android.support.v4.media.MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, coverUrl) + .build() + + service.mediaSession.setMetadata(metadata) + Log.d(TAG, "媒体会话元数据已更新: $title - $artist") + } catch (e: Exception) { + Log.e(TAG, "更新媒体会话元数据失败: ${e.message}") + } + } + } + + // 更新媒体会话播放状态 + fun updateMediaSessionPlaybackState(isPlaying: Boolean) { + instance?.let { service -> + try { + currentStatus = isPlaying + val state = if (isPlaying) PlaybackStateCompat.STATE_PLAYING else PlaybackStateCompat.STATE_PAUSED + val position = service.mediaPlayer?.currentPosition?.toLong() ?: 0L + + val playbackState = PlaybackStateCompat.Builder() + .setActions( + PlaybackStateCompat.ACTION_PLAY or + PlaybackStateCompat.ACTION_PAUSE or + PlaybackStateCompat.ACTION_PLAY_PAUSE or + PlaybackStateCompat.ACTION_SKIP_TO_NEXT or + PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS or + PlaybackStateCompat.ACTION_STOP or + PlaybackStateCompat.ACTION_SEEK_TO + ) + .setState(state, position, 1.0f, System.currentTimeMillis()) + .build() + + service.mediaSession.setPlaybackState(playbackState) + Log.d(TAG, "媒体会话播放状态已更新: $isPlaying") + } catch (e: Exception) { + Log.e(TAG, "更新媒体会话播放状态失败: ${e.message}") + } + } + } } private var isReceiverRegistered = false private lateinit var mediaSession: MediaSessionCompat @@ -198,10 +280,42 @@ class MusicService : MediaBrowserServiceCompat() { ) // 回调中处理具体操作 setCallback(object : MediaSessionCompat.Callback() { - override fun onPlay() { Log.i(TAG, "onPlay 通知!"); resume()} - override fun onPause() { Log.i(TAG, "onPause 通知!"); pause()} - override fun onSkipToNext(){ Log.i(TAG, "onSkipToNext 通知!"); playNext()} - override fun onSkipToPrevious() { Log.i(TAG, "onSkipToNext 通知!"); playPrevious()} + override fun onPlay() { + Log.i(TAG, "onPlay 通知!") + resume() + // 通知Flutter层 + flutterChannel?.invokeMethod("onMediaSessionPlay", null) + } + override fun onPause() { + Log.i(TAG, "onPause 通知!") + pause() + // 通知Flutter层 + flutterChannel?.invokeMethod("onMediaSessionPause", null) + } + override fun onSkipToNext(){ + Log.i(TAG, "onSkipToNext 通知!") + playNext() + // 通知Flutter层 + flutterChannel?.invokeMethod("onMediaSessionNext", null) + } + override fun onSkipToPrevious() { + Log.i(TAG, "onSkipToPrevious 通知!") + playPrevious() + // 通知Flutter层 + flutterChannel?.invokeMethod("onMediaSessionPrevious", null) + } + override fun onSeekTo(pos: Long) { + Log.i(TAG, "onSeekTo 通知: ${pos}ms") + seekTo(pos.toInt()) + // 通知Flutter层 + flutterChannel?.invokeMethod("onMediaSessionSeekTo", mapOf("position" to pos.toInt())) + } + override fun onStop() { + Log.i(TAG, "onStop 通知!") + stopSelf() + // 通知Flutter层 + flutterChannel?.invokeMethod("onMediaSessionStop", null) + } }) // 初始播放状态 setPlaybackState( diff --git a/local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicServicePlugin.kt b/local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicServicePlugin.kt index 74ee11e41..0c2fe1256 100644 --- a/local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicServicePlugin.kt +++ b/local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicServicePlugin.kt @@ -83,6 +83,31 @@ class MusicServicePlugin: FlutterPlugin, MethodCallHandler { result.error("INVALID_ARGUMENT", "Missing 'position' parameter", null) } } + "updateMediaSessionProgress" -> { + val currentPosition = call.argument("currentPosition") ?: 0 + val duration = call.argument("duration") ?: 0 + val progress = call.argument("progress") ?: 0.0 + MusicService.updateMediaSessionProgress(currentPosition, duration, progress.toFloat()) + result.success(null) + } + "enableMediaSessionCallbacks" -> { + MusicService.enableMediaSessionCallbacks(channel) + result.success(null) + } + "updateMediaSessionMetadata" -> { + val song = call.argument>("song") + if (song != null) { + MusicService.updateMediaSessionMetadata(song) + result.success(null) + } else { + result.error("INVALID_ARGUMENT", "Song data is required", null) + } + } + "updateMediaSessionPlaybackState" -> { + val isPlaying = call.argument("isPlaying") ?: false + MusicService.updateMediaSessionPlaybackState(isPlaying) + result.success(null) + } else -> result.notImplemented() } } diff --git a/local_plugins/music_service/ios/music_service/Sources/music_service/MusicService.swift b/local_plugins/music_service/ios/music_service/Sources/music_service/MusicService.swift index 81aa3d2c3..45b830e4a 100644 --- a/local_plugins/music_service/ios/music_service/Sources/music_service/MusicService.swift +++ b/local_plugins/music_service/ios/music_service/Sources/music_service/MusicService.swift @@ -16,6 +16,8 @@ public class MusicService: NSObject { private var playlist: [[String: String]] = [] private var currentIndex = 0 public var isPlaying = false + private var flutterChannel: FlutterMethodChannel? + private var progressTimer: Timer? private override init() { super.init() @@ -147,4 +149,158 @@ public class MusicService: NSObject { private func updatePlaybackState() { // iOS manages playback state automatically, but you can update NowPlayingInfo if needed } + + // MARK: - 新增媒体会话功能 + + /// 更新媒体会话进度信息 + public func updateMediaSessionProgress(currentPosition: Int, duration: Int, progress: Float) { + var nowPlayingInfo = MPNowPlayingInfoCenter.default().nowPlayingInfo ?? [:] + + // 更新播放时长和当前位置 + nowPlayingInfo[MPMediaItemPropertyPlaybackDuration] = Double(duration) / 1000.0 + nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = Double(currentPosition) / 1000.0 + nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = isPlaying ? 1.0 : 0.0 + + MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo + } + + /// 启用媒体会话回调 + public func enableMediaSessionCallbacks(channel: FlutterMethodChannel) { + flutterChannel = channel + setupEnhancedRemoteCommandCenter() + startProgressTimer() + } + + /// 更新媒体会话元数据 + public func updateMediaSessionMetadata(song: [String: Any]) { + let title = song["title"] as? String ?? song["name"] as? String ?? "Unknown Title" + let artist = song["artist"] as? String ?? song["sgener"] as? String ?? "Unknown Artist" + let coverUrl = song["coverUrl"] as? String ?? song["image"] as? String ?? "" + + var nowPlayingInfo: [String: Any] = [ + MPMediaItemPropertyTitle: title, + MPMediaItemPropertyArtist: artist + ] + + // 异步加载封面图片 + 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 { + MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo + } + } + + /// 更新媒体会话播放状态 + public func updateMediaSessionPlaybackState(isPlaying: Bool) { + self.isPlaying = isPlaying + var nowPlayingInfo = MPNowPlayingInfoCenter.default().nowPlayingInfo ?? [:] + nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = isPlaying ? 1.0 : 0.0 + MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo + + if isPlaying { + startProgressTimer() + } else { + stopProgressTimer() + } + } + + /// 设置增强的远程控制中心 + private func setupEnhancedRemoteCommandCenter() { + let commandCenter = MPRemoteCommandCenter.shared() + + // 清除之前的目标 + commandCenter.playCommand.removeTarget(nil) + commandCenter.pauseCommand.removeTarget(nil) + commandCenter.nextTrackCommand.removeTarget(nil) + commandCenter.previousTrackCommand.removeTarget(nil) + + // 播放命令 + commandCenter.playCommand.addTarget { [weak self] _ in + self?.resume() + self?.flutterChannel?.invokeMethod("onMediaSessionPlay", arguments: nil) + return .success + } + + // 暂停命令 + commandCenter.pauseCommand.addTarget { [weak self] _ in + self?.pause() + self?.flutterChannel?.invokeMethod("onMediaSessionPause", arguments: nil) + return .success + } + + // 下一曲命令 + commandCenter.nextTrackCommand.addTarget { [weak self] _ in + self?.next() + self?.flutterChannel?.invokeMethod("onMediaSessionNext", arguments: nil) + return .success + } + + // 上一曲命令 + commandCenter.previousTrackCommand.addTarget { [weak self] _ in + self?.previous() + self?.flutterChannel?.invokeMethod("onMediaSessionPrevious", arguments: nil) + return .success + } + + // 跳转命令 + commandCenter.changePlaybackPositionCommand.isEnabled = true + commandCenter.changePlaybackPositionCommand.addTarget { [weak self] event in + if let event = event as? MPChangePlaybackPositionCommandEvent { + let positionMs = Int(event.positionTime * 1000) + self?.seekTo(positionMs: positionMs) + self?.flutterChannel?.invokeMethod("onMediaSessionSeekTo", arguments: ["position": positionMs]) + return .success + } + return .commandFailed + } + + // 停止命令 + commandCenter.stopCommand.addTarget { [weak self] _ in + self?.stop() + self?.flutterChannel?.invokeMethod("onMediaSessionStop", arguments: nil) + return .success + } + } + + /// 启动进度定时器 + private func startProgressTimer() { + stopProgressTimer() + progressTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in + self?.updateProgressInfo() + } + } + + /// 停止进度定时器 + private func stopProgressTimer() { + progressTimer?.invalidate() + progressTimer = nil + } + + /// 更新进度信息 + private func updateProgressInfo() { + guard let player = player, let currentItem = player.currentItem, isPlaying else { return } + + let currentTime = CMTimeGetSeconds(player.currentTime()) + let duration = CMTimeGetSeconds(currentItem.duration) + + if !currentTime.isNaN && !currentTime.isInfinite && !duration.isNaN && !duration.isInfinite { + var nowPlayingInfo = MPNowPlayingInfoCenter.default().nowPlayingInfo ?? [:] + nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = currentTime + nowPlayingInfo[MPMediaItemPropertyPlaybackDuration] = duration + nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = 1.0 + MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo + } + } } diff --git a/local_plugins/music_service/lib/music_service.dart b/local_plugins/music_service/lib/music_service.dart index ac091afb0..b9d43552e 100644 --- a/local_plugins/music_service/lib/music_service.dart +++ b/local_plugins/music_service/lib/music_service.dart @@ -79,4 +79,66 @@ class MusicService { Future playMusicPlaylist(List> songs) async { await _methodChannel.invokeMethod('playlist', {'songs': songs}); } + + /// 更新媒体会话进度信息 + Future updateMediaSessionProgress( + int currentPosition, int duration, double progress) async { + await _methodChannel.invokeMethod('updateMediaSessionProgress', { + 'currentPosition': currentPosition, + 'duration': duration, + 'progress': progress, + }); + } + + /// 设置媒体会话回调 + Future setMediaSessionCallbacks({ + required Function() onPlay, + required Function() onPause, + required Function() onNext, + required Function() onPrevious, + required Function(int) onSeekTo, + required Function() onStop, + }) async { + // 设置方法调用处理器来处理来自原生平台的回调 + _methodChannel.setMethodCallHandler((call) async { + switch (call.method) { + case 'onMediaSessionPlay': + onPlay(); + break; + case 'onMediaSessionPause': + onPause(); + break; + case 'onMediaSessionNext': + onNext(); + break; + case 'onMediaSessionPrevious': + onPrevious(); + break; + case 'onMediaSessionSeekTo': + final position = call.arguments['position'] as int; + onSeekTo(position); + break; + case 'onMediaSessionStop': + onStop(); + break; + } + }); + + // 通知原生平台启用媒体会话回调 + await _methodChannel.invokeMethod('enableMediaSessionCallbacks'); + } + + /// 更新媒体会话元数据 + Future updateMediaSessionMetadata(Map song) async { + await _methodChannel.invokeMethod('updateMediaSessionMetadata', { + 'song': song, + }); + } + + /// 更新媒体会话播放状态 + Future updateMediaSessionPlaybackState(bool isPlaying) async { + await _methodChannel.invokeMethod('updateMediaSessionPlaybackState', { + 'isPlaying': isPlaying, + }); + } }