mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-04-01 00:56:05 +00:00
9.1 KiB
9.1 KiB
Audio/Video Call Flow
Related spec: spec/services/calls.md
Overview
WebRTC-based audio and video calling in SimpleX Chat iOS. Calls are end-to-end encrypted with an additional shared key negotiated over the E2E encrypted SMP channel. The iOS app integrates with CallKit for native call UI (incoming call screen, lock screen integration) with a fallback mode for regions where CallKit is restricted (China). Call signaling (offer/answer/ICE candidates) is exchanged via SMP messages, not through a central signaling server.
Prerequisites
- Established direct contact connection (calls are 1:1 only, not available in groups)
- Microphone permission granted (audio calls)
- Camera permission granted (video calls)
- Network connectivity for WebRTC peer-to-peer or relay
Step-by-Step Processes
1. Initiate Call
- User opens a direct chat in
ChatView. - Taps the audio or video call button in the navigation bar.
CallControllerdetermines call type:CallType(media: .audio/.video, capabilities: CallCapabilities(encryption: true)).- If CallKit is enabled (
CallController.useCallKit()):CXStartCallActionis requested viaCXCallController.- CallKit reports the outgoing call.
provider(perform: CXStartCallAction)fulfills and reportsreportOutgoingCall(startedConnectingAt:).
- Calls
apiSendCallInvitation(contact:callType:):func apiSendCallInvitation(_ contact: Contact, _ callType: CallType) async throws - Sends
ChatCommand.apiSendCallInvitation(contact:callType:). - Core sends the call invitation to the contact via SMP.
ChatModel.shared.activeCallis set with the call state.
2. Receive Call
ChatReceiverreceivesChatEvent.callInvitation(callInvitation: RcvCallInvitation).RcvCallInvitationcontains:user,contact,callType,sharedKey,callUUID,callTs.- Processing in
processReceivedMsg:- Call invitation is stored in
chatModel.callInvitations.
- Call invitation is stored in
- If CallKit is enabled:
CXProvider.reportNewIncomingCallpresents the native iOS incoming call UI.- Works even on lock screen and in background.
- If CallKit is disabled (China / user preference):
IncomingCallViewis shown as an in-app overlay.SoundPlayerplays the ringtone.
- User chooses to accept or reject.
3. Accept Call
- Via CallKit: User swipes to accept on the native incoming call screen.
provider(perform: CXAnswerCallAction)is triggered.- Waits for chat to be started if needed (
waitUntilChatStarted(timeoutMs: 30_000)). callManager.answerIncomingCall(callUUID:)begins WebRTC setup.fulfillOnConnectis set -- the action is fulfilled only when WebRTC reaches connected state (required for audio/mic to work on lock screen).
- Via in-app UI: User taps "Accept" in
IncomingCallView.- Directly starts WebRTC setup.
4. Reject Call
- Via CallKit: User taps "Decline" on native UI.
provider(perform: CXEndCallAction)is triggered.callManager.endCall(callUUID:)cleans up.
- Via API:
apiRejectCall(contact:)sends rejection to peer. - Call invitation is removed from
chatModel.callInvitations.
5. WebRTC Setup (Signaling)
All signaling messages are exchanged via E2E encrypted SMP messages (no central signaling server).
Caller side:
WebRTCClientcreates aRTCPeerConnection.- Creates SDP offer.
- Calls
apiSendCallOffer(contact:rtcSession:rtcIceCandidates:media:capabilities:):func apiSendCallOffer(_ contact: Contact, _ rtcSession: String, _ rtcIceCandidates: String, media: CallMediaType, capabilities: CallCapabilities) async throws - Constructs
WebRTCCallOffer(callType:rtcSession:)and sends viaChatCommand.apiSendCallOffer. - Gathers ICE candidates and sends via
apiSendCallExtraInfo(contact:rtcIceCandidates:).
Callee side:
- Receives the offer via SMP.
WebRTCClientsets remote description from the offer.- Creates SDP answer.
- Calls
apiSendCallAnswer(contact:rtcSession:rtcIceCandidates:):func apiSendCallAnswer(_ contact: Contact, _ rtcSession: String, _ rtcIceCandidates: String) async throws - Constructs
WebRTCSession(rtcSession:rtcIceCandidates:)and sends. - Gathers and sends additional ICE candidates via
apiSendCallExtraInfo.
6. Media Streaming
- WebRTC peer connection transitions to connected state.
- If CallKit is used,
fulfillOnConnectaction is fulfilled (enables audio hardware). - Audio/video streams are active.
ActiveCallViewdisplays:- Remote video (full screen)
- Local video preview (picture-in-picture corner)
- Call controls: mute, speaker, camera toggle, end call
- Call duration timer
CallViewRenderersmanages WebRTC video rendering surfaces.- Call status updates are sent via
apiCallStatus(contact:status:).
7. Audio Routing
CallAudioDeviceManagerhandles audio device selection.- Options: earpiece (receiver), speaker, Bluetooth devices.
AudioDevicePickerprovides UI for device selection during call.- Uses
AVAudioSessionfor routing configuration.
8. End Call
- Either party taps "End" button.
- Calls
apiEndCall(contact:):func apiEndCall(_ contact: Contact) async throws - Sends
ChatCommand.apiEndCall(contact:)via SMP to notify peer. WebRTCClientcloses peer connection, releases media resources.- If CallKit:
CXEndCallActionis requested,provider(perform: CXEndCallAction)fulfills. ChatModel.shared.activeCallis cleared.- A
CICallItemViewevent item is added to the chat history (call duration, type).
9. CallKit-Free Mode
CallController.isInChinachecksSKStorefront().countryCode == "CHN".- If in China or user disabled CallKit (
callKitEnabledGroupDefault):useCallKit()returnsfalse. - Incoming calls use
IncomingCallViewoverlay instead of native CallKit UI. SoundPlayerhandles ringtone playback.- No lock-screen call answering; app must be in foreground or notified via push.
Data Structures
| Type | Location | Description |
|---|---|---|
CallType |
SimpleXChat/CallTypes.swift |
media: CallMediaType (.audio/.video), capabilities: CallCapabilities |
CallMediaType |
SimpleXChat/CallTypes.swift |
.audio or .video |
CallCapabilities |
SimpleXChat/CallTypes.swift |
encryption: Bool for E2E call encryption support |
RcvCallInvitation |
SimpleXChat/CallTypes.swift |
Incoming call: user, contact, callType, sharedKey, callUUID, callTs |
WebRTCCallOffer |
SimpleXChat/CallTypes.swift |
SDP offer with call type and WebRTC session data |
WebRTCSession |
SimpleXChat/CallTypes.swift |
rtcSession (SDP) and rtcIceCandidates (serialized) |
WebRTCExtraInfo |
SimpleXChat/CallTypes.swift |
Additional ICE candidates sent after initial offer/answer |
WebRTCCallStatus |
SimpleXChat/CallTypes.swift |
Call lifecycle states for status reporting |
CallMediaSource |
SimpleXChat/CallTypes.swift |
.mic, .camera, .screenAudio, .screenVideo, .unknown |
VideoCamera |
SimpleXChat/CallTypes.swift |
.user (front) or .environment (rear) camera |
Error Cases
| Error | Cause | Handling |
|---|---|---|
| Chat not ready on CallKit answer | App suspended, slow startup | waitUntilChatStarted with 30s timeout; action.fail() on timeout |
| Call invitation not found | Race condition between notification and event processing | justRefreshCallInvitations() retry |
| WebRTC peer connection failure | NAT traversal, network issues | Call ends with error status |
| CallKit action fail | Internal state mismatch | action.fail() called, call cleaned up |
| No camera/mic permission | User denied permissions | Permission request dialog shown |
Key Files
| File | Purpose |
|---|---|
Shared/Views/Call/CallController.swift |
CallKit integration, CXProvider delegate, PKPushRegistry, call lifecycle management |
Shared/Views/Call/CallManager.swift |
Call state management, starting/answering/ending calls |
Shared/Views/Call/WebRTCClient.swift |
WebRTC peer connection, SDP offer/answer, ICE candidate handling |
Shared/Views/Call/ActiveCallView.swift |
Active call UI: video renderers, controls, duration |
Shared/Views/Call/CallViewRenderers.swift |
WebRTC video rendering surfaces |
Shared/Views/Call/IncomingCallView.swift |
Non-CallKit incoming call overlay |
Shared/Views/Call/CallAudioDeviceManager.swift |
Audio routing: speaker, earpiece, Bluetooth |
Shared/Views/Call/AudioDevicePicker.swift |
Audio device selection UI |
Shared/Views/Call/SoundPlayer.swift |
Ringtone and call sound playback |
Shared/Views/Call/WebRTC.swift |
WebRTC configuration and utilities |
SimpleXChat/CallTypes.swift |
All call-related type definitions |
Shared/Model/SimpleXAPI.swift |
Call API functions: apiSendCallInvitation, apiSendCallOffer, apiSendCallAnswer, apiSendCallExtraInfo, apiEndCall, apiRejectCall, apiCallStatus |
Related Specifications
apps/ios/product/README.md-- Product overview: Calls capabilityapps/ios/product/flows/connection.md-- Calls require an established direct connection