diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/call/CallController.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/call/CallController.kt index a91ff58aa..9280010ef 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/call/CallController.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/call/CallController.kt @@ -35,6 +35,7 @@ import org.webrtc.IceCandidate import org.webrtc.MediaStream import org.webrtc.SessionDescription import java.util.UUID +import java.util.concurrent.CopyOnWriteArrayList class CallController( private val context: Context, @@ -47,6 +48,8 @@ class CallController( private val callFactory = WebRtcCallFactory() private var currentCallId: String? = null private var currentPeerPubKey: HexKey? = null + private var remoteDescriptionSet = false + private val pendingIceCandidates = CopyOnWriteArrayList() init { scope.launch { @@ -65,6 +68,8 @@ class CallController( val callId = UUID.randomUUID().toString() currentCallId = callId currentPeerPubKey = peerPubKey + remoteDescriptionSet = false + pendingIceCandidates.clear() createWebRtcSession() webRtcSession?.addAudioTrack() @@ -85,6 +90,8 @@ class CallController( currentCallId = state.callId currentPeerPubKey = state.callerPubKey + remoteDescriptionSet = false + pendingIceCandidates.clear() createWebRtcSession() webRtcSession?.addAudioTrack() @@ -95,6 +102,7 @@ class CallController( webRtcSession?.setRemoteDescription( SessionDescription(SessionDescription.Type.OFFER, sdpOffer), ) + flushPendingIceCandidates() webRtcSession?.createAnswer { sdp -> scope.launch { @@ -107,18 +115,31 @@ class CallController( webRtcSession?.setRemoteDescription( SessionDescription(SessionDescription.Type.ANSWER, sdpAnswer), ) + flushPendingIceCandidates() } fun onIceCandidateReceived(event: CallIceCandidateEvent) { val json = event.candidateJson() try { val candidate = parseIceCandidate(json) - webRtcSession?.addIceCandidate(candidate) + if (webRtcSession != null && remoteDescriptionSet) { + webRtcSession?.addIceCandidate(candidate) + } else { + pendingIceCandidates.add(candidate) + } } catch (_: Exception) { // Ignore malformed ICE candidates } } + private fun flushPendingIceCandidates() { + remoteDescriptionSet = true + val session = webRtcSession ?: return + val candidates = pendingIceCandidates.toList() + pendingIceCandidates.clear() + candidates.forEach { session.addIceCandidate(it) } + } + fun hangup() { scope.launch { callManager.hangup() } cleanup() @@ -130,6 +151,8 @@ class CallController( webRtcSession = null currentCallId = null currentPeerPubKey = null + remoteDescriptionSet = false + pendingIceCandidates.clear() } private fun createWebRtcSession() { @@ -147,7 +170,6 @@ class CallController( onRemoteStream = { _: MediaStream -> }, onDisconnected = { scope.launch { callManager.hangup() } - cleanup() }, ) webRtcSession?.initialize() diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/call/CallManager.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/call/CallManager.kt index 6527c80d9..0ad79e1f3 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/call/CallManager.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/call/CallManager.kt @@ -60,8 +60,11 @@ class CallManager( companion object { const val CALL_TIMEOUT_MS = 60_000L // 60 seconds ringing timeout const val ENDED_DISPLAY_MS = 2_000L // show "call ended" briefly before resetting + const val MAX_EVENT_AGE_SECONDS = 30L // discard signaling events older than this } + private fun isEventTooOld(event: Event): Boolean = TimeUtils.now() - event.createdAt > MAX_EVENT_AGE_SECONDS + suspend fun initiateCall( calleePubKey: HexKey, callType: CallType, @@ -194,6 +197,8 @@ class CallManager( } fun onSignalingEvent(event: Event) { + if (isEventTooOld(event)) return + when (event) { is CallOfferEvent -> onIncomingCallEvent(event) is CallAnswerEvent -> onCallAnswered(event)