fix: wire SDP renegotiation so video toggle works for remote peer

When toggling video mid-call, addVideoTrack triggers
onRenegotiationNeeded but the callback was empty — the remote peer
never received the updated SDP, so video never appeared on their side.

This wires up the full renegotiation flow using the existing
CallRenegotiateEvent (kind 25055):
- WebRtcCallSession: forward onRenegotiationNeeded to a callback
- CallController: create and send renegotiation offers, handle
  incoming renegotiation offers by creating answers
- CallManager: route CallRenegotiateEvent, allow CallAnswerEvent
  during Connected state for renegotiation answers

https://claude.ai/code/session_01WdmqktFBjXKB6PYRZ5b4FD
This commit is contained in:
Claude
2026-04-02 17:05:10 +00:00
parent ec99373ae3
commit fe1a009fb4
3 changed files with 86 additions and 6 deletions
@@ -30,6 +30,7 @@ import com.vitorpamplona.quartz.nipACWebRtcCalls.events.CallHangupEvent
import com.vitorpamplona.quartz.nipACWebRtcCalls.events.CallIceCandidateEvent
import com.vitorpamplona.quartz.nipACWebRtcCalls.events.CallOfferEvent
import com.vitorpamplona.quartz.nipACWebRtcCalls.events.CallRejectEvent
import com.vitorpamplona.quartz.nipACWebRtcCalls.events.CallRenegotiateEvent
import com.vitorpamplona.quartz.nipACWebRtcCalls.tags.CallType
import com.vitorpamplona.quartz.utils.Log
import com.vitorpamplona.quartz.utils.TimeUtils
@@ -54,6 +55,7 @@ class CallManager(
var onAnswerReceived: ((CallAnswerEvent) -> Unit)? = null
var onIceCandidateReceived: ((CallIceCandidateEvent) -> Unit)? = null
var onRenegotiationOfferReceived: ((CallRenegotiateEvent) -> Unit)? = null
private var timeoutJob: Job? = null
private var resetJob: Job? = null
@@ -119,12 +121,26 @@ class CallManager(
fun onCallAnswered(event: CallAnswerEvent) {
val current = _state.value
if (current !is CallState.Offering) return
if (event.callId() != current.callId) return
val callId = event.callId()
_state.value = CallState.Connecting(current.callId, current.peerPubKey, current.callType)
cancelTimeout()
onAnswerReceived?.invoke(event)
when (current) {
is CallState.Offering -> {
if (callId != current.callId) return
_state.value = CallState.Connecting(current.callId, current.peerPubKey, current.callType)
cancelTimeout()
onAnswerReceived?.invoke(event)
}
is CallState.Connected -> {
// Renegotiation answer (e.g., peer accepted our video upgrade offer)
if (callId != current.callId) return
onAnswerReceived?.invoke(event)
}
else -> {
return
}
}
}
fun onCallRejected(event: CallRejectEvent) {
@@ -139,6 +155,33 @@ class CallManager(
onIceCandidateReceived?.invoke(event)
}
fun onRenegotiate(event: CallRenegotiateEvent) {
val current = _state.value
val callId = event.callId()
val currentCallId =
when (current) {
is CallState.Connecting -> current.callId
is CallState.Connected -> current.callId
else -> return
}
if (callId != currentCallId) return
onRenegotiationOfferReceived?.invoke(event)
}
suspend fun sendRenegotiation(sdpOffer: String) {
val callId = currentCallId() ?: return
val peerPubKey = currentPeerPubKey() ?: return
val result = factory.createRenegotiate(sdpOffer, peerPubKey, callId, signer)
publishEvent(result.wrap)
}
suspend fun sendRenegotiationAnswer(sdpAnswer: String) {
val callId = currentCallId() ?: return
val peerPubKey = currentPeerPubKey() ?: return
val result = factory.createCallAnswer(sdpAnswer, peerPubKey, callId, signer)
publishEvent(result.wrap)
}
fun onPeerConnected() {
val current = _state.value
if (current !is CallState.Connecting) return
@@ -213,6 +256,7 @@ class CallManager(
is CallRejectEvent -> onCallRejected(event)
is CallHangupEvent -> onPeerHangup(event)
is CallIceCandidateEvent -> onIceCandidate(event)
is CallRenegotiateEvent -> onRenegotiate(event)
}
}