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?
EDIT
After going through the code for the Nth time thinking "concurrency", I've found the culprit. I don't know why I didn't get suspicious about this earlier:
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)
This code updates the lock screen's time elapsed when the user scrubs or skips forward/back. If I comment it out, the nowPlayingInfo
update from setTrackNumber
works as expected under any condition.
Revised questions: how are these two pieces of code interacting when they're both run on the main queue? Is there any way I can do a nowPlayingInfo
update on AVPlayerItemTimeJumpedNotification
given that there will be a jump when there's a call on setTrackNumber
?
The problem is that nowPlayingInfo
is updated in two places at the same time when the track automatically changes: in the setTrackNumber
method which is triggered by AVPlayerItemDidPlayToEndTimeNotification
and in the playerTimeJumped
method which is triggered by AVPlayerItemTimeJumpedNotification
.
This causes a race condition. More details are provided by an Apple staff member here.
The problem can be solved by keeping a local nowPlayingInfo
dictionary that gets updated as needed and always setting MPNowPlayingInfoCenter.defaultCenter().nowPlayingInfo
from that instead of setting individual values.