ui: reconnect call (#7711)

* Update Call.hs

* Update Internal.hs

* add reconnecting string

* Update WebRTC.kt

* desktop: show reconnecting state, keep call duration

* android keep connection time correct wiith adding reconnecting

* Update CallTypes.swift add reconnecting status on IOS

* Update WebRTC.swift add reconnecting

* add reconnecting and make sure call duration is correct

* Update call.ts

* core: regenerate call.js from call.ts

* Update Call.hs revert call status change

minimization

* Update Internal.hs rever to minimize changes

* Update CallTypes.swift minimize changes

* Update WebRTC.kt minimize changes

* Update CallView.desktop.kt desktop shows reconnecting without sending to core

* Update CallView.android.kt show reconnecting without sending it to the core

* Update ActiveCallView.swift show reconnecting to ui without sending to core

* Update CallView.desktop.kt add reconnecting sound on reconnect

* Update CallView.android.kt add reconnecting sound

* Update ActiveCallView.swift play reconnecting sound when reconnecting

* Update RecAndPlay.desktop.kt

* rebuild

* refactor

* build

* android, desktop: restart ICE to reconnect calls

* ios: restart ICE to reconnect calls

* webrtc: add call reconnection e2e tests

* android, desktop: fix lost call commands

---------

Co-authored-by: GuavaGoat <320862384+GuavaGoat@users.noreply.github.com>
Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
Co-authored-by: Evgeny @ SimpleX Chat <259188159+evgeny-simplex@users.noreply.github.com>
This commit is contained in:
authored and GitHub committed 2026-10-19 12:17:16 +01:00
1 parent 634fa5ecff
commit 2ea1ec0d50
27 files changed
+1247 -136

No files matched your search

+11 -1
View File
@@ -99,6 +99,7 @@ Both platforms share a common signaling flow through the Haskell core API.
```
Outgoing: WaitCapabilities -> InvitationSent -> OfferSent -> AnswerReceived -> Negotiated -> Connected -> Ended
Incoming: InvitationAccepted -> OfferReceived -> Negotiated -> Connected -> Ended
Reconnection: Connected -> Reconnecting -> Connected | Ended
```
| State | Description |
@@ -111,8 +112,17 @@ Incoming: InvitationAccepted -> OfferReceived -> Negotiated -> Connected -> Ende
| `AnswerReceived` | Caller received SDP answer |
| `Negotiated` | ICE negotiation complete |
| `Connected` | Media flowing |
| `Reconnecting` | Connection lost, waiting for ICE restart |
| `Ended` | Call terminated |
### 3.1 Reconnection
1. A connected call loses its WebRTC connection, e.g. when the device switches between Wi-Fi and mobile network.
2. The WebRTC engine reports `"reconnecting"` connection state; the app shows "reconnecting…" and plays the connecting sound. This state is not reported to the core, and `connectedAt` is kept, so call duration continues.
3. The callee's WebRTC engine (the party that sent the initial offer) restarts ICE every 10 seconds, sending the new offer in `WCallResponse.Ice` with `session` via `apiSendCallExtraInfo`. The caller answers the same way.
4. When the connection is restored, the call returns to `Connected` and the sound stops.
5. If the connection is not restored in 30 seconds, the call ends as disconnected. Clients without ICE restart support ignore the restart offer, so the call ends in this case.
---
## 4. Ending a Call
@@ -184,7 +194,7 @@ Incoming: InvitationAccepted -> OfferReceived -> Negotiated -> Connected -> Ende
| `apiRejectCall(rh, contact)` | Reject incoming call |
| `apiSendCallOffer(rh, contact, rtcSession, rtcIceCandidates, media, capabilities)` | Send SDP offer |
| `apiSendCallAnswer(rh, contact, rtcSession, rtcIceCandidates)` | Send SDP answer |
| `apiSendCallExtraInfo(rh, contact, rtcIceCandidates)` | Send additional ICE candidates |
| `apiSendCallExtraInfo(rh, contact, rtcSession, rtcIceCandidates)` | Send additional ICE candidates, or ICE restart offer/answer |
| `apiEndCall(rh, contact)` | End active call |
| `apiCallStatus(rh, contact, status)` | Report WebRTC connection status |
+5 -4
View File
@@ -71,8 +71,8 @@ Sound: `SoundPlayer.start()` plays ringtone while banner is visible (unless call
| 1. Capabilities | `WCallResponse.Capabilities` | Local capabilities reported; `apiSendCallInvitation()` called |
| 2. Offer | `WCallResponse.Offer` | SDP offer + ICE candidates sent via `apiSendCallOffer()` |
| 3. Answer | `WCallResponse.Answer` | SDP answer + ICE candidates sent via `apiSendCallAnswer()` |
| 4. ICE | `WCallResponse.Ice` | Additional ICE candidates exchanged via `apiSendCallExtraInfo()` |
| 5. Connection | `WCallResponse.Connection` | WebRTC connection state changes; `CallState.Connected` set on success |
| 4. ICE | `WCallResponse.Ice` | Additional ICE candidates, and ICE restart offer/answer in `session`, exchanged via `apiSendCallExtraInfo()` |
| 5. Connection | `WCallResponse.Connection` | WebRTC connection state changes; `CallState.Connected` set on success, `CallState.Reconnecting` when a connected call loses connection |
| 6. Connected | `WCallResponse.Connected` | Connection info (relay/direct) stored in `call.connectionInfo` |
| 7. PeerMedia | `WCallResponse.PeerMedia` | Remote party media source changes (mic, camera, screen) |
| 8. Media control | `WCallCommand.Media` | Toggle local media sources (mic, camera, screen audio/video) |
@@ -90,14 +90,15 @@ Sound: `SoundPlayer.start()` plays ringtone while banner is visible (unless call
| `OfferReceived` | Callee received SDP offer |
| `AnswerReceived` | Caller received SDP answer |
| `Negotiated` | ICE negotiation complete |
| `Connected` | WebRTC media flowing; `connectedAt` timestamp set |
| `Connected` | WebRTC media flowing; `connectedAt` timestamp set on the first connection |
| `Reconnecting` | Connection lost; "reconnecting…" shown while ICE restarts, call ends after 30 seconds without connection |
| `Ended` | Call terminated |
### Call Sounds
| Sound | Trigger |
|---|---|
| Connecting sound | `CallSoundsPlayer.startConnectingCallSound()` after invitation sent |
| Connecting sound | `CallSoundsPlayer.startConnectingCallSound()` after invitation sent, and while reconnecting |
| In-call sound | `CallSoundsPlayer.startInCallSound()` when delivery receipt received |
| Ringtone | `SoundPlayer.start()` for incoming calls |
| End vibration | `CallSoundsPlayer.vibrate()` on call end (if was connected) |