fix: buffer ICE candidates and discard stale signaling events
Two fixes for call connectivity: 1. ICE candidate buffering: Candidates arriving before the WebRTC session exists (callee ringing) or before remote description is set (caller waiting for answer) are now queued in pendingIceCandidates and flushed once setRemoteDescription is called. This was the root cause of calls getting stuck at "Connecting" — ICE candidates were silently dropped. 2. Stale event filter: All signaling events older than 30 seconds are discarded in CallManager.onSignalingEvent() to prevent old cached events from triggering phantom calls. Also: removed cleanup() from WebRTC onDisconnected callback to avoid double-cleanup race with the CallManager state observer. https://claude.ai/code/session_017hZm7yu7CzmcQgZGSaqSXS
This commit is contained in:
@@ -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<IceCandidate>()
|
||||
|
||||
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)
|
||||
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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user