本文简介
本文是关于 Flutter 平台上 audio_service 库的使用实操,通过该库实现 iPhone、Android 手机下拉窗口、锁屏页面中的音频控制台。 audio_service 库可以实现的功能很多,这里只用来做音频控制台,并未涉及其他功能,且只在 iPhone 设备有测试。 以下是 audio_service 库的官方文档介绍:
This plugin wraps around your existing audio code to allow it to run in the background and interact with the media notification, the lock screen, headset buttons, wearables and Android Auto. It supports Android, iOS, web and Linux (via audio_service_mpris). 这个插件包装你现有的音频代码,允许它在后台运行,并与媒体通知,锁屏,耳机按钮,可穿戴设备和Android自动交互。它支持Android, iOS, web和Linux(通过audio_service_mpris)。
依赖
- 文中的代码会依赖我之前文章中的全局音频播放单例控制器 AudioPlayerUtil ,相关信息请移步Flutter 全局音频播放单例实现(附完整源码)------基于 just_audio 的零依赖方案
- audio_service: ^0.18.18
- just_audio: ^0.10.5
继承 audio_service 中的 BaseAudioHandler 实现 AudioCustomHandler
在 AudioCustomHandler 类中对接系统控制台与 Flutter 软件的音频交互逻辑。 为了释放在 AudioCustomHandler 初始化时占用的资源,该类使用单例模式。
- _instance: AudioCustomHandler 的单例对象
- static AudioCustomHandler of():单例获取函数
dart
class AudioCustomHandler extends BaseAudioHandler {
static AudioCustomHandler? _instance;
static AudioCustomHandler of() {
_instance ??= AudioCustomHandler();
return _instance!;
}
// ......
}
AudioCustomHandler 的初始化函数
初始化控制台的状态、功能、信息
控制台要提前进行初始化,后续控制台状态变更只需要复制当前状态,然后修改对应状态即可。
- controls:设置控制台功能
- 上一首:MediaControl.skipToPrevious
- 播放:MediaControl.play
- 暂停:MediaControl.pause
- 下一首:MediaControl.skipToNext
- systemActions:设置软件支持的系统级操作
- 进度切换:MediaAction.seek
- processingState:音频处理状态
- idle:还没有加载任何资源。
- loading:资源加载中。
- buffering:正在缓存资源。
- ready:资源有足够的缓冲,可用于回放。
- completed:到达资源的终点。
- error:资源加载异常。
- playing:播放状态。
- updatePosition:播放位置。
- bufferedPosition:缓冲位置
监听音频播放状态、信息
这里要用到全局音频播放单例 ,控制台的状态变化务必使用 playbackState.value.copyWith 拷贝当前状态,然后对需要更新的状态进行更新。
- _streamSubscriptions:该参数用于存储订阅信息,以便在软件退出时进行释放。
- 订阅播放状态:AudioPlayerUtil.of().playerStateStream
- 订阅播放信息变化:AudioPlayerUtil.of().currentIndexStream
- 订阅播放进度变化:AudioPlayerUtil.of().positionStream
dart
class AudioCustomHandler extends BaseAudioHandler {
// ......
final List<StreamSubscription> _streamSubscriptions = [];
/// 初始化
AudioCustomHandler init() {
// 初始化控制台的状态、功能、信息
playbackState.add(
PlaybackState(
controls: [
MediaControl.skipToPrevious,
MediaControl.play,
MediaControl.pause,
MediaControl.skipToNext,
],
systemActions: {MediaAction.seek},
processingState: AudioProcessingState.ready,
playing: false,
updatePosition: Duration.zero,
bufferedPosition: Duration.zero,
),
);
// 监听播放器状态变化,同步到通知栏
_streamSubscriptions.add(
AudioPlayerUtil.of().playerStateStream.listen((state) {
AudioProcessingState processingState =
state.processingState == ProcessingState.completed
? AudioProcessingState.completed
: state.processingState == ProcessingState.ready
? AudioProcessingState.ready
: state.processingState == ProcessingState.loading
? AudioProcessingState.loading
: state.processingState == ProcessingState.buffering
? AudioProcessingState.buffering
: AudioProcessingState.idle;
playbackState.add(
playbackState.value.copyWith(
playing: state.playing,
processingState: processingState,
),
);
}),
);
// 监听当前播放歌曲
_streamSubscriptions.add(
AudioPlayerUtil.of().currentIndexStream.listen((index) {
mediaItem.add(
MediaItem(
id: 'media_id',
title: AudioPlayerUtil.of().currentAudio?.name ?? '未知歌曲',
artist: AudioPlayerUtil.of().currentAudio?.artist,
duration: AudioPlayerUtil.of().duration,
artUri: Uri.parse(
AudioPlayerUtil.of().currentAudio?.image ??
'assets/images/logo.png',
),
),
);
}),
);
// 监听播放进度
_streamSubscriptions.add(
AudioPlayerUtil.of().positionStream.listen((position) {
playbackState.add(
playbackState.value.copyWith(
updatePosition: position,
bufferedPosition: position,
),
);
}),
);
return this;
}
// ......
}
方法介绍
这里的方法都是对 **BaseAudioHandler 方法的重写,然后使用全局音频控制器 AudioPlayerUtil **进行控制。
- 音频播放:Future<void> play()
- 音频暂停:Future<void> pause()
- 音频停止:Future<void> stop()
- 指定音频播放位置:Future<void> seek(Duration position)
- 跳转到指定音频:Future<void> skipToQueueItem(int index)
- 下一首:Future<void> skipToNext()
- 上一首:Future<void> skipToPrevious()
dart
class AudioCustomHandler extends BaseAudioHandler {
// ......
@override
Future<void> play() => AudioPlayerUtil.of().play();
@override
Future<void> pause() => AudioPlayerUtil.of().pause();
@override
Future<void> stop() => AudioPlayerUtil.of().stop();
@override
Future<void> seek(Duration position) => AudioPlayerUtil.of().seek(position);
@override
Future<void> skipToQueueItem(int index) =>
AudioPlayerUtil.of().seek(Duration.zero, index: index);
@override
Future<void> skipToNext() => AudioPlayerUtil.of().next();
@override
Future<void> skipToPrevious() => AudioPlayerUtil.of().previous();
}
audio_service 库的初始化
初始化位置一般在启动页,在用户同意协议之后。
- builder:这个参数需要的是 AudioHandler 对象,AudioCustomHandler.of().init() 函数会返回 AudioCustomHandler 对象,AudioCustomHandler 继承自 BaseAudioHandler, BaseAudioHandler 又继承自 AudioHandler
- config:这个参数我并未深究,其用途读者可以自行查阅 audio_service 的文档
dart
// ....
AudioService.init(
builder: () => AudioCustomHandler.of().init(),
config: AudioServiceConfig(
androidNotificationChannelId: 'com.xxx.xxxxxx.audio',
androidNotificationChannelName: 'xxxxxx',
),
);
// ...
资源销毁
单例模式中的资源不释放/销毁问题也不大,因为单例的释放/销毁一般都伴随着整个进程的结束。但为了养成一个良好的编程习惯,还是要销毁。 销毁时机可以放在主页面的销毁函数中,一般主页面销毁意味着整个 App 的退出,进程的结束。
dart
class AudioCustomHandler extends BaseAudioHandler {
// ......
Future<void> dispose() async {
while (_streamSubscriptions.isNotEmpty) {
await _streamSubscriptions.removeLast().cancel();
}
return Future.value();
}
}
附上源码
dart
import 'dart:async';
import 'package:audio_service/audio_service.dart';
import 'package:ephemeris_mobile/utils/AudioPlayerUtil.dart';
import 'package:just_audio/just_audio.dart';
class AudioCustomHandler extends BaseAudioHandler {
final List<StreamSubscription> _streamSubscriptions = [];
static AudioCustomHandler? _instance;
static AudioCustomHandler of() {
_instance ??= AudioCustomHandler();
return _instance!;
}
/// 初始化
AudioCustomHandler init() {
// 初始化控制台的状态、功能、信息
playbackState.add(
PlaybackState(
controls: [
MediaControl.skipToPrevious,
MediaControl.play,
MediaControl.pause,
MediaControl.skipToNext,
],
systemActions: {MediaAction.seek},
processingState: AudioProcessingState.ready,
playing: false,
updatePosition: Duration.zero,
bufferedPosition: Duration.zero,
),
);
// 监听播放器状态变化,同步到通知栏
_streamSubscriptions.add(
AudioPlayerUtil.of().playerStateStream.listen((state) {
AudioProcessingState processingState =
state.processingState == ProcessingState.completed
? AudioProcessingState.completed
: state.processingState == ProcessingState.ready
? AudioProcessingState.ready
: state.processingState == ProcessingState.loading
? AudioProcessingState.loading
: state.processingState == ProcessingState.buffering
? AudioProcessingState.buffering
: AudioProcessingState.idle;
playbackState.add(
playbackState.value.copyWith(
playing: state.playing,
processingState: processingState,
),
);
}),
);
// 监听当前播放歌曲
_streamSubscriptions.add(
AudioPlayerUtil.of().currentIndexStream.listen((index) {
mediaItem.add(
MediaItem(
id: 'media_id',
title: AudioPlayerUtil.of().currentAudio?.name ?? '未知歌曲',
artist: AudioPlayerUtil.of().currentAudio?.artist,
duration: AudioPlayerUtil.of().duration,
artUri: Uri.parse(
AudioPlayerUtil.of().currentAudio?.image ??
'assets/images/logo.png',
),
),
);
}),
);
// 监听播放进度
_streamSubscriptions.add(
AudioPlayerUtil.of().positionStream.listen((position) {
playbackState.add(
playbackState.value.copyWith(
updatePosition: position,
bufferedPosition: position,
),
);
}),
);
return this;
}
@override
Future<void> play() => AudioPlayerUtil.of().play();
@override
Future<void> pause() => AudioPlayerUtil.of().pause();
@override
Future<void> stop() => AudioPlayerUtil.of().stop();
@override
Future<void> seek(Duration position) => AudioPlayerUtil.of().seek(position);
@override
Future<void> skipToQueueItem(int index) =>
AudioPlayerUtil.of().seek(Duration.zero, index: index);
@override
Future<void> skipToNext() => AudioPlayerUtil.of().next();
@override
Future<void> skipToPrevious() => AudioPlayerUtil.of().previous();
Future<void> dispose() async {
while (_streamSubscriptions.isNotEmpty) {
await _streamSubscriptions.removeLast().cancel();
}
return Future.value();
}
}