VoIP: continue refactoring

This commit is contained in:
ganfra 2020-11-26 12:07:32 +01:00
parent 7620aa4264
commit 1a9b0265dc
14 changed files with 219 additions and 249 deletions

View File

@ -153,7 +153,7 @@ interface VectorComponent {
fun pinLocker(): PinLocker fun pinLocker(): PinLocker
fun webRtcPeerConnectionManager(): WebRtcCallManager fun webRtcCallManager(): WebRtcCallManager
@Component.Factory @Component.Factory
interface Factory { interface Factory {

View File

@ -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()
callManager = vectorComponent().webRtcPeerConnectionManager() callManager = vectorComponent().webRtcCallManager()
callRingPlayerIncoming = CallRingPlayerIncoming(applicationContext) callRingPlayerIncoming = CallRingPlayerIncoming(applicationContext)
callRingPlayerOutgoing = CallRingPlayerOutgoing(applicationContext) callRingPlayerOutgoing = CallRingPlayerOutgoing(applicationContext)
wiredHeadsetStateReceiver = WiredHeadsetStateReceiver.createAndRegister(this, this) wiredHeadsetStateReceiver = WiredHeadsetStateReceiver.createAndRegister(this, this)

View File

@ -23,7 +23,7 @@ import im.vector.app.core.utils.DebouncedClickListener
import im.vector.app.features.call.webrtc.WebRtcCallManager 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 im.vector.app.features.call.webrtc.WebRtcCall
import org.webrtc.RendererCommon import org.webrtc.RendererCommon
import org.webrtc.SurfaceViewRenderer import org.webrtc.SurfaceViewRenderer
@ -32,26 +32,28 @@ class ActiveCallViewHolder {
private var activeCallPiP: SurfaceViewRenderer? = null private var activeCallPiP: SurfaceViewRenderer? = null
private var activeCallView: ActiveCallView? = null private var activeCallView: ActiveCallView? = null
private var pipWrapper: CardView? = null private var pipWrapper: CardView? = null
private var activeCall: WebRtcCall? = null
private var activeCallPipInitialized = false private var activeCallPipInitialized = false
fun updateCall(activeCall: MxCall?, callManager: WebRtcCallManager) { fun updateCall(activeCall: WebRtcCall?) {
val hasActiveCall = activeCall?.state is CallState.Connected this.activeCall = activeCall
val hasActiveCall = activeCall?.mxCall?.state is CallState.Connected
if (hasActiveCall) { if (hasActiveCall) {
val isVideoCall = activeCall?.isVideoCall == true val isVideoCall = activeCall?.mxCall?.isVideoCall == true
if (isVideoCall) initIfNeeded() if (isVideoCall) initIfNeeded()
activeCallView?.isVisible = !isVideoCall activeCallView?.isVisible = !isVideoCall
pipWrapper?.isVisible = isVideoCall pipWrapper?.isVisible = isVideoCall
activeCallPiP?.isVisible = isVideoCall activeCallPiP?.isVisible = isVideoCall
activeCallPiP?.let { activeCallPiP?.let {
callManager.attachViewRenderers(null, it, null) activeCall?.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 {
callManager.detachRenderers(listOf(it)) activeCall?.detachRenderers(listOf(it))
} }
} }
} }
@ -82,9 +84,9 @@ class ActiveCallViewHolder {
) )
} }
fun unBind(callManager: WebRtcCallManager) { fun unBind() {
activeCallPiP?.let { activeCallPiP?.let {
callManager.detachRenderers(listOf(it)) activeCall?.detachRenderers(listOf(it))
} }
if (activeCallPipInitialized) { if (activeCallPipInitialized) {
activeCallPiP?.release() activeCallPiP?.release()

View File

@ -18,6 +18,7 @@ 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.WebRtcCall
import im.vector.app.features.call.webrtc.WebRtcCallManager 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
@ -26,27 +27,27 @@ class SharedActiveCallViewModel @Inject constructor(
private val callManager: WebRtcCallManager private val callManager: WebRtcCallManager
) : ViewModel() { ) : ViewModel() {
val activeCall: MutableLiveData<MxCall?> = MutableLiveData() val activeCall: MutableLiveData<WebRtcCall?> = MutableLiveData()
val callStateListener = object : MxCall.StateListener { val callStateListener = object : WebRtcCall.Listener {
override fun onStateUpdate(call: MxCall) { override fun onStateUpdate(call: MxCall) {
if (activeCall.value?.callId == call.callId) { if (activeCall.value?.callId == call.callId) {
activeCall.postValue(call) activeCall.postValue(callManager.getCallById(call.callId))
} }
} }
} }
private val listener = object : WebRtcCallManager.CurrentCallListener { private val listener = object : WebRtcCallManager.CurrentCallListener {
override fun onCurrentCallChange(call: MxCall?) { override fun onCurrentCallChange(call: WebRtcCall?) {
activeCall.value?.removeListener(callStateListener) activeCall.value?.mxCall?.removeListener(callStateListener)
activeCall.postValue(call) activeCall.postValue(call)
call?.addListener(callStateListener) call?.addListener(callStateListener)
} }
} }
init { init {
activeCall.postValue(callManager.currentCall?.mxCall) activeCall.postValue(callManager.currentCall)
callManager.addCurrentCallListener(listener) callManager.addCurrentCallListener(listener)
} }

View File

@ -45,6 +45,8 @@ import im.vector.app.core.utils.PERMISSIONS_FOR_AUDIO_IP_CALL
import im.vector.app.core.utils.PERMISSIONS_FOR_VIDEO_IP_CALL import im.vector.app.core.utils.PERMISSIONS_FOR_VIDEO_IP_CALL
import im.vector.app.core.utils.allGranted import im.vector.app.core.utils.allGranted
import im.vector.app.core.utils.checkPermissions import im.vector.app.core.utils.checkPermissions
import im.vector.app.features.call.utils.EglUtils
import im.vector.app.features.call.webrtc.WebRtcCallManager
import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.home.AvatarRenderer
import im.vector.app.features.home.room.detail.RoomDetailActivity import im.vector.app.features.home.room.detail.RoomDetailActivity
import im.vector.app.features.home.room.detail.RoomDetailArgs import im.vector.app.features.home.room.detail.RoomDetailArgs
@ -52,8 +54,6 @@ import io.reactivex.android.schedulers.AndroidSchedulers
import kotlinx.android.parcel.Parcelize 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.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
@ -211,7 +211,7 @@ class VectorCallActivity : VectorBaseActivity(), CallControlsView.InteractionLis
} }
override fun onDestroy() { override fun onDestroy() {
callManager.detachRenderers(listOf(pipRenderer, fullscreenRenderer)) callManager.getCallById(callArgs.callId)?.detachRenderers(listOf(pipRenderer, fullscreenRenderer))
if (surfaceRenderersAreInitialized) { if (surfaceRenderersAreInitialized) {
pipRenderer.release() pipRenderer.release()
fullscreenRenderer.release() fullscreenRenderer.release()
@ -234,7 +234,7 @@ class VectorCallActivity : VectorBaseActivity(), CallControlsView.InteractionLis
callConnectingProgress.isVisible = false callConnectingProgress.isVisible = false
when (callState) { when (callState) {
is CallState.Idle, is CallState.Idle,
is CallState.Dialing -> { is CallState.Dialing -> {
callVideoGroup.isInvisible = true callVideoGroup.isInvisible = true
callInfoGroup.isVisible = true callInfoGroup.isVisible = true
callStatusText.setText(R.string.call_ring) callStatusText.setText(R.string.call_ring)
@ -248,14 +248,14 @@ class VectorCallActivity : VectorBaseActivity(), CallControlsView.InteractionLis
configureCallInfo(state) configureCallInfo(state)
} }
is CallState.Answering -> { is CallState.Answering -> {
callVideoGroup.isInvisible = true callVideoGroup.isInvisible = true
callInfoGroup.isVisible = true callInfoGroup.isVisible = true
callStatusText.setText(R.string.call_connecting) callStatusText.setText(R.string.call_connecting)
callConnectingProgress.isVisible = true callConnectingProgress.isVisible = true
configureCallInfo(state) configureCallInfo(state)
} }
is CallState.Connected -> { is CallState.Connected -> {
if (callState.iceConnectionState == MxPeerConnectionState.CONNECTED) { if (callState.iceConnectionState == MxPeerConnectionState.CONNECTED) {
if (callArgs.isVideoCall) { if (callArgs.isVideoCall) {
callVideoGroup.isVisible = true callVideoGroup.isVisible = true
@ -276,12 +276,12 @@ class VectorCallActivity : VectorBaseActivity(), CallControlsView.InteractionLis
callConnectingProgress.isVisible = true callConnectingProgress.isVisible = true
} }
// ensure all attached? // ensure all attached?
callManager.attachViewRenderers(pipRenderer, fullscreenRenderer, null) callManager.getCallById(callArgs.callId)?.attachViewRenderers(pipRenderer, fullscreenRenderer, null)
} }
is CallState.Terminated -> { is CallState.Terminated -> {
finish() finish()
} }
null -> { null -> {
} }
} }
} }
@ -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 */)
callManager.attachViewRenderers(pipRenderer, fullscreenRenderer, callManager.getCallById(callArgs.callId)?.attachViewRenderers(pipRenderer, fullscreenRenderer,
intent.getStringExtra(EXTRA_MODE)?.takeIf { isFirstCreation() }) intent.getStringExtra(EXTRA_MODE)?.takeIf { isFirstCreation() })
pipRenderer.setOnClickListener { pipRenderer.setOnClickListener {
@ -338,14 +338,14 @@ class VectorCallActivity : VectorBaseActivity(), CallControlsView.InteractionLis
private fun handleViewEvents(event: VectorCallViewEvents?) { private fun handleViewEvents(event: VectorCallViewEvents?) {
Timber.v("## VOIP handleViewEvents $event") Timber.v("## VOIP handleViewEvents $event")
when (event) { when (event) {
VectorCallViewEvents.DismissNoCall -> { VectorCallViewEvents.DismissNoCall -> {
CallService.onNoActiveCall(this) CallService.onNoActiveCall(this)
finish() finish()
} }
is VectorCallViewEvents.ConnectionTimeout -> { is VectorCallViewEvents.ConnectionTimeout -> {
onErrorTimoutConnect(event.turn) onErrorTimoutConnect(event.turn)
} }
null -> { null -> {
} }
} }
} }

View File

@ -107,7 +107,7 @@ class VectorCallViewModel @AssistedInject constructor(
} }
private val currentCallListener = object : WebRtcCallManager.CurrentCallListener { private val currentCallListener = object : WebRtcCallManager.CurrentCallListener {
override fun onCurrentCallChange(call: MxCall?) { override fun onCurrentCallChange(call: WebRtcCall?) {
// we need to check the state // we need to check the state
if (call == null) { if (call == null) {
// we should dismiss, e.g handled by other session? // we should dismiss, e.g handled by other session?
@ -155,9 +155,9 @@ class VectorCallViewModel @AssistedInject constructor(
otherUserMatrixItem = item?.let { Success(it) } ?: Uninitialized, otherUserMatrixItem = item?.let { Success(it) } ?: Uninitialized,
soundDevice = currentSoundDevice, soundDevice = currentSoundDevice,
availableSoundDevices = callManager.callAudioManager.getAvailableSoundDevices(), availableSoundDevices = callManager.callAudioManager.getAvailableSoundDevices(),
isFrontCamera = callManager.currentCameraType() == CameraType.FRONT, isFrontCamera = call?.currentCameraType() == CameraType.FRONT,
canSwitchCamera = callManager.canSwitchCamera(), canSwitchCamera = call?.canSwitchCamera() ?: false,
isHD = webRtcCall.mxCall.isVideoCall && callManager.currentCaptureFormat() is CaptureFormat.HD isHD = webRtcCall.mxCall.isVideoCall && webRtcCall.currentCaptureFormat() is CaptureFormat.HD
) )
} }
} }

View File

@ -27,17 +27,22 @@ class CallHeadsUpActionReceiver : BroadcastReceiver() {
companion object { companion object {
const val EXTRA_CALL_ACTION_KEY = "EXTRA_CALL_ACTION_KEY" const val EXTRA_CALL_ACTION_KEY = "EXTRA_CALL_ACTION_KEY"
const val EXTRA_CALL_ID = "EXTRA_CALL_ID"
const val CALL_ACTION_REJECT = 0 const val CALL_ACTION_REJECT = 0
} }
override fun onReceive(context: Context, intent: Intent?) { override fun onReceive(context: Context, intent: Intent?) {
val peerConnectionManager = (context.applicationContext as? HasVectorInjector) val webRtcCallManager = (context.applicationContext as? HasVectorInjector)
?.injector() ?.injector()
?.webRtcPeerConnectionManager() ?.webRtcCallManager()
?: return ?: return
when (intent?.getIntExtra(EXTRA_CALL_ACTION_KEY, 0)) { when (intent?.getIntExtra(EXTRA_CALL_ACTION_KEY, 0)) {
CALL_ACTION_REJECT -> onCallRejectClicked(peerConnectionManager) CALL_ACTION_REJECT -> {
val callId = intent.getStringExtra(EXTRA_CALL_ID) ?: return
onCallRejectClicked(webRtcCallManager, callId)
}
} }
// Not sure why this should be needed // Not sure why this should be needed
@ -48,9 +53,9 @@ class CallHeadsUpActionReceiver : BroadcastReceiver() {
// context.stopService(Intent(context, CallHeadsUpService::class.java)) // context.stopService(Intent(context, CallHeadsUpService::class.java))
} }
private fun onCallRejectClicked(callManager: WebRtcCallManager) { private fun onCallRejectClicked(callManager: WebRtcCallManager, callId: String) {
Timber.d("onCallRejectClicked") Timber.d("onCallRejectClicked")
callManager.endCall() callManager.getCallById(callId)?.endCall()
} }
// private fun onCallAnswerClicked(context: Context) { // private fun onCallAnswerClicked(context: Context) {

View File

@ -19,6 +19,7 @@ package im.vector.app.features.call.webrtc
import im.vector.app.features.call.CallAudioManager import im.vector.app.features.call.CallAudioManager
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.matrix.android.sdk.api.session.room.model.call.CallHangupContent
import org.webrtc.DataChannel import org.webrtc.DataChannel
import org.webrtc.IceCandidate import org.webrtc.IceCandidate
import org.webrtc.MediaStream import org.webrtc.MediaStream
@ -132,9 +133,7 @@ class PeerConnectionObserver(private val webRtcCall: WebRtcCall,
* It is, however, possible that the ICE agent did find compatible connections for some components. * It is, however, possible that the ICE agent did find compatible connections for some components.
*/ */
PeerConnection.IceConnectionState.FAILED -> { PeerConnection.IceConnectionState.FAILED -> {
// I should not hangup here.. webRtcCall.endCall(true, CallHangupContent.Reason.ICE_FAILED)
// because new candidates could arrive
// webRtcCall.mxCall.hangUp()
} }
/** /**
* The ICE agent has finished gathering candidates, has checked all pairs against one another, and has found a connection for all components. * The ICE agent has finished gathering candidates, has checked all pairs against one another, and has found a connection for all components.

View File

@ -42,6 +42,7 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.failure.Failure
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
import org.matrix.android.sdk.api.session.call.MxCall import org.matrix.android.sdk.api.session.call.MxCall
@ -88,9 +89,9 @@ class WebRtcCall(val mxCall: MxCall,
private val dispatcher: CoroutineContext, private val dispatcher: CoroutineContext,
private val sessionProvider: Provider<Session?>, private val sessionProvider: Provider<Session?>,
private val peerConnectionFactoryProvider: Provider<PeerConnectionFactory?>, private val peerConnectionFactoryProvider: Provider<PeerConnectionFactory?>,
private val onCallEnded: (WebRtcCall) -> Unit): MxCall.StateListener { private val onCallEnded: (WebRtcCall) -> Unit) : MxCall.StateListener {
interface Listener: MxCall.StateListener { interface Listener : MxCall.StateListener {
fun onCaptureStateChanged() {} fun onCaptureStateChanged() {}
fun onCameraChange() {} fun onCameraChange() {}
} }
@ -245,7 +246,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)
} }
@ -461,8 +462,13 @@ class WebRtcCall(val mxCall: MxCall,
?: backCamera?.also { cameraInUse = backCamera } ?: backCamera?.also { cameraInUse = backCamera }
?: null.also { cameraInUse = null } ?: null.also { cameraInUse = null }
listeners.forEach {
tryOrNull { it.onCameraChange() }
}
if (camera != null) { if (camera != null) {
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()
videoCapturerIsInError = false videoCapturerIsInError = false
@ -470,12 +476,25 @@ class WebRtcCall(val mxCall: MxCall,
override fun onCameraClosed() { override fun onCameraClosed() {
super.onCameraClosed() super.onCameraClosed()
Timber.v("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
videoCapturerIsInError = true videoCapturerIsInError = true
val cameraManager = context.getSystemService<CameraManager>() val cameraManager = context.getSystemService<CameraManager>()
cameraAvailabilityCallback = object : CameraManager.AvailabilityCallback() { cameraAvailabilityCallback = object : CameraManager.AvailabilityCallback() {
override fun onCameraUnavailable(cameraId: String) {
super.onCameraUnavailable(cameraId)
Timber.v("On camera unavailable: $cameraId")
}
override fun onCameraAccessPrioritiesChanged() {
super.onCameraAccessPrioritiesChanged()
Timber.v("onCameraAccessPrioritiesChanged")
}
override fun onCameraAvailable(cameraId: String) { override fun onCameraAvailable(cameraId: String) {
Timber.v("On camera available: $cameraId")
if (cameraId == camera.name) { if (cameraId == camera.name) {
videoCapturer?.startCapture(currentCaptureFormat.width, currentCaptureFormat.height, currentCaptureFormat.fps) videoCapturer?.startCapture(currentCaptureFormat.width, currentCaptureFormat.height, currentCaptureFormat.fps)
cameraManager?.unregisterAvailabilityCallback(this) cameraManager?.unregisterAvailabilityCallback(this)
@ -505,11 +524,9 @@ class WebRtcCall(val mxCall: MxCall,
} }
fun setCaptureFormat(format: CaptureFormat) { fun setCaptureFormat(format: CaptureFormat) {
GlobalScope.launch(dispatcher) { Timber.v("## VOIP setCaptureFormat $format")
Timber.v("## VOIP setCaptureFormat $format") videoCapturer?.changeCaptureFormat(format.width, format.height, format.fps)
videoCapturer?.changeCaptureFormat(format.width, format.height, format.fps) currentCaptureFormat = format
currentCaptureFormat = format
}
} }
private fun updateMuteStatus() { private fun updateMuteStatus() {
@ -565,31 +582,41 @@ class WebRtcCall(val mxCall: MxCall,
} }
fun canSwitchCamera(): Boolean { fun canSwitchCamera(): Boolean {
return availableCamera.size > 0 return availableCamera.size > 1
}
private fun getOppositeCameraIfAny(): CameraProxy? {
val currentCamera = cameraInUse ?: return null
return if (currentCamera.type == CameraType.FRONT) {
availableCamera.firstOrNull { it.type == CameraType.BACK }
} else {
availableCamera.firstOrNull { it.type == CameraType.FRONT }
}
} }
fun switchCamera() { fun switchCamera() {
Timber.v("## VOIP switchCamera") Timber.v("## VOIP switchCamera")
if (!canSwitchCamera()) return
if (mxCall.state is CallState.Connected && mxCall.isVideoCall) { if (mxCall.state is CallState.Connected && mxCall.isVideoCall) {
videoCapturer?.switchCamera(object : CameraVideoCapturer.CameraSwitchHandler { val oppositeCamera = getOppositeCameraIfAny() ?: return
// Invoked on success. |isFrontCamera| is true if the new camera is front facing. videoCapturer?.switchCamera(
override fun onCameraSwitchDone(isFrontCamera: Boolean) { object : CameraVideoCapturer.CameraSwitchHandler {
Timber.v("## VOIP onCameraSwitchDone isFront $isFrontCamera") // Invoked on success. |isFrontCamera| is true if the new camera is front facing.
cameraInUse = availableCamera.first { if (isFrontCamera) it.type == CameraType.FRONT else it.type == CameraType.BACK } override fun onCameraSwitchDone(isFrontCamera: Boolean) {
localSurfaceRenderers.forEach { Timber.v("## VOIP onCameraSwitchDone isFront $isFrontCamera")
it.get()?.setMirror(isFrontCamera) cameraInUse = oppositeCamera
} localSurfaceRenderers.forEach {
listeners.forEach { it.get()?.setMirror(isFrontCamera)
tryOrNull { it.onCameraChange() } }
} listeners.forEach {
tryOrNull { it.onCameraChange() }
}
}
} override fun onCameraSwitchError(errorDescription: String?) {
Timber.v("## VOIP onCameraSwitchError isFront $errorDescription")
override fun onCameraSwitchError(errorDescription: String?) { }
Timber.v("## VOIP onCameraSwitchError isFront $errorDescription") }, oppositeCamera.name
} )
})
} }
} }
@ -665,6 +692,9 @@ class WebRtcCall(val mxCall: MxCall,
} }
fun endCall(originatedByMe: Boolean = true, reason: CallHangupContent.Reason? = null) { fun endCall(originatedByMe: Boolean = true, reason: CallHangupContent.Reason? = null) {
if(mxCall.state == CallState.Terminated){
return
}
mxCall.state = CallState.Terminated mxCall.state = CallState.Terminated
//Close tracks ASAP //Close tracks ASAP
localVideoTrack?.setEnabled(false) localVideoTrack?.setEnabled(false)
@ -704,6 +734,7 @@ class WebRtcCall(val mxCall: MxCall,
try { try {
peerConnection?.awaitSetRemoteDescription(sdp) peerConnection?.awaitSetRemoteDescription(sdp)
} catch (failure: Throwable) { } catch (failure: Throwable) {
endCall(true, CallHangupContent.Reason.UNKWOWN_ERROR)
return@launch return@launch
} }
if (mxCall.opponentPartyId?.hasValue().orFalse()) { if (mxCall.opponentPartyId?.hasValue().orFalse()) {

View File

@ -31,7 +31,6 @@ import im.vector.app.features.call.VectorCallActivity
import im.vector.app.features.call.utils.EglUtils import im.vector.app.features.call.utils.EglUtils
import im.vector.app.push.fcm.FcmHelper import im.vector.app.push.fcm.FcmHelper
import kotlinx.coroutines.asCoroutineDispatcher import kotlinx.coroutines.asCoroutineDispatcher
import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.call.CallListener import org.matrix.android.sdk.api.session.call.CallListener
@ -67,7 +66,7 @@ class WebRtcCallManager @Inject constructor(
get() = activeSessionDataSource.currentValue?.orNull() get() = activeSessionDataSource.currentValue?.orNull()
interface CurrentCallListener { interface CurrentCallListener {
fun onCurrentCallChange(call: MxCall?) fun onCurrentCallChange(call: WebRtcCall?)
fun onCaptureStateChanged() {} fun onCaptureStateChanged() {}
fun onAudioDevicesChange() {} fun onAudioDevicesChange() {}
fun onCameraChange() {} fun onCameraChange() {}
@ -118,31 +117,32 @@ class WebRtcCallManager @Inject constructor(
set(value) { set(value) {
field = value field = value
currentCallsListeners.forEach { currentCallsListeners.forEach {
tryOrNull { it.onCurrentCallChange(value?.mxCall) } tryOrNull { it.onCurrentCallChange(value) }
} }
} }
private val callsByCallId = HashMap<String, WebRtcCall>() private val callsByCallId = HashMap<String, WebRtcCall>()
private val callsByRoomId = HashMap<String, ArrayList<WebRtcCall>>()
fun getCallById(callId: String): WebRtcCall? { fun getCallById(callId: String): WebRtcCall? {
return callsByCallId[callId] return callsByCallId[callId]
} }
fun headSetButtonTapped() { fun getCallsByRoomId(roomId: String): List<WebRtcCall> {
Timber.v("## VOIP headSetButtonTapped") return callsByRoomId[roomId] ?: emptyList()
val call = currentCall?.mxCall ?: return
if (call.state is CallState.LocalRinging) {
// accept call
acceptIncomingCall()
}
if (call.state is CallState.Connected) {
// end call?
endCall()
}
} }
fun attachViewRenderers(localViewRenderer: SurfaceViewRenderer?, remoteViewRenderer: SurfaceViewRenderer, mode: String?) { fun headSetButtonTapped() {
currentCall?.attachViewRenderers(localViewRenderer, remoteViewRenderer, mode) Timber.v("## VOIP headSetButtonTapped")
val call = currentCall ?: return
if (call.mxCall.state is CallState.LocalRinging) {
// accept call
call.acceptIncomingCall()
}
if (call.mxCall.state is CallState.Connected) {
// end call?
call.endCall()
}
} }
private fun createPeerConnectionFactoryIfNeeded() { private fun createPeerConnectionFactoryIfNeeded() {
@ -174,20 +174,13 @@ class WebRtcCallManager @Inject constructor(
.createPeerConnectionFactory() .createPeerConnectionFactory()
} }
fun acceptIncomingCall() {
currentCall?.acceptIncomingCall()
}
fun detachRenderers(renderers: List<SurfaceViewRenderer>?) {
currentCall?.detachRenderers(renderers)
}
private fun onCallEnded(call: WebRtcCall) { private fun onCallEnded(call: WebRtcCall) {
Timber.v("## VOIP WebRtcPeerConnectionManager onCall ended: ${call.mxCall.callId}") 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) callsByCallId.remove(call.mxCall.callId)
callsByRoomId[call.mxCall.roomId]?.remove(call)
// 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,9 +224,48 @@ class WebRtcCallManager @Inject constructor(
currentCall?.onCallIceCandidateReceived(iceCandidatesContent) currentCall?.onCallIceCandidateReceived(iceCandidatesContent)
} }
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
callsByRoomId.getOrPut(mxCall.roomId, { ArrayList() }).add(webRtcCall)
return webRtcCall
}
fun acceptIncomingCall() {
currentCall?.acceptIncomingCall()
}
fun endCall(originatedByMe: Boolean = true) {
currentCall?.endCall(originatedByMe)
}
fun onWiredDeviceEvent(event: WiredHeadsetStateReceiver.HeadsetPlugEvent) {
Timber.v("## VOIP onWiredDeviceEvent $event")
currentCall ?: return
// sometimes we received un-wanted unplugged...
callAudioManager.wiredStateChange(event)
}
fun onWirelessDeviceEvent(event: BluetoothHeadsetReceiver.BTHeadsetPlugEvent) {
Timber.v("## VOIP onWirelessDeviceEvent $event")
callAudioManager.bluetoothStateChange(event.plugged)
}
override fun onCallInviteReceived(mxCall: MxCall, callInviteContent: CallInviteContent) { override fun onCallInviteReceived(mxCall: MxCall, callInviteContent: CallInviteContent) {
Timber.v("## VOIP onCallInviteReceived callId ${mxCall.callId}") Timber.v("## VOIP onCallInviteReceived callId ${mxCall.callId}")
// to simplify we only treat one call at a time, and ignore others
if (currentCall != null) { if (currentCall != null) {
Timber.w("## VOIP receiving incoming call while already in call?") Timber.w("## VOIP receiving incoming call while already in call?")
// Just ignore, maybe we could answer from other session? // Just ignore, maybe we could answer from other session?
@ -267,74 +299,11 @@ class WebRtcCallManager @Inject constructor(
} }
} }
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) {
currentCall?.muteCall(muted)
}
fun enableVideo(enabled: Boolean) {
currentCall?.enableVideo(enabled)
}
fun switchCamera() {
currentCall?.switchCamera()
}
fun canSwitchCamera(): Boolean {
return currentCall?.canSwitchCamera() ?: false
}
fun currentCameraType(): CameraType? {
return currentCall?.currentCameraType()
}
fun setCaptureFormat(format: CaptureFormat) {
currentCall?.setCaptureFormat(format)
}
fun currentCaptureFormat(): CaptureFormat {
return currentCall?.currentCaptureFormat() ?: CaptureFormat.HD
}
fun endCall(originatedByMe: Boolean = true) {
currentCall?.endCall(originatedByMe)
}
fun onWiredDeviceEvent(event: WiredHeadsetStateReceiver.HeadsetPlugEvent) {
Timber.v("## VOIP onWiredDeviceEvent $event")
currentCall ?: return
// sometimes we received un-wanted unplugged...
callAudioManager.wiredStateChange(event)
}
fun onWirelessDeviceEvent(event: BluetoothHeadsetReceiver.BTHeadsetPlugEvent) {
Timber.v("## VOIP onWirelessDeviceEvent $event")
callAudioManager.bluetoothStateChange(event.plugged)
}
override fun onCallAnswerReceived(callAnswerContent: CallAnswerContent) { override fun onCallAnswerReceived(callAnswerContent: CallAnswerContent) {
val call = currentCall ?: return val call = callsByCallId[callAnswerContent.callId]
if (call.mxCall.callId != callAnswerContent.callId) return Unit.also { ?: return Unit.also {
Timber.w("onCallAnswerReceived for non active call? ${callAnswerContent.callId}") Timber.w("onCallAnswerReceived for non active call? ${callAnswerContent.callId}")
} }
val mxCall = call.mxCall val mxCall = call.mxCall
// Update service state // Update service state
val name = currentSession?.getUser(mxCall.opponentUserId)?.getBestName() val name = currentSession?.getUser(mxCall.opponentUserId)?.getBestName()
@ -351,48 +320,49 @@ class WebRtcCallManager @Inject constructor(
} }
override fun onCallHangupReceived(callHangupContent: CallHangupContent) { override fun onCallHangupReceived(callHangupContent: CallHangupContent) {
val call = currentCall ?: return val call = callsByCallId[callHangupContent.callId]
// Remote echos are filtered, so it's only remote hangups that i will get here ?: return Unit.also {
if (call.mxCall.callId != callHangupContent.callId) return Unit.also { Timber.w("onCallHangupReceived for non active call? ${callHangupContent.callId}")
Timber.w("onCallHangupReceived for non active call? ${callHangupContent.callId}") }
} call.endCall(false)
endCall(false)
} }
override fun onCallRejectReceived(callRejectContent: CallRejectContent) { override fun onCallRejectReceived(callRejectContent: CallRejectContent) {
val call = currentCall ?: return val call = callsByCallId[callRejectContent.callId]
// Remote echos are filtered, so it's only remote hangups that i will get here ?: return Unit.also {
if (call.mxCall.callId != callRejectContent.callId) return Unit.also { Timber.w("onCallRejectReceived for non active call? ${callRejectContent.callId}")
Timber.w("onCallRejected for non active call? ${callRejectContent.callId}") }
} call.endCall(false)
endCall(false)
} }
override fun onCallSelectAnswerReceived(callSelectAnswerContent: CallSelectAnswerContent) { override fun onCallSelectAnswerReceived(callSelectAnswerContent: CallSelectAnswerContent) {
val call = currentCall ?: return val call = callsByCallId[callSelectAnswerContent.callId]
if (call.mxCall.callId != callSelectAnswerContent.callId) return Unit.also { ?: return Unit.also {
Timber.w("onCallSelectAnswerReceived for non active call? ${callSelectAnswerContent.callId}") Timber.w("onCallSelectAnswerReceived for non active call? ${callSelectAnswerContent.callId}")
} }
val selectedPartyId = callSelectAnswerContent.selectedPartyId val selectedPartyId = callSelectAnswerContent.selectedPartyId
if (selectedPartyId != call.mxCall.ourPartyId) { if (selectedPartyId != call.mxCall.ourPartyId) {
Timber.i("Got select_answer for party ID ${selectedPartyId}: we are party ID ${call.mxCall.ourPartyId}."); Timber.i("Got select_answer for party ID ${selectedPartyId}: we are party ID ${call.mxCall.ourPartyId}.");
// The other party has picked somebody else's answer // The other party has picked somebody else's answer
endCall(false) call.endCall(false)
} }
} }
override fun onCallNegotiateReceived(callNegotiateContent: CallNegotiateContent) { override fun onCallNegotiateReceived(callNegotiateContent: CallNegotiateContent) {
val call = currentCall ?: return val call = callsByCallId[callNegotiateContent.callId]
if (call.mxCall.callId != callNegotiateContent.callId) return Unit.also { ?: return Unit.also {
Timber.w("onCallNegotiateReceived for non active call? ${callNegotiateContent.callId}") Timber.w("onCallNegotiateReceived for non active call? ${callNegotiateContent.callId}")
} }
call.onCallNegotiateReceived(callNegotiateContent) call.onCallNegotiateReceived(callNegotiateContent)
} }
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) val webRtcCall = callsByCallId.remove(callId)
if (webRtcCall != null) {
callsByRoomId[webRtcCall.mxCall.roomId]?.remove(webRtcCall)
}
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
@ -405,24 +375,4 @@ class WebRtcCallManager @Inject constructor(
} }
} }
} }
/**
* Indicates whether we are 'on hold' to the remote party (ie. if true,
* they cannot hear us). Note that this will return true when we put the
* remote on hold too due to the way hold is implemented (since we don't
* wish to play hold music when we put a call on hold, we use 'inactive'
* rather than 'sendonly')
* @returns true if the other party has put us on hold
*/
fun isLocalOnHold(): Boolean {
return currentCall?.isLocalOnHold().orFalse()
}
fun isRemoteOnHold(): Boolean {
return currentCall?.remoteOnHold.orFalse()
}
fun setRemoteOnHold(onHold: Boolean) {
currentCall?.updateRemoteOnHold(onHold)
}
} }

View File

@ -120,7 +120,7 @@ class HomeDetailFragment @Inject constructor(
sharedCallActionViewModel sharedCallActionViewModel
.activeCall .activeCall
.observe(viewLifecycleOwner, Observer { .observe(viewLifecycleOwner, Observer {
activeCallViewHolder.updateCall(it, callManager) activeCallViewHolder.updateCall(it)
invalidateOptionsMenu() invalidateOptionsMenu()
}) })
} }
@ -331,10 +331,10 @@ class HomeDetailFragment @Inject constructor(
VectorCallActivity.newIntent( VectorCallActivity.newIntent(
context = requireContext(), context = requireContext(),
callId = call.callId, callId = call.callId,
roomId = call.roomId, roomId = call.mxCall.roomId,
otherUserId = call.opponentUserId, otherUserId = call.mxCall.opponentUserId,
isIncomingCall = !call.isOutgoing, isIncomingCall = !call.mxCall.isOutgoing,
isVideoCall = call.isVideoCall, isVideoCall = call.mxCall.isVideoCall,
mode = null mode = null
).let { ).let {
startActivity(it) startActivity(it)

View File

@ -315,7 +315,7 @@ class RoomDetailFragment @Inject constructor(
sharedCallActionViewModel sharedCallActionViewModel
.activeCall .activeCall
.observe(viewLifecycleOwner, Observer { .observe(viewLifecycleOwner, Observer {
activeCallViewHolder.updateCall(it, callManager) activeCallViewHolder.updateCall(it)
invalidateOptionsMenu() invalidateOptionsMenu()
}) })
@ -514,7 +514,7 @@ class RoomDetailFragment @Inject constructor(
} }
override fun onDestroy() { override fun onDestroy() {
activeCallViewHolder.unBind(callManager) activeCallViewHolder.unBind()
roomDetailViewModel.handle(RoomDetailAction.ExitTrackingUnreadMessagesState) roomDetailViewModel.handle(RoomDetailAction.ExitTrackingUnreadMessagesState)
super.onDestroy() super.onDestroy()
} }
@ -712,7 +712,7 @@ class RoomDetailFragment @Inject constructor(
val activeCall = sharedCallActionViewModel.activeCall.value val activeCall = sharedCallActionViewModel.activeCall.value
if (activeCall != null) { if (activeCall != null) {
// resume existing if same room, if not prompt to kill and then restart new call? // resume existing if same room, if not prompt to kill and then restart new call?
if (activeCall.roomId == roomDetailArgs.roomId) { if (activeCall.mxCall.roomId == roomDetailArgs.roomId) {
onTapToReturnToCall() onTapToReturnToCall()
} }
// else { // else {
@ -1961,10 +1961,10 @@ class RoomDetailFragment @Inject constructor(
VectorCallActivity.newIntent( VectorCallActivity.newIntent(
context = requireContext(), context = requireContext(),
callId = call.callId, callId = call.callId,
roomId = call.roomId, roomId = call.mxCall.roomId,
otherUserId = call.opponentUserId, otherUserId = call.mxCall.opponentUserId,
isIncomingCall = !call.isOutgoing, isIncomingCall = !call.mxCall.isOutgoing,
isVideoCall = call.isVideoCall, isVideoCall = call.mxCall.isVideoCall,
mode = null mode = null
).let { ).let {
startActivity(it) startActivity(it)

View File

@ -565,8 +565,8 @@ class RoomDetailViewModel @AssistedInject constructor(
R.id.clear_all -> state.asyncRoomSummary()?.hasFailedSending == true R.id.clear_all -> state.asyncRoomSummary()?.hasFailedSending == true
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 -> callManager.getCallsByRoomId(state.roomId).isEmpty()
R.id.hangup_call -> callManager.currentCall != null R.id.hangup_call -> callManager.getCallsByRoomId(state.roomId).isNotEmpty()
R.id.search -> true R.id.search -> true
else -> false else -> false
} }

View File

@ -296,7 +296,6 @@ class NotificationUtils @Inject constructor(private val context: Context,
builder.priority = NotificationCompat.PRIORITY_HIGH builder.priority = NotificationCompat.PRIORITY_HIGH
// //
val requestId = Random.nextInt(1000)
// val pendingIntent = stackBuilder.getPendingIntent(requestId, PendingIntent.FLAG_UPDATE_CURRENT) // val pendingIntent = stackBuilder.getPendingIntent(requestId, PendingIntent.FLAG_UPDATE_CURRENT)
val contentIntent = VectorCallActivity.newIntent( val contentIntent = VectorCallActivity.newIntent(
@ -326,16 +325,7 @@ class NotificationUtils @Inject constructor(private val context: Context,
) )
.getPendingIntent(System.currentTimeMillis().toInt(), PendingIntent.FLAG_UPDATE_CURRENT) .getPendingIntent(System.currentTimeMillis().toInt(), PendingIntent.FLAG_UPDATE_CURRENT)
val rejectCallActionReceiver = Intent(context, CallHeadsUpActionReceiver::class.java).apply { val rejectCallPendingIntent = buildRejectCallPendingIntent(callId)
putExtra(CallHeadsUpActionReceiver.EXTRA_CALL_ACTION_KEY, CallHeadsUpActionReceiver.CALL_ACTION_REJECT)
}
// val answerCallPendingIntent = PendingIntent.getBroadcast(context, requestId, answerCallActionReceiver, PendingIntent.FLAG_UPDATE_CURRENT)
val rejectCallPendingIntent = PendingIntent.getBroadcast(
context,
requestId + 1,
rejectCallActionReceiver,
PendingIntent.FLAG_UPDATE_CURRENT
)
builder.addAction( builder.addAction(
NotificationCompat.Action( NotificationCompat.Action(
@ -375,8 +365,6 @@ class NotificationUtils @Inject constructor(private val context: Context,
.setLights(accentColor, 500, 500) .setLights(accentColor, 500, 500)
.setOngoing(true) .setOngoing(true)
val requestId = Random.nextInt(1000)
val contentIntent = VectorCallActivity.newIntent( val contentIntent = VectorCallActivity.newIntent(
context = context, context = context,
callId = callId, callId = callId,
@ -390,16 +378,7 @@ class NotificationUtils @Inject constructor(private val context: Context,
} }
val contentPendingIntent = PendingIntent.getActivity(context, System.currentTimeMillis().toInt(), contentIntent, 0) val contentPendingIntent = PendingIntent.getActivity(context, System.currentTimeMillis().toInt(), contentIntent, 0)
val rejectCallActionReceiver = Intent(context, CallHeadsUpActionReceiver::class.java).apply { val rejectCallPendingIntent = buildRejectCallPendingIntent(callId)
putExtra(CallHeadsUpActionReceiver.EXTRA_CALL_ACTION_KEY, CallHeadsUpActionReceiver.CALL_ACTION_REJECT)
}
val rejectCallPendingIntent = PendingIntent.getBroadcast(
context,
requestId + 1,
rejectCallActionReceiver,
PendingIntent.FLAG_UPDATE_CURRENT
)
builder.addAction( builder.addAction(
NotificationCompat.Action( NotificationCompat.Action(
@ -446,17 +425,7 @@ class NotificationUtils @Inject constructor(private val context: Context,
builder.setOngoing(true) builder.setOngoing(true)
} }
val rejectCallActionReceiver = Intent(context, CallHeadsUpActionReceiver::class.java).apply { val rejectCallPendingIntent = buildRejectCallPendingIntent(callId)
data = Uri.parse("mxcall://end?$callId")
putExtra(CallHeadsUpActionReceiver.EXTRA_CALL_ACTION_KEY, CallHeadsUpActionReceiver.CALL_ACTION_REJECT)
}
val rejectCallPendingIntent = PendingIntent.getBroadcast(
context,
System.currentTimeMillis().toInt(),
rejectCallActionReceiver,
PendingIntent.FLAG_UPDATE_CURRENT
)
builder.addAction( builder.addAction(
NotificationCompat.Action( NotificationCompat.Action(
@ -476,6 +445,19 @@ class NotificationUtils @Inject constructor(private val context: Context,
return builder.build() return builder.build()
} }
private fun buildRejectCallPendingIntent(callId: String): PendingIntent {
val rejectCallActionReceiver = Intent(context, CallHeadsUpActionReceiver::class.java).apply {
putExtra(CallHeadsUpActionReceiver.EXTRA_CALL_ID, callId)
putExtra(CallHeadsUpActionReceiver.EXTRA_CALL_ACTION_KEY, CallHeadsUpActionReceiver.CALL_ACTION_REJECT)
}
return PendingIntent.getBroadcast(
context,
System.currentTimeMillis().toInt(),
rejectCallActionReceiver,
PendingIntent.FLAG_UPDATE_CURRENT
)
}
/** /**
* Build a temporary (because service will be stopped just after) notification for the CallService, when a call is ended * Build a temporary (because service will be stopped just after) notification for the CallService, when a call is ended
*/ */