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)
}
Related
I'm working on a voip app now and want to support holding.
But when a second call comes and I hold my current call. Switching to my first call I hear no sound at all.
The way to hear it is to navigate from callKit native screen to my app and hence I can hear the voice.
func configureAudioSession() {
_ = try? AVAudioSession.sharedInstance().setCategory(AVAudioSession.Category.playAndRecord, mode: .videoChat, options: AVAudioSession.CategoryOptions.mixWithOthers)
_ = try? AVAudioSession.sharedInstance().overrideOutputAudioPort(AVAudioSession.PortOverride.none)
_ = try? AVAudioSession.sharedInstance().setMode(AVAudioSession.Mode.voiceChat)
}
func startAudio() {
print("Starting audio")
do {
_ = try AVAudioSession.sharedInstance().setActive(true)
} catch {
}
}
func stopAudio() {
print("Stopping audio")
do {
_ = try AVAudioSession.sharedInstance().setActive(false)
} catch {
}
}
For supporting holding, you dont have to start/stop audio session, instead you can use CXSetHeldCallAction provided by Callkit itself. Here, is the code for hold that i use.
let callKitCallController = CXCallController()
func performHoldAction(isOnHold:Bool, uuid:UUID) {
let holdCallAction = CXSetHeldCallAction(call: uuid, onHold: isOnHold)
let transaction = CXTransaction(action: holdCallAction)
callKitCallController.request(transaction) { error in
if let error = error {
CPrint("holdCallAction transaction request failed: \(error.localizedDescription).")
return
}
CPrint("holdCallAction transaction request successful")
}
}
Once system puts the call on hold(by above method OR due to other incoming call accepting or any other reason), then in CXProviderDelegate, the method func provider(_ provider: CXProvider, perform action: CXSetHeldCallAction) gives you the callback for the detail.
Here, system/callkit itself interacts with audio, you dont have to explicitly start or stop audio for holding.
Note: Make sure that you given supportsHolding to true for the CXCallUpdate that you gave for new call.
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
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 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.
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.