Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

MPNowPlayingInfoCenter nowPlayingInfo not updating at end of track

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?

like image 314
Hélène Martin Avatar asked Jan 19 '16 01:01

Hélène Martin


2 Answers

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.

like image 172
Hélène Martin Avatar answered Nov 18 '22 14:11

Hélène Martin


For background info update, my colleague suggests some implementations are necessary. Maybe you can check and verify some of these requirements in your view controller:

    //1: Set true for canBecomeFirstResponder func
    override func canBecomeFirstResponder() -> Bool {
        return true
    }

    //2: Set view controller becomeFirstResponder & allow to receive remote control events
    override func viewDidLoad() {
        super.viewDidLoad()
        self.becomeFirstResponder()
        UIApplication.sharedApplication().beginReceivingRemoteControlEvents()
        ....
    }

    //3: Implement actions after did receive events from remote control
    override func remoteControlReceivedWithEvent(event: UIEvent?) {
        guard let event = event else {
            return
        }
        switch event.subtype {
        case .RemoteControlPlay:
            ....
            break
        case .RemoteControlPause:
            ....
            break
        case .RemoteControlStop:
            ....
            break
        default:
            print("default action")
        }
    }
like image 1
Allen Avatar answered Nov 18 '22 14:11

Allen