Headset button not responds in a call on my app

Hi, Team.

We are currently creating a VoIP calling app using pjsip and want to be able to end a call using the headset button while the app is in the middle of a call (AVAudioSession.category == .playAndRecord), but MPRemoteCommand does not receive any events.

After trying various things, We found that the button will respond if the audio output destination is set to the speaker or if .allowBluetoothA2DP is set as an option, but this is not suitable for this use case because audio input and output would be from the device rather than the headset.

=================================================

Problem

Headset button events cannot be received from MPRemoteCommand during a call.

What is expected to happen?

When the headset button is pressed during a call, a handler registered in some MPRemoteCommand is called back.

What does actually happen?

No MPRemoteCommand responds when the headset button is pressed during a call.

Information

Sample code

Echoes back the audio input with a 5-second delay to simulate a phone call. https://github.com/ryu-akaike/HeadsetTalkTest-iOS/

Versions

macOS: Sonoma 14.5

Xcode: 15.3

iPhone: 11

iOS: 17.5.1

=================================================

Thank you.

Ryu Akaike

Answered by DTS Engineer in 799523022

We are currently creating a VoIP calling app using pjsip and want to be able to end a call using the headset button while the app is in the middle of a call (AVAudioSession.category == .playAndRecord), but MPRemoteCommand does not receive any events.

So, I'm going to give two answers here:

1. Short Answer:

Your code is wrong. You're activating the audio session directly (calling "setActive"), which then causes a cascade of failures that create the issue you're seeing. Remove those "setActive" calls, let CallKit activate the session for you, and everything will "just work". See the "Speakerbox" sample app for exactly how this should work.

FYI, Speakerbox is a first class Fake Call App and the first place you should always start with any CallKit issues. If the problem doesn't happen in Speakerbox, then something is wrong with your app, with direct session activation being the most common mistake by far.

2. Long Answer:

CallKit actually relies on a "special" audio session type that is not available through the standard audio type. For example, if you carefully compare the maximum volume of a CallKit/Phone call with the maximum volume of a standard PlayAndRecord session, you'll find that the CallKit call is noticeably louder. This is also why CallKit calls can interrupt the now playing app but CANNOT be interrupted by now playing app. The phone audio session has a higher priority that all other audio.

The problem here is that session still follows the standard audio session rules, which means you cannot activate an audio session while another session is active. By calling "setActive" yourself, you prevented the phone session from activating, which then causes everything else.

Case in point:

We found that the button will respond if the audio output destination is set to the speaker or if .allowBluetoothA2DP is set as an option, but this is not suitable for this

Bluetooth actually has two parallel architectures, "A2DP" (for audio playback) and "HFP" ("Hands Free Profile" for play and record). Any bluetooth device that supports is actually being switched into HFP mode every time a call occurs. In any case, the key point here is that those two profiles are COMPETELY different. A2DP is entirely about "playing audio", so it includes commands (through AVRCP) like "pause", "play", "next track", etc. HFP is entirely about "making phone calls", so it includes commands like "Answer Call", "Hang Up", "Dial Number", etc.

That leads to:

Headset button events cannot be received from MPRemoteCommand during a call.

Yes, that's correct and always has been. MPRemoteCommandCenter handles A2DP commands but phone calls go over HFP, not A2DP. The system doesn't actual have any API an app can use to receive HFP commands (and never has). HFP has always been directly processed by the system which is why, for example, a car head unit can let you directly dial out a number. It sent a "Dial Number" command over HFP and the system does the rest.

CallKit will handle all that for you as well, however, that process is tied to the phone audio session... which never activated properly because you called "setActive".

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

We are currently creating a VoIP calling app using pjsip and want to be able to end a call using the headset button while the app is in the middle of a call (AVAudioSession.category == .playAndRecord), but MPRemoteCommand does not receive any events.

So, I'm going to give two answers here:

1. Short Answer:

Your code is wrong. You're activating the audio session directly (calling "setActive"), which then causes a cascade of failures that create the issue you're seeing. Remove those "setActive" calls, let CallKit activate the session for you, and everything will "just work". See the "Speakerbox" sample app for exactly how this should work.

FYI, Speakerbox is a first class Fake Call App and the first place you should always start with any CallKit issues. If the problem doesn't happen in Speakerbox, then something is wrong with your app, with direct session activation being the most common mistake by far.

2. Long Answer:

CallKit actually relies on a "special" audio session type that is not available through the standard audio type. For example, if you carefully compare the maximum volume of a CallKit/Phone call with the maximum volume of a standard PlayAndRecord session, you'll find that the CallKit call is noticeably louder. This is also why CallKit calls can interrupt the now playing app but CANNOT be interrupted by now playing app. The phone audio session has a higher priority that all other audio.

The problem here is that session still follows the standard audio session rules, which means you cannot activate an audio session while another session is active. By calling "setActive" yourself, you prevented the phone session from activating, which then causes everything else.

Case in point:

We found that the button will respond if the audio output destination is set to the speaker or if .allowBluetoothA2DP is set as an option, but this is not suitable for this

Bluetooth actually has two parallel architectures, "A2DP" (for audio playback) and "HFP" ("Hands Free Profile" for play and record). Any bluetooth device that supports is actually being switched into HFP mode every time a call occurs. In any case, the key point here is that those two profiles are COMPETELY different. A2DP is entirely about "playing audio", so it includes commands (through AVRCP) like "pause", "play", "next track", etc. HFP is entirely about "making phone calls", so it includes commands like "Answer Call", "Hang Up", "Dial Number", etc.

That leads to:

Headset button events cannot be received from MPRemoteCommand during a call.

Yes, that's correct and always has been. MPRemoteCommandCenter handles A2DP commands but phone calls go over HFP, not A2DP. The system doesn't actual have any API an app can use to receive HFP commands (and never has). HFP has always been directly processed by the system which is why, for example, a car head unit can let you directly dial out a number. It sent a "Dial Number" command over HFP and the system does the rest.

CallKit will handle all that for you as well, however, that process is tied to the phone audio session... which never activated properly because you called "setActive".

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

Headset button not responds in a call on my app
 
 
Q