mirror of
https://github.com/vector-im/element-android.git
synced 2024-11-16 02:05:06 +08:00
VoIP: continue refactoring by introducing WebRtcCall
This commit is contained in:
parent
be3bfe7e5e
commit
d7f7aa09fc
@ -42,7 +42,7 @@ import im.vector.app.core.di.HasVectorInjector
|
||||
import im.vector.app.core.di.VectorComponent
|
||||
import im.vector.app.core.extensions.configureAndStart
|
||||
import im.vector.app.core.rx.RxConfig
|
||||
import im.vector.app.features.call.WebRtcPeerConnectionManager
|
||||
import im.vector.app.features.call.webrtc.WebRtcPeerConnectionManager
|
||||
import im.vector.app.features.configuration.VectorConfiguration
|
||||
import im.vector.app.features.disclaimer.doNotShowDisclaimerDialog
|
||||
import im.vector.app.features.lifecycle.VectorActivityLifecycleCallbacks
|
||||
|
@ -18,7 +18,7 @@ package im.vector.app.core.di
|
||||
|
||||
import arrow.core.Option
|
||||
import im.vector.app.ActiveSessionDataSource
|
||||
import im.vector.app.features.call.WebRtcPeerConnectionManager
|
||||
import im.vector.app.features.call.webrtc.WebRtcPeerConnectionManager
|
||||
import im.vector.app.features.crypto.keysrequest.KeyRequestHandler
|
||||
import im.vector.app.features.crypto.verification.IncomingVerificationRequestHandler
|
||||
import im.vector.app.features.notifications.PushRuleTriggerListener
|
||||
|
@ -29,7 +29,7 @@ import im.vector.app.core.error.ErrorFormatter
|
||||
import im.vector.app.core.pushers.PushersManager
|
||||
import im.vector.app.core.utils.AssetReader
|
||||
import im.vector.app.core.utils.DimensionConverter
|
||||
import im.vector.app.features.call.WebRtcPeerConnectionManager
|
||||
import im.vector.app.features.call.webrtc.WebRtcPeerConnectionManager
|
||||
import im.vector.app.features.configuration.VectorConfiguration
|
||||
import im.vector.app.features.crypto.keysrequest.KeyRequestHandler
|
||||
import im.vector.app.features.crypto.verification.IncomingVerificationRequestHandler
|
||||
|
@ -25,7 +25,7 @@ import android.view.KeyEvent
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.media.session.MediaButtonReceiver
|
||||
import im.vector.app.core.extensions.vectorComponent
|
||||
import im.vector.app.features.call.WebRtcPeerConnectionManager
|
||||
import im.vector.app.features.call.webrtc.WebRtcPeerConnectionManager
|
||||
import im.vector.app.features.call.telecom.CallConnection
|
||||
import im.vector.app.features.notifications.NotificationUtils
|
||||
import timber.log.Timber
|
||||
|
@ -20,7 +20,7 @@ import android.view.View
|
||||
import androidx.cardview.widget.CardView
|
||||
import androidx.core.view.isVisible
|
||||
import im.vector.app.core.utils.DebouncedClickListener
|
||||
import im.vector.app.features.call.WebRtcPeerConnectionManager
|
||||
import im.vector.app.features.call.webrtc.WebRtcPeerConnectionManager
|
||||
import org.matrix.android.sdk.api.session.call.CallState
|
||||
import im.vector.app.features.call.utils.EglUtils
|
||||
import org.matrix.android.sdk.api.session.call.MxCall
|
||||
|
@ -18,6 +18,7 @@ package im.vector.app.features.call
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import im.vector.app.features.call.webrtc.WebRtcPeerConnectionManager
|
||||
import org.matrix.android.sdk.api.session.call.MxCall
|
||||
import javax.inject.Inject
|
||||
|
||||
|
@ -53,11 +53,11 @@ import kotlinx.android.parcel.Parcelize
|
||||
import kotlinx.android.synthetic.main.activity_call.*
|
||||
import org.matrix.android.sdk.api.session.call.CallState
|
||||
import im.vector.app.features.call.utils.EglUtils
|
||||
import im.vector.app.features.call.webrtc.WebRtcPeerConnectionManager
|
||||
import org.matrix.android.sdk.api.session.call.MxCallDetail
|
||||
import org.matrix.android.sdk.api.session.call.MxPeerConnectionState
|
||||
import org.matrix.android.sdk.api.session.call.TurnServerResponse
|
||||
import org.webrtc.EglBase
|
||||
import org.webrtc.PeerConnection
|
||||
import org.webrtc.RendererCommon
|
||||
import org.webrtc.SurfaceViewRenderer
|
||||
import timber.log.Timber
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -20,7 +20,7 @@ import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import im.vector.app.core.di.HasVectorInjector
|
||||
import im.vector.app.features.call.WebRtcPeerConnectionManager
|
||||
import im.vector.app.features.call.webrtc.WebRtcPeerConnectionManager
|
||||
import timber.log.Timber
|
||||
|
||||
class CallHeadsUpActionReceiver : BroadcastReceiver() {
|
||||
|
@ -22,7 +22,7 @@ import android.telecom.Connection
|
||||
import android.telecom.DisconnectCause
|
||||
import androidx.annotation.RequiresApi
|
||||
import im.vector.app.features.call.VectorCallViewModel
|
||||
import im.vector.app.features.call.WebRtcPeerConnectionManager
|
||||
import im.vector.app.features.call.webrtc.WebRtcPeerConnectionManager
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
|
@ -16,8 +16,7 @@
|
||||
|
||||
package im.vector.app.features.call.utils
|
||||
|
||||
import im.vector.app.features.call.SdpObserverAdapter
|
||||
import org.matrix.android.sdk.api.session.call.MxPeerConnectionState
|
||||
import im.vector.app.features.call.webrtc.SdpObserverAdapter
|
||||
import org.webrtc.MediaConstraints
|
||||
import org.webrtc.PeerConnection
|
||||
import org.webrtc.SessionDescription
|
||||
|
@ -0,0 +1,194 @@
|
||||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.app.features.call.webrtc
|
||||
|
||||
import im.vector.app.features.call.CallAudioManager
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import org.matrix.android.sdk.api.session.call.CallState
|
||||
import org.matrix.android.sdk.api.session.call.MxPeerConnectionState
|
||||
import org.webrtc.DataChannel
|
||||
import org.webrtc.IceCandidate
|
||||
import org.webrtc.MediaStream
|
||||
import org.webrtc.PeerConnection
|
||||
import org.webrtc.RtpReceiver
|
||||
import timber.log.Timber
|
||||
|
||||
class PeerConnectionObserver(private val webRtcCall: WebRtcCall,
|
||||
private val callAudioManager: CallAudioManager) : PeerConnection.Observer {
|
||||
|
||||
override fun onConnectionChange(newState: PeerConnection.PeerConnectionState?) {
|
||||
Timber.v("## VOIP StreamObserver onConnectionChange: $newState")
|
||||
when (newState) {
|
||||
/**
|
||||
* Every ICE transport used by the connection is either in use (state "connected" or "completed")
|
||||
* or is closed (state "closed"); in addition, at least one transport is either "connected" or "completed"
|
||||
*/
|
||||
PeerConnection.PeerConnectionState.CONNECTED -> {
|
||||
webRtcCall.mxCall.state = CallState.Connected(MxPeerConnectionState.CONNECTED)
|
||||
callAudioManager.onCallConnected(webRtcCall.mxCall)
|
||||
}
|
||||
/**
|
||||
* One or more of the ICE transports on the connection is in the "failed" state.
|
||||
*/
|
||||
PeerConnection.PeerConnectionState.FAILED -> {
|
||||
// This can be temporary, e.g when other ice not yet received...
|
||||
// webRtcCall.mxCall.state = CallState.ERROR
|
||||
webRtcCall.mxCall.state = CallState.Connected(MxPeerConnectionState.FAILED)
|
||||
}
|
||||
/**
|
||||
* At least one of the connection's ICE transports (RTCIceTransports or RTCDtlsTransports) are in the "new" state,
|
||||
* and none of them are in one of the following states: "connecting", "checking", "failed", or "disconnected",
|
||||
* or all of the connection's transports are in the "closed" state.
|
||||
*/
|
||||
PeerConnection.PeerConnectionState.NEW,
|
||||
/**
|
||||
* One or more of the ICE transports are currently in the process of establishing a connection;
|
||||
* that is, their RTCIceConnectionState is either "checking" or "connected", and no transports are in the "failed" state
|
||||
*/
|
||||
PeerConnection.PeerConnectionState.CONNECTING -> {
|
||||
webRtcCall.mxCall.state = CallState.Connected(MxPeerConnectionState.CONNECTING)
|
||||
}
|
||||
/**
|
||||
* The RTCPeerConnection is closed.
|
||||
* This value was in the RTCSignalingState enum (and therefore found by reading the value of the signalingState)
|
||||
* property until the May 13, 2016 draft of the specification.
|
||||
*/
|
||||
PeerConnection.PeerConnectionState.CLOSED -> {
|
||||
webRtcCall.mxCall.state = CallState.Connected(MxPeerConnectionState.CLOSED)
|
||||
}
|
||||
/**
|
||||
* At least one of the ICE transports for the connection is in the "disconnected" state and none of
|
||||
* the other transports are in the state "failed", "connecting", or "checking".
|
||||
*/
|
||||
PeerConnection.PeerConnectionState.DISCONNECTED -> {
|
||||
webRtcCall.mxCall.state = CallState.Connected(MxPeerConnectionState.DISCONNECTED)
|
||||
}
|
||||
null -> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onIceCandidate(iceCandidate: IceCandidate) {
|
||||
Timber.v("## VOIP StreamObserver onIceCandidate: $iceCandidate")
|
||||
webRtcCall.iceCandidateSource.onNext(iceCandidate)
|
||||
}
|
||||
|
||||
override fun onDataChannel(dc: DataChannel) {
|
||||
Timber.v("## VOIP StreamObserver onDataChannel: ${dc.state()}")
|
||||
}
|
||||
|
||||
override fun onIceConnectionReceivingChange(receiving: Boolean) {
|
||||
Timber.v("## VOIP StreamObserver onIceConnectionReceivingChange: $receiving")
|
||||
}
|
||||
|
||||
override fun onIceConnectionChange(newState: PeerConnection.IceConnectionState) {
|
||||
Timber.v("## VOIP StreamObserver onIceConnectionChange IceConnectionState:$newState")
|
||||
when (newState) {
|
||||
/**
|
||||
* the ICE agent is gathering addresses or is waiting to be given remote candidates through
|
||||
* calls to RTCPeerConnection.addIceCandidate() (or both).
|
||||
*/
|
||||
PeerConnection.IceConnectionState.NEW -> {
|
||||
}
|
||||
/**
|
||||
* The ICE agent has been given one or more remote candidates and is checking pairs of local and remote candidates
|
||||
* against one another to try to find a compatible match, but has not yet found a pair which will allow
|
||||
* the peer connection to be made. It's possible that gathering of candidates is also still underway.
|
||||
*/
|
||||
PeerConnection.IceConnectionState.CHECKING -> {
|
||||
}
|
||||
|
||||
/**
|
||||
* A usable pairing of local and remote candidates has been found for all components of the connection,
|
||||
* and the connection has been established.
|
||||
* It's possible that gathering is still underway, and it's also possible that the ICE agent is still checking
|
||||
* candidates against one another looking for a better connection to use.
|
||||
*/
|
||||
PeerConnection.IceConnectionState.CONNECTED -> {
|
||||
}
|
||||
/**
|
||||
* Checks to ensure that components are still connected failed for at least one component of the RTCPeerConnection.
|
||||
* This is a less stringent test than "failed" and may trigger intermittently and resolve just as spontaneously on less reliable networks,
|
||||
* or during temporary disconnections. When the problem resolves, the connection may return to the "connected" state.
|
||||
*/
|
||||
PeerConnection.IceConnectionState.DISCONNECTED -> {
|
||||
}
|
||||
/**
|
||||
* The ICE candidate has checked all candidates pairs against one another and has failed to find
|
||||
* compatible matches for all components of the connection.
|
||||
* It is, however, possible that the ICE agent did find compatible connections for some components.
|
||||
*/
|
||||
PeerConnection.IceConnectionState.FAILED -> {
|
||||
// I should not hangup here..
|
||||
// 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.
|
||||
*/
|
||||
PeerConnection.IceConnectionState.COMPLETED -> {
|
||||
}
|
||||
/**
|
||||
* The ICE agent for this RTCPeerConnection has shut down and is no longer handling requests.
|
||||
*/
|
||||
PeerConnection.IceConnectionState.CLOSED -> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAddStream(stream: MediaStream) {
|
||||
Timber.v("## VOIP StreamObserver onAddStream: $stream")
|
||||
webRtcCall.onAddStream(stream)
|
||||
|
||||
}
|
||||
|
||||
override fun onRemoveStream(stream: MediaStream) {
|
||||
Timber.v("## VOIP StreamObserver onRemoveStream")
|
||||
webRtcCall.onRemoveStream()
|
||||
}
|
||||
|
||||
override fun onIceGatheringChange(newState: PeerConnection.IceGatheringState) {
|
||||
Timber.v("## VOIP StreamObserver onIceGatheringChange: $newState")
|
||||
}
|
||||
|
||||
override fun onSignalingChange(newState: PeerConnection.SignalingState) {
|
||||
Timber.v("## VOIP StreamObserver onSignalingChange: $newState")
|
||||
}
|
||||
|
||||
override fun onIceCandidatesRemoved(candidates: Array<out IceCandidate>) {
|
||||
Timber.v("## VOIP StreamObserver onIceCandidatesRemoved: ${candidates.contentToString()}")
|
||||
}
|
||||
|
||||
override fun onRenegotiationNeeded() {
|
||||
Timber.v("## VOIP StreamObserver onRenegotiationNeeded")
|
||||
if (webRtcCall.mxCall.state != CallState.CreateOffer && webRtcCall.mxCall.opponentVersion == 0) {
|
||||
Timber.v("Opponent does not support renegotiation: ignoring onRenegotiationNeeded event")
|
||||
return
|
||||
}
|
||||
webRtcCall.sendSpdOffer()
|
||||
}
|
||||
|
||||
/**
|
||||
* This happens when a new track of any kind is added to the media stream.
|
||||
* This event is fired when the browser adds a track to the stream
|
||||
* (such as when a RTCPeerConnection is renegotiated or a stream being captured using HTMLMediaElement.captureStream()
|
||||
* gets a new set of tracks because the media element being captured loaded a new source.
|
||||
*/
|
||||
override fun onAddTrack(p0: RtpReceiver?, p1: Array<out MediaStream>?) {
|
||||
Timber.v("## VOIP StreamObserver onAddTrack")
|
||||
}
|
||||
}
|
@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.app.features.call
|
||||
package im.vector.app.features.call.webrtc
|
||||
|
||||
import org.webrtc.DataChannel
|
||||
import org.webrtc.IceCandidate
|
@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.app.features.call
|
||||
package im.vector.app.features.call.webrtc
|
||||
|
||||
import org.webrtc.SdpObserver
|
||||
import org.webrtc.SessionDescription
|
@ -0,0 +1,722 @@
|
||||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.app.features.call.webrtc
|
||||
|
||||
import android.content.Context
|
||||
import android.hardware.camera2.CameraManager
|
||||
import androidx.core.content.getSystemService
|
||||
import im.vector.app.core.services.CallService
|
||||
import im.vector.app.features.call.CallAudioManager
|
||||
import im.vector.app.features.call.CameraEventsHandlerAdapter
|
||||
import im.vector.app.features.call.CameraProxy
|
||||
import im.vector.app.features.call.CameraType
|
||||
import im.vector.app.features.call.CaptureFormat
|
||||
import im.vector.app.features.call.VectorCallActivity
|
||||
import im.vector.app.features.call.utils.asWebRTC
|
||||
import im.vector.app.features.call.utils.awaitCreateAnswer
|
||||
import im.vector.app.features.call.utils.awaitCreateOffer
|
||||
import im.vector.app.features.call.utils.awaitSetLocalDescription
|
||||
import im.vector.app.features.call.utils.awaitSetRemoteDescription
|
||||
import im.vector.app.features.call.utils.mapToCallCandidate
|
||||
import io.reactivex.disposables.Disposable
|
||||
import io.reactivex.subjects.PublishSubject
|
||||
import io.reactivex.subjects.ReplaySubject
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.asCoroutineDispatcher
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.matrix.android.sdk.api.extensions.orFalse
|
||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||
import org.matrix.android.sdk.api.session.Session
|
||||
import org.matrix.android.sdk.api.session.call.CallState
|
||||
import org.matrix.android.sdk.api.session.call.MxCall
|
||||
import org.matrix.android.sdk.api.session.call.TurnServerResponse
|
||||
import org.matrix.android.sdk.api.session.room.model.call.CallAnswerContent
|
||||
import org.matrix.android.sdk.api.session.room.model.call.CallCandidatesContent
|
||||
import org.matrix.android.sdk.api.session.room.model.call.CallInviteContent
|
||||
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.internal.util.awaitCallback
|
||||
import org.webrtc.AudioSource
|
||||
import org.webrtc.AudioTrack
|
||||
import org.webrtc.Camera1Enumerator
|
||||
import org.webrtc.Camera2Enumerator
|
||||
import org.webrtc.CameraVideoCapturer
|
||||
import org.webrtc.EglBase
|
||||
import org.webrtc.IceCandidate
|
||||
import org.webrtc.MediaConstraints
|
||||
import org.webrtc.MediaStream
|
||||
import org.webrtc.PeerConnection
|
||||
import org.webrtc.PeerConnectionFactory
|
||||
import org.webrtc.RtpTransceiver
|
||||
import org.webrtc.SessionDescription
|
||||
import org.webrtc.SurfaceTextureHelper
|
||||
import org.webrtc.SurfaceViewRenderer
|
||||
import org.webrtc.VideoSource
|
||||
import org.webrtc.VideoTrack
|
||||
import timber.log.Timber
|
||||
import java.lang.ref.WeakReference
|
||||
import java.util.concurrent.Executor
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.inject.Provider
|
||||
|
||||
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()
|
||||
|
||||
class WebRtcCall(val mxCall: MxCall,
|
||||
private val callAudioManager: CallAudioManager,
|
||||
private val rootEglBase: EglBase?,
|
||||
private val context: Context,
|
||||
private val session: Session,
|
||||
private val executor: Executor,
|
||||
private val peerConnectionFactoryProvider: Provider<PeerConnectionFactory?>) {
|
||||
|
||||
private val dispatcher = executor.asCoroutineDispatcher()
|
||||
|
||||
var peerConnection: PeerConnection? = null
|
||||
var localAudioSource: AudioSource? = null
|
||||
var localAudioTrack: AudioTrack? = null
|
||||
var localVideoSource: VideoSource? = null
|
||||
var localVideoTrack: VideoTrack? = null
|
||||
var remoteVideoTrack: VideoTrack? = null
|
||||
|
||||
// Perfect negotiation state: https://www.w3.org/TR/webrtc/#perfect-negotiation-example
|
||||
var makingOffer: Boolean = false
|
||||
var ignoreOffer: Boolean = false
|
||||
|
||||
private var videoCapturer: CameraVideoCapturer? = null
|
||||
|
||||
private val availableCamera = ArrayList<CameraProxy>()
|
||||
private var cameraInUse: CameraProxy? = null
|
||||
private var currentCaptureFormat: CaptureFormat = CaptureFormat.HD
|
||||
private var capturerIsInError = false
|
||||
private var cameraAvailabilityCallback: CameraManager.AvailabilityCallback? = null
|
||||
|
||||
// Mute status
|
||||
var micMuted = false
|
||||
var videoMuted = false
|
||||
var remoteOnHold = false
|
||||
|
||||
var offerSdp: CallInviteContent.Offer? = null
|
||||
|
||||
private var localSurfaceRenderers: MutableList<WeakReference<SurfaceViewRenderer>> = ArrayList()
|
||||
private var remoteSurfaceRenderers: MutableList<WeakReference<SurfaceViewRenderer>> = ArrayList()
|
||||
|
||||
val iceCandidateSource: PublishSubject<IceCandidate> = PublishSubject.create()
|
||||
private val iceCandidateDisposable = iceCandidateSource
|
||||
.buffer(300, TimeUnit.MILLISECONDS)
|
||||
.subscribe {
|
||||
// omit empty :/
|
||||
if (it.isNotEmpty()) {
|
||||
Timber.v("## Sending local ice candidates to call")
|
||||
// it.forEach { peerConnection?.addIceCandidate(it) }
|
||||
mxCall.sendLocalIceCandidates(it.mapToCallCandidate())
|
||||
}
|
||||
}
|
||||
|
||||
var remoteCandidateSource: ReplaySubject<IceCandidate> = ReplaySubject.create()
|
||||
var remoteIceCandidateDisposable: Disposable? = null
|
||||
|
||||
private fun createLocalStream() {
|
||||
val peerConnectionFactory = peerConnectionFactoryProvider.get() ?: return
|
||||
Timber.v("Create local stream for call ${mxCall.callId}")
|
||||
configureAudioTrack(peerConnectionFactory)
|
||||
// add video track if needed
|
||||
if (mxCall.isVideoCall) {
|
||||
configureVideoTrack(peerConnectionFactory)
|
||||
}
|
||||
updateMuteStatus()
|
||||
}
|
||||
|
||||
private fun configureAudioTrack(peerConnectionFactory: PeerConnectionFactory) {
|
||||
val audioSource = peerConnectionFactory.createAudioSource(DEFAULT_AUDIO_CONSTRAINTS)
|
||||
val audioTrack = peerConnectionFactory.createAudioTrack(AUDIO_TRACK_ID, audioSource)
|
||||
audioTrack.setEnabled(true)
|
||||
Timber.v("Add audio track $AUDIO_TRACK_ID to call ${mxCall.callId}")
|
||||
peerConnection?.addTrack(audioTrack, listOf(STREAM_ID))
|
||||
localAudioSource = audioSource
|
||||
localAudioTrack = audioTrack
|
||||
}
|
||||
|
||||
fun sendSpdOffer() = GlobalScope.launch(dispatcher) {
|
||||
val constraints = MediaConstraints()
|
||||
// These are deprecated options
|
||||
// constraints.mandatory.add(MediaConstraints.KeyValuePair("OfferToReceiveAudio", "true"))
|
||||
// constraints.mandatory.add(MediaConstraints.KeyValuePair("OfferToReceiveVideo", if (currentCall?.mxCall?.isVideoCall == true) "true" else "false"))
|
||||
|
||||
val peerConnection = peerConnection ?: return@launch
|
||||
Timber.v("## VOIP creating offer...")
|
||||
makingOffer = true
|
||||
try {
|
||||
val sessionDescription = peerConnection.awaitCreateOffer(constraints) ?: return@launch
|
||||
peerConnection.awaitSetLocalDescription(sessionDescription)
|
||||
if (peerConnection.iceGatheringState() == PeerConnection.IceGatheringState.GATHERING) {
|
||||
// Allow a short time for initial candidates to be gathered
|
||||
delay(200)
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
private fun createPeerConnection(turnServerResponse: TurnServerResponse?) {
|
||||
val peerConnectionFactory = peerConnectionFactoryProvider.get() ?: return
|
||||
val iceServers = mutableListOf<PeerConnection.IceServer>().apply {
|
||||
turnServerResponse?.let { server ->
|
||||
server.uris?.forEach { uri ->
|
||||
add(
|
||||
PeerConnection
|
||||
.IceServer
|
||||
.builder(uri)
|
||||
.setUsername(server.username)
|
||||
.setPassword(server.password)
|
||||
.createIceServer()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Timber.v("## VOIP creating peer connection...with iceServers $iceServers ")
|
||||
val rtcConfig = PeerConnection.RTCConfiguration(iceServers).apply {
|
||||
sdpSemantics = PeerConnection.SdpSemantics.UNIFIED_PLAN
|
||||
}
|
||||
peerConnection = peerConnectionFactory.createPeerConnection(rtcConfig, PeerConnectionObserver(this, callAudioManager))
|
||||
}
|
||||
|
||||
fun attachViewRenderers(localViewRenderer: SurfaceViewRenderer?, remoteViewRenderer: SurfaceViewRenderer, mode: String?) {
|
||||
Timber.v("## VOIP attachViewRenderers localRendeder $localViewRenderer / $remoteViewRenderer")
|
||||
// this.localSurfaceRenderer = WeakReference(localViewRenderer)
|
||||
// this.remoteSurfaceRenderer = WeakReference(remoteViewRenderer)
|
||||
localSurfaceRenderers.addIfNeeded(localViewRenderer)
|
||||
remoteSurfaceRenderers.addIfNeeded(remoteViewRenderer)
|
||||
|
||||
// The call is going to resume from background, we can reduce notif
|
||||
mxCall
|
||||
.takeIf { it.state is CallState.Connected }
|
||||
?.let { mxCall ->
|
||||
val name = session.getUser(mxCall.opponentUserId)?.getBestName()
|
||||
?: mxCall.roomId
|
||||
// Start background service with notification
|
||||
CallService.onPendingCall(
|
||||
context = context,
|
||||
isVideo = mxCall.isVideoCall,
|
||||
roomName = name,
|
||||
roomId = mxCall.roomId,
|
||||
matrixId = session.myUserId,
|
||||
callId = mxCall.callId)
|
||||
}
|
||||
|
||||
GlobalScope.launch(dispatcher) {
|
||||
when (mode) {
|
||||
VectorCallActivity.INCOMING_ACCEPT -> {
|
||||
internalAcceptIncomingCall()
|
||||
}
|
||||
VectorCallActivity.INCOMING_RINGING -> {
|
||||
// wait until accepted to create peer connection
|
||||
// TODO eventually we could already display local stream in PIP?
|
||||
}
|
||||
VectorCallActivity.OUTGOING_CREATED -> {
|
||||
setupOutgoingCall()
|
||||
}
|
||||
else -> {
|
||||
// sink existing tracks (configuration change, e.g screen rotation)
|
||||
attachViewRenderersInternal()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun acceptIncomingCall() = GlobalScope.launch {
|
||||
if (mxCall.state == CallState.LocalRinging) {
|
||||
internalAcceptIncomingCall()
|
||||
}
|
||||
}
|
||||
|
||||
fun detachRenderers(renderers: List<SurfaceViewRenderer>?) {
|
||||
Timber.v("## VOIP detachRenderers")
|
||||
// currentCall?.localMediaStream?.let { currentCall?.peerConnection?.removeStream(it) }
|
||||
if (renderers.isNullOrEmpty()) {
|
||||
// remove all sinks
|
||||
localSurfaceRenderers.forEach {
|
||||
if (it.get() != null) localVideoTrack?.removeSink(it.get())
|
||||
}
|
||||
remoteSurfaceRenderers.forEach {
|
||||
if (it.get() != null) remoteVideoTrack?.removeSink(it.get())
|
||||
}
|
||||
localSurfaceRenderers.clear()
|
||||
remoteSurfaceRenderers.clear()
|
||||
} else {
|
||||
renderers.forEach {
|
||||
localSurfaceRenderers.removeIfNeeded(it)
|
||||
remoteSurfaceRenderers.removeIfNeeded(it)
|
||||
// no need to check if it's in the track, removeSink is doing it
|
||||
localVideoTrack?.removeSink(it)
|
||||
remoteVideoTrack?.removeSink(it)
|
||||
}
|
||||
}
|
||||
if (remoteSurfaceRenderers.isEmpty()) {
|
||||
// The call is going to continue in background, so ensure notification is visible
|
||||
mxCall
|
||||
.takeIf { it.state is CallState.Connected }
|
||||
?.let { mxCall ->
|
||||
// Start background service with notification
|
||||
val name = session.getUser(mxCall.opponentUserId)?.getBestName()
|
||||
?: mxCall.opponentUserId
|
||||
CallService.onOnGoingCallBackground(
|
||||
context = context,
|
||||
isVideo = mxCall.isVideoCall,
|
||||
roomName = name,
|
||||
roomId = mxCall.roomId,
|
||||
matrixId = session.myUserId ,
|
||||
callId = mxCall.callId
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun setupOutgoingCall() = withContext(dispatcher) {
|
||||
val turnServer = getTurnServer()
|
||||
mxCall.state = CallState.CreateOffer
|
||||
// 1. Create RTCPeerConnection
|
||||
createPeerConnection(turnServer)
|
||||
// 2. Access camera (if video call) + microphone, create local stream
|
||||
createLocalStream()
|
||||
attachViewRenderersInternal()
|
||||
Timber.v("## VOIP remoteCandidateSource ${remoteCandidateSource}")
|
||||
remoteIceCandidateDisposable = remoteCandidateSource.subscribe({
|
||||
Timber.v("## VOIP adding remote ice candidate $it")
|
||||
peerConnection?.addIceCandidate(it)
|
||||
}, {
|
||||
Timber.v("## VOIP failed to add remote ice candidate $it")
|
||||
})
|
||||
// Now we wait for negotiation callback
|
||||
}
|
||||
|
||||
private suspend fun internalAcceptIncomingCall() = withContext(dispatcher) {
|
||||
val turnServerResponse = getTurnServer()
|
||||
// Update service state
|
||||
withContext(Dispatchers.Main) {
|
||||
val name = session.getUser(mxCall.opponentUserId)?.getBestName()
|
||||
?: mxCall.roomId
|
||||
CallService.onPendingCall(
|
||||
context = context,
|
||||
isVideo = mxCall.isVideoCall,
|
||||
roomName = name,
|
||||
roomId = mxCall.roomId,
|
||||
matrixId = session.myUserId,
|
||||
callId = mxCall.callId
|
||||
)
|
||||
}
|
||||
// 1) create peer connection
|
||||
createPeerConnection(turnServerResponse)
|
||||
|
||||
// create sdp using offer, and set remote description
|
||||
// the offer has beed stored when invite was received
|
||||
val offerSdp = offerSdp?.sdp?.let {
|
||||
SessionDescription(SessionDescription.Type.OFFER, it)
|
||||
}
|
||||
if (offerSdp == null) {
|
||||
Timber.v("We don't have any offer to process")
|
||||
return@withContext
|
||||
}
|
||||
Timber.v("Offer sdp for invite: ${offerSdp.description}")
|
||||
try {
|
||||
peerConnection?.awaitSetRemoteDescription(offerSdp)
|
||||
} catch (failure: Throwable) {
|
||||
Timber.v("Failure putting remote description")
|
||||
return@withContext
|
||||
}
|
||||
// 2) Access camera + microphone, create local stream
|
||||
createLocalStream()
|
||||
attachViewRenderersInternal()
|
||||
|
||||
// create a answer, set local description and send via signaling
|
||||
createAnswer()?.also {
|
||||
mxCall.accept(it.description)
|
||||
}
|
||||
Timber.v("## VOIP remoteCandidateSource ${remoteCandidateSource}")
|
||||
remoteIceCandidateDisposable = remoteCandidateSource.subscribe({
|
||||
Timber.v("## VOIP adding remote ice candidate $it")
|
||||
peerConnection?.addIceCandidate(it)
|
||||
}, {
|
||||
Timber.v("## VOIP failed to add remote ice candidate $it")
|
||||
})
|
||||
}
|
||||
|
||||
private fun attachViewRenderersInternal() {
|
||||
// render local video in pip view
|
||||
localSurfaceRenderers.forEach { renderer ->
|
||||
renderer.get()?.let { pipSurface ->
|
||||
pipSurface.setMirror(this.cameraInUse?.type == CameraType.FRONT)
|
||||
// no need to check if already added, addSink is checking that
|
||||
localVideoTrack?.addSink(pipSurface)
|
||||
}
|
||||
}
|
||||
|
||||
// If remote track exists, then sink it to surface
|
||||
remoteSurfaceRenderers.forEach { renderer ->
|
||||
renderer.get()?.let { participantSurface ->
|
||||
remoteVideoTrack?.addSink(participantSurface)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getTurnServer(): TurnServerResponse? {
|
||||
return tryOrNull {
|
||||
awaitCallback {
|
||||
session.callSignalingService().getTurnServer(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun configureVideoTrack(peerConnectionFactory: PeerConnectionFactory) {
|
||||
availableCamera.clear()
|
||||
val cameraIterator = if (Camera2Enumerator.isSupported(context)) {
|
||||
Camera2Enumerator(context)
|
||||
} else {
|
||||
Camera1Enumerator(false)
|
||||
}
|
||||
// I don't realy know how that works if there are 2 front or 2 back cameras
|
||||
val frontCamera = cameraIterator.deviceNames
|
||||
?.firstOrNull { cameraIterator.isFrontFacing(it) }
|
||||
?.let {
|
||||
CameraProxy(it, CameraType.FRONT).also { availableCamera.add(it) }
|
||||
}
|
||||
|
||||
val backCamera = cameraIterator.deviceNames
|
||||
?.firstOrNull { cameraIterator.isBackFacing(it) }
|
||||
?.let {
|
||||
CameraProxy(it, CameraType.BACK).also { availableCamera.add(it) }
|
||||
}
|
||||
|
||||
val camera = frontCamera?.also { cameraInUse = frontCamera }
|
||||
?: backCamera?.also { cameraInUse = backCamera }
|
||||
?: null.also { cameraInUse = null }
|
||||
|
||||
if (camera != null) {
|
||||
val videoCapturer = cameraIterator.createCapturer(camera.name, object : CameraEventsHandlerAdapter() {
|
||||
override fun onFirstFrameAvailable() {
|
||||
super.onFirstFrameAvailable()
|
||||
capturerIsInError = false
|
||||
}
|
||||
|
||||
override fun onCameraClosed() {
|
||||
super.onCameraClosed()
|
||||
// This could happen if you open the camera app in chat
|
||||
// We then register in order to restart capture as soon as the camera is available again
|
||||
capturerIsInError = true
|
||||
val cameraManager = context.getSystemService<CameraManager>()
|
||||
cameraAvailabilityCallback = object : CameraManager.AvailabilityCallback() {
|
||||
override fun onCameraAvailable(cameraId: String) {
|
||||
if (cameraId == camera.name) {
|
||||
videoCapturer?.startCapture(currentCaptureFormat.width, currentCaptureFormat.height, currentCaptureFormat.fps)
|
||||
cameraManager?.unregisterAvailabilityCallback(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
cameraManager?.registerAvailabilityCallback(cameraAvailabilityCallback!!, null)
|
||||
}
|
||||
})
|
||||
|
||||
val videoSource = peerConnectionFactory.createVideoSource(videoCapturer.isScreencast)
|
||||
val surfaceTextureHelper = SurfaceTextureHelper.create("CaptureThread", rootEglBase!!.eglBaseContext)
|
||||
Timber.v("## VOIP Local video source created")
|
||||
|
||||
videoCapturer.initialize(surfaceTextureHelper, context, videoSource.capturerObserver)
|
||||
// HD
|
||||
videoCapturer.startCapture(currentCaptureFormat.width, currentCaptureFormat.height, currentCaptureFormat.fps)
|
||||
this.videoCapturer = videoCapturer
|
||||
|
||||
val videoTrack = peerConnectionFactory.createVideoTrack(VIDEO_TRACK_ID, videoSource)
|
||||
Timber.v("Add video track $VIDEO_TRACK_ID to call ${mxCall.callId}")
|
||||
videoTrack.setEnabled(true)
|
||||
peerConnection?.addTrack(videoTrack, listOf(STREAM_ID))
|
||||
localVideoSource = videoSource
|
||||
localVideoTrack = videoTrack
|
||||
}
|
||||
}
|
||||
|
||||
fun setCaptureFormat(format: CaptureFormat) {
|
||||
Timber.v("## VOIP setCaptureFormat $format")
|
||||
executor.execute {
|
||||
// videoCapturer?.stopCapture()
|
||||
videoCapturer?.changeCaptureFormat(format.width, format.height, format.fps)
|
||||
currentCaptureFormat = format
|
||||
//currentCallsListeners.forEach { tryOrNull { it.onCaptureStateChanged() } }
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateMuteStatus() {
|
||||
val micShouldBeMuted = micMuted || remoteOnHold
|
||||
localAudioTrack?.setEnabled(!micShouldBeMuted)
|
||||
val vidShouldBeMuted = videoMuted || remoteOnHold
|
||||
localVideoTrack?.setEnabled(!vidShouldBeMuted)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
if (mxCall.state !is CallState.Connected) return false
|
||||
var callOnHold = true
|
||||
// We consider a call to be on hold only if *all* the tracks are on hold
|
||||
// (is this the right thing to do?)
|
||||
for (transceiver in peerConnection?.transceivers ?: emptyList()) {
|
||||
val trackOnHold = transceiver.currentDirection == RtpTransceiver.RtpTransceiverDirection.INACTIVE
|
||||
|| transceiver.currentDirection == RtpTransceiver.RtpTransceiverDirection.RECV_ONLY
|
||||
if (!trackOnHold) callOnHold = false;
|
||||
}
|
||||
return callOnHold;
|
||||
}
|
||||
|
||||
fun updateRemoteOnHold(onHold: Boolean) {
|
||||
if (remoteOnHold == onHold) return
|
||||
remoteOnHold = onHold
|
||||
val direction = if (onHold) {
|
||||
RtpTransceiver.RtpTransceiverDirection.INACTIVE
|
||||
} else {
|
||||
RtpTransceiver.RtpTransceiverDirection.SEND_RECV
|
||||
}
|
||||
for (transceiver in peerConnection?.transceivers ?: emptyList()) {
|
||||
transceiver.direction = direction
|
||||
}
|
||||
updateMuteStatus()
|
||||
}
|
||||
|
||||
fun muteCall(muted: Boolean) {
|
||||
micMuted = muted
|
||||
updateMuteStatus()
|
||||
}
|
||||
|
||||
fun enableVideo(enabled: Boolean) {
|
||||
videoMuted = !enabled
|
||||
updateMuteStatus()
|
||||
}
|
||||
|
||||
fun canSwitchCamera(): Boolean {
|
||||
return availableCamera.size > 0
|
||||
}
|
||||
|
||||
fun switchCamera() {
|
||||
Timber.v("## VOIP switchCamera")
|
||||
if (!canSwitchCamera()) return
|
||||
if (mxCall.state is CallState.Connected && mxCall.isVideoCall) {
|
||||
videoCapturer?.switchCamera(object : CameraVideoCapturer.CameraSwitchHandler {
|
||||
// Invoked on success. |isFrontCamera| is true if the new camera is front facing.
|
||||
override fun onCameraSwitchDone(isFrontCamera: Boolean) {
|
||||
Timber.v("## VOIP onCameraSwitchDone isFront $isFrontCamera")
|
||||
cameraInUse = availableCamera.first { if (isFrontCamera) it.type == CameraType.FRONT else it.type == CameraType.BACK }
|
||||
localSurfaceRenderers.forEach {
|
||||
it.get()?.setMirror(isFrontCamera)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCameraSwitchError(errorDescription: String?) {
|
||||
Timber.v("## VOIP onCameraSwitchError isFront $errorDescription")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun createAnswer(): SessionDescription? {
|
||||
Timber.w("## VOIP createAnswer")
|
||||
val peerConnection = peerConnection ?: return null
|
||||
val constraints = MediaConstraints().apply {
|
||||
mandatory.add(MediaConstraints.KeyValuePair("OfferToReceiveAudio", "true"))
|
||||
mandatory.add(MediaConstraints.KeyValuePair("OfferToReceiveVideo", if (mxCall.isVideoCall) "true" else "false"))
|
||||
}
|
||||
return try {
|
||||
val localDescription = peerConnection.awaitCreateAnswer(constraints) ?: return null
|
||||
peerConnection.awaitSetLocalDescription(localDescription)
|
||||
localDescription
|
||||
} catch (failure: Throwable) {
|
||||
Timber.v("Fail to create answer")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun currentCameraType(): CameraType? {
|
||||
return cameraInUse?.type
|
||||
}
|
||||
|
||||
fun currentCaptureFormat(): CaptureFormat {
|
||||
return currentCaptureFormat
|
||||
}
|
||||
|
||||
fun release() {
|
||||
videoCapturer?.stopCapture()
|
||||
videoCapturer?.dispose()
|
||||
videoCapturer = null
|
||||
remoteIceCandidateDisposable?.dispose()
|
||||
iceCandidateDisposable?.dispose()
|
||||
peerConnection?.close()
|
||||
peerConnection?.dispose()
|
||||
localAudioSource?.dispose()
|
||||
localVideoSource?.dispose()
|
||||
localAudioSource = null
|
||||
localAudioTrack = null
|
||||
localVideoSource = null
|
||||
localVideoTrack = null
|
||||
}
|
||||
|
||||
fun onAddStream(stream: MediaStream) {
|
||||
executor.execute {
|
||||
// reportError("Weird-looking stream: " + stream);
|
||||
if (stream.audioTracks.size > 1 || stream.videoTracks.size > 1) {
|
||||
Timber.e("## VOIP StreamObserver weird looking stream: $stream")
|
||||
// TODO maybe do something more??
|
||||
mxCall.hangUp()
|
||||
return@execute
|
||||
}
|
||||
if (stream.videoTracks.size == 1) {
|
||||
val remoteVideoTrack = stream.videoTracks.first()
|
||||
remoteVideoTrack.setEnabled(true)
|
||||
this.remoteVideoTrack = remoteVideoTrack
|
||||
// sink to renderer if attached
|
||||
remoteSurfaceRenderers.forEach { it.get()?.let { remoteVideoTrack.addSink(it) } }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun onRemoveStream() {
|
||||
executor.execute {
|
||||
remoteSurfaceRenderers
|
||||
.mapNotNull { it.get() }
|
||||
.forEach { remoteVideoTrack?.removeSink(it) }
|
||||
remoteVideoTrack = null
|
||||
}
|
||||
}
|
||||
|
||||
fun endCall(originatedByMe: Boolean) {
|
||||
mxCall.state = CallState.Terminated
|
||||
localVideoTrack?.setEnabled(false)
|
||||
localVideoTrack?.setEnabled(false)
|
||||
|
||||
cameraAvailabilityCallback?.let { cameraAvailabilityCallback ->
|
||||
val cameraManager = context.getSystemService<CameraManager>()!!
|
||||
cameraManager.unregisterAvailabilityCallback(cameraAvailabilityCallback)
|
||||
}
|
||||
release()
|
||||
if (originatedByMe) {
|
||||
// send hang up event
|
||||
mxCall.hangUp()
|
||||
}
|
||||
}
|
||||
|
||||
// Call listener
|
||||
|
||||
fun onCallIceCandidateReceived(iceCandidatesContent: CallCandidatesContent) {
|
||||
executor.execute {
|
||||
iceCandidatesContent.candidates.forEach {
|
||||
Timber.v("## VOIP onCallIceCandidateReceived for call ${mxCall.callId} sdp: ${it.candidate}")
|
||||
val iceCandidate = IceCandidate(it.sdpMid, it.sdpMLineIndex, it.candidate)
|
||||
remoteCandidateSource.onNext(iceCandidate)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun onCallAnswerReceived(callAnswerContent: CallAnswerContent) {
|
||||
GlobalScope.launch(dispatcher) {
|
||||
Timber.v("## VOIP onCallAnswerReceived ${callAnswerContent.callId}")
|
||||
val sdp = SessionDescription(SessionDescription.Type.ANSWER, callAnswerContent.answer.sdp)
|
||||
try {
|
||||
peerConnection?.awaitSetRemoteDescription(sdp)
|
||||
} catch (failure: Throwable) {
|
||||
return@launch
|
||||
}
|
||||
if (mxCall.opponentPartyId?.hasValue().orFalse()) {
|
||||
mxCall.selectAnswer()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
try {
|
||||
val sdp = SessionDescription(type.asWebRTC(), sdpText)
|
||||
peerConnection.awaitSetRemoteDescription(sdp)
|
||||
if (type == SdpType.OFFER) {
|
||||
createAnswer()?.also {
|
||||
mxCall.negotiate(sdpText)
|
||||
}
|
||||
}
|
||||
} catch (failure: Throwable) {
|
||||
Timber.e(failure, "Failed to complete negotiation")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
@ -0,0 +1,513 @@
|
||||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.app.features.call.webrtc
|
||||
|
||||
import android.content.Context
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleObserver
|
||||
import androidx.lifecycle.OnLifecycleEvent
|
||||
import im.vector.app.ActiveSessionDataSource
|
||||
import im.vector.app.core.services.BluetoothHeadsetReceiver
|
||||
import im.vector.app.core.services.CallService
|
||||
import im.vector.app.core.services.WiredHeadsetStateReceiver
|
||||
import im.vector.app.features.call.CallAudioManager
|
||||
import im.vector.app.features.call.CameraType
|
||||
import im.vector.app.features.call.CaptureFormat
|
||||
import im.vector.app.features.call.VectorCallActivity
|
||||
import im.vector.app.features.call.utils.EglUtils
|
||||
import im.vector.app.features.call.utils.awaitCreateAnswer
|
||||
import im.vector.app.features.call.utils.awaitSetLocalDescription
|
||||
import im.vector.app.push.fcm.FcmHelper
|
||||
import kotlinx.coroutines.asCoroutineDispatcher
|
||||
import org.matrix.android.sdk.api.extensions.orFalse
|
||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||
import org.matrix.android.sdk.api.session.Session
|
||||
import org.matrix.android.sdk.api.session.call.CallListener
|
||||
import org.matrix.android.sdk.api.session.call.CallState
|
||||
import org.matrix.android.sdk.api.session.call.MxCall
|
||||
import org.matrix.android.sdk.api.session.call.TurnServerResponse
|
||||
import org.matrix.android.sdk.api.session.room.model.call.CallAnswerContent
|
||||
import org.matrix.android.sdk.api.session.room.model.call.CallCandidatesContent
|
||||
import org.matrix.android.sdk.api.session.room.model.call.CallHangupContent
|
||||
import org.matrix.android.sdk.api.session.room.model.call.CallInviteContent
|
||||
import org.matrix.android.sdk.api.session.room.model.call.CallNegotiateContent
|
||||
import org.matrix.android.sdk.api.session.room.model.call.CallRejectContent
|
||||
import org.matrix.android.sdk.api.session.room.model.call.CallSelectAnswerContent
|
||||
import org.matrix.android.sdk.internal.util.awaitCallback
|
||||
import org.webrtc.DefaultVideoDecoderFactory
|
||||
import org.webrtc.DefaultVideoEncoderFactory
|
||||
import org.webrtc.MediaConstraints
|
||||
import org.webrtc.PeerConnectionFactory
|
||||
import org.webrtc.SessionDescription
|
||||
import org.webrtc.SurfaceViewRenderer
|
||||
import timber.log.Timber
|
||||
import java.lang.ref.WeakReference
|
||||
import java.util.concurrent.Executors
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Provider
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
* Manage peerConnectionFactory & Peer connections outside of activity lifecycle to resist configuration changes
|
||||
* Use app context
|
||||
*/
|
||||
@Singleton
|
||||
class WebRtcPeerConnectionManager @Inject constructor(
|
||||
private val context: Context,
|
||||
private val activeSessionDataSource: ActiveSessionDataSource
|
||||
) : CallListener, LifecycleObserver {
|
||||
|
||||
private val currentSession: Session?
|
||||
get() = activeSessionDataSource.currentValue?.orNull()
|
||||
|
||||
interface CurrentCallListener {
|
||||
fun onCurrentCallChange(call: MxCall?)
|
||||
fun onCaptureStateChanged() {}
|
||||
fun onAudioDevicesChange() {}
|
||||
fun onCameraChange() {}
|
||||
}
|
||||
|
||||
private val currentCallsListeners = emptyList<CurrentCallListener>().toMutableList()
|
||||
fun addCurrentCallListener(listener: CurrentCallListener) {
|
||||
currentCallsListeners.add(listener)
|
||||
}
|
||||
|
||||
fun removeCurrentCallListener(listener: CurrentCallListener) {
|
||||
currentCallsListeners.remove(listener)
|
||||
}
|
||||
|
||||
val callAudioManager = CallAudioManager(context.applicationContext) {
|
||||
currentCallsListeners.forEach {
|
||||
tryOrNull { it.onAudioDevicesChange() }
|
||||
}
|
||||
}
|
||||
|
||||
private var peerConnectionFactory: PeerConnectionFactory? = null
|
||||
private val executor = Executors.newSingleThreadExecutor()
|
||||
private val dispatcher = executor.asCoroutineDispatcher()
|
||||
|
||||
private val rootEglBase by lazy { EglUtils.rootEglBase }
|
||||
|
||||
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)
|
||||
fun entersForeground() {
|
||||
isInBackground = false
|
||||
}
|
||||
|
||||
@OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
|
||||
fun entersBackground() {
|
||||
isInBackground = true
|
||||
}
|
||||
|
||||
var currentCall: WebRtcCall? = null
|
||||
set(value) {
|
||||
field = value
|
||||
currentCallsListeners.forEach {
|
||||
tryOrNull { it.onCurrentCallChange(value?.mxCall) }
|
||||
}
|
||||
}
|
||||
|
||||
fun headSetButtonTapped() {
|
||||
Timber.v("## VOIP headSetButtonTapped")
|
||||
val call = currentCall?.mxCall ?: return
|
||||
if (call.state is CallState.LocalRinging) {
|
||||
// accept call
|
||||
acceptIncomingCall()
|
||||
}
|
||||
if (call.state is CallState.Connected) {
|
||||
// end call?
|
||||
endCall()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getTurnServer(): TurnServerResponse? {
|
||||
return tryOrNull {
|
||||
awaitCallback {
|
||||
currentSession?.callSignalingService()?.getTurnServer(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun attachViewRenderers(localViewRenderer: SurfaceViewRenderer?, remoteViewRenderer: SurfaceViewRenderer, mode: String?) {
|
||||
currentCall?.attachViewRenderers(localViewRenderer, remoteViewRenderer, mode)
|
||||
}
|
||||
|
||||
private fun createPeerConnectionFactory() {
|
||||
if (peerConnectionFactory != null) return
|
||||
Timber.v("## VOIP createPeerConnectionFactory")
|
||||
val eglBaseContext = rootEglBase?.eglBaseContext ?: return Unit.also {
|
||||
Timber.e("## VOIP No EGL BASE")
|
||||
}
|
||||
|
||||
Timber.v("## VOIP PeerConnectionFactory.initialize")
|
||||
PeerConnectionFactory.initialize(PeerConnectionFactory
|
||||
.InitializationOptions.builder(context.applicationContext)
|
||||
.createInitializationOptions()
|
||||
)
|
||||
|
||||
val options = PeerConnectionFactory.Options()
|
||||
val defaultVideoEncoderFactory = DefaultVideoEncoderFactory(
|
||||
eglBaseContext,
|
||||
/* enableIntelVp8Encoder */
|
||||
true,
|
||||
/* enableH264HighProfile */
|
||||
true)
|
||||
val defaultVideoDecoderFactory = DefaultVideoDecoderFactory(eglBaseContext)
|
||||
Timber.v("## VOIP PeerConnectionFactory.createPeerConnectionFactory ...")
|
||||
peerConnectionFactory = PeerConnectionFactory.builder()
|
||||
.setOptions(options)
|
||||
.setVideoEncoderFactory(defaultVideoEncoderFactory)
|
||||
.setVideoDecoderFactory(defaultVideoDecoderFactory)
|
||||
.createPeerConnectionFactory()
|
||||
|
||||
// attachViewRenderersInternal()
|
||||
}
|
||||
|
||||
fun acceptIncomingCall() {
|
||||
Timber.v("## VOIP acceptIncomingCall from state ${currentCall?.mxCall?.state}")
|
||||
currentCall?.acceptIncomingCall()
|
||||
}
|
||||
|
||||
fun detachRenderers(renderers: List<SurfaceViewRenderer>?) {
|
||||
currentCall?.detachRenderers(renderers)
|
||||
}
|
||||
|
||||
fun close() {
|
||||
Timber.v("## VOIP WebRtcPeerConnectionManager close() >")
|
||||
CallService.onNoActiveCall(context)
|
||||
callAudioManager.stop()
|
||||
currentCall = null
|
||||
// This must be done in this thread
|
||||
executor.execute {
|
||||
if (currentCall == null) {
|
||||
Timber.v("## VOIP Dispose peerConnectionFactory as there is no need to keep one")
|
||||
peerConnectionFactory?.dispose()
|
||||
peerConnectionFactory = null
|
||||
}
|
||||
Timber.v("## VOIP WebRtcPeerConnectionManager close() executor done")
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private const val STREAM_ID = "ARDAMS"
|
||||
private const val AUDIO_TRACK_ID = "ARDAMSa0"
|
||||
private const val VIDEO_TRACK_ID = "ARDAMSv0"
|
||||
|
||||
private val DEFAULT_AUDIO_CONSTRAINTS = MediaConstraints().apply {
|
||||
// add all existing audio filters to avoid having echos
|
||||
// mandatory.add(MediaConstraints.KeyValuePair("googEchoCancellation", "true"))
|
||||
// mandatory.add(MediaConstraints.KeyValuePair("googEchoCancellation2", "true"))
|
||||
// mandatory.add(MediaConstraints.KeyValuePair("googDAEchoCancellation", "true"))
|
||||
//
|
||||
// mandatory.add(MediaConstraints.KeyValuePair("googTypingNoiseDetection", "true"))
|
||||
//
|
||||
// mandatory.add(MediaConstraints.KeyValuePair("googAutoGainControl", "true"))
|
||||
// mandatory.add(MediaConstraints.KeyValuePair("googAutoGainControl2", "true"))
|
||||
//
|
||||
// mandatory.add(MediaConstraints.KeyValuePair("googNoiseSuppression", "true"))
|
||||
// mandatory.add(MediaConstraints.KeyValuePair("googNoiseSuppression2", "true"))
|
||||
//
|
||||
// mandatory.add(MediaConstraints.KeyValuePair("googAudioMirroring", "false"))
|
||||
// mandatory.add(MediaConstraints.KeyValuePair("googHighpassFilter", "true"))
|
||||
}
|
||||
}
|
||||
|
||||
fun startOutgoingCall(signalingRoomId: String, otherUserId: String, isVideoCall: Boolean) {
|
||||
Timber.v("## VOIP startOutgoingCall in room $signalingRoomId to $otherUserId isVideo $isVideoCall")
|
||||
executor.execute {
|
||||
if (peerConnectionFactory == null) {
|
||||
createPeerConnectionFactory()
|
||||
}
|
||||
}
|
||||
|
||||
val createdCall = currentSession?.callSignalingService()?.createOutgoingCall(signalingRoomId, otherUserId, isVideoCall) ?: return
|
||||
val webRtcCall = WebRtcCall(
|
||||
mxCall = createdCall,
|
||||
callAudioManager = callAudioManager,
|
||||
rootEglBase = rootEglBase,
|
||||
context = context,
|
||||
executor = executor,
|
||||
peerConnectionFactoryProvider = Provider {
|
||||
createPeerConnectionFactory()
|
||||
peerConnectionFactory
|
||||
},
|
||||
session = currentSession!!
|
||||
)
|
||||
|
||||
callAudioManager.startForCall(createdCall)
|
||||
currentCall = webRtcCall
|
||||
|
||||
val name = currentSession?.getUser(createdCall.opponentUserId)?.getBestName()
|
||||
?: createdCall.opponentUserId
|
||||
CallService.onOutgoingCallRinging(
|
||||
context = context.applicationContext,
|
||||
isVideo = createdCall.isVideoCall,
|
||||
roomName = name,
|
||||
roomId = createdCall.roomId,
|
||||
matrixId = currentSession?.myUserId ?: "",
|
||||
callId = createdCall.callId)
|
||||
|
||||
// start the activity now
|
||||
context.applicationContext.startActivity(VectorCallActivity.newIntent(context, createdCall))
|
||||
}
|
||||
|
||||
override fun onCallIceCandidateReceived(mxCall: MxCall, iceCandidatesContent: CallCandidatesContent) {
|
||||
Timber.v("## VOIP onCallIceCandidateReceived for call ${mxCall.callId}")
|
||||
if (currentCall?.mxCall?.callId != mxCall.callId) return Unit.also {
|
||||
Timber.w("## VOIP ignore ice candidates from other call")
|
||||
}
|
||||
currentCall?.onCallIceCandidateReceived(iceCandidatesContent)
|
||||
}
|
||||
|
||||
override fun onCallInviteReceived(mxCall: MxCall, callInviteContent: CallInviteContent) {
|
||||
Timber.v("## VOIP onCallInviteReceived callId ${mxCall.callId}")
|
||||
// to simplify we only treat one call at a time, and ignore others
|
||||
if (currentCall != null) {
|
||||
Timber.w("## VOIP receiving incoming call while already in call?")
|
||||
// Just ignore, maybe we could answer from other session?
|
||||
return
|
||||
}
|
||||
val webRtcCall = WebRtcCall(
|
||||
mxCall = mxCall,
|
||||
callAudioManager = callAudioManager,
|
||||
rootEglBase = rootEglBase,
|
||||
context = context,
|
||||
executor = executor,
|
||||
peerConnectionFactoryProvider = {
|
||||
createPeerConnectionFactory()
|
||||
peerConnectionFactory
|
||||
},
|
||||
session = currentSession!!
|
||||
)
|
||||
currentCall = webRtcCall
|
||||
callAudioManager.startForCall(mxCall)
|
||||
// Start background service with notification
|
||||
val name = currentSession?.getUser(mxCall.opponentUserId)?.getBestName()
|
||||
?: mxCall.opponentUserId
|
||||
CallService.onIncomingCallRinging(
|
||||
context = context,
|
||||
isVideo = mxCall.isVideoCall,
|
||||
roomName = name,
|
||||
roomId = mxCall.roomId,
|
||||
matrixId = currentSession?.myUserId ?: "",
|
||||
callId = mxCall.callId
|
||||
)
|
||||
webRtcCall.offerSdp = callInviteContent.offer
|
||||
|
||||
// If this is received while in background, the app will not sync,
|
||||
// and thus won't be able to received events. For example if the call is
|
||||
// accepted on an other session this device will continue ringing
|
||||
if (isInBackground) {
|
||||
if (FcmHelper.isPushSupported()) {
|
||||
// only for push version as fdroid version is already doing it?
|
||||
currentSession?.startAutomaticBackgroundSync(30, 0)
|
||||
} else {
|
||||
// Maybe increase sync freq? but how to set back to default values?
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun createAnswer(call: WebRtcCall): SessionDescription? {
|
||||
Timber.w("## VOIP createAnswer")
|
||||
val peerConnection = call.peerConnection ?: return null
|
||||
val constraints = MediaConstraints().apply {
|
||||
mandatory.add(MediaConstraints.KeyValuePair("OfferToReceiveAudio", "true"))
|
||||
mandatory.add(MediaConstraints.KeyValuePair("OfferToReceiveVideo", if (call.mxCall.isVideoCall) "true" else "false"))
|
||||
}
|
||||
return try {
|
||||
val localDescription = peerConnection.awaitCreateAnswer(constraints) ?: return null
|
||||
peerConnection.awaitSetLocalDescription(localDescription)
|
||||
localDescription
|
||||
} catch (failure: Throwable) {
|
||||
Timber.v("Fail to create answer")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
// Update service state
|
||||
CallService.onNoActiveCall(context)
|
||||
// close tracks ASAP
|
||||
currentCall?.endCall(originatedByMe)
|
||||
close()
|
||||
}
|
||||
|
||||
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) {
|
||||
val call = currentCall ?: return
|
||||
if (call.mxCall.callId != callAnswerContent.callId) return Unit.also {
|
||||
Timber.w("onCallAnswerReceived for non active call? ${callAnswerContent.callId}")
|
||||
}
|
||||
val mxCall = call.mxCall
|
||||
// Update service state
|
||||
val name = currentSession?.getUser(mxCall.opponentUserId)?.getBestName()
|
||||
?: mxCall.opponentUserId
|
||||
CallService.onPendingCall(
|
||||
context = context,
|
||||
isVideo = mxCall.isVideoCall,
|
||||
roomName = name,
|
||||
roomId = mxCall.roomId,
|
||||
matrixId = currentSession?.myUserId ?: "",
|
||||
callId = mxCall.callId
|
||||
)
|
||||
call.onCallAnswerReceived(callAnswerContent)
|
||||
}
|
||||
|
||||
override fun onCallHangupReceived(callHangupContent: CallHangupContent) {
|
||||
val call = currentCall ?: return
|
||||
// Remote echos are filtered, so it's only remote hangups that i will get here
|
||||
if (call.mxCall.callId != callHangupContent.callId) return Unit.also {
|
||||
Timber.w("onCallHangupReceived for non active call? ${callHangupContent.callId}")
|
||||
}
|
||||
endCall(false)
|
||||
}
|
||||
|
||||
override fun onCallRejectReceived(callRejectContent: CallRejectContent) {
|
||||
val call = currentCall ?: return
|
||||
// Remote echos are filtered, so it's only remote hangups that i will get here
|
||||
if (call.mxCall.callId != callRejectContent.callId) return Unit.also {
|
||||
Timber.w("onCallRejected for non active call? ${callRejectContent.callId}")
|
||||
}
|
||||
endCall(false)
|
||||
}
|
||||
|
||||
override fun onCallSelectAnswerReceived(callSelectAnswerContent: CallSelectAnswerContent) {
|
||||
val call = currentCall ?: return
|
||||
if (call.mxCall.callId != callSelectAnswerContent.callId) return Unit.also {
|
||||
Timber.w("onCallSelectAnswerReceived for non active call? ${callSelectAnswerContent.callId}")
|
||||
}
|
||||
val selectedPartyId = callSelectAnswerContent.selectedPartyId
|
||||
if (selectedPartyId != 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
|
||||
endCall(false)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCallNegotiateReceived(callNegotiateContent: CallNegotiateContent) {
|
||||
val call = currentCall ?: return
|
||||
if (call.mxCall.callId != callNegotiateContent.callId) return Unit.also {
|
||||
Timber.w("onCallNegotiateReceived for non active call? ${callNegotiateContent.callId}")
|
||||
}
|
||||
call.onCallNegotiateReceived(callNegotiateContent)
|
||||
}
|
||||
|
||||
override fun onCallManagedByOtherSession(callId: String) {
|
||||
Timber.v("## VOIP onCallManagedByOtherSession: $callId")
|
||||
currentCall = null
|
||||
CallService.onNoActiveCall(context)
|
||||
|
||||
// did we start background sync? so we should stop it
|
||||
if (isInBackground) {
|
||||
if (FcmHelper.isPushSupported()) {
|
||||
currentSession?.stopAnyBackgroundSync()
|
||||
} else {
|
||||
// for fdroid we should not stop, it should continue syncing
|
||||
// maybe we should restore default timeout/delay though?
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
}
|
||||
}
|
@ -35,7 +35,7 @@ import im.vector.app.core.ui.views.ActiveCallViewHolder
|
||||
import im.vector.app.core.ui.views.KeysBackupBanner
|
||||
import im.vector.app.features.call.SharedActiveCallViewModel
|
||||
import im.vector.app.features.call.VectorCallActivity
|
||||
import im.vector.app.features.call.WebRtcPeerConnectionManager
|
||||
import im.vector.app.features.call.webrtc.WebRtcPeerConnectionManager
|
||||
import im.vector.app.features.home.room.list.RoomListFragment
|
||||
import im.vector.app.features.home.room.list.RoomListParams
|
||||
import im.vector.app.features.popup.PopupAlertManager
|
||||
|
@ -116,7 +116,7 @@ import im.vector.app.features.attachments.preview.AttachmentsPreviewArgs
|
||||
import im.vector.app.features.attachments.toGroupedContentAttachmentData
|
||||
import im.vector.app.features.call.SharedActiveCallViewModel
|
||||
import im.vector.app.features.call.VectorCallActivity
|
||||
import im.vector.app.features.call.WebRtcPeerConnectionManager
|
||||
import im.vector.app.features.call.webrtc.WebRtcPeerConnectionManager
|
||||
import im.vector.app.features.call.conference.JitsiCallViewModel
|
||||
import im.vector.app.features.command.Command
|
||||
import im.vector.app.features.crypto.keysbackup.restore.KeysBackupRestoreActivity
|
||||
|
@ -32,7 +32,7 @@ import im.vector.app.core.extensions.exhaustive
|
||||
import im.vector.app.core.platform.VectorViewModel
|
||||
import im.vector.app.core.resources.StringProvider
|
||||
import im.vector.app.core.utils.subscribeLogError
|
||||
import im.vector.app.features.call.WebRtcPeerConnectionManager
|
||||
import im.vector.app.features.call.webrtc.WebRtcPeerConnectionManager
|
||||
import im.vector.app.features.command.CommandParser
|
||||
import im.vector.app.features.command.ParsedCommand
|
||||
import im.vector.app.features.crypto.verification.SupportedVerificationMethodsProvider
|
||||
|
Loading…
Reference in New Issue
Block a user