AVAudioSession - How to switch between speaker and headphones output - ios

I'm trying to mimic behaviour as in Phone app during calling. You can easily switch output sources from/to speaker or headphones.
I know I can force speaker as an output when headphones are connected by calling:
try! audioSession.setCategory(AVAudioSessionCategoryPlayAndRecord)
try! audioSession.overrideOutputAudioPort(.speaker)
However, when I do that, I don't see any way to detect if headphones are still connected to the device.
I initially thought outputDataSources on AVAudioSession would return all posible outputs but it always returns nil.
Is there something I'm missing

You need to change the outputDataSources, as when you overrode it,
now it contains only the .Speaker option
in the Documentation you can find the solution to this,
If your app uses the playAndRecord category, calling this method with the AVAudioSession.PortOverride.speaker option causes audio to be routed to the built-in speaker and microphone regardless of other settings. This change remains in effect only until the current route changes or you call this method again with the AVAudioSession.PortOverride.none option.
Therefore the audio is routed to the built-in speaker, This change remains in effect only untill the current route changes or you call this method again with .noneOption.
it's not possible to forcefully direct sound to headphone unless an accessory is plugged to headphone jack (which activates a physical switch to direct voice to headphone).
So when you want to switch back to headphone, this should work.
And if there is no headphone connected will switch the output device to the small speaker output on the top of the device instead of the big speaker.
let session: AVAudioSession = AVAudioSession.sharedInstance()
do {
try session.setCategory(AVAudioSessionCategoryPlayAndRecord)
try session.overrideOutputAudioPort(AVAudioSession.PortOverride.none)
try session.setActive(true)
} catch {
print("Couldn't override output audio port")
}
Read about this AVAdioSession/OverrideOutputAudioPort Here.
You can check if headset connected adding this extension,
extension AVAudioSession {
static var isHeadphonesConnected: Bool {
return sharedInstance().isHeadphonesConnected
}
var isHeadphonesConnected: Bool {
return !currentRoute.outputs.filter { $0.isHeadphones }.isEmpty
}
}
extension AVAudioSessionPortDescription {
var isHeadphones: Bool {
return portType == AVAudioSessionPortHeadphones
}
}
And simply use this line of code
session.isHeadphonesConnected

Related

AVAudioSession in bluetooth switches strangely when interrupting playback of other apps

I'm working on a VoIP app and i have handled the interruption of AVAudioSession in normal cases. But i find that in bluetooth mode, when i start a call, the switching process from PlayBack to PlayAndRecord will not complete instantly. There will be a short play in low quality and loud volume during the switching time. After that, the playback will not resume even though the call is over. This phenomenon appears randomly. Mostly at the beginning time, and then everything is OK. But after several times of calling, it happens again, which confuses me a lot.
I guessed at the beginning that it might be the problem of bluetooth category, so i added the options [.allowBluetooth, .allowBluetoothA2DP]. But it did not work. Here's my code.
// setting of AVAudioSession
public static let voipCall: AudioSessionScenario = {
if #available(iOS 10.0, *) {
return AudioSessionScenario("voip", category: .playAndRecord, mode: .voiceChat, options: [.allowBluetooth, .allowBluetoothA2DP])
} else {
return AudioSessionScenario("voip", category: .playAndRecord, mode: .voiceChat, options: [.allowBluetooth])
}
}()
// when user starts a call
AVAudioSession.queue.async {
AVAudioSession.entry(AudioSessionScenario.voipCall)
}
// the entry method does nothing but set the setting of sharedInstance
if #available(iOS 10.0, *) {
try session.setCategory(category, mode: scenario.mode, options: scenario.options)
} else {
AVAudioSession.sharedInstance().perform(NSSelectorFromString("setCategory:withOptions:error:"), with: category, with: scenario.options)
}
I expect that there is no strange play in the switching process and the playback will resume after the call. It works well in normal cases including speaker and wired headset. Bluetooth devices like Bose QC30 or Airpods have the unstable problem. Does anyone know the reason? Thx.

How can i control my headset for my music player?

I am creating an music player iOS app and getting data from firebase. I can able to control play, pause, next and previous in simulator or iPhone. While headset is connect to device play, next and previous functionalities are not working properly.
Here is the code which i've used;
func setupRemoteCommandCenter() {
// Get the shared MPRemoteCommandCenter
let commandCenter = MPRemoteCommandCenter.shared()
// Add handler for Play Command
commandCenter.playCommand.addTarget { event in
player?.play()
print("headset play")
return .success
}
// Add handler for Pause Command
commandCenter.pauseCommand.addTarget { event in
player?.pause()
print("headset pause")
return .success
}
// Add handler for Next Command
commandCenter.nextTrackCommand.addTarget { event in
return .success
}
// Add handler for Previous Command
commandCenter.previousTrackCommand.addTarget { event in
return .success
}
}
And calling setupRemoteCommandCenter function in viewdidload
This document says that to receive player event notifications you need to
begin playing audio
be the "Now playing app"
The definition of "Now playing app" is hard to pin down, but it seems to be any app that has an active, non-mixable audio session and is playing audio (or has very recently played audio, there seems to be a brief grace period here) . One possible non-mixable audio session is:
let session = AVAudioSession.sharedInstance()
do {
try session.setCategory(AVAudioSessionCategoryPlayback)
try session.setActive(true)
} catch let err as NSError {
print("Error setting up non mixable audio session \(err)")
}
p.s. if you want the headset controls to work while the screen is locked (or if you want the lockscreen controls to work for that matter), you will need to add audio to Background Modes, because technically, if the screen is locked then your app has been backgrounded:

AVAudioSession: Some Bluetooth devices are not working properly on my App

I'm developing a swift audio/video and text chat iOS App using AVAudioSession.
Whenever I select to use some Bluetooth devices the sound played on the device is not the App audio stream. They play only the system sound sent by text chat library whenever messages are sent/received instead. It doesn't happen on all Bluetooth devices, on some of them everything works fine. On Builtin Mic and Speaker the App works fine too.
Here are the most important methods from my class to manage the device:
class MyAudioSession
{
private var mAudioSession: AVAudioSession;
init!()
{
self.mAudioSession = AVAudioSession.sharedInstance();
do {
try self.mAudioSession.setActive(false);
try self.mAudioSession.setCategory(AVAudioSessionCategoryPlayAndRecord, withOptions: .AllowBluetooth);
try self.mAudioSession.setMode(AVAudioSessionModeVideoChat);
try self.mAudioSession.setActive(true);
}
catch {
return nil;
}
}
func switchToDevice(device: AVAudioSessionPortDescription!) -> Bool
{
var ret = false;
if (device != nil) {
do {
try self.mAudioSession.setPreferredInput(device);
ret = true;
}
catch {
self.logSwitch(device, error: error);
}
}
return ret;
}
}
I'd like to understand why my App is not working fine on just SOME Bluetooth devices. These same devices work properly on the other Apps on my Cel.
I did another test: I changed all of this for MPVolumeView, and exactly the same issue occurred, so the problem seems to be on audio player.
Could anybody give me a suggestion to fix this ?
Thx.
Jorg,
While this might not be the best answer I have been able to overcome the weird Bluetooth issues. My problem seems to be similar to yours as I too was using:
AVAudioSessionCategoryPlayAndRecord
This was causing issues for me on some Bluetooth devices (not all but some).
What I wound up doing was setting the Category to:
AVAudioSessionCategoryPlayback
Then when ever I needed to record I would switch the Category over to:
AVAudioSessionCategoryRecord
Then back to Playback after completing my recording.
This was the only way at this time I could get a consistent result from switching between the different outputs (Speaker, Headphones, Bluetooth).
Hope that helps some. Guessing this is a bug in the "AVAudioSessionCategoryPlayAndRecord"

Using Spotify/background music with camera open

I have an app that needs to have:
Background music playing while using the app (eg. spotify)
Background music playing while watching movie from AVPlayer
Stop the music when recording a video
Like Snapchat, the camera-viewcontroller is part of a "swipeview" and therefore always on.
However, when opening and closing the app, the music makes a short "crack" noise/sound that ruins the music.
I recorded it here:
https://soundcloud.com/morten-stulen/hacky-sound-ios
(3 occurrences)
I use these settings for changing the AVAudiosession in the appdelegate didFinishLaunchingWithOptions:
do {
try AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryPlayAndRecord,withOptions:
[AVAudioSessionCategoryOptions.MixWithOthers,
AVAudioSessionCategoryOptions.DefaultToSpeaker])
try AVAudioSession.sharedInstance().setActive(true)
} catch {
print("error")
}
I use the LLSimpleCamera control for video recording and I've set the session there to:
_session.automaticallyConfiguresApplicationAudioSession = NO;
It seems others have the same problem with other camera libraries as well:
https://github.com/rFlex/SCRecorder/issues/127
https://github.com/rFlex/SCRecorder/issues/224
This guy removed the audioDeviceInput, but I kinda need that for recording video.
https://github.com/omergul123/LLSimpleCamera/issues/48
I also tried with Apple's code "AvCam", and I still have the same issue. How does Snapchat do this?!
Any help would be greatly appreciated, and I'll gladly provide more info or code!
I do something similar to what you're wanting, but without the camera aspect, but I think this will do what you want. My app allows background audio that will mix with non-fullscreen video/audio. When the user plays an audio file or a full screen video file, I stop the background audio completely.
The reason I do SoloAmbient then Playback is because I allow my audio to be played in the background when the device is locked. Going SoloAmbient will stop all background music playing and then switching to Playback lets my audio play in the app as well as in the background.
This is why you see a call to a method that sets the lock screen information in the Unload method. In this case, it is nulling it out so that there is no lock screen info.
In AppDelegate.swift
//MARK: Audio Session Mixing
func allowBackgroundAudio()
{
do {
try AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryPlayback, withOptions: .MixWithOthers)
} catch {
NSLog("AVAudioSession SetCategory - Playback:MixWithOthers failed")
}
}
func preventBackgroundAudio()
{
do {
//Ask for Solo Ambient to prevent any background audio playing, then change to normal Playback so we can play while locked
try AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategorySoloAmbient)
try AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryPlayback)
} catch {
NSLog("AVAudioSession SetCategory - SoloAmbient failed")
}
}
When I want to stop background audio, for example when playing an audio track that should be alone, I do the following:
In MyAudioPlayer.swift
func playUrl(url: NSURL?, backgroundImageUrl: NSURL?, title: String, subtitle: String)
{
ForgeHelper.appDelegate().preventBackgroundAudio()
if _mediaPlayer == nil {
self._mediaPlayer = MediaPlayer()
_mediaPlayer!.delegate = self
}
//... Code removed for brevity
}
And when I'm done with my media playing, I do this:
private func unloadMediaPlayer()
{
if _mediaPlayer != nil {
_mediaPlayer!.unload()
self._mediaPlayer = nil
}
_controlView.updateForProgress(0, duration: 0, animate: false)
ForgeHelper.appDelegate().allowBackgroundAudio()
setLockScreenInfo()
}
Hope this helps you out!

AVAudioSession overrideOutputAudioPort & localNotifications in mute

Hello I tried the various solutions to similar questions but couldnt get my code to work. I have the following function that I call in my app:
func PlaySound (WhenToPlaySound:String) {
AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryPlayAndRecord, error: nil)
AVAudioSession.sharedInstance().setActive(true, error: nil)
if WhenToPlaySound == "BeginningOfRound" {
if UIApplication.sharedApplication().applicationState == UIApplicationState.Background {
soundnotification.soundName = "BoxingBellStart.wav"
UIApplication.sharedApplication().scheduleLocalNotification(soundnotification)
println("timer is done in background mode")
} else {
// Load Sound
soundlocation = NSBundle.mainBundle().URLForResource("BoxingBellStart", withExtension: "wav")!
player = AVAudioPlayer(contentsOfURL: soundlocation, error: &Error)
player.volume = 1.0
// Play Sound
player.play()
println("timer is done in active mode")
}
} else {
if UIApplication.sharedApplication().applicationState == UIApplicationState.Background {
soundnotification.soundName = "Boxing.wav"
UIApplication.sharedApplication().scheduleLocalNotification(soundnotification)
println("timer is done in background mode")
} else {
// Load Sound
soundlocation = NSBundle.mainBundle().URLForResource("Boxing", withExtension: "wav")!
player = AVAudioPlayer(contentsOfURL: soundlocation, error: &Error)
player.volume = 1.0
// Play Sound
player.play()
println("timer is done in active mode")
}
}
}
Mostly it works except two things:
1. I can't seem to override the speaker volume. I want the system volume to be full volume before I play my sound.
The LocalNotifications that are set to activate when the app is in background mode only pay when the device isn't muted.
To address the first problem I wanted to the following but didnt know how to use it:
AVAudioSession.sharedInstance().overrideOutputAudioPort(<#portOverride: AVAudioSessionPortOverride#>, error: <#NSErrorPointer#>)
Thanks in advance,
Ace
overrideOutputAudioPort affects the audio routing, not the volume.
When you say that you want to "override the speaker volume", I assume that you mean you want to control the system output volume from you app code. This is not possible as Apple believes that output volume should remain in the control of the user at all times.
AVAudioPlayer's volume property sets the volume relative to the system output level. It defaults to 1.0 (player volume == system volume). You can't turn it up higher, spinal-tap style, to 1.1...
See also my answer here ... if you want to take control of the system volume, you will need the user interface provided by MPVolumeView.
Similarly regarding your notifications - if the user has muted the device, your app won't be able to ignore that.
update
regarding notifications, it isn't as straightforward as I suggested. It might work if you set the AVAudioSession category to AVAudioSessionCategoryPlayback (and read the apple docs on this setting).
When using this category, your app audio continues with the Silent switch set to silent or when the screen locks
You may also need to add 'audio' to UIBackgroundModes in your info.plist.

Resources