From c79f2896dacf1e515328296ed3c3a8544224a56c Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Thu, 24 Aug 2023 16:31:23 +0200 Subject: [PATCH] Started audio routes --- .../ui/voip/viewmodel/CurrentCallViewModel.kt | 60 ++++-- .../org/linphone/utils/AudioRouteUtils.kt | 173 ++++++++++++++++++ .../res/layout/voip_call_main_actions.xml | 2 +- 3 files changed, 218 insertions(+), 17 deletions(-) create mode 100644 app/src/main/java/org/linphone/utils/AudioRouteUtils.kt diff --git a/app/src/main/java/org/linphone/ui/voip/viewmodel/CurrentCallViewModel.kt b/app/src/main/java/org/linphone/ui/voip/viewmodel/CurrentCallViewModel.kt index e80702c07..a56b61de1 100644 --- a/app/src/main/java/org/linphone/ui/voip/viewmodel/CurrentCallViewModel.kt +++ b/app/src/main/java/org/linphone/ui/voip/viewmodel/CurrentCallViewModel.kt @@ -30,12 +30,14 @@ import androidx.lifecycle.ViewModel import java.util.Locale import org.linphone.LinphoneApplication.Companion.coreContext import org.linphone.R +import org.linphone.core.AudioDevice import org.linphone.core.Call import org.linphone.core.CallListenerStub import org.linphone.core.MediaDirection import org.linphone.core.MediaEncryption import org.linphone.core.tools.Log import org.linphone.ui.main.contacts.model.ContactAvatarModel +import org.linphone.utils.AudioRouteUtils import org.linphone.utils.Event import org.linphone.utils.LinphoneUtils @@ -105,10 +107,18 @@ class CurrentCallViewModel @UiThread constructor() : ViewModel() { @WorkerThread override fun onStateChanged(call: Call, state: Call.State?, message: String) { + if (CurrentCallViewModel@call != call) { + return + } + if (LinphoneUtils.isCallOutgoing(call.state)) { isVideoEnabled.postValue(call.params.isVideoEnabled) } else { val videoEnabled = call.currentParams.isVideoEnabled + if (videoEnabled && isVideoEnabled.value == false) { + Log.i("$TAG Video enabled, routing audio to speaker") + AudioRouteUtils.routeAudioToSpeaker(call) + } isVideoEnabled.postValue(videoEnabled) // Toggle full screen OFF when remote disables video @@ -117,6 +127,12 @@ class CurrentCallViewModel @UiThread constructor() : ViewModel() { } } } + + @WorkerThread + override fun onAudioDeviceChanged(call: Call, audioDevice: AudioDevice) { + Log.i("$TAG Audio device changed [$audioDevice]") + isSpeakerEnabled.postValue(AudioRouteUtils.isSpeakerAudioRouteCurrentlyUsed(call)) + } } init { @@ -133,8 +149,9 @@ class CurrentCallViewModel @UiThread constructor() : ViewModel() { call = currentCall Log.i("$TAG Found call [$call]") configureCall(call) + isSpeakerEnabled.postValue(AudioRouteUtils.isSpeakerAudioRouteCurrentlyUsed(call)) } else { - Log.e("$TAG Failed to find outgoing call!") + Log.e("$TAG Failed to find call!") } showSwitchCamera.postValue(coreContext.showSwitchCameraButton()) @@ -155,16 +172,20 @@ class CurrentCallViewModel @UiThread constructor() : ViewModel() { @UiThread fun answer() { coreContext.postOnCoreThread { - Log.i("$TAG Answering call [$call]") - call.accept() + if (::call.isInitialized) { + Log.i("$TAG Answering call [$call]") + call.accept() + } } } @UiThread fun hangUp() { coreContext.postOnCoreThread { - Log.i("$TAG Terminating call [$call]") - call.terminate() + if (::call.isInitialized) { + Log.i("$TAG Terminating call [$call]") + call.terminate() + } } } @@ -187,15 +208,30 @@ class CurrentCallViewModel @UiThread constructor() : ViewModel() { // TODO: request record audio permission return } + coreContext.postOnCoreThread { - call.microphoneMuted = !call.microphoneMuted - isMicrophoneMuted.postValue(call.microphoneMuted) + if (::call.isInitialized) { + call.microphoneMuted = !call.microphoneMuted + isMicrophoneMuted.postValue(call.microphoneMuted) + } } } @UiThread fun changeAudioOutputDevice() { - // TODO: display list of all output devices + // TODO: display list of all output devices if more then earpiece & + + val routeAudioToSpeaker = isSpeakerEnabled.value != true + + coreContext.postOnCoreThread { + if (::call.isInitialized) { + if (routeAudioToSpeaker) { + AudioRouteUtils.routeAudioToSpeaker(call) + } else { + AudioRouteUtils.routeAudioToEarpiece(call) + } + } + } } @UiThread @@ -258,14 +294,6 @@ class CurrentCallViewModel @UiThread constructor() : ViewModel() { } } - @WorkerThread - fun forceShowZrtpSasDialog() { - val authToken = call.authenticationToken - if (authToken.orEmpty().isNotEmpty()) { - showZrtpSasDialog(authToken!!.uppercase(Locale.getDefault())) - } - } - @WorkerThread private fun showZrtpSasDialog(authToken: String) { val toRead: String diff --git a/app/src/main/java/org/linphone/utils/AudioRouteUtils.kt b/app/src/main/java/org/linphone/utils/AudioRouteUtils.kt new file mode 100644 index 000000000..7d4bc2ec9 --- /dev/null +++ b/app/src/main/java/org/linphone/utils/AudioRouteUtils.kt @@ -0,0 +1,173 @@ +/* + * Copyright (c) 2010-2023 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.linphone.utils + +import androidx.annotation.WorkerThread +import org.linphone.LinphoneApplication.Companion.coreContext +import org.linphone.core.AudioDevice +import org.linphone.core.Call +import org.linphone.core.tools.Log + +class AudioRouteUtils { + companion object { + private const val TAG = "[Audio Route Utils]" + + @WorkerThread + fun isSpeakerAudioRouteCurrentlyUsed(call: Call? = null): Boolean { + val currentCall = if (coreContext.core.callsNb > 0) { + call ?: coreContext.core.currentCall ?: coreContext.core.calls[0] + } else { + Log.w("$TAG No call found, checking audio route on Core") + null + } + val conference = coreContext.core.conference + + val audioDevice = if (conference != null && conference.isIn) { + conference.outputAudioDevice + } else if (currentCall != null) { + currentCall.outputAudioDevice + } else { + coreContext.core.outputAudioDevice + } + + if (audioDevice == null) return false + Log.i( + "$TAG Playback audio device currently in use is [${audioDevice.deviceName} (${audioDevice.driverName}) ${audioDevice.type}]" + ) + return audioDevice.type == AudioDevice.Type.Speaker + } + + @WorkerThread + fun routeAudioToEarpiece(call: Call? = null) { + routeAudioTo(call, arrayListOf(AudioDevice.Type.Earpiece)) + } + + @WorkerThread + fun routeAudioToSpeaker(call: Call? = null) { + routeAudioTo(call, arrayListOf(AudioDevice.Type.Speaker)) + } + + @WorkerThread + private fun routeAudioTo( + call: Call?, + types: List + ) { + val currentCall = call ?: coreContext.core.currentCall ?: coreContext.core.calls.firstOrNull() + if (currentCall != null) { + applyAudioRouteChange(currentCall, types) + changeCaptureDeviceToMatchAudioRoute(currentCall, types) + } else { + applyAudioRouteChange(call, types) + changeCaptureDeviceToMatchAudioRoute(call, types) + } + } + + @WorkerThread + private fun applyAudioRouteChange( + call: Call?, + types: List, + output: Boolean = true + ) { + val currentCall = if (coreContext.core.callsNb > 0) { + call ?: coreContext.core.currentCall ?: coreContext.core.calls[0] + } else { + Log.w("$TAG No call found, setting audio route on Core") + null + } + + val capability = if (output) { + AudioDevice.Capabilities.CapabilityPlay + } else { + AudioDevice.Capabilities.CapabilityRecord + } + val preferredDriver = if (output) { + coreContext.core.defaultOutputAudioDevice?.driverName + } else { + coreContext.core.defaultInputAudioDevice?.driverName + } + + val extendedAudioDevices = coreContext.core.extendedAudioDevices + Log.i( + "$TAG Looking for an ${if (output) "output" else "input"} audio device with capability [$capability], driver name [$preferredDriver] and type [$types] in extended audio devices list (size ${extendedAudioDevices.size})" + ) + val foundAudioDevice = extendedAudioDevices.find { + it.driverName == preferredDriver && types.contains(it.type) && it.hasCapability( + capability + ) + } + val audioDevice = if (foundAudioDevice == null) { + Log.w( + "$TAG Failed to find an audio device with capability [$capability], driver name [$preferredDriver] and type [$types]" + ) + extendedAudioDevices.find { + types.contains(it.type) && it.hasCapability(capability) + } + } else { + foundAudioDevice + } + + if (audioDevice == null) { + Log.e( + "$TAG Couldn't find audio device with capability [$capability] and type [$types]" + ) + for (device in extendedAudioDevices) { + Log.i( + "$TAG Extended audio device: [${device.deviceName} (${device.driverName}) ${device.type} / ${device.capabilities}]" + ) + } + return + } + if (currentCall != null) { + Log.i( + "$TAG Found [${audioDevice.type}] ${if (output) "playback" else "recorder"} audio device [${audioDevice.deviceName} (${audioDevice.driverName})], routing call audio to it" + ) + if (output) { + currentCall.outputAudioDevice = audioDevice + } else { + currentCall.inputAudioDevice = audioDevice + } + } else { + Log.i( + "$TAG Found [${audioDevice.type}] ${if (output) "playback" else "recorder"} audio device [${audioDevice.deviceName} (${audioDevice.driverName})], changing core default audio device" + ) + if (output) { + coreContext.core.outputAudioDevice = audioDevice + } else { + coreContext.core.inputAudioDevice = audioDevice + } + } + } + + @WorkerThread + private fun changeCaptureDeviceToMatchAudioRoute(call: Call?, types: List) { + when (types.first()) { + AudioDevice.Type.Earpiece, AudioDevice.Type.Speaker -> { + Log.i( + "$TAG Audio route requested to Earpiece or Speaker, setting input to Microphone" + ) + applyAudioRouteChange(call, (arrayListOf(AudioDevice.Type.Microphone)), false) + } + else -> { + Log.w("$TAG Unexpected audio device type: ${types.first()}") + } + } + } + } +} diff --git a/app/src/main/res/layout/voip_call_main_actions.xml b/app/src/main/res/layout/voip_call_main_actions.xml index 585b9d7a6..e4917bd41 100644 --- a/app/src/main/res/layout/voip_call_main_actions.xml +++ b/app/src/main/res/layout/voip_call_main_actions.xml @@ -81,7 +81,7 @@ android:layout_height="@dimen/voip_button_size" android:layout_marginEnd="30dp" android:padding="@dimen/voip_button_icon_padding" - android:src="@{viewModel.isSpeakerEnabled ? @drawable/speaker_slash : @drawable/speaker_high, default=@drawable/speaker_slash}" + android:src="@{viewModel.isSpeakerEnabled ? @drawable/speaker_high : @drawable/speaker_slash, default=@drawable/speaker_slash}" android:background="@drawable/in_call_button_background" app:tint="@color/white" app:layout_constraintBottom_toBottomOf="parent"