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
}
val heldCalls = connectedCalls.filter {
it.isLocalOnHold() || it.remoteOnHold
it.isLocalOnHold || it.remoteOnHold
}
if (connectedCalls.size == 1) {
if (heldCalls.size == 1) {

View file

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

View file

@ -16,82 +16,45 @@
package im.vector.app.core.utils
import android.os.Handler
import android.os.SystemClock
import io.reactivex.Flowable
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) {
private var startTimestamp: Long = 0
private var delayTime: Long = 0
private var lastPauseTimestamp: Long = 0
private var isRunning: Boolean = false
private val elapsedTime: AtomicLong = AtomicLong()
private val resumed: AtomicBoolean = AtomicBoolean(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
private val tickHandler: Handler = Handler()
private val tickSelector = Runnable {
if (isRunning) {
tickListener?.onTick(time)
startTicking()
}
fun elapsedTime(): Long{
return elapsedTime.get()
}
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() {
if (isRunning) {
lastPauseTimestamp = SystemClock.elapsedRealtime()
isRunning = false
stopTicking()
}
resumed.set(false)
}
/**
* Resume the timer
*/
fun resume() {
if (!isRunning) {
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() {
tickHandler.removeCallbacksAndMessages(null)
val time = time
val remainingTimeInInterval = intervalInMs - time % intervalInMs
tickHandler.postDelayed(tickSelector, remainingTimeInInterval)
resumed.set(true)
}
private fun stopTicking() {
tickHandler.removeCallbacksAndMessages(null)
fun stop() {
disposable.dispose()
}
interface TickListener {
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 io.reactivex.android.schedulers.AndroidSchedulers
import kotlinx.parcelize.Parcelize
import okhttp3.internal.concurrent.formatDuration
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.MxCallDetail
@ -166,7 +165,8 @@ class VectorCallActivity : VectorBaseActivity<ActivityCallBinding>(), CallContro
views.smallIsHeldIcon.isVisible = false
when (callState) {
is CallState.Idle,
is CallState.Dialing -> {
is CallState.CreateOffer,
is CallState.Dialing -> {
views.callVideoGroup.isInvisible = true
views.callInfoGroup.isVisible = true
views.callStatusText.setText(R.string.call_ring)
@ -187,9 +187,9 @@ class VectorCallActivity : VectorBaseActivity<ActivityCallBinding>(), CallContro
views.callConnectingProgress.isVisible = true
configureCallInfo(state)
}
is CallState.Connected -> {
is CallState.Connected -> {
if (callState.iceConnectionState == MxPeerConnectionState.CONNECTED) {
if (state.isLocalOnHold) {
if (state.isLocalOnHold || state.isRemoteOnHold) {
views.smallIsHeldIcon.isVisible = true
views.callVideoGroup.isInvisible = true
views.callInfoGroup.isVisible = true
@ -226,13 +226,11 @@ class VectorCallActivity : VectorBaseActivity<ActivityCallBinding>(), CallContro
views.callStatusText.setText(R.string.call_connecting)
views.callConnectingProgress.isVisible = true
}
// ensure all attached?
callManager.getCallById(callArgs.callId)?.attachViewRenderers(views.pipRenderer, views.fullscreenRenderer, null)
}
is CallState.Terminated -> {
is CallState.Terminated -> {
finish()
}
null -> {
null -> {
}
}
}
@ -255,7 +253,7 @@ class VectorCallActivity : VectorBaseActivity<ActivityCallBinding>(), CallContro
val colorFilter = ContextCompat.getColor(this, R.color.bg_call_screen)
avatarRenderer.renderBlur(state.otherKnownCallInfo.otherUserItem, views.otherKnownCallAvatarView, sampling = 20, rounded = false, colorFilter = colorFilter)
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() {
setState {
copy(
isLocalOnHold = call?.isLocalOnHold() ?: false,
isLocalOnHold = call?.isLocalOnHold ?: false,
isRemoteOnHold = call?.remoteOnHold ?: false
)
}
@ -179,7 +179,7 @@ class VectorCallViewModel @AssistedInject constructor(
callState = Success(webRtcCall.mxCall.state),
callInfo = VectorCallViewState.CallInfo(callId, item),
soundDevice = currentSoundDevice,
isLocalOnHold = webRtcCall.isLocalOnHold(),
isLocalOnHold = webRtcCall.isLocalOnHold,
isRemoteOnHold = webRtcCall.remoteOnHold,
availableSoundDevices = callManager.callAudioManager.getAvailableSoundDevices(),
isFrontCamera = webRtcCall.currentCameraType() == CameraType.FRONT,

View file

@ -135,7 +135,7 @@ class WebRtcCall(val mxCall: MxCall,
private var currentCaptureFormat: CaptureFormat = CaptureFormat.HD
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 {
override fun onTick(milliseconds: Long) {
val formattedDuration = formatDuration(Duration.ofMillis(milliseconds))
@ -146,7 +146,6 @@ class WebRtcCall(val mxCall: MxCall,
}
}
// Mute status
var micMuted = false
private set
@ -154,6 +153,11 @@ class WebRtcCall(val mxCall: MxCall,
private set
var remoteOnHold = false
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
@ -228,7 +232,7 @@ class WebRtcCall(val mxCall: MxCall,
fun formattedDuration(): String {
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?) {
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 ->
// Start background service with notification
CallService.onPendingCall(
context = context,
callId = mxCall.callId)
}
GlobalScope.launch(dispatcher) {
when (mode) {
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")
// currentCall?.localMediaStream?.let { currentCall?.peerConnection?.removeStream(it) }
if (renderers.isNullOrEmpty()) {
// remove all sinks
localSurfaceRenderers.forEach {
@ -545,7 +536,7 @@ class WebRtcCall(val mxCall: MxCall,
* rather than 'sendonly')
* @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
var callOnHold = true
// 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
}
fun updateRemoteOnHold(onHold: Boolean) = synchronized(this) {
if (remoteOnHold == onHold) return
remoteOnHold = onHold
if (!onHold) {
onCallBecomeActive(this)
fun updateRemoteOnHold(onHold: Boolean) {
GlobalScope.launch(dispatcher) {
if (remoteOnHold == onHold) return@launch
val direction: RtpTransceiver.RtpTransceiverDirection
if (onHold) {
wasLocalOnHold = isLocalOnHold
remoteOnHold = true
isLocalOnHold = true
direction = RtpTransceiver.RtpTransceiverDirection.INACTIVE
} else {
remoteOnHold = false
isLocalOnHold = wasLocalOnHold
onCallBecomeActive(this@WebRtcCall)
direction = RtpTransceiver.RtpTransceiverDirection.SEND_RECV
}
for (transceiver in peerConnection?.transceivers ?: emptyList()) {
transceiver.direction = direction
}
updateMuteStatus()
listeners.forEach {
tryOrNull { it.onHoldUnhold() }
}
}
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) = synchronized(this) {
fun muteCall(muted: Boolean) {
micMuted = muted
updateMuteStatus()
}
fun enableVideo(enabled: Boolean) = synchronized(this) {
fun enableVideo(enabled: Boolean) {
videoMuted = !enabled
updateMuteStatus()
}
fun canSwitchCamera(): Boolean = synchronized(this) {
fun canSwitchCamera(): Boolean {
return availableCamera.size > 1
}
private fun getOppositeCameraIfAny(): CameraProxy? = synchronized(this) {
private fun getOppositeCameraIfAny(): CameraProxy? {
val currentCamera = cameraInUse ?: return null
return if (currentCamera.type == CameraType.FRONT) {
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")
if (mxCall.state is CallState.Connected && mxCall.isVideoCall) {
val oppositeCamera = getOppositeCameraIfAny() ?: return
@ -641,17 +640,18 @@ class WebRtcCall(val mxCall: MxCall,
}
}
fun currentCameraType(): CameraType? = synchronized(this) {
fun currentCameraType(): CameraType? {
return cameraInUse?.type
}
fun currentCaptureFormat(): CaptureFormat = synchronized(this) {
fun currentCaptureFormat(): CaptureFormat {
return currentCaptureFormat
}
private fun release() {
listeners.clear()
mxCall.removeListener(this)
timer.reset()
timer.stop()
timer.tickListener = null
videoCapturer?.stopCapture()
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) {
return
}
val wasConnected = mxCall.state is CallState.Connected
mxCall.state = CallState.Terminated
// Close tracks ASAP
localVideoTrack?.setEnabled(false)
@ -715,12 +716,13 @@ class WebRtcCall(val mxCall: MxCall,
val cameraManager = context.getSystemService<CameraManager>()!!
cameraManager.unregisterAvailabilityCallback(cameraAvailabilityCallback)
}
release()
listeners.clear()
GlobalScope.launch(dispatcher) {
release()
}
onCallEnded(this)
if (originatedByMe) {
// send hang up event
if (mxCall.state is CallState.Connected) {
if (wasConnected) {
mxCall.hangUp(reason)
} else {
mxCall.reject()
@ -783,7 +785,7 @@ class WebRtcCall(val mxCall: MxCall,
Timber.i("Ignoring colliding negotiate event because we're impolite")
return@launch
}
val prevOnHold = isLocalOnHold()
val prevOnHold = computeIsLocalOnHold()
try {
val sdp = SessionDescription(type.asWebRTC(), sdpText)
peerConnection.awaitSetRemoteDescription(sdp)
@ -795,8 +797,10 @@ class WebRtcCall(val mxCall: MxCall,
} catch (failure: Throwable) {
Timber.e(failure, "Failed to complete negotiation")
}
val nowOnHold = isLocalOnHold()
val nowOnHold = computeIsLocalOnHold()
wasLocalOnHold = nowOnHold
if (prevOnHold != nowOnHold) {
isLocalOnHold = nowOnHold
if (nowOnHold) {
timer.pause()
} else {

View file

@ -297,11 +297,6 @@ class NotificationUtils @Inject constructor(private val context: Context,
.setLights(accentColor, 500, 500)
.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(
context = context,
@ -340,9 +335,11 @@ class NotificationUtils @Inject constructor(private val context: Context,
answerCallPendingIntent
)
)
builder.setFullScreenIntent(contentPendingIntent, true)
if (fromBg) {
// Compat: Display the incoming call notification on the lock screen
builder.priority = NotificationCompat.PRIORITY_HIGH
builder.setFullScreenIntent(contentPendingIntent, true)
}
return builder.build()
}