VoIP: add select answer

This commit is contained in:
ganfra 2020-11-13 16:32:45 +01:00
parent ba11ca0e9d
commit 03e89743b4
7 changed files with 78 additions and 2 deletions

View File

@ -21,6 +21,7 @@ import org.matrix.android.sdk.api.session.room.model.call.CallCandidatesContent
import org.matrix.android.sdk.api.session.room.model.call.CallHangupContent import org.matrix.android.sdk.api.session.room.model.call.CallHangupContent
import org.matrix.android.sdk.api.session.room.model.call.CallInviteContent import org.matrix.android.sdk.api.session.room.model.call.CallInviteContent
import org.matrix.android.sdk.api.session.room.model.call.CallRejectContent import org.matrix.android.sdk.api.session.room.model.call.CallRejectContent
import org.matrix.android.sdk.api.session.room.model.call.CallSelectAnswerContent
interface CallListener { interface CallListener {
/** /**
@ -45,5 +46,10 @@ interface CallListener {
*/ */
fun onCallRejectReceived(callRejectContent: CallRejectContent) fun onCallRejectReceived(callRejectContent: CallRejectContent)
/**
* Called when an answer has been selected
*/
fun onCallSelectAnswerReceived(callSelectAnswerContent: CallSelectAnswerContent)
fun onCallManagedByOtherSession(callId: String) fun onCallManagedByOtherSession(callId: String)
} }

View File

@ -51,6 +51,11 @@ interface MxCall : MxCallDetail {
*/ */
fun accept(sdp: SessionDescription) fun accept(sdp: SessionDescription)
/**
* This has to be sent by the caller's client once it has chosen an answer.
*/
fun selectAnswer()
/** /**
* Reject an incoming call * Reject an incoming call
*/ */

View File

@ -40,7 +40,8 @@ data class CallHangupContent(
/** /**
* Optional error reason for the hangup. This should not be provided when the user naturally ends or rejects the call. * Optional error reason for the hangup. This should not be provided when the user naturally ends or rejects the call.
* When there was an error in the call negotiation, this should be `ice_failed` for when ICE negotiation fails * When there was an error in the call negotiation, this should be `ice_failed` for when ICE negotiation fails
* or `invite_timeout` for when the other party did not answer in time. One of: ["ice_failed", "invite_timeout"] * or `invite_timeout` for when the other party did not answer in time.
* One of: ["ice_failed", "invite_timeout"]
*/ */
@Json(name = "reason") val reason: Reason? = null @Json(name = "reason") val reason: Reason? = null
) : CallSignallingContent { ) : CallSignallingContent {

View File

@ -24,6 +24,7 @@ import org.matrix.android.sdk.api.session.room.model.call.CallCandidatesContent
import org.matrix.android.sdk.api.session.room.model.call.CallHangupContent import org.matrix.android.sdk.api.session.room.model.call.CallHangupContent
import org.matrix.android.sdk.api.session.room.model.call.CallInviteContent import org.matrix.android.sdk.api.session.room.model.call.CallInviteContent
import org.matrix.android.sdk.api.session.room.model.call.CallRejectContent import org.matrix.android.sdk.api.session.room.model.call.CallRejectContent
import org.matrix.android.sdk.api.session.room.model.call.CallSelectAnswerContent
/** /**
* Dispatch each method safely to all listeners. * Dispatch each method safely to all listeners.
@ -54,6 +55,10 @@ class CallListenersDispatcher(private val listeners: Set<CallListener>) : CallLi
it.onCallManagedByOtherSession(callId) it.onCallManagedByOtherSession(callId)
} }
override fun onCallSelectAnswerReceived(callSelectAnswerContent: CallSelectAnswerContent) = dispatch {
it.onCallSelectAnswerReceived(callSelectAnswerContent)
}
private fun dispatch(lambda: (CallListener) -> Unit) { private fun dispatch(lambda: (CallListener) -> Unit) {
listeners.toList().forEach { listeners.toList().forEach {
tryOrNull { tryOrNull {

View File

@ -31,6 +31,7 @@ import org.matrix.android.sdk.api.session.room.model.call.CallCandidatesContent
import org.matrix.android.sdk.api.session.room.model.call.CallHangupContent import org.matrix.android.sdk.api.session.room.model.call.CallHangupContent
import org.matrix.android.sdk.api.session.room.model.call.CallInviteContent import org.matrix.android.sdk.api.session.room.model.call.CallInviteContent
import org.matrix.android.sdk.api.session.room.model.call.CallRejectContent import org.matrix.android.sdk.api.session.room.model.call.CallRejectContent
import org.matrix.android.sdk.api.session.room.model.call.CallSelectAnswerContent
import org.matrix.android.sdk.api.session.room.model.call.CallSignallingContent import org.matrix.android.sdk.api.session.room.model.call.CallSignallingContent
import org.matrix.android.sdk.api.util.Cancelable import org.matrix.android.sdk.api.util.Cancelable
import org.matrix.android.sdk.api.util.NoOpCancellable import org.matrix.android.sdk.api.util.NoOpCancellable
@ -152,9 +153,31 @@ internal class DefaultCallSignalingService @Inject constructor(
EventType.CALL_CANDIDATES -> { EventType.CALL_CANDIDATES -> {
handleCallCandidatesEvent(event) handleCallCandidatesEvent(event)
} }
EventType.CALL_SELECT_ANSWER -> {
handleCallSelectAnswerEvent(event)
}
} }
} }
private fun handleCallSelectAnswerEvent(event: Event) {
val content = event.getClearContent().toModel<CallSelectAnswerContent>() ?: return
val call = content.getCall() ?: return
if (call.ourPartyId == content.partyId) {
// Ignore remote echo
return
}
if (call.isOutgoing) {
Timber.v("Got selectAnswer for an outbound call: ignoring")
return
}
val selectedPartyId = content.selectedPartyId
if (selectedPartyId == null) {
Timber.w("Got nonsensical select_answer with null selected_party_id: ignoring")
return
}
callListenersDispatcher.onCallSelectAnswerReceived(content)
}
private fun handleCallCandidatesEvent(event: Event) { private fun handleCallCandidatesEvent(event: Event) {
val content = event.getClearContent().toModel<CallCandidatesContent>() ?: return val content = event.getClearContent().toModel<CallCandidatesContent>() ?: return
val call = content.getCall() ?: return val call = content.getCall() ?: return
@ -185,7 +208,7 @@ internal class DefaultCallSignalingService @Inject constructor(
val content = event.getClearContent().toModel<CallHangupContent>() ?: return val content = event.getClearContent().toModel<CallHangupContent>() ?: return
val call = content.getCall() ?: return val call = content.getCall() ?: return
if (call.state != CallState.Terminated) { if (call.state != CallState.Terminated) {
// Need to check for party_id? // Need to check for party_id?
activeCallHandler.removeCall(content.callId) activeCallHandler.removeCall(content.callId)
callListenersDispatcher.onCallHangupReceived(content) callListenersDispatcher.onCallHangupReceived(content)
} }

View File

@ -29,6 +29,7 @@ import org.matrix.android.sdk.api.session.room.model.call.CallCandidatesContent
import org.matrix.android.sdk.api.session.room.model.call.CallHangupContent import org.matrix.android.sdk.api.session.room.model.call.CallHangupContent
import org.matrix.android.sdk.api.session.room.model.call.CallInviteContent import org.matrix.android.sdk.api.session.room.model.call.CallInviteContent
import org.matrix.android.sdk.api.session.room.model.call.CallRejectContent import org.matrix.android.sdk.api.session.room.model.call.CallRejectContent
import org.matrix.android.sdk.api.session.room.model.call.CallSelectAnswerContent
import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.api.util.Optional
import org.matrix.android.sdk.internal.session.call.DefaultCallSignalingService import org.matrix.android.sdk.internal.session.call.DefaultCallSignalingService
import org.matrix.android.sdk.internal.session.room.send.queue.EventSenderProcessor import org.matrix.android.sdk.internal.session.room.send.queue.EventSenderProcessor
@ -143,6 +144,7 @@ internal class MxCallImpl(
CallHangupContent( CallHangupContent(
callId = callId, callId = callId,
partyId = ourPartyId, partyId = ourPartyId,
reason = CallHangupContent.Reason.USER_HANGUP
) )
.let { createEventAndLocalEcho(type = EventType.CALL_HANGUP, roomId = roomId, content = it.toContent()) } .let { createEventAndLocalEcho(type = EventType.CALL_HANGUP, roomId = roomId, content = it.toContent()) }
.also { eventSenderProcessor.postEvent(it) } .also { eventSenderProcessor.postEvent(it) }
@ -162,6 +164,19 @@ internal class MxCallImpl(
.also { eventSenderProcessor.postEvent(it) } .also { eventSenderProcessor.postEvent(it) }
} }
override fun selectAnswer() {
Timber.v("## VOIP select answer $callId")
if (isOutgoing) return
state = CallState.Answering
CallSelectAnswerContent(
callId = callId,
partyId = ourPartyId,
selectedPartyId = opponentPartyId?.getOrNull()
)
.let { createEventAndLocalEcho(type = EventType.CALL_SELECT_ANSWER, roomId = roomId, content = it.toContent()) }
.also { eventSenderProcessor.postEvent(it) }
}
private fun createEventAndLocalEcho(localId: String = LocalEcho.createLocalEchoId(), type: String, roomId: String, content: Content): Event { private fun createEventAndLocalEcho(localId: String = LocalEcho.createLocalEchoId(), type: String, roomId: String, content: Content): Event {
return Event( return Event(
roomId = roomId, roomId = roomId,

View File

@ -31,6 +31,7 @@ import io.reactivex.disposables.Disposable
import io.reactivex.subjects.PublishSubject import io.reactivex.subjects.PublishSubject
import io.reactivex.subjects.ReplaySubject import io.reactivex.subjects.ReplaySubject
import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.call.CallState import org.matrix.android.sdk.api.session.call.CallState
@ -43,6 +44,7 @@ import org.matrix.android.sdk.api.session.room.model.call.CallCandidatesContent
import org.matrix.android.sdk.api.session.room.model.call.CallHangupContent import org.matrix.android.sdk.api.session.room.model.call.CallHangupContent
import org.matrix.android.sdk.api.session.room.model.call.CallInviteContent import org.matrix.android.sdk.api.session.room.model.call.CallInviteContent
import org.matrix.android.sdk.api.session.room.model.call.CallRejectContent import org.matrix.android.sdk.api.session.room.model.call.CallRejectContent
import org.matrix.android.sdk.api.session.room.model.call.CallSelectAnswerContent
import org.webrtc.AudioSource import org.webrtc.AudioSource
import org.webrtc.AudioTrack import org.webrtc.AudioTrack
import org.webrtc.Camera1Enumerator import org.webrtc.Camera1Enumerator
@ -303,6 +305,10 @@ class WebRtcPeerConnectionManager @Inject constructor(
callContext.peerConnection?.setLocalDescription(object : SdpObserverAdapter() {}, p0) callContext.peerConnection?.setLocalDescription(object : SdpObserverAdapter() {}, p0)
// send offer to peer // send offer to peer
currentCall?.mxCall?.offerSdp(p0) currentCall?.mxCall?.offerSdp(p0)
if(currentCall?.mxCall?.opponentPartyId?.hasValue().orFalse()){
currentCall?.mxCall?.selectAnswer()
}
} }
}, constraints) }, constraints)
} }
@ -884,6 +890,21 @@ class WebRtcPeerConnectionManager @Inject constructor(
endCall(false) endCall(false)
} }
override fun onCallSelectAnswerReceived(callSelectAnswerContent: CallSelectAnswerContent) {
val call = currentCall ?: return
if (call.mxCall.callId != callSelectAnswerContent.callId) return Unit.also {
Timber.w("onCallSelectAnswerReceived for non active call? ${callSelectAnswerContent.callId}")
}
val selectedPartyId = callSelectAnswerContent.selectedPartyId
if (selectedPartyId != call.mxCall.ourPartyId) {
Timber.i("Got select_answer for party ID ${selectedPartyId}: we are party ID ${call.mxCall.ourPartyId}.");
// The other party has picked somebody else's answer
call.mxCall.state = CallState.Terminated
endCall(false)
}
}
override fun onCallManagedByOtherSession(callId: String) { override fun onCallManagedByOtherSession(callId: String) {
Timber.v("## VOIP onCallManagedByOtherSession: $callId") Timber.v("## VOIP onCallManagedByOtherSession: $callId")
currentCall = null currentCall = null