Flutter 多端音频控制台:基于 audio_service 实现 iOS、Android 锁屏与通知中心播放控制
这里的方法都是对 **BaseAudioHandler方法的重写,然后使用全局音频控制器 AudioPlayerUtil **进行控制。音频播放:Future<void> play()音频暂停:Future<void> pause()音频停止:Future<void> stop()指定音频播放位置:Future<void> seek(Duration position)跳转到指定音频:Future
继承 audio_service 中的 BaseAudioHandler 实现 AudioCustomHandler
在 AudioCustomHandler 类中对接系统控制台与 Flutter 软件的音频交互逻辑。 为了释放在 AudioCustomHandler 初始化时占用的资源,该类使用单例模式。
- _instance: AudioCustomHandler 的单例对象
- static AudioCustomHandler of():单例获取函数
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
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()
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 的文档
// .... AudioService.init( builder: () => AudioCustomHandler.of().init(), config: AudioServiceConfig( androidNotificationChannelId: 'com.xxx.xxxxxx.audio', androidNotificationChannelName: 'xxxxxx', ), ); // ...
资源销毁
单例模式中的资源不释放/销毁问题也不大,因为单例的释放/销毁一般都伴随着整个进程的结束。但为了养成一个良好的编程习惯,还是要销毁。 销毁时机可以放在主页面的销毁函数中,一般主页面销毁意味着整个 App 的退出,进程的结束。
class AudioCustomHandler extends BaseAudioHandler { // ...... Future<void> dispose() async { while (_streamSubscriptions.isNotEmpty) { await _streamSubscriptions.removeLast().cancel(); } return Future.value(); } }
附上源码
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(); } }
更多推荐


所有评论(0)