手機音頻的輸出有外放(Speaker)、聽筒(Telephone Receiver)、有線耳機(WiredHeadset)、藍牙音箱(Bluetooth A2DP)等輸出設備。在平時,電話免提、插拔耳機、連接斷開藍牙設備等操作系統都會自動切換Audio音頻到相應的輸出設備上。比如電話免提就是從聽筒切換到外放揚聲器,插入耳機就是從外放切換到耳機。
場景需求
Android系統自動切換的這些策略,並不能全部滿足我們的產品需求,比如音樂App需要對聽歌時拔出耳機的操作進行阻止(暫停播放),防止突然切換到外放導致尷尬。
最近項目需求希望即使在連接藍牙音箱的情況下,仍舊使用手機外放播放音頻
。這就需要強制切換Audio輸出通道,打破系統原有的策略。
查閱資料,看到了Android中可以通過AudioManager
查詢、切換當前Audio輸出通道,並且在Audio輸出發生變化時,捕獲並處理這種變化。
首先提醒下大家,使用下面的方法時,需要添加權限:
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
Audio輸出狀態查詢
AudioManager 提供的下列方法可以用來查詢當前Audio輸出的狀態:
-
isBluetoothA2dpOn()
:檢查A2DPAudio音頻輸出是否通過藍牙耳機; -
isSpeakerphoneOn()
:檢查揚聲器是否打開; -
isWiredHeadsetOn()
:檢查線控耳機是否連着;注意這個方法只是用來判斷耳機是否是插入狀態,並不能用它的結果來判定當前的Audio是通過耳機輸出的,這還依賴於其他條件。 -
setSpeakerphoneOn(boolean on)
:直接選擇外放揚聲器發聲; -
setBluetoothScoOn(boolean on)
:要求使用藍牙SCO耳機進行通訊;
此處根據這篇文章簡單地介紹一下藍牙耳機的兩種鏈路,A2DP及SCO。android的api表明:
- A2DP:是一種單向的高品質音頻數據傳輸鏈路,通常用於播放立體聲音樂;
- SCO: 則是一種雙向的音頻數據的傳輸鏈路,該鏈路只支持8K及16K單聲道的音頻數據,只能用於普通語音的傳輸,若用於播放音樂那就只能呵呵了。
兩者的主要區別是:A2DP只能播放,默認是打開的,而SCO既能錄音也能播放,默認是關閉的。 如果要錄音肯定要打開sco啦,因此調用上面的 setBluetoothScoOn(boolean on) 就可以通過藍牙耳機錄音、播放音頻了,錄完、播放完記得要關閉。
另外,在Android系統中通過AudioManager.setMode()
方法來管理播放模式。在setMode()
方法中有以下幾種對應不同的播放模式:
MODE_NORMAL
: 普通模式,既不是鈴聲模式也不是通話模式MODE_RINGTONE
: 鈴聲模式MODE_IN_CALL
: 通話模式MODE_IN_COMMUNICATION
: 通信模式,包括音/視頻,VoIP通話.(3.0加入的,與通話模式類似)
在設置播放模式的時候,需要考慮流類型,我在這里使用的流類型是 STREAM_MUSIC
,所以切換播放設備的時候就需要設置為MODE_IN_COMMUNICATION
模式而不是 MODE_NORMAL
模式。可以參考這個問題。
解決問題
使用以下方法切換音頻Audio輸出,參考Android : Switching audio between Bluetooth and Phone Speaker is inconsistent:
AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); /** * 切換到外放 */ public void changeToSpeaker(){ //注意此處,藍牙未斷開時使用MODE_IN_COMMUNICATION而不是MODE_NORMAL mAudioManager.setMode(bluetoothIsConnected ? AudioManager.MODE_IN_COMMUNICATION : AudioManager.MODE_NORMAL); mAudioManager.stopBluetoothSco(); mAudioManager.setBluetoothScoOn(false); mAudioManager.setSpeakerphoneOn(true); } /** * 切換到藍牙音箱 */ public void changeToHeadset(){ mAudioManager.setMode(AudioManager.MODE_IN_COMMUNICATION); mAudioManager.startBluetoothSco(); mAudioManager.setBluetoothScoOn(true); mAudioManager.setSpeakerphoneOn(false); } /************************************************************/ //注意:以下兩個方法還未驗證 /************************************************************/ /** * 切換到耳機模式 */ public void changeToHeadset(){ mAudioManager.setMode(AudioManager.MODE_IN_COMMUNICATION); mAudioManager.stopBluetoothSco(); mAudioManager.setBluetoothScoOn(false); mAudioManager.setSpeakerphoneOn(false); } /** * 切換到聽筒 */ public void changeToReceiver(){ audioManager.setSpeakerphoneOn(false); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB){ audioManager.setMode(AudioManager.MODE_IN_COMMUNICATION); } else { audioManager.setMode(AudioManager.MODE_IN_CALL); } }
直接切換輸出通道的方法我們已經知道了。剩下需要解決的問題是,當藍牙設備斷開、連接的時候,我們希望可以自動切換到用戶原本設置的輸出通道上,比如在藍牙未連接時,用戶設置的是希望通過藍牙播報,所以應該在藍牙一旦連接以后,就把音頻切換到藍牙設備上。
下面我們就看看如何監聽藍牙設備的連接狀態。
首先注意使用前需要以下權限:
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" /> <uses-permission android:name="android.permission.BLUETOOTH" />
根據這篇文章,我們發現可以使用 AudioManager.ACTION_AUDIO_BECOMING_NOISY
這個Intent Action來監聽藍牙斷開、耳機插拔的廣播,但是測試發現,它也只能收到藍牙斷開的廣播,無法接收到藍牙連接的廣播,所以不是我們想要的。
進一步找到這篇文章:關於藍牙開發,必須注意的廣播,總結了以下藍牙廣播。
/** * 有注釋的廣播,藍牙連接時都會用到 */ intentFilter.addAction(BluetoothDevice.ACTION_FOUND); //搜索藍壓設備,每搜到一個設備發送一條廣播 intentFilter.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED); //配對開始時,配對成功時 intentFilter.addAction(BluetoothDevice.ACTION_ACL_CONNECTED); //配對時,發起連接 intentFilter.addAction(BluetoothDevice.ACTION_ACL_DISCONNECT_REQUESTED); intentFilter.addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED); //配對結束時,斷開連接 intentFilter.addAction(PAIRING_REQUEST); //配對請求(Android.bluetooth.device.action.PAIRING_REQUEST) intentFilter.addAction(BluetoothAdapter.ACTION_DISCOVERY_STARTED); //開始搜索 intentFilter.addAction(BluetoothAdapter.ACTION_DISCOVERY_FINISHED); //搜索結束。重新搜索時,會先終止搜索 intentFilter.addAction(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE); intentFilter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED); //本機開啟、關閉藍牙開關 intentFilter.addAction(BluetoothAdapter.ACTION_CONNECTION_STATE_CHANGED); //藍牙設備連接或斷開 intentFilter.addAction(BluetoothAdapter.ACTION_LOCAL_NAME_CHANGED); //更改藍牙名稱,打開藍牙時,可能會調用多次 intentFilter.addAction(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE); intentFilter.addAction(BluetoothAdapter.ACTION_REQUEST_ENABLE); intentFilter.addAction(BluetoothAdapter.ACTION_SCAN_MODE_CHANGED); //搜索模式改變
我們發現了BluetoothAdapter.ACTION_CONNECTION_STATE_CHANGED
和 BluetoothAdapter.ACTION_STATE_CHANGED
這兩個Intent廣播。
那么這兩個廣播Intent的區別是什么呢?只用其中一個可以嗎?查看Google文檔發現
-
BluetoothAdapter.ACTION_CONNECTION_STATE_CHANGED
:指的是本地藍牙適配器的連接狀態的發生改變(比如沒有關閉本機藍牙開關時,另外一個配對設備自己把連接斷開) -
BluetoothAdapter.ACTION_STATE_CHANGED
:指的是本地藍牙適配器的狀態已更改。 例如,藍牙開關打開或關閉。
換句話說,一個是用於連接狀態的變化,另一個用於藍牙適配器本身的狀態變化。經過測試發現,如果只使用BluetoothAdapter.ACTION_CONNECTION_STATE_CHANGED
監聽廣播,則會接收不到“主動關閉本機藍牙開關”的廣播事件。但只是用BluetoothAdapter.ACTION_STATE_CHANGED
的話,很明顯這時候藍牙設備並未真正配對。
動態注冊藍牙連接、斷開廣播的方式如下:
- 動態注冊廣播
public class BluetoothConnectionReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent){ if (BluetoothAdapter.ACTION_CONNECTION_STATE_CHANGED.equals(intent.getAction())) { //藍牙連接狀態 int state = intent.getIntExtra(BluetoothAdapter.EXTRA_CONNECTION_STATE, -1); if (state == BluetoothAdapter.STATE_CONNECTED || state == BluetoothAdapter.STATE_DISCONNECTED) { //連接或失聯,切換音頻輸出(到藍牙、或者強制仍然揚聲器外放) } } else if (BluetoothAdapter.ACTION_STATE_CHANGED.equals(intent.getAction())){ //本地藍牙打開或關閉 int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, -1); if (state == BluetoothAdapter.STATE_OFF || state == BluetoothAdapter.STATE_TURNING_OFF) { //斷開,切換音頻輸出 } } } }
BluetoothConnectionReceiver audioNoisyReceiver = new BluetoothConnectionReceiver(); //藍牙狀態廣播監聽 IntentFilter audioFilter = new IntentFilter(); audioFilter.addAction(BluetoothAdapter.ACTION_CONNECTION_STATE_CHANGED); audioFilter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED); mContext.registerReceiver(audioNoisyReceiver, audioFilter);
之后,我們就可以根據上面切換音頻輸出通道的代碼來實現藍牙設備連接、斷開以后強制打破操作系統原有的輸出通道切換策略,來實現我們自己想要的切換功能了。