Callkit use bluetooth headset as audio output - ios

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.

Related

Unable to switch to speaker output when bluetooth headsets are connected

I'm trying to allow for a toggle between bluetooth headsets (airpods in my case) and the phone speaker, using AVAudioSession. I initialize my session as so:
AVAudioSessionCategoryOptions options = (AVAudioSessionCategoryOptionAllowBluetooth | AVAudioSessionCategoryOptionDefaultToSpeaker);
NSError *error = nil;
[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayAndRecord withOptions:options error:&error];
Then I try to alternate between output modes as so:
-(void)setIncomingSoundMode:(IncomingSoundMode)incomingSoundMode{
[self removeAudioRouteChangedObserver];
[NNLogger logFromInstance: self message: #"Audio stream setting use speaker" data: #(incomingSoundMode)];
_incomingSoundMode = incomingSoundMode;
AVAudioSession *session = [AVAudioSession sharedInstance];
AVAudioSessionPortDescription *routePort = session.currentRoute.outputs.firstObject;
NSString *portType = routePort.portType;
NSLog(#"current port type: %#",portType);
NSError *audioPortError = nil;
if(incomingSoundMode == IncomingSoundModeSpeaker){
[session overrideOutputAudioPort:AVAudioSessionPortOverrideSpeaker error:&audioPortError];
[self muteChannel:NO];
} else if(incomingSoundMode == IncomingSoundModeHeadset){
[session overrideOutputAudioPort:AVAudioSessionPortOverrideNone error:&audioPortError];
[self muteChannel:NO];
} else if(incomingSoundMode == IncomingSoundModeBluetooth){
[session overrideOutputAudioPort:AVAudioSessionPortOverrideNone error:&audioPortError];
[self muteChannel:NO];
} else if(incomingSoundMode == IncomingSoundModeSilent){
[self muteChannel:YES];
}
if(audioPortError){
NSLog(#"audioPortError - %#",audioPortError.localizedDescription);
}
NSError *sessionError = nil;
[session setActive: YES error:&sessionError];
if(sessionError){
NSLog(#"sessionError - %#",sessionError.localizedDescription);
}
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[self addAudioRouteChangedObserver];
});
}
The issue is when I try to override to speaker output while bluetooth headphones are connected - it simply doesn't switch to the speaker. This same functionality works with wired headphones, or when toggling device speaker to headset:
[session overrideOutputAudioPort:AVAudioSessionPortOverrideSpeaker error:&audioPortError];
[session setActive: YES error:&sessionError];
Any clues on what I'm doing wrong here??
Thanks
I have exact same issue in the application I'm working on and it's reproducible only on iOS 12+ and only for W1 / H1 chip bluetooth devices (AirPods / new Beats Studio, etc.).
I asked apple audio engineers on WWDC about it (by showing my bug report) and they have confirmed that this is a known issue they're working on. Should be fixed in iOS update.. somewhen later :) To speed things up, you could duplicate this bug report: rdar://49734534
P.S. There is no pretty workaround for this issue, but there are few options:
Use MPVolumeView's route button. It works, and seems like this is
how WhatsApp handles this case.
Disable bluetooth option for AVAudioSession. Far from perfect solution, but may work for some apps.

How can stream Audio when iPhone is locked?

I'm working on AppRTC for Audio call, It's working fine when app is open. Audio call is not working when device is locked, If wI un-lock device while call and open app, It's start working normally but while it's locked it's not working. (Audio is not passing or playing).
I already added "App plays audio or streams audio/video using AirPlay" in "Required background modes" of plist.
Audio session is also configured. Please check code below.
- (void)configureAVAudioSession:(BOOL)isSpeakerOn
{
// Get your app's audioSession singleton object
AVAudioSession *session = [AVAudioSession sharedInstance];
// Error handling
BOOL success;
NSError *error;
// set the audioSession category.
// Needs to be Record or PlayAndRecord to use audioRouteOverride:
success = [session setCategory:AVAudioSessionCategoryPlayAndRecord
error:&error];
if (!success) {
NSLog(#"AVAudioSession error setting category:%#",error);
}
// Set the audioSession override
success = [session overrideOutputAudioPort:isSpeakerOn?AVAudioSessionPortOverrideSpeaker:AVAudioSessionPortOverrideNone
error:&error];
if (!success) {
NSLog(#"AVAudioSession error overrideOutputAudioPort:%#",error);
}
// Activate the audio session
success = [session setActive:YES error:&error];
if (!success) {
NSLog(#"AVAudioSession error activating: %#",error);
}
else {
NSLog(#"AudioSession active");
}
}
Please let me know if anything is missing or i'd implemented wrongly.
Regards,

No audio from AVCaptureSession after changing AVAudioSession

I'm making an app that supports both video playback and recording. I always want to allow background audio mixing except for during video playback (during playback, background audio should be muted). Therefore, I use the two methods below while changing the state of playback. When my AVPlayer starts loading, I call MuteBackgroundAudio, and when I dismiss the view controller containing it, I call ResumeBackgroundAudio. This works as expected, and the audio returns successfully after leaving playback.
The issue is that after doing this at least once, whenever I record anything using AVCaptureSession, no sounds gets recorded. My session is configured like so:
AVCaptureDevice *audioDevice = [[AVCaptureDevice devicesWithMediaType:AVMediaTypeAudio] firstObject];
AVCaptureDeviceInput *audioDeviceInput = [AVCaptureDeviceInput deviceInputWithDevice:audioDevice error:&error];
if (error)
{
NSLog(#"%#", error);
}
if ([self.session canAddInput:audioDeviceInput])
{
[self.session addInput:audioDeviceInput];
}
[self.session setAutomaticallyConfiguresApplicationAudioSession:NO];
// ... videoDeviceInput
Note that I have not set usesApplicationAudioSession, so it defaults to YES.
void MuteBackgroundAudio(void)
{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
if ([[AVAudioSession sharedInstance] isOtherAudioPlaying] && !isMuted)
{
isMuted = YES;
NSError *error = nil;
[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayAndRecord
withOptions:AVAudioSessionCategoryOptionDefaultToSpeaker
error:&error];
if (error)
{
NSLog(#"DEBUG - Set category error %ld, %#", (long)error.code, error.localizedDescription);
}
NSError *error2 = nil;
[[AVAudioSession sharedInstance] setActive:YES
withOptions:0
error:&error2];
if (error2)
{
NSLog(#"DEBUG - Set active error 2 %ld, %#", (long)error.code, error.localizedDescription);
}
}
});
}
void ResumeBackgroundAudio(void)
{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
if (isMuted)
{
AVAudioSession *audioSession = [AVAudioSession sharedInstance];
NSError *deactivationError = nil;
[audioSession setActive:NO withOptions:AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation error:&deactivationError];
if (deactivationError)
{
NSLog(#"DEBUG - Failed at deactivating audio session, retrying...");
ResumeBackgroundAudio();
return;
}
isMuted = NO;
NSLog(#"DEBUG - Audio session deactivated");
NSError *categoryError = nil;
[audioSession setCategory:AVAudioSessionCategoryPlayAndRecord
withOptions:AVAudioSessionCategoryOptionDefaultToSpeaker | AVAudioSessionCategoryOptionMixWithOthers
error:&categoryError];
if (categoryError)
{
NSLog(#"DEBUG - Failed at setting category");
return;
}
NSLog(#"DEBUG - Audio session category set to mix with others");
NSError *activationError = nil;
[audioSession setActive:YES error:&activationError];
if (activationError)
{
NSLog(#"DEBUG - Failed at activating audio session");
return;
}
NSLog(#"DEBUG - Audio session activated");
}
});
}
Debugging
I have noticed that the audioSession always needs two tries to successfully deactivate after calling ResumeBackgroundAudio. It seems my AVPlayer does not get deallocated or stopped in time, due to this comment in AVAudioSession.h:
Note that this method will throw an exception in apps linked on or
after iOS 8 if the session is set inactive while it has running or
paused I/O (e.g. audio queues, players, recorders, converters, remote
I/Os, etc.).
The fact that no sound gets recorded bring me to believe the audioSession does not actually get activated, but my logging says it does (always in the second iteration of the recursion).
I got the idea of using recursion to solve this problem from this post.
To clarify, the flow that causes the problem is the following:
Open app with Spotify playing
Begin playback of any content in the app
Spotify gets muted, playback begins (MuteBackgroundAudio)
Playback ends, Spotify starts playing again (ResumeBackgroundAudio)
Start recording
Stop recording, get mad that there is no audio
I've had the exact same issue as you're describing, down to the very last detail ([audioSession setActive:NO withOptions:AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation error:&deactivationError]; failed the first time no matter what)
The only acceptable way, for my case, was to stop then start the AVCaptureSession. I'm not entirely sure but I think this is the same exact way Whatsapp handles it - their camera behaves exactly like mine with the solution I'm suggesting.
Removing and adding the same audio input on the already running session also seemed to work but I got a terrible camera freeze - nothing compared to the session start / stop.
The recursion solution is nice, but I think it would be a great idea to 'throttle' that recursion call (add a short delay & a max retry count of some sort) in case for a legit reason the set fails every time. Otherwise your stack will overflow and your app will crash.
If you or anyone found a better solution to this I would love to know it.

Background Audio for iOS Video App

So I was working on a video capture app that plays background audio (from Spotify or Apple Music) and I'm having a small problem where there's a small audio interruption when I open my app while audio is being played.
Here's what I have for allowing background audio to play (located in my didFinishLaunchingWithOptions in my AppDelegate class:
[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayAndRecord
withOptions:AVAudioSessionCategoryOptionDefaultToSpeaker | AVAudioSessionCategoryOptionMixWithOthers
error:nil];
[[AVAudioSession sharedInstance] setActive:YES error:nil];
Any clues to stopping that beginning interruption? Thanks!!
EDIT
I should also mention after setting the AVAudioSession I am setting my AVCaptureSession. I initialize it then set the properties.
self.session.usesApplicationAudioSession = YES;
self.session.automaticallyConfiguresApplicationAudioSession = NO;
I think the reason of interruption is you are updating category in every case. You may use below function to check and update category only if it is needed.
-(BOOL) checkAndUpdateCategory {
NSError *error;
AVAudioSession *session = [AVAudioSession sharedInstance];
BOOL result = [session.category isEqualToString:AVAudioSessionCategoryPlayAndRecord];
if(!result) {
result = [session setCategory:AVAudioSessionCategoryPlayAndRecord withOptions:AVAudioSessionCategoryOptionDefaultToSpeaker | AVAudioSessionCategoryOptionMixWithOthers error:&error];
if(error) {
//Handle Error
NSLOG(#"Error:%#", error);
}
}
return result;
}

iOS 7 Can't have audio via bluetooth receiver using AVAudioSession

I'm facing an issue using bluetooth for my app on iOS 7.
To be more precise, the bluetooth receiver i'd like to connect to is : http://www.amazon.co.uk/Sony-Bluetooth-Receiver-Enhancement-Smartphone-Black/dp/B00G9YK7LS
Of course the iPhone is paired to the device before I start testing.
My app should just make an audio pass through (from the mic to the speakers) using the EZAudio framework. It works fine using the iPhone headset but not with the bluetooth.
Here's the code I put in the delegate class (didFinishLaunchingWithOptions function) to init the audio session :
// configure the audio session
AVAudioSession *audioSession = [AVAudioSession sharedInstance];
NSError *err = NULL;
// deactivate session
[audioSession setActive:NO error:&err];
if (err) {
NSLog(#"There was an error deactivating the audio session");
}
[audioSession setCategory:AVAudioSessionCategoryPlayAndRecord withOptions:AVAudioSessionCategoryOptionAllowBluetooth error:&err];
if( err ){
NSLog(#"There was an error creating the audio session");
}
[audioSession overrideOutputAudioPort:AVAudioSessionPortOverrideNone error:&err];
if( err ){
NSLog(#"There was an error sending the audio to the speakers");
}
// Activate the session
[audioSession setActive:YES error:&err];
Then I use EZAudio framework to send audio to the audio device:
/**
Start the output
*/
[EZOutput sharedOutput].outputDataSource = self;
[[EZOutput sharedOutput] startPlayback];
Anyone has any idea about this issue please? Have I missed something?
Thanks for your help!
You should set the category to AVAudioSessionCategoryPlayback. When you have it set to AVAudioSessionCategoryPlayAndRecord, the bluetooth device needs to be available for input as well as output. If it is not, the device will default to internal speakers for playback.

Resources