androidflutteraudio-playerbackground-audioaudio-service

flutter audio_service: AudioProcessingState not updating after completely playing media item


I developing a music player application in flutter.

Scenario: When user selects one song from list, once the selected song is completely played, automatically need to start the next song in the list.

Currently I am listening to PlaybackState, Position streams in order to update song details and slider info on UI. But, I am unable to figure out when the selected song is finished playing. Idea here is once the selected song is played, the next song in the list has to be played. Initially, the AudioProcessingState is changing from connecting state to ready state. The first media item is played successfully. But,

Issue 1: Even after completely playing the first media Item, the AudioProcessingState is still ready (not changed to completed). Even the playing is true always. Below is the logic for play and pause the UI.

      StreamBuilder<PlaybackState>(
                      stream: AudioService.playbackStateStream,
                      builder: (context, snapshot) {
                        final processingState = snapshot.data?.processingState;
                        print('Current Processingstate: $processingState');
                        final playing = snapshot.data?.playing ?? false;

                        if (playing)
                          return IconButton(
                              iconSize: 40,
                              onPressed: () {
                                AudioService.pause();
                              },
                              icon: Icon(FontAwesomeIcons.pause,
                                  color: Colors.white));
                        else
                          return IconButton(
                              iconSize: 40,
                              onPressed: () {
                                if (AudioService.running) {
                                  AudioService.play();
                                } else {
                                  List<dynamic> list = [];

                                  for (int i = 0;
                                      i < this.widget.mediaList.length;
                                      i++) {
                                    var m = this.widget.mediaList[i].toJson();
                                    list.add(m);
                                  }
                                  var params = {
                                    "data": list,
                                    'index': this.widget.currentIndex
                                  };

                                  AudioService.connect();
                                  AudioService.start(
                                      backgroundTaskEntrypoint:
                                          _backgroundTaskEntrypoint,
                                      params: params);
                                }
                              },
                              icon: Icon(FontAwesomeIcons.play,
                                  color: Colors.white));
                      },
                    ),

Issue 2: Also, the Position stream is continuously receiving even after finishing the selected song. Because of this I am unable to stop updating the duration label.

                StreamBuilder<Duration>(
                      stream: AudioService.positionStream,
                      builder: (context, snapshot) {
                        final mediaState = snapshot.data;
                        return SeekBar(
                          duration: this.widget.mediaList[current].duration ??
                              Duration.zero,
                          position: aPosition,
                          onChangeEnd: (newPosition) {
                            AudioService.seekTo(newPosition);
                          },
                        );
                      },
                    ),

In brief,

  1. How can I come to know the selected song is completed playing?
  2. How to update the slider once the song is completed playing?

Solution

  • Updated answer:

    As of version 0.9.28 you can now detect when the player auto-advances to the next item this way:

    _player.positionDiscontinuityStream.listen((discontinuity) {
      if (discontinuity.reason == PositionDiscontinuityReason.autoAdvance) {
        // INSERT CODE TO REFLECT NEW SONG
      }
    });
    

    Original answer:

    There is an outstanding feature request to add a new type of event subscription that can inform you when the player auto-advances to the next item in the sequence here. You can vote for that feature by clicking the thumbs up button.

    However, there is also a workaround described in this comment. What you can do is listen to player.currentIndexStream which tells you whenever the current item changes. When the player auto-advances to the next item, this index will change, but you can't assume that every time the current index changes that it was because of an auto-advance, because it also could have been because of a manual seek. For this reason, the workaround is to just keep a boolean variable that indicates whether a manual seek was used so that you'll know what the case was. Every time you seek, you set that boolean variable to true, and set it back to false after the seek has completed.

    To make it easier, you could make your own version of the seek method to do this, e.g. mySeek and even add this as an extension method on AudioPlayer:

    bool _seeking = false;
    extension AudioPlayerExtension on AudioPlayer {
      Future<void> mySeek(Duration position, {int? index}) async {
        _seeking = true;
        await seek(position);
        _seeking = false;
      }
    }
    

    Then the other part of the solution is to listen to the currentIndexStream:

    _player.currentIndexStream.listen((index) {
      if (index == null) return;
      if (_seeking) {
        // the index changed due to a manual seek
      } else {
        // the index changed due to an auto-advance
        onAutoAdvance();
      }
    });
    

    So now, every time you want to seek manually to a different item, you use:

    player.mySeek(pos, index: i);
    

    And then to handle when the item changed due to an auto advance, write your code to handle it in here:

    void onAutoAdvance() {
    }