'Detect end of a track's playback with Spotify Web API / Playback SDK

Working with Spotify Web API and Web Playback SDK, does anybody know a good way to detect when a track's playback has finished, i.e. the song is over?

I know about some event named player_state_changed in Web Playback SDK, but the data provided in the event doesn't seem to be very helpful to me.

In my web app, the next song to be played is determined pretty spontaneously. It would be nice, if I could determine it only, when the current track has reached its end. Hence, there is no playlist. Instead, the app plays single tracks in series and should determine the next song, when the current one has ended.

Meanwhile I try it with this code here:

var bCallingNextSong = false;

player.addListener('player_state_changed', state => { 
    if (state != null && 
        state.position == 0 && 
        state.duration == 0 && 
        state.paused == true) { 

        if (!bCallingNextSong) {
            playNextSong();
        }
    }
});


function playNextSong() {
   $.ajax({
            url:"playback.php", //the page containing php script
            type: "post", //request type,
            dataType: 'json',
            data: {action: "next", device: deviceId},
            success: function(result) {
                bCallingNextSong = false;
            },
            error: function(XMLHttpRequest, textStatus, errorThrown) {  
                bCallingNextSong = false;
            } 
    });     
}

and in playback.php:

switch ($action) {

    case "next":
        //some blackbox magic here to determine next song, do logging etc.
        //but for now just play the same song over and over

        $api->play($deviceId, ['uris' => "spotify:track:0eGsygTp906u18L0Oimnem"],]);

        echo json_encode(array("answer"=>"played next"));
        break;  

// more code
}

However, this will often (but not always) throw an InvalidStateError (I haven't found out where exactly yet nor why). Stack says dequeueUpdates / tryUpdate / _processUpdate / _appendUpdate. Yet, I'm a beginner and I've gotta learn how to cope with Firefox Debugger first.. :)



Solution 1:[1]

My last answer got broken so I will now add new solution which work for now.

this.player.addListener('player_state_changed', (state) => {
    console.log(state);
    if (
      this.state
      && state.track_window.previous_tracks.find(x => x.id === state.track_window.current_track.id)
      && !this.state.paused
      && state.paused
      ) {
      console.log('Track ended');
      this.setTrackEnd(true);
    }
    this.state = state;
  });

So what is different from last solution? I check that current_track is in previous track list. But there was problem now with position check because it seems that current track appears to previous track list before track is completely end. But that wasn't problem in my app because there is always small delays with commands.

Solution 2:[2]

EDIT: This is not working anymore for some reason. Check my other answer which will work now. https://stackoverflow.com/a/51114848/8081009

Here is my solution. It works most of the time. Actually I didn't get it broken in any scenarios I can imagine. So hold last state and check that is it gone to paused state. Hopefully Spotify will ease our work with track ended event. It would be great if they does. This is still better than polling their /me/player/currently-playing endpoint for every 1 second or so.

this.player.addListener(
    'player_state_changed',
    state => 
    {
      console.log(state);
      if(this.state && !this.state.paused && state.paused && state.position === 0) {
        console.log('Track ended');
        this.setTrackEnd(true);
      }
      this.state = state;
    }
  );

Solution 3:[3]

I had this same problem, this is how I solved it.

When a song finish playing it gets added to the state.track_window.previous_tracks list

state.track_window.previous_tracks gets populated only if there is a next track and it starts playing

But every time you call the api to play a new track, it clears the state.track_window.previous_tracks & state.track_window.next_tracks list.

If there is a next track, I want to stop and choose my own next track and then call the api

var alreadyPlayed = state.track_window.previous_tracks

if(alreadyPlayed.length > 0) {
  player.pause()
}

if there is no next track the player will be paused when the song finish

if(state.paused && state.position === 0) {
  //song ended
  ChooseNextSong()
}

the state callback gets fired many times but I want to call ChooseNextSong() only once, so I increment startCheck every time it fires

if(state.paused && state.position === 0) {
  //song ended
  startCheck++
  if(startCheck === 1) {
    ChooseNextSong()
  }
}

check if song started

if(state.position === 0 && alreadyPlayed.length === 0 && !state.paused) 
{
  //song started
  startCheck = 0
}

Solution 4:[4]

You should use the player_state_changed event and look at which track is playing to see if the track has changed. You can find the current track in playerStateObject.track_window.current_track.

A possible implementation could look like this:

let currentTrack = '';

player.on('player_state_changed', state => {
    if( state.track_window.current_track.id != currentTrack ) {
        // The track changed!
        currentTrack = state.track_window.current_track.id;
    }
});

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Janne Harju
Solution 2
Solution 3
Solution 4 arirawr