gitee项目地址 github项目地址
如果喜欢请点点star
这里今天说一下项目的播放器选择
查看了一下pub.dev 上主流的播放器插件主要有audioplayers和just_audio
额外提一嘴android 和 ios 的配置
androidAndroidManifest.xml
ios
CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName 仿网易云 CFBundlePackageType APPL CFBundleShortVersionString $(FLUTTER_BUILD_NAME) CFBundleSignature ???? CFBundleVersion $(FLUTTER_BUILD_NUMBER) LSRequiresIPhoneOS UILaunchStoryboardName LaunchScreen UIMainStoryboardFile Main UISupportedInterfaceOrientations UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UISupportedInterfaceOrientations~ipad UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIViewControllerbasedStatusBarAppearance UIBackgroundModes audio NSAppTransportSecurity NSExceptionMinimumTLSVersion TLSv1.0 NSAllowsArbitraryLoads NSAllowsArbitraryLoadsInWebContent NSAllowsArbitraryLoadsForMedia NSExceptionRequiresForwardSecrecy NSIncludesSubdomains NSTemporaryExceptionAllowsInsecureHTTPLoads
主要是http协议 至于ios的我也不懂 反正我配置了不得所以在网上瞎找了一下并且将网易云http改换成https才能播放,如果有哪位大哥懂的可以交流一下
两种播放器我都在本项目中使用过,一开始选择用audioplayers,操作也简便,但是考虑到搭配audio_service实现后台控制音乐,我琢磨了一段时间audioplayers发现我太会使用o(╥﹏╥)o,所以选择了与audio_service匹配的just_audio,通过audio_service的官方实例差不多可以使用,官方给的教程实例
根据官方文档的介绍需要安装的依赖有 just_audio、audio_service、get_it
创建目录 services、notifiers文件 page_manager.dart
先从services中先说service_locator.dart
// 初始化音频背景处理 import 'package:get_it/get_it.dart'; import '../page_manager.dart'; import 'audio_handler.dart'; import 'playlist_repository.dart'; GetIt getIt = GetIt.instance; FuturesetupServiceLocator() async { getIt.registerSingleton(await initAudioService()); getIt.registerLazySingleton (() => DemoPlaylist()); getIt.registerLazySingleton (() => PageManager()); }
这里使用到了git_it包,主要是可以全局去控制,具体我也不懂,我也是看官方是咧
AudioPlayerHandler 第一个是创建audio_service
PlaylistRepository 第二是创建初始化的播放列表 主要是给后续加载缓存中的上次的播放列表
PageManager 第三个是主要用于控制audio_service 和 just_audio播放音频
// 播放列表
import 'package:netease_app/model/songs.dart';
abstract class PlaylistRepository {
Future>> fetchInitialPlaylist();
Future fetchAnotherSong(Song song);
}
class DemoPlaylist extends PlaylistRepository {
@override
Future>> fetchInitialPlaylist(
{int length = 3}) async {
return [];
}
@override
Future fetchAnotherSong(Song song) async {
return song;
}
}
这个就是 playlist_repository.dart
fetchInitialPlaylist 是用来初始化播放列表,这里暂时没有用到
fetchAnotherSong 是用来返回一个song音频文件
这里创建了一个song来保存每个音频文件
// 构造class 歌曲
class Song {
int id; // 歌曲id
String name; // 歌曲名字
String artists; // 艺术家
String picUrl; // 歌曲图片
Duration timer;
Song(this.id, this.timer,
{this.name = '', this.artists = '', this.picUrl = ''});
Song.fromJson(Map json)
: id = int.parse(json['id']),
name = json['name'],
artists = json['artists'],
timer = Duration(milliseconds: int.parse(json["timer"])),
picUrl = json['picUrl'];
Map toJson() => {
'id': id,
'name': name,
'artists': artists,
'picUrl': picUrl,
'timer': timer,
};
@override
String toString() {
return '{"id": "$id", "name": "$name", "artists": "$artists","picUrl": "$picUrl","timer": "${timer.inMilliseconds}"}';
}
}
audio_handler.dart
首先说一下我理解到的逻辑 MediaItem 是指当前正在播放的歌曲 / queue 是指播放列表
所以当更新时需要修改mediaitem的参数 播放列表改变时需要改变queue列表
再说一下后台播放几个按钮对应触发的函数 播放 play() pause() seek() skipTonext() skipToPrevious() stop() 这几个最好不要在里面做其他的处理
由于官方的just_audio自带的下一首逻辑没有带有网易云的心动模式,所以没有用自带的
不用这个的还有一个原因就是这是我需要获取音频url,没办法直接setAudioSource,如果你们有更好的办法,也可以教教我
然后自定义了几个我需要用到的方法
abstract class AudioPlayerHandler implements AudioHandler {
// 添加公共方法
Future changeQueueLists(List list, {int index = 0});
// 改变播放列表
Future readySongUrl();
// 获取歌曲url
Future playIndex(int index);
// 从下标播放
Future addFmItems(List mediaItems, bool isAddcurIndex);
// 私人fm
}
这里初始化audio_service
Future initAudioService() async {
return await AudioService.init(
builder: () => MyAudioHandler(),
config: AudioServiceConfig(
androidNotificationChannelId: 'com.mycompany.myapp.audio',
androidNotificationChannelName: '网易云音乐',
androidNotificationOngoing: true,
androidStopForegroundOnPause: true,
),
);
}
然后创建自定义的audio_handler
class MyAudioHandler extends baseAudioHandler
with SeekHandler
implements AudioPlayerHandler {
final _player = AudioPlayer(); // 播放器
final _playlist = ConcatenatingAudioSource(children: []); // 播放列表
final _songlist = []; // 这个是播放列表
int _curIndex = 0; // 播放列表索引
MyAudioHandler() {
// 初始化
// _loadEmptyPlaylist(); // 加载播放列表
_notifyAudioHandleraboutPlaybackEvents(); // 背景状态更改
_listenForDurationChanges(); // 当时间更改时更新背景
_listenPlayEnd();
// _listenForCurrentSongIndexChanges(); // 这个也是改背景
}
UriAudioSource _createAudioSource(MediaItem mediaItem) {
return AudioSource.uri(
Uri.parse(mediaItem.id),
tag: mediaItem,
);
}
Future _loadEmptyPlaylist() async {
final session = await AudioSession.instance;
await session.configure(AudioSessionConfiguration.speech());
try {
await _player.setAudioSource(_playlist);
} catch (e) {
print("错误:$e");
}
}
void _listenPlayEnd() {
_player.playerStateStream.listen((state) {
if (state.playing) {
} else {}
switch (state.processingState) {
case ProcessingState.idle:
break;
case ProcessingState.loading:
break;
case ProcessingState.buffering:
break;
case ProcessingState.ready:
break;
case ProcessingState.completed:
skipTonext();
break;
}
});
}
void _listenForDurationChanges() {
// 当时间发送变化时(也就是意味着歌曲发送变化) 这个是更新时间
_player.durationStream.listen((duration) {
// final index = _player.currentIndex; // 当前播放下标
final newQueue = queue.value;
if (_curIndex == null || newQueue.isEmpty) return;
final oldMediaItem = newQueue[_curIndex];
final newMediaItem = oldMediaItem.copyWith(duration: duration);
newQueue[_curIndex] = newMediaItem;
queue.add(newQueue);
mediaItem.add(newMediaItem);
});
}
void _notifyAudioHandleraboutPlaybackEvents() {
_player.playbackEventStream.listen((PlaybackEvent event) {
final playing = _player.playing;
playbackState.add(playbackState.value.copyWith(
controls: [
MediaControl.skipToPrevious,
if (playing) MediaControl.pause else MediaControl.play,
MediaControl.stop,
MediaControl.skipToNext,
],
systemActions: const {
MediaAction.seek,
},
androidCompactActionIndices: const [0, 1, 3],
processingState: const {
ProcessingState.idle: AudioProcessingState.idle,
ProcessingState.loading: AudioProcessingState.loading,
ProcessingState.buffering: AudioProcessingState.buffering,
ProcessingState.ready: AudioProcessingState.ready,
ProcessingState.completed: AudioProcessingState.completed,
}[_player.processingState]!,
shuffleMode: (_player.shuffleModeEnabled)
? AudioServiceShuffleMode.all
: AudioServiceShuffleMode.none,
playing: playing,
updatePosition: _player.position,
bufferedPosition: _player.bufferedPosition,
speed: _player.speed,
queueIndex: _curIndex,
));
});
}
// void _listenForCurrentSongIndexChanges() {
// _player.currentIndexStream.listen((index) {
// final playlist = queue.value;
// if (index == null || playlist.isEmpty) return;
// mediaItem.add(playlist[index]);
// });
// }
@override
Future addQueueItems(List mediaItems) async {
final audioSource = mediaItems.map(_futterSongItem);
if (_songlist.length > 0) {
// 判断当前歌曲的位置是否是处于最后一位
_songlist.insertAll(_curIndex + 1, audioSource.toList());
final newQueue = queue.value..insertAll(_curIndex + 1, mediaItems);
// _curIndex++;
queue.add(newQueue);
} else {
_songlist.insertAll(_curIndex, audioSource.toList());
final newQueue = queue.value..insertAll(_curIndex, mediaItems);
queue.add(newQueue);
}
}
// 私人Fm的添加
@override
Future addFmItems(List mediaItems, bool isadd) async {
final audioSource = mediaItems.map(_futterSongItem);
if (_songlist.length > 0) {
// 判断当前歌曲的位置是否是处于最后一位
_songlist.insertAll(_curIndex + 1, audioSource.toList());
final newQueue = queue.value..insertAll(_curIndex + 1, mediaItems);
if (isadd) {
_curIndex++;
}
queue.add(newQueue);
} else {
_songlist.insertAll(_curIndex, audioSource.toList());
final newQueue = queue.value..insertAll(_curIndex, mediaItems);
queue.add(newQueue);
}
}
Song _futterSongItem(MediaItem mediaItem) {
return Song(
int.parse(mediaItem.id),
mediaItem.duration!,
artists: mediaItem.artist ?? '',
picUrl: mediaItem.extras?["picUrl"] ?? '',
name: mediaItem.title,
);
}
@override
Future changeQueueLists(List mediaitems,
{int index = 0}) async {
// 这里是替换播放列表
final audioSource = mediaitems.map(_futterSongItem);
// _playlist.clear();
// _playlist.addAll(audioSource.toList()); // 添加到播放列表
_songlist.clear();
_songlist.addAll(audioSource.toList());
_curIndex = index; // 更换了播放列表,将索引归0
// notify system
queue.value.clear();
final newQueue = queue.value..addAll(mediaitems);
queue.add(newQueue); // 添加到背景播放列表
}
@override
Future playIndex(int index) async {
// 接收到下标
_curIndex = index;
readySongUrl();
}
@override
Future removeQueueItemAt(int index) async {
// manage Just Audio
_playlist.removeAt(index);
// notify system
final newQueue = queue.value..removeAt(index);
queue.add(newQueue);
}
@override
Future addQueueItem(MediaItem mediaItem) async {
// manage Just Audio
if (_songlist.length > 0) {
// 判断当前歌曲的位置是否是处于最后一位
_songlist.insert(_curIndex + 1, _futterSongItem(mediaItem));
final newQueue = queue.value..insert(_curIndex + 1, mediaItem);
_curIndex++;
queue.add(newQueue);
} else {
_songlist.insert(_curIndex, _futterSongItem(mediaItem));
final newQueue = queue.value..insert(_curIndex, mediaItem);
queue.add(newQueue);
}
// notify system
}
@override
Future readySongUrl() async {
// 这里是获取歌曲url
var song = this._songlist[_curIndex];
String url = await getSongUrl({"id": '${song.id}'});
if (url.isNotEmpty) {
// 加载音乐
url = url.replaceFirst('http', 'https');
try {
await _player.setAudioSource(AudioSource.uri(
Uri.parse(url),
tag: MediaItem(
id: '${song.id}',
title: song.name,
artist: song.artists,
duration: song.timer,
artUri: Uri.parse(song.picUrl),
),
));
} catch (e) {
print('error======$e');
}
// 这里需要重新更新一次
final playlist = queue.value;
if (_curIndex == null || playlist.isEmpty) return;
mediaItem.add(playlist[_curIndex]);
play(); // 播放
}
}
@override
Future play() async {
_player.play();
}
@override
Future pause() => _player.pause();
@override
Future seek(Duration position) => _player.seek(position);
@override
Future skipTonext() async {
// 当触发播放下一首
if (_curIndex >= _songlist.length - 1) {
_curIndex = 0;
} else {
_curIndex++;
}
// 然后触发获取url
readySongUrl();
final model = getIt();
print('触发播放下一首');
if (model.isPersonFm.value) {
// 如果是私人fm
print('私人fm========$_curIndex');
if (_curIndex == _songlist.length - 1) {
// 判断如果是最后一首
print('触发');
model.getPersonFmList();
}
}
}
@override
Future skipToPrevious() async {
if (_curIndex <= 0) {
_curIndex = _songlist.length - 1;
} else {
_curIndex--;
}
readySongUrl();
}
@override
Future stop() async {
await _player.stop();
return super.stop();
}
}
_player 舒适化播放器 _playlist 这个已经废弃没有用到 _songlist 播放列表 _curIndex 播放索引
MyAudioHandler() {
// 初始化
// _loadEmptyPlaylist(); // 加载播放列表
_notifyAudioHandleraboutPlaybackEvents(); // 背景状态更改
_listenForDurationChanges(); // 当时间更改时更新背景
_listenPlayEnd();
// _listenForCurrentSongIndexChanges(); // 这个也是改背景
}
我就简单说说上面的代码有少许注释
readySongUrl 是我自定义的在play之前触发的加载音频url
@override FuturereadySongUrl() async { // 这里是获取歌曲url var song = this._songlist[_curIndex]; String url = await getSongUrl({"id": '${song.id}'}); if (url.isNotEmpty) { // 加载音乐 url = url.replaceFirst('http', 'https'); try { await _player.setAudioSource(AudioSource.uri( Uri.parse(url), tag: MediaItem( id: '${song.id}', title: song.name, artist: song.artists, duration: song.timer, artUri: Uri.parse(song.picUrl), ), )); } catch (e) { print('error======$e'); } // 这里需要重新更新一次 final playlist = queue.value; if (_curIndex == null || playlist.isEmpty) return; mediaItem.add(playlist[_curIndex]); play(); // 播放 } }
如何在页面上控制播放
// 全局处理
import 'dart:async';
import 'package:audio_service/audio_service.dart';
import 'package:flutter/foundation.dart';
import 'package:netease_app/http/request.dart';
import 'package:netease_app/model/songs.dart';
import 'package:netease_app/notifiers/play_button_notifier.dart';
import 'package:netease_app/notifiers/play_item_notifier.dart';
import 'package:netease_app/notifiers/progress_notifier.dart';
import 'package:netease_app/services/audio_handler.dart';
import 'package:netease_app/services/playlist_repository.dart';
import 'package:netease_app/services/service_locator.dart';
class PageManager {
final currentSongTitleNotifier = PlayItemNotifier();
final playlistNotifier = ValueNotifier([]); // 播放列表
final progressNotifier = ProgressNotifier(); // 时间进度条
// final repeatButtonNotifier = RepeatButtonNotifier();
final isFirstSongNotifier = ValueNotifier(true); // 是不是第一首歌
final playButtonNotifier = PlayButtonNotifier();
final isLastSongNotifier = ValueNotifier(true); // 是不是最后一首歌
final isShuffleModeEnabledNotifier = ValueNotifier(false);
final _audioHandler = getIt();
final isPersonFm = ValueNotifier(false); // 当前是否是私人fm
final waitPersonFm = ValueNotifier(false);
// 初始化
void init() async {
await _loadPlaylist();
_listenToChangesInPlaylist();
_listenToPlaybackState();
_listenToCurrentPosition();
_listenToBufferedPosition();
_listenToTotalDuration();
_listenToChangesInSong();
}
Future _loadPlaylist() async {
final songRepository = getIt();
final playlist = await songRepository.fetchInitialPlaylist();
final mediaItems = playlist
.map((song) => MediaItem(
id: song['id'] ?? '',
album: song['album'] ?? '',
title: song['title'] ?? '',
extras: {'url': song['url']},
))
.toList();
_audioHandler.addQueueItems(mediaItems);
}
void _listenToChangesInPlaylist() {
_audioHandler.queue.listen((playlist) {
if (playlist.isEmpty) {
playlistNotifier.value = [];
currentSongTitleNotifier.value = MediaItem(id: '', title: '');
} else {
final newList = playlist;
playlistNotifier.value = newList;
}
_updateSkipButtons();
});
}
void _listenToPlaybackState() {
// 监听播放状态
_audioHandler.playbackState.listen((playbackState) {
final isPlaying = playbackState.playing;
final processingState = playbackState.processingState;
print(processingState);
if (processingState == AudioProcessingState.loading ||
processingState == AudioProcessingState.buffering) {
// 加载中
playButtonNotifier.value = ButtonState.loading;
} else if (!isPlaying) {
// 没有播放
playButtonNotifier.value = ButtonState.paused;
} else if (processingState != AudioProcessingState.completed) {
// 播放中
playButtonNotifier.value = ButtonState.playing;
} else if (isPlaying) {
// playButtonNotifier.value = ButtonState.playing;
} else {
// 重置
// print('重置触发');
// _audioHandler.seek(Duration.zero);
// _audioHandler.pause();
}
});
}
// 更新播放时间
void _listenToCurrentPosition() {
AudioService.position.listen((position) {
// print('播放时间$position');
final oldState = progressNotifier.value;
progressNotifier.value = ProgressBarState(
current: position,
buffered: oldState.buffered,
total: oldState.total,
);
});
}
// 更新缓冲位置
void _listenToBufferedPosition() {
_audioHandler.playbackState.listen((playbackState) {
final oldState = progressNotifier.value;
progressNotifier.value = ProgressBarState(
current: oldState.current,
buffered: playbackState.bufferedPosition,
total: oldState.total,
);
});
}
// 更新总时长
void _listenToTotalDuration() {
_audioHandler.mediaItem.listen((mediaItem) {
final oldState = progressNotifier.value;
progressNotifier.value = ProgressBarState(
current: oldState.current,
buffered: oldState.buffered,
total: mediaItem?.duration ?? Duration.zero,
);
});
}
// 更新歌曲显示最新的歌曲
void _listenToChangesInSong() {
_audioHandler.mediaItem.listen((mediaItem) {
currentSongTitleNotifier.value = mediaItem!;
print('Item$mediaItem');
_updateSkipButtons();
});
}
void _updateSkipButtons() {
final mediaItem = _audioHandler.mediaItem.value;
final playlist = _audioHandler.queue.value;
if (playlist.length < 2 || mediaItem == null) {
isFirstSongNotifier.value = true;
isLastSongNotifier.value = true;
} else {
isFirstSongNotifier.value = playlist.first == mediaItem;
isLastSongNotifier.value = playlist.last == mediaItem;
}
// if (isPersonFm.value) {
// // 如果这个是私人Fm的话 当播放的这首歌是最后一首歌的时候,遇到获取新的歌曲添加到歌单
// print(
// 'object============${isLastSongNotifier.value}===========${waitPersonFm.value}');
// if (isLastSongNotifier.value && !waitPersonFm.value) {
// waitPersonFm.value = true;
// print('触发===============');
// this.getPersonFmList();
// }
// }
}
// 改变是否是私人Fm
void changeIsPersonFm(bool personFm) {
isPersonFm.value = personFm;
}
// 获取私人Fm
Future getPersonFmList() async {
Map params = {
"timestamp": '${DateTime.now().microsecondsSinceEpoch}',
};
getPersonaFm(params).then((value) {
if (value["code"] == 200) {
List playsongslist = [];
value["data"].forEach((element) {
playsongslist.add(Song(
element["id"],
Duration(milliseconds: element["duration"]),
name: element["name"],
picUrl: element["album"]["picUrl"],
artists: createArtistString(element["artists"]),
));
});
addSongs(playsongslist);
}
});
}
createArtistString(List list) {
List artistString = [];
list.forEach((element) {
artistString.add(element["name"]);
});
return artistString.join('/');
}
Future play() async {
await _audioHandler.readySongUrl();
}
Future resumePlay() async {
await _audioHandler.play();
}
sinkProgress(int timer) async {
final oldState = progressNotifier.value;
progressNotifier.value = ProgressBarState(
current: Duration(milliseconds: timer),
buffered: oldState.buffered,
total: oldState.total,
);
}
void pasue() {
_audioHandler.pause();
}
void togglePlay() {
if (playButtonNotifier.value == ButtonState.paused) {
// 执行播放
resumePlay();
} else {
// 执行暂停
pasue();
}
}
void playInex(int index) async {
await _audioHandler.playIndex(index);
}
void seek(Duration position) => _audioHandler.seek(position); // 时间跳转
void previous() => _audioHandler.skipToPrevious(); // 上一首
void next() => _audioHandler.skipTonext(); // 下一首
void repeat() {}
void shuffle() {}
// 添加一首歌
Future add(Song song) async {
final songRepository = getIt();
final songitem = await songRepository.fetchAnotherSong(song);
final mediaItem = MediaItem(
id: songitem.id.toString(),
album: '',
artist: songitem.artists,
duration: songitem.timer,
title: songitem.name,
artUri: Uri.parse(songitem.picUrl),
extras: {
'picUrl': songitem.picUrl,
},
);
_audioHandler.addQueueItem(mediaItem);
}
// 添加一个列表到播放列表
Future addSongs(List songlist, {int index = 0}) async {
final List mediaItems = [];
songlist.forEach((element) {
mediaItems.add(MediaItem(
id: element.id.toString(),
album: '',
artist: element.artists,
duration: element.timer,
title: element.name,
artUri: Uri.parse(element.picUrl),
extras: {
'picUrl': element.picUrl,
},
));
});
await _audioHandler.addQueueItems(mediaItems);
}
Future changesonglistplay(List list, {int index = 0}) async {
final List mediaItems = [];
list.forEach((element) {
mediaItems.add(MediaItem(
id: element.id.toString(),
album: '',
artist: element.artists,
duration: element.timer,
title: element.name,
artUri: Uri.parse(element.picUrl),
extras: {
'picUrl': element.picUrl,
},
));
});
await _audioHandler.changeQueueLists(mediaItems, index: index);
}
void remove() {
final lastIndex = _audioHandler.queue.value.length - 1;
if (lastIndex < 0) return;
_audioHandler.removeQueueItemAt(lastIndex);
}
Future changelist(list) async {}
void dispose() {
_audioHandler.customAction('dispose');
}
void stop() {
_audioHandler.stop();
}
}



