Flutter:如何从UI向继承BackgroundAudioTask的AudioServiceTask类传递数据及队列为空问题排查
解决Flutter AudioService中UI与BackgroundAudioTask数据传递及队列异常问题
核心问题根源
你遇到的队列为空问题,本质是AudioService的后台任务运行在独立的隔离进程中,和你的UI进程完全是两个内存空间——UI里的全局queue变量,和AudioPlayerTask里访问的全局queue,根本不是同一个对象,所以后台进程里的queue初始为空,自然拿不到UI传递的数据。
解决方案:正确的队列传递与状态管理
1. 避免使用全局变量共享数据
后台任务和UI进程内存隔离,全局变量、静态变量都无法跨进程共享。我们需要用AudioService提供的官方API来传递和同步队列:
2. 修改UI端的服务启动逻辑
确保服务启动完成后再更新队列,或者把队列作为启动参数传递:
Future<void> audioPlayerButton() async { // 等待服务启动完成,确保后台任务已初始化 await AudioService.start( backgroundTaskEntrypoint: _audioPlayerTaskEntrypoint, androidNotificationChannelName: 'Audio Service Demo', androidNotificationColor: 0xFF2196f3, androidNotificationIcon: 'mipmap/ic_launcher', androidEnableQueue: true, // 可选:将队列序列化后作为启动参数传递 params: {'queue': MediaItem.encodeList(queue)}, ); // 服务启动后,通过官方API更新队列 await AudioService.updateQueue(queue); await AudioService.setRepeatMode(AudioServiceRepeatMode.none); await AudioService.setShuffleMode(AudioServiceShuffleMode.none); await AudioService.play(); }
3. 重构后台任务,维护自身队列状态
在AudioPlayerTask中维护自己的队列实例,通过onStart的params或onUpdateQueue回调获取队列数据:
class AudioPlayerTask extends BackgroundAudioTask { AudioPlayer _player = AudioPlayer(); Seeker _seeker; StreamSubscription<PlaybackEvent> _eventSubscription; String kUrl = ''; String key = "38346591"; String decrypt = ""; String preferredQuality = '320'; // 后台任务自身维护的队列,不再依赖全局变量 List<MediaItem> _queue = []; int get index => _player.currentIndex ?? 0; MediaItem get mediaItem => _queue.isNotEmpty ? _queue[index] : null; // 获取歌曲URL的方法保持不变 fetchSongUrl(songId) async { print('starting fetching url'); String songUrl = "https://www.jiosaavn.com/api.php?app_version=5.18.3&api_version=4&readable_version=5.18.3&v=79&_format=json&__call=song.getDetails&pids=" + songId; var res = await get(songUrl, headers: {"Accept": "application/json"}); var resEdited = (res.body).split("-->"); var getMain = jsonDecode(resEdited[1]); kUrl = await DesPlugin.decrypt( key, getMain[songId]["more_info"]["encrypted_media_url"]); kUrl = kUrl.replaceAll('96', '$preferredQuality'); print('fetched url'); return kUrl; } @override Future<void> onStart(Map<String, dynamic> params) async { print('inside onStart of audioPlayertask'); final session = await AudioSession.instance; await session.configure(AudioSessionConfiguration.speech()); // 1. 从启动参数中恢复队列(如果传递了的话) if (params.containsKey('queue')) { _queue = MediaItem.decodeList(params['queue']); print('Queue from startup params: $_queue'); } // 2. 监听UI端的队列更新事件 AudioServiceBackground.onUpdateQueue.listen((newQueue) { _queue = newQueue; // 如果当前无播放内容,自动加载第一个项 if (_player.processingState == ProcessingState.idle && _queue.isNotEmpty) { _loadMediaItem(0); } }); // 播放器状态监听逻辑调整为使用本地_queue _player.currentIndexStream.listen((index) { if (index != null && _queue.isNotEmpty) { AudioServiceBackground.setMediaItem(_queue[index]); } }); _eventSubscription = _player.playbackEventStream.listen((event) { _broadcastState(); }); _player.processingStateStream.listen((state) { switch (state) { case ProcessingState.completed: if (_queue.isNotEmpty && index < _queue.length - 1) { AudioService.skipToNext(); } else { AudioService.stop(); } break; case ProcessingState.ready: break; default: break; } }); // 初始化时如果已有队列,加载第一个项 if (_queue.isNotEmpty) { await _loadMediaItem(index); } else { print('Queue is empty in onStart - waiting for UI update'); } } // 封装媒体项加载逻辑,复用代码 Future<void> _loadMediaItem(int itemIndex) async { if (itemIndex < 0 || itemIndex >= _queue.length) return; var mediaItem = _queue[itemIndex]; // 仅当URL未缓存时才去获取 if (mediaItem.extras == null || !mediaItem.extras.containsKey('URL')) { String url = await fetchSongUrl(mediaItem.id); mediaItem = mediaItem.copyWith(extras: {'URL': url}); // 更新本地队列并同步到AudioService _queue[itemIndex] = mediaItem; await AudioServiceBackground.setQueue(_queue); } await _player.setUrl(mediaItem.extras['URL']); await onPlay(); } @override Future<void> onSkipToQueueItem(String mediaId) async { final newIndex = _queue.indexWhere((item) => item.id == mediaId); if (newIndex == -1) return; await _player.pause(); await _loadMediaItem(newIndex); await AudioServiceBackground.setMediaItem(_queue[newIndex]); } @override Future<void> onUpdateQueue(List<MediaItem> newQueue) async { _queue = newQueue; await AudioServiceBackground.setQueue(_queue); // 如果当前播放索引超出新队列范围,自动切换到第一个 if (index >= _queue.length && _queue.isNotEmpty) { await _loadMediaItem(0); } await super.onUpdateQueue(newQueue); } // 其余方法(onPlay、onPause、_broadcastState等)保持不变 @override Future<void> onPlay() => _player.play(); @override Future<void> onPause() => _player.pause(); @override Future<void> onSeekTo(Duration position) => _player.seek(position); @override Future<void> onFastForward() => _seekRelative(fastForwardInterval); @override Future<void> onRewind() => _seekRelative(-rewindInterval); @override Future<void> onSeekForward(bool begin) async => _seekContinuously(begin, 1); @override Future<void> onSeekBackward(bool begin) async => _seekContinuously(begin, -1); @override Future<void> onStop() async { await _player.dispose(); _eventSubscription.cancel(); await _broadcastState(); await super.onStop(); } Future<void> _seekRelative(Duration offset) async { var newPosition = _player.position + offset; if (newPosition < Duration.zero) newPosition = Duration.zero; if (newPosition > mediaItem.duration) newPosition = mediaItem.duration; await _player.seek(newPosition); } void _seekContinuously(bool begin, int direction) { _seeker?.stop(); if (begin) { _seeker = Seeker(_player, Duration(seconds: 10 * direction), Duration(seconds: 1), mediaItem) ..start(); } } Future<void> _broadcastState() async { await AudioServiceBackground.setState( controls: [ MediaControl.skipToPrevious, if (_player.playing) MediaControl.pause else MediaControl.play, MediaControl.stop, MediaControl.skipToNext, ], systemActions: [ MediaAction.seekTo, MediaAction.seekForward, MediaAction.seekBackward, ], androidCompactActions: [0, 1, 3], processingState: _getProcessingState(), playing: _player.playing, position: _player.position, bufferedPosition: _player.bufferedPosition, speed: _player.speed, ); } AudioProcessingState _getProcessingState() { switch (_player.processingState) { case ProcessingState.idle: return AudioProcessingState.stopped; case ProcessingState.loading: return AudioProcessingState.connecting; case ProcessingState.buffering: return AudioProcessingState.buffering; case ProcessingState.ready: return AudioProcessingState.ready; case ProcessingState.completed: return AudioProcessingState.completed; default: throw Exception("Invalid state: ${_player.processingState}"); } } }
关键注意事项
- 跨进程数据传递:必须使用AudioService提供的
params(启动时)或updateQueue/onUpdateQueue(运行时)API,所有传递的数据必须是可序列化的(MediaItem已经实现了序列化方法)。 - 后台任务状态独立性:后台任务必须完全独立于UI进程,所有状态(队列、播放位置等)都要在任务内部维护,不能依赖UI进程的任何变量。
- 异步操作顺序:启动服务时一定要用
await,确保服务初始化完成后再执行后续的队列更新、播放操作,避免因服务未就绪导致的API调用失败。
内容的提问来源于stack exchange,提问作者Ankit Sangwan
相关产品推荐
相关产品推荐

