MPNowPlayingInfoCenter nowPlayingInfo not updating at end of track

Note: I posted this on Stackoverflow but haven't gotten any answers. Please let me know if the question is unclear or the code sample is insufficient.


I have a method that changes the audio track played by my app's

AVPlayer
and also sets
MPNowPlayingInfoCenter.defaultCenter().nowPlayingInfo
for the new track:
func setTrackNumber(trackNum: Int) { 
    self.trackNum = trackNum 
    player.replaceCurrentItemWithPlayerItem(tracks[trackNum]) 
    var nowPlayingInfo: [String: AnyObject] = [ : ] 
    nowPlayingInfo[MPMediaItemPropertyAlbumTitle] = tracks[trackNum].albumTitle 
    nowPlayingInfo[MPMediaItemPropertyTitle] = "Track \(trackNum)" 
    ... 
    MPNowPlayingInfoCenter.defaultCenter().nowPlayingInfo = nowPlayingInfo 
    print("Now playing local: \(nowPlayingInfo)") 
    print("Now playing lock screen: \(MPNowPlayingInfoCenter.defaultCenter().nowPlayingInfo)") 
}

I call this method when the user explicitly selects an album or track and when a track ends and the next one automatically starts. The lock screen correctly shows the track metadata when the user sets an album or track but NOT when a track ends and the next one is automatically set.


I added print statements to make sure I was correctly populating the

nowPlayingInfo
dictionary. As expected, the two print statements print the same dictionary content when this method is called for a user-initiated change of album or track. However, in the case when the method is called after an automatic track change, the local
nowPlayingInfo
variable shows the new
trackNum
whereas
MPNowPlayingInfoCenter.defaultCenter().nowPlayingInfo
shows the previous
trackNum
:
Now playing local: ["title": Track 9, "albumTitle": Test Album, ...] 
Now playing set: Optional(["title": Track 8, "albumTitle": Test Album, ...]


I discovered that when I set a breakpoint on the line that sets

MPNowPlayingInfoCenter.defaultCenter().nowPlayingInfo
to
nowPlayingInfo
, then the track number is correctly updated on the lock screen. Adding
sleep(1)
right after that line also ensures that the track on the lock screen is correctly updated.


I have verified that

nowPlayingInfo
is always set from the main queue. I've tried explicitly running this code in the main queue or in a different queue with no change in behavior.


What is preventing my change to

MPNowPlayingInfoCenter.defaultCenter().nowPlayingInfo
? How can I make sure that setting
MPNowPlayingInfoCenter.defaultCenter().nowPlayingInfo
always updates the lock screen info?

Accepted Reply

One thing to keep in mind is that dispatch_async causes the block of code to be executed on the desired thread, scheduled at some point in the future. And for a serial queue, it is FIFO, but it would be on a subsequent run loop.


I'm looking into whether or not the setter is asynchronous, but I think maintaining your own dictionary and setting all of the values correctly will always ensure that you are setting the data you intend to, as opposed to fetching the current dictionary, changing one value, and setting it, which is effectively what is going on currently in your code. That way, you control the "source of truth" and are always ensuring that anytime your class is asked for or provides the current state, that it is doing so based on being the master of the domain.

Replies

Any ideas? Some new ideas to look into would be a huge help.


I also have a bounty on the Stackoverflow post.

When the automatic track change occurs, is this using an AVQueuePlayer, or some other means?

I am using an AVPlayer and have an observer on AVPlayerItemDidPlayToEndTimeNotification that triggers the track change including the call on setTrackNumber.


Looks like I've found the problem:

func playerTimeJumped() { 
    let currentTime = currentItem().currentTime() 
    dispatch_async(dispatch_get_main_queue()) { 
        MPNowPlayingInfoCenter.defaultCenter().nowPlayingInfo?[MPNowPlayingInfoPropertyElapsedPlaybackTime] = CMTimeGetSeconds(currentTime) 
    } 
} 


NSNotificationCenter.defaultCenter().addObserver( self, 
        selector: "playerTimeJumped", 
        name: AVPlayerItemTimeJumpedNotification, 
        object: nil)


I thought that since both updates to nowPlayingInfo were done on the main queue, they wouldn't interfere with each other. How is it that they do? I have the playerTimeJumped method so that the lock screen info will update when the user scrubs or skips forward/back. The best solution I have found is to explicitly update the elapsed playback time on srub and separately on skip rather than using AVPlayerItemTimeJumpedNotification to capture both. Is there a cleaner way? (Also, it's not ideal because a track change caused by a skip still fails: both AVPlayerItemTimeJumpedNotification and AVPlayerItemDidPlayToEndTimeNotification fire in that case.)

It feels to me like you probably have a race condition here, where you have a couple of events occuring at about the same time where you are settting entirely new info, but also updating the old info, and they probably aren't always happening in the order you expect. The fact that adding a sleep() to the function setting the new info resolves the issue points to this.


I suspect you need to find a strategy to modify your code so you aren't potentially getting old info with the playerTimeJumped() method where you just are updating the one property in the dictionary.


Another option would be to maintain a dictionary with the current info in your model object, and always set the entire dictionary, so that you're never potentially retrieving stale info with the MPNowPlayingInfoCenter.defaultCenter().nowPlayingInfo getter.

Thank you very much for the helpful ideas!


For my own mental model and to try to avoid this kind of thing in the future -- I expected that as long as all my sets or modifications were done on the main thread, that they would be done one after the other. Does the fact that they're not have to mean that the nowPlayingInfo setter does something asynchronous? If so, is this documented anywhere?

One thing to keep in mind is that dispatch_async causes the block of code to be executed on the desired thread, scheduled at some point in the future. And for a serial queue, it is FIFO, but it would be on a subsequent run loop.


I'm looking into whether or not the setter is asynchronous, but I think maintaining your own dictionary and setting all of the values correctly will always ensure that you are setting the data you intend to, as opposed to fetching the current dictionary, changing one value, and setting it, which is effectively what is going on currently in your code. That way, you control the "source of truth" and are always ensuring that anytime your class is asked for or provides the current state, that it is doing so based on being the master of the domain.

Yes one would assume that nowPlayingInfo was a Singleton which could be used to reliably hold its own source of truth, but it loses its values when switching between threads. This either shouldn't happen, or it should be documented that it does happen.


The annoying part is like most multithreading issues, it worked most of the time, until it didnt, and I lost a lot of hours trying to fix it.