Browse Source

feat(音乐服务): 实现媒体会话功能并优化播放列表界面

1. 在音乐服务插件中新增媒体会话功能,支持锁屏控制和通知栏显示
2. 为AgentController添加播放列表管理和媒体会话回调处理
3. 重构音乐播放列表界面,优化布局和交互体验
4. 修复播放进度同步和ID类型转换问题
5. 简化圆形进度按钮的点击交互逻辑
newdev_shunjiawei
lxm 1 year ago
parent
commit
d69a3bb4c6
  1. 52
      lib/data/services/music_manager.dart
  2. 221
      lib/modules/agent/controllers/agent_controller.dart
  3. 32
      lib/modules/agent/views/circular_progress_button.dart
  4. 12
      lib/modules/login/controllers/login_controller.dart
  5. 5
      lib/modules/music/controllers/music_playlist_controller.dart
  6. 672
      lib/modules/music/views/music_playlist_view.dart
  7. 122
      local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicService.kt
  8. 25
      local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicServicePlugin.kt
  9. 156
      local_plugins/music_service/ios/music_service/Sources/music_service/MusicService.swift
  10. 62
      local_plugins/music_service/lib/music_service.dart

52
lib/data/services/music_manager.dart

@ -85,4 +85,56 @@ class MusicManager extends GetxService {
Future<void> playPlaylist(List<Map<String, dynamic>> playlist) async {
await _musicService.playMusicPlaylist(playlist);
}
/// 更新媒体会话进度信息
Future<void> updateMediaSessionProgress(
int currentPosition, int duration, double progress) async {
try {
await _musicService.updateMediaSessionProgress(
currentPosition, duration, progress);
} catch (e) {
Logger.d('MusicManager', '更新媒体会话进度失败: $e');
}
}
/// 设置媒体会话回调
Future<void> 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<void> updateMediaSessionMetadata(Map<String, dynamic> song) async {
try {
await _musicService.updateMediaSessionMetadata(song);
} catch (e) {
Logger.d('MusicManager', '更新媒体会话元数据失败: $e');
}
}
/// 更新媒体会话播放状态
Future<void> updateMediaSessionPlaybackState(bool isPlaying) async {
try {
await _musicService.updateMediaSessionPlaybackState(isPlaying);
} catch (e) {
Logger.d('MusicManager', '更新媒体会话播放状态失败: $e');
}
}
}

221
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 = <Map<String, dynamic>>[].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<String, dynamic>;
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<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 {
@ -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<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();
}
}

32
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,

12
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()");

5
lib/modules/music/controllers/music_playlist_controller.dart

@ -12,8 +12,5 @@ class MusicPlaylistController extends GetxController {
playlist.assignAll(playlistData);
}
@override
void onClose() {
super.onClose();
}
}

672
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<Map<String, dynamic>> playlist, bool isDarkMode) {
return Obx(() {
// 查找当前播放的歌曲 - 修复自动切换时不更新的问题
Map<String, dynamic>? 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<Map<String, dynamic>> 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<Map<String, dynamic>> 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<Map<String, dynamic>> 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<void> _togglePlayPause(AgentController controller) async {
if (controller.isPlaying.value) {
await controller.musicPause();
} else {
await controller.musicResume();
}
}
// 播放上一曲
void _playPrevious(
AgentController controller, List<Map<String, dynamic>> playlist) {
// 确保播放列表上下文是最新的
controller.updateCurrentPlaylistContext(
playlist, controller.currentId.value);
controller.playPrevious();
}
// 播放下一曲
void _playNext(
AgentController controller, List<Map<String, dynamic>> playlist) {
// 确保播放列表上下文是最新的
controller.updateCurrentPlaylistContext(
playlist, controller.currentId.value);
controller.playNext();
}
// 播放选中的歌曲
void _playSelectedSong(AgentController controller, Map<String, dynamic> song,
List<Map<String, dynamic>> playlist) {
// 更新播放列表上下文
final songId = int.tryParse(song['id']?.toString() ?? '0') ?? 0;
controller.updateCurrentPlaylistContext(playlist, songId);
// 播放选中的歌曲
controller.musicPlay(song);
}
}

122
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<String, Any>) {
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(

25
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<Int>("currentPosition") ?: 0
val duration = call.argument<Int>("duration") ?: 0
val progress = call.argument<Double>("progress") ?: 0.0
MusicService.updateMediaSessionProgress(currentPosition, duration, progress.toFloat())
result.success(null)
}
"enableMediaSessionCallbacks" -> {
MusicService.enableMediaSessionCallbacks(channel)
result.success(null)
}
"updateMediaSessionMetadata" -> {
val song = call.argument<Map<String, Any>>("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<Boolean>("isPlaying") ?: false
MusicService.updateMediaSessionPlaybackState(isPlaying)
result.success(null)
}
else -> result.notImplemented()
}
}

156
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
}
}
}

62
local_plugins/music_service/lib/music_service.dart

@ -79,4 +79,66 @@ class MusicService {
Future<void> playMusicPlaylist(List<Map<String, dynamic>> songs) async {
await _methodChannel.invokeMethod('playlist', {'songs': songs});
}
/// 更新媒体会话进度信息
Future<void> updateMediaSessionProgress(
int currentPosition, int duration, double progress) async {
await _methodChannel.invokeMethod('updateMediaSessionProgress', {
'currentPosition': currentPosition,
'duration': duration,
'progress': progress,
});
}
/// 设置媒体会话回调
Future<void> 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<void> updateMediaSessionMetadata(Map<String, dynamic> song) async {
await _methodChannel.invokeMethod('updateMediaSessionMetadata', {
'song': song,
});
}
/// 更新媒体会话播放状态
Future<void> updateMediaSessionPlaybackState(bool isPlaying) async {
await _methodChannel.invokeMethod('updateMediaSessionPlaybackState', {
'isPlaying': isPlaying,
});
}
}

Loading…
Cancel
Save