VoIP: fix bunch of issues

This commit is contained in:
ganfra 2020-12-22 12:30:48 +01:00
parent a16086db6f
commit c53111a85a
7 changed files with 90 additions and 125 deletions

View File

@ -52,7 +52,7 @@ class CurrentCallsView @JvmOverloads constructor(
it.mxCall.state is CallState.Connected it.mxCall.state is CallState.Connected
} }
val heldCalls = connectedCalls.filter { val heldCalls = connectedCalls.filter {
it.isLocalOnHold() || it.remoteOnHold it.isLocalOnHold || it.remoteOnHold
} }
if (connectedCalls.size == 1) { if (connectedCalls.size == 1) {
if (heldCalls.size == 1) { if (heldCalls.size == 1) {

View File

@ -42,6 +42,9 @@ class KnownCallsViewHolder {
} }
fun updateCall(currentCall: WebRtcCall?, calls: List<WebRtcCall>) { fun updateCall(currentCall: WebRtcCall?, calls: List<WebRtcCall>) {
activeCallPiP?.let {
this.currentCall?.detachRenderers(listOf(it))
}
this.currentCall?.removeListener(tickListener) this.currentCall?.removeListener(tickListener)
this.currentCall = currentCall this.currentCall = currentCall
this.currentCall?.addListener(tickListener) this.currentCall?.addListener(tickListener)

View File

@ -16,80 +16,43 @@
package im.vector.app.core.utils package im.vector.app.core.utils
import android.os.Handler import io.reactivex.Flowable
import android.os.SystemClock import io.reactivex.Observable
import io.reactivex.disposables.Disposable
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicLong
class CountUpTimer(private val intervalInMs: Long) { class CountUpTimer(private val intervalInMs: Long) {
private var startTimestamp: Long = 0 private val elapsedTime: AtomicLong = AtomicLong()
private var delayTime: Long = 0 private val resumed: AtomicBoolean = AtomicBoolean(false)
private var lastPauseTimestamp: Long = 0
private var isRunning: Boolean = false private val disposable = Observable.interval(intervalInMs, TimeUnit.MILLISECONDS)
.filter { _ -> resumed.get() }
.doOnNext { _ -> elapsedTime.addAndGet(intervalInMs) }
.subscribe {
tickListener?.onTick(elapsedTime.get())
}
var tickListener: TickListener? = null var tickListener: TickListener? = null
private val tickHandler: Handler = Handler() fun elapsedTime(): Long{
private val tickSelector = Runnable { return elapsedTime.get()
if (isRunning) {
tickListener?.onTick(time)
startTicking()
}
} }
init {
reset()
}
/**
* Reset the timer, also clears all laps information. Running status will not affected
*/
fun reset() {
startTimestamp = SystemClock.elapsedRealtime()
delayTime = 0
lastPauseTimestamp = startTimestamp
}
/**
* Pause the timer
*/
fun pause() { fun pause() {
if (isRunning) { resumed.set(false)
lastPauseTimestamp = SystemClock.elapsedRealtime()
isRunning = false
stopTicking()
}
} }
/**
* Resume the timer
*/
fun resume() { fun resume() {
if (!isRunning) { resumed.set(true)
val currentTime: Long = SystemClock.elapsedRealtime()
delayTime += currentTime - lastPauseTimestamp
isRunning = true
startTicking()
}
}
val time: Long
get() = if (isRunning) {
SystemClock.elapsedRealtime() - startTimestamp - delayTime
} else {
lastPauseTimestamp - startTimestamp - delayTime
} }
private fun startTicking() { fun stop() {
tickHandler.removeCallbacksAndMessages(null) disposable.dispose()
val time = time
val remainingTimeInInterval = intervalInMs - time % intervalInMs
tickHandler.postDelayed(tickSelector, remainingTimeInInterval)
} }
private fun stopTicking() {
tickHandler.removeCallbacksAndMessages(null)
}
interface TickListener { interface TickListener {
fun onTick(milliseconds: Long) fun onTick(milliseconds: Long)
} }

View File

@ -50,7 +50,6 @@ 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
import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.android.schedulers.AndroidSchedulers
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import okhttp3.internal.concurrent.formatDuration
import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.extensions.orFalse
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.MxCallDetail import org.matrix.android.sdk.api.session.call.MxCallDetail
@ -166,6 +165,7 @@ class VectorCallActivity : VectorBaseActivity<ActivityCallBinding>(), CallContro
views.smallIsHeldIcon.isVisible = false views.smallIsHeldIcon.isVisible = false
when (callState) { when (callState) {
is CallState.Idle, is CallState.Idle,
is CallState.CreateOffer,
is CallState.Dialing -> { is CallState.Dialing -> {
views.callVideoGroup.isInvisible = true views.callVideoGroup.isInvisible = true
views.callInfoGroup.isVisible = true views.callInfoGroup.isVisible = true
@ -189,7 +189,7 @@ class VectorCallActivity : VectorBaseActivity<ActivityCallBinding>(), CallContro
} }
is CallState.Connected -> { is CallState.Connected -> {
if (callState.iceConnectionState == MxPeerConnectionState.CONNECTED) { if (callState.iceConnectionState == MxPeerConnectionState.CONNECTED) {
if (state.isLocalOnHold) { if (state.isLocalOnHold || state.isRemoteOnHold) {
views.smallIsHeldIcon.isVisible = true views.smallIsHeldIcon.isVisible = true
views.callVideoGroup.isInvisible = true views.callVideoGroup.isInvisible = true
views.callInfoGroup.isVisible = true views.callInfoGroup.isVisible = true
@ -226,8 +226,6 @@ class VectorCallActivity : VectorBaseActivity<ActivityCallBinding>(), CallContro
views.callStatusText.setText(R.string.call_connecting) views.callStatusText.setText(R.string.call_connecting)
views.callConnectingProgress.isVisible = true views.callConnectingProgress.isVisible = true
} }
// ensure all attached?
callManager.getCallById(callArgs.callId)?.attachViewRenderers(views.pipRenderer, views.fullscreenRenderer, null)
} }
is CallState.Terminated -> { is CallState.Terminated -> {
finish() finish()
@ -255,7 +253,7 @@ class VectorCallActivity : VectorBaseActivity<ActivityCallBinding>(), CallContro
val colorFilter = ContextCompat.getColor(this, R.color.bg_call_screen) val colorFilter = ContextCompat.getColor(this, R.color.bg_call_screen)
avatarRenderer.renderBlur(state.otherKnownCallInfo.otherUserItem, views.otherKnownCallAvatarView, sampling = 20, rounded = false, colorFilter = colorFilter) avatarRenderer.renderBlur(state.otherKnownCallInfo.otherUserItem, views.otherKnownCallAvatarView, sampling = 20, rounded = false, colorFilter = colorFilter)
views.otherKnownCallLayout.isVisible = true views.otherKnownCallLayout.isVisible = true
views.otherSmallIsHeldIcon.isVisible = otherCall?.let { it.isLocalOnHold() || it.remoteOnHold }.orFalse() views.otherSmallIsHeldIcon.isVisible = otherCall?.let { it.isLocalOnHold || it.remoteOnHold }.orFalse()
} }
} }

View File

@ -55,7 +55,7 @@ class VectorCallViewModel @AssistedInject constructor(
override fun onHoldUnhold() { override fun onHoldUnhold() {
setState { setState {
copy( copy(
isLocalOnHold = call?.isLocalOnHold() ?: false, isLocalOnHold = call?.isLocalOnHold ?: false,
isRemoteOnHold = call?.remoteOnHold ?: false isRemoteOnHold = call?.remoteOnHold ?: false
) )
} }
@ -179,7 +179,7 @@ class VectorCallViewModel @AssistedInject constructor(
callState = Success(webRtcCall.mxCall.state), callState = Success(webRtcCall.mxCall.state),
callInfo = VectorCallViewState.CallInfo(callId, item), callInfo = VectorCallViewState.CallInfo(callId, item),
soundDevice = currentSoundDevice, soundDevice = currentSoundDevice,
isLocalOnHold = webRtcCall.isLocalOnHold(), isLocalOnHold = webRtcCall.isLocalOnHold,
isRemoteOnHold = webRtcCall.remoteOnHold, isRemoteOnHold = webRtcCall.remoteOnHold,
availableSoundDevices = callManager.callAudioManager.getAvailableSoundDevices(), availableSoundDevices = callManager.callAudioManager.getAvailableSoundDevices(),
isFrontCamera = webRtcCall.currentCameraType() == CameraType.FRONT, isFrontCamera = webRtcCall.currentCameraType() == CameraType.FRONT,

View File

@ -135,7 +135,7 @@ class WebRtcCall(val mxCall: MxCall,
private var currentCaptureFormat: CaptureFormat = CaptureFormat.HD private var currentCaptureFormat: CaptureFormat = CaptureFormat.HD
private var cameraAvailabilityCallback: CameraManager.AvailabilityCallback? = null private var cameraAvailabilityCallback: CameraManager.AvailabilityCallback? = null
private val timer = CountUpTimer(1000).apply { private val timer = CountUpTimer(Duration.ofSeconds(1).toMillis()).apply {
tickListener = object : CountUpTimer.TickListener { tickListener = object : CountUpTimer.TickListener {
override fun onTick(milliseconds: Long) { override fun onTick(milliseconds: Long) {
val formattedDuration = formatDuration(Duration.ofMillis(milliseconds)) val formattedDuration = formatDuration(Duration.ofMillis(milliseconds))
@ -146,7 +146,6 @@ class WebRtcCall(val mxCall: MxCall,
} }
} }
// Mute status // Mute status
var micMuted = false var micMuted = false
private set private set
@ -154,6 +153,11 @@ class WebRtcCall(val mxCall: MxCall,
private set private set
var remoteOnHold = false var remoteOnHold = false
private set private set
var isLocalOnHold = false
private set
// This value is used to track localOnHold when changing remoteOnHold value
private var wasLocalOnHold = false
var offerSdp: CallInviteContent.Offer? = null var offerSdp: CallInviteContent.Offer? = null
@ -228,7 +232,7 @@ class WebRtcCall(val mxCall: MxCall,
fun formattedDuration(): String { fun formattedDuration(): String {
return formatDuration( return formatDuration(
Duration.ofMillis(timer.time) Duration.ofMillis(timer.elapsedTime())
) )
} }
@ -257,21 +261,9 @@ class WebRtcCall(val mxCall: MxCall,
fun attachViewRenderers(localViewRenderer: SurfaceViewRenderer?, remoteViewRenderer: SurfaceViewRenderer, mode: String?) { fun attachViewRenderers(localViewRenderer: SurfaceViewRenderer?, remoteViewRenderer: SurfaceViewRenderer, mode: String?) {
Timber.v("## VOIP attachViewRenderers localRendeder $localViewRenderer / $remoteViewRenderer") Timber.v("## VOIP attachViewRenderers localRendeder $localViewRenderer / $remoteViewRenderer")
// this.localSurfaceRenderer = WeakReference(localViewRenderer)
// this.remoteSurfaceRenderer = WeakReference(remoteViewRenderer)
localSurfaceRenderers.addIfNeeded(localViewRenderer) localSurfaceRenderers.addIfNeeded(localViewRenderer)
remoteSurfaceRenderers.addIfNeeded(remoteViewRenderer) remoteSurfaceRenderers.addIfNeeded(remoteViewRenderer)
// The call is going to resume from background, we can reduce notif
mxCall
.takeIf { it.state is CallState.Connected }
?.let { mxCall ->
// Start background service with notification
CallService.onPendingCall(
context = context,
callId = mxCall.callId)
}
GlobalScope.launch(dispatcher) { GlobalScope.launch(dispatcher) {
when (mode) { when (mode) {
VectorCallActivity.INCOMING_ACCEPT -> { VectorCallActivity.INCOMING_ACCEPT -> {
@ -301,9 +293,8 @@ class WebRtcCall(val mxCall: MxCall,
} }
} }
fun detachRenderers(renderers: List<SurfaceViewRenderer>?) = synchronized(this) { fun detachRenderers(renderers: List<SurfaceViewRenderer>?) {
Timber.v("## VOIP detachRenderers") Timber.v("## VOIP detachRenderers")
// currentCall?.localMediaStream?.let { currentCall?.peerConnection?.removeStream(it) }
if (renderers.isNullOrEmpty()) { if (renderers.isNullOrEmpty()) {
// remove all sinks // remove all sinks
localSurfaceRenderers.forEach { localSurfaceRenderers.forEach {
@ -545,7 +536,7 @@ class WebRtcCall(val mxCall: MxCall,
* rather than 'sendonly') * rather than 'sendonly')
* @returns true if the other party has put us on hold * @returns true if the other party has put us on hold
*/ */
fun isLocalOnHold(): Boolean = synchronized(this) { private fun computeIsLocalOnHold(): Boolean {
if (mxCall.state !is CallState.Connected) return false if (mxCall.state !is CallState.Connected) return false
var callOnHold = true var callOnHold = true
// We consider a call to be on hold only if *all* the tracks are on hold // We consider a call to be on hold only if *all* the tracks are on hold
@ -558,38 +549,46 @@ class WebRtcCall(val mxCall: MxCall,
return callOnHold return callOnHold
} }
fun updateRemoteOnHold(onHold: Boolean) = synchronized(this) { fun updateRemoteOnHold(onHold: Boolean) {
if (remoteOnHold == onHold) return GlobalScope.launch(dispatcher) {
remoteOnHold = onHold if (remoteOnHold == onHold) return@launch
if (!onHold) { val direction: RtpTransceiver.RtpTransceiverDirection
onCallBecomeActive(this) if (onHold) {
} wasLocalOnHold = isLocalOnHold
val direction = if (onHold) { remoteOnHold = true
RtpTransceiver.RtpTransceiverDirection.INACTIVE isLocalOnHold = true
direction = RtpTransceiver.RtpTransceiverDirection.INACTIVE
} else { } else {
RtpTransceiver.RtpTransceiverDirection.SEND_RECV remoteOnHold = false
isLocalOnHold = wasLocalOnHold
onCallBecomeActive(this@WebRtcCall)
direction = RtpTransceiver.RtpTransceiverDirection.SEND_RECV
} }
for (transceiver in peerConnection?.transceivers ?: emptyList()) { for (transceiver in peerConnection?.transceivers ?: emptyList()) {
transceiver.direction = direction transceiver.direction = direction
} }
updateMuteStatus() updateMuteStatus()
listeners.forEach {
tryOrNull { it.onHoldUnhold() }
}
}
} }
fun muteCall(muted: Boolean) = synchronized(this) { fun muteCall(muted: Boolean) {
micMuted = muted micMuted = muted
updateMuteStatus() updateMuteStatus()
} }
fun enableVideo(enabled: Boolean) = synchronized(this) { fun enableVideo(enabled: Boolean) {
videoMuted = !enabled videoMuted = !enabled
updateMuteStatus() updateMuteStatus()
} }
fun canSwitchCamera(): Boolean = synchronized(this) { fun canSwitchCamera(): Boolean {
return availableCamera.size > 1 return availableCamera.size > 1
} }
private fun getOppositeCameraIfAny(): CameraProxy? = synchronized(this) { private fun getOppositeCameraIfAny(): CameraProxy? {
val currentCamera = cameraInUse ?: return null val currentCamera = cameraInUse ?: return null
return if (currentCamera.type == CameraType.FRONT) { return if (currentCamera.type == CameraType.FRONT) {
availableCamera.firstOrNull { it.type == CameraType.BACK } availableCamera.firstOrNull { it.type == CameraType.BACK }
@ -598,7 +597,7 @@ class WebRtcCall(val mxCall: MxCall,
} }
} }
fun switchCamera() = synchronized(this) { fun switchCamera() {
Timber.v("## VOIP switchCamera") Timber.v("## VOIP switchCamera")
if (mxCall.state is CallState.Connected && mxCall.isVideoCall) { if (mxCall.state is CallState.Connected && mxCall.isVideoCall) {
val oppositeCamera = getOppositeCameraIfAny() ?: return val oppositeCamera = getOppositeCameraIfAny() ?: return
@ -641,17 +640,18 @@ class WebRtcCall(val mxCall: MxCall,
} }
} }
fun currentCameraType(): CameraType? = synchronized(this) { fun currentCameraType(): CameraType? {
return cameraInUse?.type return cameraInUse?.type
} }
fun currentCaptureFormat(): CaptureFormat = synchronized(this) { fun currentCaptureFormat(): CaptureFormat {
return currentCaptureFormat return currentCaptureFormat
} }
private fun release() { private fun release() {
listeners.clear()
mxCall.removeListener(this) mxCall.removeListener(this)
timer.reset() timer.stop()
timer.tickListener = null timer.tickListener = null
videoCapturer?.stopCapture() videoCapturer?.stopCapture()
videoCapturer?.dispose() videoCapturer?.dispose()
@ -703,10 +703,11 @@ class WebRtcCall(val mxCall: MxCall,
} }
} }
fun endCall(originatedByMe: Boolean = true, reason: CallHangupContent.Reason? = null) = synchronized(this) { fun endCall(originatedByMe: Boolean = true, reason: CallHangupContent.Reason? = null) {
if (mxCall.state == CallState.Terminated) { if (mxCall.state == CallState.Terminated) {
return return
} }
val wasConnected = mxCall.state is CallState.Connected
mxCall.state = CallState.Terminated mxCall.state = CallState.Terminated
// Close tracks ASAP // Close tracks ASAP
localVideoTrack?.setEnabled(false) localVideoTrack?.setEnabled(false)
@ -715,12 +716,13 @@ class WebRtcCall(val mxCall: MxCall,
val cameraManager = context.getSystemService<CameraManager>()!! val cameraManager = context.getSystemService<CameraManager>()!!
cameraManager.unregisterAvailabilityCallback(cameraAvailabilityCallback) cameraManager.unregisterAvailabilityCallback(cameraAvailabilityCallback)
} }
GlobalScope.launch(dispatcher) {
release() release()
listeners.clear() }
onCallEnded(this) onCallEnded(this)
if (originatedByMe) { if (originatedByMe) {
// send hang up event // send hang up event
if (mxCall.state is CallState.Connected) { if (wasConnected) {
mxCall.hangUp(reason) mxCall.hangUp(reason)
} else { } else {
mxCall.reject() mxCall.reject()
@ -783,7 +785,7 @@ class WebRtcCall(val mxCall: MxCall,
Timber.i("Ignoring colliding negotiate event because we're impolite") Timber.i("Ignoring colliding negotiate event because we're impolite")
return@launch return@launch
} }
val prevOnHold = isLocalOnHold() val prevOnHold = computeIsLocalOnHold()
try { try {
val sdp = SessionDescription(type.asWebRTC(), sdpText) val sdp = SessionDescription(type.asWebRTC(), sdpText)
peerConnection.awaitSetRemoteDescription(sdp) peerConnection.awaitSetRemoteDescription(sdp)
@ -795,8 +797,10 @@ class WebRtcCall(val mxCall: MxCall,
} catch (failure: Throwable) { } catch (failure: Throwable) {
Timber.e(failure, "Failed to complete negotiation") Timber.e(failure, "Failed to complete negotiation")
} }
val nowOnHold = isLocalOnHold() val nowOnHold = computeIsLocalOnHold()
wasLocalOnHold = nowOnHold
if (prevOnHold != nowOnHold) { if (prevOnHold != nowOnHold) {
isLocalOnHold = nowOnHold
if (nowOnHold) { if (nowOnHold) {
timer.pause() timer.pause()
} else { } else {

View File

@ -297,11 +297,6 @@ class NotificationUtils @Inject constructor(private val context: Context,
.setLights(accentColor, 500, 500) .setLights(accentColor, 500, 500)
.setOngoing(true) .setOngoing(true)
// Compat: Display the incoming call notification on the lock screen
builder.priority = NotificationCompat.PRIORITY_HIGH
//
// val pendingIntent = stackBuilder.getPendingIntent(requestId, PendingIntent.FLAG_UPDATE_CURRENT)
val contentIntent = VectorCallActivity.newIntent( val contentIntent = VectorCallActivity.newIntent(
context = context, context = context,
@ -340,9 +335,11 @@ class NotificationUtils @Inject constructor(private val context: Context,
answerCallPendingIntent answerCallPendingIntent
) )
) )
if (fromBg) {
// Compat: Display the incoming call notification on the lock screen
builder.priority = NotificationCompat.PRIORITY_HIGH
builder.setFullScreenIntent(contentPendingIntent, true) builder.setFullScreenIntent(contentPendingIntent, true)
}
return builder.build() return builder.build()
} }