Действия VideoCall bluetooth audioManager не отправляет звук
В моем приложении я создал WebRtcAudioManager для обработки нескольких выходов, доступных пользователю, чтобы он мог перенаправить звук, в котором он когда-либо выбирает. Я создал следующий класс менеджера.
class WebRtcAudioManagerImpl(private val appContext: Context) : WebRtcAudioManager {
/**
* AudioDevice is the names of possible audio devices that we currently
* support.
*/
enum class AudioDevice {
SPEAKER_PHONE, WIRED_HEADSET, EARPIECE, BLUETOOTH, NONE
}
/** AudioManager state. */
enum class AudioManagerState {
UNINITIALIZED, PREINITIALIZED, RUNNING
}
/** Selected audio device change event. */
interface AudioManagerEvents {
// Callback fired once audio device is changed or list of available audio devices changed.
fun onAudioDeviceChanged(
selectedAudioDevice: AudioDevice?, availableAudioDevices: Set<AudioDevice?>?
)
}
private var audioManager: AudioManager
@Nullable
private var audioManagerEvents: AudioManagerEvents? = null
private var amState: AudioManagerState
private var savedAudioMode = AudioManager.MODE_INVALID
private var savedIsSpeakerPhoneOn = false
private var savedIsMicrophoneMute = false
private var hasWiredHeadset = false
// Default audio device; speaker phone for video calls or earpiece for audio
// only calls.
private var defaultAudioDevice: AudioDevice? = null
// Contains the currently selected audio device.
// This device is changed automatically using a certain scheme where e.g.
// a wired headset "wins" over speaker phone. It is also possible for a
// user to explicitly select a device (and override any predefined scheme).
// See |userSelectedAudioDevice| for details.
private var selectedAudioDevice: AudioDevice? = null
// Contains the user-selected audio device which overrides the predefined
// selection scheme.
private var userSelectedAudioDevice: AudioDevice? = null
private var mBluetoothAdapter: BluetoothAdapter? = null
// Contains speakerphone setting: auto, true or false
@Nullable
private val useSpeakerphone: String?
// Contains a list of available audio devices. A Set collection is used to
// avoid duplicate elements.
private var audioDevices: MutableSet<AudioDevice?> = HashSet()
private val _audioDevices = MutableLiveData<List<AudioDevice?>>(emptyList())
override val devices: LiveData<List<AudioDevice?>>
get() = _audioDevices
// Broadcast receiver for wired headset intent broadcasts.
private val wiredHeadsetReceiver: BroadcastReceiver
// Broadcast receiver for bluetooth headset intent broadcasts.
private val bluetoothReceiver: BroadcastReceiver
// Callback method for changes in audio focus.
@Nullable
private var audioFocusChangeListener: AudioManager.OnAudioFocusChangeListener? = null
/* Receiver which handles changes in wired headset availability. */
private inner class WiredHeadsetReceiver : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent) {
val state = intent.getIntExtra("state", STATE_UNPLUGGED)
val microphone = intent.getIntExtra("microphone", HAS_NO_MIC)
val name = intent.getStringExtra("name")
Log.d(
TAG, "WiredHeadsetReceiver.onReceive"
+ ": " + "a=" + intent.action.toString() + ", s=" +
(if (state == STATE_UNPLUGGED) "unplugged" else "plugged").toString()
+ ", m=" + (if (microphone == HAS_MIC) "mic" else "no mic").toString()
+ ", n=" + name.toString() + ", sb=" + isInitialStickyBroadcast
)
hasWiredHeadset = (state == STATE_PLUGGED)
updateAudioDeviceState()
}
private val STATE_UNPLUGGED = 0
private val STATE_PLUGGED = 1
private val HAS_NO_MIC = 0
private val HAS_MIC = 1
}
var mBluetoothA2dp: BluetoothA2dp? = null
private val mProfileListener: BluetoothProfile.ServiceListener =
object : BluetoothProfile.ServiceListener {
override fun onServiceConnected(profile: Int, proxy: BluetoothProfile) {
if (profile == BluetoothProfile.A2DP) {
updateAudioDeviceState()
}
}
override fun onServiceDisconnected(profile: Int) {
if (profile == BluetoothProfile.A2DP) {
mBluetoothA2dp = null
}
updateAudioDeviceState()
}
}
private inner class BluetoothHeadsetReceiver : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent) {
val action = intent.action
val state: Int
if (action == BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED) {
state = intent.getIntExtra(
BluetoothHeadset.EXTRA_STATE,
BluetoothHeadset.STATE_DISCONNECTED
)
if (state == BluetoothHeadset.STATE_CONNECTED) {
updateAudioDeviceState()
} else if (state == BluetoothHeadset.STATE_DISCONNECTED) {
updateAudioDeviceState()
}
} else if (action == BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED) {
updateAudioDeviceState()
}
}
}
override fun start(audioManagerEvents: AudioManagerEvents?) {
Log.d(TAG, "start")
ThreadUtils.checkIsOnMainThread()
if (amState == AudioManagerState.RUNNING) {
Log.e(TAG, "AudioManager is already active")
return
}
this.audioManagerEvents = audioManagerEvents
amState = AudioManagerState.RUNNING
// Get the default adapter
mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter()
// Establish connection to the proxy.
mBluetoothAdapter?.getProfileProxy(appContext, mProfileListener, BluetoothProfile.A2DP)
// Store current audio state so we can restore it when stop() is called.
savedAudioMode = audioManager.mode
savedIsSpeakerPhoneOn = audioManager.isSpeakerphoneOn
savedIsMicrophoneMute = audioManager.isMicrophoneMute
hasWiredHeadset = hasWiredHeadset()
// Create an AudioManager.OnAudioFocusChangeListener instance.
audioFocusChangeListener =
AudioManager.OnAudioFocusChangeListener { focusChange ->
// Called on the listener to notify if the audio focus for this listener has been changed.
// The |focusChange| value indicates whether the focus was gained, whether the focus was lost,
// and whether that loss is transient, or whether the new focus holder will hold it for an
// unknown amount of time.
val typeOfChange = when (focusChange) {
AudioManager.AUDIOFOCUS_GAIN -> "AUDIOFOCUS_GAIN"
AudioManager.AUDIOFOCUS_GAIN_TRANSIENT -> "AUDIOFOCUS_GAIN_TRANSIENT"
AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE -> "AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE"
AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK -> "AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK"
AudioManager.AUDIOFOCUS_LOSS -> "AUDIOFOCUS_LOSS"
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> "AUDIOFOCUS_LOSS_TRANSIENT"
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> "AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK"
else -> "AUDIOFOCUS_INVALID"
}
Log.d(TAG, "onAudioFocusChange: $typeOfChange")
}
// Request audio play out focus (without ducking) and install listener for changes in focus.
val result = audioManager.requestAudioFocus(
audioFocusChangeListener,
AudioManager.STREAM_VOICE_CALL, AudioManager.AUDIOFOCUS_GAIN_TRANSIENT
)
if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
Log.d(TAG, "Audio focus request granted for VOICE_CALL streams")
} else {
Log.e(TAG, "Audio focus request failed")
}
// Start by setting MODE_IN_COMMUNICATION as default audio mode. It is
// required to be in this mode when playout and/or recording starts for
// best possible VoIP performance.
audioManager.mode = AudioManager.MODE_IN_COMMUNICATION
// Always disable microphone mute during a WebRTC call.
setMicrophoneMute(false)
// Set initial device states.
userSelectedAudioDevice = AudioDevice.NONE
selectedAudioDevice = AudioDevice.NONE
audioDevices.clear()
// Do initial selection of audio device. This setting can later be changed
// either by adding/removing a BT or wired headset or by covering/uncovering
// the proximity sensor.
updateAudioDeviceState()
// Register receiver for broadcast intents related to adding/removing a
// wired headset.
registerReceiver(wiredHeadsetReceiver, IntentFilter(Intent.ACTION_HEADSET_PLUG))
registerReceiver(
bluetoothReceiver,
IntentFilter(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED)
)
registerReceiver(
bluetoothReceiver,
IntentFilter(BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED)
)
Log.d(TAG, "AudioManager started")
}
override fun stop() {
Log.d(TAG, "stop")
ThreadUtils.checkIsOnMainThread()
if (amState != AudioManagerState.RUNNING) {
Log.e(
TAG,
"Trying to stop AudioManager in incorrect state: $amState"
)
return
}
amState = AudioManagerState.UNINITIALIZED
unregisterReceiver(wiredHeadsetReceiver)
unregisterReceiver(bluetoothReceiver)
// Restore previously stored audio states.
setSpeakerphoneOn(savedIsSpeakerPhoneOn)
setMicrophoneMute(savedIsMicrophoneMute)
// audioManager.mode = savedAudioMode
// Abandon audio focus. Gives the previous focus owner, if any, focus.
audioManager.abandonAudioFocus(audioFocusChangeListener)
audioFocusChangeListener = null
Log.d(TAG, "Abandoned audio focus for VOICE_CALL streams")
audioManagerEvents = null
Log.d(TAG, "AudioManager stopped")
}
/** Changes selection of the currently active audio device. */
private fun setAudioDeviceInternal(device: AudioDevice?) {
Log.d(TAG, "setAudioDeviceInternal(device=$device)")
if (audioDevices.contains(device)) {
when (device) {
AudioDevice.SPEAKER_PHONE -> setSpeakerphoneOn(true)
AudioDevice.EARPIECE -> setSpeakerphoneOn(false)
AudioDevice.WIRED_HEADSET -> setSpeakerphoneOn(false)
AudioDevice.BLUETOOTH -> setSpeakerphoneOn(false)
else -> Log.e(TAG, "Invalid audio device selection")
}
}
selectedAudioDevice = device
}
/**
* Changes default audio device.
*/
override fun setDefaultAudioDevice(defaultDevice: AudioDevice?) {
ThreadUtils.checkIsOnMainThread()
when (defaultDevice) {
AudioDevice.BLUETOOTH -> defaultAudioDevice = defaultDevice
AudioDevice.SPEAKER_PHONE -> defaultAudioDevice = defaultDevice
AudioDevice.EARPIECE -> defaultAudioDevice =
if (hasEarpiece()) defaultDevice else AudioDevice.SPEAKER_PHONE
else -> Log.e(TAG, "Invalid default audio device selection")
}
Log.d(TAG, "setDefaultAudioDevice(device=$defaultAudioDevice)")
updateAudioDeviceState()
}
/** Changes selection of the currently active audio device. */
override fun selectAudioDevice(device: AudioDevice) {
ThreadUtils.checkIsOnMainThread()
if (!audioDevices.contains(device)) {
Log.e(
TAG,
"Can not select $device from available $audioDevices"
)
}
userSelectedAudioDevice = device
selectedAudioDevice = device
updateAudioDeviceState()
}
/** Helper method for receiver registration. */
private fun registerReceiver(receiver: BroadcastReceiver, filter: IntentFilter) {
appContext.registerReceiver(receiver, filter)
}
/** Helper method for unregistration of an existing receiver. */
private fun unregisterReceiver(receiver: BroadcastReceiver) {
appContext.unregisterReceiver(receiver)
}
/** Sets the speaker phone mode. */
private fun setSpeakerphoneOn(on: Boolean) {
audioManager.isSpeakerphoneOn = on
}
/** Sets the microphone mute state. */
private fun setMicrophoneMute(on: Boolean) {
audioManager.isMicrophoneMute = on
}
/** Gets the current earpiece state. */
private fun hasEarpiece(): Boolean {
return appContext.packageManager.hasSystemFeature(PackageManager.FEATURE_TELEPHONY)
}
/**
* Checks whether a wired headset is connected or not.
* This is not a valid indication that audio playback is actually over
* the wired headset as audio routing depends on other conditions. We
* only use it as an early indicator (during initialization) of an attached
* wired headset.
*/
@Deprecated("")
private fun hasWiredHeadset(): Boolean {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
return audioManager.isWiredHeadsetOn
} else {
val devices = audioManager.getDevices(AudioManager.GET_DEVICES_INPUTS)
for (device: AudioDeviceInfo in devices) {
val type = device.type
if (type == AudioDeviceInfo.TYPE_WIRED_HEADSET) {
Log.d(TAG, "hasWiredHeadset: found wired headset")
return true
} else if (type == AudioDeviceInfo.TYPE_USB_DEVICE) {
Log.d(TAG, "hasWiredHeadset: found USB audio device")
return true
}
}
return false
}
}
private fun hasBluetoothHeadset(): Boolean =
mBluetoothAdapter?.let {
it.isEnabled && (it.getProfileConnectionState(BluetoothHeadset.HEADSET) == BluetoothHeadset.STATE_CONNECTED
|| it.getProfileConnectionState(BluetoothHeadset.A2DP) == BluetoothHeadset.STATE_CONNECTED)
} ?: false
/**
* Updates list of possible audio devices and make new device selection.
*/
fun updateAudioDeviceState() {
ThreadUtils.checkIsOnMainThread()
Log.d(
TAG, ("Device status: "
+ "available=" + audioDevices + ", "
+ "selected=" + selectedAudioDevice + ", "
+ "user selected=" + userSelectedAudioDevice)
)
// If a wired headset is connected, then it is the only possible option.
// Else if the bluetooth is connected then it is the only possible option
// In any other case we deal with the device audio hardware
// No wired headset, hence the audio-device list can contain speaker
// phone (on a tablet), or speaker phone and earpiece (on mobile phone).
// Update the set of available audio devices.
val hasBluetoothHeadset = hasBluetoothHeadset()
val newAudioDevices: MutableSet<AudioDevice?> = HashSet()
if (hasWiredHeadset) newAudioDevices.add(AudioDevice.WIRED_HEADSET)
if (hasBluetoothHeadset) newAudioDevices.add(AudioDevice.BLUETOOTH)
if (hasEarpiece()) newAudioDevices.add(AudioDevice.EARPIECE)
newAudioDevices.add(AudioDevice.SPEAKER_PHONE)
// Store state which is set to true if the device list has changed.
// And update the existing audio device set.
val audioDeviceSetUpdated = audioDevices != newAudioDevices
audioDevices = newAudioDevices
_audioDevices.postValue(audioDevices.toMutableList())
setAudioDeviceInternal(selectedAudioDevice)
audioManagerEvents?.onAudioDeviceChanged(selectedAudioDevice, audioDevices)
Log.d(TAG, "--- updateAudioDeviceState done")
}
companion object {
private val TAG = "WebRtcAudioManager"
private val SPEAKERPHONE_AUTO = "auto"
private val SPEAKERPHONE_TRUE = "true"
private val SPEAKERPHONE_FALSE = "false"
}
init {
ThreadUtils.checkIsOnMainThread()
audioManager = SystemServiceUtils.getAudioManager(appContext)
wiredHeadsetReceiver = WiredHeadsetReceiver()
bluetoothReceiver = BluetoothHeadsetReceiver()
amState = AudioManagerState.UNINITIALIZED
useSpeakerphone = SPEAKERPHONE_AUTO
defaultAudioDevice =
if (useSpeakerphone == SPEAKERPHONE_FALSE) AudioDevice.EARPIECE else AudioDevice.SPEAKER_PHONE
Log.d(TAG, "useSpeakerphone: $useSpeakerphone")
Log.d(TAG, "defaultAudioDevice: $defaultAudioDevice")
}
}
Из этого класса пользователь может наблюдать в действии, какое устройство в настоящее время выбрано в качестве вывода звука, а также он может также выбрать вывод звука. Вот часть активности, которая перенаправляет звук в соответствии с устройством вывода звука.
roomViewModel.webRtcAudioManager.start(object : WebRtcAudioManagerImpl.AudioManagerEvents {
override fun onAudioDeviceChanged(
selectedAudioDevice: WebRtcAudioManagerImpl.AudioDevice?,
availableAudioDevices: Set<WebRtcAudioManagerImpl.AudioDevice?>?
) {
when (selectedAudioDevice) {
WebRtcAudioManagerImpl.AudioDevice.WIRED_HEADSET -> {
audioManager.mode = AudioManager.MODE_IN_COMMUNICATION
audioManager.stopBluetoothSco()
audioManager.isBluetoothScoOn = false
audioManager.isSpeakerphoneOn = false
}
WebRtcAudioManagerImpl.AudioDevice.BLUETOOTH -> {
audioManager.mode = AudioManager.MODE_IN_COMMUNICATION
audioManager.startBluetoothSco()
audioManager.isBluetoothScoOn = true
audioManager.isSpeakerphoneOn = false
}
WebRtcAudioManagerImpl.AudioDevice.EARPIECE -> {
audioManager.mode = AudioManager.MODE_IN_COMMUNICATION
audioManager.stopBluetoothSco()
audioManager.isBluetoothScoOn = false
audioManager.isSpeakerphoneOn = false
}
WebRtcAudioManagerImpl.AudioDevice.SPEAKER_PHONE -> {
audioManager.mode = AudioManager.MODE_NORMAL
audioManager.stopBluetoothSco()
audioManager.isBluetoothScoOn = false
audioManager.isSpeakerphoneOn = true
}
else -> {
audioManager.mode = AudioManager.MODE_IN_COMMUNICATION
audioManager.stopBluetoothSco()
audioManager.isBluetoothScoOn = false
audioManager.isSpeakerphoneOn = false
}
}
}
})
и где-то в коде есть выбор устройства, отображаемого в listView
viewAdapter = AudioDeviceAdapter(object: RecyclerCallback.AudioDeviceCallback {
override fun onAudioDeviceClicked(audioDevice: WebRtcAudioManagerImpl.AudioDevice) {
// Here we select BLUETOOTH / WIRED_HEADSET / SPEAKER / EARPIECE
roomViewModel.webRtcAudioManager.selectAudioDevice(audioDevice)
}
})
У меня две проблемы.
Всякий раз, когда у меня подключено устройство Bluetooth, звук идет на Bluetooth, когда устройства Bluetooth подключаются к мобильному телефону. Как только я выберу другой выход (например, динамик), звук перейдет в динамик, как и ожидалось. С этого момента, если я хочу снова услышать звук из Bluetooth, выбрав Bluetooth в качестве устройства вывода, звук поступает только в динамик мобильного телефона.
Я хочу, чтобы устройство Bluetooth было устройством по умолчанию, когда оно доступно другим динамиком мобильного телефона.
Может кто-нибудь понять, как решить эти проблемы? Спасибо