Merge branch 'feature/fga/voip_v1_start' into feature/fga/voip_timeline

This commit is contained in:
ganfra 2020-12-08 11:58:45 +01:00
commit e817844c5d
25 changed files with 379 additions and 281 deletions

View File

@ -1,6 +1,6 @@
#Mon Dec 07 18:05:35 CET 2020
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionSha256Sum=22449f5231796abd892c98b2a07c9ceebe4688d192cd2d6763f8e3bf8acbedeb
distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-all.zip
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-all.zip

View File

@ -16,7 +16,6 @@
package org.matrix.android.sdk.api.session.call package org.matrix.android.sdk.api.session.call
sealed class CallState { sealed class CallState {
/** Idle, setting up objects */ /** Idle, setting up objects */

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2020 New Vector Ltd * Copyright (c) 2020 The Matrix.org Foundation C.I.C.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -26,5 +26,5 @@ public enum MxPeerConnectionState {
CONNECTED, CONNECTED,
DISCONNECTED, DISCONNECTED,
FAILED, FAILED,
CLOSED; CLOSED
} }

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2020 New Vector Ltd * Copyright (c) 2020 The Matrix.org Foundation C.I.C.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.

View File

@ -35,7 +35,7 @@ data class CallNegotiateContent(
/** /**
* Required. The time in milliseconds that the negotiation is valid for. Once exceeded the sender * Required. The time in milliseconds that the negotiation is valid for. Once exceeded the sender
* of the negotiate event should consider the negotiation failed (timed out) and the recipient should ignore it. * of the negotiate event should consider the negotiation failed (timed out) and the recipient should ignore it.
**/ **/
@Json(name = "lifetime") val lifetime: Int?, @Json(name = "lifetime") val lifetime: Int?,
/** /**
* Required. The session description object * Required. The session description object
@ -45,9 +45,9 @@ data class CallNegotiateContent(
/** /**
* Required. The version of the VoIP specification this message adheres to. * Required. The version of the VoIP specification this message adheres to.
*/ */
@Json(name = "version") override val version: String?, @Json(name = "version") override val version: String?
): CallSignallingContent { ): CallSignallingContent {
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class Description( data class Description(
/** /**
@ -59,6 +59,4 @@ data class CallNegotiateContent(
*/ */
@Json(name = "sdp") val sdp: String? @Json(name = "sdp") val sdp: String?
) )
} }

View File

@ -36,5 +36,5 @@ data class CallRejectContent(
/** /**
* Required. The version of the VoIP specification this message adheres to. * Required. The version of the VoIP specification this message adheres to.
*/ */
@Json(name = "version") override val version: String?, @Json(name = "version") override val version: String?
):CallSignallingContent ):CallSignallingContent

View File

@ -40,5 +40,5 @@ data class CallSelectAnswerContent(
/** /**
* Required. The version of the VoIP specification this message adheres to. * Required. The version of the VoIP specification this message adheres to.
*/ */
@Json(name = "version") override val version: String?, @Json(name = "version") override val version: String?
): CallSignallingContent ): CallSignallingContent

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2020 New Vector Ltd * Copyright (c) 2020 The Matrix.org Foundation C.I.C.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.

View File

@ -71,7 +71,6 @@ internal class EventInsertLiveObserver @Inject constructor(@SessionDatabase real
return@forEach return@forEach
} }
val domainEvent = event.asDomain() val domainEvent = event.asDomain()
// decryptIfNeeded(domainEvent)
processors.filter { processors.filter {
it.shouldProcess(eventId, domainEvent.getClearType(), eventInsert.insertType) it.shouldProcess(eventId, domainEvent.getClearType(), eventInsert.insertType)
}.forEach { }.forEach {
@ -83,6 +82,7 @@ internal class EventInsertLiveObserver @Inject constructor(@SessionDatabase real
.findAll() .findAll()
.deleteAllFromRealm() .deleteAllFromRealm()
} }
processors.forEach { it.onPostProcess() }
} }
} }

View File

@ -25,4 +25,12 @@ internal interface EventInsertLiveProcessor {
fun shouldProcess(eventId: String, eventType: String, insertType: EventInsertType): Boolean fun shouldProcess(eventId: String, eventType: String, insertType: EventInsertType): Boolean
suspend fun process(realm: Realm, event: Event) suspend fun process(realm: Realm, event: Event)
/**
* Called after transaction.
* Maybe you prefer to process the events outside of the realm transaction.
*/
suspend fun onPostProcess() {
// Noop by default
}
} }

View File

@ -16,19 +16,16 @@
package org.matrix.android.sdk.internal.session.call package org.matrix.android.sdk.internal.session.call
import io.realm.Realm
import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.internal.database.model.EventInsertType import org.matrix.android.sdk.internal.database.model.EventInsertType
import org.matrix.android.sdk.internal.di.UserId
import org.matrix.android.sdk.internal.session.EventInsertLiveProcessor import org.matrix.android.sdk.internal.session.EventInsertLiveProcessor
import io.realm.Realm
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
internal class CallEventProcessor @Inject constructor( internal class CallEventProcessor @Inject constructor(private val callSignalingHandler: CallSignalingHandler)
@UserId private val userId: String, : EventInsertLiveProcessor {
private val callService: DefaultCallSignalingService
) : EventInsertLiveProcessor {
private val allowedTypes = listOf( private val allowedTypes = listOf(
EventType.CALL_ANSWER, EventType.CALL_ANSWER,
@ -41,6 +38,8 @@ internal class CallEventProcessor @Inject constructor(
EventType.ENCRYPTED EventType.ENCRYPTED
) )
private val eventsToPostProcess = mutableListOf<Event>()
override fun shouldProcess(eventId: String, eventType: String, insertType: EventInsertType): Boolean { override fun shouldProcess(eventId: String, eventType: String, insertType: EventInsertType): Boolean {
if (insertType != EventInsertType.INCREMENTAL_SYNC) { if (insertType != EventInsertType.INCREMENTAL_SYNC) {
return false return false
@ -49,10 +48,17 @@ internal class CallEventProcessor @Inject constructor(
} }
override suspend fun process(realm: Realm, event: Event) { override suspend fun process(realm: Realm, event: Event) {
update(realm, event) eventsToPostProcess.add(event)
} }
private fun update(realm: Realm, event: Event) { override suspend fun onPostProcess() {
eventsToPostProcess.forEach {
dispatchToCallSignalingHandlerIfNeeded(it)
}
eventsToPostProcess.clear()
}
private fun dispatchToCallSignalingHandlerIfNeeded(event: Event) {
val now = System.currentTimeMillis() val now = System.currentTimeMillis()
// TODO might check if an invite is not closed (hangup/answsered) in the same event batch? // TODO might check if an invite is not closed (hangup/answsered) in the same event batch?
event.roomId ?: return Unit.also { event.roomId ?: return Unit.also {
@ -63,10 +69,6 @@ internal class CallEventProcessor @Inject constructor(
// To old to ring? // To old to ring?
return return
} }
event.ageLocalTs callSignalingHandler.onCallEvent(event)
if (EventType.isCallEvent(event.getClearType())) {
callService.onCallEvent(event)
}
Timber.v("$realm : $userId")
} }
} }

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2020 New Vector Ltd * Copyright (c) 2020 The Matrix.org Foundation C.I.C.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.

View File

@ -0,0 +1,206 @@
/*
* Copyright (c) 2020 The Matrix.org Foundation C.I.C.
*
* 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.internal.session.call
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.MxCall
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.room.model.call.CallAnswerContent
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.CallSignallingContent
import org.matrix.android.sdk.api.util.Optional
import org.matrix.android.sdk.internal.di.UserId
import org.matrix.android.sdk.internal.session.SessionScope
import timber.log.Timber
import java.math.BigDecimal
import javax.inject.Inject
@SessionScope
internal class CallSignalingHandler @Inject constructor(private val activeCallHandler: ActiveCallHandler,
private val mxCallFactory: MxCallFactory,
@UserId private val userId: String) {
private val callListeners = mutableSetOf<CallListener>()
private val callListenersDispatcher = CallListenersDispatcher(callListeners)
fun addCallListener(listener: CallListener) {
callListeners.add(listener)
}
fun removeCallListener(listener: CallListener) {
callListeners.remove(listener)
}
fun onCallEvent(event: Event) {
when (event.getClearType()) {
EventType.CALL_ANSWER -> {
handleCallAnswerEvent(event)
}
EventType.CALL_INVITE -> {
handleCallInviteEvent(event)
}
EventType.CALL_HANGUP -> {
handleCallHangupEvent(event)
}
EventType.CALL_REJECT -> {
handleCallRejectEvent(event)
}
EventType.CALL_CANDIDATES -> {
handleCallCandidatesEvent(event)
}
EventType.CALL_SELECT_ANSWER -> {
handleCallSelectAnswerEvent(event)
}
EventType.CALL_NEGOTIATE -> {
handleCallNegotiateEvent(event)
}
}
}
private fun handleCallNegotiateEvent(event: Event) {
val content = event.getClearContent().toModel<CallNegotiateContent>() ?: return
val call = content.getCall() ?: return
if (call.ourPartyId == content.partyId) {
// Ignore remote echo
return
}
callListenersDispatcher.onCallNegotiateReceived(content)
}
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) {
val content = event.getClearContent().toModel<CallCandidatesContent>() ?: return
val call = content.getCall() ?: return
if (call.ourPartyId == content.partyId) {
// Ignore remote echo
return
}
if (call.opponentPartyId != null && !call.partyIdsMatches(content)) {
Timber.v("Ignoring candidates from party ID ${content.partyId} we have chosen party ID ${call.opponentPartyId}")
return
}
callListenersDispatcher.onCallIceCandidateReceived(call, content)
}
private fun handleCallRejectEvent(event: Event) {
val content = event.getClearContent().toModel<CallRejectContent>() ?: return
val call = content.getCall() ?: return
activeCallHandler.removeCall(content.callId)
// No need to check party_id for reject because if we'd received either
// an answer or reject, we wouldn't be in state InviteSent
if (call.state != CallState.Dialing) {
return
}
callListenersDispatcher.onCallRejectReceived(content)
}
private fun handleCallHangupEvent(event: Event) {
val content = event.getClearContent().toModel<CallHangupContent>() ?: return
val call = content.getCall() ?: return
// party ID must match (our chosen partner hanging up the call) or be undefined (we haven't chosen
// a partner yet but we're treating the hangup as a reject as per VoIP v0)
if (call.opponentPartyId != null && !call.partyIdsMatches(content)) {
Timber.v("Ignoring hangup from party ID ${content.partyId} we have chosen party ID ${call.opponentPartyId}")
return
}
if (call.state != CallState.Terminated) {
activeCallHandler.removeCall(content.callId)
callListenersDispatcher.onCallHangupReceived(content)
}
}
private fun handleCallInviteEvent(event: Event) {
if (event.senderId == userId) {
// ignore invites you send
return
}
if (event.roomId == null || event.senderId == null) {
return
}
val content = event.getClearContent().toModel<CallInviteContent>() ?: return
val incomingCall = mxCallFactory.createIncomingCall(
roomId = event.roomId,
senderId = event.senderId,
content = content
) ?: return
activeCallHandler.addCall(incomingCall)
callListenersDispatcher.onCallInviteReceived(incomingCall, content)
}
private fun handleCallAnswerEvent(event: Event) {
val content = event.getClearContent().toModel<CallAnswerContent>() ?: return
val call = content.getCall() ?: return
if (call.ourPartyId == content.partyId) {
// Ignore remote echo
return
}
if (event.senderId == userId) {
// discard current call, it's answered by another of my session
callListenersDispatcher.onCallManagedByOtherSession(content.callId)
} else {
if (call.opponentPartyId != null) {
Timber.v("Ignoring answer from party ID ${content.partyId} we already have an answer from ${call.opponentPartyId}")
return
}
call.apply {
opponentPartyId = Optional.from(content.partyId)
opponentVersion = content.version?.let { BigDecimal(it).intValueExact() } ?: MxCall.VOIP_PROTO_VERSION
}
callListenersDispatcher.onCallAnswerReceived(content)
}
}
private fun MxCall.partyIdsMatches(contentSignallingContent: CallSignallingContent): Boolean {
return opponentPartyId?.getOrNull() == contentSignallingContent.partyId
}
private fun CallSignallingContent.getCall(): MxCall? {
val currentCall = callId?.let {
activeCallHandler.getCallWithId(it)
}
if (currentCall == null) {
Timber.v("Call for content: $this is null")
}
return currentCall
}
}

View File

@ -16,116 +16,46 @@
package org.matrix.android.sdk.internal.session.call package org.matrix.android.sdk.internal.session.call
import android.os.SystemClock import kotlinx.coroutines.Dispatchers
import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.session.call.CallListener import org.matrix.android.sdk.api.session.call.CallListener
import org.matrix.android.sdk.api.session.call.CallSignalingService import org.matrix.android.sdk.api.session.call.CallSignalingService
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.MxCall
import org.matrix.android.sdk.api.session.call.TurnServerResponse import org.matrix.android.sdk.api.session.call.TurnServerResponse
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.room.model.call.CallAnswerContent
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.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.Optional
import org.matrix.android.sdk.internal.di.DeviceId
import org.matrix.android.sdk.internal.di.UserId
import org.matrix.android.sdk.internal.session.SessionScope import org.matrix.android.sdk.internal.session.SessionScope
import org.matrix.android.sdk.internal.session.call.model.MxCallImpl
import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory
import org.matrix.android.sdk.internal.session.room.send.queue.EventSenderProcessor
import org.matrix.android.sdk.internal.task.TaskExecutor import org.matrix.android.sdk.internal.task.TaskExecutor
import org.matrix.android.sdk.internal.task.configureWith import org.matrix.android.sdk.internal.task.launchToCallback
import timber.log.Timber import timber.log.Timber
import java.math.BigDecimal
import java.util.UUID
import javax.inject.Inject import javax.inject.Inject
@SessionScope @SessionScope
internal class DefaultCallSignalingService @Inject constructor( internal class DefaultCallSignalingService @Inject constructor(
@UserId private val callSignalingHandler: CallSignalingHandler,
private val userId: String, private val mxCallFactory: MxCallFactory,
@DeviceId
private val deviceId: String?,
private val activeCallHandler: ActiveCallHandler, private val activeCallHandler: ActiveCallHandler,
private val localEchoEventFactory: LocalEchoEventFactory,
private val eventSenderProcessor: EventSenderProcessor,
private val taskExecutor: TaskExecutor, private val taskExecutor: TaskExecutor,
private val turnServerTask: GetTurnServerTask private val turnServerDataSource: TurnServerDataSource
) : CallSignalingService { ) : CallSignalingService {
private val callListeners = mutableSetOf<CallListener>()
private val callListenersDispatcher = CallListenersDispatcher(callListeners)
private val cachedTurnServerResponse = object {
// Keep one minute safe to avoid considering the data is valid and then actually it is not when effectively using it.
private val MIN_TTL = 60
private val now = { SystemClock.elapsedRealtime() / 1000 }
private var expiresAt: Long = 0
var data: TurnServerResponse? = null
get() = if (expiresAt > now()) field else null
set(value) {
expiresAt = now() + (value?.ttl ?: 0) - MIN_TTL
field = value
}
}
override fun getTurnServer(callback: MatrixCallback<TurnServerResponse>): Cancelable { override fun getTurnServer(callback: MatrixCallback<TurnServerResponse>): Cancelable {
if (cachedTurnServerResponse.data != null) { return taskExecutor.executorScope.launchToCallback(Dispatchers.Default, callback) {
cachedTurnServerResponse.data?.let { callback.onSuccess(it) } turnServerDataSource.getTurnServer()
return NoOpCancellable
} }
return turnServerTask
.configureWith(GetTurnServerTask.Params) {
this.callback = object : MatrixCallback<TurnServerResponse> {
override fun onSuccess(data: TurnServerResponse) {
cachedTurnServerResponse.data = data
callback.onSuccess(data)
}
override fun onFailure(failure: Throwable) {
callback.onFailure(failure)
}
}
}
.executeBy(taskExecutor)
} }
override fun createOutgoingCall(roomId: String, otherUserId: String, isVideoCall: Boolean): MxCall { override fun createOutgoingCall(roomId: String, otherUserId: String, isVideoCall: Boolean): MxCall {
val call = MxCallImpl( return mxCallFactory.createOutgoingCall(roomId, otherUserId, isVideoCall).also {
callId = UUID.randomUUID().toString(), activeCallHandler.addCall(it)
isOutgoing = true,
roomId = roomId,
userId = userId,
ourPartyId = deviceId ?: "",
opponentUserId = otherUserId,
isVideoCall = isVideoCall,
localEchoEventFactory = localEchoEventFactory,
eventSenderProcessor = eventSenderProcessor
)
activeCallHandler.addCall(call).also {
return call
} }
} }
override fun addCallListener(listener: CallListener) { override fun addCallListener(listener: CallListener) {
callListeners.add(listener) callSignalingHandler.addCallListener(listener)
} }
override fun removeCallListener(listener: CallListener) { override fun removeCallListener(listener: CallListener) {
callListeners.remove(listener) callSignalingHandler.removeCallListener(listener)
} }
override fun getCallWithId(callId: String): MxCall? { override fun getCallWithId(callId: String): MxCall? {
@ -137,154 +67,6 @@ internal class DefaultCallSignalingService @Inject constructor(
return activeCallHandler.getActiveCallsLiveData().value?.isNotEmpty() == true return activeCallHandler.getActiveCallsLiveData().value?.isNotEmpty() == true
} }
internal fun onCallEvent(event: Event) {
when (event.getClearType()) {
EventType.CALL_ANSWER -> {
handleCallAnswerEvent(event)
}
EventType.CALL_INVITE -> {
handleCallInviteEvent(event)
}
EventType.CALL_HANGUP -> {
handleCallHangupEvent(event)
}
EventType.CALL_REJECT -> {
handleCallRejectEvent(event)
}
EventType.CALL_CANDIDATES -> {
handleCallCandidatesEvent(event)
}
EventType.CALL_SELECT_ANSWER -> {
handleCallSelectAnswerEvent(event)
}
EventType.CALL_NEGOTIATE -> {
handleCallNegotiateEvent(event)
}
}
}
private fun handleCallNegotiateEvent(event: Event) {
val content = event.getClearContent().toModel<CallNegotiateContent>() ?: return
val call = content.getCall() ?: return
if (call.ourPartyId == content.partyId) {
// Ignore remote echo
return
}
callListenersDispatcher.onCallNegotiateReceived(content)
}
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) {
val content = event.getClearContent().toModel<CallCandidatesContent>() ?: return
val call = content.getCall() ?: return
if (call.ourPartyId == content.partyId) {
// Ignore remote echo
return
}
if (call.opponentPartyId != Optional.from(content.partyId)) {
Timber.v("Ignoring candidates from party ID ${content.partyId} we have chosen party ID ${call.opponentPartyId}")
return
}
callListenersDispatcher.onCallIceCandidateReceived(call, content)
}
private fun handleCallRejectEvent(event: Event) {
val content = event.getClearContent().toModel<CallRejectContent>() ?: return
val call = content.getCall() ?: return
activeCallHandler.removeCall(content.callId)
// No need to check party_id for reject because if we'd received either
// an answer or reject, we wouldn't be in state InviteSent
if (call.state != CallState.Dialing) {
return
}
callListenersDispatcher.onCallRejectReceived(content)
}
private fun handleCallHangupEvent(event: Event) {
val content = event.getClearContent().toModel<CallHangupContent>() ?: return
val call = content.getCall() ?: return
if (call.state != CallState.Terminated) {
// Need to check for party_id?
activeCallHandler.removeCall(content.callId)
callListenersDispatcher.onCallHangupReceived(content)
}
}
private fun handleCallInviteEvent(event: Event) {
if (event.senderId == userId) {
// ignore invites you send
return
}
val content = event.getClearContent().toModel<CallInviteContent>() ?: return
val incomingCall = MxCallImpl(
callId = content.callId ?: return,
isOutgoing = false,
roomId = event.roomId ?: return,
userId = userId,
ourPartyId = deviceId ?: "",
opponentUserId = event.senderId ?: return,
isVideoCall = content.isVideo(),
localEchoEventFactory = localEchoEventFactory,
eventSenderProcessor = eventSenderProcessor
).apply {
opponentPartyId = Optional.from(content.partyId)
opponentVersion = content.version?.let { BigDecimal(it).intValueExact() } ?: MxCall.VOIP_PROTO_VERSION
}
activeCallHandler.addCall(incomingCall)
callListenersDispatcher.onCallInviteReceived(incomingCall, content)
}
private fun handleCallAnswerEvent(event: Event) {
val content = event.getClearContent().toModel<CallAnswerContent>() ?: return
val call = content.getCall() ?: return
if (call.ourPartyId == content.partyId) {
// Ignore remote echo
return
}
if (event.senderId == userId) {
// discard current call, it's answered by another of my session
callListenersDispatcher.onCallManagedByOtherSession(content.callId)
} else {
if (call.opponentPartyId != null) {
Timber.v("Ignoring answer from party ID ${content.partyId} we already have an answer from ${call.opponentPartyId}")
return
}
call.apply {
opponentPartyId = Optional.from(content.partyId)
opponentVersion = content.version?.let { BigDecimal(it).intValueExact() } ?: MxCall.VOIP_PROTO_VERSION
}
callListenersDispatcher.onCallAnswerReceived(content)
}
}
private fun CallSignallingContent.getCall(): MxCall? {
val currentCall = callId?.let {
activeCallHandler.getCallWithId(it)
}
if (currentCall == null) {
Timber.v("Call for content: $this is null")
}
return currentCall
}
companion object { companion object {
const val CALL_TIMEOUT_MS = 120_000 const val CALL_TIMEOUT_MS = 120_000
} }

View File

@ -0,0 +1,69 @@
/*
* Copyright (c) 2020 The Matrix.org Foundation C.I.C.
*
* 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.internal.session.call
import org.matrix.android.sdk.api.session.call.MxCall
import org.matrix.android.sdk.api.session.room.model.call.CallInviteContent
import org.matrix.android.sdk.api.util.Optional
import org.matrix.android.sdk.internal.di.DeviceId
import org.matrix.android.sdk.internal.di.UserId
import org.matrix.android.sdk.internal.session.call.model.MxCallImpl
import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory
import org.matrix.android.sdk.internal.session.room.send.queue.EventSenderProcessor
import java.math.BigDecimal
import java.util.UUID
import javax.inject.Inject
internal class MxCallFactory @Inject constructor(
@DeviceId private val deviceId: String?,
private val localEchoEventFactory: LocalEchoEventFactory,
private val eventSenderProcessor: EventSenderProcessor,
@UserId private val userId: String
) {
fun createIncomingCall(roomId: String, senderId: String, content: CallInviteContent): MxCall? {
if (content.callId == null) return null
return MxCallImpl(
callId = content.callId,
isOutgoing = false,
roomId = roomId,
userId = userId,
ourPartyId = deviceId ?: "",
opponentUserId = senderId,
isVideoCall = content.isVideo(),
localEchoEventFactory = localEchoEventFactory,
eventSenderProcessor = eventSenderProcessor
).apply {
opponentPartyId = Optional.from(content.partyId)
opponentVersion = content.version?.let { BigDecimal(it).intValueExact() } ?: MxCall.VOIP_PROTO_VERSION
}
}
fun createOutgoingCall(roomId: String, otherUserId: String, isVideoCall: Boolean): MxCall {
return MxCallImpl(
callId = UUID.randomUUID().toString(),
isOutgoing = true,
roomId = roomId,
userId = userId,
ourPartyId = deviceId ?: "",
opponentUserId = otherUserId,
isVideoCall = isVideoCall,
localEchoEventFactory = localEchoEventFactory,
eventSenderProcessor = eventSenderProcessor
)
}
}

View File

@ -0,0 +1,46 @@
/*
* Copyright (c) 2020 The Matrix.org Foundation C.I.C.
*
* 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.internal.session.call
import android.os.SystemClock
import org.matrix.android.sdk.api.session.call.TurnServerResponse
import javax.inject.Inject
internal class TurnServerDataSource @Inject constructor(private val turnServerTask: GetTurnServerTask) {
private val cachedTurnServerResponse = object {
// Keep one minute safe to avoid considering the data is valid and then actually it is not when effectively using it.
private val MIN_TTL = 60
private val now = { SystemClock.elapsedRealtime() / 1000 }
private var expiresAt: Long = 0
var data: TurnServerResponse? = null
get() = if (expiresAt > now()) field else null
set(value) {
expiresAt = now() + (value?.ttl ?: 0) - MIN_TTL
field = value
}
}
suspend fun getTurnServer(): TurnServerResponse {
return cachedTurnServerResponse.data ?: turnServerTask.execute(GetTurnServerTask.Params).also {
cachedTurnServerResponse.data = it
}
}
}

View File

@ -121,8 +121,8 @@ internal class MxCallImpl(
} }
override fun reject() { override fun reject() {
if(opponentVersion < 1){ if (opponentVersion < 1) {
Timber.v("Opponent version is less than 1 (${opponentVersion}): sending hangup instead of reject") Timber.v("Opponent version is less than 1 ($opponentVersion): sending hangup instead of reject")
hangUp() hangUp()
return return
} }
@ -203,5 +203,4 @@ internal class MxCallImpl(
) )
.also { localEchoEventFactory.createLocalEcho(it) } .also { localEchoEventFactory.createLocalEcho(it) }
} }
} }

View File

@ -20,7 +20,6 @@ import android.view.View
import androidx.cardview.widget.CardView import androidx.cardview.widget.CardView
import androidx.core.view.isVisible import androidx.core.view.isVisible
import im.vector.app.core.utils.DebouncedClickListener import im.vector.app.core.utils.DebouncedClickListener
import im.vector.app.features.call.webrtc.WebRtcCallManager
import org.matrix.android.sdk.api.session.call.CallState import org.matrix.android.sdk.api.session.call.CallState
import im.vector.app.features.call.utils.EglUtils import im.vector.app.features.call.utils.EglUtils
import im.vector.app.features.call.webrtc.WebRtcCall import im.vector.app.features.call.webrtc.WebRtcCall

View File

@ -30,7 +30,6 @@ import im.vector.app.R
import kotlinx.android.synthetic.main.view_call_controls.view.* 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.CallState
import org.matrix.android.sdk.api.session.call.MxPeerConnectionState import org.matrix.android.sdk.api.session.call.MxPeerConnectionState
import org.webrtc.PeerConnection
class CallControlsView @JvmOverloads constructor( class CallControlsView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0

View File

@ -36,7 +36,6 @@ 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.call.TurnServerResponse
import org.matrix.android.sdk.api.util.MatrixItem import org.matrix.android.sdk.api.util.MatrixItem
import org.matrix.android.sdk.api.util.toMatrixItem import org.matrix.android.sdk.api.util.toMatrixItem
import timber.log.Timber
import java.util.Timer import java.util.Timer
import java.util.TimerTask import java.util.TimerTask
@ -141,7 +140,6 @@ class VectorCallViewModel @AssistedInject constructor(
) )
} }
} }
} }
init { init {

View File

@ -45,6 +45,4 @@ data class VectorCallViewState(
roomId = callArgs.roomId, roomId = callArgs.roomId,
isVideoCall = callArgs.isVideoCall isVideoCall = callArgs.isVideoCall
) )
} }

View File

@ -37,7 +37,6 @@ class CallHeadsUpActionReceiver : BroadcastReceiver() {
?.webRtcCallManager() ?.webRtcCallManager()
?: return ?: return
when (intent?.getIntExtra(EXTRA_CALL_ACTION_KEY, 0)) { when (intent?.getIntExtra(EXTRA_CALL_ACTION_KEY, 0)) {
CALL_ACTION_REJECT -> { CALL_ACTION_REJECT -> {
val callId = intent.getStringExtra(EXTRA_CALL_ID) ?: return val callId = intent.getStringExtra(EXTRA_CALL_ID) ?: return

View File

@ -36,5 +36,3 @@ fun SdpType.asWebRTC(): SessionDescription.Type {
SessionDescription.Type.ANSWER SessionDescription.Type.ANSWER
} }
} }

View File

@ -330,7 +330,7 @@ class WebRtcCall(val mxCall: MxCall,
// 2. Access camera (if video call) + microphone, create local stream // 2. Access camera (if video call) + microphone, create local stream
createLocalStream() createLocalStream()
attachViewRenderersInternal() attachViewRenderersInternal()
Timber.v("## VOIP remoteCandidateSource ${remoteCandidateSource}") Timber.v("## VOIP remoteCandidateSource $remoteCandidateSource")
remoteIceCandidateDisposable = remoteCandidateSource.subscribe({ remoteIceCandidateDisposable = remoteCandidateSource.subscribe({
Timber.v("## VOIP adding remote ice candidate $it") Timber.v("## VOIP adding remote ice candidate $it")
peerConnection?.addIceCandidate(it) peerConnection?.addIceCandidate(it)
@ -383,7 +383,7 @@ class WebRtcCall(val mxCall: MxCall,
createAnswer()?.also { createAnswer()?.also {
mxCall.accept(it.description) mxCall.accept(it.description)
} }
Timber.v("## VOIP remoteCandidateSource ${remoteCandidateSource}") Timber.v("## VOIP remoteCandidateSource $remoteCandidateSource")
remoteIceCandidateDisposable = remoteCandidateSource.subscribe({ remoteIceCandidateDisposable = remoteCandidateSource.subscribe({
Timber.v("## VOIP adding remote ice candidate $it") Timber.v("## VOIP adding remote ice candidate $it")
peerConnection?.addIceCandidate(it) peerConnection?.addIceCandidate(it)
@ -554,9 +554,9 @@ class WebRtcCall(val mxCall: MxCall,
for (transceiver in peerConnection?.transceivers ?: emptyList()) { for (transceiver in peerConnection?.transceivers ?: emptyList()) {
val trackOnHold = transceiver.currentDirection == RtpTransceiver.RtpTransceiverDirection.INACTIVE val trackOnHold = transceiver.currentDirection == RtpTransceiver.RtpTransceiverDirection.INACTIVE
|| transceiver.currentDirection == RtpTransceiver.RtpTransceiverDirection.RECV_ONLY || transceiver.currentDirection == RtpTransceiver.RtpTransceiverDirection.RECV_ONLY
if (!trackOnHold) callOnHold = false; if (!trackOnHold) callOnHold = false
} }
return callOnHold; return callOnHold
} }
fun updateRemoteOnHold(onHold: Boolean) { fun updateRemoteOnHold(onHold: Boolean) {
@ -704,7 +704,7 @@ class WebRtcCall(val mxCall: MxCall,
return return
} }
mxCall.state = CallState.Terminated mxCall.state = CallState.Terminated
//Close tracks ASAP // Close tracks ASAP
localVideoTrack?.setEnabled(false) localVideoTrack?.setEnabled(false)
localVideoTrack?.setEnabled(false) localVideoTrack?.setEnabled(false)
cameraAvailabilityCallback?.let { cameraAvailabilityCallback -> cameraAvailabilityCallback?.let { cameraAvailabilityCallback ->
@ -760,7 +760,7 @@ class WebRtcCall(val mxCall: MxCall,
val type = description?.type val type = description?.type
val sdpText = description?.sdp val sdpText = description?.sdp
if (type == null || sdpText == null) { if (type == null || sdpText == null) {
Timber.i("Ignoring invalid m.call.negotiate event"); Timber.i("Ignoring invalid m.call.negotiate event")
return@launch return@launch
} }
val peerConnection = peerConnection ?: return@launch val peerConnection = peerConnection ?: return@launch

View File

@ -25,8 +25,6 @@ import im.vector.app.core.services.BluetoothHeadsetReceiver
import im.vector.app.core.services.CallService import im.vector.app.core.services.CallService
import im.vector.app.core.services.WiredHeadsetStateReceiver import im.vector.app.core.services.WiredHeadsetStateReceiver
import im.vector.app.features.call.CallAudioManager import im.vector.app.features.call.CallAudioManager
import im.vector.app.features.call.CameraType
import im.vector.app.features.call.CaptureFormat
import im.vector.app.features.call.VectorCallActivity import im.vector.app.features.call.VectorCallActivity
import im.vector.app.features.call.utils.EglUtils import im.vector.app.features.call.utils.EglUtils
import im.vector.app.push.fcm.FcmHelper import im.vector.app.push.fcm.FcmHelper
@ -46,7 +44,6 @@ import org.matrix.android.sdk.api.session.room.model.call.CallSelectAnswerConten
import org.webrtc.DefaultVideoDecoderFactory import org.webrtc.DefaultVideoDecoderFactory
import org.webrtc.DefaultVideoEncoderFactory import org.webrtc.DefaultVideoEncoderFactory
import org.webrtc.PeerConnectionFactory import org.webrtc.PeerConnectionFactory
import org.webrtc.SurfaceViewRenderer
import timber.log.Timber import timber.log.Timber
import java.util.concurrent.Executors import java.util.concurrent.Executors
import javax.inject.Inject import javax.inject.Inject
@ -230,7 +227,8 @@ class WebRtcCallManager @Inject constructor(
) )
currentCall = webRtcCall currentCall = webRtcCall
callsByCallId[mxCall.callId] = webRtcCall callsByCallId[mxCall.callId] = webRtcCall
callsByRoomId.getOrPut(mxCall.roomId, { ArrayList() }).add(webRtcCall) callsByRoomId.getOrPut(mxCall.roomId) { ArrayList() }
.add(webRtcCall)
return webRtcCall return webRtcCall
} }
@ -332,7 +330,7 @@ class WebRtcCallManager @Inject constructor(
} }
val selectedPartyId = callSelectAnswerContent.selectedPartyId val selectedPartyId = callSelectAnswerContent.selectedPartyId
if (selectedPartyId != call.mxCall.ourPartyId) { if (selectedPartyId != call.mxCall.ourPartyId) {
Timber.i("Got select_answer for party ID ${selectedPartyId}: we are party ID ${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 // The other party has picked somebody else's answer
call.endCall(false) call.endCall(false)
} }