18 KiB
WebRTC Calling Service
Table of Contents
- Overview
- Call State Machine
- Android Implementation
- Desktop Implementation
- Common Call API
- IncomingCallAlertView
- Source Files
Executive Summary
WebRTC calling in SimpleX Chat operates over SMP (SimpleX Messaging Protocol) for signaling, with platform-specific WebRTC media implementations. Android uses a WebView-based approach with a dedicated CallActivity and foreground CallService, while Desktop runs native WebRTC in-process through webrtc-java. Both platforms share a common CallManager for call lifecycle and a CallState enum for state tracking. Both use the same WCallCommand/WCallResponse protocol: Android exchanges it as JSON with the WebRTC JavaScript layer, Desktop passes it to NativeCallEngine.
1. Overview
Call signaling uses the same SMP protocol on all platforms -- call invitations, offers, answers, ICE candidates, and status updates flow through the chat backend via API commands. The WebRTC media plane, however, is implemented differently per platform:
- Android: WebView loads
call.htmlfrom bundled assets; a@JavascriptInterfacebridge (WebRTCInterface) forwards JSON messages between Kotlin and JavaScript. - Desktop:
NativeCallEnginehandles the same commands with webrtc-java (native libwebrtc), interoperating withcall.tsand iOS peers: same transceiver layout, VP8, frame encryption format and SDP/ICE encoding.
Both platforms share the CallManager class (119 lines), which orchestrates incoming call acceptance, call ending, and notification management.
2. Call State Machine
Defined in WebRTC.kt:
enum class CallState {
WaitCapabilities, // Call initiated, waiting for local WebRTC capabilities
InvitationSent, // Invitation sent to peer via SMP
InvitationAccepted, // Peer's invitation accepted locally
OfferSent, // SDP offer sent to peer
OfferReceived, // SDP offer received from peer
AnswerReceived, // SDP answer received from peer
Negotiated, // ICE negotiation in progress
Connected, // Media flowing
Reconnecting, // Connection lost, waiting for ICE to recover
Ended; // Call terminated
}
Outgoing call flow: WaitCapabilities -> InvitationSent -> OfferSent -> AnswerReceived -> Negotiated -> Connected -> Ended
Incoming call flow: InvitationAccepted -> OfferReceived -> Negotiated -> Connected -> Ended
State transitions are driven by WCallResponse messages from the WebRTC layer. Each transition typically triggers a corresponding API command (e.g., apiSendCallInvitation, apiSendCallOffer).
Reconnection: Connected -> Reconnecting -> Connected or Ended. When a connected call loses its WebRTC connection, call.js reports "reconnecting" connection state instead of ending the call. The app sets CallState.Reconnecting and plays the connecting sound; this state is not sent to the core with apiCallStatus, so the call item duration is not affected, and connectedAt is kept. The party that sent the initial offer (callee) restarts ICE every 10 seconds until reconnected: the restart offer and the answer are sent as WCallResponse.Ice / WCallCommand.Ice with session, via apiSendCallExtraInfo (x.call.extra with rtcSession). If the connection is not restored in 30 seconds, call.js reports "disconnected" and ends the call.
3. Android Implementation
3.1 CallActivity.kt (468 lines)
A dedicated ComponentActivity that hosts the call UI. Key responsibilities:
- Intent handling (line 65): On
AcceptCallActionintent, looks up the matchingRcvCallInvitationand callscallManager.acceptIncomingCall(). - Lock screen support (line 161):
unlockForIncomingCall()usessetShowWhenLocked(true)/setTurnScreenOn(true)on API 27+, falls back to window flags on older versions.lockAfterIncomingCall()reverses these settings. - Picture-in-Picture (line 100):
setPipParams()configures PiP aspect ratio and source rect hint. On Android 12+ (Build.VERSION_CODES.S), auto-enter PiP is enabled for video calls.onPictureInPictureModeChangedtogglesactiveCallViewIsCollapsedand sends aWCallCommand.Layoutcommand. - Permission checks (line 123): Checks
RECORD_AUDIOand conditionallyCAMERApermissions. - Service binding (line 182): Binds to
CallServiceas a workaround for Android 12 background activity launch restrictions. - CallActivityView composable (line 209): Renders
ActiveCallView()when permissions are granted and a call is active. ShowsCallPermissionsViewwhen permissions are needed. ShowsIncomingCallLockScreenAlertfor incoming calls on the lock screen.
3.2 CallService.kt (207 lines)
An Android foreground Service that keeps the call alive when the app is backgrounded:
- Foreground notification (line 131): Shows contact name (respecting
NotificationPreviewMode), call type (audio/video), a chronometer when connected, and an "End call" action button. - WakeLock (line 66): Acquires
PARTIAL_WAKE_LOCKto prevent CPU sleep during calls. - Notification channel (line 121): Creates
CALL_NOTIFICATION_CHANNEL_IDwithIMPORTANCE_DEFAULT. - Foreground service type (line 100): Uses
MEDIA_PLAYBACK | MICROPHONE(+CAMERAfor video) on API 30+,REMOTE_MESSAGINGon API 34+ when no active call. - Binder (line 158):
CallServiceBinderallowsCallActivityto callupdateNotification()when call state changes. - CallActionReceiver (line 170):
BroadcastReceiverthat handles theEndCallActionfrom the notification.
3.3 CallView.android.kt (891 lines)
The actual platform implementation of ActiveCallView() and supporting composables:
- ActiveCallState (line 74): Manages proximity lock (screen-off wake lock),
CallAudioDeviceManagerfor audio routing (earpiece/speaker/bluetooth),CallSoundsPlayerfor ringtones and vibration. ImplementsCloseableto clean up resources on call end. - ActiveCallView (line 114): Renders
WebRTCViewplusActiveCallOverlay. HandlesWCallResponsemessages and dispatches corresponding API calls. Manages volume control stream (STREAM_VOICE_CALL), screen keep-on, and call command lifecycle. - WebRTCView (line 691): Creates/reuses a static
WebViewviaAndroidView. ConfiguresWebViewAssetLoaderfor local asset loading. Sets upWebRTCInterfaceJavaScript bridge. Loadsfile:android_asset/www/android/call.html. ProcessesWCallCommandqueue by evaluatingprocessCommand()JavaScript. - ActiveCallOverlayLayout (line 329): Full overlay with mic toggle, speaker/device selector, end call, video toggle, and camera flip buttons. Adapts layout for video vs audio calls.
- CallPermissionsView (line 569): Handles runtime permission requests for microphone and camera with a fallback to settings if the system dialog is not shown.
3.4 ActiveCallState
ActiveCallState (line 74 of CallView.android.kt):
| Component | Purpose |
|---|---|
proximityLock |
PROXIMITY_SCREEN_OFF_WAKE_LOCK -- turns screen off when phone is held to ear |
callAudioDeviceManager |
Manages audio routing between earpiece, speaker, Bluetooth, wired headset |
CallSoundsPlayer |
Plays connecting/ringing sounds and vibration patterns |
wasConnected |
Tracks if call ever connected (for end-of-call vibration) |
close() |
Stops sounds, vibrates on disconnect, releases proximity lock, clears audio manager overrides |
4. Desktop Implementation
4.1 CallView.desktop.kt and NativeCallEngine.kt
CallView.desktop.kt shows the call in the main window; NativeCallEngine.kt runs WebRTC in-process:
- ActiveCallView (line 59): Handles
WCallResponsemessages with the same state machine as Android. ACapabilitieserror shows the engine error alert and ends the call; a call without a microphone proceeds without sending audio. - NativeCallController (line 172): Creates the engine for the call, drains the
chatModel.callCommandqueue into it, and closes it on dispose. - ActiveCallLayout (line 203): Full-window call view over the app: remote screen or camera video, local camera preview, state and encryption info, and controls for mic, peer audio, camera, screen share (screen or window picker), a devices menu (microphone, speaker and camera) and hang up. The back button collapses the view to the floating call avatar, which expands it again.
- NativeCallEngine (line 29): Processes
WCallCommandon a single thread and reportsWCallResponsewithcall.tssemantics: four transceivers (mic, camera, screen audio, screen video), VP8 preferred, ICE candidates batched as incall.ts, connection state and peer media changes reported. - Audio sending (line 330): Device-captured audio reaches every audio sender, so the mic is muted and the screen audio sender silenced by deactivating the sender's encoding, which stops RTP.
- Peer media (line 580): A received source is reported muted after 3 seconds without received bytes, as in
call.ts. - Reconnection (line 477): As in
call.ts, a connected call that loses its connection is reported as"reconnecting". The party that sent the initial offer restarts ICE every 10 seconds and sends the restart offer inWCallResponse.Icewithsession; the other party answers it the same way. The call ends as"disconnected"if it does not reconnect in 30 seconds. - Frame encryption (
CallFrameCipher.kt): AES-256-GCM in thecall.tsframe layout, applied as encoded-frame transforms on senders and receivers. - Audio devices (
CallAudioDevices.kt): The engine uses webrtc-java'sCubebAudioDeviceModule, or the dummy audio layer when it cannot be created (no audio backend or device).AudioDeviceChoiceselects the saved microphone and speaker (desktopCallMic,desktopCallSpeaker: ID and name) by ID, then by name if exactly one device has it, else the system default, which follows the OS default. The saved device stays chosen while absent and is selected again when listed. While it is missing, the module uses the system default, or no device whenprivacyCallAudioFallbackis off; the engine reports each change asAudioDeviceNotice, shown as a toast.
5. Common Call API
Defined in SimpleXAPI.kt:
| Function | Line | Description |
|---|---|---|
apiGetCallInvitations |
L1842 | Retrieve pending call invitations from the backend |
apiSendCallInvitation |
L1849 | Send call invitation to a contact with CallType |
apiRejectCall |
L1854 | Reject an incoming call |
apiSendCallOffer |
L1859 | Send SDP offer with ICE candidates and capabilities |
apiSendCallAnswer |
L1866 | Send SDP answer with ICE candidates |
apiSendCallExtraInfo |
L2156 | Send additional ICE candidates discovered after initial exchange, or ICE restart offer/answer in rtcSession |
apiEndCall |
L1878 | Terminate a call |
apiCallStatus |
L1883 | Report WebRTC connection status to the backend |
All functions send commands via sendCmd() to the chat core and return Boolean success status (except apiGetCallInvitations which returns List<RcvCallInvitation>).
6. IncomingCallAlertView
IncomingCallAlertView.kt (128 lines)
An in-app notification banner shown when a call invitation arrives while the app is in the foreground:
- IncomingCallAlertView (line 27): Starts
SoundPlayerfor the ringtone (suppressed if already in a call view). ShowsIncomingCallAlertLayout. - IncomingCallAlertLayout (line 49): Colored banner with
ProfilePreviewof the caller, call type icon (audio/video), and three action buttons: Reject (red), Ignore (primary), Accept (green). - IncomingCallInfo (line 74): Shows the user profile image (for multi-user), call media type icon, and call type text (encrypted/unencrypted audio/video).
7. Source Files
| File | Path | Lines | Description |
|---|---|---|---|
CallView.kt |
common/src/commonMain/.../views/call/CallView.kt |
28 | expect fun ActiveCallView(), delivery receipt waiting |
CallView.android.kt |
common/src/androidMain/.../views/call/CallView.android.kt |
891 | Android WebView WebRTC, overlay, permissions |
CallView.desktop.kt |
common/src/desktopMain/.../views/call/CallView.desktop.kt |
462 | Desktop call view and state machine |
NativeCallEngine.kt |
common/src/desktopMain/.../views/call/NativeCallEngine.kt |
764 | Desktop native WebRTC engine (webrtc-java) |
CallAudioDevices.kt |
common/src/desktopMain/.../views/call/CallAudioDevices.kt |
62 | Desktop microphone and speaker choice and fallback notices |
CallActivity.kt |
android/src/main/java/.../views/call/CallActivity.kt |
472 | Android call Activity, PiP, lock screen |
CallService.kt |
android/src/main/java/.../CallService.kt |
207 | Android foreground service for calls |
CallManager.kt |
common/src/commonMain/.../views/call/CallManager.kt |
119 | Call lifecycle management |
WebRTC.kt |
common/src/commonMain/.../views/call/WebRTC.kt |
-- | CallState enum, WCallCommand, WCallResponse types |
IncomingCallAlertView.kt |
common/src/commonMain/.../views/call/IncomingCallAlertView.kt |
128 | In-app incoming call notification banner |
SimpleXAPI.kt |
common/src/commonMain/.../model/SimpleXAPI.kt |
-- | Call API commands (L1837--L1881) |