Home > Back-end >  How to interrupt device audio to play a sound and then let the device audio continue on iOS
How to interrupt device audio to play a sound and then let the device audio continue on iOS

Time:03-30

I have an app where the user can play voice messages received from other users. Playing the voice messages should interrupt device audio (music, podcast, etc playing from other apps), play the voice messages and then let the device audio continue.

Here is a use specific use case I am trying to achieve

  • the user starts playing music on the device via Apple Music
  • the user opens the app and taps a voice message
  • the Apple Music stops
  • voice message in the app plays
  • Apple Music continues playing

With setting AVAudioSessions category to .ambient I can play the voice message "over" the playing Apple Music, but that is not what I need exactly.

If I use the .playback category that makes the Apple Music stop, plays the voice message in the app but Apple Music does not continue playing afterwards.

CodePudding user response:

I think those apps that should continue like Apple Music, Spotify, Radio apps etc implement the functionality to handle interruptions and when another app's audio is deactivated / wants to hand back responsibility of the audio.

So could you try and see if this works

// I play the audio using this AVAudioPlayer
var player: AVAudioPlayer?

// Implement playing a sound
func playSound() {
    
    // Local url for me, but you could configure as you need
    guard let url = Bundle.main.url(forResource: "se_1",
                                    withExtension: "wav")
    else { return }
    
    do {
        // Set the category to playback to interrupt the current audio
        try AVAudioSession.sharedInstance().setCategory(.playback,
                                                        mode: .default)
        
        try AVAudioSession.sharedInstance().setActive(true)
        
        player = try AVAudioPlayer(contentsOf: url)
        
        // This is to know when the sound has ended
        player?.delegate = self
        
        player?.play()
        
    } catch let error {
        print(error.localizedDescription)
    }
}

extension AVAudioInterruptVC: AVAudioPlayerDelegate {
    
    func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer,
                                     successfully flag: Bool) {
        
        do {
            
            // When the sound has ended, notify other apps
            // You can do this where ever you want, I just show the
            // example of doing it when the audio has ended
            try AVAudioSession.sharedInstance()
                .setActive(false, options: [.notifyOthersOnDeactivation])
        }
        catch {
            print(error)
        }
        
    }
}

CodePudding user response:

You've already discovered that you can interrupt other audio apps by activating a .playback category audio session. When you finish playing your audio and want the interrupted audio to continue, deactivate your audio session and pass the notifyOthersOnDeactivation option.

e.g.

try! audioSession.setActive(false, options: .notifyOthersOnDeactivation)

CodePudding user response:

In theory, Apple has provided a "protocol" for interrupting and resuming background audio, and in a downloadable example, I show you what it is and prove that it works:

https://github.com/mattneub/Programming-iOS-Book-Examples/tree/master/bk2ch14p653backgroundPlayerAndInterrupter

In that example, there are two projects, representing two different apps. You run both of them simultaneously. BackgroundPlayer plays sound in the background; Interrupter interrupts it, pausing it, and when it is finished interrupting, BackgroundPlayer resumes.

This, as you will see, is done by having Interrupter change its audio session category from ambient to playback while interrupting, and changing it back when finished, along with first deactivating itself entirely while sending the .notifyOthersOnDeactivation signal:

func playFile(atPath path:String) {
    self.player?.delegate = nil
    self.player?.stop()
    let fileURL = URL(fileURLWithPath: path)
    guard let p = try? AVAudioPlayer(contentsOf: fileURL) else {return} // nicer
    self.player = p
    // error-checking omitted
    
    // switch to playback category while playing, interrupt background audio
    try? AVAudioSession.sharedInstance().setCategory(.playback, mode:.default)
    try? AVAudioSession.sharedInstance().setActive(true)
    
    self.player.prepareToPlay()
    self.player.delegate = self
    let ok = self.player.play()
    print("interrupter trying to play \(path): \(ok)")
}

func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) { // *
    let sess = AVAudioSession.sharedInstance()
    // this is the key move
    try? sess.setActive(false, options: .notifyOthersOnDeactivation)
    // now go back to ambient
    try? sess.setCategory(.ambient, mode:.default)
    try? sess.setActive(true)
    delegate?.soundFinished(self)
}

The trouble, however, is that response to .notifyOthersOnDeactivation is entirely dependent on the other app being well behaved. My other app, BackgroundPlayer, is well behaved. This is what it does:

self.observer = NotificationCenter.default.addObserver(forName:
    AVAudioSession.interruptionNotification, object: nil, queue: nil) {
        [weak self] n in
        guard let self = self else { return } // legal in Swift 4.2
        let why = n.userInfo![AVAudioSessionInterruptionTypeKey] as! UInt
        let type = AVAudioSession.InterruptionType(rawValue: why)!
        switch type {
        case .began:
            print("interruption began:\n\(n.userInfo!)")
        case .ended:
            print("interruption ended:\n\(n.userInfo!)")
            guard let opt = n.userInfo![AVAudioSessionInterruptionOptionKey] as? UInt else {return}
            let opts = AVAudioSession.InterruptionOptions(rawValue: opt)
            if opts.contains(.shouldResume) {
                print("should resume")
                self.player.prepareToPlay()
                let ok = self.player.play()
                print("bp tried to resume play: did I? \(ok as Any)")
            } else {
                print("not should resume")
            }
        @unknown default:
            fatalError()
        }
}

As you can see, we register for interruption notifications, and if we are interrupted, we look for the .shouldResume option — which is the result of the interrupter setting the notifyOthersOnDeactivation in the first place.

So far, so good. But there's a snag. Some apps are not well behaved in this regard. And the most non-well-behaved is Apple's own Music app! Thus it is actually impossible to get the Music app to do what you want it to do. You are better off using ducking, where the system just adjusts the relative levels of the two apps for you, allowing the background app (Music) to continue playing but more quietly.

  • Related