Call lifecycle
The happy path (1:1)
The glare-avoidance rule
Two peers both creating an offer at once is glare - it produces two
incompatible PeerConnection states. Viora avoids it with a strict rule
derived from join ordering:
The peer that receives a non-empty
room-joinedsnapshot is the offerer. The peer that receivesuser-joinedwaits for the offer.
In MainActivity.dispatchSignalingMessage:
"room-joined" -> if (!msg.peers.isNullOrEmpty()) {
remotePeerId = msg.peers[0].peerId
isOfferer = true
webRtcClient?.createOffer { sdp -> send("offer", targetPeerId = remotePeerId, sdp) }
}
"user-joined" -> {
remotePeerId = msg.peerId
isOfferer = false // they will offer to us
}
"offer" -> {
remotePeerId = msg.senderPeerId
isOfferer = false
webRtcClient?.handleRemoteOffer(sdp) { answer -> send("answer", targetPeerId = remotePeerId, answer) }
}isOfferer is later used by ICE recovery: only the
offerer re-offers on a dropped route.
ICE candidate buffering
Candidates can arrive before the remote description is set. WebRtcClient buffers
them in a CopyOnWriteArrayList and drains them once
setRemoteDescription succeeds:
fun addIceCandidate(candidate: IceCandidate) {
if (isRemoteDescriptionSet) peerConnection?.addIceCandidate(candidate)
else pendingIceCandidates.add(candidate)
}
// after setRemoteDescription onSetSuccess:
isRemoteDescriptionSet = true
pendingIceCandidates.forEach { peerConnection?.addIceCandidate(it) }
pendingIceCandidates.clear()Connection-state handling
PeerConnection.PeerConnectionState drives the CallScreen when:
| State | UI |
|---|---|
NEW / CONNECTING | “Connecting…” with the peer’s avatar |
CONNECTED | Full call surface; callConnectedAtMs set on first entry → the call timer starts |
DISCONNECTED | Call surface stays; iceRecoveryRunnable scheduled |
FAILED | ICE restart attempt (offerer only) |
CLOSED | Back to Home |
callConnectedAtMs is set exactly once (on the first CONNECTED) and cleared
only when the peer leaves - a brief DISCONNECTED blip doesn’t reset the timer.
Hang up
close() is @Synchronized, disposes every native object, removes all handler
callbacks, and detaches the audio sinks. Because a fresh WebRtcClient is built
immediately, the Home preview lights back up without an app restart.
Peer leaves first
If the other peer hangs up, the server sends user-left. MainActivity clears
remotePeerId, resets the remote* scalars, sets peerLeftState = true, and
CallScreen shows a calm “‹name› left” waiting state with the call still
technically open - the local user can wait for a re-join or hang up themselves.