mirror of
https://gitlab.linphone.org/BC/public/linphone-android.git
synced 2026-01-17 19:38:08 +00:00
Trying to route audio using androidx Telecom API...
This commit is contained in:
parent
39ad8347c7
commit
476eabd0fe
5 changed files with 142 additions and 56 deletions
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue