You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.29 08:22:30