MPNowPlayingInfoCenter nowPlayingInfo not updating at end of track
Asked Answered
S

6

15

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?

Sharpedged answered 19/1, 2016 at 1:56 Comment(9)
Has anyone else experienced anything like this?Interdiction
I'm speculating here but MPNowPlayingInfoCenter must presumably dispatch nowPlayingInfo changes to whatever system process manages the lock screen. So I wouldn't be at all surprised at your debug output... the setNowPlayingInfo method is effectively completing asynchronously.Twice
When the track changes automatically, is setTrackNumber definitely being called every time? Even when the app is backgrounded?Twice
@ReubenScratton - agreed that setting nowPlayingInfo is likely done asynchronously so the prints are not all that helpful. But I'd expect the set to complete at some point! When the phone is showing the lockscreen, I know setTrackNumber is always called because my debug prints show up on the console.Interdiction
@HélèneMartin Not sure if it's related. Just wondering if you open "Background Modes" in "Capabilities"?Offbeat
@Offbeat - yes, I have background audio enabled and it works great!Interdiction
Just to clarify for a little more insight; what happens when, on the locked screen, the user presses the pause button? Also, when viewing the lock screen at the time when a new track is played (automatically after a track finishes playing), what is displayed on the lock screen? does the next track continue to playing?Pantie
All the buttons on the lock screen work as expected. For example, the pause button correctly pauses the currently playing track. When a new track is played automatically, the audio changes correctly but the lock screen displays the previous track's information.Interdiction
@HélèneMartin look at my answer...Soapy
S
11

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.

Sharpedged answered 12/2, 2016 at 12:3 Comment(0)
O
1

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")
        }
    }
Offbeat answered 4/2, 2016 at 13:30 Comment(3)
I do #2. #3 should not be directly related since it has to do with remote control events but yes, I do handle remote control events correctly. #1 and self.becomeFirstResponder() I don't totally understand. I think it's so that a view can receive remote control events? The setTrackNumber method is in a class that wraps AVPlayer and that I use a singleton of throughout my views.Interdiction
@HélèneMartin Yeah, I know what you mean. That's what I thought at first; however, after I check Event Handling Guide for iOS from Apple (which also contains topic for responder chains). I found the topic "Providing Now Playing Information" is under the section "Remote Control Events" developer.apple.com/library/ios/documentation/EventHandling/… Therefore, I think probably background info update might serve for Remote Control at its design concept.Offbeat
@HélèneMartin Also if you are testing it in iOS Simulator, always include the MPNowPlayingInfoPropertyPlaybackRate key in your nowPlayingInfo dictionary according to the document.Offbeat
S
1

Could you try this code? This worked for my example...

override func viewDidLoad() {
super.viewDidLoad()

if NSClassFromString("MPNowPlayingInfoCenter") != nil {
    let albumArt = MPMediaItemArtwork(image: image) // any image
    var songInfo: NSMutableDictionary = [
        MPMediaItemPropertyTitle: "Whatever",
        MPMediaItemPropertyArtist: "Whatever",
        MPMediaItemPropertyArtwork: albumArt
    ]
    MPNowPlayingInfoCenter.defaultCenter().nowPlayingInfo = songInfo as [NSObject : AnyObject]
}
try! AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryPlayback, withOptions: [])
   try! AVAudioSession.sharedInstance().setActive(true)
}

Explanation: You have to check if the MPNowPlayingInfo is active due at some time it is going into background. If it is in the background then you have to make it active which does this line of code:

try! AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryPlayback, withOptions: [])
       try! AVAudioSession.sharedInstance().setActive(true)

Write me if that worked...

Edit

If this doesn't work you could also try this code but the code above is the more modern solution

if (AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryPlayback, error: nil)) {
    println("Receiving remote control events")
    UIApplication.sharedApplication().beginReceivingRemoteControlEvents()
} else {
    println("Audio Session error.")
}

Here you are also trying to make it active, the same as above. This is a old version which might not work...

Soapy answered 8/2, 2016 at 0:49 Comment(1)
I do set the session's category and make it active. Please note that the track change as triggered automatically by the end of the previous track never updates the nowPlayingInfo correctly, even when the app is in the foreground.Interdiction
M
1

First off, definitely enable background modes for your app in the .plist file. This will allow your app to use background tasks and run your update code while locked.

Secondly, I would let the update function get called by the AVAudioPlayer delegate function audioPlayerDidFinishPlaying:successfully: if you want to update it at the proper time. Also you could try registering for a notification that it is finished as an alternative.

Mechling answered 10/2, 2016 at 22:3 Comment(1)
Background modes are enabled and work correctly. I use an AVPlayer and the call on setTrackNumber is triggered by AVPlayerItemDidPlayToEndTimeNotification. As you can see in my revision, the problem is that two different notifications can sometimes trigger an update on nowPlayingInfo at the same time.Interdiction
A
0

I can't comment on the above answer, but when using MPRemoteCommandCenter it is not necessary to call -[UIApplication beginReceivingRemoteControlEvents] nor -[UIResponder becomeFirstResponder] to handle remote events. The above answer is referring to an older implementation that is no longer recommended.

It is recommended to set as many keys as possible in the nowPlayingInfo dictionary. MPMediaItemPropertyPlaybackDuration, MPNowPlayingInfoPropertyPlaybackRate, and MPNowPlayingInfoPropertyElapsedPlaybackTime can affect whether MPNowPlayingInfoCenter updates.

Axolotl answered 7/2, 2016 at 22:9 Comment(1)
That is correct -- I use the new implementation for remote control events and they are working correctly. I set all the nowPlayingInfo keys I have information for including all the ones you mention.Interdiction
G
0

Here is my case:

The AVAudioSession matters.


not this:

let audioSession = AVAudioSession.sharedInstance()
        do {
            try audioSession.setCategory(AVAudioSession.Category.playback, options: AVAudioSession.CategoryOptions.mixWithOthers)

            audioSession.requestRecordPermission({ (isGranted: Bool) in  })

            try AVAudioSession.sharedInstance().setActive(true, options: AVAudioSession.SetActiveOptions.notifyOthersOnDeactivation)

        } catch  {

        }

It doesn't work


but

       do {
            //keep alive audio at background
            try AVAudioSession.sharedInstance().setCategory(AVAudioSession.Category.playback)
        } catch _ { }

        do {
            try AVAudioSession.sharedInstance().setActive(true)
        } catch _ { }

It works

Gooch answered 11/5, 2020 at 5:47 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.