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 8d8eca8e0..0cbb6021b 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 @@ -539,11 +539,37 @@ class CallController( Log.d(TAG) { "Renegotiation offer from ${peerPubKey.take(8)}, sdpLength=${sdpOffer.length}" } scope.launch { - ps.session.setRemoteDescription(SessionDescription(SessionDescription.Type.OFFER, sdpOffer)) - ps.session.createAnswer { sdp -> - scope.launch { - callManager.sendRenegotiationAnswer(sdp.description, peerPubKey) + val signalingState = ps.session.getSignalingState() + + if (signalingState == PeerConnection.SignalingState.HAVE_LOCAL_OFFER) { + // Glare: both sides sent offers simultaneously. + // Use pubkey comparison as tiebreaker — higher pubkey wins. + val myPubKey = signerProvider().pubKey + if (myPubKey > peerPubKey) { + Log.d(TAG) { "Glare with ${peerPubKey.take(8)}: I win (my offer takes priority), ignoring remote offer" } + return@launch } + Log.d(TAG) { "Glare with ${peerPubKey.take(8)}: I lose, rolling back my offer" } + ps.session.rollback { + scope.launch { + applyRenegotiationOffer(ps, peerPubKey, sdpOffer) + } + } + } else { + applyRenegotiationOffer(ps, peerPubKey, sdpOffer) + } + } + } + + private fun applyRenegotiationOffer( + ps: PeerSessionState, + peerPubKey: HexKey, + sdpOffer: String, + ) { + ps.session.setRemoteDescription(SessionDescription(SessionDescription.Type.OFFER, sdpOffer)) + ps.session.createAnswer { sdp -> + scope.launch { + callManager.sendRenegotiationAnswer(sdp.description, peerPubKey) } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/call/WebRtcCallSession.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/call/WebRtcCallSession.kt index 44745c0ee..3379e2160 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/call/WebRtcCallSession.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/call/WebRtcCallSession.kt @@ -257,6 +257,32 @@ class WebRtcCallSession( fun getSignalingState(): PeerConnection.SignalingState? = peerConnection?.signalingState() + /** + * Rolls back a local offer so an incoming remote offer can be accepted + * (WebRTC offe glare handling). + */ + fun rollback(onDone: () -> Unit) { + val rollbackSdp = SessionDescription(SessionDescription.Type.ROLLBACK, "") + peerConnection?.setLocalDescription( + object : SdpObserver { + override fun onCreateSuccess(sdp: SessionDescription?) {} + + override fun onCreateFailure(error: String?) {} + + override fun onSetSuccess() { + Log.d(TAG) { "Rollback SUCCESS" } + onDone() + } + + override fun onSetFailure(error: String?) { + Log.e(TAG, "Rollback FAILED: $error") + error?.let { onError("Rollback failed: $it") } + } + }, + rollbackSdp, + ) + } + fun dispose() { peerConnection?.close() peerConnection?.dispose()