How to detect a song end?

I'm playing library items (MPMediaItem) and apple music tracks (Track) in MPMusicPlayerApplicationController.applicationQueuePlayer, but I can't use the actual Queue functionality because I can't figure out how to get both media types into the same queue. If there's a way to get both types in a single queue, that would solve my problem, but I've given up on that one.

Because I can't use a queue, I have to be able to detect when a song ends so that I can put the next song in the queue and play it. The only way I can figure out to detect when a song ends is by watching the playBackState, and I've actually got that pretty much working, but it's really ugly, because you get playBackState of paused when a song ends, and when a bluetooth speaker disconnects, etc.

The only answer I've been able to find on the internet is to watch the MPMusicPlayerControllerNowPlayingItemDidChange, and when that fires, and the nowPlayingItem is NIL, a song ends.. but that's not the case. When a song ends, the nowPlayingItem remains the same. There's got to be an answer to this problem, right?

Accepted Reply

Hello @samhall,

Thank you for your feedback about using both MusicKit and MediaPlayer frameworks.

If you really need to play both library items represented as MPMediaItem and catalog items represented by Track in the same queue, you can actually achieve that using a single player, the one from MediaPlayer, by converting the Track's playParameters into MPMusicPlayerPlayParameters. You can achieve this by leveraging the fact that both of these types conform to Codable.

Assuming you have a local variable named tracks which is an array of Track objects from MusicKit, you can append them to the end of MPMusicPlayerController's applicationQueuePlayer's queue as follows:

let tracksPlayParametersQueue = try tracks.compactMap { track -> MPMusicPlayerPlayParameters? in
    var playParameters: MPMusicPlayerPlayParameters?
    if let trackPlayParameters = track.playParameters {
        let encoder = JSONEncoder()
        let trackPlayParametersData = try encoder.encode(trackPlayParameters)
        
        let decoder = JSONDecoder()
        playParameters = try decoder.decode(MPMusicPlayerPlayParameters.self, from: trackPlayParametersData)
    }
    return playParameters
}

let tracksQueueDescriptor = MPMusicPlayerPlayParametersQueueDescriptor(playParametersQueue: tracksPlayParametersQueue)
MPMusicPlayerController.applicationQueuePlayer.append(tracksQueueDescriptor)

I hope this helps.

Best regards,

  • Thank you so much, that solved my problem!

Add a Comment

Replies

Hello @samhall,

Thank you for your feedback about using both MusicKit and MediaPlayer frameworks.

If you really need to play both library items represented as MPMediaItem and catalog items represented by Track in the same queue, you can actually achieve that using a single player, the one from MediaPlayer, by converting the Track's playParameters into MPMusicPlayerPlayParameters. You can achieve this by leveraging the fact that both of these types conform to Codable.

Assuming you have a local variable named tracks which is an array of Track objects from MusicKit, you can append them to the end of MPMusicPlayerController's applicationQueuePlayer's queue as follows:

let tracksPlayParametersQueue = try tracks.compactMap { track -> MPMusicPlayerPlayParameters? in
    var playParameters: MPMusicPlayerPlayParameters?
    if let trackPlayParameters = track.playParameters {
        let encoder = JSONEncoder()
        let trackPlayParametersData = try encoder.encode(trackPlayParameters)
        
        let decoder = JSONDecoder()
        playParameters = try decoder.decode(MPMusicPlayerPlayParameters.self, from: trackPlayParametersData)
    }
    return playParameters
}

let tracksQueueDescriptor = MPMusicPlayerPlayParametersQueueDescriptor(playParametersQueue: tracksPlayParametersQueue)
MPMusicPlayerController.applicationQueuePlayer.append(tracksQueueDescriptor)

I hope this helps.

Best regards,

  • Thank you so much, that solved my problem!

Add a Comment

I note with frustration that the marked answer does in fact not answer the specifically asked question at all although yes, it's very helpful for the OP in answering a different question from the one they asked, which helps them move forward and that's great.

The marked answer does tell you how to unobviously fudge the awkwardly different API classes into a player queue, but ignores all of the entirely valid issues that the OP brings up on detecting end-of-track - an elementary and obvious thing to need and available in every other player API I've ever used. It's just extraordinary that the API could be so obtuse - the playback status is nonsensical (paused), indistinguishable from other events (speaker disconnect), the now-playing items don't make sense (sometimes nil, sometimes not) and the current position is meaningless. Indeed, when it's not zero, often the current position when the event arrives will be returned as either slightly before the song's stated duration, or even slightly after the end of the track! How on earth it's possible to be this completely wrong is a mystery - we've had other remarkably robust APIs for streaming music since dialup in the 1990s.

The next question the OP will have is why their queue of music keeps just randomly skipping tracks - and indeed in the worst case, with looping turned on, can even get stuck in a tight loop skipping every single track endlessly - because of the years-old "failed to prepare to play" bug. And that's before we even get to all the new bugs added with lossless, wherein, of course, nothing else actually got fixed - I mean, heaven forbid we improve product quality rather than just jamming in even more buggy features, right?

The MediaPlayer API is a very poorly designed interface but on top of that, it's by far the most buggy API I've ever worked with in over 25 years of professional development plus my spare time hobby projects. Apple - hang your head in shame; it's atrocious. I'd be so, so happy to hear that you're going to do some serious engineering to fix it up, but I'm sadly very confident that you just don't care at all.

Hello @adh1003,

Thank you for your feedback on MediaPlayer's playback API.

However, I'm afraid that this sort of laundry list of vague complaints is not actionable for us.

We'd be happy to help and investigate specific issues as long as we can get relevant data for us to efficiently triage them, such as specific tickets on Feedback Assistant including a sysdiagnose, and ideally, a sample app, or a snippet of the app's code that exhibits the problem.

Beyond that, you might have noticed that we are making a real effort to engage with the developer community on the forums, and that we try to be pretty responsive about any inquiries tagged with MusicKit.

Lastly, I would kindly suggest that you focus on constructive feedback when reaching out to us about any issues you're encountering.

Best regards,

  • Comment removed in favour of a full "answer" response, since newlines are stripped out of comments by these forums! This made the response illegible.

Add a Comment

Hi @JoeKun. I'm delighted to hear that Apple are now engaging more on the forums. I have in the past submitted polite, fully constructed Feedback Assistant reports related to the playback performance of Apple Music and so far been met with silence. I hope to hear back soon. During the months of development, I've read countless posts on these and other forums from other developers facing what sound like exactly the same bugs, spread over several years, during which time sadly no fixes seem to have been forthcoming. I can only hope that at some point there is a concerted effort within Apple to improve the implementation's reliability. No more "failed to prepare to play"!

So far, nobody has directly answered the OP's question. This is the primary issue here: Detecting end of playback.

It kinda "feels" like people dancing around admitting that it's impossible, at least cleanly. For whatever reason, when the Apple Music API finishes playing a track and there's nothing else in a playlist, it performs the very strange behaviour of entering a paused state and seeking to the start offset of either the first playlist track, or the last playlist track (LATER EDIT: I also do see sometimes behaviour where current position is reported as fractionally after the end of the track, too). I've never been too clear on this because none of its behaviour in this regard appears to be described in the API documentation, so we have to guess and use observation, leaving developers with no idea if they're relying upon behaviour that's meant to be part of the API's contract between implementation and consumer, or just a random observed quirk.

There is a "stopped" state for the media player, but it does not enter this state. Consequently, distinguishing between a user initiated pause event or an "actually I stopped playing" pause event, or a "user initiated pause then the user scrubbed a position slider back to the start of the track" edge case thus takes time-consuming coding effort and heuristic fudging, all because the API doesn't enter a stopped state.

At the risk of mentioning a second item and building a laundry list, it'd also be lovely if we could set playback volume for our applications. This used to be possible, but then the ability was removed.

It's really quite surprising to me that in 2021 we might be using a media playback API where reliably detecting end of playback is very difficult, and setting playback volume is impossible. I really hope that these matters can be addressed with haste.

Hello @adh1003,

I checked with my colleagues who work on our playback engine, and they confirmed to me that the current behavior where the player goes back to .paused state when it reaches the end of the playback queue is actually the intended behavior.

Before iOS 10, the player would enter the .stopped state at the end of the playback queue, which implied that the playback queue had become empty.

This behavior of the playback engine actually lead to several problems. For example, in the iOS Music app, the mini player would consequently show Not Playing, which made it impossible for users to restart what they had been listening to. Additionally, entering the .stopped state would also cause various issues with car integrations.

For all those reasons, a decision was made to change the behavior of the playback engine in iOS 10 so that, upon reaching the end of the playback queue, it would return to the beginning of the queue, but leave it .paused.

I hope this helps.

Best regards,

  • very poor indeed, no way to reliably detect end of song. NotificationCenter nowPlayingItemDidChange notification fires on start of playback but not at end of last track in queue. So no way to detect all tracks in queue have finished playing. Need a itemDidEnd notification. A real hole in the API. I had to resort to a timer which checks currentPlaybackTime every second. a brute force approach

Add a Comment