// Author: fengshengxiong // Date: 2024/5/30 // Description: 音乐播放器控制器 import 'dart:async'; import 'dart:io'; import 'package:easy_debounce/easy_debounce.dart'; import 'package:get/get.dart'; import 'package:just_audio/just_audio.dart'; import 'package:just_audio_background/just_audio_background.dart'; import 'package:tone_snap/components/base_easyloading.dart'; import 'package:tone_snap/data/api/music_api.dart'; import 'package:tone_snap/data/cache/music_cache_manager.dart'; import 'package:tone_snap/data/enum/play_mode.dart'; import 'package:tone_snap/data/models/music_model.dart'; import 'package:tone_snap/data/models/player_model.dart'; import 'package:tone_snap/data/storage/music_box.dart'; import 'package:tone_snap/data/storage/offline_box.dart'; import 'package:tone_snap/firebase/firebase_analytics_manager.dart'; import 'package:tone_snap/modules/sideb/music_bar/music_bar.dart'; import 'package:tone_snap/utils/audio_util.dart'; import 'package:tone_snap/utils/log_util.dart'; import 'package:tone_snap/utils/num_util.dart'; import 'package:tone_snap/utils/obj_util.dart'; class MusicPlayerController extends GetxController { static MusicPlayerController get to => Get.put(MusicPlayerController(), permanent: true); final _player = AudioPlayer(); StreamSubscription? _bufferedSubscription; StreamSubscription? _durationSubscription; StreamSubscription? _positionSubscription; StreamSubscription? _playerStateSubscription; /// 总时长、已播放的时长、缓冲时长 var totalDuration = Duration.zero.obs; var positionDuration = Duration.zero.obs; var bufferedDuration = Duration.zero.obs; /// 缓存管理器 // final _cacheManager = DefaultCacheManager(); final _cacheManager = MusicCacheManager.instance; /// 是否正在播放 var isPlaying = false.obs; /// 播放器处理状态 var processingState = ProcessingState.idle.obs; /// _player.seek(Duration.zero) 时,这可能会导致播放状态再次变为 completed。 /// 为了避免这种情况,在处理 completed 状态时添加一个标志位,确保在处理完成状态时不会重复触发 /// 是否已经处理过 completed 状态 var _isCompletedHandled = false; /// 拖动开始时是否是播放状态 var _seekFrontPlaying = true; /// 播放模式,默认列表循环 var playMode = MusicBox().getPlayMode().obs; var _playModeIndex = 0; /// 播放历史 videoId 集合 /// 随机播放切换到上一首时,从历史记录中取出上一个播放的 videoId final List _playHistory = []; /// 播放列表 var playlist = [].obs; /// 当前播放的歌曲索引 var currentIndex = 0.obs; /// 播放成功打点时的videoId,防止同一首歌播放状态监听重复打点 String? _logVideoId; @override void onInit() { super.onInit(); AudioUtil.configAudioSession(); } @override void onClose() { MusicBar().hide(); _cancelListening(); _player.dispose(); super.onClose(); } void playMusic(String? videoId, {List? playList, bool isCurrentPlaylist = false}) { if (ObjUtil.isEmpty(videoId)) return; if (!isCurrentPlaylist) { if (playList == null || playList.isEmpty) return; _playHistory.clear(); playlist.value = playList.map((e) => e).toList(); } MusicModel? model = playlist.firstWhereOrNull((e) => e.videoId == videoId); currentIndex.value = model != null ? playlist.indexOf(model) : 0; _startPlay(); } MusicModel? getMusicModel() { return playlist.isNotEmpty ? playlist[currentIndex.value] : null; } /// 播放歌曲 Future _startPlay() async { if (playlist.isNotEmpty) { FirebaseAnalyticsManager.logPlayerBpv(getMusicModel()?.videoId, getMusicModel()?.title, getMusicModel()?.subtitle); resetPlaybackStatus(); // Get.currentRoute == AppRoutes.playPage ? MusicBar().hide() : MusicBar().show(); try { final mediaItem = MediaItem( id: ObjUtil.getStr(getMusicModel()?.videoId), artist: ObjUtil.getStr(getMusicModel()?.subtitle), title: ObjUtil.getStr(getMusicModel()?.title), artUri: Uri.parse(ObjUtil.getStr(getMusicModel()?.coverUrl)), ); DateTime startLoadDateTime = DateTime.now(); var model = OfflineBox().getList().firstWhereOrNull((e) => e.videoId == getMusicModel()?.videoId); if (model != null && ObjUtil.isNotEmpty(model.localPath)) { // 有下载 LogUtil.d('读取下载路径=${model.localPath}'); if (!await File(model.localPath!).exists()) { BaseEasyLoading.toast('file does not exist'); FirebaseAnalyticsManager.logPlayerBFailAction('本地文件不存在'); return; } await _player.setFilePath(model.localPath!, tag: mediaItem); } else { // 无下载 var fileInfo = await MusicCacheManager.checkCache(getMusicModel()?.videoId); if (fileInfo != null) { // 有缓存 LogUtil.d('读取缓存路径=${fileInfo.file.path}'); // 如果有缓存,使用缓存文件 await _player.setFilePath(fileInfo.file.path, tag: mediaItem); } else { // 无缓存 final url = await _getMusicUrl(); if (ObjUtil.isEmpty(url)) { return; } await _player.setUrl(url!, tag: mediaItem); // 同时启动缓存下载 _cacheManager.downloadFile(url, key: MusicCacheManager.getCacheKey(getMusicModel()!.videoId!)).then((fileInfo) { LogUtil.d('缓存下载路径=${fileInfo.file.path}'); }); } } _cancelListening(); _addListening(); _player.play(); DateTime endLoadDateTime = DateTime.now(); // 获取毫秒差 int millisecondsDifference = endLoadDateTime.difference(startLoadDateTime).inMilliseconds; FirebaseAnalyticsManager.logPlayerBDelayAction( getMusicModel()?.videoId, getMusicModel()?.title, getMusicModel()?.subtitle, '${millisecondsDifference}ms', ); } catch (e) { LogUtil.e('Play failed: $e'); BaseEasyLoading.toast('Play failed'); FirebaseAnalyticsManager.logPlayerBFailAction(e.toString()); } } } /// 获取当前歌曲的播放 url Future _getMusicUrl() async { PlayerModel? model = await MusicApi.player(videoId: playlist[currentIndex.value].videoId); if (model != null && model.streamingData != null) { var formats = model.streamingData?.formats; if (formats != null && playlist.isNotEmpty) { return formats[0].url; } } return null; } /// 监听 void _addListening() { _player.playbackEventStream.listen((event) {}, onError: (Object e, StackTrace stackTrace) { LogUtil.e('A stream error occurred: $e'); }); _durationSubscription = _player.durationStream.listen((duration) { totalDuration.value = duration ?? Duration.zero; }); _positionSubscription = _player.positionStream.listen((position) { int comparison = position.compareTo(totalDuration.value); if (comparison > 0) { positionDuration.value = totalDuration.value; } else { positionDuration.value = position; } }); _bufferedSubscription = _player.bufferedPositionStream.listen((bufferedPosition) { int comparison = bufferedPosition.compareTo(totalDuration.value); if (comparison > 0) { bufferedDuration.value = totalDuration.value; } else { bufferedDuration.value = bufferedPosition; } }); _playerStateSubscription = _player.playerStateStream.listen((playerState) { isPlaying.value = _player.playing; processingState.value = playerState.processingState; switch (playerState.processingState) { case ProcessingState.idle: break; case ProcessingState.loading: break; case ProcessingState.buffering: break; case ProcessingState.ready: _isCompletedHandled = false; break; case ProcessingState.completed: if (!_isCompletedHandled) { _isCompletedHandled = true; _player.seek(Duration.zero); nextTrack(manualSwitch: false); } break; } if (isPlaying.value) { if (_logVideoId != getMusicModel()?.videoId) { _logVideoId = getMusicModel()?.videoId; FirebaseAnalyticsManager.logPlayerBSuccessAction( getMusicModel()?.videoId, getMusicModel()?.title, getMusicModel()?.subtitle, ); } } }); } /// 取消监听 void _cancelListening() { _bufferedSubscription?.cancel(); _durationSubscription?.cancel(); _positionSubscription?.cancel(); _playerStateSubscription?.cancel(); } /// 将播放器的播放位置设置为指定的时间 void seekToPosition(double value) { if (processingState.value == ProcessingState.ready) { positionDuration.value = Duration(seconds: value.toInt()); _player.seek(positionDuration.value); } } /// 开始/结束拖动进度条 Future seekStartEnd(int value) async { if (value == 0) { if (processingState.value == ProcessingState.ready) { _seekFrontPlaying = _player.playing; if (_player.playing) { _player.pause(); } } } else { // 拖动前如果是播放状态,拖动完成后恢复播放 if (_seekFrontPlaying) { _player.play(); } } } /// 播放/暂停 void playPause() { if (_player.playing) { pause(); } else { play(); } } /// 播放 void play() { if (processingState.value == ProcessingState.ready) { _player.play(); } } /// 暂停 void pause() { _player.pause(); } /// 上一首 Future previousTrack() async { // 通过防抖防止在音频源之间快速切换会导致PlatformException EasyDebounce.debounce( 'cutTheSong', const Duration(milliseconds: 300), () { if (playlist.length > 1) { switch(playMode.value) { case PlayMode.listLoop: currentIndex.value = (currentIndex.value - 1 + playlist.length) % playlist.length; _startPlay(); break; case PlayMode.random: bool historyExist = false; for (var i = _playHistory.length - 1; i >= 0; --i) { var history = _playHistory[i]; MusicModel? model = playlist.firstWhereOrNull((e) => e.videoId == history); if (model != null) { currentIndex.value = playlist.indexOf(model); _playHistory.remove(history); historyExist = true; break; } else { _playHistory.remove(history); } } if (!historyExist) { currentIndex.value = _getRandomNumber(); } _startPlay(); break; case PlayMode.singleCycle: currentIndex.value = (currentIndex.value - 1 + playlist.length) % playlist.length; _startPlay(); break; } } }, ); } /// 下一首 Future nextTrack({bool manualSwitch = true}) async { // 通过防抖防止在音频源之间快速切换会导致PlatformException EasyDebounce.debounce( 'cutTheSong', const Duration(milliseconds: 300), () { if (playlist.length > 1) { switch(playMode.value) { case PlayMode.listLoop: currentIndex.value = (currentIndex.value + 1) % playlist.length; _startPlay(); break; case PlayMode.random: // 记录当前播放的索引 _playHistory.add(getMusicModel()!.videoId!); currentIndex.value = _getRandomNumber(); _startPlay(); break; case PlayMode.singleCycle: if (manualSwitch) { currentIndex.value = (currentIndex.value + 1) % playlist.length; _startPlay(); } break; } } }, ); } /// 在列表范围内生成一个不包括当前索引的随机数 int _getRandomNumber() { return NumUtil.getRandomNumberExcludingCurrent(0, playlist.length, currentIndex.value); } /// 切换播放模式 void switchPlayMode() { if (_playModeIndex >= PlayMode.values.length - 1) { _playModeIndex = 0; } else { _playModeIndex++; } playMode.value = PlayMode.values[_playModeIndex]; MusicBox().putPlayMode(playMode.value); if (playMode.value == PlayMode.listLoop) { BaseEasyLoading.toast('List loop'); } if (playMode.value == PlayMode.random) { BaseEasyLoading.toast('Shuffle Playback'); } if (playMode.value == PlayMode.singleCycle) { BaseEasyLoading.toast('Single cycle'); } } /// 重置播放状态 Future resetPlaybackStatus() async { await _player.stop(); _player.seek(Duration.zero); totalDuration.value = Duration.zero; positionDuration.value = Duration.zero; bufferedDuration.value = Duration.zero; } }