AVAudioSession-Category與語音喚醒和音訊播放的恩怨糾葛
最近,在開發一款音樂播放器型別專案中遇到的一些與AVAudioSession-Category設定的一些坑,以下是整個過程的一些經驗總結。
1.常規播放
一般如果應用只有簡單音樂播放功能,那麼我們的AVAudioSession-Category只用像如下一樣設定即可:
[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback error:nil];
[[AVAudioSession sharedInstance] setActive:YES error:nil];
此時如果我們只是播放音樂,而不需要獨佔鎖屏介面時,還可以設定:
[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback
withOptions:AVAudioSessionCategoryOptionMixWithOthers
error:nil];
這樣我們相容其他後臺播放的音樂一起進行播放,不過大部分場景下,我們是需要獨佔式後臺播放。
2.常規錄音
在錄音的時候,我們一般如以下設定:
[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryRecord
error:nil];
[[AVAudioSession sharedInstance] setActive:YES
error:nil];
3.如果將錄音和播放同時進行時,我們改選擇何種Category?
同時進行播放和錄音時,我們需要這樣設定:
[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayAndRecord error:nil]; [[AVAudioSession sharedInstance] setActive:YES error:nil];
需要注意的是,設定成這樣的情況下,如果,在錄音未開啟的情況下,直接進行播放,則會出現,播放音量特別小的情況,我們需要在播放之前,將錄音開啟。
4.前後臺切換
上述的模式,在iOS系統下,是不允許錄音和播放在後臺狀態下同時進行的(PS:語音視訊通話是通過CallKit實現的,不用於常規的播放和錄音功能)。由此,我們在應用進入後臺時就需要關掉其中一個功能。
以後臺支援播放為例,在應用將要失活時,先切換模式,再關掉錄音功能:
// stopRecording...
// 切換模式
[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback error:nil];
[[AVAudioSession sharedInstance] setActive:YES error:nil];
應用即將進入前臺時,切換模式,再開啟錄音功能:
[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayAndRecord
error:nil];
[[AVAudioSession sharedInstance] setActive:YES
error:nil];
// 延遲恢復,否則會導致AVAudioSession的i/o錯誤
[self performSelectorOnMainThread:@selector(startRecording)
withObject:nil
waitUntilDone:NO];
5.電話中斷
電話鬧鐘的中斷也會對,[AVAudioSession sharedInstance] 產生影響。
我們一般場景下會用 下面這個通知進行監控並處理暫停和恢復的工作:
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleInterruption:) name:AVAudioSessionInterruptionNotification object:nil];
- (void)handleInterruption:(NSNotification*)notification { NSLog(@"interruption info:%@",notification.userInfo); }
但是,當我們在處理第四個場景前後臺的情況下,這個通知,在中斷的時候會進入,但是電話結束後,不會再接收到中斷結束的通知。
原因:
有的app使用了AVCaptureDevice和AVCaptureSession,以進行錄音錄影操作。為了調優app設定,以更好的進行錄音錄影,從iOS7開始,在預設情況下,AVCaptureSession會使用app的AVAudioSession,並對其進行修改。這樣,設定的中斷監聽方法會失效。
而電話來電也會使我們的應用接收到 失活的通知,在失活的時候處理了AVAudioSession,就會導致上述通知失效。
解決方案:我這裡採用了比較折中的方案,因為我們的需求,對於第四條的處理是必要的。使用的是 CoreTelephony框架下的CTCallCenter物件,來監控電話的 撥入接通、結束通話等狀態。程式碼如下:
self.center = [[CTCallCenter alloc] init];
// TODO: 檢測到來電後的處理
self.center.callEventHandler = ^(CTCall * call)
{
if (call.callState == CTCallStateIncoming ||
call.callState == CTCallStateConnected ||
call.callState == CTCallStateDialing)
{
}
else if (call.callState == CTCallStateDisconnected)
{
}
};
通過各種打電話的場景測試後,可以實現電話中斷恢復功能。
ps:至於鬧鐘的中斷以及siri等其他中斷,暫時沒有調研和實現。
6.藍芽車載
終於來到了本文的最後一個部分了,也是最為曲折的一部分。
本來以為車載的車機連線後對於iPhone的播放控制與鎖屏控制類似,直接在系統媒體遠端控制監控中就能夠拿到相應的控制方法回撥。
在APPDelegate中加上如下程式碼:
//監聽遠端互動方法
- (void)remoteControlReceivedWithEvent:(UIEvent *)event
{
switch (event.subtype) {
//播放
case UIEventSubtypeRemoteControlPlay:
{
}
break;
//停止
case UIEventSubtypeRemoteControlPause:
{
}
break;
//下一首
case UIEventSubtypeRemoteControlNextTrack:
{
}
break;
//上一首
case UIEventSubtypeRemoteControlPreviousTrack:
{
}
break;
default:
break;
}
}
事實上,當我們的應用只有簡單的播放功能的時候,上述程式碼的確可以完美的實現車機對於播放的控制功能。但是當應用出於前臺的情況下,我們新增上了一直錄音的功能的時候,用車機控制播放,就完全沒有任何響應了。可以注意到的是,我們看到車機的螢幕上,會顯示通話中。查閱了各種資料和文章,都沒有找到相關的解決辦法和原理解釋。
最後,想到了看看有沒有其他類似的語音識別及播放功能的應用(iOS)有沒有類似的處理,結果調研到百度地圖 中的小度 有相關的處理。在它的設定中,找到 語音設定有一個藍芽連線設定 。兩個模式設定 如下:
a.藍芽裝置播報,小度無法喚醒使用(播放體驗最佳)
b.藍芽裝置播報,小度喚醒正常使用(車機顯示通話中,播報音量可能變小)
由此可以看出,a場景下 錄音功能關閉,只有語音播報功能,b場景下,錄音功能開啟,車機就是會識別到手機裝置在錄音和播放中,認為就是在通話中,這個是車機本身的限制,無法從應用層進行優化。而且,百度地圖的給用的預設選擇就是,連線藍芽的情況下,小度不能喚醒。
綜合上面我們協同產品,從互動層面上更改,保證,在連線車機的情況下,能夠控制播放。具體處理互動如下:
在應用進入到前臺時,檢測到連線了藍芽裝置,彈出彈框,讓使用者選擇,繼續開啟喚醒功能開始,關閉喚醒功能(保證播放控制功能)。繼續開啟的情況下,車機無法控制播放。
下面是檢測是否有輸出裝置連線的程式碼(並未找到檢查當前是否有連線藍芽裝置的方法):
+ (BOOL)checkIsConnectToBluetooth
{
BOOL isBluetooth = NO;
// 找出當前所有支援輸入的裝置 availableInputs 這裡面會出現 iPhone麥克風, 藍芽耳機1, 藍芽耳機2 , 三個物件, 在一個數組裡.
NSArray* inputArray = [[AVAudioSession sharedInstance] availableInputs];
for (AVAudioSessionPortDescription* desc in inputArray)
{
if ([desc.portType isEqualToString:AVAudioSessionPortBluetoothLE]
|| [desc.portType isEqualToString:AVAudioSessionPortBluetoothHFP]
|| [desc.portType isEqualToString:AVAudioSessionPortBluetoothA2DP])
{
isBluetooth = YES;
}
}
return isBluetooth;
}
同時,還需要配合Category的設定:
[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayAndRecord
withOptions:AVAudioSessionCategoryOptionAllowBluetooth
error:&error];
[[AVAudioSession sharedInstance] setActive:YES error:&error1];
AVAudioSessionCategoryOptionAllowBluetooth這是必須要新增的,否則上面的方法,連線藍芽後,在應用即將活躍的監控的時候,是會返回NO,拿不到準確的值。
最後,上面的所有的經驗和總結,都是通過各種查閱資料和不斷除錯得來的,並沒有較為科學嚴謹的理論依據,也沒有相關的官方文件的支援。總結出來,只是希望給後續如果有人遇到與我一樣的難題時,少走一些彎路,有一些啟發,僅此而已。