iosswiftmpmusicplayercontroller

Rearrange MediaPlayer Music Queue [iOS]


I am really struggling to figure something out and I've looked all over the internet and I can't find an answer. As you know, music apps like Spotify and Apple Music itself let the user rearrange the up next queue even while the user is listening to a song. I haven't been able to figure out how to do the same. I want to be able to completely rearrange the queue including adding, deleting, and moving around songs in the up next queue. I've tried various ways of doing so such as

musicPlayer.setQueue([queue])

but this would require me to call musicPlayer.prepareToPlay() afterwords to update the queue which would end the current song and initiate the new queue.

I've also tried using

musicPlayer.perform(queueTransaction: { (currentQueue) in
    let oldItems = currentQueue.items

    var currentItem: MPMediaItem? = nil
    for (i, item) in oldItems.enumerated() {
        if i == musicPlayer.indexOfNowPlayingItem {
            currentItem = item
            continue
        }

        currentQueue.remove(item)
    }

    currentQueue.insert(queue, after: currentItem)
}, completionHandler: { (newQueue, error) in
    if let error = error {
        print("there was an error updating the queue", error)
        return
    }

    print("===  NEW QUEUE  ===")  //insert does not work
    for item in newQueue.items {
        print("  -", item.title ?? "[No Title]")
    }
})

but as you may know from this question: MPMusicPlayerControllerMutableQueue insert an Apple Music song not working

the insert after item function of a queue doesn't seem to work (and I've also tried setting item to nil which sometimes removed the current item and set the queue to nothing). I also know about musicPlayer.prepend([queue]) which will add the new queue after the currently playing item but I would also have to remove the previous queue from memory so the up next queue doesn't end up being over 300 songs long. I've also tried using the prepend function inside the queue transaction but sometimes the song would stop in the middle and the music player would set the nowPlayingItem to nil. I really hope someone knows the answer to this because I've been having this issue for over 4 days now and I can't seem to find a solution that works 100% of the time.


Solution

  • MPMusicPlayerController (and its related subclasses) makes it very difficult to manage a queue like this seamlessly. If I recall correctly, you are not even able to add duplicate MPMediaItem objects to the queue, and tapping on the playing media from Control Center will actually launch the Music app instead of your own. Essentially, your app can only act as a shell to the music playing experience, and the Music app is doing all the heavy-lifting.

    Depending on your requirements, you may have another option, albeit with some compromises. The AVFoundation framework provides AVPlayer and AVQueuePlayer, which make it easier to manage a queue and provide more advanced playback options. However, you are limited to playing only downloaded, DRM-free content (i.e. songs from Apple Music will not work).

    Set up your player:

    let audioPlayer = AVQueuePlayer()
    
    // Play next song in queue.
    NotificationCenter.default.addObserver(self, selector: #selector(didPlayToEndTime), name: .AVPlayerItemDidPlayToEndTime, object: nil)
    
    // Recover from failed playback.
    NotificationCenter.default.addObserver(self, selector: #selector(failedToPlayToEndTime(_:)), name: .AVPlayerItemFailedToPlayToEndTime, object: nil)
    
    // Resume music after interruptions.
    NotificationCenter.default.addObserver(self, selector: #selector(handleAudioSessionInterruption(_:)), name: .AVAudioSessionInterruption, object: AVAudioSession.sharedInstance())
    

    Insert new items in the queue:

    let media = MPMediaQuery.songs()
    let mediaItem: MPMediaItem = media.items.first!
    
    // MPMediaItem objects stored in iCloud or from Apple Music
    // will not have a URL, therefore, they cannot be played.
    guard let assetURL: URL = mediaItem.assetURL else { return }
    
    let playerItem = AVPlayerItem(url: assetURL)
    playerItem.seek(to: kCMTimeZero)
    audioPlayer.insert(playerItem, after: nil)
    

    In my music player app, I keep an Array of media to make really simple to reorder, insert, and remove media, and an index to keep track of the currently-playing track. I then pass the media into AVQueuePlayer one or two at a time, so that I don't have to mess with its APIs for inserting/removing media.