From 0488245200988ef8f7423ef10498b313f0e357ea Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 3 Apr 2026 15:36:50 +0000 Subject: [PATCH] fix: send ICE candidates to all peers and exclude self from peer set Two bugs caused WebRTC group calls to get stuck in "Connecting": 1. CallController.onLocalIceCandidate() used currentPeerPubKey() which returns only the first peer via firstOrNull(). ICE candidates were gift-wrapped to only one peer; the others never received them. Fixed by iterating over all currentPeerPubKeys(). 2. CallManager.acceptCall() set Connecting.peerPubKeys to groupMembers which includes the local user's own pubkey. This caused currentPeerPubKey() to potentially return self, sending ICE candidates to oneself instead of the caller. Fixed by filtering out signer.pubKey from the peer set. https://claude.ai/code/session_01J5fJx9YbSBx1BsBMctiAm8 --- .../vitorpamplona/amethyst/service/call/CallController.kt | 8 +++++--- .../vitorpamplona/amethyst/commons/call/CallManager.kt | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) 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 25200c4ba..9b2ebbff7 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 @@ -491,13 +491,15 @@ class CallController( private fun onLocalIceCandidate(candidate: IceCandidate) { Log.d(TAG) { "Local ICE candidate: ${candidate.sdp.take(50)}" } val callId = callManager.currentCallId() ?: return - val peerPubKey = callManager.currentPeerPubKey() ?: return + val peerPubKeys = callManager.currentPeerPubKeys()?.takeIf { it.isNotEmpty() } ?: return val candidateJson = CallIceCandidateEvent.serializeCandidate(candidate.sdp, candidate.sdpMid, candidate.sdpMLineIndex) scope.launch { val signer = signerProvider() - val result = callFactory.createIceCandidate(candidateJson, peerPubKey, callId, signer) - publishWrap(result.wrap) + for (peerPubKey in peerPubKeys) { + val result = callFactory.createIceCandidate(candidateJson, peerPubKey, callId, signer) + publishWrap(result.wrap) + } } } 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 f3672bc6d..07abddbae 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 @@ -148,7 +148,7 @@ class CallManager( } Log.d("CallManager") { "acceptCall: callId=${current.callId}, transitioning to Connecting, sdpAnswerLength=${sdpAnswer.length}" } - _state.value = CallState.Connecting(current.callId, current.peerPubKeys(), current.callType) + _state.value = CallState.Connecting(current.callId, current.peerPubKeys() - signer.pubKey, current.callType) cancelTimeout() // Include all group members + self so other devices get notified too.