# PHASE 4: VOICE & VIDEO CALLS - DOCUMENTATION ## Overview Phase 4 implements WebRTC-based voice and video calling with support for: - 1-on-1 audio and video calls - Group calls with up to 20 participants - Screen sharing - TURN/STUN servers for NAT traversal - Real-time media controls (mute, video toggle) - Connection quality monitoring - Call recording support (infrastructure) ## Architecture ### WebRTC Topology **1-on-1 Calls**: Mesh topology with direct peer-to-peer connections **Group Calls**: SFU (Selective Forwarding Unit) using Mediasoup (placeholder for future implementation) ### Components ``` ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ Client A │◄───────►│ Server │◄───────►│ Client B │ │ (Browser) │ WebRTC │ Socket.io │ WebRTC │ (Browser) │ │ │ Signals │ Signaling │ Signals │ │ └─────────────┘ └─────────────┘ └─────────────┘ ▲ │ ▲ │ │ │ │ ┌──────────▼──────────┐ │ └───────────►│ TURN/STUN Server │◄───────────┘ │ (NAT Traversal) │ └─────────────────────┘ ``` ## Database Schema ### Calls Table ```sql CREATE TABLE calls ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), conversation_id UUID NOT NULL REFERENCES conversations(id) ON DELETE CASCADE, type VARCHAR(20) NOT NULL DEFAULT 'audio', -- 'audio', 'video', 'screen' status VARCHAR(20) NOT NULL DEFAULT 'initiated', -- Status: 'initiated', 'ringing', 'active', 'ended', 'missed', 'rejected', 'failed' initiated_by UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, started_at TIMESTAMPTZ, ended_at TIMESTAMPTZ, duration_seconds INTEGER, end_reason VARCHAR(50), sfu_room_id VARCHAR(255), recording_url TEXT, quality_stats JSONB, created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW() ); ``` ### Call Participants Table ```sql CREATE TABLE call_participants ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), call_id UUID NOT NULL REFERENCES calls(id) ON DELETE CASCADE, user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, status VARCHAR(20) NOT NULL DEFAULT 'invited', -- Status: 'invited', 'ringing', 'joined', 'left', 'rejected', 'missed' joined_at TIMESTAMPTZ, left_at TIMESTAMPTZ, ice_candidates JSONB, media_state JSONB DEFAULT '{"audioEnabled": true, "videoEnabled": true, "screenSharing": false}', media_stats JSONB, connection_quality VARCHAR(20), created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW() ); ``` ### TURN Credentials Table ```sql CREATE TABLE turn_credentials ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, username VARCHAR(255) NOT NULL, credential VARCHAR(255) NOT NULL, expires_at TIMESTAMPTZ NOT NULL, created_at TIMESTAMPTZ DEFAULT NOW() ); -- Auto-cleanup function CREATE OR REPLACE FUNCTION cleanup_expired_turn_credentials() RETURNS void AS $$ BEGIN DELETE FROM turn_credentials WHERE expires_at < NOW(); END; $$ LANGUAGE plpgsql; ``` ## API Endpoints ### 1. POST `/api/calls/initiate` Initiate a new call. **Request:** ```json { "conversationId": "uuid", "type": "video", // or "audio" "participantIds": ["uuid1", "uuid2"] } ``` **Response:** ```json { "callId": "uuid", "status": "initiated", "participants": [ { "userId": "uuid", "userName": "John Doe", "userIdentifier": "john@example.com", "status": "invited" } ] } ``` ### 2. POST `/api/calls/:callId/answer` Answer an incoming call. **Response:** ```json { "callId": "uuid", "status": "active", "startedAt": "2025-01-10T14:30:00Z" } ``` ### 3. POST `/api/calls/:callId/reject` Reject an incoming call. **Response:** ```json { "callId": "uuid", "status": "rejected" } ``` ### 4. POST `/api/calls/:callId/end` End an active call. **Response:** ```json { "callId": "uuid", "status": "ended", "duration": 120, "endReason": "ended-by-user" } ``` ### 5. PATCH `/api/calls/:callId/media` Update media state (mute/unmute, video on/off). **Request:** ```json { "audioEnabled": true, "videoEnabled": false, "screenSharing": false } ``` **Response:** ```json { "success": true, "mediaState": { "audioEnabled": true, "videoEnabled": false, "screenSharing": false } } ``` ### 6. GET `/api/calls/turn-credentials` Get temporary TURN server credentials. **Response:** ```json { "credentials": { "urls": ["turn:turn.example.com:3478"], "username": "1736517600:username", "credential": "hmac-sha1-hash" }, "expiresAt": "2025-01-11T14:00:00Z" } ``` ### 7. GET `/api/calls/:callId` Get call details. **Response:** ```json { "call": { "id": "uuid", "conversationId": "uuid", "type": "video", "status": "active", "initiatedBy": "uuid", "startedAt": "2025-01-10T14:30:00Z", "participants": [...] } } ``` ## WebSocket Events ### Client → Server #### `call:offer` Send WebRTC offer to peer. ```javascript socket.emit('call:offer', { callId: 'uuid', targetUserId: 'uuid', offer: RTCSessionDescription }); ``` #### `call:answer` Send WebRTC answer to peer. ```javascript socket.emit('call:answer', { callId: 'uuid', targetUserId: 'uuid', answer: RTCSessionDescription }); ``` #### `call:ice-candidate` Send ICE candidate to peer. ```javascript socket.emit('call:ice-candidate', { callId: 'uuid', targetUserId: 'uuid', candidate: RTCIceCandidate }); ``` ### Server → Client #### `call:incoming` Notify user of incoming call. ```javascript socket.on('call:incoming', (data) => { // data: { callId, conversationId, type, initiatedBy, participants } }); ``` #### `call:offer` Receive WebRTC offer from peer. ```javascript socket.on('call:offer', (data) => { // data: { callId, fromUserId, offer } }); ``` #### `call:answer` Receive WebRTC answer from peer. ```javascript socket.on('call:answer', (data) => { // data: { callId, fromUserId, answer } }); ``` #### `call:ice-candidate` Receive ICE candidate from peer. ```javascript socket.on('call:ice-candidate', (data) => { // data: { callId, fromUserId, candidate } }); ``` #### `call:ended` Notify that call has ended. ```javascript socket.on('call:ended', (data) => { // data: { callId, reason, endedBy } }); ``` #### `call:participant-joined` Notify that participant joined group call. ```javascript socket.on('call:participant-joined', (data) => { // data: { callId, userId, userName, userIdentifier } }); ``` #### `call:participant-left` Notify that participant left group call. ```javascript socket.on('call:participant-left', (data) => { // data: { callId, userId } }); ``` #### `call:media-state-changed` Notify that participant's media state changed. ```javascript socket.on('call:media-state-changed', (data) => { // data: { callId, userId, mediaState } }); ``` ## Frontend Integration ### WebRTC Manager Usage ```javascript import WebRTCManager from './utils/webrtc'; // Initialize const webrtcManager = new WebRTCManager(socket); // Set TURN credentials const turnCreds = await fetch('/api/calls/turn-credentials'); await webrtcManager.setTurnCredentials(turnCreds.credentials); // Get local media stream const localStream = await webrtcManager.initializeLocalStream(true, true); localVideoRef.current.srcObject = localStream; // Setup event handlers webrtcManager.onRemoteStream = (userId, stream) => { remoteVideoRef.current.srcObject = stream; }; // Initiate call webrtcManager.currentCallId = callId; webrtcManager.isInitiator = true; await webrtcManager.initiateCallToUser(targetUserId); // Toggle audio/video webrtcManager.toggleAudio(false); // mute webrtcManager.toggleVideo(false); // video off // Screen sharing await webrtcManager.startScreenShare(); webrtcManager.stopScreenShare(); // Cleanup webrtcManager.cleanup(); ``` ### Call Component Usage ```javascript import Call from './components/Call'; function App() { const [showCall, setShowCall] = useState(false); return (