I use AVAudioPlayer to play audio. I have background audio enabled and the audio sessions are configured correctly.
I implemented the audioSessionGotInterrupted method to be informed if the audio session gets interrupted. This is my current code:
#objc private func audioSessionGotInterrupted(note: NSNotification) {
guard let userInfo = note.userInfo,
let typeValue = userInfo[AVAudioSessionInterruptionTypeKey] as? UInt,
let type = AVAudioSessionInterruptionType(rawValue: typeValue) else {
return
}
if type == .began {
print("interrupted")
// Interruption began, take appropriate actions
player.pause()
saveCurrentPlayerPosition()
}
else if type == .ended {
if let optionsValue = userInfo[AVAudioSessionInterruptionOptionKey] as? UInt {
let options = AVAudioSessionInterruptionOptions(rawValue: optionsValue)
if options == .shouldResume {
print("restored")
// Interruption Ended - playback should resume
setupPlayer()
player.play()
} else {
// Interruption Ended - playback should NOT resume
// just keep the player paused
}
}
}
}
Now I do the following:
Play some audio
Lock the phone
Pause the audio
Wait for some seconds until I see in the XCode debugger that the app has been stopped in background
I hit play in the lockscreen
My commandCenter play() methods gets called as expected. However also the audioSessionGotInterrupted method gets called with type == .began.
How is that possible? I expect to see no notification of that kind or at least .ended
I use iOS 10 beta 8.
Check this
https://developer.apple.com/documentation/avfoundation/avaudiosession/1616596-interruptionnotification
Starting in iOS 10, the system will deactivate the audio session of most apps in response to the app process being suspended. When the app starts running again, it will receive an interruption notification that its audio session has been deactivated by the system. This notification is necessarily delayed in time because it can only be delivered once the app is running again. If your app's audio session was suspended for this reason, the userInfo dictionary will contain the AVAudioSessionInterruptionWasSuspendedKey key with a value of true.
If your audio session is configured to be non-mixable (the default behavior for the playback, playAndRecord, soloAmbient, and multiRoute categories), it's recommended that you deactivate your audio session if you're not actively using audio when you go into the background. Doing so will avoid having your audio session deactivated by the system (and receiving this somewhat confusing notification).
if let reason = AVAudioSessionInterruptionType(rawValue: reasonType as! UInt) {
switch reason {
case .began:
var shouldPause = true
if #available(iOS 10.3, *) {
if let _ = notification.userInfo?[AVAudioSessionInterruptionWasSuspendedKey] {
shouldPause = false
}
}
if shouldPause {
self.pause()
}
break
case .ended:
break
}
}
While the answer above is not wrong it still caused a lot of trouble in my app and a lot of boilerplate code for checking multiple cases.
If you read the description of AVAudioSessionInterruptionWasSuspendedKey it says that the notification is thrown if you didn't deactivate your audio session before your app was sent to the background (which happens every time you lock the screen)
To solve this issue you simply have to deactivate your session if there is no sound playing when app is sent to background and activate it back if the sound is playing. After that you will not receive the AVAudioSessionInterruptionWasSuspendedKey notification.
NotificationCenter.default.addObserver(forName: UIApplication.willResignActiveNotification, object: nil, queue: .main) { sender in
guard self.player.isPlaying == false else { return }
self.setSession(active: false)
}
NotificationCenter.default.addObserver(forName: UIApplication.didBecomeActiveNotification, object: nil, queue: .main) { sender in
guard self.player.isPlaying else { return }
self.setSession(active: true)
}
func setSession(active: Bool) -> Bool {
let session = AVAudioSession.sharedInstance()
do {
try session.setCategory(.playback, mode: .default)
try session.setActive(active)
return true
} catch let error {
print("*** Failed to activate audio session: \(error.localizedDescription)")
return false
}
}
Note: Activating session is probably not necessary because it is handled by Apple's internal playback classes (like AVPlayer for example) but it is a good practice to do it manually.
Related
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.
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.
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)
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)
}
}
}
in my application I have one requirement. I am running my application and playing one video using AVPlayerViewcontroller. In middle I received call and I had answer the call and my video will pause.After 5 seconds I will get new url from server to play in AVPlayerViewcontroller. That time new url is playing in background can able to hear the sound along with phone call. In this scenario I want to send phone app to background and want to see the video which is playing in avplayer.
Please let me know is there any way to achieve this.
SWIFT:
Observe for Interruption Notifications
func setupNotifications() {
let notificationCenter = NotificationCenter.default
notificationCenter.addObserver(self,
selector: #selector(handleInterruption),
name: .AVAudioSessionInterruption,
object: nil)
}
Respond to Interruption Notifications
#objc func handleInterruption(notification: Notification) {
guard let userInfo = notification.userInfo,
let typeValue = userInfo[AVAudioSessionInterruptionTypeKey] as? UInt,
let type = AVAudioSessionInterruptionType(rawValue: typeValue) else {
return
}
if type == .began {
// Interruption began, take appropriate actions
}
else if type == .ended {
if let optionsValue = userInfo[AVAudioSessionInterruptionOptionKey] as? UInt {
let options = AVAudioSessionInterruptionOptions(rawValue: optionsValue)
if options.contains(.shouldResume) {
// Interruption Ended - playback should resume
} else {
// Interruption Ended - playback should NOT resume
}
}
}
}
Apple documentation
I have a workout app that plays short clips of sound every couple of seconds. I have background music enabled so that music from other apps can be played while working out. The problem arises when I get a remote push notification (in my case, Slack) that has a sound, which somehow cancels out my duckingOther audio session and the music from other apps becomes loud again.
Question - How do I reset my duckingOthers audiosession when the user gets this type of interruption?
I set the audio session by calling the below function in didFinishLaunchingWithOptions:
private func setupAudioSession(){
do {
try AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryPlayback, with: [.mixWithOthers, .duckOthers, .interruptSpokenAudioAndMixWithOthers])
print("AVAudioSession Category Playback OK")
do {
try AVAudioSession.sharedInstance().setActive(true)
print("AVAudioSession is Active")
} catch let error as NSError {
print(error.localizedDescription)
}
} catch let error as NSError {
print(error.localizedDescription)
}
}
I have tried treating this as a hard interruption (for example a phone call), but when trying to apply the techniques used for this type of interruption, it seems that remote push notifications pass through the cracks. Below is what I used from a different question to try and catch interruptions.
#objc func handleInterruption(notification: Notification) {
guard let userInfo = notification.userInfo,
let typeValue = userInfo[AVAudioSessionInterruptionTypeKey] as? UInt,
let type = AVAudioSessionInterruptionType(rawValue: typeValue) else {
return
}
if type == .began {
// Interruption began, take appropriate actions
print("interruption started")
}
else if type == .ended {
if let optionsValue = userInfo[AVAudioSessionInterruptionOptionKey] as? UInt {
let options = AVAudioSessionInterruptionOptions(rawValue: optionsValue)
if options.contains(.shouldResume) {
// Interruption Ended - playback should resume
print("should resume playback")
setupAudioSession()
} else {
// Interruption Ended - playback should NOT resume
print("should not resume playback")
}
}
}
}
func setupNotifications() {
let notificationCenter = NotificationCenter.default
notificationCenter.addObserver(self,
selector: #selector(handleInterruption),
name: .AVAudioSessionInterruption,
object: nil)
}
My audio starts and stops as I would expect. When I background the app the music keeps playing, if I activate Siri, the music interrupts but then resumes as I would expect.
The issue I have is that, if my sounds are playing in the background, and I start up Apple Music or Podcasts, the audio mixes together which I don't want however if I use Siri, my audio stops then resumes after.
I want my music to stop and the other to take control of the audio just like it does with Siri. I have tried removing .mixWithOthers but when I do that, it seems that once I background my app and I start Siri, afterwards my audio is no longer able to start again even though the code within the .ended case is called.
func commonInit() {
try? AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryPlayback, with: .mixWithOthers)
NotificationCenter.default.addObserver(self, selector: #selector(handleInterruption), name: .AVAudioSessionInterruption, object: nil)
}
var shouldResume: Bool = false
#objc func handleInterruption(_ notification: Notification) {
guard let info = notification.userInfo,
let typeValue = info[AVAudioSessionInterruptionTypeKey] as? UInt,
let type = AVAudioSessionInterruptionType(rawValue: typeValue)
else { return }
switch type {
case .began:
player?.pause()
case .ended:
guard let optionsValue = info[AVAudioSessionInterruptionOptionKey] as? UInt
else { return }
let options = AVAudioSessionInterruptionOptions(rawValue: optionsValue)
if options.contains(.shouldResume) {
player?.play()
}
}
}
Ideally I want my app to resume after any interruptions, but I also want my app to stop playing if there are any interruptions.
Thanks
Adding UIApplication.shared.beginReceivingRemoteControlEvents() after setting the category fixed this issue but I'm not sure why.
I need to start playing sound when user closes app. I use applicationDidEnterBackground method. Here it is:
func applicationDidEnterBackground(application: UIApplication) {
let dispatchQueue =
dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)
dispatch_async(dispatchQueue, {[weak self] in
var audioSessionError: NSError?
let audioSession = AVAudioSession.sharedInstance()
NSNotificationCenter.defaultCenter().addObserver(self!,
selector: "handleInterruption:",
name: AVAudioSessionInterruptionNotification,
object: nil)
audioSession.setActive(true, error: nil)
if audioSession.setCategory(AVAudioSessionCategoryPlayback,
error: &audioSessionError){
println("Successfully set the audio session")
} else {
println("Could not set the audio session")
}
let filePath = NSBundle.mainBundle().pathForResource("MySong",
ofType:"mp3")
let fileData = NSData(contentsOfFile: filePath!,
options: .DataReadingMappedIfSafe,
error: nil)
var error:NSError?
/* Start the audio player */
self!.audioPlayer = AVAudioPlayer(data: fileData, error: &error)
/* Did we get an instance of AVAudioPlayer? */
if let theAudioPlayer = self!.audioPlayer{
theAudioPlayer.delegate = self;
if theAudioPlayer.prepareToPlay() &&
theAudioPlayer.play(){
println("Successfully started playing")
} else {
println("Failed to play")
}
} else {
/* Handle the failure of instantiating the audio player */
}
})
}
func handleInterruption(notification: NSNotification){
/* Audio Session is interrupted. The player will be paused here */
let interruptionTypeAsObject =
notification.userInfo![AVAudioSessionInterruptionTypeKey] as! NSNumber
let interruptionType = AVAudioSessionInterruptionType(rawValue:
interruptionTypeAsObject.unsignedLongValue)
if let type = interruptionType{
if type == .Ended{
/* resume the audio if needed */
}
}
}
func audioPlayerDidFinishPlaying(player: AVAudioPlayer!,
successfully flag: Bool){
println("Finished playing the song")
/* The flag parameter tells us if the playback was successfully
finished or not */
if player == audioPlayer{
audioPlayer = nil
}
}
It does not work. After debugging I see that theAudioPlayer.play() returns false. If I run this code for example in applicationDidFinishLaunching it plays sound. I added background mode App plays audio or streams audio/video using AirPlay to my Info.plist What's wrong here?
At a very basic level there are three prerequisites your app should satisfy to play audio in the background:
Music must be playing
Setup ‘Background Modes’ for Audio in your Target Capabilities.
For basic playback, initialise your audio session, and set your audio session category to AVAudioSessionCategoryPlayback. If you don’t, you’ll get the default behaviour.
You should also configure your app to respond to
changes in audio output
audio session interruptions (e.g. phone call)
remote control events (via control centre, or the lock screen)
Check out this Swift example.