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?

Answered by Frameworks Engineer in 684570022

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,

Accepted Answer

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,

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,

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,

I know I’m resurrecting an old thread but this problem is still not resolved. When writing my player code for Apple Music I encountered all the issues @adh1003 mentioned.

all we want is to have some way of being notified when song finishes playing. I tried to code a workaround for it but at this point I feel it’s just impossible. The best way I came up with was waiting for state changed notification and checking if player’s playback time is greater than track’s duration, but that doesn’t work if user presses „Skip” button on lock screen

Of course player’s playback time being greater than track’s duration seems like a bug but at least it’s somewhat helpful…

@JoeKun to be honest if framework’s behaviour had to be dumbed down because Apple Music app had trouble deciding if „Not Playing” label should be displayed it means that your programmers are not up to task. If the player actually changed its state to „.stopped” our lives would be much easier

I recently worked with Spotify SDK for iOS and that API actually reports correctly about the state changes so I don’t know why Apple’s own framework cannot

This issue is literally the only thing that prevents us from creating our own usable wrappers around MPMusicPlayerController. At least in my case, I managed to work around other issues but this one is the most frustrating

This API has been left basically unchanged for 12+ years now so I’m not holding my breath but I was excitied when I saw that @JoeKun has been very active in responding to feedback here.

Thanks

I agree with the posters here – it is very surprising that we are not able to get a notification for when the queue has ended.

I imagine the MPMusicPlayer framework can emit a notification like "MPMusicPlayerControllerQueueEnded" – it is clearly doing its own cleanup operations when a queue actually ends, so it should be straightforward to add this in.

I have hacked around this with a non-ideal solution, one that I'm afraid this might not cover all edge cases.

I run this on receiving a MPMusicPlayerControllerPlaybackStateDidChange notification:

if player.playbackState == .paused,
   let currentTrack = currentTrack,
   currentTrack == queue.last,
   player.currentPlaybackTime >= currentTrack.unwrappedDuration - 1 {
            EndOfQueueManager.handler()
}

And I run this on receiving a MPMusicPlayerControllerNowPlayingItemDidChange notification:

if UIApplication.shared.applicationState != .active,
           queuePosition == queue.count - 1 {
            EndOfQueueManager.handler()
}

currentTrack, queuePosition and queue are objects that I (reluctantly) maintain, but that's pretty much to work around the limitations of the API.

I filed a bug report back in 2019, FB6489728 Recent Similar Reports:None Resolution:Open

MPMusicPlayerController EOF notification.

Description:

"When the player is set to MPMusicRepeatModeNone: There should be sent a notification when a song reaches EOF file. For my particular use I only enqueue one song and need to know when the songs has ended. MPMusicPlayerControllerPlaybackStateDidChangeNotification and looking for MPMusicPlaybackStateStopped or MPMusicPlaybackStatePaused can be used but it's very hard to to be 100% sure if it was because reaching EOF."

//

For my app I have solved the problem but with code I don't want anyone to see. Full of heuristic kludge, hard to maintain and can break anytime Apple changes something. A simple EOF notification would solve the problem.

And while I'm at it, please add a pitch change setting!!

I'm going to pile on here. This is a basic basic requirement of a Music engine. Other first-party Apple Frameworks handle end-state events, a dedicated custom state needs to be added which rolls up that track end -> last item in queue -> return to pause scenario into an event.

The Android MusicKit SDK can report the end of a track (via the item changed event) btw.

Apologies for being a little hostile, reading the responses from the Apple folks here annoyed me because they're being snippy over what are glaring omissions in a large first-party framework.

@JoeKun Still no answer here as to the best method to have a task triggered by the end of playback of an item.

@JoeKun Bumping the thread, this functionality is crucial to a music player API

@JoeKun Bumping this again - is this something that might get addressed in iOS 18?

How to detect a song end?
 
 
Q