VoIP: remove dependency over WebRtc on SDK

This commit is contained in:
ganfra 2020-11-20 12:19:30 +01:00
parent f960cf2ce9
commit be3bfe7e5e
16 changed files with 157 additions and 94 deletions

View file

@ -178,12 +178,6 @@ dependencies {
// Phone number https://github.com/google/libphonenumber
implementation 'com.googlecode.libphonenumber:libphonenumber:8.10.23'
// Web RTC
// org.webrtc:google-webrtc is for development purposes only. See http://webrtc.github.io/webrtc-org/native-code/android/
// implementation 'org.webrtc:google-webrtc:1.0.+'
// Use the same WebRTC library than the one used by Jitsi library
implementation('com.facebook.react:react-native-webrtc:1.84.0-jitsi-5112273@aar')
testImplementation 'junit:junit:4.13'
testImplementation 'org.robolectric:robolectric:4.3'
//testImplementation 'org.robolectric:shadows-support-v4:3.0'

View file

@ -16,7 +16,6 @@
package org.matrix.android.sdk.api.session.call
import org.webrtc.PeerConnection
sealed class CallState {
@ -42,7 +41,7 @@ sealed class CallState {
* Notice that the PeerState failed is not always final, if you switch network, new ice candidtates
* could be exchanged, and the connection could go back to connected
* */
data class Connected(val iceConnectionState: PeerConnection.PeerConnectionState) : CallState()
data class Connected(val iceConnectionState: MxPeerConnectionState) : CallState()
/** Terminated. Incoming/Outgoing call, the call is terminated */
object Terminated : CallState()

View file

@ -16,9 +16,8 @@
package org.matrix.android.sdk.api.session.call
import org.matrix.android.sdk.api.session.room.model.call.CallCandidate
import org.matrix.android.sdk.api.util.Optional
import org.webrtc.IceCandidate
import org.webrtc.SessionDescription
interface MxCallDetail {
val callId: String
@ -47,12 +46,12 @@ interface MxCall : MxCallDetail {
* Pick Up the incoming call
* It has no effect on outgoing call
*/
fun accept(sdp: SessionDescription)
fun accept(sdpString: String)
/**
* SDP negotiation for media pause, hold/resume, ICE restarts and voice/video call up/downgrading
*/
fun negotiate(sdp: SessionDescription)
fun negotiate(sdpString: String)
/**
* This has to be sent by the caller's client once it has chosen an answer.
@ -73,17 +72,17 @@ interface MxCall : MxCallDetail {
* Start a call
* Send offer SDP to the other participant.
*/
fun offerSdp(sdp: SessionDescription)
fun offerSdp(sdpString: String)
/**
* Send Ice candidate to the other participant.
*/
fun sendLocalIceCandidates(candidates: List<IceCandidate>)
fun sendLocalIceCandidates(candidates: List<CallCandidate>)
/**
* Send removed ICE candidates to the other participant.
*/
fun sendLocalIceCandidateRemovals(candidates: List<IceCandidate>)
fun sendLocalIceCandidateRemovals(candidates: List<CallCandidate>)
fun addListener(listener: StateListener)
fun removeListener(listener: StateListener)

View file

@ -0,0 +1,30 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.api.session.call;
/**
* This is a copy of https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/connectionState
* to avoid having the dependency over WebRtc library on sdk.
*/
public enum MxPeerConnectionState {
NEW,
CONNECTING,
CONNECTED,
DISCONNECTED,
FAILED,
CLOSED;
}

View file

@ -0,0 +1,36 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.api.session.room.model.call
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class CallCandidate(
/**
* Required. The SDP media type this candidate is intended for.
*/
@Json(name = "sdpMid") val sdpMid: String,
/**
* Required. The index of the SDP 'm' line this candidate is intended for.
*/
@Json(name = "sdpMLineIndex") val sdpMLineIndex: Int,
/**
* Required. The SDP 'a' line of the candidate.
*/
@Json(name = "candidate") val candidate: String
)

View file

@ -36,26 +36,9 @@ data class CallCandidatesContent(
/**
* Required. Array of objects describing the candidates.
*/
@Json(name = "candidates") val candidates: List<Candidate> = emptyList(),
@Json(name = "candidates") val candidates: List<CallCandidate> = emptyList(),
/**
* Required. The version of the VoIP specification this messages adheres to. This specification is version 0.
*/
@Json(name = "version") override val version: String? = "0"
): CallSignallingContent {
@JsonClass(generateAdapter = true)
data class Candidate(
/**
* Required. The SDP media type this candidate is intended for.
*/
@Json(name = "sdpMid") val sdpMid: String,
/**
* Required. The index of the SDP 'm' line this candidate is intended for.
*/
@Json(name = "sdpMLineIndex") val sdpMLineIndex: Int,
/**
* Required. The SDP 'a' line of the candidate.
*/
@Json(name = "candidate") val candidate: String
)
}
): CallSignallingContent

View file

@ -18,7 +18,6 @@ package org.matrix.android.sdk.api.session.room.model.call
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import org.webrtc.SessionDescription
@JsonClass(generateAdapter = false)
enum class SdpType {
@ -28,19 +27,3 @@ enum class SdpType {
@Json(name = "answer")
ANSWER;
}
fun SdpType.asWebRTC(): SessionDescription.Type {
return if (this == SdpType.OFFER) {
SessionDescription.Type.OFFER
} else {
SessionDescription.Type.ANSWER
}
}
fun SessionDescription.Type.toSdpType(): SdpType {
return if (this == SessionDescription.Type.OFFER) {
SdpType.OFFER
} else {
SdpType.ANSWER
}
}

View file

@ -25,19 +25,18 @@ import org.matrix.android.sdk.api.session.events.model.LocalEcho
import org.matrix.android.sdk.api.session.events.model.UnsignedData
import org.matrix.android.sdk.api.session.events.model.toContent
import org.matrix.android.sdk.api.session.room.model.call.CallAnswerContent
import org.matrix.android.sdk.api.session.room.model.call.CallCandidate
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.CallInviteContent
import org.matrix.android.sdk.api.session.room.model.call.CallNegotiateContent
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.toSdpType
import org.matrix.android.sdk.api.session.room.model.call.SdpType
import org.matrix.android.sdk.api.util.Optional
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.LocalEchoEventFactory
import org.webrtc.IceCandidate
import org.webrtc.SessionDescription
import timber.log.Timber
internal class MxCallImpl(
@ -90,7 +89,7 @@ internal class MxCallImpl(
}
}
override fun offerSdp(sdp: SessionDescription) {
override fun offerSdp(sdpString: String) {
if (!isOutgoing) return
Timber.v("## VOIP offerSdp $callId")
state = CallState.Dialing
@ -98,29 +97,23 @@ internal class MxCallImpl(
callId = callId,
partyId = ourPartyId,
lifetime = DefaultCallSignalingService.CALL_TIMEOUT_MS,
offer = CallInviteContent.Offer(sdp = sdp.description)
offer = CallInviteContent.Offer(sdp = sdpString)
)
.let { createEventAndLocalEcho(type = EventType.CALL_INVITE, roomId = roomId, content = it.toContent()) }
.also { eventSenderProcessor.postEvent(it) }
}
override fun sendLocalIceCandidates(candidates: List<IceCandidate>) {
override fun sendLocalIceCandidates(candidates: List<CallCandidate>) {
CallCandidatesContent(
callId = callId,
partyId = ourPartyId,
candidates = candidates.map {
CallCandidatesContent.Candidate(
sdpMid = it.sdpMid,
sdpMLineIndex = it.sdpMLineIndex,
candidate = it.sdp
)
}
candidates = candidates
)
.let { createEventAndLocalEcho(type = EventType.CALL_CANDIDATES, roomId = roomId, content = it.toContent()) }
.also { eventSenderProcessor.postEvent(it) }
}
override fun sendLocalIceCandidateRemovals(candidates: List<IceCandidate>) {
override fun sendLocalIceCandidateRemovals(candidates: List<CallCandidate>) {
// For now we don't support this flow
}
@ -153,26 +146,26 @@ internal class MxCallImpl(
state = CallState.Terminated
}
override fun accept(sdp: SessionDescription) {
override fun accept(sdpString: String) {
Timber.v("## VOIP accept $callId")
if (isOutgoing) return
state = CallState.Answering
CallAnswerContent(
callId = callId,
partyId = ourPartyId,
answer = CallAnswerContent.Answer(sdp = sdp.description)
answer = CallAnswerContent.Answer(sdp = sdpString)
)
.let { createEventAndLocalEcho(type = EventType.CALL_ANSWER, roomId = roomId, content = it.toContent()) }
.also { eventSenderProcessor.postEvent(it) }
}
override fun negotiate(sdp: SessionDescription) {
override fun negotiate(sdpString: String) {
Timber.v("## VOIP negotiate $callId")
CallNegotiateContent(
callId = callId,
partyId = ourPartyId,
lifetime = DefaultCallSignalingService.CALL_TIMEOUT_MS,
description = CallNegotiateContent.Description(sdp = sdp.description, type = sdp.type.toSdpType())
description = CallNegotiateContent.Description(sdp = sdpString, type = SdpType.OFFER)
)
.let { createEventAndLocalEcho(type = EventType.CALL_NEGOTIATE, roomId = roomId, content = it.toContent()) }
.also { eventSenderProcessor.postEvent(it) }

View file

@ -22,7 +22,7 @@ import androidx.core.view.isVisible
import im.vector.app.core.utils.DebouncedClickListener
import im.vector.app.features.call.WebRtcPeerConnectionManager
import org.matrix.android.sdk.api.session.call.CallState
import org.matrix.android.sdk.api.session.call.EglUtils
import im.vector.app.features.call.utils.EglUtils
import org.matrix.android.sdk.api.session.call.MxCall
import org.webrtc.RendererCommon
import org.webrtc.SurfaceViewRenderer

View file

@ -29,6 +29,7 @@ import butterknife.OnClick
import im.vector.app.R
import kotlinx.android.synthetic.main.view_call_controls.view.*
import org.matrix.android.sdk.api.session.call.CallState
import org.matrix.android.sdk.api.session.call.MxPeerConnectionState
import org.webrtc.PeerConnection
class CallControlsView @JvmOverloads constructor(
@ -129,7 +130,7 @@ class CallControlsView @JvmOverloads constructor(
connectedControls.isVisible = false
}
is CallState.Connected -> {
if (callState.iceConnectionState == PeerConnection.PeerConnectionState.CONNECTED) {
if (callState.iceConnectionState == MxPeerConnectionState.CONNECTED) {
ringingControls.isVisible = false
connectedControls.isVisible = true
iv_video_toggle.isVisible = state.isVideoCall

View file

@ -52,8 +52,9 @@ import io.reactivex.android.schedulers.AndroidSchedulers
import kotlinx.android.parcel.Parcelize
import kotlinx.android.synthetic.main.activity_call.*
import org.matrix.android.sdk.api.session.call.CallState
import org.matrix.android.sdk.api.session.call.EglUtils
import im.vector.app.features.call.utils.EglUtils
import org.matrix.android.sdk.api.session.call.MxCallDetail
import org.matrix.android.sdk.api.session.call.MxPeerConnectionState
import org.matrix.android.sdk.api.session.call.TurnServerResponse
import org.webrtc.EglBase
import org.webrtc.PeerConnection
@ -255,7 +256,7 @@ class VectorCallActivity : VectorBaseActivity(), CallControlsView.InteractionLis
configureCallInfo(state)
}
is CallState.Connected -> {
if (callState.iceConnectionState == PeerConnection.PeerConnectionState.CONNECTED) {
if (callState.iceConnectionState == MxPeerConnectionState.CONNECTED) {
if (callArgs.isVideoCall) {
callVideoGroup.isVisible = true
callInfoGroup.isVisible = false

View file

@ -30,6 +30,7 @@ import org.matrix.android.sdk.api.MatrixCallback
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.MxCall
import org.matrix.android.sdk.api.session.call.MxPeerConnectionState
import org.matrix.android.sdk.api.session.call.TurnServerResponse
import org.matrix.android.sdk.api.util.MatrixItem
import org.matrix.android.sdk.api.util.toMatrixItem
@ -53,7 +54,7 @@ class VectorCallViewModel @AssistedInject constructor(
private val callStateListener = object : MxCall.StateListener {
override fun onStateUpdate(call: MxCall) {
val callState = call.state
if (callState is CallState.Connected && callState.iceConnectionState == PeerConnection.PeerConnectionState.CONNECTED) {
if (callState is CallState.Connected && callState.iceConnectionState == MxPeerConnectionState.CONNECTED) {
hasBeenConnectedOnce = true
connectionTimeoutTimer?.cancel()
connectionTimeoutTimer = null

View file

@ -46,8 +46,11 @@ import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.call.CallListener
import org.matrix.android.sdk.api.session.call.CallState
import org.matrix.android.sdk.api.session.call.EglUtils
import im.vector.app.features.call.utils.EglUtils
import im.vector.app.features.call.utils.asWebRTC
import im.vector.app.features.call.utils.mapToCallCandidate
import org.matrix.android.sdk.api.session.call.MxCall
import org.matrix.android.sdk.api.session.call.MxPeerConnectionState
import org.matrix.android.sdk.api.session.call.TurnServerResponse
import org.matrix.android.sdk.api.session.room.model.call.CallAnswerContent
import org.matrix.android.sdk.api.session.room.model.call.CallCandidatesContent
@ -57,7 +60,6 @@ import org.matrix.android.sdk.api.session.room.model.call.CallNegotiateContent
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.SdpType
import org.matrix.android.sdk.api.session.room.model.call.asWebRTC
import org.matrix.android.sdk.internal.util.awaitCallback
import org.webrtc.AudioSource
import org.webrtc.AudioTrack
@ -150,7 +152,7 @@ class WebRtcPeerConnectionManager @Inject constructor(
if (it.isNotEmpty()) {
Timber.v("## Sending local ice candidates to call")
// it.forEach { peerConnection?.addIceCandidate(it) }
mxCall.sendLocalIceCandidates(it)
mxCall.sendLocalIceCandidates(it.mapToCallCandidate())
}
}
@ -329,9 +331,9 @@ class WebRtcPeerConnectionManager @Inject constructor(
}
if (call.state == CallState.CreateOffer) {
// send offer to peer
call.offerSdp(sessionDescription)
call.offerSdp(sessionDescription.description)
} else {
call.negotiate(sessionDescription)
call.negotiate(sessionDescription.description)
}
} catch (failure: Throwable) {
// Need to handle error properly.
@ -455,7 +457,7 @@ class WebRtcPeerConnectionManager @Inject constructor(
// create a answer, set local description and send via signaling
createAnswer(callContext)?.also {
callContext.mxCall.accept(it)
callContext.mxCall.accept(it.description)
}
Timber.v("## VOIP remoteCandidateSource ${callContext.remoteCandidateSource}")
callContext.remoteIceCandidateDisposable = callContext.remoteCandidateSource?.subscribe({
@ -969,7 +971,7 @@ class WebRtcPeerConnectionManager @Inject constructor(
peerConnection.awaitSetRemoteDescription(sdp)
if (type == SdpType.OFFER) {
createAnswer(call)?.also {
call.mxCall.negotiate(it)
call.mxCall.negotiate(sdpText)
}
}
} catch (failure: Throwable) {
@ -1053,7 +1055,7 @@ class WebRtcPeerConnectionManager @Inject constructor(
* or is closed (state "closed"); in addition, at least one transport is either "connected" or "completed"
*/
PeerConnection.PeerConnectionState.CONNECTED -> {
callContext.mxCall.state = CallState.Connected(newState)
callContext.mxCall.state = CallState.Connected(MxPeerConnectionState.CONNECTED)
callAudioManager.onCallConnected(callContext.mxCall)
}
/**
@ -1062,7 +1064,7 @@ class WebRtcPeerConnectionManager @Inject constructor(
PeerConnection.PeerConnectionState.FAILED -> {
// This can be temporary, e.g when other ice not yet received...
// callContext.mxCall.state = CallState.ERROR
callContext.mxCall.state = CallState.Connected(newState)
callContext.mxCall.state = CallState.Connected(MxPeerConnectionState.FAILED)
}
/**
* At least one of the connection's ICE transports (RTCIceTransports or RTCDtlsTransports) are in the "new" state,
@ -1070,26 +1072,27 @@ class WebRtcPeerConnectionManager @Inject constructor(
* or all of the connection's transports are in the "closed" state.
*/
PeerConnection.PeerConnectionState.NEW,
/**
* One or more of the ICE transports are currently in the process of establishing a connection;
* that is, their RTCIceConnectionState is either "checking" or "connected", and no transports are in the "failed" state
*/
PeerConnection.PeerConnectionState.CONNECTING -> {
callContext.mxCall.state = CallState.Connected(PeerConnection.PeerConnectionState.CONNECTING)
callContext.mxCall.state = CallState.Connected(MxPeerConnectionState.CONNECTING)
}
/**
* The RTCPeerConnection is closed.
* This value was in the RTCSignalingState enum (and therefore found by reading the value of the signalingState)
* property until the May 13, 2016 draft of the specification.
*/
PeerConnection.PeerConnectionState.CLOSED,
/**
* At least one of the ICE transports for the connection is in the "disconnected" state and none of
* the other transports are in the state "failed", "connecting", or "checking".
*/
PeerConnection.PeerConnectionState.CLOSED -> {
callContext.mxCall.state = CallState.Connected(MxPeerConnectionState.CLOSED)
}
/**
* At least one of the ICE transports for the connection is in the "disconnected" state and none of
* the other transports are in the state "failed", "connecting", or "checking".
*/
PeerConnection.PeerConnectionState.DISCONNECTED -> {
callContext.mxCall.state = CallState.Connected(newState)
callContext.mxCall.state = CallState.Connected(MxPeerConnectionState.DISCONNECTED)
}
null -> {
}

View file

@ -1,5 +1,5 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.api.session.call
package im.vector.app.features.call.utils
import org.webrtc.EglBase
import timber.log.Timber

View file

@ -17,6 +17,7 @@
package im.vector.app.features.call.utils
import im.vector.app.features.call.SdpObserverAdapter
import org.matrix.android.sdk.api.session.call.MxPeerConnectionState
import org.webrtc.MediaConstraints
import org.webrtc.PeerConnection
import org.webrtc.SessionDescription
@ -79,4 +80,3 @@ suspend fun PeerConnection.awaitSetRemoteDescription(sessionDescription: Session
}
}, sessionDescription)
}

View file

@ -0,0 +1,40 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.call.utils
import org.matrix.android.sdk.api.session.room.model.call.CallCandidate
import org.matrix.android.sdk.api.session.room.model.call.SdpType
import org.webrtc.IceCandidate
import org.webrtc.SessionDescription
fun List<IceCandidate>.mapToCallCandidate() = map {
CallCandidate(
sdpMid = it.sdpMid,
sdpMLineIndex = it.sdpMLineIndex,
candidate = it.sdp
)
}
fun SdpType.asWebRTC(): SessionDescription.Type {
return if (this == SdpType.OFFER) {
SessionDescription.Type.OFFER
} else {
SessionDescription.Type.ANSWER
}
}