Routing audio output between multiple bluetooth devices with iOS using AVAudioSession - ios

Got a video conference app, implementing device selection feature of switching between Earpiece, Speaker, and connected Bluetooth devices. All seems to function appropriately apart from switching between bluetooth devices themselves.
For some reason, only the last connected device gets the audio routed to it, and you can't switch back to the other ones even if they're available in availableInputs of AVAudioSession SharedInstance, using setPreferredInput with OverridePortNone
I tried searching for resolutions but found the same unanswered issue from 5 years ago, I tried doing the same as changing setActive calls but was also unsuccessful.
Following is the test code, which is taken from here:
AVAudioSession *_audioSession = [AVAudioSession sharedInstance];
AVAudioSessionCategoryOptions _incallAudioCategoryOptionsAll = AVAudioSessionCategoryOptionMixWithOthers|AVAudioSessionCategoryOptionAllowBluetooth|AVAudioSessionCategoryOptionAllowAirPlay;
[_audioSession setCategory:AVAudioSessionCategoryPlayAndRecord
withOptions:_incallAudioCategoryOptionsAll
error:nil];
[_audioSession setMode:AVAudioSessionModeVoiceChat error: nil];
RCT_EXPORT_METHOD(setAudioDevice:(NSString *)device
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject) {
BOOL success;
NSError *error = nil;
NSLog(#"[setAudioDevice] - Attempting to set audiodevice as: %#", device);
if ([device isEqualToString:kDeviceTypeSpeaker]) {
success = [_audioSession overrideOutputAudioPort:AVAudioSessionPortOverrideSpeaker error:&error];
} else {
AVAudioSessionPortDescription *port = nil;
for (AVAudioSessionPortDescription *portDesc in _audioSession.availableInputs) {
if ([portDesc.UID isEqualToString:device]) {
port = portDesc;
break;
}
}
if (port != nil) {
[_audioSession overrideOutputAudioPort:AVAudioSessionPortOverrideNone error:nil];
success = [_audioSession setPreferredInput:port error:&error];
if(error != nil)
{
NSLog(#"setAudioDevice %# %#", error.localizedDescription, error);
}
} else {
success = NO;
error = RCTErrorWithMessage(#"Could not find audio device");
}
}
if (success) {
resolve(#"setAudioDevice success!");
NSLog(#"resolved success");
} else {
reject(#"setAudioDevice", error != nil ? error.localizedDescription : #"", error);
NSLog(#"sent reject");
}
}
So how can I make it that we're able to successfully change from one bluetooth device to other?

Related

iPad Pro(3) M1 iOS 15.0, Code=201 "Siri and Dictation are disabled"

An Objective-C project of iOS. Used the Apple SpeechKit. The Speech recognition send a error 'Error Domain=kLSRErrorDomain Code=201 "Siri and Dictation are disabled" UserInfo={NSLocalizedDescription=Siri and Dictation are disabled}'
An error will be reported in resultHandler: Error Domain=kLSRErrorDomain Code=201 "Siri and Dictation are disabled" UserInfo={NSLocalizedDescription=Siri and Dictation are disabled}
- (void)resetRecognitionTask
{
// Cancel the previous task if it's running.
if (self.recognitionTask) {
//[self.recognitionTask cancel]; // Will cause the system error and memory problems.
[self.recognitionTask finish];
}
self.recognitionTask = nil;
// Configure the audio session for the app.
NSError *error = nil;
if (AVAudioSession.sharedInstance.categoryOptions != (AVAudioSessionCategoryOptionMixWithOthers|AVAudioSessionCategoryOptionDefaultToSpeaker|AVAudioSessionCategoryOptionAllowBluetooth)) {
[AVAudioSession.sharedInstance setCategory:AVAudioSessionCategoryPlayAndRecord mode:AVAudioSessionModeMeasurement options:AVAudioSessionCategoryOptionMixWithOthers|AVAudioSessionCategoryOptionDefaultToSpeaker|AVAudioSessionCategoryOptionAllowBluetooth error:&error];
}
//[AVAudioSession.sharedInstance setCategory:AVAudioSessionCategoryPlayAndRecord withOptions:AVAudioSessionCategoryOptionDuckOthers error:&error];
if (error)
{
[self stopWithError:error];
return;
}
[AVAudioSession.sharedInstance setActive:YES withOptions:AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation error:&error];
if (error)
{
[self stopWithError:error];
return;
}
// Create and configure the speech recognition request.
self.recognitionRequest = [[SFSpeechAudioBufferRecognitionRequest alloc] init];
self.recognitionRequest.taskHint = SFSpeechRecognitionTaskHintConfirmation;
// Keep speech recognition data on device
if (#available(iOS 13, *)) {
self.recognitionRequest.requiresOnDeviceRecognition = NO;
}
// Create a recognition task for the speech recognition session.
// Keep a reference to the task so that it can be canceled.
__weak typeof(self)weakSelf = self;
self.speechRecognizer = nil;
self.recognitionTask = [self.speechRecognizer recognitionTaskWithRequest:self.recognitionRequest resultHandler:^(SFSpeechRecognitionResult * _Nullable result, NSError * _Nullable error) {
__strong typeof(self)strongSelf = weakSelf;
if (result != nil) {
[strongSelf resultCallback:result];
}
}];
}
I think this is an oversight of iOS system.
Solution: Settings-> General -> Keyboards -> Enable Dictation
Turn it ON.

Callkit use bluetooth headset as audio output

I'm working on VOIP with Callkit
It work fine, except audio output source
It always output audio by iPhone speaker
some of so answer said set AvAudioSession Option as AVAudioSessionCategoryOptionAllowBluetooth will work, but still failed
and I tried to set bluetooth headset as preferred, like this, failed
by the way, how to make ringtone broadcast by headset?
below is my code, follow the suggestion in this discussion, I configure AVAudioSession right after dialing and get coming call
- (void)getCall:(NSDictionary *)infoDic {
CXCallUpdate *update = [[CXCallUpdate alloc]init];
// config update
NSUUID *uuid = [NSUUID UUID];
[self.provider reportNewIncomingCallWithUUID:uuid update:update completion:^(NSError * _Nullable error) {
if (error)
NSLog(#"%#", error.localizedDescription);
}];
NSArray *video = #[#(ReceiveVideoReq), #(VideoCalling)];
if ([video containsObject:#(self.client.callStage)])
[ProviderManager configureAudio:true];
else
[ProviderManager configureAudio:false];
}
- (void)dialPhone:(BOOL)isVideo {
CXHandle *handle = [[CXHandle alloc]initWithType:CXHandleTypePhoneNumber value:#"AAAA"];
CXStartCallAction *start = [[CXStartCallAction alloc]initWithCallUUID:uuid handle:handle];
start.video = isVideo;
CXTransaction *trans = [[CXTransaction alloc]initWithAction:start];
[self callControlReq:trans];
[ProviderManager configureAudio:isVideo];
}
+ (void)configureAudio:(BOOL)isVideo {
NSError *error=nil, *sessionError = nil;
AVAudioSession *sess = [AVAudioSession sharedInstance];
[sess setCategory:AVAudioSessionCategoryPlayAndRecord withOptions:AVAudioSessionCategoryOptionAllowBluetooth | AVAudioSessionCategoryOptionAllowBluetoothA2DP error:&sessionError];
if (sessionError)
NSLog(#"ERROR: setCategory %#", [sessionError localizedDescription]);
if (isVideo)
[sess setMode:#"AVAudioSessionModeVideoChat" error:&sessionError];
else
[sess setMode:#"AVAudioSessionModeVoiceChat" error:&sessionError];
if (sessionError) {
NSLog(#"ERROR: setCategory %#", [sessionError localizedDescription]);
}
[sess overrideOutputAudioPort:AVAudioSessionPortOverrideNone error:&sessionError];
[[AVAudioSession sharedInstance] setActive:true withOptions:AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation error:&error];
[[UIApplication sharedApplication] beginReceivingRemoteControlEvents];
}
After research, I found the root case of this issue.
Open setting->Accessibility->Touch->Call Audio Routing, there are three option: Automatic, Bluetooth Headset, Speaker.
Default is Automatic, that means if you answer a phone call(Include callkit), when you answer call by your bluetooth device, audio will route to bluetooth, if you answer the call in iPhone, it will route to receiver.
So this actually not an issue, just system behavior.
And do not try to force switch to bluetooth by using [AVAudioSession setPreferredInput:], it will cause more serious issue.
In my case: I call this force switch to bluetooth after audio connected, it worked, but when the bluetooth device disconnected, my audio completely not work and app not get any audio route change callback, even not work after connected bluetooth device again.

Twilio client voice call speaker ON/OFF issue

I am using Twilio client voice call service for calling.
Here I am facing issue in spearke ON/OFF feature.I am able to Mute/Unmute call but not able to turn On/OFF speaker. I have a same instance for both functionality. I have also checked their demo project basic phone in that this speaker ON/OFF is working and I am doing same thing in my project bur not able to do the same.
Here is my code:
if(isSpeaker == NO)
{
isSpeaker=YES;
[self.phone setSpeakerEnabled:YES];
}
else{
isSpeaker=NO;
[self.phone setSpeakerEnabled:NO];
}
self.phone is the sharedInstance of BasicPhone (their call manager class) and I am testing application in > iOS 9 both demo and my project.
I've used this code on iOS to successfully enable/disable speakerphone. It doesn't use the Twilio device, and is somewhat specific to React Native, but the core of each function should work:
RCT_EXPORT_METHOD(setSpeakerPhoneOn) {
AVAudioSession *session = [AVAudioSession sharedInstance];
NSError *error;
[session overrideOutputAudioPort:AVAudioSessionPortOverrideSpeaker error:&error];
}
RCT_EXPORT_METHOD(setSpeakerPhoneOff) {
AVAudioSession *session = [AVAudioSession sharedInstance];
NSError *error;
[session overrideOutputAudioPort:AVAudioSessionPortOverrideNone error:&error];
}
Swift 5, use below code snippet
// Change the audio route after connecting to a Room.
func moveToMic() {
audioDevice.block = {
DefaultAudioDevice.DefaultAVAudioSessionConfigurationBlock()
do {
try AVAudioSession.sharedInstance().setMode(.voiceChat)
try AVAudioSession.sharedInstance().overrideOutputAudioPort(.none)
} catch {
print(error)
}
}
audioDevice.block();
}
func moveToSpeaker() {
audioDevice.block = {
DefaultAudioDevice.DefaultAVAudioSessionConfigurationBlock()
do {
try AVAudioSession.sharedInstance().setMode(.videoChat)
try AVAudioSession.sharedInstance().overrideOutputAudioPort(.none)
} catch {
print(error)
}
}
audioDevice.block();
}

How can I detect whether an HFP or A2DP is connected in iOS?

I am working on a project which can play music via HFP device. But here's a problem that I want to detect whether an HFP or A2DP is connected when music is playing.
Now I am using the AVFoundation framework to do this. Here's the code:
- (BOOL)isConnectedToBluetoothPeripheral
{
BOOL isMatch = NO;
NSString* categoryString = [AVAudioSession sharedInstance].category;
AVAudioSessionCategoryOptions categoryOptions = [AVAudioSession sharedInstance].categoryOptions;
if ((![categoryString isEqualToString:AVAudioSessionCategoryPlayAndRecord] &&
![categoryString isEqualToString:AVAudioSessionCategoryRecord]) ||
categoryOptions != AVAudioSessionCategoryOptionAllowBluetooth)
{
NSError * error = nil;
[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayAndRecord
withOptions:AVAudioSessionCategoryOptionAllowBluetooth
error:&error];
if (error) {
[[AVAudioSession sharedInstance] setCategory:categoryString
withOptions:categoryOptions
error:&error];
return isMatch;
}
}
NSArray * availableInputs = [AVAudioSession sharedInstance].availableInputs;
for (AVAudioSessionPortDescription *desc in availableInputs)
{
if ([[desc portType] isEqualToString:AVAudioSessionPortBluetoothA2DP] || [[desc portType] isEqualToString:AVAudioSessionPortBluetoothHFP])
{
isMatch = YES;
break;
}
}
if (!isMatch)
{
NSArray * outputs = [[[AVAudioSession sharedInstance] currentRoute] outputs];
for (AVAudioSessionPortDescription * desc in outputs)
{
if ([[desc portType] isEqualToString:AVAudioSessionPortBluetoothA2DP] || [[desc portType] isEqualToString:AVAudioSessionPortBluetoothHFP])
{
isMatch = YES;
break;
}
}
}
NSError * error = nil;
[[AVAudioSession sharedInstance] setCategory:categoryString
withOptions:categoryOptions
error:&error];
return isMatch;
}
It works well but cause another problem: when music is playing, using this method to detect HFP connection will make music playing interrupt for about two seconds.
So I tried another way which can reduce the effect of detecting HFP connecting. I am using a flag
static BOOL isHFPConnectedFlag
To indicate whether HFP or A2DP is connected. I use previous method to detect the connection only once (when the app is launching) and save the result into isHFPConnectedFlag. What's more, I observe the AudioSessionRouteChange to sync the connection status:
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(handleAudioSessionRouteChangeWithState:) name:AVAudioSessionRouteChangeNotification object:nil];
When the route change reason is AVAudioSessionRouteChangeReasonNewDeviceAvailable or AVAudioSessionRouteChangeReasonOldDeviceUnavailable I can know HFP is connected or disconnected. Unfortunately, when I connect some HFP in my iPhone, the system will not post this notification, so I cannot detect the connection in this situation.
Does anyone know the reason or a better way to implements this (Detecting HFP connection without music playing interrupting)?
you can use like this!
-(BOOL) bluetoothDeviceA2DPAvailable {
BOOL available = NO;
AVAudioSession *audioSession = [AVAudioSession sharedInstance];
AVAudioSessionRouteDescription *currentRoute = [audioSession currentRoute];
for (AVAudioSessionPortDescription *output in currentRoute.outputs) {
if (([[output portType] isEqualToString:AVAudioSessionPortBluetoothA2DP] ||
[[output portType] isEqualToString:AVAudioSessionPortBluetoothHFP])) {
available = YES;
break;
}
}
return available;
}
Swift 5 version:
func bluetoothDeviceHFPAvailable() -> Bool {
let audioSession = AVAudioSession.sharedInstance()
let currentRoute = audioSession.currentRoute
for output in currentRoute.outputs {
if output.portType == .bluetoothHFP || output.portType == .bluetoothA2DP {
return true
}
}
return false
}

Check current or previous audio category iOS

I have an application with multiple AudioSession's and their own aduioRouteChangeCallback's. I need a way of seeing what the current or previous Audio Category was in order to display the proper message to the user.
Does anyone know a way this can be done? From what I've seen on the web so far my hopes are not high that this is possible from what I've found on the web. Any help is greatly appreciate because I'm working against a deadline.
Below is an example of one of my audioRouteChangeCallback,
- (void) audioRouteChangeListener: (NSNotification*)notification {
// Initiallize dictionary with notification and grab route change reason
NSDictionary *interuptionDict = notification.userInfo;
NSInteger routeChangeReason = [[interuptionDictvalueForKey:
AVAudioSessionRouteChangeReasonKey] integerValue];
NSLog(#"MADE IT: sensorAudioRouteChageListener");
switch (routeChangeReason) {
// Sensor inserted
case AVAudioSessionRouteChangeReasonNewDeviceAvailable:
// Start IO communication
[self startCollecting];
NSLog(#"Sensor INSERTED");
break;
// Sensor removed
case AVAudioSessionRouteChangeReasonOldDeviceUnavailable:
// Stop IO audio unit
[self stopCollecting];
NSLog(#"Sensor REMOVED");
break;
// Category changed from PlayAndRecord
case AVAudioSessionRouteChangeReasonCategoryChange:
// Stop IO audio unit
[self stopCollecting];
NSLog(#"Category CHANGED");
break;
default:
NSLog(#"Blowing it in- audioRouteChangeListener with route change reason: %ld",
(long)routeChangeReason);
break;
}
}
The session is initialized in the objects init function,
- (id) init {
self = [super init];
if (!self) return nil;
// Set up AVAudioSession
self->noiseAudioSession = [AVAudioSession sharedInstance];
BOOL success;
NSError *error;
success = [self->noiseAudioSession setCategory:AVAudioSessionCategoryPlayAndRecord error:&error];
if (!success) NSLog(#"ERROR initNoiseRecorder: AVAudioSession failed overrideOutputAudio- %#", error);
success = [self->noiseAudioSession setActive:YES error:&error];
if (!success) NSLog(#"ERROR initNoiseRecorder: AVAudioSession failed activating- %#", error);
return self;
}

Resources