Trying to route audio using androidx Telecom API...

This commit is contained in:
Sylvain Berfini 2023-08-28 17:06:35 +02:00
parent 39ad8347c7
commit 476eabd0fe
5 changed files with 142 additions and 56 deletions

View file

@ -54,7 +54,7 @@ class CoreContext @UiThread constructor(val context: Context) : HandlerThread("C
val notificationsManager = NotificationsManager(context)
private val telecomManager = TelecomManager(context)
val telecomManager = TelecomManager(context)
private val activityMonitor = ActivityMonitor()

View file

@ -23,14 +23,18 @@ import android.telecom.DisconnectCause
import androidx.core.telecom.CallAttributesCompat
import androidx.core.telecom.CallControlCallback
import androidx.core.telecom.CallControlScope
import androidx.core.telecom.CallEndpointCompat
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.core.AudioDevice
import org.linphone.core.Call
import org.linphone.core.CallListenerStub
import org.linphone.core.tools.Log
import org.linphone.utils.AudioRouteUtils
class TelecomCallControlCallback constructor(
private val call: Call,
@ -41,14 +45,21 @@ class TelecomCallControlCallback constructor(
private const val TAG = "[Telecom Call Control Callback]"
}
private var availableEndpoints: List<CallEndpointCompat> = arrayListOf()
private val callListener = object : CallListenerStub() {
override fun onStateChanged(call: Call, state: Call.State?, message: String) {
Log.i("$TAG Call state changed [$state]")
if (state == Call.State.Connected) {
if (call.dir == Call.Dir.Incoming) {
scope.launch {
Log.i("$TAG Answering call")
callControl.answer(CallAttributesCompat.CALL_TYPE_AUDIO_CALL) // TODO
val type = if (call.currentParams.isVideoEnabled) {
CallAttributesCompat.CALL_TYPE_VIDEO_CALL
} else {
CallAttributesCompat.CALL_TYPE_AUDIO_CALL
}
Log.i("$TAG Answering call with type [$type]")
callControl.answer(type)
}
} else {
scope.launch {
@ -90,6 +101,7 @@ class TelecomCallControlCallback constructor(
callControl.availableEndpoints.onEach { list ->
Log.i("$TAG New available audio endpoints list")
availableEndpoints = list
for (endpoint in list) {
Log.i("$TAG Available audio endpoint [${endpoint.name}]")
}
@ -97,6 +109,30 @@ class TelecomCallControlCallback constructor(
callControl.currentCallEndpoint.onEach { endpoint ->
Log.i("$TAG We're asked to use [${endpoint.name}] audio endpoint")
// Change audio route in SDK, this way the usual listener will trigger
// and we'll be able to update the UI accordingly
val route = arrayListOf<AudioDevice.Type>()
when (endpoint.type) {
CallEndpointCompat.Companion.TYPE_EARPIECE -> {
route.add(AudioDevice.Type.Earpiece)
}
CallEndpointCompat.Companion.TYPE_SPEAKER -> {
route.add(AudioDevice.Type.Speaker)
}
CallEndpointCompat.Companion.TYPE_BLUETOOTH -> {
route.add(AudioDevice.Type.Bluetooth)
}
CallEndpointCompat.Companion.TYPE_WIRED_HEADSET -> {
route.add(AudioDevice.Type.Headphones)
route.add(AudioDevice.Type.Headset)
}
else -> null
}
if (route.isNotEmpty()) {
coreContext.postOnCoreThread {
AudioRouteUtils.applyAudioRouteChangeInLinphone(call, route)
}
}
}.launchIn(scope)
callControl.isMuted.onEach { muted ->
@ -105,12 +141,63 @@ class TelecomCallControlCallback constructor(
}.launchIn(scope)
}
fun applyAudioRouteToCallWithId(routes: List<AudioDevice.Type>) {
Log.i("$TAG Looking for audio endpoint with type [${routes.first()}]")
for (endpoint in availableEndpoints) {
Log.i(
"$TAG Found audio endpoint [${endpoint.name}] with type [${endpoint.type}]"
)
val found = when (endpoint.type) {
CallEndpointCompat.Companion.TYPE_EARPIECE -> {
routes.find { it == AudioDevice.Type.Earpiece }
}
CallEndpointCompat.Companion.TYPE_SPEAKER -> {
routes.find { it == AudioDevice.Type.Speaker }
}
CallEndpointCompat.Companion.TYPE_BLUETOOTH -> {
routes.find { it == AudioDevice.Type.Bluetooth }
}
CallEndpointCompat.Companion.TYPE_WIRED_HEADSET -> {
routes.find { it == AudioDevice.Type.Headset || it == AudioDevice.Type.Headphones }
}
else -> null
}
if (found != null) {
Log.i(
"$TAG Found matching audio endpoint [${endpoint.name}], trying to use it"
)
scope.launch {
Log.i("$TAG Requesting audio endpoint change with [${endpoint.name}]")
var audioRouteUpdated = callControl.requestEndpointChange(endpoint)
var attempts = 1
while (!audioRouteUpdated && attempts <= 10) {
delay(100)
Log.i("$TAG Requesting audio endpoint change with [${endpoint.name}]")
audioRouteUpdated = callControl.requestEndpointChange(endpoint)
attempts += 1
}
if (!audioRouteUpdated) {
Log.e("$TAG Failed to change endpoint audio device!")
} else {
Log.i("$TAG It took [$attempts] to change endpoint audio device...")
}
}
} else {
Log.w("$TAG No matching audio endpoint found...")
}
}
}
override suspend fun onAnswer(callType: Int): Boolean {
Log.i("$TAG We're asked to answer the call")
Log.i("$TAG We're asked to answer the call with type [$callType]")
coreContext.postOnCoreThread {
if (call.state == Call.State.IncomingReceived || call.state == Call.State.IncomingEarlyMedia) {
Log.i("$TAG Answering call")
call.accept()
coreContext.answerCall(call) // TODO: use call type
}
}
return true
@ -120,7 +207,7 @@ class TelecomCallControlCallback constructor(
Log.i("$TAG We're asked to terminate the call with reason [$disconnectCause]")
coreContext.postOnCoreThread {
Log.i("$TAG Terminating call")
call.terminate()
call.terminate() // TODO: use cause
}
return true
}

View file

@ -29,6 +29,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.core.AudioDevice
import org.linphone.core.Call
import org.linphone.core.Core
import org.linphone.core.CoreListenerStub
@ -77,7 +78,7 @@ class TelecomManager @WorkerThread constructor(context: Context) {
coreContext.postOnCoreThread {
val callId = call.callLog.callId.orEmpty()
if (callId.isNotEmpty()) {
Log.i("$TAG Storing callbacks (why?) for call ID [$callId]")
Log.i("$TAG Storing our callbacks for call ID [$callId]")
map[callId] = callbacks
}
}
@ -108,4 +109,19 @@ class TelecomManager @WorkerThread constructor(context: Context) {
Log.i("$TAG Core is being stopped")
core.removeListener(coreListener)
}
@WorkerThread
fun applyAudioRouteToCallWithId(routes: List<AudioDevice.Type>, callId: String): Boolean {
Log.i(
"$TAG Looking for audio endpoint with type [${routes.first()}] for call with ID [$callId]"
)
val callControlCallback = map[callId]
if (callControlCallback == null) {
Log.w("$TAG Failed to find callbacks for call with ID [$callId]")
return false
}
callControlCallback.applyAudioRouteToCallWithId(routes)
return true
}
}

View file

@ -182,7 +182,7 @@ class CurrentCallViewModel @UiThread constructor() : ViewModel() {
coreContext.postOnCoreThread {
if (::call.isInitialized) {
Log.i("$TAG Answering call [$call]")
call.accept()
coreContext.answerCall(call)
}
}
}
@ -244,7 +244,12 @@ class CurrentCallViewModel @UiThread constructor() : ViewModel() {
coreContext.postOnCoreThread {
Log.i("$TAG Selected audio device with ID [${device.id}]")
if (::call.isInitialized) {
call.outputAudioDevice = device
when {
isHeadset -> AudioRouteUtils.routeAudioToHeadset(call)
isBluetooth -> AudioRouteUtils.routeAudioToBluetooth(call)
isSpeaker -> AudioRouteUtils.routeAudioToSpeaker(call)
else -> AudioRouteUtils.routeAudioToEarpiece(call)
}
}
}
}

View file

@ -29,31 +29,6 @@ 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))
@ -85,10 +60,8 @@ class AudioRouteUtils {
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)
applyAudioRouteChange(null, types)
}
}
@ -96,7 +69,8 @@ class AudioRouteUtils {
private fun applyAudioRouteChange(
call: Call?,
types: List<AudioDevice.Type>,
output: Boolean = true
output: Boolean = true,
skipTelecom: Boolean = false
) {
val currentCall = if (coreContext.core.callsNb > 0) {
call ?: coreContext.core.currentCall ?: coreContext.core.calls[0]
@ -105,6 +79,25 @@ class AudioRouteUtils {
null
}
if (!skipTelecom) {
val callId = currentCall?.callLog?.callId.orEmpty()
val success = coreContext.telecomManager.applyAudioRouteToCallWithId(types, callId)
if (!success) {
Log.w("$TAG Failed to change audio endpoint to [$types] for call ID [$callId]")
applyAudioRouteChange(currentCall, types, output, skipTelecom = true)
} else {
return
}
}
applyAudioRouteChangeInLinphone(currentCall, types, output)
}
fun applyAudioRouteChangeInLinphone(
call: Call?,
types: List<AudioDevice.Type>,
output: Boolean = true
) {
val capability = if (output) {
AudioDevice.Capabilities.CapabilityPlay
} else {
@ -147,14 +140,14 @@ class AudioRouteUtils {
}
return
}
if (currentCall != null) {
if (call != 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
call.outputAudioDevice = audioDevice
} else {
currentCall.inputAudioDevice = audioDevice
call.inputAudioDevice = audioDevice
}
} else {
Log.i(
@ -167,20 +160,5 @@ class AudioRouteUtils {
}
}
}
@WorkerThread
private fun changeCaptureDeviceToMatchAudioRoute(call: Call?, types: List<AudioDevice.Type>) {
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()}")
}
}
}
}
}