MPRemoteCommandCenter not updating play/pause button to proper state on iOS

So I'm using AVAudioEngine. When playing audio I become the 'now playing' app using MPNowPlayingInfoCenter/MPRemoteCommandCenter APIs.

When configuring MPRemoteCommandCenter I add a play/pause command target via -addTargetWithHandler on the togglePlayPauseCommand property.

Now I also have a play/pause button in my app's UI. When I pause playback from my app's UI (which means I'm the active app, I'm in the foreground), what I do is this:

-I pause the AVAudioPlayerNode I'm using with AVAudioEngine.

I do not, stop, reset, etc. the AVAudioEngine. I only pause the player node. My thought process here is that the user just pressed pause and it is very likely that he will hit 'play' to resume playback in the near future because

  • My app is in the foreground and the user just hit the pause button.

Now if my app moves to the background and if I receive a memory warning I presume it'd make sense to tear down the engine or pause it. Perhaps I'm wrong about this?

So when I initially hit the play button from my app's UI I also activate my AVAudioSession. I do this in high priority NSOperation since the documentation warns that "we recommend that applications not activate their session from a thread where a long blocking operation will be problematic."

So now I'm playing and I hit pause from my app's UI. Then I quickly bring up the "Now Playing" center and I see I'm the "Now Playing" app but the play-pause button is showing the pause icon instead of the play icon but I'm in the pause state. I do set MPNowPlayingInfoCenter's playbackState to MPNowPlayingPlaybackStatePaused when I pause. Not surprisingly this doesn't work. The documentation states this is for macOS only.

So the only way to get MPRemoteCommandCenter to show the "play" image for the play-pause button is to deactivate my AVAudioSession when I pause playback? Since I change the active state of my audio session in a NSOperation because documentation recommends "we recommend that applications not activate their session from a thread where a long blocking operation will be problematic." the play-pause toggle in the remote command center won't immediately update since I'm doing it on another thread.

IMO it feels kind of inappropriate for a play-pause button to wait on a NSOperation activating the audio session before updating its UI when I already know my play/paused state, it should update right away like the button in my app does. Wouldn't it be nicer to just use MPNowPlayingInfoCenter's playbackState property on iOS too? If I'm no the longer the now playing app/active audio session it doesn't matter since I'm not in the now playing UI, just ignore it?

Also is it recommended that I deactivate my audio session explicitly every time the user pauses audio in my app (when I'm in the foreground)?

Also when I do deactivate the audio session I get an error: AVAudioSessionErrorCodeIsBusy (but the button in the now playing center updates to the proper image). I do this :

-(void)pause
{
        [self.playerNode pause];
        [self runOperationToDeactivateAudioSession];

    // This does nothing on iOS:
      MPNowPlayingInfoCenter *nowPlayingCenter = [MPNowPlayingInfoCenter defaultCenter];
        nowPlayingCenter.playbackState = MPNowPlayingPlaybackStatePaused;
}

So in -runOperationToDeactivateAudioSession I get the AVAudioSessionErrorCodeIsBusy. According to the documentation

Starting in iOS 8, if the session has running I/Os at the time that deactivation is requested, the session will be deactivated, but the method will return NO and populate the NSError with the code property set to AVAudioSessionErrorCodeIsBusy to indicate the misuse of the API.

So pausing the player node when pausing isn't enough to meet the deactivation criteria. I guess I have to pause or stop the audio engine. I could probably wait until I receive a scene went to background notification or something before deactivating my audio session (which is async, so the button may not update to the correct image in time). This seems like a lot of code to have to write to get a play-pause toggle to update, especially in iPad-multi window scene environment.

What's the recommended approach?

  • Should I pause the AudioEngine instead of the player node always?

  • Should I always explicitly deactivate my audio session when the user pauses playback from my app's UI even if I'm in the foreground?

I personally like the idea of just being able to set

 [MPNowPlayingInfoCenter defaultCenter].playbackState = MPNowPlayingPlaybackStatePaused;

But maybe that's because that would just make things easier on me. This does feels overcomplicated though. If anyone can share some tips on how I should handle this, I'd appreciate it.

So instead of deactivating the audio session in my pause method I just leave it active and wait this:

// Listen for UIApplicationWillResignActiveNotification
-(void)appWillResignActive:(NSNotification*)notification {
       //  player node paused? 
        if (self.amIPaused) //
        {
              // Then we must audio engine pause.
               [self.audioEngine pause];
             // Then we must deactivate the audio session.
                [self runOperationToDeactivateAudioSession]; 
        }

}

After fiddling with this some more I noticed that

-When I deactivate the audio session after pausing the player node (and get AVAudioSessionErrorCodeIsBusy but the play/pause button in the 'now playing center' does update its image properly)...this has a bad consequence when I resume audio. When I resume the player node I don't hear any playback. My code looks something like this:

-(void)resume
{
      [self runOperationToActivateAudioSession];
       [self.playerNode play];
}

So after the calls -pause (which gives AVAudioSessionErrorCodeIsBusy) and then -resume... when resuming the player node just skips to the end of the buffer instead of resuming playback (I don't hear any audio).

Now if I go back to the pause method and comment out the method call to deactivate the audio session resuming playback works as expected. So it seems deactivating the audio session on pause has this side effect on resume. But if I don't deactivate the session the 'Now Playing' center shows the wrong image for the play/pause button.

I tried pausing the player node and the audio engine:

-(void)pause
{
        [self.playerNode pause];
        [self.audioEngine pause];
        [self runOperationToDeactivateAudioSession];

    // This does nothing on iOS:
      MPNowPlayingInfoCenter *nowPlayingCenter = [MPNowPlayingInfoCenter defaultCenter];
        nowPlayingCenter.playbackState = MPNowPlayingPlaybackStatePaused;
}

The good news? My AVAudioSession -setActive:error: call to deactivate the session no longer gives me a AVAudioSessionErrorCodeIsBusy error. The bad news? When resuming playback, it doesn't play any audio still. The rest of the buffer at the time of the call to -pause is just skipped. No errors reported

So I think the playback time issue (audio being skipped) when I pause both the playerNode and the audio engine is that when resuming the playback...I call a helper method that does something like this:

-(void)resume
{
        if (!self.playerNode.isPlaying)
        {
                [self _startAudioEngineIfNotRunningAndConnectPlayerNodeToMainMixer];
   
         }
        else
     {
          // Already playing.
     }
}

Which has a check like

-(BOOL)_startAudioEngineIfNotRunningAndConnectPlayerNodeToMainMixer
 if (!self.audioEngine.isRunning)
    {
          // connect the player node to the main mixer node and start the engine..
      //code here...
   }
else
{
    // engine already running...
   return YES;
}

If I modify the _startAudioEngineIfNotRunningAndConnectPlayerNodeToMainMixer to include this check:

   NSArray<AVAudioConnectionPoint *> *connections = [engine outputConnectionPointsForNode:playerNode outputBus:0];
        if (connections.count > 0) {
            // player node already connected... only need to start the engine
     }
else{
 // connect the player node...
}

Playback position isn't lost on resume. So that's good. When I deactivated the audio session and got that AVAudioSessionErrorCodeIsBusy error perhaps the system paused the audio session and returned that error to tell me about my "incorrect API use."

In regards to my initial issue regarding the MPRemoteCommandCenter I think what I'm going to do is this:

  1. Only pause the player node in my -pause method and keep the audio session running if my app is in the foreground.
  2. When/if my app is moved to the background if my player node is paused I'll pause the audio engine as well and deactivate the audio session for the sake of getting the 'now playing center' to update the toggle play-pause button image. Since this is async and in a background task I have to hope it runs and updates the now playing center button before the user can see it

I'm still thinking about this: I don't think this is currently possible...but I'm not sure if there is ever a situation where my app can be on screen at the same time as the "Now Playing Center" in a multi-window situation. If so the play-pause toggle button will have the wrong image until my app is backgrounded. So there is that, if that's even possible but I don't think I have any other options?

--

Perhaps my thinking is naive but I think this could be a lot easier if the system just read the MPNowPlayingInfoCenter.playbackState property on iOS instead of inferring it from AVAudioSession. If I don't own the now playing center who cares what I set the .playbackState property to? Feels like the system is in a better position to make a decision of whether or not my audio session should be deactivated than I am. But I have to explicitly deactivate my audio session when my app is backgrounded every time (if my audio is paused).

Accepted Answer

So instead of deactivating the audio session in my pause method I just leave it active and wait this:

// Listen for UIApplicationWillResignActiveNotification
-(void)appWillResignActive:(NSNotification*)notification {
       //  player node paused? 
        if (self.amIPaused) //
        {
              // Then we must audio engine pause.
               [self.audioEngine pause];
             // Then we must deactivate the audio session.
                [self runOperationToDeactivateAudioSession]; 
        }

}
MPRemoteCommandCenter not updating play/pause button to proper state on iOS
 
 
Q