From 7620aa4264569c3cec7044d7024f704a9a9536b4 Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 24 Nov 2020 17:30:13 +0100 Subject: [PATCH] VoIP: continue refactoring --- .../sdk/api/session/call/CallListener.kt | 3 + .../android/sdk/api/session/call/MxCall.kt | 3 +- .../internal/session/call/model/MxCallImpl.kt | 4 +- .../java/im/vector/app/VectorApplication.kt | 6 +- .../vector/app/core/di/ActiveSessionHolder.kt | 8 +- .../im/vector/app/core/di/VectorComponent.kt | 4 +- .../vector/app/core/services/CallService.kt | 12 +- .../app/core/ui/views/ActiveCallViewHolder.kt | 12 +- .../call/SharedActiveCallViewModel.kt | 12 +- .../app/features/call/VectorCallActivity.kt | 14 +- .../app/features/call/VectorCallViewModel.kt | 167 +++++------ .../app/features/call/VectorCallViewState.kt | 15 +- .../call/service/CallHeadsUpActionReceiver.kt | 6 +- .../features/call/telecom/CallConnection.kt | 4 +- .../call/webrtc/PeerConnectionObserver.kt | 10 +- .../app/features/call/webrtc/WebRtcCall.kt | 282 +++++++++++------- ...nectionManager.kt => WebRtcCallManager.kt} | 187 ++++-------- .../app/features/home/HomeDetailFragment.kt | 6 +- .../home/room/detail/RoomDetailFragment.kt | 8 +- .../home/room/detail/RoomDetailViewModel.kt | 10 +- 20 files changed, 366 insertions(+), 407 deletions(-) rename vector/src/main/java/im/vector/app/features/call/webrtc/{WebRtcPeerConnectionManager.kt => WebRtcCallManager.kt} (74%) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/CallListener.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/CallListener.kt index c68b6494e6..303add747f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/CallListener.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/CallListener.kt @@ -57,5 +57,8 @@ interface CallListener { */ fun onCallNegotiateReceived(callNegotiateContent: CallNegotiateContent) + /** + * Called when the call has been managed by an other session + */ fun onCallManagedByOtherSession(callId: String) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/MxCall.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/MxCall.kt index 31b835a6dc..1d17a7f4cd 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/MxCall.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/MxCall.kt @@ -17,6 +17,7 @@ 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.session.room.model.call.CallHangupContent import org.matrix.android.sdk.api.util.Optional interface MxCallDetail { @@ -66,7 +67,7 @@ interface MxCall : MxCallDetail { /** * End the call */ - fun hangUp() + fun hangUp(reason: CallHangupContent.Reason? = null) /** * Start a call diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/model/MxCallImpl.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/model/MxCallImpl.kt index 5254cf1c7e..cd12465355 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/model/MxCallImpl.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/model/MxCallImpl.kt @@ -134,12 +134,12 @@ internal class MxCallImpl( state = CallState.Terminated } - override fun hangUp() { + override fun hangUp(reason: CallHangupContent.Reason?) { Timber.v("## VOIP hangup $callId") CallHangupContent( callId = callId, partyId = ourPartyId, - reason = CallHangupContent.Reason.USER_HANGUP + reason = reason ?: CallHangupContent.Reason.USER_HANGUP ) .let { createEventAndLocalEcho(type = EventType.CALL_HANGUP, roomId = roomId, content = it.toContent()) } .also { eventSenderProcessor.postEvent(it) } diff --git a/vector/src/main/java/im/vector/app/VectorApplication.kt b/vector/src/main/java/im/vector/app/VectorApplication.kt index f33df9b426..b9ab23ad7d 100644 --- a/vector/src/main/java/im/vector/app/VectorApplication.kt +++ b/vector/src/main/java/im/vector/app/VectorApplication.kt @@ -42,7 +42,7 @@ import im.vector.app.core.di.HasVectorInjector import im.vector.app.core.di.VectorComponent import im.vector.app.core.extensions.configureAndStart import im.vector.app.core.rx.RxConfig -import im.vector.app.features.call.webrtc.WebRtcPeerConnectionManager +import im.vector.app.features.call.webrtc.WebRtcCallManager import im.vector.app.features.configuration.VectorConfiguration import im.vector.app.features.disclaimer.doNotShowDisclaimerDialog import im.vector.app.features.lifecycle.VectorActivityLifecycleCallbacks @@ -90,7 +90,7 @@ class VectorApplication : @Inject lateinit var rxConfig: RxConfig @Inject lateinit var popupAlertManager: PopupAlertManager @Inject lateinit var pinLocker: PinLocker - @Inject lateinit var webRtcPeerConnectionManager: WebRtcPeerConnectionManager + @Inject lateinit var callManager: WebRtcCallManager lateinit var vectorComponent: VectorComponent @@ -175,7 +175,7 @@ class VectorApplication : }) ProcessLifecycleOwner.get().lifecycle.addObserver(appStateHandler) ProcessLifecycleOwner.get().lifecycle.addObserver(pinLocker) - ProcessLifecycleOwner.get().lifecycle.addObserver(webRtcPeerConnectionManager) + ProcessLifecycleOwner.get().lifecycle.addObserver(callManager) // This should be done as early as possible // initKnownEmojiHashSet(appContext) diff --git a/vector/src/main/java/im/vector/app/core/di/ActiveSessionHolder.kt b/vector/src/main/java/im/vector/app/core/di/ActiveSessionHolder.kt index a1745ddffc..77ca68fcf1 100644 --- a/vector/src/main/java/im/vector/app/core/di/ActiveSessionHolder.kt +++ b/vector/src/main/java/im/vector/app/core/di/ActiveSessionHolder.kt @@ -18,7 +18,7 @@ package im.vector.app.core.di import arrow.core.Option import im.vector.app.ActiveSessionDataSource -import im.vector.app.features.call.webrtc.WebRtcPeerConnectionManager +import im.vector.app.features.call.webrtc.WebRtcCallManager import im.vector.app.features.crypto.keysrequest.KeyRequestHandler import im.vector.app.features.crypto.verification.IncomingVerificationRequestHandler import im.vector.app.features.notifications.PushRuleTriggerListener @@ -35,7 +35,7 @@ class ActiveSessionHolder @Inject constructor(private val authenticationService: private val sessionObservableStore: ActiveSessionDataSource, private val keyRequestHandler: KeyRequestHandler, private val incomingVerificationRequestHandler: IncomingVerificationRequestHandler, - private val webRtcPeerConnectionManager: WebRtcPeerConnectionManager, + private val callManager: WebRtcCallManager, private val pushRuleTriggerListener: PushRuleTriggerListener, private val sessionListener: SessionListener, private val imageManager: ImageManager @@ -52,7 +52,7 @@ class ActiveSessionHolder @Inject constructor(private val authenticationService: incomingVerificationRequestHandler.start(session) session.addListener(sessionListener) pushRuleTriggerListener.startWithSession(session) - session.callSignalingService().addCallListener(webRtcPeerConnectionManager) + session.callSignalingService().addCallListener(callManager) imageManager.onSessionStarted(session) } @@ -60,7 +60,7 @@ class ActiveSessionHolder @Inject constructor(private val authenticationService: // Do some cleanup first getSafeActiveSession()?.let { Timber.w("clearActiveSession of ${it.myUserId}") - it.callSignalingService().removeCallListener(webRtcPeerConnectionManager) + it.callSignalingService().removeCallListener(callManager) it.removeListener(sessionListener) } diff --git a/vector/src/main/java/im/vector/app/core/di/VectorComponent.kt b/vector/src/main/java/im/vector/app/core/di/VectorComponent.kt index 8e30be50a8..5230069f1e 100644 --- a/vector/src/main/java/im/vector/app/core/di/VectorComponent.kt +++ b/vector/src/main/java/im/vector/app/core/di/VectorComponent.kt @@ -29,7 +29,7 @@ import im.vector.app.core.error.ErrorFormatter import im.vector.app.core.pushers.PushersManager import im.vector.app.core.utils.AssetReader import im.vector.app.core.utils.DimensionConverter -import im.vector.app.features.call.webrtc.WebRtcPeerConnectionManager +import im.vector.app.features.call.webrtc.WebRtcCallManager import im.vector.app.features.configuration.VectorConfiguration import im.vector.app.features.crypto.keysrequest.KeyRequestHandler import im.vector.app.features.crypto.verification.IncomingVerificationRequestHandler @@ -153,7 +153,7 @@ interface VectorComponent { fun pinLocker(): PinLocker - fun webRtcPeerConnectionManager(): WebRtcPeerConnectionManager + fun webRtcPeerConnectionManager(): WebRtcCallManager @Component.Factory interface Factory { diff --git a/vector/src/main/java/im/vector/app/core/services/CallService.kt b/vector/src/main/java/im/vector/app/core/services/CallService.kt index 2371dae213..4fcc9cee28 100644 --- a/vector/src/main/java/im/vector/app/core/services/CallService.kt +++ b/vector/src/main/java/im/vector/app/core/services/CallService.kt @@ -25,7 +25,7 @@ import android.view.KeyEvent import androidx.core.content.ContextCompat import androidx.media.session.MediaButtonReceiver import im.vector.app.core.extensions.vectorComponent -import im.vector.app.features.call.webrtc.WebRtcPeerConnectionManager +import im.vector.app.features.call.webrtc.WebRtcCallManager import im.vector.app.features.call.telecom.CallConnection import im.vector.app.features.notifications.NotificationUtils import timber.log.Timber @@ -38,7 +38,7 @@ class CallService : VectorService(), WiredHeadsetStateReceiver.HeadsetEventListe private val connections = mutableMapOf() private lateinit var notificationUtils: NotificationUtils - private lateinit var webRtcPeerConnectionManager: WebRtcPeerConnectionManager + private lateinit var callManager: WebRtcCallManager private var callRingPlayerIncoming: CallRingPlayerIncoming? = null private var callRingPlayerOutgoing: CallRingPlayerOutgoing? = null @@ -53,7 +53,7 @@ class CallService : VectorService(), WiredHeadsetStateReceiver.HeadsetEventListe override fun onMediaButtonEvent(mediaButtonEvent: Intent?): Boolean { val keyEvent = mediaButtonEvent?.getParcelableExtra(Intent.EXTRA_KEY_EVENT) ?: return false if (keyEvent.keyCode == KeyEvent.KEYCODE_HEADSETHOOK) { - webRtcPeerConnectionManager.headSetButtonTapped() + callManager.headSetButtonTapped() return true } return false @@ -63,7 +63,7 @@ class CallService : VectorService(), WiredHeadsetStateReceiver.HeadsetEventListe override fun onCreate() { super.onCreate() notificationUtils = vectorComponent().notificationUtils() - webRtcPeerConnectionManager = vectorComponent().webRtcPeerConnectionManager() + callManager = vectorComponent().webRtcPeerConnectionManager() callRingPlayerIncoming = CallRingPlayerIncoming(applicationContext) callRingPlayerOutgoing = CallRingPlayerOutgoing(applicationContext) wiredHeadsetStateReceiver = WiredHeadsetStateReceiver.createAndRegister(this, this) @@ -375,11 +375,11 @@ class CallService : VectorService(), WiredHeadsetStateReceiver.HeadsetEventListe override fun onHeadsetEvent(event: WiredHeadsetStateReceiver.HeadsetPlugEvent) { Timber.v("## VOIP: onHeadsetEvent $event") - webRtcPeerConnectionManager.onWiredDeviceEvent(event) + callManager.onWiredDeviceEvent(event) } override fun onBTHeadsetEvent(event: BluetoothHeadsetReceiver.BTHeadsetPlugEvent) { Timber.v("## VOIP: onBTHeadsetEvent $event") - webRtcPeerConnectionManager.onWirelessDeviceEvent(event) + callManager.onWirelessDeviceEvent(event) } } diff --git a/vector/src/main/java/im/vector/app/core/ui/views/ActiveCallViewHolder.kt b/vector/src/main/java/im/vector/app/core/ui/views/ActiveCallViewHolder.kt index d3ab42d01c..55ce5cb0d7 100644 --- a/vector/src/main/java/im/vector/app/core/ui/views/ActiveCallViewHolder.kt +++ b/vector/src/main/java/im/vector/app/core/ui/views/ActiveCallViewHolder.kt @@ -20,7 +20,7 @@ import android.view.View import androidx.cardview.widget.CardView import androidx.core.view.isVisible import im.vector.app.core.utils.DebouncedClickListener -import im.vector.app.features.call.webrtc.WebRtcPeerConnectionManager +import im.vector.app.features.call.webrtc.WebRtcCallManager import org.matrix.android.sdk.api.session.call.CallState import im.vector.app.features.call.utils.EglUtils import org.matrix.android.sdk.api.session.call.MxCall @@ -35,7 +35,7 @@ class ActiveCallViewHolder { private var activeCallPipInitialized = false - fun updateCall(activeCall: MxCall?, webRtcPeerConnectionManager: WebRtcPeerConnectionManager) { + fun updateCall(activeCall: MxCall?, callManager: WebRtcCallManager) { val hasActiveCall = activeCall?.state is CallState.Connected if (hasActiveCall) { val isVideoCall = activeCall?.isVideoCall == true @@ -44,14 +44,14 @@ class ActiveCallViewHolder { pipWrapper?.isVisible = isVideoCall activeCallPiP?.isVisible = isVideoCall activeCallPiP?.let { - webRtcPeerConnectionManager.attachViewRenderers(null, it, null) + callManager.attachViewRenderers(null, it, null) } } else { activeCallView?.isVisible = false activeCallPiP?.isVisible = false pipWrapper?.isVisible = false activeCallPiP?.let { - webRtcPeerConnectionManager.detachRenderers(listOf(it)) + callManager.detachRenderers(listOf(it)) } } } @@ -82,9 +82,9 @@ class ActiveCallViewHolder { ) } - fun unBind(webRtcPeerConnectionManager: WebRtcPeerConnectionManager) { + fun unBind(callManager: WebRtcCallManager) { activeCallPiP?.let { - webRtcPeerConnectionManager.detachRenderers(listOf(it)) + callManager.detachRenderers(listOf(it)) } if (activeCallPipInitialized) { activeCallPiP?.release() diff --git a/vector/src/main/java/im/vector/app/features/call/SharedActiveCallViewModel.kt b/vector/src/main/java/im/vector/app/features/call/SharedActiveCallViewModel.kt index a4b81664d6..bff4a164ca 100644 --- a/vector/src/main/java/im/vector/app/features/call/SharedActiveCallViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/call/SharedActiveCallViewModel.kt @@ -18,12 +18,12 @@ package im.vector.app.features.call import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel -import im.vector.app.features.call.webrtc.WebRtcPeerConnectionManager +import im.vector.app.features.call.webrtc.WebRtcCallManager import org.matrix.android.sdk.api.session.call.MxCall import javax.inject.Inject class SharedActiveCallViewModel @Inject constructor( - private val webRtcPeerConnectionManager: WebRtcPeerConnectionManager + private val callManager: WebRtcCallManager ) : ViewModel() { val activeCall: MutableLiveData = MutableLiveData() @@ -37,7 +37,7 @@ class SharedActiveCallViewModel @Inject constructor( } } - private val listener = object : WebRtcPeerConnectionManager.CurrentCallListener { + private val listener = object : WebRtcCallManager.CurrentCallListener { override fun onCurrentCallChange(call: MxCall?) { activeCall.value?.removeListener(callStateListener) activeCall.postValue(call) @@ -46,13 +46,13 @@ class SharedActiveCallViewModel @Inject constructor( } init { - activeCall.postValue(webRtcPeerConnectionManager.currentCall?.mxCall) - webRtcPeerConnectionManager.addCurrentCallListener(listener) + activeCall.postValue(callManager.currentCall?.mxCall) + callManager.addCurrentCallListener(listener) } override fun onCleared() { activeCall.value?.removeListener(callStateListener) - webRtcPeerConnectionManager.removeCurrentCallListener(listener) + callManager.removeCurrentCallListener(listener) super.onCleared() } } diff --git a/vector/src/main/java/im/vector/app/features/call/VectorCallActivity.kt b/vector/src/main/java/im/vector/app/features/call/VectorCallActivity.kt index ba33e0fc73..188932c43d 100644 --- a/vector/src/main/java/im/vector/app/features/call/VectorCallActivity.kt +++ b/vector/src/main/java/im/vector/app/features/call/VectorCallActivity.kt @@ -53,7 +53,7 @@ import kotlinx.android.parcel.Parcelize import kotlinx.android.synthetic.main.activity_call.* import org.matrix.android.sdk.api.session.call.CallState import im.vector.app.features.call.utils.EglUtils -import im.vector.app.features.call.webrtc.WebRtcPeerConnectionManager +import im.vector.app.features.call.webrtc.WebRtcCallManager 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 @@ -67,7 +67,7 @@ import javax.inject.Inject @Parcelize data class CallArgs( val roomId: String, - val callId: String?, + val callId: String, val participantUserId: String, val isIncomingCall: Boolean, val isVideoCall: Boolean @@ -87,7 +87,7 @@ class VectorCallActivity : VectorBaseActivity(), CallControlsView.InteractionLis private val callViewModel: VectorCallViewModel by viewModel() private lateinit var callArgs: CallArgs - @Inject lateinit var peerConnectionManager: WebRtcPeerConnectionManager + @Inject lateinit var callManager: WebRtcCallManager @Inject lateinit var viewModelFactory: VectorCallViewModel.Factory @@ -211,7 +211,7 @@ class VectorCallActivity : VectorBaseActivity(), CallControlsView.InteractionLis } override fun onDestroy() { - peerConnectionManager.detachRenderers(listOf(pipRenderer, fullscreenRenderer)) + callManager.detachRenderers(listOf(pipRenderer, fullscreenRenderer)) if (surfaceRenderersAreInitialized) { pipRenderer.release() fullscreenRenderer.release() @@ -276,7 +276,7 @@ class VectorCallActivity : VectorBaseActivity(), CallControlsView.InteractionLis callConnectingProgress.isVisible = true } // ensure all attached? - peerConnectionManager.attachViewRenderers(pipRenderer, fullscreenRenderer, null) + callManager.attachViewRenderers(pipRenderer, fullscreenRenderer, null) } is CallState.Terminated -> { finish() @@ -326,7 +326,7 @@ class VectorCallActivity : VectorBaseActivity(), CallControlsView.InteractionLis pipRenderer.setEnableHardwareScaler(true /* enabled */) fullscreenRenderer.setEnableHardwareScaler(true /* enabled */) - peerConnectionManager.attachViewRenderers(pipRenderer, fullscreenRenderer, + callManager.attachViewRenderers(pipRenderer, fullscreenRenderer, intent.getStringExtra(EXTRA_MODE)?.takeIf { isFirstCreation() }) pipRenderer.setOnClickListener { @@ -382,7 +382,7 @@ class VectorCallActivity : VectorBaseActivity(), CallControlsView.InteractionLis } fun newIntent(context: Context, - callId: String?, + callId: String, roomId: String, otherUserId: String, isIncomingCall: Boolean, diff --git a/vector/src/main/java/im/vector/app/features/call/VectorCallViewModel.kt b/vector/src/main/java/im/vector/app/features/call/VectorCallViewModel.kt index 3e7a32821f..1fe8e3a0f1 100644 --- a/vector/src/main/java/im/vector/app/features/call/VectorCallViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/call/VectorCallViewModel.kt @@ -26,6 +26,8 @@ import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.AssistedInject import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.VectorViewModel +import im.vector.app.features.call.webrtc.WebRtcCall +import im.vector.app.features.call.webrtc.WebRtcCallManager import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.call.CallState @@ -34,24 +36,41 @@ 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 -import org.webrtc.PeerConnection import java.util.Timer import java.util.TimerTask class VectorCallViewModel @AssistedInject constructor( @Assisted initialState: VectorCallViewState, - @Assisted val args: CallArgs, val session: Session, - val webRtcPeerConnectionManager: WebRtcPeerConnectionManager, + val callManager: WebRtcCallManager, val proximityManager: CallProximityManager ) : VectorViewModel(initialState) { - private var call: MxCall? = null + private var call: WebRtcCall? = null private var connectionTimeoutTimer: Timer? = null private var hasBeenConnectedOnce = false - private val callStateListener = object : MxCall.StateListener { + private val callListener = object : WebRtcCall.Listener { + + override fun onCaptureStateChanged() { + setState { + copy( + isVideoCaptureInError = call?.videoCapturerIsInError ?: false, + isHD = call?.currentCaptureFormat() is CaptureFormat.HD + ) + } + } + + override fun onCameraChange() { + setState { + copy( + canSwitchCamera = call?.canSwitchCamera() ?: false, + isFrontCamera = call?.currentCameraType() == CameraType.FRONT + ) + } + } + override fun onStateUpdate(call: MxCall) { val callState = call.state if (callState is CallState.Connected && callState.iceConnectionState == MxPeerConnectionState.CONNECTED) { @@ -87,7 +106,7 @@ class VectorCallViewModel @AssistedInject constructor( } } - private val currentCallListener = object : WebRtcPeerConnectionManager.CurrentCallListener { + private val currentCallListener = object : WebRtcCallManager.CurrentCallListener { override fun onCurrentCallChange(call: MxCall?) { // we need to check the state if (call == null) { @@ -96,17 +115,8 @@ class VectorCallViewModel @AssistedInject constructor( } } - override fun onCaptureStateChanged() { - setState { - copy( - isVideoCaptureInError = webRtcPeerConnectionManager.capturerIsInError, - isHD = webRtcPeerConnectionManager.currentCaptureFormat() is CaptureFormat.HD - ) - } - } - override fun onAudioDevicesChange() { - val currentSoundDevice = webRtcPeerConnectionManager.callAudioManager.getCurrentSoundDevice() + val currentSoundDevice = callManager.callAudioManager.getCurrentSoundDevice() if (currentSoundDevice == CallAudioManager.SoundDevice.PHONE) { proximityManager.start() } else { @@ -115,94 +125,77 @@ class VectorCallViewModel @AssistedInject constructor( setState { copy( - availableSoundDevices = webRtcPeerConnectionManager.callAudioManager.getAvailableSoundDevices(), + availableSoundDevices = callManager.callAudioManager.getAvailableSoundDevices(), soundDevice = currentSoundDevice ) } } - override fun onCameraChange() { + } + + init { + val webRtcCall = callManager.getCallById(initialState.callId) + if (webRtcCall == null) { + setState { + copy(callState = Fail(IllegalArgumentException("No call"))) + } + } else { + call = webRtcCall + callManager.addCurrentCallListener(currentCallListener) + val item: MatrixItem? = session.getUser(webRtcCall.mxCall.opponentUserId)?.toMatrixItem() + webRtcCall.addListener(callListener) + val currentSoundDevice = callManager.callAudioManager.getCurrentSoundDevice() + if (currentSoundDevice == CallAudioManager.SoundDevice.PHONE) { + proximityManager.start() + } setState { copy( - canSwitchCamera = webRtcPeerConnectionManager.canSwitchCamera(), - isFrontCamera = webRtcPeerConnectionManager.currentCameraType() == CameraType.FRONT + isVideoCall = webRtcCall.mxCall.isVideoCall, + callState = Success(webRtcCall.mxCall.state), + otherUserMatrixItem = item?.let { Success(it) } ?: Uninitialized, + soundDevice = currentSoundDevice, + availableSoundDevices = callManager.callAudioManager.getAvailableSoundDevices(), + isFrontCamera = callManager.currentCameraType() == CameraType.FRONT, + canSwitchCamera = callManager.canSwitchCamera(), + isHD = webRtcCall.mxCall.isVideoCall && callManager.currentCaptureFormat() is CaptureFormat.HD ) } } } - init { - initialState.callId?.let { - webRtcPeerConnectionManager.addCurrentCallListener(currentCallListener) - - session.callSignalingService().getCallWithId(it)?.let { mxCall -> - this.call = mxCall - mxCall.opponentUserId - val item: MatrixItem? = session.getUser(mxCall.opponentUserId)?.toMatrixItem() - - mxCall.addListener(callStateListener) - - val currentSoundDevice = webRtcPeerConnectionManager.callAudioManager.getCurrentSoundDevice() - if (currentSoundDevice == CallAudioManager.SoundDevice.PHONE) { - proximityManager.start() - } - - setState { - copy( - isVideoCall = mxCall.isVideoCall, - callState = Success(mxCall.state), - otherUserMatrixItem = item?.let { Success(it) } ?: Uninitialized, - soundDevice = currentSoundDevice, - availableSoundDevices = webRtcPeerConnectionManager.callAudioManager.getAvailableSoundDevices(), - isFrontCamera = webRtcPeerConnectionManager.currentCameraType() == CameraType.FRONT, - canSwitchCamera = webRtcPeerConnectionManager.canSwitchCamera(), - isHD = mxCall.isVideoCall && webRtcPeerConnectionManager.currentCaptureFormat() is CaptureFormat.HD - ) - } - } ?: run { - setState { - copy( - callState = Fail(IllegalArgumentException("No call")) - ) - } - } - } - } - override fun onCleared() { - // session.callService().removeCallListener(callServiceListener) - webRtcPeerConnectionManager.removeCurrentCallListener(currentCallListener) - this.call?.removeListener(callStateListener) + callManager.removeCurrentCallListener(currentCallListener) + call?.removeListener(callListener) proximityManager.stop() super.onCleared() } override fun handle(action: VectorCallViewActions) = withState { state -> when (action) { - VectorCallViewActions.EndCall -> webRtcPeerConnectionManager.endCall() - VectorCallViewActions.AcceptCall -> { + VectorCallViewActions.EndCall -> call?.endCall() + VectorCallViewActions.AcceptCall -> { setState { copy(callState = Loading()) } - webRtcPeerConnectionManager.acceptIncomingCall() + call?.acceptIncomingCall() } - VectorCallViewActions.DeclineCall -> { + VectorCallViewActions.DeclineCall -> { setState { copy(callState = Loading()) } - webRtcPeerConnectionManager.endCall() + call?.endCall() } - VectorCallViewActions.ToggleMute -> { + VectorCallViewActions.ToggleMute -> { val muted = state.isAudioMuted - webRtcPeerConnectionManager.muteCall(!muted) + call?.muteCall(!muted) setState { copy(isAudioMuted = !muted) } } - VectorCallViewActions.ToggleVideo -> { + VectorCallViewActions.ToggleVideo -> { if (state.isVideoCall) { val videoEnabled = state.isVideoEnabled - webRtcPeerConnectionManager.enableVideo(!videoEnabled) + call?.enableVideo(!videoEnabled) setState { copy(isVideoEnabled = !videoEnabled) } @@ -210,14 +203,14 @@ class VectorCallViewModel @AssistedInject constructor( Unit } is VectorCallViewActions.ChangeAudioDevice -> { - webRtcPeerConnectionManager.callAudioManager.setCurrentSoundDevice(action.device) + callManager.callAudioManager.setCurrentSoundDevice(action.device) setState { copy( - soundDevice = webRtcPeerConnectionManager.callAudioManager.getCurrentSoundDevice() + soundDevice = callManager.callAudioManager.getCurrentSoundDevice() ) } } - VectorCallViewActions.SwitchSoundDevice -> { + VectorCallViewActions.SwitchSoundDevice -> { _viewEvents.post( VectorCallViewEvents.ShowSoundDeviceChooser(state.availableSoundDevices, state.soundDevice) ) @@ -225,45 +218,35 @@ class VectorCallViewModel @AssistedInject constructor( VectorCallViewActions.HeadSetButtonPressed -> { if (state.callState.invoke() is CallState.LocalRinging) { // accept call - webRtcPeerConnectionManager.acceptIncomingCall() + call?.acceptIncomingCall() } if (state.callState.invoke() is CallState.Connected) { // end call? - webRtcPeerConnectionManager.endCall() + call?.endCall() } Unit } - VectorCallViewActions.ToggleCamera -> { - webRtcPeerConnectionManager.switchCamera() + VectorCallViewActions.ToggleCamera -> { + call?.switchCamera() } - VectorCallViewActions.ToggleHDSD -> { + VectorCallViewActions.ToggleHDSD -> { if (!state.isVideoCall) return@withState - webRtcPeerConnectionManager.setCaptureFormat(if (state.isHD) CaptureFormat.SD else CaptureFormat.HD) + call?.setCaptureFormat(if (state.isHD) CaptureFormat.SD else CaptureFormat.HD) } }.exhaustive } @AssistedInject.Factory interface Factory { - fun create(initialState: VectorCallViewState, args: CallArgs): VectorCallViewModel + fun create(initialState: VectorCallViewState): VectorCallViewModel } companion object : MvRxViewModelFactory { @JvmStatic - override fun create(viewModelContext: ViewModelContext, state: VectorCallViewState): VectorCallViewModel? { + override fun create(viewModelContext: ViewModelContext, state: VectorCallViewState): VectorCallViewModel { val callActivity: VectorCallActivity = viewModelContext.activity() - val callArgs: CallArgs = viewModelContext.args() - return callActivity.viewModelFactory.create(state, callArgs) - } - - override fun initialState(viewModelContext: ViewModelContext): VectorCallViewState? { - val args: CallArgs = viewModelContext.args() - return VectorCallViewState( - callId = args.callId, - roomId = args.roomId, - isVideoCall = args.isVideoCall - ) + return callActivity.viewModelFactory.create(state) } } } diff --git a/vector/src/main/java/im/vector/app/features/call/VectorCallViewState.kt b/vector/src/main/java/im/vector/app/features/call/VectorCallViewState.kt index f24e810400..e90a7bd458 100644 --- a/vector/src/main/java/im/vector/app/features/call/VectorCallViewState.kt +++ b/vector/src/main/java/im/vector/app/features/call/VectorCallViewState.kt @@ -23,8 +23,8 @@ import org.matrix.android.sdk.api.session.call.CallState import org.matrix.android.sdk.api.util.MatrixItem data class VectorCallViewState( - val callId: String? = null, - val roomId: String = "", + val callId: String, + val roomId: String, val isVideoCall: Boolean, val isAudioMuted: Boolean = false, val isVideoEnabled: Boolean = true, @@ -36,4 +36,13 @@ data class VectorCallViewState( val availableSoundDevices: List = emptyList(), val otherUserMatrixItem: Async = Uninitialized, val callState: Async = Uninitialized -) : MvRxState +) : MvRxState { + + constructor(callArgs: CallArgs): this( + callId = callArgs.callId, + roomId = callArgs.roomId, + isVideoCall = callArgs.isVideoCall + ) + + +} diff --git a/vector/src/main/java/im/vector/app/features/call/service/CallHeadsUpActionReceiver.kt b/vector/src/main/java/im/vector/app/features/call/service/CallHeadsUpActionReceiver.kt index 8510fe04a7..b2b24a8e24 100644 --- a/vector/src/main/java/im/vector/app/features/call/service/CallHeadsUpActionReceiver.kt +++ b/vector/src/main/java/im/vector/app/features/call/service/CallHeadsUpActionReceiver.kt @@ -20,7 +20,7 @@ import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import im.vector.app.core.di.HasVectorInjector -import im.vector.app.features.call.webrtc.WebRtcPeerConnectionManager +import im.vector.app.features.call.webrtc.WebRtcCallManager import timber.log.Timber class CallHeadsUpActionReceiver : BroadcastReceiver() { @@ -48,9 +48,9 @@ class CallHeadsUpActionReceiver : BroadcastReceiver() { // context.stopService(Intent(context, CallHeadsUpService::class.java)) } - private fun onCallRejectClicked(peerConnectionManager: WebRtcPeerConnectionManager) { + private fun onCallRejectClicked(callManager: WebRtcCallManager) { Timber.d("onCallRejectClicked") - peerConnectionManager.endCall() + callManager.endCall() } // private fun onCallAnswerClicked(context: Context) { diff --git a/vector/src/main/java/im/vector/app/features/call/telecom/CallConnection.kt b/vector/src/main/java/im/vector/app/features/call/telecom/CallConnection.kt index ca50fad4ed..dfcc11f5e9 100644 --- a/vector/src/main/java/im/vector/app/features/call/telecom/CallConnection.kt +++ b/vector/src/main/java/im/vector/app/features/call/telecom/CallConnection.kt @@ -22,7 +22,7 @@ import android.telecom.Connection import android.telecom.DisconnectCause import androidx.annotation.RequiresApi import im.vector.app.features.call.VectorCallViewModel -import im.vector.app.features.call.webrtc.WebRtcPeerConnectionManager +import im.vector.app.features.call.webrtc.WebRtcCallManager import timber.log.Timber import javax.inject.Inject @@ -32,7 +32,7 @@ import javax.inject.Inject val callId: String ) : Connection() { - @Inject lateinit var peerConnectionManager: WebRtcPeerConnectionManager + @Inject lateinit var callManager: WebRtcCallManager @Inject lateinit var callViewModel: VectorCallViewModel init { diff --git a/vector/src/main/java/im/vector/app/features/call/webrtc/PeerConnectionObserver.kt b/vector/src/main/java/im/vector/app/features/call/webrtc/PeerConnectionObserver.kt index 0985b38c17..40670412c9 100644 --- a/vector/src/main/java/im/vector/app/features/call/webrtc/PeerConnectionObserver.kt +++ b/vector/src/main/java/im/vector/app/features/call/webrtc/PeerConnectionObserver.kt @@ -17,7 +17,6 @@ package im.vector.app.features.call.webrtc import im.vector.app.features.call.CallAudioManager -import kotlinx.coroutines.GlobalScope import org.matrix.android.sdk.api.session.call.CallState import org.matrix.android.sdk.api.session.call.MxPeerConnectionState import org.webrtc.DataChannel @@ -84,7 +83,7 @@ class PeerConnectionObserver(private val webRtcCall: WebRtcCall, override fun onIceCandidate(iceCandidate: IceCandidate) { Timber.v("## VOIP StreamObserver onIceCandidate: $iceCandidate") - webRtcCall.iceCandidateSource.onNext(iceCandidate) + webRtcCall.onIceCandidate(iceCandidate) } override fun onDataChannel(dc: DataChannel) { @@ -153,7 +152,6 @@ class PeerConnectionObserver(private val webRtcCall: WebRtcCall, override fun onAddStream(stream: MediaStream) { Timber.v("## VOIP StreamObserver onAddStream: $stream") webRtcCall.onAddStream(stream) - } override fun onRemoveStream(stream: MediaStream) { @@ -175,11 +173,7 @@ class PeerConnectionObserver(private val webRtcCall: WebRtcCall, override fun onRenegotiationNeeded() { Timber.v("## VOIP StreamObserver onRenegotiationNeeded") - if (webRtcCall.mxCall.state != CallState.CreateOffer && webRtcCall.mxCall.opponentVersion == 0) { - Timber.v("Opponent does not support renegotiation: ignoring onRenegotiationNeeded event") - return - } - webRtcCall.sendSpdOffer() + webRtcCall.onRenegationNeeded() } /** diff --git a/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt b/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt index 50ac19bda0..cb8dcf7820 100644 --- a/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt +++ b/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt @@ -37,7 +37,6 @@ import io.reactivex.subjects.PublishSubject import io.reactivex.subjects.ReplaySubject import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.asCoroutineDispatcher import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -49,6 +48,7 @@ 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.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.SdpType @@ -72,9 +72,9 @@ import org.webrtc.VideoSource import org.webrtc.VideoTrack import timber.log.Timber import java.lang.ref.WeakReference -import java.util.concurrent.Executor import java.util.concurrent.TimeUnit import javax.inject.Provider +import kotlin.coroutines.CoroutineContext private const val STREAM_ID = "ARDAMS" private const val AUDIO_TRACK_ID = "ARDAMSa0" @@ -85,29 +85,44 @@ class WebRtcCall(val mxCall: MxCall, private val callAudioManager: CallAudioManager, private val rootEglBase: EglBase?, private val context: Context, - private val session: Session, - private val executor: Executor, - private val peerConnectionFactoryProvider: Provider) { + private val dispatcher: CoroutineContext, + private val sessionProvider: Provider, + private val peerConnectionFactoryProvider: Provider, + private val onCallEnded: (WebRtcCall) -> Unit): MxCall.StateListener { - private val dispatcher = executor.asCoroutineDispatcher() + interface Listener: MxCall.StateListener { + fun onCaptureStateChanged() {} + fun onCameraChange() {} + } - var peerConnection: PeerConnection? = null - var localAudioSource: AudioSource? = null - var localAudioTrack: AudioTrack? = null - var localVideoSource: VideoSource? = null - var localVideoTrack: VideoTrack? = null - var remoteVideoTrack: VideoTrack? = null + private val listeners = ArrayList() + + fun addListener(listener: Listener) { + listeners.add(listener) + } + + fun removeListener(listener: Listener) { + listeners.remove(listener) + } + + val callId = mxCall.callId + + private var peerConnection: PeerConnection? = null + private var localAudioSource: AudioSource? = null + private var localAudioTrack: AudioTrack? = null + private var localVideoSource: VideoSource? = null + private var localVideoTrack: VideoTrack? = null + private var remoteVideoTrack: VideoTrack? = null // Perfect negotiation state: https://www.w3.org/TR/webrtc/#perfect-negotiation-example - var makingOffer: Boolean = false - var ignoreOffer: Boolean = false + private var makingOffer: Boolean = false + private var ignoreOffer: Boolean = false private var videoCapturer: CameraVideoCapturer? = null private val availableCamera = ArrayList() private var cameraInUse: CameraProxy? = null private var currentCaptureFormat: CaptureFormat = CaptureFormat.HD - private var capturerIsInError = false private var cameraAvailabilityCallback: CameraManager.AvailabilityCallback? = null // Mute status @@ -117,10 +132,17 @@ class WebRtcCall(val mxCall: MxCall, var offerSdp: CallInviteContent.Offer? = null + var videoCapturerIsInError = false + set(value) { + field = value + listeners.forEach { + tryOrNull { it.onCaptureStateChanged() } + } + } private var localSurfaceRenderers: MutableList> = ArrayList() private var remoteSurfaceRenderers: MutableList> = ArrayList() - val iceCandidateSource: PublishSubject = PublishSubject.create() + private val iceCandidateSource: PublishSubject = PublishSubject.create() private val iceCandidateDisposable = iceCandidateSource .buffer(300, TimeUnit.MILLISECONDS) .subscribe { @@ -132,60 +154,51 @@ class WebRtcCall(val mxCall: MxCall, } } - var remoteCandidateSource: ReplaySubject = ReplaySubject.create() - var remoteIceCandidateDisposable: Disposable? = null + private val remoteCandidateSource: ReplaySubject = ReplaySubject.create() + private var remoteIceCandidateDisposable: Disposable? = null - private fun createLocalStream() { - val peerConnectionFactory = peerConnectionFactoryProvider.get() ?: return - Timber.v("Create local stream for call ${mxCall.callId}") - configureAudioTrack(peerConnectionFactory) - // add video track if needed - if (mxCall.isVideoCall) { - configureVideoTrack(peerConnectionFactory) - } - updateMuteStatus() + init { + mxCall.addListener(this) } - private fun configureAudioTrack(peerConnectionFactory: PeerConnectionFactory) { - val audioSource = peerConnectionFactory.createAudioSource(DEFAULT_AUDIO_CONSTRAINTS) - val audioTrack = peerConnectionFactory.createAudioTrack(AUDIO_TRACK_ID, audioSource) - audioTrack.setEnabled(true) - Timber.v("Add audio track $AUDIO_TRACK_ID to call ${mxCall.callId}") - peerConnection?.addTrack(audioTrack, listOf(STREAM_ID)) - localAudioSource = audioSource - localAudioTrack = audioTrack - } + fun onIceCandidate(iceCandidate: IceCandidate) = iceCandidateSource.onNext(iceCandidate) - fun sendSpdOffer() = GlobalScope.launch(dispatcher) { - val constraints = MediaConstraints() - // These are deprecated options + fun onRenegationNeeded() { + GlobalScope.launch(dispatcher) { + if (mxCall.state != CallState.CreateOffer && mxCall.opponentVersion == 0) { + Timber.v("Opponent does not support renegotiation: ignoring onRenegotiationNeeded event") + return@launch + } + val constraints = MediaConstraints() + // These are deprecated options // constraints.mandatory.add(MediaConstraints.KeyValuePair("OfferToReceiveAudio", "true")) // constraints.mandatory.add(MediaConstraints.KeyValuePair("OfferToReceiveVideo", if (currentCall?.mxCall?.isVideoCall == true) "true" else "false")) - val peerConnection = peerConnection ?: return@launch - Timber.v("## VOIP creating offer...") - makingOffer = true - try { - val sessionDescription = peerConnection.awaitCreateOffer(constraints) ?: return@launch - peerConnection.awaitSetLocalDescription(sessionDescription) - if (peerConnection.iceGatheringState() == PeerConnection.IceGatheringState.GATHERING) { - // Allow a short time for initial candidates to be gathered - delay(200) + val peerConnection = peerConnection ?: return@launch + Timber.v("## VOIP creating offer...") + makingOffer = true + try { + val sessionDescription = peerConnection.awaitCreateOffer(constraints) ?: return@launch + peerConnection.awaitSetLocalDescription(sessionDescription) + if (peerConnection.iceGatheringState() == PeerConnection.IceGatheringState.GATHERING) { + // Allow a short time for initial candidates to be gathered + delay(200) + } + if (mxCall.state == CallState.Terminated) { + return@launch + } + if (mxCall.state == CallState.CreateOffer) { + // send offer to peer + mxCall.offerSdp(sessionDescription.description) + } else { + mxCall.negotiate(sessionDescription.description) + } + } catch (failure: Throwable) { + // Need to handle error properly. + Timber.v("Failure while creating offer") + } finally { + makingOffer = false } - if (mxCall.state == CallState.Terminated) { - return@launch - } - if (mxCall.state == CallState.CreateOffer) { - // send offer to peer - mxCall.offerSdp(sessionDescription.description) - } else { - mxCall.negotiate(sessionDescription.description) - } - } catch (failure: Throwable) { - // Need to handle error properly. - Timber.v("Failure while creating offer") - } finally { - makingOffer = false } } @@ -223,7 +236,8 @@ class WebRtcCall(val mxCall: MxCall, mxCall .takeIf { it.state is CallState.Connected } ?.let { mxCall -> - val name = session.getUser(mxCall.opponentUserId)?.getBestName() + val session = sessionProvider.get() + val name = session?.getUser(mxCall.opponentUserId)?.getBestName() ?: mxCall.roomId // Start background service with notification CallService.onPendingCall( @@ -231,7 +245,7 @@ class WebRtcCall(val mxCall: MxCall, isVideo = mxCall.isVideoCall, roomName = name, roomId = mxCall.roomId, - matrixId = session.myUserId, + matrixId = session?.myUserId ?:"", callId = mxCall.callId) } @@ -255,9 +269,12 @@ class WebRtcCall(val mxCall: MxCall, } } - fun acceptIncomingCall() = GlobalScope.launch { - if (mxCall.state == CallState.LocalRinging) { - internalAcceptIncomingCall() + fun acceptIncomingCall() { + GlobalScope.launch { + Timber.v("## VOIP acceptIncomingCall from state ${mxCall.state}") + if (mxCall.state == CallState.LocalRinging) { + internalAcceptIncomingCall() + } } } @@ -289,14 +306,15 @@ class WebRtcCall(val mxCall: MxCall, .takeIf { it.state is CallState.Connected } ?.let { mxCall -> // Start background service with notification - val name = session.getUser(mxCall.opponentUserId)?.getBestName() + val session = sessionProvider.get() + val name = session?.getUser(mxCall.opponentUserId)?.getBestName() ?: mxCall.opponentUserId CallService.onOnGoingCallBackground( context = context, isVideo = mxCall.isVideoCall, roomName = name, roomId = mxCall.roomId, - matrixId = session.myUserId , + matrixId = session?.myUserId ?: "", callId = mxCall.callId ) } @@ -325,14 +343,15 @@ class WebRtcCall(val mxCall: MxCall, val turnServerResponse = getTurnServer() // Update service state withContext(Dispatchers.Main) { - val name = session.getUser(mxCall.opponentUserId)?.getBestName() + val session = sessionProvider.get() + val name = session?.getUser(mxCall.opponentUserId)?.getBestName() ?: mxCall.roomId CallService.onPendingCall( context = context, isVideo = mxCall.isVideoCall, roomName = name, roomId = mxCall.roomId, - matrixId = session.myUserId, + matrixId = session?.myUserId ?: "", callId = mxCall.callId ) } @@ -393,13 +412,33 @@ class WebRtcCall(val mxCall: MxCall, private suspend fun getTurnServer(): TurnServerResponse? { return tryOrNull { awaitCallback { - session.callSignalingService().getTurnServer(it) + sessionProvider.get()?.callSignalingService()?.getTurnServer(it) } } } + private fun createLocalStream() { + val peerConnectionFactory = peerConnectionFactoryProvider.get() ?: return + Timber.v("Create local stream for call ${mxCall.callId}") + configureAudioTrack(peerConnectionFactory) + // add video track if needed + if (mxCall.isVideoCall) { + configureVideoTrack(peerConnectionFactory) + } + updateMuteStatus() + } + + private fun configureAudioTrack(peerConnectionFactory: PeerConnectionFactory) { + val audioSource = peerConnectionFactory.createAudioSource(DEFAULT_AUDIO_CONSTRAINTS) + val audioTrack = peerConnectionFactory.createAudioTrack(AUDIO_TRACK_ID, audioSource) + audioTrack.setEnabled(true) + Timber.v("Add audio track $AUDIO_TRACK_ID to call ${mxCall.callId}") + peerConnection?.addTrack(audioTrack, listOf(STREAM_ID)) + localAudioSource = audioSource + localAudioTrack = audioTrack + } + private fun configureVideoTrack(peerConnectionFactory: PeerConnectionFactory) { - availableCamera.clear() val cameraIterator = if (Camera2Enumerator.isSupported(context)) { Camera2Enumerator(context) } else { @@ -426,14 +465,14 @@ class WebRtcCall(val mxCall: MxCall, val videoCapturer = cameraIterator.createCapturer(camera.name, object : CameraEventsHandlerAdapter() { override fun onFirstFrameAvailable() { super.onFirstFrameAvailable() - capturerIsInError = false + videoCapturerIsInError = false } override fun onCameraClosed() { super.onCameraClosed() // This could happen if you open the camera app in chat // We then register in order to restart capture as soon as the camera is available again - capturerIsInError = true + videoCapturerIsInError = true val cameraManager = context.getSystemService() cameraAvailabilityCallback = object : CameraManager.AvailabilityCallback() { override fun onCameraAvailable(cameraId: String) { @@ -466,12 +505,10 @@ class WebRtcCall(val mxCall: MxCall, } fun setCaptureFormat(format: CaptureFormat) { - Timber.v("## VOIP setCaptureFormat $format") - executor.execute { - // videoCapturer?.stopCapture() + GlobalScope.launch(dispatcher) { + Timber.v("## VOIP setCaptureFormat $format") videoCapturer?.changeCaptureFormat(format.width, format.height, format.fps) currentCaptureFormat = format - //currentCallsListeners.forEach { tryOrNull { it.onCaptureStateChanged() } } } } @@ -543,6 +580,10 @@ class WebRtcCall(val mxCall: MxCall, localSurfaceRenderers.forEach { it.get()?.setMirror(isFrontCamera) } + listeners.forEach { + tryOrNull { it.onCameraChange() } + } + } override fun onCameraSwitchError(errorDescription: String?) { @@ -577,7 +618,8 @@ class WebRtcCall(val mxCall: MxCall, return currentCaptureFormat } - fun release() { + private fun release() { + mxCall.removeListener(this) videoCapturer?.stopCapture() videoCapturer?.dispose() videoCapturer = null @@ -591,21 +633,22 @@ class WebRtcCall(val mxCall: MxCall, localAudioTrack = null localVideoSource = null localVideoTrack = null + cameraAvailabilityCallback = null } fun onAddStream(stream: MediaStream) { - executor.execute { + GlobalScope.launch(dispatcher) { // reportError("Weird-looking stream: " + stream); if (stream.audioTracks.size > 1 || stream.videoTracks.size > 1) { Timber.e("## VOIP StreamObserver weird looking stream: $stream") // TODO maybe do something more?? mxCall.hangUp() - return@execute + return@launch } if (stream.videoTracks.size == 1) { val remoteVideoTrack = stream.videoTracks.first() remoteVideoTrack.setEnabled(true) - this.remoteVideoTrack = remoteVideoTrack + this@WebRtcCall.remoteVideoTrack = remoteVideoTrack // sink to renderer if attached remoteSurfaceRenderers.forEach { it.get()?.let { remoteVideoTrack.addSink(it) } } } @@ -613,7 +656,7 @@ class WebRtcCall(val mxCall: MxCall, } fun onRemoveStream() { - executor.execute { + GlobalScope.launch(dispatcher) { remoteSurfaceRenderers .mapNotNull { it.get() } .forEach { remoteVideoTrack?.removeSink(it) } @@ -621,26 +664,31 @@ class WebRtcCall(val mxCall: MxCall, } } - fun endCall(originatedByMe: Boolean) { + fun endCall(originatedByMe: Boolean = true, reason: CallHangupContent.Reason? = null) { mxCall.state = CallState.Terminated + //Close tracks ASAP localVideoTrack?.setEnabled(false) localVideoTrack?.setEnabled(false) - cameraAvailabilityCallback?.let { cameraAvailabilityCallback -> val cameraManager = context.getSystemService()!! cameraManager.unregisterAvailabilityCallback(cameraAvailabilityCallback) } release() + onCallEnded(this) if (originatedByMe) { // send hang up event - mxCall.hangUp() + if (mxCall.state is CallState.Connected) { + mxCall.hangUp(reason) + } else { + mxCall.reject() + } } } // Call listener fun onCallIceCandidateReceived(iceCandidatesContent: CallCandidatesContent) { - executor.execute { + GlobalScope.launch(dispatcher) { iceCandidatesContent.candidates.forEach { Timber.v("## VOIP onCallIceCandidateReceived for call ${mxCall.callId} sdp: ${it.candidate}") val iceCandidate = IceCandidate(it.sdpMid, it.sdpMLineIndex, it.candidate) @@ -665,43 +713,49 @@ class WebRtcCall(val mxCall: MxCall, } fun onCallNegotiateReceived(callNegotiateContent: CallNegotiateContent) { - val description = callNegotiateContent.description - val type = description?.type - val sdpText = description?.sdp - if (type == null || sdpText == null) { - Timber.i("Ignoring invalid m.call.negotiate event"); - return; - } - val peerConnection = peerConnection ?: return - // Politeness always follows the direction of the call: in a glare situation, - // we pick either the inbound or outbound call, so one side will always be - // inbound and one outbound - val polite = !mxCall.isOutgoing - // Here we follow the perfect negotiation logic from - // https://developer.mozilla.org/en-US/docs/Web/API/WebRTC_API/Perfect_negotiation - val offerCollision = description.type == SdpType.OFFER - && (makingOffer || peerConnection.signalingState() != PeerConnection.SignalingState.STABLE) - - ignoreOffer = !polite && offerCollision - if (ignoreOffer) { - Timber.i("Ignoring colliding negotiate event because we're impolite") - return - } - GlobalScope.launch(dispatcher) { + val description = callNegotiateContent.description + val type = description?.type + val sdpText = description?.sdp + if (type == null || sdpText == null) { + Timber.i("Ignoring invalid m.call.negotiate event"); + return@launch + } + val peerConnection = peerConnection ?: return@launch + // Politeness always follows the direction of the call: in a glare situation, + // we pick either the inbound or outbound call, so one side will always be + // inbound and one outbound + val polite = !mxCall.isOutgoing + // Here we follow the perfect negotiation logic from + // https://developer.mozilla.org/en-US/docs/Web/API/WebRTC_API/Perfect_negotiation + val offerCollision = description.type == SdpType.OFFER + && (makingOffer || peerConnection.signalingState() != PeerConnection.SignalingState.STABLE) + + ignoreOffer = !polite && offerCollision + if (ignoreOffer) { + Timber.i("Ignoring colliding negotiate event because we're impolite") + return@launch + } try { val sdp = SessionDescription(type.asWebRTC(), sdpText) peerConnection.awaitSetRemoteDescription(sdp) if (type == SdpType.OFFER) { - createAnswer()?.also { - mxCall.negotiate(sdpText) - } + createAnswer() + mxCall.negotiate(sdpText) } } catch (failure: Throwable) { Timber.e(failure, "Failed to complete negotiation") } } } + + // MxCall.StateListener + + override fun onStateUpdate(call: MxCall) { + listeners.forEach { + tryOrNull { it.onStateUpdate(call) } + } + } } private fun MutableList>.addIfNeeded(renderer: SurfaceViewRenderer?) { diff --git a/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcPeerConnectionManager.kt b/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCallManager.kt similarity index 74% rename from vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcPeerConnectionManager.kt rename to vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCallManager.kt index ae2389a9b4..913379752b 100644 --- a/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcPeerConnectionManager.kt +++ b/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCallManager.kt @@ -29,8 +29,6 @@ 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.utils.EglUtils -import im.vector.app.features.call.utils.awaitCreateAnswer -import im.vector.app.features.call.utils.awaitSetLocalDescription import im.vector.app.push.fcm.FcmHelper import kotlinx.coroutines.asCoroutineDispatcher import org.matrix.android.sdk.api.extensions.orFalse @@ -39,7 +37,6 @@ 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.MxCall -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 import org.matrix.android.sdk.api.session.room.model.call.CallHangupContent @@ -47,18 +44,13 @@ 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.internal.util.awaitCallback import org.webrtc.DefaultVideoDecoderFactory import org.webrtc.DefaultVideoEncoderFactory -import org.webrtc.MediaConstraints import org.webrtc.PeerConnectionFactory -import org.webrtc.SessionDescription import org.webrtc.SurfaceViewRenderer import timber.log.Timber -import java.lang.ref.WeakReference import java.util.concurrent.Executors import javax.inject.Inject -import javax.inject.Provider import javax.inject.Singleton /** @@ -66,7 +58,7 @@ import javax.inject.Singleton * Use app context */ @Singleton -class WebRtcPeerConnectionManager @Inject constructor( +class WebRtcCallManager @Inject constructor( private val context: Context, private val activeSessionDataSource: ActiveSessionDataSource ) : CallListener, LifecycleObserver { @@ -81,6 +73,14 @@ class WebRtcPeerConnectionManager @Inject constructor( fun onCameraChange() {} } + var capturerIsInError = false + set(value) { + field = value + currentCallsListeners.forEach { + tryOrNull { it.onCaptureStateChanged() } + } + } + private val currentCallsListeners = emptyList().toMutableList() fun addCurrentCallListener(listener: CurrentCallListener) { currentCallsListeners.add(listener) @@ -90,7 +90,7 @@ class WebRtcPeerConnectionManager @Inject constructor( currentCallsListeners.remove(listener) } - val callAudioManager = CallAudioManager(context.applicationContext) { + val callAudioManager = CallAudioManager(context) { currentCallsListeners.forEach { tryOrNull { it.onAudioDevicesChange() } } @@ -104,34 +104,6 @@ class WebRtcPeerConnectionManager @Inject constructor( private var isInBackground: Boolean = true - var capturerIsInError = false - set(value) { - field = value - currentCallsListeners.forEach { - tryOrNull { it.onCaptureStateChanged() } - } - } - - var localSurfaceRenderers: MutableList> = ArrayList() - var remoteSurfaceRenderers: MutableList> = ArrayList() - - private fun MutableList>.addIfNeeded(renderer: SurfaceViewRenderer?) { - if (renderer == null) return - val exists = any { - it.get() == renderer - } - if (!exists) { - add(WeakReference(renderer)) - } - } - - private fun MutableList>.removeIfNeeded(renderer: SurfaceViewRenderer?) { - if (renderer == null) return - removeAll { - it.get() == renderer - } - } - @OnLifecycleEvent(Lifecycle.Event.ON_RESUME) fun entersForeground() { isInBackground = false @@ -150,6 +122,12 @@ class WebRtcPeerConnectionManager @Inject constructor( } } + private val callsByCallId = HashMap() + + fun getCallById(callId: String): WebRtcCall? { + return callsByCallId[callId] + } + fun headSetButtonTapped() { Timber.v("## VOIP headSetButtonTapped") val call = currentCall?.mxCall ?: return @@ -163,19 +141,11 @@ class WebRtcPeerConnectionManager @Inject constructor( } } - private suspend fun getTurnServer(): TurnServerResponse? { - return tryOrNull { - awaitCallback { - currentSession?.callSignalingService()?.getTurnServer(it) - } - } - } - fun attachViewRenderers(localViewRenderer: SurfaceViewRenderer?, remoteViewRenderer: SurfaceViewRenderer, mode: String?) { currentCall?.attachViewRenderers(localViewRenderer, remoteViewRenderer, mode) } - private fun createPeerConnectionFactory() { + private fun createPeerConnectionFactoryIfNeeded() { if (peerConnectionFactory != null) return Timber.v("## VOIP createPeerConnectionFactory") val eglBaseContext = rootEglBase?.eglBaseContext ?: return Unit.also { @@ -202,12 +172,9 @@ class WebRtcPeerConnectionManager @Inject constructor( .setVideoEncoderFactory(defaultVideoEncoderFactory) .setVideoDecoderFactory(defaultVideoDecoderFactory) .createPeerConnectionFactory() - - // attachViewRenderersInternal() } fun acceptIncomingCall() { - Timber.v("## VOIP acceptIncomingCall from state ${currentCall?.mxCall?.state}") currentCall?.acceptIncomingCall() } @@ -215,11 +182,12 @@ class WebRtcPeerConnectionManager @Inject constructor( currentCall?.detachRenderers(renderers) } - fun close() { - Timber.v("## VOIP WebRtcPeerConnectionManager close() >") + private fun onCallEnded(call: WebRtcCall) { + Timber.v("## VOIP WebRtcPeerConnectionManager onCall ended: ${call.mxCall.callId}") CallService.onNoActiveCall(context) callAudioManager.stop() currentCall = null + callsByCallId.remove(call.mxCall.callId) // This must be done in this thread executor.execute { if (currentCall == null) { @@ -231,68 +199,28 @@ class WebRtcPeerConnectionManager @Inject constructor( } } - companion object { - - private const val STREAM_ID = "ARDAMS" - private const val AUDIO_TRACK_ID = "ARDAMSa0" - private const val VIDEO_TRACK_ID = "ARDAMSv0" - - private val DEFAULT_AUDIO_CONSTRAINTS = MediaConstraints().apply { - // add all existing audio filters to avoid having echos -// mandatory.add(MediaConstraints.KeyValuePair("googEchoCancellation", "true")) -// mandatory.add(MediaConstraints.KeyValuePair("googEchoCancellation2", "true")) -// mandatory.add(MediaConstraints.KeyValuePair("googDAEchoCancellation", "true")) -// -// mandatory.add(MediaConstraints.KeyValuePair("googTypingNoiseDetection", "true")) -// -// mandatory.add(MediaConstraints.KeyValuePair("googAutoGainControl", "true")) -// mandatory.add(MediaConstraints.KeyValuePair("googAutoGainControl2", "true")) -// -// mandatory.add(MediaConstraints.KeyValuePair("googNoiseSuppression", "true")) -// mandatory.add(MediaConstraints.KeyValuePair("googNoiseSuppression2", "true")) -// -// mandatory.add(MediaConstraints.KeyValuePair("googAudioMirroring", "false")) -// mandatory.add(MediaConstraints.KeyValuePair("googHighpassFilter", "true")) - } - } - fun startOutgoingCall(signalingRoomId: String, otherUserId: String, isVideoCall: Boolean) { Timber.v("## VOIP startOutgoingCall in room $signalingRoomId to $otherUserId isVideo $isVideoCall") executor.execute { - if (peerConnectionFactory == null) { - createPeerConnectionFactory() - } + createPeerConnectionFactoryIfNeeded() } - val createdCall = currentSession?.callSignalingService()?.createOutgoingCall(signalingRoomId, otherUserId, isVideoCall) ?: return - val webRtcCall = WebRtcCall( - mxCall = createdCall, - callAudioManager = callAudioManager, - rootEglBase = rootEglBase, - context = context, - executor = executor, - peerConnectionFactoryProvider = Provider { - createPeerConnectionFactory() - peerConnectionFactory - }, - session = currentSession!! - ) + val mxCall = currentSession?.callSignalingService()?.createOutgoingCall(signalingRoomId, otherUserId, isVideoCall) ?: return + createWebRtcCall(mxCall) + callAudioManager.startForCall(mxCall) - callAudioManager.startForCall(createdCall) - currentCall = webRtcCall - - val name = currentSession?.getUser(createdCall.opponentUserId)?.getBestName() - ?: createdCall.opponentUserId + val name = currentSession?.getUser(mxCall.opponentUserId)?.getBestName() + ?: mxCall.opponentUserId CallService.onOutgoingCallRinging( context = context.applicationContext, - isVideo = createdCall.isVideoCall, + isVideo = mxCall.isVideoCall, roomName = name, - roomId = createdCall.roomId, + roomId = mxCall.roomId, matrixId = currentSession?.myUserId ?: "", - callId = createdCall.callId) + callId = mxCall.callId) // start the activity now - context.applicationContext.startActivity(VectorCallActivity.newIntent(context, createdCall)) + context.startActivity(VectorCallActivity.newIntent(context, mxCall)) } override fun onCallIceCandidateReceived(mxCall: MxCall, iceCandidatesContent: CallCandidatesContent) { @@ -311,19 +239,9 @@ class WebRtcPeerConnectionManager @Inject constructor( // Just ignore, maybe we could answer from other session? return } - val webRtcCall = WebRtcCall( - mxCall = mxCall, - callAudioManager = callAudioManager, - rootEglBase = rootEglBase, - context = context, - executor = executor, - peerConnectionFactoryProvider = { - createPeerConnectionFactory() - peerConnectionFactory - }, - session = currentSession!! - ) - currentCall = webRtcCall + createWebRtcCall(mxCall).apply { + offerSdp = callInviteContent.offer + } callAudioManager.startForCall(mxCall) // Start background service with notification val name = currentSession?.getUser(mxCall.opponentUserId)?.getBestName() @@ -336,8 +254,6 @@ class WebRtcPeerConnectionManager @Inject constructor( matrixId = currentSession?.myUserId ?: "", callId = mxCall.callId ) - webRtcCall.offerSdp = callInviteContent.offer - // If this is received while in background, the app will not sync, // and thus won't be able to received events. For example if the call is // accepted on an other session this device will continue ringing @@ -351,21 +267,23 @@ class WebRtcPeerConnectionManager @Inject constructor( } } - private suspend fun createAnswer(call: WebRtcCall): SessionDescription? { - Timber.w("## VOIP createAnswer") - val peerConnection = call.peerConnection ?: return null - val constraints = MediaConstraints().apply { - mandatory.add(MediaConstraints.KeyValuePair("OfferToReceiveAudio", "true")) - mandatory.add(MediaConstraints.KeyValuePair("OfferToReceiveVideo", if (call.mxCall.isVideoCall) "true" else "false")) - } - return try { - val localDescription = peerConnection.awaitCreateAnswer(constraints) ?: return null - peerConnection.awaitSetLocalDescription(localDescription) - localDescription - } catch (failure: Throwable) { - Timber.v("Fail to create answer") - null - } + private fun createWebRtcCall(mxCall: MxCall): WebRtcCall { + val webRtcCall = WebRtcCall( + mxCall = mxCall, + callAudioManager = callAudioManager, + rootEglBase = rootEglBase, + context = context, + dispatcher = dispatcher, + peerConnectionFactoryProvider = { + createPeerConnectionFactoryIfNeeded() + peerConnectionFactory + }, + sessionProvider = { currentSession }, + onCallEnded = this::onCallEnded + ) + currentCall = webRtcCall + callsByCallId[mxCall.callId] = webRtcCall + return webRtcCall } fun muteCall(muted: Boolean) { @@ -397,11 +315,7 @@ class WebRtcPeerConnectionManager @Inject constructor( } fun endCall(originatedByMe: Boolean = true) { - // Update service state - CallService.onNoActiveCall(context) - // close tracks ASAP currentCall?.endCall(originatedByMe) - close() } fun onWiredDeviceEvent(event: WiredHeadsetStateReceiver.HeadsetPlugEvent) { @@ -478,6 +392,7 @@ class WebRtcPeerConnectionManager @Inject constructor( override fun onCallManagedByOtherSession(callId: String) { Timber.v("## VOIP onCallManagedByOtherSession: $callId") currentCall = null + callsByCallId.remove(callId) CallService.onNoActiveCall(context) // did we start background sync? so we should stop it diff --git a/vector/src/main/java/im/vector/app/features/home/HomeDetailFragment.kt b/vector/src/main/java/im/vector/app/features/home/HomeDetailFragment.kt index b204320a05..2178bfccd6 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeDetailFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeDetailFragment.kt @@ -35,7 +35,7 @@ import im.vector.app.core.ui.views.ActiveCallViewHolder import im.vector.app.core.ui.views.KeysBackupBanner import im.vector.app.features.call.SharedActiveCallViewModel import im.vector.app.features.call.VectorCallActivity -import im.vector.app.features.call.webrtc.WebRtcPeerConnectionManager +import im.vector.app.features.call.webrtc.WebRtcCallManager import im.vector.app.features.home.room.list.RoomListFragment import im.vector.app.features.home.room.list.RoomListParams import im.vector.app.features.popup.PopupAlertManager @@ -62,7 +62,7 @@ class HomeDetailFragment @Inject constructor( private val serverBackupStatusViewModelFactory: ServerBackupStatusViewModel.Factory, private val avatarRenderer: AvatarRenderer, private val alertManager: PopupAlertManager, - private val webRtcPeerConnectionManager: WebRtcPeerConnectionManager, + private val callManager: WebRtcCallManager, private val vectorPreferences: VectorPreferences ) : VectorBaseFragment(), KeysBackupBanner.Delegate, ActiveCallView.Callback, ServerBackupStatusViewModel.Factory { @@ -120,7 +120,7 @@ class HomeDetailFragment @Inject constructor( sharedCallActionViewModel .activeCall .observe(viewLifecycleOwner, Observer { - activeCallViewHolder.updateCall(it, webRtcPeerConnectionManager) + activeCallViewHolder.updateCall(it, callManager) invalidateOptionsMenu() }) } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt index 925380944c..ac577a0e68 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt @@ -116,7 +116,7 @@ import im.vector.app.features.attachments.preview.AttachmentsPreviewArgs import im.vector.app.features.attachments.toGroupedContentAttachmentData import im.vector.app.features.call.SharedActiveCallViewModel import im.vector.app.features.call.VectorCallActivity -import im.vector.app.features.call.webrtc.WebRtcPeerConnectionManager +import im.vector.app.features.call.webrtc.WebRtcCallManager import im.vector.app.features.call.conference.JitsiCallViewModel import im.vector.app.features.command.Command import im.vector.app.features.crypto.keysbackup.restore.KeysBackupRestoreActivity @@ -218,7 +218,7 @@ class RoomDetailFragment @Inject constructor( private val vectorPreferences: VectorPreferences, private val colorProvider: ColorProvider, private val notificationUtils: NotificationUtils, - private val webRtcPeerConnectionManager: WebRtcPeerConnectionManager, + private val callManager: WebRtcCallManager, private val matrixItemColorProvider: MatrixItemColorProvider, private val imageContentRenderer: ImageContentRenderer, private val roomDetailPendingActionStore: RoomDetailPendingActionStore @@ -315,7 +315,7 @@ class RoomDetailFragment @Inject constructor( sharedCallActionViewModel .activeCall .observe(viewLifecycleOwner, Observer { - activeCallViewHolder.updateCall(it, webRtcPeerConnectionManager) + activeCallViewHolder.updateCall(it, callManager) invalidateOptionsMenu() }) @@ -514,7 +514,7 @@ class RoomDetailFragment @Inject constructor( } override fun onDestroy() { - activeCallViewHolder.unBind(webRtcPeerConnectionManager) + activeCallViewHolder.unBind(callManager) roomDetailViewModel.handle(RoomDetailAction.ExitTrackingUnreadMessagesState) super.onDestroy() } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt index b757830104..b6472291b2 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt @@ -32,7 +32,7 @@ import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.resources.StringProvider import im.vector.app.core.utils.subscribeLogError -import im.vector.app.features.call.webrtc.WebRtcPeerConnectionManager +import im.vector.app.features.call.webrtc.WebRtcCallManager import im.vector.app.features.command.CommandParser import im.vector.app.features.command.ParsedCommand import im.vector.app.features.crypto.verification.SupportedVerificationMethodsProvider @@ -114,7 +114,7 @@ class RoomDetailViewModel @AssistedInject constructor( private val stickerPickerActionHandler: StickerPickerActionHandler, private val roomSummaryHolder: RoomSummaryHolder, private val typingHelper: TypingHelper, - private val webRtcPeerConnectionManager: WebRtcPeerConnectionManager, + private val callManager: WebRtcCallManager, timelineSettingsFactory: TimelineSettingsFactory ) : VectorViewModel(initialState), Timeline.Listener { @@ -306,12 +306,12 @@ class RoomDetailViewModel @AssistedInject constructor( private fun handleStartCall(action: RoomDetailAction.StartCall) { room.roomSummary()?.otherMemberIds?.firstOrNull()?.let { - webRtcPeerConnectionManager.startOutgoingCall(room.roomId, it, action.isVideo) + callManager.startOutgoingCall(room.roomId, it, action.isVideo) } } private fun handleEndCall() { - webRtcPeerConnectionManager.endCall() + callManager.endCall() } private fun handleSelectStickerAttachment() { @@ -566,7 +566,7 @@ class RoomDetailViewModel @AssistedInject constructor( R.id.open_matrix_apps -> true R.id.voice_call, R.id.video_call -> true // always show for discoverability - R.id.hangup_call -> webRtcPeerConnectionManager.currentCall != null + R.id.hangup_call -> callManager.currentCall != null R.id.search -> true else -> false }