如何将数据发送到从 UI 扩展 BackgroundAudioTask 的 AudioServiceTask class

How to send data to AudioServiceTask class which extends BackgroundAudioTask from UI

好吧,我被这个问题困住了。我有一个 audioservice (audioplayer.dart) 的代码,需要 queue 才能播放。我使用 ModalRouteplaylist.dart in audioplayer.dart 获取队列并保存在全局变量 queue 中。然后,我初始化 AudioPlayerService。现在到这里为止一切都很好,但是在扩展 BackgroundAudioTaskAudioPlayerTask class 内部,当我尝试访问变量(在 onStart 内部)时,它变成了一个空列表.我不知道问题出在哪里,而且我对 BackgroundAudioTask class 不是很熟悉。这是它的样子:

import .....

List<MediaItem> queue = [];

class TempScreen extends StatefulWidget {
  @override
  _TempScreenState createState() => _TempScreenState();
}

class _TempScreenState extends State<TempScreen> {
      @override
      Widget build(BuildContext context) {
        queue = ModalRoute.of(context).settings.arguments;
        // NOW HERE THE QUEUE IS FINE
        return Container(.....all ui code);
        }

    // I'm using this button to start the service
    audioPlayerButton() {
    AudioService.start(
      backgroundTaskEntrypoint: _audioPlayerTaskEntrypoint,
      androidNotificationChannelName: 'Audio Service Demo',
      androidNotificationColor: 0xFF2196f3,
      androidNotificationIcon: 'mipmap/ic_launcher',
      androidEnableQueue: true,
    );
    AudioService.updateQueue(queue);
    print('updated queue at the start');
    print('queue now is $queue');
    AudioService.setRepeatMode(AudioServiceRepeatMode.none);
    AudioService.setShuffleMode(AudioServiceShuffleMode.none);
    AudioService.play();
  }
      }
    

void _audioPlayerTaskEntrypoint() async {
  AudioServiceBackground.run(() => AudioPlayerTask());
}

class AudioPlayerTask extends BackgroundAudioTask {
  AudioPlayer _player = AudioPlayer();
  Seeker _seeker;
  StreamSubscription<PlaybackEvent> _eventSubscription;
  String kUrl = '';
  String key = "38346591";
  String decrypt = "";
  String preferredQuality = '320';

  int get index => _player.currentIndex == null ? 0 : _player.currentIndex;
  MediaItem get mediaItem => index == null ? queue[0] : queue[index];

  // This is just a function i'm using to get song URLs
  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');
    print('queue now is $queue');
    // NOW HERE QUEUE COMES OUT TO BE AN EMPTY LIST
    final session = await AudioSession.instance;
    await session.configure(AudioSessionConfiguration.speech());

    if (queue.length == 0) {
      print('queue is found to be null.........');
    }
    _player.currentIndexStream.listen((index) {
      if (index != null) AudioServiceBackground.setMediaItem(queue[index]);
    });
    // Propagate all events from the audio player to AudioService clients.
    _eventSubscription = _player.playbackEventStream.listen((event) {
      _broadcastState();
    });
    // Special processing for state transitions.
    _player.processingStateStream.listen((state) {
      switch (state) {
        case ProcessingState.completed:
          AudioService.currentMediaItem != queue.last
              ? AudioService.skipToNext()
              : AudioService.stop();
          break;
        case ProcessingState.ready:
          break;
        default:
          break;
      }
    });

    // Load and broadcast the queue
    print('queue is');
    print(queue);
    print('Index is $index');
    print('MediaItem is');
    print(queue[index]);
    try {
      if (queue[index].extras == null) {
        queue[index] = queue[index].copyWith(extras: {
          'URL': await fetchSongUrl(queue[index].id),
        });
      }

      await AudioServiceBackground.setQueue(queue);
      await _player.setUrl(queue[index].extras['URL']);
      onPlay();
    } catch (e) {
      print("Error: $e");
      onStop();
    }
  }

  @override
  Future<void> onSkipToQueueItem(String mediaId) async {
    // Then default implementations of onSkipToNext and onSkipToPrevious will
    // delegate to this method.
    final newIndex = queue.indexWhere((item) => item.id == mediaId);
    if (newIndex == -1) return;
    _player.pause();
    if (queue[newIndex].extras == null) {
      queue[newIndex] = queue[newIndex].copyWith(extras: {
        'URL': await fetchSongUrl(queue[newIndex].id),
      });
      await AudioServiceBackground.setQueue(queue);
      // AudioService.updateQueue(queue);
    }
    await _player.setUrl(queue[newIndex].extras['URL']);
    _player.play();
    await AudioServiceBackground.setMediaItem(queue[newIndex]);
  }

  @override
  Future<void> onUpdateQueue(List<MediaItem> queue) {
    AudioServiceBackground.setQueue(queue = queue);
    return super.onUpdateQueue(queue);
  }

  @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();
    // Shut down this task
    await super.onStop();
  }

  Future<void> _seekRelative(Duration offset) async {
    var newPosition = _player.position + offset;
    // Make sure we don't jump out of bounds.
    if (newPosition < Duration.zero) newPosition = Duration.zero;
    if (newPosition > mediaItem.duration) newPosition = mediaItem.duration;
    // Perform the jump via a seek.
    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();
    }
  }

  /// Broadcasts the current state to all clients.
  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}");
    }
  }
}

This 是 AudioService 的完整代码,以备不时之需。

(答案更新:从v0.18开始,由于共享隔离中的UI和后台代码运行,这种陷阱不存在。下面的答案仅与v0相关.17 及更早版本。)

audio_service 运行 将您的 BackgroundAudioTask 放在单独的隔离区中。在README中是这样写的:

Note that your UI and background task run in separate isolates and do not share memory. The only way they communicate is via message passing. Your Flutter UI will only use the AudioService API to communicate with the background task, while your background task will only use the AudioServiceBackground API to interact with the UI and other clients.

这里的关键点是 isolate 不共享内存。如果您在 UI isolate 中设置了一个“全局”变量,它不会在后台 isolate 中设置,因为后台 isolate 有自己独立的内存块。这就是为什么您的全局 queue 变量为空。它实际上不是同一个变量,因为现在你实际上有两个变量副本:一个在 UI isolate 中已经设置了一个值,另一个在背景 isolate 中(尚未)设置一个值。

现在,您的后台 isolate 确实“稍后”将其自己的队列变量副本设置为某物,这是通过消息传递 API 发生的,您从 UI isolate 传递队列到 updateQueue 中,后台 isolate 收到该消息并将其存储到 onUpdateQueue 中它自己的变量副本中。如果您在此之后打印出队列,它将不再为空。

您的 onStart 中也有一行是您尝试设置队列的地方,尽管您可能应该删除该代码并让队列仅在 onUpdateQueue 中设置。您不应尝试访问 onStart 中的队列,因为您的队列要到 onUpdateQueue 才会收到它的值。如果你想在设置之前避免任何空指针异常,你可以在后台隔离中将队列初始化为一个空列表,它最终将被 onUpdateQueue 中的非空列表替换,而不会为空。

我还建议您避免将 queue 设为全局变量。全局变量通常是不好的,但在这种情况下,它实际上可能会让您感到困惑,认为队列变量在 UI 和背景隔离区中都是相同的,而实际上每个隔离区都有自己的副本可能具有不同值的变量。因此,如果您制作两个单独的“本地”变量,您的代码将更加清晰。一个在 UI 中,一个在后台任务中。

还有一个建议就是大家要注意消息传递中的方法API是异步方法。您应该等待音频服务启动,然后再向其发送消息,例如设置队列。并且在尝试从队列中播放之前,您应该等待设置队列:

await AudioService.start(....);
// Now the service has started, it is safe to send messages.
await AudioService.updateQueue(...);
// Now the queue has been updated, it is safe to play from it.