简介
本篇文章是关于 Flutter 平台中音频播放单例控制器的实现,用于在 App 中全局控制音频播放,存储音频播放信息、目录信息。 在 App 的任何位置都可以直接通过该控制器获取当前播放的音频信息、播放目录信息。 备注:本文中介绍的控制器是本人根据自身需求量身定制的,不可能符合每个人的期待,建议有相关需求的朋友自己编写符合自身需求的控制器,本文只提供一个思路。
依赖
该控制器是对 Flutter 平台中的 just_audio 库的封装。
- 版本库:just_audio: ^0.10.5
- 参考链接:pub.dev/packages/ju...
控制器音频存储类 AudioInformation
AudioInformation 用于存储每个需要播放的音频信息,以及与 json 对象的转换方法。
dart
class AudioInformation {
final String image;
final String name;
final String artist;
final String url;
final AudioSource audioSource;
AudioInformation({
required this.image,
required this.name,
required this.artist,
required this.url,
}) : audioSource = AudioSource.uri(Uri.parse(url));
AudioInformation.fromJson(Map<String, dynamic> json)
: image = json['image'] ?? 'assets/images/default.jpg',
name = json['name'] ?? '',
artist = json['artist'] ?? '',
url = json['url'] ?? '',
audioSource = AudioSource.uri(Uri.parse(json['url']));
Map<String, dynamic> toJson() {
return {'image': image, 'name': name, 'artist': artist, 'url': url};
}
dynamic toJsonDynamic() {
return toJson();
}
}
控制器 AudioPlayerUtil 的单例实现、变量介绍、初始化
- 单例实现
- AudioPlayerUtil._() 函数是 AudioPlayerUtil 的私有构造函数。
- _instance 是 AudioPlayerUtil 的单例对象。
- static AudioPlayerUtil of() 静态函数用于获取 AudioPlayerUtil 的单例。
- 本地存储
- AUDIO_STRORAGE_KEY 用于本地存储播放列表的 key。
- AUDIO_INDEX_KEY 用于本地存储播放索引 key。
- Future<void> init() 函数用于恢复 App 上次播放信息、播放列表以及初始化播放索引流。
- _player 是 just_audio 的 AudioPlayer 对象。
- 播放位置更新
- _positionTimer 是位置更新定时器,在构造函数中进行初始化
- _positionController 是位置更新通知流控制器
- 定时关闭
- _countdown 用于记录定时关闭播放器的倒计时秒数。
- _countdownTimer 是倒计时定时器。
- _countdownController 是倒计时通知流控制器。
- _audioInformationList 是播放目录列表。
- 初始化
- AudioPlayerUtil.of().init();
- _streamSubscriptions 用来记录流订阅信息
dart
class AudioPlayerUtil {
AudioPlayerUtil._() {
_positionTimer = Timer.periodic(const Duration(milliseconds: 40), (timer) {
if (_player.playing) {
_positionController.add(_player.position);
}
});
}
static final AudioPlayerUtil _instance = AudioPlayerUtil._();
static AudioPlayerUtil of() {
return _instance;
}
/// 音频列表存储 key
static final String AUDIO_STRORAGE_KEY = 'AUDIO_UTIL:AUDIO_LIST';
/// 播放索引存储 key
static final String AUDIO_INDEX_KEY = 'AUDIO_UTIL:AUDIO_INDEX';
final List<StreamSubscription> _streamSubscriptions = [];
/// 初始化
Future<void> init() async {
String audioListJson = LocalStorage.of().getString(AUDIO_STRORAGE_KEY);
if (audioListJson.isNotEmpty) {
List<dynamic> jsonList = jsonDecode(audioListJson);
await setAudioSource(jsonList);
}
int index = LocalStorage.of().getInt(AUDIO_INDEX_KEY);
await seek(Duration.zero, index: index);
// 监听播放索引变化
_streamSubscriptions.add(
currentIndexStream.listen((index) {
LocalStorage.of().setInt(AUDIO_INDEX_KEY, index ?? 0);
}),
);
return Future.value(null);
}
// 播放器
final AudioPlayer _player = AudioPlayer(handleInterruptions: false);
// 位置更新定时器
late final Timer _positionTimer;
// 位置更新通知流控制器
final StreamController<Duration> _positionController =
StreamController<Duration>.broadcast();
// 定时关闭倒计时
int _countdown = -1;
// 定时关闭倒计时定时器
Timer? _countdownTimer;
// 定时关闭倒计时通知流控制器
final StreamController<int> _countdownController =
StreamController<int>.broadcast();
// 播放列表
final List<AudioInformation> _audioInformationList = [];
// ......
}
方法介绍
- 设置播放源,该方法会清空现有播放目录。
- Future<Duration?> setAudioSource(List<dynamic> jsonList, {int initialIndex = 0,})
- jsonList 播放源列表
- initialIndex 初始化索引
- Future<Duration?> setAudioSource(List<dynamic> jsonList, {int initialIndex = 0,})
- 添加播放源,该方法会将新的播放源添加在现有的播放源末尾。
- Future<void> addAudioSource(List<dynamic> jsonList)
- jsonList 播放源列表
- 添加音频至下一个播放
- void addNext(AudioInformation audio)
- audio 音频信息
- 移除指定索引的播放信息
- void removeAt(int index)
- 播放下一首:Future<void> next()
- 播放上一首:Future<void> previous()
- 指定播放音频位置和播放源:Future<void> seek(Duration position, {int? index})
- 播放 or 播放指定音频:Future<void> play({int? index})
- 暂停:Future<void> pause()
- 停止:Future<void> stop(),该方法会释放解码器和播放音频所需的其他本地平台资源
- 设置循环模式:Future<void> setLoopMode(LoopMode loopMode)
- 设置随机播放:Future<void> setShuffleModeEnabled(bool randomModeEnabled)
- 获取随机播放启用状态:bool get shuffleModeEnabled
- 获取循环模式:LoopMode get loopMode
- 获取播放列表:List<AudioInformation> get audioInformationList
- 获取当前播放进度:Duration get position
- 获取当前播放时长:Duration? get duration
- 获取当前播放状态:bool get playing
- 获取当前播放音频索引:int? get currentIndex
- 获取当前播放音频:AudioInformation? get currentAudio
- 获取播放器状态流:Stream<PlayerState> get playerStateStream
- 获取当前索引流:Stream<int?> get currentIndexStream
- 获取播放进度流:Stream<Duration> get positionStream
- 获取播放时长流:Stream<Duration?> get durationStream
- 设置定时关闭:void setCountdown(int countdownSeconds)
- 取消定时关闭:void cancelCountdown()
- 定时关闭倒计时通知流:Stream<int> get countdownStream
- 释放播放器资源: Future<void> dispose()
dart
class AudioPlayerUtil {
// ......
/// 设置播放源
Future<Duration?> setAudioSource(
List<dynamic> jsonList, {
int initialIndex = 0,
}) async {
if (jsonList.isEmpty) {
return Future.value(null);
}
// 转换为 AudioInformation 列表
List<AudioInformation> audioList = jsonList
.map((json) => AudioInformation.fromJson(json))
.toList();
_audioInformationList.clear();
// 停止播放
pause();
stop();
_player.clearAudioSources();
_audioInformationList.addAll(audioList);
// 本地存储
LocalStorage.of().setString(
AUDIO_STRORAGE_KEY,
jsonEncode(_audioInformationList.map((it) => it.toJson()).toList()),
);
return await _player.setAudioSources(
audioList.map((it) => it.audioSource).toList(),
initialIndex: initialIndex,
initialPosition: Duration.zero,
shuffleOrder: DefaultShuffleOrder(), // Customise the shuffle algorithm
);
}
/// 添加播放源
Future<void> addAudioSource(List<dynamic> jsonList) async {
if (jsonList.isEmpty) {
return Future.value(null);
}
// 添加音频
List<AudioInformation> audioList = jsonList
.map((json) => AudioInformation.fromJson(json))
.toList();
_audioInformationList.addAll(audioList);
// 本地存储
LocalStorage.of().setString(
AUDIO_STRORAGE_KEY,
jsonEncode(_audioInformationList.map((it) => it.toJson()).toList()),
);
return await _player.addAudioSources(
audioList.map((it) => it.audioSource).toList(),
);
}
/// 添加到下一个播放
void addNext(AudioInformation audio) {
int nextIndex = (_player.currentIndex ?? -1) + 1;
_audioInformationList.insert(nextIndex, audio);
_player.insertAudioSource(
nextIndex,
_audioInformationList[nextIndex].audioSource,
);
// 本地存储
LocalStorage.of().setString(
AUDIO_STRORAGE_KEY,
jsonEncode(_audioInformationList.map((it) => it.toJson()).toList()),
);
}
/// 移除指定索引的播放信息
void removeAt(int index) {
if (index < 0 ||
index >= _audioInformationList.length ||
index == _player.currentIndex) {
return;
}
_audioInformationList.removeAt(index);
_player.removeAudioSourceAt(index);
}
/// 播放下一首
Future<void> next() async {
if (_audioInformationList.isEmpty) {
return Future.value();
}
if (_player.hasNext) {
return await _player.seekToNext();
} else {
return await _player.seek(Duration.zero, index: 0);
}
}
/// 播放上一首
Future<void> previous() async {
if (_audioInformationList.isEmpty) {
return Future.value();
}
if (_player.hasPrevious) {
return await _player.seekToPrevious();
} else {
return await _player.seek(
Duration.zero,
index: _audioInformationList.length - 1,
);
}
}
/// 指定播放音频位置和播放源
Future<void> seek(Duration position, {int? index}) async {
return await _player.seek(position, index: index);
}
/// 播放 or 播放指定音频
Future<void> play({int? index}) async {
if (null == index) {
return await _player.play();
}
if (index < 0 || index >= _audioInformationList.length) {
return;
}
await seek(Duration.zero, index: index);
return await _player.play();
}
/// 暂停
Future<void> pause() async {
return await _player.pause();
}
/// 停止
Future<void> stop() async {
return await _player.stop();
}
/// 设置循环模式
Future<void> setLoopMode(LoopMode loopMode) async {
return await _player.setLoopMode(loopMode);
}
/// 设置随机播放
Future<void> setShuffleModeEnabled(bool randomModeEnabled) async {
return await _player.setShuffleModeEnabled(randomModeEnabled);
}
/// 获取随机播放
bool get shuffleModeEnabled => _player.shuffleModeEnabled;
/// 获取循环模式
LoopMode get loopMode => _player.loopMode;
/// 获取播放列表
List<AudioInformation> get audioInformationList => _audioInformationList;
/// 获取当前播放进度
Duration get position => _player.position;
/// 获取当前播放时长
Duration? get duration => _player.duration;
/// 获取当前播放状态
bool get playing => _player.playing;
/// 获取当前播放音频索引
int? get currentIndex => _player.currentIndex;
/// 获取当前播放音频
AudioInformation? get currentAudio {
if (null == _player.currentIndex) {
return null;
}
return _audioInformationList[_player.currentIndex!];
}
/// 获取播放器状态流
Stream<PlayerState> get playerStateStream => _player.playerStateStream;
/// 获取当前索引流
Stream<int?> get currentIndexStream => _player.currentIndexStream;
/// 获取播放进度流
Stream<Duration> get positionStream {
// return _player.positionStream;
return _positionController.stream;
}
/// 获取播放时长流
Stream<Duration?> get durationStream => _player.durationStream;
/// 设置定时关闭
void setCountdown(int countdownSeconds) {
_countdown = countdownSeconds;
_countdownTimer?.cancel();
_countdownController.add(_countdown);
_countdownTimer = Timer.periodic(Duration(seconds: 1), (timer) {
_countdown = _countdown - 1;
_countdownController.add(_countdown);
if (_countdown < 0) {
// 暂停播放
pause();
stop();
timer.cancel();
}
});
}
/// 取消定时关闭
void cancelCountdown() {
_countdown = -1;
_countdownController.add(_countdown);
_countdownTimer?.cancel();
}
/// 倒计时流
Stream<int> get countdownStream => _countdownController.stream;
/// 释放播放器资源
Future<void> dispose() async {
while (_streamSubscriptions.isNotEmpty) {
_streamSubscriptions.removeLast().cancel();
}
// 停止位置更新
_positionTimer.cancel();
_positionController.close();
// 停止倒计时
_countdownTimer?.cancel();
_countdownController.close();
// 先停止并清除所有音频源
await _player.pause();
await _player.stop();
await _player.clearAudioSources();
// 释放播放器资源
await _player.dispose();
// 清理播放列表
_audioInformationList.clear();
}
}
控制器中使用的本地存储工具类 LocalStorage
LocalStorage 是对 Flutter 平台中的 shared_preferences: ^2.5.3 库的封装。 使用前要进行初始化,LocalStorage.of().init();
dart
class LocalStorage {
LocalStorage._();
static final LocalStorage _instance = LocalStorage._();
SharedPreferences? _prefs;
static LocalStorage of() {
return _instance;
}
// 初始化
static Future<LocalStorage> init() async {
_instance._prefs = await SharedPreferences.getInstance();
return Future.value(_instance);
}
/// 数据存储
Future<bool?> setInt(String key, int value) async {
return await _prefs!.setInt(key, value);
}
Future<bool?> setBool(String key, bool value) async {
return await _prefs!.setBool(key, value);
}
Future<bool?> setDouble(String key, double value) async {
return await _prefs!.setDouble(key, value);
}
Future<bool?> setString(String key, String value) async {
return await _prefs!.setString(key, value);
}
Future<bool?> setStringList(String key, List<String> value) async {
return await _prefs!.setStringList(key, value);
}
/// 数据获取
int getInt(String key) {
return _prefs!.getInt(key) ?? 0;
}
double getDouble(String key) {
return _prefs!.getDouble(key) ?? 0;
}
bool getBool(String key) {
return _prefs!.getBool(key) ?? false;
}
String getString(String key) {
return _prefs!.getString(key) ?? '';
}
List<String> getStringList(String key) {
return _prefs!.getStringList(key) ?? [];
}
bool containsKey(String key) {
return _prefs!.containsKey(key);
}
void remove(String key) {
_prefs!.remove(key);
}
void clear() {
_prefs!.clear();
}
}
附上源码
dart
import 'dart:async';
import 'dart:convert';
import 'package:ephemeris_mobile/utils/LocalStorage.dart';
import 'package:just_audio/just_audio.dart';
class AudioInformation {
final String image;
final String name;
final String artist;
final String url;
final AudioSource audioSource;
AudioInformation({
required this.image,
required this.name,
required this.artist,
required this.url,
}) : audioSource = AudioSource.uri(Uri.parse(url));
AudioInformation.fromJson(Map<String, dynamic> json)
: image = json['image'] ?? 'assets/images/default.jpg',
name = json['name'] ?? '',
artist = json['artist'] ?? '',
url = json['url'] ?? '',
audioSource = AudioSource.uri(Uri.parse(json['url']));
Map<String, dynamic> toJson() {
return {'image': image, 'name': name, 'artist': artist, 'url': url};
}
dynamic toJsonDynamic() {
return toJson();
}
}
class AudioPlayerUtil {
AudioPlayerUtil._() {
_positionTimer = Timer.periodic(const Duration(milliseconds: 40), (timer) {
if (_player.playing) {
_positionController.add(_player.position);
}
});
}
static final AudioPlayerUtil _instance = AudioPlayerUtil._();
static AudioPlayerUtil of() {
return _instance;
}
/// 音频列表存储 key
static final String AUDIO_STRORAGE_KEY = 'AUDIO_UTIL:AUDIO_LIST';
/// 播放索引存储 key
static final String AUDIO_INDEX_KEY = 'AUDIO_UTIL:AUDIO_INDEX';
final List<StreamSubscription> _streamSubscriptions = [];
/// 初始化
Future<void> init() async {
String audioListJson = LocalStorage.of().getString(AUDIO_STRORAGE_KEY);
if (audioListJson.isNotEmpty) {
List<dynamic> jsonList = jsonDecode(audioListJson);
await setAudioSource(jsonList);
}
int index = LocalStorage.of().getInt(AUDIO_INDEX_KEY);
await seek(Duration.zero, index: index);
// 监听播放索引变化
_streamSubscriptions.add(
currentIndexStream.listen((index) {
LocalStorage.of().setInt(AUDIO_INDEX_KEY, index ?? 0);
}),
);
return Future.value(null);
}
// 播放器
final AudioPlayer _player = AudioPlayer(handleInterruptions: false);
// 位置更新定时器
late final Timer _positionTimer;
// 位置更新通知流控制器
final StreamController<Duration> _positionController =
StreamController<Duration>.broadcast();
// 定时关闭倒计时
int _countdown = -1;
// 定时关闭倒计时定时器
Timer? _countdownTimer;
// 定时关闭倒计时通知流控制器
final StreamController<int> _countdownController =
StreamController<int>.broadcast();
// 播放列表
final List<AudioInformation> _audioInformationList = [];
/// 设置播放源
Future<Duration?> setAudioSource(
List<dynamic> jsonList, {
int initialIndex = 0,
}) async {
if (jsonList.isEmpty) {
return Future.value(null);
}
// 转换为 AudioInformation 列表
List<AudioInformation> audioList = jsonList
.map((json) => AudioInformation.fromJson(json))
.toList();
_audioInformationList.clear();
// 停止播放
pause();
stop();
_player.clearAudioSources();
_audioInformationList.addAll(audioList);
// 本地存储
LocalStorage.of().setString(
AUDIO_STRORAGE_KEY,
jsonEncode(_audioInformationList.map((it) => it.toJson()).toList()),
);
return await _player.setAudioSources(
audioList.map((it) => it.audioSource).toList(),
initialIndex: initialIndex,
initialPosition: Duration.zero,
shuffleOrder: DefaultShuffleOrder(), // Customise the shuffle algorithm
);
}
/// 添加播放源
Future<void> addAudioSource(List<dynamic> jsonList) async {
if (jsonList.isEmpty) {
return Future.value(null);
}
// 添加音频
List<AudioInformation> audioList = jsonList
.map((json) => AudioInformation.fromJson(json))
.toList();
_audioInformationList.addAll(audioList);
// 本地存储
LocalStorage.of().setString(
AUDIO_STRORAGE_KEY,
jsonEncode(_audioInformationList.map((it) => it.toJson()).toList()),
);
return await _player.addAudioSources(
audioList.map((it) => it.audioSource).toList(),
);
}
/// 添加到下一个播放
void addNext(AudioInformation audio) {
int nextIndex = (_player.currentIndex ?? -1) + 1;
_audioInformationList.insert(nextIndex, audio);
_player.insertAudioSource(
nextIndex,
_audioInformationList[nextIndex].audioSource,
);
// 本地存储
LocalStorage.of().setString(
AUDIO_STRORAGE_KEY,
jsonEncode(_audioInformationList.map((it) => it.toJson()).toList()),
);
}
/// 移除指定索引的播放信息
void removeAt(int index) {
if (index < 0 ||
index >= _audioInformationList.length ||
index == _player.currentIndex) {
return;
}
_audioInformationList.removeAt(index);
_player.removeAudioSourceAt(index);
}
/// 播放下一首
Future<void> next() async {
if (_audioInformationList.isEmpty) {
return Future.value();
}
if (_player.hasNext) {
return await _player.seekToNext();
} else {
return await _player.seek(Duration.zero, index: 0);
}
}
/// 播放上一首
Future<void> previous() async {
if (_audioInformationList.isEmpty) {
return Future.value();
}
if (_player.hasPrevious) {
return await _player.seekToPrevious();
} else {
return await _player.seek(
Duration.zero,
index: _audioInformationList.length - 1,
);
}
}
/// 指定播放音频位置和播放源
Future<void> seek(Duration position, {int? index}) async {
return await _player.seek(position, index: index);
}
/// 播放 or 播放指定音频
Future<void> play({int? index}) async {
if (null == index) {
return await _player.play();
}
if (index < 0 || index >= _audioInformationList.length) {
return;
}
await seek(Duration.zero, index: index);
return await _player.play();
}
/// 暂停
Future<void> pause() async {
return await _player.pause();
}
/// 停止
Future<void> stop() async {
return await _player.stop();
}
/// 设置循环模式
Future<void> setLoopMode(LoopMode loopMode) async {
return await _player.setLoopMode(loopMode);
}
/// 设置随机播放
Future<void> setShuffleModeEnabled(bool randomModeEnabled) async {
return await _player.setShuffleModeEnabled(randomModeEnabled);
}
/// 获取随机播放
bool get shuffleModeEnabled => _player.shuffleModeEnabled;
/// 获取循环模式
LoopMode get loopMode => _player.loopMode;
/// 获取播放列表
List<AudioInformation> get audioInformationList => _audioInformationList;
/// 获取当前播放进度
Duration get position => _player.position;
/// 获取当前播放时长
Duration? get duration => _player.duration;
/// 获取当前播放状态
bool get playing => _player.playing;
/// 获取当前播放音频索引
int? get currentIndex => _player.currentIndex;
/// 获取当前播放音频
AudioInformation? get currentAudio {
if (null == _player.currentIndex) {
return null;
}
return _audioInformationList[_player.currentIndex!];
}
/// 获取播放器状态流
Stream<PlayerState> get playerStateStream => _player.playerStateStream;
/// 获取当前索引流
Stream<int?> get currentIndexStream => _player.currentIndexStream;
/// 获取播放进度流
Stream<Duration> get positionStream {
// return _player.positionStream;
return _positionController.stream;
}
/// 获取播放时长流
Stream<Duration?> get durationStream => _player.durationStream;
/// 设置定时关闭
void setCountdown(int countdownSeconds) {
_countdown = countdownSeconds;
_countdownTimer?.cancel();
_countdownController.add(_countdown);
_countdownTimer = Timer.periodic(Duration(seconds: 1), (timer) {
_countdown = _countdown - 1;
_countdownController.add(_countdown);
if (_countdown < 0) {
// 暂停播放
pause();
stop();
timer.cancel();
}
});
}
/// 取消定时关闭
void cancelCountdown() {
_countdown = -1;
_countdownController.add(_countdown);
_countdownTimer?.cancel();
}
/// 倒计时流
Stream<int> get countdownStream => _countdownController.stream;
/// 释放播放器资源
Future<void> dispose() async {
while (_streamSubscriptions.isNotEmpty) {
_streamSubscriptions.removeLast().cancel();
}
// 停止位置更新
_positionTimer.cancel();
_positionController.close();
// 停止倒计时
_countdownTimer?.cancel();
_countdownController.close();
// 先停止并清除所有音频源
await _player.pause();
await _player.stop();
await _player.clearAudioSources();
// 释放播放器资源
await _player.dispose();
// 清理播放列表
_audioInformationList.clear();
}
}