fix: handle WebRTC offer glare during renegotiation
When two peers simultaneously trigger renegotiation (e.g., both enable video), both create local offers and enter HAVE_LOCAL_OFFER state. When a remote offer arrives in this state, setRemoteDescription fails with "Called in wrong state: have-local-offer". Fix by implementing standard WebRTC glare handling: use pubkey comparison as a tiebreaker (higher pubkey wins). The losing peer rolls back their local offer before accepting the remote one. https://claude.ai/code/session_01XLbnNVx3GDhHrPZKPXfFdD
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user