手機(jī)音頻的輸出有外放(Speaker)谷浅、聽筒(Telephone Receiver)箫老、有線耳機(jī)(WiredHeadset)怪蔑、藍(lán)牙音箱(Bluetooth A2DP)等輸出設(shè)備歉糜。在平時卵酪,電話免提戳玫、插拔耳機(jī)熙掺、連接斷開藍(lán)牙設(shè)備等操作系統(tǒng)都會自動切換Audio音頻到相應(yīng)的輸出設(shè)備上。比如電話免提就是從聽筒切換到外放揚(yáng)聲器咕宿,插入耳機(jī)就是從外放切換到耳機(jī)币绩。
場景需求
Android系統(tǒng)自動切換的這些策略,并不能全部滿足我們的產(chǎn)品需求府阀,比如音樂App需要對聽歌時拔出耳機(jī)的操作進(jìn)行阻止(暫停播放)缆镣,防止突然切換到外放導(dǎo)致尷尬。
最近項(xiàng)目需求希望即使在連接藍(lán)牙音箱的情況下试浙,仍舊使用手機(jī)外放播放音頻
董瞻。這就需要強(qiáng)制切換Audio輸出通道,打破系統(tǒng)原有的策略田巴。
查閱資料钠糊,看到了Android中可以通過AudioManager
查詢、切換當(dāng)前Audio輸出通道壹哺,并且在Audio輸出發(fā)生變化時抄伍,捕獲并處理這種變化。
首先提醒下大家管宵,使用下面的方法時截珍,需要添加權(quán)限:
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
Audio輸出狀態(tài)查詢
AudioManager 提供的下列方法可以用來查詢當(dāng)前Audio輸出的狀態(tài):
isBluetoothA2dpOn()
:檢查A2DPAudio音頻輸出是否通過藍(lán)牙耳機(jī)攀甚;isSpeakerphoneOn()
:檢查揚(yáng)聲器是否打開;isWiredHeadsetOn()
:檢查線控耳機(jī)是否連著岗喉;注意這個方法只是用來判斷耳機(jī)是否是插入狀態(tài)秋度,并不能用它的結(jié)果來判定當(dāng)前的Audio是通過耳機(jī)輸出的,這還依賴于其他條件沈堡。setSpeakerphoneOn(boolean on)
:直接選擇外放揚(yáng)聲器發(fā)聲静陈;setBluetoothScoOn(boolean on)
:要求使用藍(lán)牙SCO耳機(jī)進(jìn)行通訊;
此處根據(jù)這篇文章簡單地介紹一下藍(lán)牙耳機(jī)的兩種鏈路诞丽,A2DP及SCO鲸拥。android的api表明:
- A2DP:是一種單向的高品質(zhì)音頻數(shù)據(jù)傳輸鏈路,通常用于播放立體聲音樂僧免;
- SCO: 則是一種雙向的音頻數(shù)據(jù)的傳輸鏈路刑赶,該鏈路只支持8K及16K單聲道的音頻數(shù)據(jù),只能用于普通語音的傳輸懂衩,若用于播放音樂那就只能呵呵了撞叨。
兩者的主要區(qū)別是:A2DP只能播放,默認(rèn)是打開的浊洞,而SCO既能錄音也能播放牵敷,默認(rèn)是關(guān)閉的。 如果要錄音肯定要打開sco啦法希,因此調(diào)用上面的setBluetoothScoOn(boolean on)
就可以通過藍(lán)牙耳機(jī)錄音枷餐、播放音頻了,錄完苫亦、播放完記得要關(guān)閉毛肋。
另外,在Android系統(tǒng)中通過AudioManager.setMode()
方法來管理播放模式屋剑。在setMode()
方法中有以下幾種對應(yīng)不同的播放模式:
-
MODE_NORMAL
: 普通模式润匙,既不是鈴聲模式也不是通話模式 -
MODE_RINGTONE
: 鈴聲模式 -
MODE_IN_CALL
: 通話模式 -
MODE_IN_COMMUNICATION
: 通信模式,包括音/視頻,VoIP通話.(3.0加入的唉匾,與通話模式類似)
在設(shè)置播放模式的時候孕讳,需要考慮流類型,我在這里使用的流類型是 STREAM_MUSIC
巍膘,所以切換播放設(shè)備的時候就需要設(shè)置為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(){
//注意此處,藍(lán)牙未斷開時使用MODE_IN_COMMUNICATION而不是MODE_NORMAL
mAudioManager.setMode(bluetoothIsConnected ? AudioManager.MODE_IN_COMMUNICATION : AudioManager.MODE_NORMAL);
mAudioManager.stopBluetoothSco();
mAudioManager.setBluetoothScoOn(false);
mAudioManager.setSpeakerphoneOn(true);
}
/**
* 切換到藍(lán)牙音箱
*/
public void changeToHeadset(){
mAudioManager.setMode(AudioManager.MODE_IN_COMMUNICATION);
mAudioManager.startBluetoothSco();
mAudioManager.setBluetoothScoOn(true);
mAudioManager.setSpeakerphoneOn(false);
}
/************************************************************/
//注意:以下兩個方法還未驗(yàn)證
/************************************************************/
/**
* 切換到耳機(jī)模式
*/
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);
}
}
直接切換輸出通道的方法我們已經(jīng)知道了逮诲。剩下需要解決的問題是帜平,當(dāng)藍(lán)牙設(shè)備斷開幽告、連接的時候,我們希望可以自動切換到用戶原本設(shè)置的輸出通道上裆甩,比如在藍(lán)牙未連接時冗锁,用戶設(shè)置的是希望通過藍(lán)牙播報,所以應(yīng)該在藍(lán)牙一旦連接以后嗤栓,就把音頻切換到藍(lán)牙設(shè)備上冻河。
下面我們就看看如何監(jiān)聽藍(lán)牙設(shè)備的連接狀態(tài)。
監(jiān)聽藍(lán)牙連接狀態(tài)
首先注意使用前需要以下權(quán)限:
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.BLUETOOTH" />
根據(jù)這篇文章茉帅,我們發(fā)現(xiàn)可以使用 AudioManager.ACTION_AUDIO_BECOMING_NOISY
這個Intent Action來監(jiān)聽藍(lán)牙斷開叨叙、耳機(jī)插拔的廣播,但是測試發(fā)現(xiàn)堪澎,它也只能收到藍(lán)牙斷開的廣播擂错,無法接收到藍(lán)牙連接的廣播,所以不是我們想要的樱蛤。
進(jìn)一步找到這篇文章:關(guān)于藍(lán)牙開發(fā)钮呀,必須注意的廣播,總結(jié)了以下藍(lán)牙廣播昨凡。
/**
* 有注釋的廣播爽醋,藍(lán)牙連接時都會用到
*/
intentFilter.addAction(BluetoothDevice.ACTION_FOUND); //搜索藍(lán)壓設(shè)備,每搜到一個設(shè)備發(fā)送一條廣播
intentFilter.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED); //配對開始時便脊,配對成功時
intentFilter.addAction(BluetoothDevice.ACTION_ACL_CONNECTED); //配對時子房,發(fā)起連接
intentFilter.addAction(BluetoothDevice.ACTION_ACL_DISCONNECT_REQUESTED);
intentFilter.addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED); //配對結(jié)束時,斷開連接
intentFilter.addAction(PAIRING_REQUEST); //配對請求(Android.bluetooth.device.action.PAIRING_REQUEST)
intentFilter.addAction(BluetoothAdapter.ACTION_DISCOVERY_STARTED); //開始搜索
intentFilter.addAction(BluetoothAdapter.ACTION_DISCOVERY_FINISHED); //搜索結(jié)束就轧。重新搜索時,會先終止搜索
intentFilter.addAction(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE);
intentFilter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED); //本機(jī)開啟田度、關(guān)閉藍(lán)牙開關(guān)
intentFilter.addAction(BluetoothAdapter.ACTION_CONNECTION_STATE_CHANGED); //藍(lán)牙設(shè)備連接或斷開
intentFilter.addAction(BluetoothAdapter.ACTION_LOCAL_NAME_CHANGED); //更改藍(lán)牙名稱妒御,打開藍(lán)牙時,可能會調(diào)用多次
intentFilter.addAction(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE);
intentFilter.addAction(BluetoothAdapter.ACTION_REQUEST_ENABLE);
intentFilter.addAction(BluetoothAdapter.ACTION_SCAN_MODE_CHANGED); //搜索模式改變
我們發(fā)現(xiàn)了BluetoothAdapter.ACTION_CONNECTION_STATE_CHANGED
和 BluetoothAdapter.ACTION_STATE_CHANGED
這兩個Intent廣播镇饺。
那么這兩個廣播Intent的區(qū)別是什么呢乎莉?只用其中一個可以嗎?查看Google文檔發(fā)現(xiàn)
BluetoothAdapter.ACTION_CONNECTION_STATE_CHANGED
:指的是本地藍(lán)牙適配器的連接狀態(tài)的發(fā)生改變(比如沒有關(guān)閉本機(jī)藍(lán)牙開關(guān)時奸笤,另外一個配對設(shè)備自己把連接斷開)BluetoothAdapter.ACTION_STATE_CHANGED
:指的是本地藍(lán)牙適配器的狀態(tài)已更改惋啃。 例如,藍(lán)牙開關(guān)打開或關(guān)閉监右。
換句話說边灭,一個是用于連接狀態(tài)的變化,另一個用于藍(lán)牙適配器本身的狀態(tài)變化健盒。經(jīng)過測試發(fā)現(xiàn)绒瘦,如果只使用BluetoothAdapter.ACTION_CONNECTION_STATE_CHANGED
監(jiān)聽廣播称簿,則會接收不到“主動關(guān)閉本機(jī)藍(lán)牙開關(guān)”的廣播事件。但只是用BluetoothAdapter.ACTION_STATE_CHANGED
的話惰帽,很明顯這時候藍(lán)牙設(shè)備并未真正配對憨降。
動態(tài)注冊藍(lán)牙連接、斷開廣播的方式如下:
- 動態(tài)注冊廣播
BluetoothConnectionReceiver audioNoisyReceiver = new BluetoothConnectionReceiver();
//藍(lán)牙狀態(tài)廣播監(jiān)聽
IntentFilter audioFilter = new IntentFilter();
audioFilter.addAction(BluetoothAdapter.ACTION_CONNECTION_STATE_CHANGED);
audioFilter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED);
mContext.registerReceiver(audioNoisyReceiver, audioFilter);
之后该酗,我們就可以根據(jù)上面切換音頻輸出通道的代碼來實(shí)現(xiàn)藍(lán)牙設(shè)備連接授药、斷開以后強(qiáng)制打破操作系統(tǒng)原有的輸出通道切換策略,來實(shí)現(xiàn)我們自己想要的切換功能了呜魄。
參考資料:
1悔叽、Android中的Audio播放:控制Audio輸出通道切換
2、Android音樂播放模式切換-外放耕赘、聽筒骄蝇、耳機(jī)
3、Android : Switching audio between Bluetooth and Phone Speaker is inconsistent
4操骡、Listening to bluetooth connections