12

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

msmialko
  • 1,439
  • 2
  • 20
  • 35

2 Answers2

6

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
Mohmmad S
  • 5,001
  • 4
  • 18
  • 50
  • Thanks Tobi. So when I've overriden category to `.speaker`, is there any way to check if headphones are still plugged in? – msmialko Sep 24 '18 at 08:42
  • Why would you check ? They OS automatically does that for you and detect the code above transmit audio from speaker to headset if there was a head set smoothly and if not no problem would occur although you can use that code and then check if output source is headset some work around like this – Mohmmad S Sep 24 '18 at 08:44
  • so steps would be, changing the output with that code then checking if there was a headset or not by checking the output source since this changes the source to headset or internal phone speaker – Mohmmad S Sep 24 '18 at 08:47
  • ill edit my answer to add if headphone connected for you @msmialko – Mohmmad S Sep 24 '18 at 09:00
  • you can simply check if headset connected or not like this and do whatever. – Mohmmad S Sep 24 '18 at 09:04
  • 1
    the problem with that code is that it check whether headphones are currently an active output. If I override output to `.speaker`, `currentRoute.outputs` won't contain headphones - even though they're plugged in. – msmialko Sep 24 '18 at 11:54
  • i updated an extension use it and you can check if headphone are connected 100% working i tried it on an app – Mohmmad S Sep 24 '18 at 11:55
  • @msmialko with that extension it should give you if headphone are connected or not not matter what you do, just ask for Session.isHeadphonesConnected , and you are good to go – Mohmmad S Sep 24 '18 at 12:00
  • 1
    `isHeadphonesConnected` returns always false when I previously called `audioSession.overrideOutputAudioPort(.speaker)`. – msmialko Sep 24 '18 at 14:10
  • I have tried it, on my device perhaps something preventing on ur case try some work around with the code up there this is the solution perhaps I digged deep to come up with this and in my opinion probably this is the only way – Mohmmad S Sep 24 '18 at 14:16
  • Beware try this on real device please – Mohmmad S Sep 24 '18 at 14:17
  • i've uploaded a test on how to use it on gitHub try this link https://github.com/mohmmadsawalha96/AVITest – Mohmmad S Sep 24 '18 at 14:28
  • If the device is using AirPods or another bluetooth like headset, for the AVAudioSessionPortDescription extension -> isHeadphones copmuted property, I think it should do something like: var isHeadphones: Bool { portType == AVAudioSession.Port.headphones || portType == AVAudioSession.Port.builtInReceiver } to account for bluetooth devices – C0D3 Jul 15 '23 at 17:30
  • But my current issue is, even though I set overrideOutputAudioPort(.none), my app is still using the .speaker output even though I have my AirPods connected and the device is definitely recognizing them. I test by switching to a music app, and music is playing from Airpods, then I switch back to my app, play the sound and its using the speaker. Not sure how to move playback to headphones once its set to speaker somehow. – C0D3 Jul 15 '23 at 17:33
  • Update: I think I was incorrect to use builtInReceiver. I think it should be .bluetoothA2DP for AirPods at least. – C0D3 Jul 15 '23 at 17:54
0

It's a fair question and I've been dabbling with this myself. Here is what I found that worked best from my testing with some AirPod Pros that I have (I hope/assume other bluetooth headsets would work the same way).

import AVFoundation

let session = AVAudioSession.sharedInstance()
do {
    try session.setCategory(.playAndRecord, mode: .default, options: [.allowBluetoothA2DP, .allowBluetooth, .defaultToSpeaker])
    try session.setActive(true, options: .notifyOthersOnDeactivation)
} catch {
    debugPrint("error: \(error.localizedDescription)")
}

This handles what you're looking for. I also set the volume on the audioPlayer to 110 (100 or a lower number would be okay too. Just depends what you're looking for). It makes a huge difference when playing through the speakers.

do {
    self.audioPlayer = try AVAudioPlayer(contentsOf: lastRecordedAudioURL)
    self.audioPlayer.delegate = self
    self.audioPlayer.volume = 110
} catch {
    debugPrint("error: \(error.localizedDescription)")
}
C0D3
  • 6,440
  • 8
  • 43
  • 67