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)
}
}
}
I have an iOS app using AVFoundation and playing audio contents.
Once launched I want it to keep playing, even in the background, until I stop it.
But now as soon as the device goes to sleep or I put the app in the background, it stops playing. How can I fix this and get the behavior I wish.
Here is the kind of code I am using in the app:
var soundSession:AVAudioSession!, audioPlayer: AVAudioPlayer?
.....
soundSession = AVAudioSession.sharedInstance()
.....
do {try soundSession.setCategory(AVAudioSession.Category.playback)
try soundSession.setActive(true)
if audioPlayer == nil {
audioPlayer = try AVAudioPlayer(contentsOf: url,
fileTypeHint: fileTypHint)
guard let _ = audioPlayer else {return}
audioPlayer!.delegate = self
}
audioPlayer?.play()
} catch {
print("Error in \(#function)\n" + error.localizedDescription)
}
I am working on an iOS app, that connects to an iBeacon.
I want to be able to play sounds from the phone, if external parameters match.
I have enabled "Audio, Airplay, and picture in picture." in Background Modes.
Right now I have the issue, that it can only play sounds from the app, while:
App is in foreground.
Screen is locked, and phone is not silent.
I would like it to also be able to play sounds, while:
Locked screen and phone is silent.
Background (when app is started by iBeacon).
Is there any way to achieve this?
The function I use to play sound is:
private var avPlayer: AVAudioPlayer?
// MARK: - Play Sound
internal func playSound(forResource resource: String, ofType type: String) {
guard let path = Bundle.main.path(forResource: resource, ofType: type) else {
print("Test: Failed getting path for \(resource)")
return
}
let url = URL(fileURLWithPath: path)
// Handle playing in background
let session = AVAudioSession.sharedInstance()
do {
try session.setCategory(.playback, options: [.duckOthers])
try session.setActive(true, options: .notifyOthersOnDeactivation)
} catch {
print("Test: Failed setting category on session \(error)")
}
do {
avPlayer = try AVAudioPlayer(contentsOf: url)
} catch let error {
print("Test: Failed adding url to AVAudioPlayer \(error)")
}
avPlayer?.play()
}
And called like:
playSound(forResource: "SoundToPlay", ofType: "wav")
If you set up your app to listen for beacon region notifications then you can trigger sounds to play when you enter or leave a region, even if the phone is locked and/or your app was not running. I'm not sure if those would play if the user has the phone muted (I suspect not)
Ok so Iv'e been trying to search for a solution for the past 2 hours but nothing had my problem fixed.
Solutions Iv'e tried:
- set the Background Modes to ON and check Audio, AirPlay, Picture...
- add to info.plist Required background modes and under that App plays audio using airplay
here's my code:
do {
audioPlayer = try AVAudioPlayer(contentsOf: destinationUrl!)
audioPlayer.prepareToPlay()
let audioSession = AVAudioSession.sharedInstance()
do{
try audioSession.setCategory(AVAudioSessionCategoryPlayback)
}catch{
}
print("starting to play...")
audioPlayer.play()
state = true
started = true
completionHandler()
} catch let error {
print(error)
}
Thanks in advance for anyone trying to help.
I want my background song to stop playing when music from another app is playing. Such as apple music or Spotify. If you download color switch you'll know what I mean. Currently Im using the method below but my background song doesn't stop it mixes with the playing music.(This method kinda works with apple music but not Spotify). I want my song to play indefinitely until other music is playing in background and for it to start again when the other music is paused.(Try it with color Switch)
Thanks
func setUpAudio() {
//dead = SKAction.playSoundFileNamed(SoundFile.dead, waitForCompletion: true)
// buttonpress = SKAction.playSoundFileNamed(SoundFile.button, waitForCompletion: true)
if GameScene.backgroundMusicPlayer == nil {
let backgroundMusicURL = Bundle.main.url(forResource: SoundFile.BackgroundMusic, withExtension: nil)
do {
let theme = try AVAudioPlayer(contentsOf: backgroundMusicURL!)
GameScene.backgroundMusicPlayer = theme
} catch {
// couldn't load file :[
}
GameScene.backgroundMusicPlayer.numberOfLoops = -1
}
if !GameScene.backgroundMusicPlayer.isPlaying {
GameScene.backgroundMusicPlayer.play()
}
let audioSession = AVAudioSession.sharedInstance()
/*try!audioSession.setCategory(AVAudioSessionCategoryAmbient, with: AVAudioSessionCategoryOptions.mixWithOthers
)*/
do { try!audioSession.setCategory(AVAudioSessionCategoryAmbient)
try AVAudioSession.sharedInstance().setActive(true) }
catch let error as NSError { print(error) }
//s audio from other sessions to be ducked (reduced in volume) while audio from this session plays
}
You're session category allows mixing, which is not what you want. Try using AVAudioSessionCategoryPlayback or AVAudioSessionCategorySoloAmbient, as these won't allow your app to mix audio with other apps.
See the docs for more info
You will need to set the AVAudioSession to active.
let _ = try? AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryPlayback)
let _ = try? AVAudioSession.sharedInstance().setActive(true)