Reputation: 53
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.. :)
Upvotes: 5
Views: 4475
Reputation: 1
This appears to still be an issue in 2025, so rather than post a new question I thought it cleaner to add to this conversation. For now I have settled for adding a sleep into my code to make it wait for the duration of the track. Each track object contains its duration so this is easy enough. But it is a klunky, highly volatile, solution. Pausing the track messes it up, for example.
So here's where hopefully some smarter people can weigh in. There are 2 other potential fixes I have noticed in my testing. Developer Tools are your friend here.
First, the index.js script that gets loaded into the hidden iframe DOES contain definitions for item_end and track_ended. But they don't seem to be emitting to the parent. I don't know if this is a bug or intentional by Spotify. But I do see in the network traffic a POST event from fetch to https://cpapi.spotify.com/v1/client/{client_id}/event/item_end so the event is firing, it's just not getting back to our app code from the embedded player. I wasn't successful in any attempts to intercept that fetch() call as a way to determine the track had ended.
Second, in Chromium-based browsers anyway, the embedded player logs typical events like load, play, pause, and ended, as viewable in the Developer Tools Media tab. (https://chromium.googlesource.com/chromium/src/+/refs/heads/main/media/base/media_log_events.h) If there's a way to listen for these library calls then that's a possibility too.
Upvotes: 0
Reputation: 6873
I have a different approach which seems to work really good. This also supports seeking.
player.addListener('player_state_changed', (state => {
if (!state) {
return;
}
if (state.paused) {
console.log('Skipping... Player is paused.');
return;
}
const endTimeoutDelay = state.duration - state.position - 5000; // Pause before 5 seconds of song ends
const waitDelay = 5 * 1000;
const resumeTimeoutDelay = endTimeoutDelay + waitDelay;
if (window.spotifyTimeout1) {
clearTimeout(window.spotifyTimeout1);
window.spotifyTimeout1 = null;
}
if (window.spotifyTimeout2) {
clearTimeout(window.spotifyTimeout2);
window.spotifyTimeout2 = null;
}
window.spotifyTimeout1 = setTimeout(() => {
clearTimeout(window.spotifyTimeout1);
console.log('Song Ended');
window.spotify_player && window.spotify_player.pause();
console.log('Waiting 5 Seconds...')
}, endTimeoutDelay);
window.spotifyTimeout2 = setTimeout(() => {
clearTimeout(window.spotifyTimeout2);
console.log('Resuming Song...');
window.spotifyTimeout1 = null;
window.spotifyTimeout2 = null;
window.spotify_player && window.spotify_player.nextTrack();
}, resumeTimeoutDelay);
}));
Upvotes: 0
Reputation: 11
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
}
Upvotes: 1
Reputation: 1171
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;
}
);
Upvotes: 1
Reputation: 1171
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.
Upvotes: 2
Reputation: 1275
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;
}
});
Upvotes: 0