VoIP: continue refactoring

This commit is contained in:
ganfra 2020-11-24 17:30:13 +01:00
parent d7f7aa09fc
commit 7620aa4264
20 changed files with 366 additions and 407 deletions

View file

@ -57,5 +57,8 @@ interface CallListener {
*/ */
fun onCallNegotiateReceived(callNegotiateContent: CallNegotiateContent) fun onCallNegotiateReceived(callNegotiateContent: CallNegotiateContent)
/**
* Called when the call has been managed by an other session
*/
fun onCallManagedByOtherSession(callId: String) fun onCallManagedByOtherSession(callId: String)
} }

View file

@ -17,6 +17,7 @@
package org.matrix.android.sdk.api.session.call 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.CallCandidate
import org.matrix.android.sdk.api.session.room.model.call.CallHangupContent
import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.api.util.Optional
interface MxCallDetail { interface MxCallDetail {
@ -66,7 +67,7 @@ interface MxCall : MxCallDetail {
/** /**
* End the call * End the call
*/ */
fun hangUp() fun hangUp(reason: CallHangupContent.Reason? = null)
/** /**
* Start a call * Start a call

View file

@ -134,12 +134,12 @@ internal class MxCallImpl(
state = CallState.Terminated state = CallState.Terminated
} }
override fun hangUp() { override fun hangUp(reason: CallHangupContent.Reason?) {
Timber.v("## VOIP hangup $callId") Timber.v("## VOIP hangup $callId")
CallHangupContent( CallHangupContent(
callId = callId, callId = callId,
partyId = ourPartyId, partyId = ourPartyId,
reason = CallHangupContent.Reason.USER_HANGUP reason = reason ?: CallHangupContent.Reason.USER_HANGUP
) )
.let { createEventAndLocalEcho(type = EventType.CALL_HANGUP, roomId = roomId, content = it.toContent()) } .let { createEventAndLocalEcho(type = EventType.CALL_HANGUP, roomId = roomId, content = it.toContent()) }
.also { eventSenderProcessor.postEvent(it) } .also { eventSenderProcessor.postEvent(it) }

View file

@ -42,7 +42,7 @@ import im.vector.app.core.di.HasVectorInjector
import im.vector.app.core.di.VectorComponent import im.vector.app.core.di.VectorComponent
import im.vector.app.core.extensions.configureAndStart import im.vector.app.core.extensions.configureAndStart
import im.vector.app.core.rx.RxConfig 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.configuration.VectorConfiguration
import im.vector.app.features.disclaimer.doNotShowDisclaimerDialog import im.vector.app.features.disclaimer.doNotShowDisclaimerDialog
import im.vector.app.features.lifecycle.VectorActivityLifecycleCallbacks import im.vector.app.features.lifecycle.VectorActivityLifecycleCallbacks
@ -90,7 +90,7 @@ class VectorApplication :
@Inject lateinit var rxConfig: RxConfig @Inject lateinit var rxConfig: RxConfig
@Inject lateinit var popupAlertManager: PopupAlertManager @Inject lateinit var popupAlertManager: PopupAlertManager
@Inject lateinit var pinLocker: PinLocker @Inject lateinit var pinLocker: PinLocker
@Inject lateinit var webRtcPeerConnectionManager: WebRtcPeerConnectionManager @Inject lateinit var callManager: WebRtcCallManager
lateinit var vectorComponent: VectorComponent lateinit var vectorComponent: VectorComponent
@ -175,7 +175,7 @@ class VectorApplication :
}) })
ProcessLifecycleOwner.get().lifecycle.addObserver(appStateHandler) ProcessLifecycleOwner.get().lifecycle.addObserver(appStateHandler)
ProcessLifecycleOwner.get().lifecycle.addObserver(pinLocker) ProcessLifecycleOwner.get().lifecycle.addObserver(pinLocker)
ProcessLifecycleOwner.get().lifecycle.addObserver(webRtcPeerConnectionManager) ProcessLifecycleOwner.get().lifecycle.addObserver(callManager)
// This should be done as early as possible // This should be done as early as possible
// initKnownEmojiHashSet(appContext) // initKnownEmojiHashSet(appContext)

View file

@ -18,7 +18,7 @@ package im.vector.app.core.di
import arrow.core.Option import arrow.core.Option
import im.vector.app.ActiveSessionDataSource 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.keysrequest.KeyRequestHandler
import im.vector.app.features.crypto.verification.IncomingVerificationRequestHandler import im.vector.app.features.crypto.verification.IncomingVerificationRequestHandler
import im.vector.app.features.notifications.PushRuleTriggerListener import im.vector.app.features.notifications.PushRuleTriggerListener
@ -35,7 +35,7 @@ class ActiveSessionHolder @Inject constructor(private val authenticationService:
private val sessionObservableStore: ActiveSessionDataSource, private val sessionObservableStore: ActiveSessionDataSource,
private val keyRequestHandler: KeyRequestHandler, private val keyRequestHandler: KeyRequestHandler,
private val incomingVerificationRequestHandler: IncomingVerificationRequestHandler, private val incomingVerificationRequestHandler: IncomingVerificationRequestHandler,
private val webRtcPeerConnectionManager: WebRtcPeerConnectionManager, private val callManager: WebRtcCallManager,
private val pushRuleTriggerListener: PushRuleTriggerListener, private val pushRuleTriggerListener: PushRuleTriggerListener,
private val sessionListener: SessionListener, private val sessionListener: SessionListener,
private val imageManager: ImageManager private val imageManager: ImageManager
@ -52,7 +52,7 @@ class ActiveSessionHolder @Inject constructor(private val authenticationService:
incomingVerificationRequestHandler.start(session) incomingVerificationRequestHandler.start(session)
session.addListener(sessionListener) session.addListener(sessionListener)
pushRuleTriggerListener.startWithSession(session) pushRuleTriggerListener.startWithSession(session)
session.callSignalingService().addCallListener(webRtcPeerConnectionManager) session.callSignalingService().addCallListener(callManager)
imageManager.onSessionStarted(session) imageManager.onSessionStarted(session)
} }
@ -60,7 +60,7 @@ class ActiveSessionHolder @Inject constructor(private val authenticationService:
// Do some cleanup first // Do some cleanup first
getSafeActiveSession()?.let { getSafeActiveSession()?.let {
Timber.w("clearActiveSession of ${it.myUserId}") Timber.w("clearActiveSession of ${it.myUserId}")
it.callSignalingService().removeCallListener(webRtcPeerConnectionManager) it.callSignalingService().removeCallListener(callManager)
it.removeListener(sessionListener) it.removeListener(sessionListener)
} }

View file

@ -29,7 +29,7 @@ import im.vector.app.core.error.ErrorFormatter
import im.vector.app.core.pushers.PushersManager import im.vector.app.core.pushers.PushersManager
import im.vector.app.core.utils.AssetReader import im.vector.app.core.utils.AssetReader
import im.vector.app.core.utils.DimensionConverter 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.configuration.VectorConfiguration
import im.vector.app.features.crypto.keysrequest.KeyRequestHandler import im.vector.app.features.crypto.keysrequest.KeyRequestHandler
import im.vector.app.features.crypto.verification.IncomingVerificationRequestHandler import im.vector.app.features.crypto.verification.IncomingVerificationRequestHandler
@ -153,7 +153,7 @@ interface VectorComponent {
fun pinLocker(): PinLocker fun pinLocker(): PinLocker
fun webRtcPeerConnectionManager(): WebRtcPeerConnectionManager fun webRtcPeerConnectionManager(): WebRtcCallManager
@Component.Factory @Component.Factory
interface Factory { interface Factory {

View file

@ -25,7 +25,7 @@ import android.view.KeyEvent
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.media.session.MediaButtonReceiver import androidx.media.session.MediaButtonReceiver
import im.vector.app.core.extensions.vectorComponent 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.call.telecom.CallConnection
import im.vector.app.features.notifications.NotificationUtils import im.vector.app.features.notifications.NotificationUtils
import timber.log.Timber import timber.log.Timber
@ -38,7 +38,7 @@ class CallService : VectorService(), WiredHeadsetStateReceiver.HeadsetEventListe
private val connections = mutableMapOf<String, CallConnection>() private val connections = mutableMapOf<String, CallConnection>()
private lateinit var notificationUtils: NotificationUtils private lateinit var notificationUtils: NotificationUtils
private lateinit var webRtcPeerConnectionManager: WebRtcPeerConnectionManager private lateinit var callManager: WebRtcCallManager
private var callRingPlayerIncoming: CallRingPlayerIncoming? = null private var callRingPlayerIncoming: CallRingPlayerIncoming? = null
private var callRingPlayerOutgoing: CallRingPlayerOutgoing? = null private var callRingPlayerOutgoing: CallRingPlayerOutgoing? = null
@ -53,7 +53,7 @@ class CallService : VectorService(), WiredHeadsetStateReceiver.HeadsetEventListe
override fun onMediaButtonEvent(mediaButtonEvent: Intent?): Boolean { override fun onMediaButtonEvent(mediaButtonEvent: Intent?): Boolean {
val keyEvent = mediaButtonEvent?.getParcelableExtra<KeyEvent>(Intent.EXTRA_KEY_EVENT) ?: return false val keyEvent = mediaButtonEvent?.getParcelableExtra<KeyEvent>(Intent.EXTRA_KEY_EVENT) ?: return false
if (keyEvent.keyCode == KeyEvent.KEYCODE_HEADSETHOOK) { if (keyEvent.keyCode == KeyEvent.KEYCODE_HEADSETHOOK) {
webRtcPeerConnectionManager.headSetButtonTapped() callManager.headSetButtonTapped()
return true return true
} }
return false return false
@ -63,7 +63,7 @@ class CallService : VectorService(), WiredHeadsetStateReceiver.HeadsetEventListe
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
notificationUtils = vectorComponent().notificationUtils() notificationUtils = vectorComponent().notificationUtils()
webRtcPeerConnectionManager = vectorComponent().webRtcPeerConnectionManager() callManager = vectorComponent().webRtcPeerConnectionManager()
callRingPlayerIncoming = CallRingPlayerIncoming(applicationContext) callRingPlayerIncoming = CallRingPlayerIncoming(applicationContext)
callRingPlayerOutgoing = CallRingPlayerOutgoing(applicationContext) callRingPlayerOutgoing = CallRingPlayerOutgoing(applicationContext)
wiredHeadsetStateReceiver = WiredHeadsetStateReceiver.createAndRegister(this, this) wiredHeadsetStateReceiver = WiredHeadsetStateReceiver.createAndRegister(this, this)
@ -375,11 +375,11 @@ class CallService : VectorService(), WiredHeadsetStateReceiver.HeadsetEventListe
override fun onHeadsetEvent(event: WiredHeadsetStateReceiver.HeadsetPlugEvent) { override fun onHeadsetEvent(event: WiredHeadsetStateReceiver.HeadsetPlugEvent) {
Timber.v("## VOIP: onHeadsetEvent $event") Timber.v("## VOIP: onHeadsetEvent $event")
webRtcPeerConnectionManager.onWiredDeviceEvent(event) callManager.onWiredDeviceEvent(event)
} }
override fun onBTHeadsetEvent(event: BluetoothHeadsetReceiver.BTHeadsetPlugEvent) { override fun onBTHeadsetEvent(event: BluetoothHeadsetReceiver.BTHeadsetPlugEvent) {
Timber.v("## VOIP: onBTHeadsetEvent $event") Timber.v("## VOIP: onBTHeadsetEvent $event")
webRtcPeerConnectionManager.onWirelessDeviceEvent(event) callManager.onWirelessDeviceEvent(event)
} }
} }

View file

@ -20,7 +20,7 @@ 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.WebRtcPeerConnectionManager 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 org.matrix.android.sdk.api.session.call.MxCall import org.matrix.android.sdk.api.session.call.MxCall
@ -35,7 +35,7 @@ class ActiveCallViewHolder {
private var activeCallPipInitialized = false private var activeCallPipInitialized = false
fun updateCall(activeCall: MxCall?, webRtcPeerConnectionManager: WebRtcPeerConnectionManager) { fun updateCall(activeCall: MxCall?, callManager: WebRtcCallManager) {
val hasActiveCall = activeCall?.state is CallState.Connected val hasActiveCall = activeCall?.state is CallState.Connected
if (hasActiveCall) { if (hasActiveCall) {
val isVideoCall = activeCall?.isVideoCall == true val isVideoCall = activeCall?.isVideoCall == true
@ -44,14 +44,14 @@ class ActiveCallViewHolder {
pipWrapper?.isVisible = isVideoCall pipWrapper?.isVisible = isVideoCall
activeCallPiP?.isVisible = isVideoCall activeCallPiP?.isVisible = isVideoCall
activeCallPiP?.let { activeCallPiP?.let {
webRtcPeerConnectionManager.attachViewRenderers(null, it, null) callManager.attachViewRenderers(null, it, null)
} }
} else { } else {
activeCallView?.isVisible = false activeCallView?.isVisible = false
activeCallPiP?.isVisible = false activeCallPiP?.isVisible = false
pipWrapper?.isVisible = false pipWrapper?.isVisible = false
activeCallPiP?.let { 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 { activeCallPiP?.let {
webRtcPeerConnectionManager.detachRenderers(listOf(it)) callManager.detachRenderers(listOf(it))
} }
if (activeCallPipInitialized) { if (activeCallPipInitialized) {
activeCallPiP?.release() activeCallPiP?.release()

View file

@ -18,12 +18,12 @@ package im.vector.app.features.call
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel 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 org.matrix.android.sdk.api.session.call.MxCall
import javax.inject.Inject import javax.inject.Inject
class SharedActiveCallViewModel @Inject constructor( class SharedActiveCallViewModel @Inject constructor(
private val webRtcPeerConnectionManager: WebRtcPeerConnectionManager private val callManager: WebRtcCallManager
) : ViewModel() { ) : ViewModel() {
val activeCall: MutableLiveData<MxCall?> = MutableLiveData() val activeCall: MutableLiveData<MxCall?> = 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?) { override fun onCurrentCallChange(call: MxCall?) {
activeCall.value?.removeListener(callStateListener) activeCall.value?.removeListener(callStateListener)
activeCall.postValue(call) activeCall.postValue(call)
@ -46,13 +46,13 @@ class SharedActiveCallViewModel @Inject constructor(
} }
init { init {
activeCall.postValue(webRtcPeerConnectionManager.currentCall?.mxCall) activeCall.postValue(callManager.currentCall?.mxCall)
webRtcPeerConnectionManager.addCurrentCallListener(listener) callManager.addCurrentCallListener(listener)
} }
override fun onCleared() { override fun onCleared() {
activeCall.value?.removeListener(callStateListener) activeCall.value?.removeListener(callStateListener)
webRtcPeerConnectionManager.removeCurrentCallListener(listener) callManager.removeCurrentCallListener(listener)
super.onCleared() super.onCleared()
} }
} }

View file

@ -53,7 +53,7 @@ import kotlinx.android.parcel.Parcelize
import kotlinx.android.synthetic.main.activity_call.* import kotlinx.android.synthetic.main.activity_call.*
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.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.MxCallDetail
import org.matrix.android.sdk.api.session.call.MxPeerConnectionState 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
@ -67,7 +67,7 @@ import javax.inject.Inject
@Parcelize @Parcelize
data class CallArgs( data class CallArgs(
val roomId: String, val roomId: String,
val callId: String?, val callId: String,
val participantUserId: String, val participantUserId: String,
val isIncomingCall: Boolean, val isIncomingCall: Boolean,
val isVideoCall: Boolean val isVideoCall: Boolean
@ -87,7 +87,7 @@ class VectorCallActivity : VectorBaseActivity(), CallControlsView.InteractionLis
private val callViewModel: VectorCallViewModel by viewModel() private val callViewModel: VectorCallViewModel by viewModel()
private lateinit var callArgs: CallArgs private lateinit var callArgs: CallArgs
@Inject lateinit var peerConnectionManager: WebRtcPeerConnectionManager @Inject lateinit var callManager: WebRtcCallManager
@Inject lateinit var viewModelFactory: VectorCallViewModel.Factory @Inject lateinit var viewModelFactory: VectorCallViewModel.Factory
@ -211,7 +211,7 @@ class VectorCallActivity : VectorBaseActivity(), CallControlsView.InteractionLis
} }
override fun onDestroy() { override fun onDestroy() {
peerConnectionManager.detachRenderers(listOf(pipRenderer, fullscreenRenderer)) callManager.detachRenderers(listOf(pipRenderer, fullscreenRenderer))
if (surfaceRenderersAreInitialized) { if (surfaceRenderersAreInitialized) {
pipRenderer.release() pipRenderer.release()
fullscreenRenderer.release() fullscreenRenderer.release()
@ -276,7 +276,7 @@ class VectorCallActivity : VectorBaseActivity(), CallControlsView.InteractionLis
callConnectingProgress.isVisible = true callConnectingProgress.isVisible = true
} }
// ensure all attached? // ensure all attached?
peerConnectionManager.attachViewRenderers(pipRenderer, fullscreenRenderer, null) callManager.attachViewRenderers(pipRenderer, fullscreenRenderer, null)
} }
is CallState.Terminated -> { is CallState.Terminated -> {
finish() finish()
@ -326,7 +326,7 @@ class VectorCallActivity : VectorBaseActivity(), CallControlsView.InteractionLis
pipRenderer.setEnableHardwareScaler(true /* enabled */) pipRenderer.setEnableHardwareScaler(true /* enabled */)
fullscreenRenderer.setEnableHardwareScaler(true /* enabled */) fullscreenRenderer.setEnableHardwareScaler(true /* enabled */)
peerConnectionManager.attachViewRenderers(pipRenderer, fullscreenRenderer, callManager.attachViewRenderers(pipRenderer, fullscreenRenderer,
intent.getStringExtra(EXTRA_MODE)?.takeIf { isFirstCreation() }) intent.getStringExtra(EXTRA_MODE)?.takeIf { isFirstCreation() })
pipRenderer.setOnClickListener { pipRenderer.setOnClickListener {
@ -382,7 +382,7 @@ class VectorCallActivity : VectorBaseActivity(), CallControlsView.InteractionLis
} }
fun newIntent(context: Context, fun newIntent(context: Context,
callId: String?, callId: String,
roomId: String, roomId: String,
otherUserId: String, otherUserId: String,
isIncomingCall: Boolean, isIncomingCall: Boolean,

View file

@ -26,6 +26,8 @@ import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject import com.squareup.inject.assisted.AssistedInject
import im.vector.app.core.extensions.exhaustive import im.vector.app.core.extensions.exhaustive
import im.vector.app.core.platform.VectorViewModel 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.MatrixCallback
import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.call.CallState import org.matrix.android.sdk.api.session.call.CallState
@ -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.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 org.webrtc.PeerConnection
import java.util.Timer import java.util.Timer
import java.util.TimerTask import java.util.TimerTask
class VectorCallViewModel @AssistedInject constructor( class VectorCallViewModel @AssistedInject constructor(
@Assisted initialState: VectorCallViewState, @Assisted initialState: VectorCallViewState,
@Assisted val args: CallArgs,
val session: Session, val session: Session,
val webRtcPeerConnectionManager: WebRtcPeerConnectionManager, val callManager: WebRtcCallManager,
val proximityManager: CallProximityManager val proximityManager: CallProximityManager
) : VectorViewModel<VectorCallViewState, VectorCallViewActions, VectorCallViewEvents>(initialState) { ) : VectorViewModel<VectorCallViewState, VectorCallViewActions, VectorCallViewEvents>(initialState) {
private var call: MxCall? = null private var call: WebRtcCall? = null
private var connectionTimeoutTimer: Timer? = null private var connectionTimeoutTimer: Timer? = null
private var hasBeenConnectedOnce = false 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) { override fun onStateUpdate(call: MxCall) {
val callState = call.state val callState = call.state
if (callState is CallState.Connected && callState.iceConnectionState == MxPeerConnectionState.CONNECTED) { 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?) { override fun onCurrentCallChange(call: MxCall?) {
// we need to check the state // we need to check the state
if (call == null) { 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() { override fun onAudioDevicesChange() {
val currentSoundDevice = webRtcPeerConnectionManager.callAudioManager.getCurrentSoundDevice() val currentSoundDevice = callManager.callAudioManager.getCurrentSoundDevice()
if (currentSoundDevice == CallAudioManager.SoundDevice.PHONE) { if (currentSoundDevice == CallAudioManager.SoundDevice.PHONE) {
proximityManager.start() proximityManager.start()
} else { } else {
@ -115,94 +125,77 @@ class VectorCallViewModel @AssistedInject constructor(
setState { setState {
copy( copy(
availableSoundDevices = webRtcPeerConnectionManager.callAudioManager.getAvailableSoundDevices(), availableSoundDevices = callManager.callAudioManager.getAvailableSoundDevices(),
soundDevice = currentSoundDevice 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 { setState {
copy( copy(
canSwitchCamera = webRtcPeerConnectionManager.canSwitchCamera(), isVideoCall = webRtcCall.mxCall.isVideoCall,
isFrontCamera = webRtcPeerConnectionManager.currentCameraType() == CameraType.FRONT 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() { override fun onCleared() {
// session.callService().removeCallListener(callServiceListener) callManager.removeCurrentCallListener(currentCallListener)
webRtcPeerConnectionManager.removeCurrentCallListener(currentCallListener) call?.removeListener(callListener)
this.call?.removeListener(callStateListener)
proximityManager.stop() proximityManager.stop()
super.onCleared() super.onCleared()
} }
override fun handle(action: VectorCallViewActions) = withState { state -> override fun handle(action: VectorCallViewActions) = withState { state ->
when (action) { when (action) {
VectorCallViewActions.EndCall -> webRtcPeerConnectionManager.endCall() VectorCallViewActions.EndCall -> call?.endCall()
VectorCallViewActions.AcceptCall -> { VectorCallViewActions.AcceptCall -> {
setState { setState {
copy(callState = Loading()) copy(callState = Loading())
} }
webRtcPeerConnectionManager.acceptIncomingCall() call?.acceptIncomingCall()
} }
VectorCallViewActions.DeclineCall -> { VectorCallViewActions.DeclineCall -> {
setState { setState {
copy(callState = Loading()) copy(callState = Loading())
} }
webRtcPeerConnectionManager.endCall() call?.endCall()
} }
VectorCallViewActions.ToggleMute -> { VectorCallViewActions.ToggleMute -> {
val muted = state.isAudioMuted val muted = state.isAudioMuted
webRtcPeerConnectionManager.muteCall(!muted) call?.muteCall(!muted)
setState { setState {
copy(isAudioMuted = !muted) copy(isAudioMuted = !muted)
} }
} }
VectorCallViewActions.ToggleVideo -> { VectorCallViewActions.ToggleVideo -> {
if (state.isVideoCall) { if (state.isVideoCall) {
val videoEnabled = state.isVideoEnabled val videoEnabled = state.isVideoEnabled
webRtcPeerConnectionManager.enableVideo(!videoEnabled) call?.enableVideo(!videoEnabled)
setState { setState {
copy(isVideoEnabled = !videoEnabled) copy(isVideoEnabled = !videoEnabled)
} }
@ -210,14 +203,14 @@ class VectorCallViewModel @AssistedInject constructor(
Unit Unit
} }
is VectorCallViewActions.ChangeAudioDevice -> { is VectorCallViewActions.ChangeAudioDevice -> {
webRtcPeerConnectionManager.callAudioManager.setCurrentSoundDevice(action.device) callManager.callAudioManager.setCurrentSoundDevice(action.device)
setState { setState {
copy( copy(
soundDevice = webRtcPeerConnectionManager.callAudioManager.getCurrentSoundDevice() soundDevice = callManager.callAudioManager.getCurrentSoundDevice()
) )
} }
} }
VectorCallViewActions.SwitchSoundDevice -> { VectorCallViewActions.SwitchSoundDevice -> {
_viewEvents.post( _viewEvents.post(
VectorCallViewEvents.ShowSoundDeviceChooser(state.availableSoundDevices, state.soundDevice) VectorCallViewEvents.ShowSoundDeviceChooser(state.availableSoundDevices, state.soundDevice)
) )
@ -225,45 +218,35 @@ class VectorCallViewModel @AssistedInject constructor(
VectorCallViewActions.HeadSetButtonPressed -> { VectorCallViewActions.HeadSetButtonPressed -> {
if (state.callState.invoke() is CallState.LocalRinging) { if (state.callState.invoke() is CallState.LocalRinging) {
// accept call // accept call
webRtcPeerConnectionManager.acceptIncomingCall() call?.acceptIncomingCall()
} }
if (state.callState.invoke() is CallState.Connected) { if (state.callState.invoke() is CallState.Connected) {
// end call? // end call?
webRtcPeerConnectionManager.endCall() call?.endCall()
} }
Unit Unit
} }
VectorCallViewActions.ToggleCamera -> { VectorCallViewActions.ToggleCamera -> {
webRtcPeerConnectionManager.switchCamera() call?.switchCamera()
} }
VectorCallViewActions.ToggleHDSD -> { VectorCallViewActions.ToggleHDSD -> {
if (!state.isVideoCall) return@withState 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 }.exhaustive
} }
@AssistedInject.Factory @AssistedInject.Factory
interface Factory { interface Factory {
fun create(initialState: VectorCallViewState, args: CallArgs): VectorCallViewModel fun create(initialState: VectorCallViewState): VectorCallViewModel
} }
companion object : MvRxViewModelFactory<VectorCallViewModel, VectorCallViewState> { companion object : MvRxViewModelFactory<VectorCallViewModel, VectorCallViewState> {
@JvmStatic @JvmStatic
override fun create(viewModelContext: ViewModelContext, state: VectorCallViewState): VectorCallViewModel? { override fun create(viewModelContext: ViewModelContext, state: VectorCallViewState): VectorCallViewModel {
val callActivity: VectorCallActivity = viewModelContext.activity() val callActivity: VectorCallActivity = viewModelContext.activity()
val callArgs: CallArgs = viewModelContext.args() return callActivity.viewModelFactory.create(state)
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
)
} }
} }
} }

View file

@ -23,8 +23,8 @@ import org.matrix.android.sdk.api.session.call.CallState
import org.matrix.android.sdk.api.util.MatrixItem import org.matrix.android.sdk.api.util.MatrixItem
data class VectorCallViewState( data class VectorCallViewState(
val callId: String? = null, val callId: String,
val roomId: String = "", val roomId: String,
val isVideoCall: Boolean, val isVideoCall: Boolean,
val isAudioMuted: Boolean = false, val isAudioMuted: Boolean = false,
val isVideoEnabled: Boolean = true, val isVideoEnabled: Boolean = true,
@ -36,4 +36,13 @@ data class VectorCallViewState(
val availableSoundDevices: List<CallAudioManager.SoundDevice> = emptyList(), val availableSoundDevices: List<CallAudioManager.SoundDevice> = emptyList(),
val otherUserMatrixItem: Async<MatrixItem> = Uninitialized, val otherUserMatrixItem: Async<MatrixItem> = Uninitialized,
val callState: Async<CallState> = Uninitialized val callState: Async<CallState> = Uninitialized
) : MvRxState ) : MvRxState {
constructor(callArgs: CallArgs): this(
callId = callArgs.callId,
roomId = callArgs.roomId,
isVideoCall = callArgs.isVideoCall
)
}

View file

@ -20,7 +20,7 @@ import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import im.vector.app.core.di.HasVectorInjector 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 import timber.log.Timber
class CallHeadsUpActionReceiver : BroadcastReceiver() { class CallHeadsUpActionReceiver : BroadcastReceiver() {
@ -48,9 +48,9 @@ class CallHeadsUpActionReceiver : BroadcastReceiver() {
// context.stopService(Intent(context, CallHeadsUpService::class.java)) // context.stopService(Intent(context, CallHeadsUpService::class.java))
} }
private fun onCallRejectClicked(peerConnectionManager: WebRtcPeerConnectionManager) { private fun onCallRejectClicked(callManager: WebRtcCallManager) {
Timber.d("onCallRejectClicked") Timber.d("onCallRejectClicked")
peerConnectionManager.endCall() callManager.endCall()
} }
// private fun onCallAnswerClicked(context: Context) { // private fun onCallAnswerClicked(context: Context) {

View file

@ -22,7 +22,7 @@ import android.telecom.Connection
import android.telecom.DisconnectCause import android.telecom.DisconnectCause
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import im.vector.app.features.call.VectorCallViewModel 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 timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@ -32,7 +32,7 @@ import javax.inject.Inject
val callId: String val callId: String
) : Connection() { ) : Connection() {
@Inject lateinit var peerConnectionManager: WebRtcPeerConnectionManager @Inject lateinit var callManager: WebRtcCallManager
@Inject lateinit var callViewModel: VectorCallViewModel @Inject lateinit var callViewModel: VectorCallViewModel
init { init {

View file

@ -17,7 +17,6 @@
package im.vector.app.features.call.webrtc package im.vector.app.features.call.webrtc
import im.vector.app.features.call.CallAudioManager 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.CallState
import org.matrix.android.sdk.api.session.call.MxPeerConnectionState import org.matrix.android.sdk.api.session.call.MxPeerConnectionState
import org.webrtc.DataChannel import org.webrtc.DataChannel
@ -84,7 +83,7 @@ class PeerConnectionObserver(private val webRtcCall: WebRtcCall,
override fun onIceCandidate(iceCandidate: IceCandidate) { override fun onIceCandidate(iceCandidate: IceCandidate) {
Timber.v("## VOIP StreamObserver onIceCandidate: $iceCandidate") Timber.v("## VOIP StreamObserver onIceCandidate: $iceCandidate")
webRtcCall.iceCandidateSource.onNext(iceCandidate) webRtcCall.onIceCandidate(iceCandidate)
} }
override fun onDataChannel(dc: DataChannel) { override fun onDataChannel(dc: DataChannel) {
@ -153,7 +152,6 @@ class PeerConnectionObserver(private val webRtcCall: WebRtcCall,
override fun onAddStream(stream: MediaStream) { override fun onAddStream(stream: MediaStream) {
Timber.v("## VOIP StreamObserver onAddStream: $stream") Timber.v("## VOIP StreamObserver onAddStream: $stream")
webRtcCall.onAddStream(stream) webRtcCall.onAddStream(stream)
} }
override fun onRemoveStream(stream: MediaStream) { override fun onRemoveStream(stream: MediaStream) {
@ -175,11 +173,7 @@ class PeerConnectionObserver(private val webRtcCall: WebRtcCall,
override fun onRenegotiationNeeded() { override fun onRenegotiationNeeded() {
Timber.v("## VOIP StreamObserver onRenegotiationNeeded") Timber.v("## VOIP StreamObserver onRenegotiationNeeded")
if (webRtcCall.mxCall.state != CallState.CreateOffer && webRtcCall.mxCall.opponentVersion == 0) { webRtcCall.onRenegationNeeded()
Timber.v("Opponent does not support renegotiation: ignoring onRenegotiationNeeded event")
return
}
webRtcCall.sendSpdOffer()
} }
/** /**

View file

@ -37,7 +37,6 @@ import io.reactivex.subjects.PublishSubject
import io.reactivex.subjects.ReplaySubject import io.reactivex.subjects.ReplaySubject
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext 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.call.TurnServerResponse
import org.matrix.android.sdk.api.session.room.model.call.CallAnswerContent 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.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.CallInviteContent
import org.matrix.android.sdk.api.session.room.model.call.CallNegotiateContent import org.matrix.android.sdk.api.session.room.model.call.CallNegotiateContent
import org.matrix.android.sdk.api.session.room.model.call.SdpType import org.matrix.android.sdk.api.session.room.model.call.SdpType
@ -72,9 +72,9 @@ import org.webrtc.VideoSource
import org.webrtc.VideoTrack import org.webrtc.VideoTrack
import timber.log.Timber import timber.log.Timber
import java.lang.ref.WeakReference import java.lang.ref.WeakReference
import java.util.concurrent.Executor
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import javax.inject.Provider import javax.inject.Provider
import kotlin.coroutines.CoroutineContext
private const val STREAM_ID = "ARDAMS" private const val STREAM_ID = "ARDAMS"
private const val AUDIO_TRACK_ID = "ARDAMSa0" private const val AUDIO_TRACK_ID = "ARDAMSa0"
@ -85,29 +85,44 @@ class WebRtcCall(val mxCall: MxCall,
private val callAudioManager: CallAudioManager, private val callAudioManager: CallAudioManager,
private val rootEglBase: EglBase?, private val rootEglBase: EglBase?,
private val context: Context, private val context: Context,
private val session: Session, private val dispatcher: CoroutineContext,
private val executor: Executor, private val sessionProvider: Provider<Session?>,
private val peerConnectionFactoryProvider: Provider<PeerConnectionFactory?>) { private val peerConnectionFactoryProvider: Provider<PeerConnectionFactory?>,
private val onCallEnded: (WebRtcCall) -> Unit): MxCall.StateListener {
private val dispatcher = executor.asCoroutineDispatcher() interface Listener: MxCall.StateListener {
fun onCaptureStateChanged() {}
fun onCameraChange() {}
}
var peerConnection: PeerConnection? = null private val listeners = ArrayList<Listener>()
var localAudioSource: AudioSource? = null
var localAudioTrack: AudioTrack? = null fun addListener(listener: Listener) {
var localVideoSource: VideoSource? = null listeners.add(listener)
var localVideoTrack: VideoTrack? = null }
var remoteVideoTrack: VideoTrack? = null
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 // Perfect negotiation state: https://www.w3.org/TR/webrtc/#perfect-negotiation-example
var makingOffer: Boolean = false private var makingOffer: Boolean = false
var ignoreOffer: Boolean = false private var ignoreOffer: Boolean = false
private var videoCapturer: CameraVideoCapturer? = null private var videoCapturer: CameraVideoCapturer? = null
private val availableCamera = ArrayList<CameraProxy>() private val availableCamera = ArrayList<CameraProxy>()
private var cameraInUse: CameraProxy? = null private var cameraInUse: CameraProxy? = null
private var currentCaptureFormat: CaptureFormat = CaptureFormat.HD private var currentCaptureFormat: CaptureFormat = CaptureFormat.HD
private var capturerIsInError = false
private var cameraAvailabilityCallback: CameraManager.AvailabilityCallback? = null private var cameraAvailabilityCallback: CameraManager.AvailabilityCallback? = null
// Mute status // Mute status
@ -117,10 +132,17 @@ class WebRtcCall(val mxCall: MxCall,
var offerSdp: CallInviteContent.Offer? = null var offerSdp: CallInviteContent.Offer? = null
var videoCapturerIsInError = false
set(value) {
field = value
listeners.forEach {
tryOrNull { it.onCaptureStateChanged() }
}
}
private var localSurfaceRenderers: MutableList<WeakReference<SurfaceViewRenderer>> = ArrayList() private var localSurfaceRenderers: MutableList<WeakReference<SurfaceViewRenderer>> = ArrayList()
private var remoteSurfaceRenderers: MutableList<WeakReference<SurfaceViewRenderer>> = ArrayList() private var remoteSurfaceRenderers: MutableList<WeakReference<SurfaceViewRenderer>> = ArrayList()
val iceCandidateSource: PublishSubject<IceCandidate> = PublishSubject.create() private val iceCandidateSource: PublishSubject<IceCandidate> = PublishSubject.create()
private val iceCandidateDisposable = iceCandidateSource private val iceCandidateDisposable = iceCandidateSource
.buffer(300, TimeUnit.MILLISECONDS) .buffer(300, TimeUnit.MILLISECONDS)
.subscribe { .subscribe {
@ -132,60 +154,51 @@ class WebRtcCall(val mxCall: MxCall,
} }
} }
var remoteCandidateSource: ReplaySubject<IceCandidate> = ReplaySubject.create() private val remoteCandidateSource: ReplaySubject<IceCandidate> = ReplaySubject.create()
var remoteIceCandidateDisposable: Disposable? = null private var remoteIceCandidateDisposable: Disposable? = null
private fun createLocalStream() { init {
val peerConnectionFactory = peerConnectionFactoryProvider.get() ?: return mxCall.addListener(this)
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) { fun onIceCandidate(iceCandidate: IceCandidate) = iceCandidateSource.onNext(iceCandidate)
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 sendSpdOffer() = GlobalScope.launch(dispatcher) { fun onRenegationNeeded() {
val constraints = MediaConstraints() GlobalScope.launch(dispatcher) {
// These are deprecated options 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("OfferToReceiveAudio", "true"))
// constraints.mandatory.add(MediaConstraints.KeyValuePair("OfferToReceiveVideo", if (currentCall?.mxCall?.isVideoCall == true) "true" else "false")) // constraints.mandatory.add(MediaConstraints.KeyValuePair("OfferToReceiveVideo", if (currentCall?.mxCall?.isVideoCall == true) "true" else "false"))
val peerConnection = peerConnection ?: return@launch val peerConnection = peerConnection ?: return@launch
Timber.v("## VOIP creating offer...") Timber.v("## VOIP creating offer...")
makingOffer = true makingOffer = true
try { try {
val sessionDescription = peerConnection.awaitCreateOffer(constraints) ?: return@launch val sessionDescription = peerConnection.awaitCreateOffer(constraints) ?: return@launch
peerConnection.awaitSetLocalDescription(sessionDescription) peerConnection.awaitSetLocalDescription(sessionDescription)
if (peerConnection.iceGatheringState() == PeerConnection.IceGatheringState.GATHERING) { if (peerConnection.iceGatheringState() == PeerConnection.IceGatheringState.GATHERING) {
// Allow a short time for initial candidates to be gathered // Allow a short time for initial candidates to be gathered
delay(200) 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 mxCall
.takeIf { it.state is CallState.Connected } .takeIf { it.state is CallState.Connected }
?.let { mxCall -> ?.let { mxCall ->
val name = session.getUser(mxCall.opponentUserId)?.getBestName() val session = sessionProvider.get()
val name = session?.getUser(mxCall.opponentUserId)?.getBestName()
?: mxCall.roomId ?: mxCall.roomId
// Start background service with notification // Start background service with notification
CallService.onPendingCall( CallService.onPendingCall(
@ -231,7 +245,7 @@ class WebRtcCall(val mxCall: MxCall,
isVideo = mxCall.isVideoCall, isVideo = mxCall.isVideoCall,
roomName = name, roomName = name,
roomId = mxCall.roomId, roomId = mxCall.roomId,
matrixId = session.myUserId, matrixId = session?.myUserId ?:"",
callId = mxCall.callId) callId = mxCall.callId)
} }
@ -255,9 +269,12 @@ class WebRtcCall(val mxCall: MxCall,
} }
} }
fun acceptIncomingCall() = GlobalScope.launch { fun acceptIncomingCall() {
if (mxCall.state == CallState.LocalRinging) { GlobalScope.launch {
internalAcceptIncomingCall() 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 } .takeIf { it.state is CallState.Connected }
?.let { mxCall -> ?.let { mxCall ->
// Start background service with notification // 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 ?: mxCall.opponentUserId
CallService.onOnGoingCallBackground( CallService.onOnGoingCallBackground(
context = context, context = context,
isVideo = mxCall.isVideoCall, isVideo = mxCall.isVideoCall,
roomName = name, roomName = name,
roomId = mxCall.roomId, roomId = mxCall.roomId,
matrixId = session.myUserId , matrixId = session?.myUserId ?: "",
callId = mxCall.callId callId = mxCall.callId
) )
} }
@ -325,14 +343,15 @@ class WebRtcCall(val mxCall: MxCall,
val turnServerResponse = getTurnServer() val turnServerResponse = getTurnServer()
// Update service state // Update service state
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
val name = session.getUser(mxCall.opponentUserId)?.getBestName() val session = sessionProvider.get()
val name = session?.getUser(mxCall.opponentUserId)?.getBestName()
?: mxCall.roomId ?: mxCall.roomId
CallService.onPendingCall( CallService.onPendingCall(
context = context, context = context,
isVideo = mxCall.isVideoCall, isVideo = mxCall.isVideoCall,
roomName = name, roomName = name,
roomId = mxCall.roomId, roomId = mxCall.roomId,
matrixId = session.myUserId, matrixId = session?.myUserId ?: "",
callId = mxCall.callId callId = mxCall.callId
) )
} }
@ -393,13 +412,33 @@ class WebRtcCall(val mxCall: MxCall,
private suspend fun getTurnServer(): TurnServerResponse? { private suspend fun getTurnServer(): TurnServerResponse? {
return tryOrNull { return tryOrNull {
awaitCallback { 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) { private fun configureVideoTrack(peerConnectionFactory: PeerConnectionFactory) {
availableCamera.clear()
val cameraIterator = if (Camera2Enumerator.isSupported(context)) { val cameraIterator = if (Camera2Enumerator.isSupported(context)) {
Camera2Enumerator(context) Camera2Enumerator(context)
} else { } else {
@ -426,14 +465,14 @@ class WebRtcCall(val mxCall: MxCall,
val videoCapturer = cameraIterator.createCapturer(camera.name, object : CameraEventsHandlerAdapter() { val videoCapturer = cameraIterator.createCapturer(camera.name, object : CameraEventsHandlerAdapter() {
override fun onFirstFrameAvailable() { override fun onFirstFrameAvailable() {
super.onFirstFrameAvailable() super.onFirstFrameAvailable()
capturerIsInError = false videoCapturerIsInError = false
} }
override fun onCameraClosed() { override fun onCameraClosed() {
super.onCameraClosed() super.onCameraClosed()
// This could happen if you open the camera app in chat // 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 // We then register in order to restart capture as soon as the camera is available again
capturerIsInError = true videoCapturerIsInError = true
val cameraManager = context.getSystemService<CameraManager>() val cameraManager = context.getSystemService<CameraManager>()
cameraAvailabilityCallback = object : CameraManager.AvailabilityCallback() { cameraAvailabilityCallback = object : CameraManager.AvailabilityCallback() {
override fun onCameraAvailable(cameraId: String) { override fun onCameraAvailable(cameraId: String) {
@ -466,12 +505,10 @@ class WebRtcCall(val mxCall: MxCall,
} }
fun setCaptureFormat(format: CaptureFormat) { fun setCaptureFormat(format: CaptureFormat) {
Timber.v("## VOIP setCaptureFormat $format") GlobalScope.launch(dispatcher) {
executor.execute { Timber.v("## VOIP setCaptureFormat $format")
// videoCapturer?.stopCapture()
videoCapturer?.changeCaptureFormat(format.width, format.height, format.fps) videoCapturer?.changeCaptureFormat(format.width, format.height, format.fps)
currentCaptureFormat = format currentCaptureFormat = format
//currentCallsListeners.forEach { tryOrNull { it.onCaptureStateChanged() } }
} }
} }
@ -543,6 +580,10 @@ class WebRtcCall(val mxCall: MxCall,
localSurfaceRenderers.forEach { localSurfaceRenderers.forEach {
it.get()?.setMirror(isFrontCamera) it.get()?.setMirror(isFrontCamera)
} }
listeners.forEach {
tryOrNull { it.onCameraChange() }
}
} }
override fun onCameraSwitchError(errorDescription: String?) { override fun onCameraSwitchError(errorDescription: String?) {
@ -577,7 +618,8 @@ class WebRtcCall(val mxCall: MxCall,
return currentCaptureFormat return currentCaptureFormat
} }
fun release() { private fun release() {
mxCall.removeListener(this)
videoCapturer?.stopCapture() videoCapturer?.stopCapture()
videoCapturer?.dispose() videoCapturer?.dispose()
videoCapturer = null videoCapturer = null
@ -591,21 +633,22 @@ class WebRtcCall(val mxCall: MxCall,
localAudioTrack = null localAudioTrack = null
localVideoSource = null localVideoSource = null
localVideoTrack = null localVideoTrack = null
cameraAvailabilityCallback = null
} }
fun onAddStream(stream: MediaStream) { fun onAddStream(stream: MediaStream) {
executor.execute { GlobalScope.launch(dispatcher) {
// reportError("Weird-looking stream: " + stream); // reportError("Weird-looking stream: " + stream);
if (stream.audioTracks.size > 1 || stream.videoTracks.size > 1) { if (stream.audioTracks.size > 1 || stream.videoTracks.size > 1) {
Timber.e("## VOIP StreamObserver weird looking stream: $stream") Timber.e("## VOIP StreamObserver weird looking stream: $stream")
// TODO maybe do something more?? // TODO maybe do something more??
mxCall.hangUp() mxCall.hangUp()
return@execute return@launch
} }
if (stream.videoTracks.size == 1) { if (stream.videoTracks.size == 1) {
val remoteVideoTrack = stream.videoTracks.first() val remoteVideoTrack = stream.videoTracks.first()
remoteVideoTrack.setEnabled(true) remoteVideoTrack.setEnabled(true)
this.remoteVideoTrack = remoteVideoTrack this@WebRtcCall.remoteVideoTrack = remoteVideoTrack
// sink to renderer if attached // sink to renderer if attached
remoteSurfaceRenderers.forEach { it.get()?.let { remoteVideoTrack.addSink(it) } } remoteSurfaceRenderers.forEach { it.get()?.let { remoteVideoTrack.addSink(it) } }
} }
@ -613,7 +656,7 @@ class WebRtcCall(val mxCall: MxCall,
} }
fun onRemoveStream() { fun onRemoveStream() {
executor.execute { GlobalScope.launch(dispatcher) {
remoteSurfaceRenderers remoteSurfaceRenderers
.mapNotNull { it.get() } .mapNotNull { it.get() }
.forEach { remoteVideoTrack?.removeSink(it) } .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 mxCall.state = CallState.Terminated
//Close tracks ASAP
localVideoTrack?.setEnabled(false) localVideoTrack?.setEnabled(false)
localVideoTrack?.setEnabled(false) localVideoTrack?.setEnabled(false)
cameraAvailabilityCallback?.let { cameraAvailabilityCallback -> cameraAvailabilityCallback?.let { cameraAvailabilityCallback ->
val cameraManager = context.getSystemService<CameraManager>()!! val cameraManager = context.getSystemService<CameraManager>()!!
cameraManager.unregisterAvailabilityCallback(cameraAvailabilityCallback) cameraManager.unregisterAvailabilityCallback(cameraAvailabilityCallback)
} }
release() release()
onCallEnded(this)
if (originatedByMe) { if (originatedByMe) {
// send hang up event // send hang up event
mxCall.hangUp() if (mxCall.state is CallState.Connected) {
mxCall.hangUp(reason)
} else {
mxCall.reject()
}
} }
} }
// Call listener // Call listener
fun onCallIceCandidateReceived(iceCandidatesContent: CallCandidatesContent) { fun onCallIceCandidateReceived(iceCandidatesContent: CallCandidatesContent) {
executor.execute { GlobalScope.launch(dispatcher) {
iceCandidatesContent.candidates.forEach { iceCandidatesContent.candidates.forEach {
Timber.v("## VOIP onCallIceCandidateReceived for call ${mxCall.callId} sdp: ${it.candidate}") Timber.v("## VOIP onCallIceCandidateReceived for call ${mxCall.callId} sdp: ${it.candidate}")
val iceCandidate = IceCandidate(it.sdpMid, it.sdpMLineIndex, it.candidate) val iceCandidate = IceCandidate(it.sdpMid, it.sdpMLineIndex, it.candidate)
@ -665,43 +713,49 @@ class WebRtcCall(val mxCall: MxCall,
} }
fun onCallNegotiateReceived(callNegotiateContent: CallNegotiateContent) { 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) { 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 { try {
val sdp = SessionDescription(type.asWebRTC(), sdpText) val sdp = SessionDescription(type.asWebRTC(), sdpText)
peerConnection.awaitSetRemoteDescription(sdp) peerConnection.awaitSetRemoteDescription(sdp)
if (type == SdpType.OFFER) { if (type == SdpType.OFFER) {
createAnswer()?.also { createAnswer()
mxCall.negotiate(sdpText) mxCall.negotiate(sdpText)
}
} }
} catch (failure: Throwable) { } catch (failure: Throwable) {
Timber.e(failure, "Failed to complete negotiation") Timber.e(failure, "Failed to complete negotiation")
} }
} }
} }
// MxCall.StateListener
override fun onStateUpdate(call: MxCall) {
listeners.forEach {
tryOrNull { it.onStateUpdate(call) }
}
}
} }
private fun MutableList<WeakReference<SurfaceViewRenderer>>.addIfNeeded(renderer: SurfaceViewRenderer?) { private fun MutableList<WeakReference<SurfaceViewRenderer>>.addIfNeeded(renderer: SurfaceViewRenderer?) {

View file

@ -29,8 +29,6 @@ import im.vector.app.features.call.CameraType
import im.vector.app.features.call.CaptureFormat 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.features.call.utils.awaitCreateAnswer
import im.vector.app.features.call.utils.awaitSetLocalDescription
import im.vector.app.push.fcm.FcmHelper import im.vector.app.push.fcm.FcmHelper
import kotlinx.coroutines.asCoroutineDispatcher import kotlinx.coroutines.asCoroutineDispatcher
import org.matrix.android.sdk.api.extensions.orFalse 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.CallListener
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.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.room.model.call.CallAnswerContent 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.CallCandidatesContent
import org.matrix.android.sdk.api.session.room.model.call.CallHangupContent 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.CallNegotiateContent
import org.matrix.android.sdk.api.session.room.model.call.CallRejectContent import org.matrix.android.sdk.api.session.room.model.call.CallRejectContent
import org.matrix.android.sdk.api.session.room.model.call.CallSelectAnswerContent import org.matrix.android.sdk.api.session.room.model.call.CallSelectAnswerContent
import org.matrix.android.sdk.internal.util.awaitCallback
import org.webrtc.DefaultVideoDecoderFactory import org.webrtc.DefaultVideoDecoderFactory
import org.webrtc.DefaultVideoEncoderFactory import org.webrtc.DefaultVideoEncoderFactory
import org.webrtc.MediaConstraints
import org.webrtc.PeerConnectionFactory import org.webrtc.PeerConnectionFactory
import org.webrtc.SessionDescription
import org.webrtc.SurfaceViewRenderer import org.webrtc.SurfaceViewRenderer
import timber.log.Timber import timber.log.Timber
import java.lang.ref.WeakReference
import java.util.concurrent.Executors import java.util.concurrent.Executors
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Provider
import javax.inject.Singleton import javax.inject.Singleton
/** /**
@ -66,7 +58,7 @@ import javax.inject.Singleton
* Use app context * Use app context
*/ */
@Singleton @Singleton
class WebRtcPeerConnectionManager @Inject constructor( class WebRtcCallManager @Inject constructor(
private val context: Context, private val context: Context,
private val activeSessionDataSource: ActiveSessionDataSource private val activeSessionDataSource: ActiveSessionDataSource
) : CallListener, LifecycleObserver { ) : CallListener, LifecycleObserver {
@ -81,6 +73,14 @@ class WebRtcPeerConnectionManager @Inject constructor(
fun onCameraChange() {} fun onCameraChange() {}
} }
var capturerIsInError = false
set(value) {
field = value
currentCallsListeners.forEach {
tryOrNull { it.onCaptureStateChanged() }
}
}
private val currentCallsListeners = emptyList<CurrentCallListener>().toMutableList() private val currentCallsListeners = emptyList<CurrentCallListener>().toMutableList()
fun addCurrentCallListener(listener: CurrentCallListener) { fun addCurrentCallListener(listener: CurrentCallListener) {
currentCallsListeners.add(listener) currentCallsListeners.add(listener)
@ -90,7 +90,7 @@ class WebRtcPeerConnectionManager @Inject constructor(
currentCallsListeners.remove(listener) currentCallsListeners.remove(listener)
} }
val callAudioManager = CallAudioManager(context.applicationContext) { val callAudioManager = CallAudioManager(context) {
currentCallsListeners.forEach { currentCallsListeners.forEach {
tryOrNull { it.onAudioDevicesChange() } tryOrNull { it.onAudioDevicesChange() }
} }
@ -104,34 +104,6 @@ class WebRtcPeerConnectionManager @Inject constructor(
private var isInBackground: Boolean = true private var isInBackground: Boolean = true
var capturerIsInError = false
set(value) {
field = value
currentCallsListeners.forEach {
tryOrNull { it.onCaptureStateChanged() }
}
}
var localSurfaceRenderers: MutableList<WeakReference<SurfaceViewRenderer>> = ArrayList()
var remoteSurfaceRenderers: MutableList<WeakReference<SurfaceViewRenderer>> = ArrayList()
private fun MutableList<WeakReference<SurfaceViewRenderer>>.addIfNeeded(renderer: SurfaceViewRenderer?) {
if (renderer == null) return
val exists = any {
it.get() == renderer
}
if (!exists) {
add(WeakReference(renderer))
}
}
private fun MutableList<WeakReference<SurfaceViewRenderer>>.removeIfNeeded(renderer: SurfaceViewRenderer?) {
if (renderer == null) return
removeAll {
it.get() == renderer
}
}
@OnLifecycleEvent(Lifecycle.Event.ON_RESUME) @OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
fun entersForeground() { fun entersForeground() {
isInBackground = false isInBackground = false
@ -150,6 +122,12 @@ class WebRtcPeerConnectionManager @Inject constructor(
} }
} }
private val callsByCallId = HashMap<String, WebRtcCall>()
fun getCallById(callId: String): WebRtcCall? {
return callsByCallId[callId]
}
fun headSetButtonTapped() { fun headSetButtonTapped() {
Timber.v("## VOIP headSetButtonTapped") Timber.v("## VOIP headSetButtonTapped")
val call = currentCall?.mxCall ?: return 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?) { fun attachViewRenderers(localViewRenderer: SurfaceViewRenderer?, remoteViewRenderer: SurfaceViewRenderer, mode: String?) {
currentCall?.attachViewRenderers(localViewRenderer, remoteViewRenderer, mode) currentCall?.attachViewRenderers(localViewRenderer, remoteViewRenderer, mode)
} }
private fun createPeerConnectionFactory() { private fun createPeerConnectionFactoryIfNeeded() {
if (peerConnectionFactory != null) return if (peerConnectionFactory != null) return
Timber.v("## VOIP createPeerConnectionFactory") Timber.v("## VOIP createPeerConnectionFactory")
val eglBaseContext = rootEglBase?.eglBaseContext ?: return Unit.also { val eglBaseContext = rootEglBase?.eglBaseContext ?: return Unit.also {
@ -202,12 +172,9 @@ class WebRtcPeerConnectionManager @Inject constructor(
.setVideoEncoderFactory(defaultVideoEncoderFactory) .setVideoEncoderFactory(defaultVideoEncoderFactory)
.setVideoDecoderFactory(defaultVideoDecoderFactory) .setVideoDecoderFactory(defaultVideoDecoderFactory)
.createPeerConnectionFactory() .createPeerConnectionFactory()
// attachViewRenderersInternal()
} }
fun acceptIncomingCall() { fun acceptIncomingCall() {
Timber.v("## VOIP acceptIncomingCall from state ${currentCall?.mxCall?.state}")
currentCall?.acceptIncomingCall() currentCall?.acceptIncomingCall()
} }
@ -215,11 +182,12 @@ class WebRtcPeerConnectionManager @Inject constructor(
currentCall?.detachRenderers(renderers) currentCall?.detachRenderers(renderers)
} }
fun close() { private fun onCallEnded(call: WebRtcCall) {
Timber.v("## VOIP WebRtcPeerConnectionManager close() >") Timber.v("## VOIP WebRtcPeerConnectionManager onCall ended: ${call.mxCall.callId}")
CallService.onNoActiveCall(context) CallService.onNoActiveCall(context)
callAudioManager.stop() callAudioManager.stop()
currentCall = null currentCall = null
callsByCallId.remove(call.mxCall.callId)
// This must be done in this thread // This must be done in this thread
executor.execute { executor.execute {
if (currentCall == null) { 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) { fun startOutgoingCall(signalingRoomId: String, otherUserId: String, isVideoCall: Boolean) {
Timber.v("## VOIP startOutgoingCall in room $signalingRoomId to $otherUserId isVideo $isVideoCall") Timber.v("## VOIP startOutgoingCall in room $signalingRoomId to $otherUserId isVideo $isVideoCall")
executor.execute { executor.execute {
if (peerConnectionFactory == null) { createPeerConnectionFactoryIfNeeded()
createPeerConnectionFactory()
}
} }
val createdCall = currentSession?.callSignalingService()?.createOutgoingCall(signalingRoomId, otherUserId, isVideoCall) ?: return val mxCall = currentSession?.callSignalingService()?.createOutgoingCall(signalingRoomId, otherUserId, isVideoCall) ?: return
val webRtcCall = WebRtcCall( createWebRtcCall(mxCall)
mxCall = createdCall, callAudioManager.startForCall(mxCall)
callAudioManager = callAudioManager,
rootEglBase = rootEglBase,
context = context,
executor = executor,
peerConnectionFactoryProvider = Provider {
createPeerConnectionFactory()
peerConnectionFactory
},
session = currentSession!!
)
callAudioManager.startForCall(createdCall) val name = currentSession?.getUser(mxCall.opponentUserId)?.getBestName()
currentCall = webRtcCall ?: mxCall.opponentUserId
val name = currentSession?.getUser(createdCall.opponentUserId)?.getBestName()
?: createdCall.opponentUserId
CallService.onOutgoingCallRinging( CallService.onOutgoingCallRinging(
context = context.applicationContext, context = context.applicationContext,
isVideo = createdCall.isVideoCall, isVideo = mxCall.isVideoCall,
roomName = name, roomName = name,
roomId = createdCall.roomId, roomId = mxCall.roomId,
matrixId = currentSession?.myUserId ?: "", matrixId = currentSession?.myUserId ?: "",
callId = createdCall.callId) callId = mxCall.callId)
// start the activity now // start the activity now
context.applicationContext.startActivity(VectorCallActivity.newIntent(context, createdCall)) context.startActivity(VectorCallActivity.newIntent(context, mxCall))
} }
override fun onCallIceCandidateReceived(mxCall: MxCall, iceCandidatesContent: CallCandidatesContent) { override fun onCallIceCandidateReceived(mxCall: MxCall, iceCandidatesContent: CallCandidatesContent) {
@ -311,19 +239,9 @@ class WebRtcPeerConnectionManager @Inject constructor(
// Just ignore, maybe we could answer from other session? // Just ignore, maybe we could answer from other session?
return return
} }
val webRtcCall = WebRtcCall( createWebRtcCall(mxCall).apply {
mxCall = mxCall, offerSdp = callInviteContent.offer
callAudioManager = callAudioManager, }
rootEglBase = rootEglBase,
context = context,
executor = executor,
peerConnectionFactoryProvider = {
createPeerConnectionFactory()
peerConnectionFactory
},
session = currentSession!!
)
currentCall = webRtcCall
callAudioManager.startForCall(mxCall) callAudioManager.startForCall(mxCall)
// Start background service with notification // Start background service with notification
val name = currentSession?.getUser(mxCall.opponentUserId)?.getBestName() val name = currentSession?.getUser(mxCall.opponentUserId)?.getBestName()
@ -336,8 +254,6 @@ class WebRtcPeerConnectionManager @Inject constructor(
matrixId = currentSession?.myUserId ?: "", matrixId = currentSession?.myUserId ?: "",
callId = mxCall.callId callId = mxCall.callId
) )
webRtcCall.offerSdp = callInviteContent.offer
// If this is received while in background, the app will not sync, // 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 // 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 // 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? { private fun createWebRtcCall(mxCall: MxCall): WebRtcCall {
Timber.w("## VOIP createAnswer") val webRtcCall = WebRtcCall(
val peerConnection = call.peerConnection ?: return null mxCall = mxCall,
val constraints = MediaConstraints().apply { callAudioManager = callAudioManager,
mandatory.add(MediaConstraints.KeyValuePair("OfferToReceiveAudio", "true")) rootEglBase = rootEglBase,
mandatory.add(MediaConstraints.KeyValuePair("OfferToReceiveVideo", if (call.mxCall.isVideoCall) "true" else "false")) context = context,
} dispatcher = dispatcher,
return try { peerConnectionFactoryProvider = {
val localDescription = peerConnection.awaitCreateAnswer(constraints) ?: return null createPeerConnectionFactoryIfNeeded()
peerConnection.awaitSetLocalDescription(localDescription) peerConnectionFactory
localDescription },
} catch (failure: Throwable) { sessionProvider = { currentSession },
Timber.v("Fail to create answer") onCallEnded = this::onCallEnded
null )
} currentCall = webRtcCall
callsByCallId[mxCall.callId] = webRtcCall
return webRtcCall
} }
fun muteCall(muted: Boolean) { fun muteCall(muted: Boolean) {
@ -397,11 +315,7 @@ class WebRtcPeerConnectionManager @Inject constructor(
} }
fun endCall(originatedByMe: Boolean = true) { fun endCall(originatedByMe: Boolean = true) {
// Update service state
CallService.onNoActiveCall(context)
// close tracks ASAP
currentCall?.endCall(originatedByMe) currentCall?.endCall(originatedByMe)
close()
} }
fun onWiredDeviceEvent(event: WiredHeadsetStateReceiver.HeadsetPlugEvent) { fun onWiredDeviceEvent(event: WiredHeadsetStateReceiver.HeadsetPlugEvent) {
@ -478,6 +392,7 @@ class WebRtcPeerConnectionManager @Inject constructor(
override fun onCallManagedByOtherSession(callId: String) { override fun onCallManagedByOtherSession(callId: String) {
Timber.v("## VOIP onCallManagedByOtherSession: $callId") Timber.v("## VOIP onCallManagedByOtherSession: $callId")
currentCall = null currentCall = null
callsByCallId.remove(callId)
CallService.onNoActiveCall(context) CallService.onNoActiveCall(context)
// did we start background sync? so we should stop it // did we start background sync? so we should stop it

View file

@ -35,7 +35,7 @@ import im.vector.app.core.ui.views.ActiveCallViewHolder
import im.vector.app.core.ui.views.KeysBackupBanner import im.vector.app.core.ui.views.KeysBackupBanner
import im.vector.app.features.call.SharedActiveCallViewModel import im.vector.app.features.call.SharedActiveCallViewModel
import im.vector.app.features.call.VectorCallActivity 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.RoomListFragment
import im.vector.app.features.home.room.list.RoomListParams import im.vector.app.features.home.room.list.RoomListParams
import im.vector.app.features.popup.PopupAlertManager import im.vector.app.features.popup.PopupAlertManager
@ -62,7 +62,7 @@ class HomeDetailFragment @Inject constructor(
private val serverBackupStatusViewModelFactory: ServerBackupStatusViewModel.Factory, private val serverBackupStatusViewModelFactory: ServerBackupStatusViewModel.Factory,
private val avatarRenderer: AvatarRenderer, private val avatarRenderer: AvatarRenderer,
private val alertManager: PopupAlertManager, private val alertManager: PopupAlertManager,
private val webRtcPeerConnectionManager: WebRtcPeerConnectionManager, private val callManager: WebRtcCallManager,
private val vectorPreferences: VectorPreferences private val vectorPreferences: VectorPreferences
) : VectorBaseFragment(), KeysBackupBanner.Delegate, ActiveCallView.Callback, ServerBackupStatusViewModel.Factory { ) : VectorBaseFragment(), KeysBackupBanner.Delegate, ActiveCallView.Callback, ServerBackupStatusViewModel.Factory {
@ -120,7 +120,7 @@ class HomeDetailFragment @Inject constructor(
sharedCallActionViewModel sharedCallActionViewModel
.activeCall .activeCall
.observe(viewLifecycleOwner, Observer { .observe(viewLifecycleOwner, Observer {
activeCallViewHolder.updateCall(it, webRtcPeerConnectionManager) activeCallViewHolder.updateCall(it, callManager)
invalidateOptionsMenu() invalidateOptionsMenu()
}) })
} }

View file

@ -116,7 +116,7 @@ import im.vector.app.features.attachments.preview.AttachmentsPreviewArgs
import im.vector.app.features.attachments.toGroupedContentAttachmentData import im.vector.app.features.attachments.toGroupedContentAttachmentData
import im.vector.app.features.call.SharedActiveCallViewModel import im.vector.app.features.call.SharedActiveCallViewModel
import im.vector.app.features.call.VectorCallActivity 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.call.conference.JitsiCallViewModel
import im.vector.app.features.command.Command import im.vector.app.features.command.Command
import im.vector.app.features.crypto.keysbackup.restore.KeysBackupRestoreActivity import im.vector.app.features.crypto.keysbackup.restore.KeysBackupRestoreActivity
@ -218,7 +218,7 @@ class RoomDetailFragment @Inject constructor(
private val vectorPreferences: VectorPreferences, private val vectorPreferences: VectorPreferences,
private val colorProvider: ColorProvider, private val colorProvider: ColorProvider,
private val notificationUtils: NotificationUtils, private val notificationUtils: NotificationUtils,
private val webRtcPeerConnectionManager: WebRtcPeerConnectionManager, private val callManager: WebRtcCallManager,
private val matrixItemColorProvider: MatrixItemColorProvider, private val matrixItemColorProvider: MatrixItemColorProvider,
private val imageContentRenderer: ImageContentRenderer, private val imageContentRenderer: ImageContentRenderer,
private val roomDetailPendingActionStore: RoomDetailPendingActionStore private val roomDetailPendingActionStore: RoomDetailPendingActionStore
@ -315,7 +315,7 @@ class RoomDetailFragment @Inject constructor(
sharedCallActionViewModel sharedCallActionViewModel
.activeCall .activeCall
.observe(viewLifecycleOwner, Observer { .observe(viewLifecycleOwner, Observer {
activeCallViewHolder.updateCall(it, webRtcPeerConnectionManager) activeCallViewHolder.updateCall(it, callManager)
invalidateOptionsMenu() invalidateOptionsMenu()
}) })
@ -514,7 +514,7 @@ class RoomDetailFragment @Inject constructor(
} }
override fun onDestroy() { override fun onDestroy() {
activeCallViewHolder.unBind(webRtcPeerConnectionManager) activeCallViewHolder.unBind(callManager)
roomDetailViewModel.handle(RoomDetailAction.ExitTrackingUnreadMessagesState) roomDetailViewModel.handle(RoomDetailAction.ExitTrackingUnreadMessagesState)
super.onDestroy() super.onDestroy()
} }

View file

@ -32,7 +32,7 @@ import im.vector.app.core.extensions.exhaustive
import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.platform.VectorViewModel
import im.vector.app.core.resources.StringProvider import im.vector.app.core.resources.StringProvider
import im.vector.app.core.utils.subscribeLogError 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.CommandParser
import im.vector.app.features.command.ParsedCommand import im.vector.app.features.command.ParsedCommand
import im.vector.app.features.crypto.verification.SupportedVerificationMethodsProvider import im.vector.app.features.crypto.verification.SupportedVerificationMethodsProvider
@ -114,7 +114,7 @@ class RoomDetailViewModel @AssistedInject constructor(
private val stickerPickerActionHandler: StickerPickerActionHandler, private val stickerPickerActionHandler: StickerPickerActionHandler,
private val roomSummaryHolder: RoomSummaryHolder, private val roomSummaryHolder: RoomSummaryHolder,
private val typingHelper: TypingHelper, private val typingHelper: TypingHelper,
private val webRtcPeerConnectionManager: WebRtcPeerConnectionManager, private val callManager: WebRtcCallManager,
timelineSettingsFactory: TimelineSettingsFactory timelineSettingsFactory: TimelineSettingsFactory
) : VectorViewModel<RoomDetailViewState, RoomDetailAction, RoomDetailViewEvents>(initialState), Timeline.Listener { ) : VectorViewModel<RoomDetailViewState, RoomDetailAction, RoomDetailViewEvents>(initialState), Timeline.Listener {
@ -306,12 +306,12 @@ class RoomDetailViewModel @AssistedInject constructor(
private fun handleStartCall(action: RoomDetailAction.StartCall) { private fun handleStartCall(action: RoomDetailAction.StartCall) {
room.roomSummary()?.otherMemberIds?.firstOrNull()?.let { room.roomSummary()?.otherMemberIds?.firstOrNull()?.let {
webRtcPeerConnectionManager.startOutgoingCall(room.roomId, it, action.isVideo) callManager.startOutgoingCall(room.roomId, it, action.isVideo)
} }
} }
private fun handleEndCall() { private fun handleEndCall() {
webRtcPeerConnectionManager.endCall() callManager.endCall()
} }
private fun handleSelectStickerAttachment() { private fun handleSelectStickerAttachment() {
@ -566,7 +566,7 @@ class RoomDetailViewModel @AssistedInject constructor(
R.id.open_matrix_apps -> true R.id.open_matrix_apps -> true
R.id.voice_call, R.id.voice_call,
R.id.video_call -> true // always show for discoverability 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 R.id.search -> true
else -> false else -> false
} }