407 lines
13 KiB
Dart
407 lines
13 KiB
Dart
// 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<Duration>? _bufferedSubscription;
|
||
StreamSubscription<Duration?>? _durationSubscription;
|
||
StreamSubscription<Duration>? _positionSubscription;
|
||
StreamSubscription<PlayerState>? _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<String> _playHistory = [];
|
||
|
||
/// 播放列表
|
||
var playlist = <MusicModel>[].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<MusicModel>? 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<void> _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<String?> _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<void> 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<void> 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<void> 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<void> resetPlaybackStatus() async {
|
||
await _player.stop();
|
||
_player.seek(Duration.zero);
|
||
totalDuration.value = Duration.zero;
|
||
positionDuration.value = Duration.zero;
|
||
bufferedDuration.value = Duration.zero;
|
||
}
|
||
}
|