Call Module Technical Details¶
This document provides a detailed overview of the Call module, a critical component of the ECall iOS application.
Overview¶
The Call module manages the entire call lifecycle, from initiation to WebRTC connection establishment, including end-to-end encryption (E2EE) for all audio and video streams.
Architecture¶
Module Structure (code-aligned)¶
Call/
├── Helpers/
│ └── CallUtils.swift # Helper utilities for call flows
├── Managers/
│ ├── GroupCallManager.swift # Handles CallKit integration and high-level orchestration
│ ├── GroupCallSessionManager.swift # Manages the state of the current call session
│ ├── WebRTCManager.swift # Manages WebRTC peer connections (publisher/subscriber)
│ ├── CallEncryptionManager.swift # Handles E2EE encryption and decryption
│ ├── JanusSocketClient.swift # WebSocket client for the Janus Gateway
│ ├── StompSignalingManager.swift # STOMP WebSocket client for signaling
│ ├── CallSignalingHandler.swift # High-level handler for signaling messages (incl. rejoin)
│ ├── CallKeyStorage.swift # Stores encrypted AES keys per call (for rejoin)
│ ├── ConnectionCoordinator.swift # Coordinates WebRTC/Janus connection lifecycle
│ ├── SignalingDelegate.swift # Signaling delegate protocol and helpers
│ ├── PushRegistryManager.swift # Manages VoIP push registration
│ ├── AudioSessionManager.swift # Manages the app's AVAudioSession
│ ├── SFXManager.swift # Plays local sound effects (ringback, end call, etc.)
│ └── STOMPClient.swift # Low-level STOMP client abstraction
├── Services/
│ ├── CallService.swift # Handles REST API calls for call operations
│ └── CredentialsService.swift # Fetches TURN server credentials
├── Models/
│ ├── CallModel.swift # Contains data models for calls and histories
│ └── CallSession.swift # Defines the session state and Participant model
├── ViewModels/
│ └── CallViewModel.swift # ViewModel driving CallView and related UI
└── Views/
├── CallView.swift # Main in-call UI
├── CallHistory/
│ ├── CallHistoryView.swift # Displays the user's call history
│ ├── CallHistoryRow.swift # Single history row
│ └── CallHistoryDetailView.swift # Detail view for a call record
└── SubViews/ # Reusable in-call subviews
├── AudioParticipantTileView.swift
├── VideoParticipantTileView.swift
├── VideoTileView.swift
├── VideoTrackRendererView.swift
├── TwoPersonCallView.swift
├── MultiPersonAudioView.swift
├── MultiPersonVideoView.swift
├── CallControlsView.swift
├── CallBusyModeView.swift
└── KeyManagerSheetView.swift
Call Flow¶
Outgoing Call Flow¶
1. A user initiates a call.
↓
2. `GroupCallManager.startCall()` is invoked.
↓
3. The app initializes a CallKit transaction (`CXStartCallAction`).
↓
4. The app fetches the public keys of all callees.
↓
5. An AES key is generated and encrypted for each callee.
↓
6. The app sets up a Janus room (creates a session, attaches a plugin, and creates the room).
↓
7. The app calls the backend API (`startGroupCall`).
↓
8. The user joins the Janus room.
↓
9. A WebRTC offer is created.
↓
10. SDP and ICE candidates are exchanged.
↓
11. A WebRTC connection is established.
↓
12. E2EE media encryption begins.
Incoming Call Flow¶
1. The app receives a call invitation via the STOMP WebSocket.
↓
2. `GroupCallManager.handleCallInvitation()` is called.
↓
3. The app decrypts the AES key from the invitation payload.
↓
4. The app reports the incoming call to CallKit.
↓
5. The user answers the call, triggering a `CXAnswerCallAction`.
↓
6. The user joins the Janus room.
↓
7. A WebRTC answer is created.
↓
8. SDP and ICE candidates are exchanged.
↓
9. A WebRTC connection is established.
↓
10. E2EE media encryption begins.
Rejoin Call Flow¶
1. A user views call history and identifies an active call.
↓
2. The user taps "Rejoin" button.
↓
3. `GroupCallSessionManager.requestRejoinCall()` is called.
↓
4. The app sends a rejoin request to the backend (`POST /api/call/{callId}/request-rejoin`).
↓
5. The backend sends `participant_request_rejoin` message to active participants via STOMP.
↓
6. An active participant receives the request and fetches the requester's public key.
↓
7. The active participant encrypts the current AES key with the requester's RSA public key.
↓
8. The active participant sends `participant_accept_rejoin` message with encrypted AES key.
↓
9. The rejoining user receives the encrypted AES key via STOMP.
↓
10. `GroupCallSessionManager.rejoinActiveCall()` decrypts the AES key using RSA private key.
↓
11. The app calls backend API (`POST /api/call/{callId}/rejoin`) to join the call.
↓
12. The backend returns call record with Janus room ID.
↓
13. The app initializes call session with `isRejoinFlow = true`.
↓
14. The app joins the Janus room and establishes WebRTC connections.
↓
15. E2EE media encryption begins with the decrypted AES key.
↓
16. The user successfully rejoins the call.
Key Components¶
GroupCallManager¶
File: ecall/Modules/Call/Managers/GroupCallManager.swift
Purpose: To orchestrate the entire call flow and integrate with CallKit.
Key Responsibilities: - Initiating and managing calls. - Integrating with CallKit (CXProviderDelegate). - Handling incoming call timeouts (60 seconds). - Encrypting and decrypting AES keys. - Setting up Janus rooms. - Making backend API calls.
Key Methods:
-
startCall(to:calleeIDs:isVideo:):- Initializes a CallKit transaction.
- Fetches the public keys of all callees.
- Encrypts the AES key for each callee.
- Sets up the Janus room.
- Calls the backend API.
- Joins the Janus room.
- Starts the WebRTC connection flow.
-
handleCallInvitation(_:):- Parses the invitation message.
- Decrypts the AES key.
- Reports the incoming call to CallKit.
- Starts a 60-second timeout timer.
-
decryptAESKey(from:):- Decodes the base64/hex-encoded encrypted key.
- Attempts decryption with multiple RSA algorithms (OAEP-SHA256, OAEP-SHA1, PKCS1).
- Handles various key formats.
CallKit Integration:
// Provider delegate methods
func provider(_ provider: CXProvider, perform action: CXStartCallAction)
func provider(_ provider: CXProvider, perform action: CXAnswerCallAction)
func provider(_ provider: CXProvider, perform action: CXEndCallAction)
Incoming Call Timeout: - A 60-second timeout is enforced. - The call is automatically rejected if not answered within the time limit. - The timer is canceled if the call is answered or ended.
GroupCallSessionManager¶
File: ecall/Modules/Call/Managers/GroupCallSessionManager.swift
Purpose: To manage the state of the call session.
Key Properties:
@Published var participants: [Participant] = []
@Published var currentHost: Participant?
@Published var callStatus: CallStatus = .ended
var currentCallUUID: UUID?
var currentCallId: UInt64?
var janusRoomId: UInt64?
var isVideoCall: Bool = false
var isSpeakerOn: Bool = false
var isRejoinFlow: Bool = false // Flag to indicate rejoin flow
var pendingRejoinCallId: UInt64? // Call ID for pending rejoin request
Key Methods:
-
startCallSession(...):- Initializes the session with a list of participants.
- Sets the call status to
.requesting.
-
updateParticipants():- Fetches the list of participants from the backend.
- Updates the local state.
- Marks the local participant.
-
updateMuteState(_:):- Updates the mute status.
- Syncs the state with the backend.
- Updates the UI.
-
updateVideoState(_:):- Updates the video-enabled status.
- Syncs the state with the backend.
- Updates the UI.
-
requestRejoinCall(callId:onSuccess:onError:):- Initiates a rejoin request for an active call.
- Sends request to backend API.
- Sets
isRejoinFlow = trueflag. - Stores
pendingRejoinCallIdfor tracking.
-
rejoinActiveCall(callId:encryptedAESKeyBase64:onError:):- Processes rejoin acceptance with encrypted AES key.
- Decrypts AES key using RSA private key.
- Joins call via backend API.
- Initializes call session with rejoin flag.
- Establishes WebRTC connections.
- Updates participant list.
-
tryConsumeRejoinAcceptance(for:):- Validates and consumes a rejoin acceptance message.
- Prevents duplicate processing.
- Returns true if acceptance is valid and consumed.
-
clearPendingRejoinRequest():- Clears pending rejoin request state.
- Resets flags and call ID.
Call Status:
enum CallStatus {
case requesting // Call initiated, waiting for an answer
case connecting // Call answered, connecting WebRTC
case connected // WebRTC connected, call is active
case ended // Call has ended
}
WebRTCManager¶
File: ecall/Modules/Call/Managers/WebRTCManager.swift
Purpose: To manage WebRTC peer connections.
Architecture: - Publisher: Manages local media publishing. - Subscriber: Manages remote media receiving. - Separate instances are used for the publisher and subscriber.
Key Components:
-
PeerConnection Setup:
func setupPubPeerConnection() func setupSubPeerConnection() -
Media Tracks:
var localVideoTrack: RTCVideoTrack? var localAudioTrack: RTCAudioTrack? var remoteVideoTracks: [UInt64: RTCVideoTrack] = [:] var remoteAudioTracks: [UInt64: RTCAudioTrack] = [:] -
WebRTC Signaling:
func createPubOffer() func createSubAnswer() func handleOffer(_ offer: RTCSessionDescription) func handleAnswer(_ answer: RTCSessionDescription) func addIceCandidate(_ candidate: RTCIceCandidate) -
ICE Connection:
- A 5-second ICE connection timeout is implemented.
- Candidates are buffered with a timeout.
- A 30-second RTCP keepalive is used.
ICE Servers: - STUN servers from Google. - TURN servers, with credentials fetched from the backend. - Falls back to STUN-only if TURN is unavailable.
Connection State:
@Published var connectionState: RTCPeerConnectionState = .new
CallEncryptionManager¶
File: ecall/Modules/Call/Managers/CallEncryptionManager.swift
Purpose: To manage E2EE encryption and decryption for calls.
Key Methods:
-
prepareCallInvitation(with:):- Generates an AES-256 key.
- Encrypts the key with an RSA public key (OAEP-SHA256).
- Sets up encryption for video and audio.
-
processCallInvitation(encryptedAESKey:calleeRSAPrivateKey:):- Decrypts the AES key with an RSA private key.
- Attempts decryption with multiple algorithms (OAEP-SHA256, OAEP-SHA1, PKCS1).
- Sets up encryption for video and audio.
-
encryptCallMediaData(_:)/decryptCallMediaData(_:):- Encrypts and decrypts media data using AES-GCM.
Encryption Setup:
func setUpAesKey(_ aesKey: Data) {
// Set up video encryption
let manager = CRTEncryptionManager.shared()
manager.setup(withKey: aesKey, iv: ivData)
// Set up audio encryption
let audioCrypto = CustomAudioCrypto(key: SymmetricKey(data: aesKey))
RTCAudioCryptoManager.shared().delegate = audioCrypto
}
JanusSocketClient¶
File: ecall/Modules/Call/Managers/JanusSocketClient.swift
Purpose: A WebSocket client for the Janus Gateway.
Janus Flow:
-
Create Session:
func createSession(completion: @escaping (Result<UInt64, Error>) -> Void)- Connects to the Janus WebSocket.
- Creates a session and retrieves a
session_id.
-
Attach Plugin:
func attachPublisher(completion: @escaping (Result<UInt64, Error>) -> Void)- Attaches the video room plugin and retrieves a
handle_id.
- Attaches the video room plugin and retrieves a
-
Create/Join Room:
func createRoom(completion: @escaping (Result<UInt64, Error>) -> Void) func joinRoom(room: UInt64, display: String, completion: @escaping (Result<UInt64, Error>) -> Void)- Creates a new room or joins an existing one, retrieving a
room_id.
- Creates a new room or joins an existing one, retrieving a
-
WebRTC Signaling:
func publishOffer(_ offer: RTCSessionDescription) func publishAnswer(_ answer: RTCSessionDescription) func handleRemoteJsep(_ jsep: [String: Any]) func addIceCandidate(_ candidate: RTCIceCandidate)
Message Types: - create: Creates a session. - attach: Attaches a plugin. - message: Handles room operations (create, join, publish, subscribe). - trickle: Sends ICE candidates. - ack: Acknowledges messages.
StompSignalingManager¶
File: ecall/Modules/Call/Managers/StompSignalingManager.swift
Purpose: A STOMP WebSocket for call invitation signaling.
Message Types: - call_invitation: An incoming call invitation. - call_accept: A notification that a call was accepted. - call_reject: A notification that a call was rejected. - call_end: A notification that a call has ended. - participant_request_rejoin: A request from a user to rejoin an active call. - participant_accept_rejoin: An acceptance message containing encrypted AES key for rejoining user.
Message Structure:
struct SignalMessage {
let type: MessageType
let callerId: UInt64?
let callerName: String?
let callerDeviceId: UInt64?
let callId: UInt64?
let roomId: UInt64?
let encryptedAESKey: String?
let isVideo: Bool?
let isOnGoing: Bool?
}
Connection Management: - Automatically reconnects on disconnect. - Manages the connection state. - Routes incoming messages.
AudioSessionManager¶
File: ecall/Modules/Call/Managers/AudioSessionManager.swift
Purpose: To manage the AVAudioSession for calls.
Key Methods:
func configureAudioSession()
func setSpeakerMode(_ enabled: Bool)
func setMuteMode(_ muted: Bool)
Audio Session Configuration: - Category: .playAndRecord - Mode: .voiceChat - Options: .defaultToSpeaker, .allowBluetooth
Call Service¶
CallService¶
File: ecall/Modules/Call/Services/CallService.swift
Purpose: To handle all REST API calls for call operations.
Key Methods:
-
startGroupCall(...):- Creates a call record on the backend.
- Sends invitations to all callees.
- Returns the created call record.
-
joinGroupCall(callId:):- Joins an existing call.
- Returns the call record with a list of participants.
-
inviteToGroupCall(...):- Invites additional participants to a call.
- Returns a list of the invited users.
-
endCall(callId:):- Ends a call and updates its status on the backend.
-
fetchCallHistory():- Retrieves the user's call history.
-
fetchCallParticipants(callId:):- Retrieves the list of participants for a given call.
-
requestRejoinGroupCall(callId:completion:):- Sends a rejoin request to the backend.
- Backend forwards request to active participants via STOMP.
- Returns success/failure result.
-
rejoinGroupCall(callId:completion:):- Joins an existing active call.
- Returns call record with Janus room ID and participant list.
- Used after receiving encrypted AES key for rejoin.
CredentialsService¶
File: ecall/Modules/Call/Services/CredentialsService.swift
Purpose: To fetch TURN server credentials from the backend.
Key Methods:
func fetchCredentials()
func loadCredentials() -> TurnCredentials?
Credentials Structure:
struct TurnCredentials {
let turnUsername: String
let turnPassword: String
let noneTlsUrl: String
let tlsUrl: String?
}
Models¶
CallRecord¶
File: ecall/Modules/Call/Models/CallModel.swift
Structure:
struct CallRecord: Identifiable, Codable {
let id: UInt64?
let contactId: UInt64?
let contactName: String?
let callType: CallType? // incoming, outgoing
let callMediaType: CallMediaType? // audio, video
let callCategory: CallCategory? // personal, group
let startedAt: String?
let answeredAt: String?
let endedAt: String?
let status: String // active, ended, missed
let janusRoomId: UInt64?
let duration: Int?
}
Participant¶
File: ecall/Modules/Call/Models/CallSession.swift
Structure:
struct Participant: Identifiable, Codable {
let id: UInt64
let userId: UInt64
let deviceId: UInt64
let displayName: String
let isHost: Bool
var isLocal: Bool
var feedId: UInt64
var isMuted: Bool
var isVideoEnabled: Bool
var status: ParticipantStatus? // inviting, connected, disconnected
}
Participant Status:
enum ParticipantStatus: String, Codable {
case inviting
case connected
case disconnected
}
UI Components¶
CallView¶
File: ecall/Modules/Call/Views/CallView.swift
Purpose: The main UI for a call, including video and audio controls.
Features: - Local and remote video previews. - Audio controls (mute, speaker). - A button to toggle video on or off. - An end call button. - A list of participants. - A display for the call duration.
CallHistoryView¶
File: ecall/Modules/Call/Views/CallHistory/CallHistoryView.swift
Purpose: To display the user's call history with filtering options.
Features: - A filterable list of calls. - Search functionality. - A button to rejoin active calls. - The ability to delete call records. - A detailed view for each call. - Visual indication of active calls that can be rejoined.
Error Handling¶
Error Types¶
enum AppError {
case userBusy
case userNotAvailable
case networkError
case encryptionError
case janusError
}
Error Handling Strategy¶
- Network Errors: Retry with exponential backoff.
- Encryption Errors: Log the error and show a user-friendly message.
- Janus Errors: Retry the connection and fall back to STUN-only if necessary.
- CallKit Errors: Handle gracefully and display an error message.
Performance Considerations¶
WebRTC Optimization¶
- Buffer ICE candidates to reduce signaling traffic.
- Implement a connection timeout to avoid hanging connections.
- Use an RTCP keepalive to maintain the connection.
- Optimize the video codec (H264).
Memory Management¶
- Clean up WebRTC connections when a call ends.
- Release media tracks when they are no longer needed.
- Ensure proper delegate cleanup.
Network Optimization¶
- Use TURN servers for a more reliable connection.
- Fall back to STUN-only if TURN is unavailable.
- Implement connection retry logic.
Security Considerations¶
E2EE Encryption¶
- Use AES-256-GCM for media encryption.
- Use RSA-2048 for key exchange.
- Store keys securely in the Keychain.
- Ensure no server-side access to keys.
Key Exchange Security¶
- Use RSA OAEP-SHA256 for key encryption.
- Implement a fallback to multiple algorithms for compatibility.
- Use robust base64/hex decoding.
Testing Strategy¶
Unit Tests¶
- Encryption and decryption logic.
- Call state management.
- Error handling.
Integration Tests¶
- WebRTC connection establishment.
- Janus Gateway integration.
- CallKit integration.
End-to-End Tests¶
- The complete call flow.
- Multi-participant calls.
- Various error scenarios.
Rejoin Call Feature¶
Overview¶
The rejoin call feature allows users who were previously part of an active call to reconnect after disconnection, app restart, or network issues. This feature maintains E2EE encryption by securely sharing the AES key with the rejoining user.
Key Components¶
CallSignalingHandler¶
File: ecall/Modules/Call/Managers/CallSignalingHandler.swift
Responsibilities: - Handles participant_request_rejoin messages from rejoining users. - Fetches rejoining user's public key. - Encrypts current AES key with rejoining user's RSA public key. - Sends participant_accept_rejoin message with encrypted AES key.
Key Methods: - handleParticipantRequestRejoin(_:): Processes rejoin request from a participant. - handleParticipantAcceptRejoin(_:): Processes rejoin acceptance message.
CallKeyStorage¶
Purpose: Stores encrypted AES keys for calls to enable rejoin functionality.
Key Methods: - storeEncryptedAESKey(_:for:): Stores encrypted AES key for a call ID. - getEncryptedAESKey(for:): Retrieves encrypted AES key for a call ID.
Rejoin Flow Details¶
Step 1: User Initiates Rejoin¶
- User views call history and sees active call.
- User taps "Rejoin" button in
CallHistoryVieworCallHistoryDetailView. GroupCallSessionManager.requestRejoinCall()is called.
Step 2: Backend Processing¶
- Backend receives rejoin request via
POST /api/call/{callId}/request-rejoin. - Backend sends
participant_request_rejoinSTOMP message to all active participants. - Message contains:
callId,participantId(requester's userId),calleeDeviceId.
Step 3: Active Participant Response¶
- Active participant receives
participant_request_rejoinmessage. - Participant fetches requester's public key using
UserService.fetchPublicKeys(). - Participant encrypts current AES key with requester's RSA public key.
- Participant sends
participant_accept_rejoinmessage with encrypted AES key.
Step 4: Rejoining User Processing¶
- Rejoining user receives
participant_accept_rejoinmessage via STOMP. GroupCallSessionManager.rejoinActiveCall()is called.- AES key is decrypted using RSA private key.
- Backend API is called (
POST /api/call/{callId}/rejoin). - Call session is initialized with
isRejoinFlow = true.
Step 5: WebRTC Connection¶
- Janus room is joined using existing room ID.
- WebRTC publisher/subscriber connections are established.
- Participant list is updated.
- E2EE encryption begins with decrypted AES key.
Security Considerations¶
- AES key is encrypted with rejoining user's RSA public key (OAEP-SHA256).
- Only active participants can share the AES key.
- Rejoin request includes userId and deviceId for verification.
- Encrypted AES key is transmitted securely via STOMP WebSocket.
- Private key never leaves the device.
Error Handling¶
- If encrypted AES key is missing, rejoin fails with error message.
- If AES key decryption fails, rejoin fails with error message.
- If Janus room ID is missing, rejoin fails with error message.
- Network errors trigger retry logic or show error to user.