Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Resume AVAudioPlayer after phone call not working

There are some seemingly similar questions already on SO, but after hours of searching and experimenting with my code, I have been unable to find a clear answer. The closest thing I could find was this answer which alludes to a "known bug" 4 years ago, but does not elaborate on how to address it.

I have an audioPlayer class that is playing an audio file with AVAudioPlayer and listening for the AVAudioSessionDelegate methods beginInterruption and endInterruption. I know that endInterruption is not guaranteed so I store a boolean in beginInterruption and handle restarting the audio playback in applicationDidBecomeActive.

This all works perfectly as expected if the phone receives a call and the user declines it or lets it go to voicemail. As soon as my app goes back into the active state, audio playback resumes.

Here's where I'm getting weird behavior: If the user answers the call, once they hang up everything seems to work as expected but no sound comes out through either the headphones or the speakers.

I can verify that the audio is technically playing by printing its volume and currentTime every second, but the sound isn't coming out.

If I wait for 20-40 seconds, the audio suddenly cuts in and becomes audible, as if it had been silently playing in the background.

After more debugging, I noticed that AVAudioSession.sharedInstance().secondaryAudioShouldBeSilencedHint remains true for these 20-40 seconds of silence, before suddenly changing to false and letting the audio play.

I subscribed to the AVAudioSessionSilenceSecondaryAudioHintNotification to see if I could detect this change, but it never gets called, even when .secondaryAudioShouldBeSilencedHint changes from true to false.

I even tried explicitly setting AVAudioSession.sharedInstance().setActive(true) in the method that resumes playback of the audio, but the behavior did not change.

Lastly, I tried setting a timer to delay the resume for 10 seconds after applicationDidBecomeActive, but the behavior did not change.

So, why does it seem that the phone call is not relinquishing control of the audio session back to my app?

Thanks for taking a look!


Code:

AVAudioSession setup in init():

NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(self.handleAudioHintChange), name: AVAudioSessionSilenceSecondaryAudioHintNotification, object: nil)
NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(self.handleAudioInterruption), name: AVAudioSessionInterruptionNotification, object: nil)

do {
  try AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryPlayback, withOptions: AVAudioSessionCategoryOptions.MixWithOthers)
  print("AVAudioSession Category Playback OK")
  do {
    try AVAudioSession.sharedInstance().setActive(true, withOptions: .NotifyOthersOnDeactivation)
    print("AVAudioSession is Active")
  } catch let error as NSError {
    print(error.localizedDescription)
  }
} catch let error as NSError {
  print(error.localizedDescription)
}

Notification handlers:

///////////////////////////////////
// This never gets called :( //////

func handleAudioHintChange(notification: NSNotification) {
  print("audio hint changed")
}
///////////////////////////////////

func handleAudioInterruption(notification: NSNotification) {
  if notification.name != AVAudioSessionInterruptionNotification || notification.userInfo == nil{
    return
  }

  if let typeKey = notification.userInfo [AVAudioSessionInterruptionTypeKey] as? UInt,
  let type = AVAudioSessionInterruptionType(rawValue: typeKey) {
    switch type {
      case .Began:
        print("Audio Interruption Began")
        NSUserDefaults.standardUserDefaults().setBool(true, forKey:"wasInterrupted")

      case .Ended:
        print("Audio Interuption Ended")
    }
  }
}

App Delegate:

func applicationDidBecomeActive(application: UIApplication) {
  if(NSUserDefaults.standardUserDefaults().boolForKey("wasInterrupted")) {
    audioPlayer.resumeAudioFromInterruption()
  }
}

Restart function:

// This works great if the phone call is declined
func resumeAudioFromInterruption() {
  NSUserDefaults.standardUserDefaults().removeObjectForKey("wasInterrupted")
  do {
    try AVAudioSession.sharedInstance().setActive(true)
    print("AVAudioSession is Active")
  } catch let error as NSError {
    print(error.localizedDescription)
  }
  thisFunctionPlaysMyAudio()

}

like image 880
Ivaylo Getov Avatar asked Apr 16 '16 10:04

Ivaylo Getov


3 Answers

I tried to do the same, although I didn't use any options in the setActive method.

There seem to be a bug in iOS 9.3.1 that doesn't resume playback of the AVAudioPlayer after the phone call ends.

Here is a snippet of what solved it for me: (Sorry for the Objective-C)

- (void)handleInterruption:(NSNotification *) notification{
    if (notification.name != AVAudioSessionInterruptionNotification || notification.userInfo == nil) {
        return;
    }

    NSDictionary *info = notification.userInfo;

    if ([notification.name isEqualToString:AVAudioSessionInterruptionNotification]) {

        if ([[info valueForKey:AVAudioSessionInterruptionTypeKey] isEqualToNumber:[NSNumber numberWithInt:AVAudioSessionInterruptionTypeBegan]]) {
            NSLog(@"InterruptionTypeBegan");
        } else {
            NSLog(@"InterruptionTypeEnded");

            //*The* Workaround - Add a small delay to the avplayer's play call; Without the delay, the playback will *not* be resumed
            //
            //(I didn't play much with the times, but 0.01 works with my iPhone 6S 9.3.1)
            dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 0.01 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
                NSLog(@"playing");
                [_player play];
            });
        }
    }
}

I added a small delay to the player's play call, and it worked.

You can find the full Demo project I made here: https://github.com/liorazi/AVAudioSessionWorkaround

I submitted a radar to Apple, hopefully it'll get fixed on the next release.

like image 193
L A Avatar answered Sep 22 '22 06:09

L A


Use this after you're done with Audio.

AVAudioSession.sharedInstance().setActive(false, withOptions: .NotifyOthersOnDeactivation)

like image 21
mswlogo Avatar answered Sep 24 '22 06:09

mswlogo


I had same issue. Try to reload player after interruption:

    func interruptionNotification(_ notification: Notification) {
    guard let type = notification.userInfo?[AVAudioSessionInterruptionTypeKey] as? UInt,
      let interruption = AVAudioSessionInterruptionType(rawValue: type) else {
        return
    }
    if interruption == .ended && playerWasPlayingBeforeInterruption {
      player.replaceCurrentItem(with: AVPlayerItem(url: radioStation.url))
      play()
    }
  }
like image 21
Vitalii Gozhenko Avatar answered Sep 22 '22 06:09

Vitalii Gozhenko