Lesson 03-WebRTC Quick Start Establishing Real-Time Communication Flow

WebRTC (Web Real-Time Communication) enables direct real-time audio and video communication between browsers without requiring plugins or intermediate servers (except for a signaling server). Below, I’ll guide you through quickly mastering the WebRTC communication flow, starting from scratch to build a simple video call application.

WebRTC Communication Basic Flow

The core process of establishing a WebRTC connection can be broken down into the following key steps:

  1. Acquire Media Stream: Use getUserMedia to obtain camera and microphone permissions.
  2. Create RTCPeerConnection: Establish a peer-to-peer connection object.
  3. Exchange SDP: Exchange session descriptions via a signaling server.
  4. Exchange ICE Candidates: Exchange network information via the signaling server.
  5. Establish Connection: Complete the connection setup and begin communication.

Quick Start Sample Code

Below is a complete HTML file containing all the code needed to establish a WebRTC video call:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>WebRTC Quick Start</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            max-width: 800px;
            margin: 0 auto;
            padding: 20px;
        }
        .video-container {
            display: flex;
            flex-wrap: wrap;
            gap: 20px;
            margin-bottom: 20px;
        }
        video {
            width: 320px;
            height: 240px;
            background-color: #000;
            border: 1px solid #ccc;
        }
        .controls {
            margin-bottom: 20px;
        }
        button {
            padding: 8px 16px;
            margin-right: 10px;
            cursor: pointer;
        }
        #roomId {
            padding: 8px;
            width: 200px;
        }
    </style>
</head>
<body>
    <h1>WebRTC Quick Start Example</h1>

    <div class="controls">
        <input type="text" id="roomId" placeholder="Enter Room ID">
        <button id="joinBtn">Join Room</button>
    </div>

    <div class="video-container">
        <div>
            <h3>Local Video</h3>
            <video id="localVideo" autoplay playsinline muted></video>
        </div>
        <div>
            <h3>Remote Video</h3>
            <video id="remoteVideo" autoplay playsinline></video>
        </div>
    </div>

    <div class="controls">
        <button id="startBtn" disabled>Start Call</button>
        <button id="hangupBtn" disabled>Hang Up</button>
    </div>

    <script>
        // Global variables
        let localStream;
        let peerConnection;
        let roomId;
        let isInitiator = false;
        let signalingChannel;

        // DOM elements
        const localVideo = document.getElementById('localVideo');
        const remoteVideo = document.getElementById('remoteVideo');
        const roomIdInput = document.getElementById('roomId');
        const joinBtn = document.getElementById('joinBtn');
        const startBtn = document.getElementById('startBtn');
        const hangupBtn = document.getElementById('hangupBtn');

        // Join room button click event
        joinBtn.addEventListener('click', () => {
            roomId = roomIdInput.value.trim();
            if (!roomId) {
                alert('Please enter a room ID');
                return;
            }

            // Create signaling channel (simulating WebSocket)
            signalingChannel = createSignalingChannel();

            // Enable start button
            startBtn.disabled = false;
        });

        // Start call button click event
        startBtn.addEventListener('click', async () => {
            try {
                // Acquire local media stream
                localStream = await navigator.mediaDevices.getUserMedia({
                    video: true,
                    audio: true
                });
                localVideo.srcObject = localStream;

                // Create RTCPeerConnection
                createPeerConnection();

                // Add local stream to connection
                localStream.getTracks().forEach(track => {
                    peerConnection.addTrack(track, localStream);
                });

                // If initiator, create offer
                isInitiator = true;
                const offer = await peerConnection.createOffer();
                await peerConnection.setLocalDescription(offer);

                // Send offer via signaling channel
                signalingChannel.send({
                    type: 'offer',
                    sdp: peerConnection.localDescription,
                    roomId: roomId
                });

                startBtn.disabled = true;
                hangupBtn.disabled = false;
            } catch (err) {
                console.error('Failed to start call:', err);
                alert('Failed to acquire media stream: ' + err.message);
            }
        });

        // Hang up button click event
        hangupBtn.addEventListener('click', () => {
            if (peerConnection) {
                peerConnection.close();
                peerConnection = null;
            }

            if (localStream) {
                localStream.getTracks().forEach(track => track.stop());
                localStream = null;
                localVideo.srcObject = null;
                remoteVideo.srcObject = null;
            }

            startBtn.disabled = false;
            hangupBtn.disabled = true;
        });

        // Create signaling channel (simulating WebSocket)
        function createSignalingChannel() {
            // In real applications, this should create a WebSocket connection
            // Here, we use a simple object to simulate the signaling channel
            return {
                send: function(data) {
                    console.log('Sending signaling:', data);

                    // Simulate network delay
                    setTimeout(() => {
                        // Simulate receiver processing signaling
                        if (data.type === 'offer') {
                            // Simulate receiver creating answer
                            setTimeout(() => {
                                const simulatedAnswer = {
                                    type: 'answer',
                                    sdp: { /* Simulated SDP */ },
                                    roomId: data.roomId
                                };
                                onSignalingMessage(simulatedAnswer);

                                // Simulate ICE candidate exchange
                                setTimeout(() => {
                                    const simulatedCandidate = {
                                        type: 'ice-candidate',
                                        candidate: { /* Simulated ICE candidate */ },
                                        roomId: data.roomId
                                    };
                                    onSignalingMessage(simulatedCandidate);
                                }, 100);
                            }, 100);
                        } else if (data.type === 'ice-candidate') {
                            // Simulate receiving ICE candidate
                            setTimeout(() => {
                                onSignalingMessage(data);
                            }, 100);
                        }
                    }, 100);
                },
                onmessage: function(message) {
                    // In real applications, this would receive messages from WebSocket
                    // Here, we directly call the handler function
                    onSignalingMessage(message);
                }
            };
        }

        // Handle signaling messages
        async function onSignalingMessage(message) {
            console.log('Received signaling message:', message);

            if (message.roomId !== roomId) {
                // Not a message for the current room
                return;
            }

            switch (message.type) {
                case 'offer':
                    // If receiver
                    if (!isInitiator) {
                        // Create RTCPeerConnection
                        createPeerConnection();

                        // Set remote description
                        await peerConnection.setRemoteDescription(new RTCSessionDescription(message.sdp));

                        // Create answer
                        const answer = await peerConnection.createAnswer();
                        await peerConnection.setLocalDescription(answer);

                        // Send answer
                        signalingChannel.send({
                            type: 'answer',
                            sdp: peerConnection.localDescription,
                            roomId: roomId
                        });
                    }
                    break;

                case 'answer':
                    // If initiator
                    if (isInitiator) {
                        // Set remote description
                        await peerConnection.setRemoteDescription(new RTCSessionDescription(message.sdp));
                    }
                    break;

                case 'ice-candidate':
                    // Add ICE candidate
                    if (peerConnection) {
                        await peerConnection.addIceCandidate(new RTCIceCandidate(message.candidate));
                    }
                    break;
            }
        }

        // Create RTCPeerConnection
        function createPeerConnection() {
            // Configure ICE servers (using Google's public STUN server)
            const configuration = {
                iceServers: [
                    { urls: 'stun:stun.l.google.com:19302' }
                    // In real applications, you may need to add a TURN server
                    // { urls: 'turn:your-turn-server.com', username: 'user', credential: 'pass' }
                ]
            };

            peerConnection = new RTCPeerConnection(configuration);

            // Monitor ICE candidates
            peerConnection.onicecandidate = event => {
                if (event.candidate) {
                    signalingChannel.send({
                        type: 'ice-candidate',
                        candidate: event.candidate,
                        roomId: roomId
                    });
                }
            };

            // Monitor remote stream
            peerConnection.ontrack = event => {
                remoteVideo.srcObject = event.streams[0];
            };

            // Monitor connection state changes
            peerConnection.oniceconnectionstatechange = () => {
                console.log('ICE connection state:', peerConnection.iceConnectionState);

                if (peerConnection.iceConnectionState === 'connected') {
                    console.log('Connection established');
                } else if (peerConnection.iceConnectionState === 'failed' || 
                          peerConnection.iceConnectionState === 'disconnected') {
                    console.error('Connection failed or disconnected');
                    // Add reconnection logic here if needed
                }
            };
        }
    </script>
</body>
</html>

Complete Flow in Real Applications

The example code above simplifies the signaling part. In real applications, you need to:

Implement a Real Signaling Server:

  • Use WebSocket or Socket.io to create a signaling server.
  • Handle room management.
  • Forward signaling messages.

    Key APIs Explained

    getUserMedia API

    // Acquire media stream
    navigator.mediaDevices.getUserMedia({
        video: true,  // Request video
        audio: true   // Request audio
    })
    .then(stream => {
        // Successfully acquired media stream
        videoElement.srcObject = stream;
    })
    .catch(err => {
        // Handle errors
        console.error('Failed to acquire media stream:', err);
    });

    RTCPeerConnection API

    // Create RTCPeerConnection
    const pc = new RTCPeerConnection({
        iceServers: [
            { urls: 'stun:stun.l.google.com:19302' }  // STUN server configuration
        ]
    });
    
    // Add local stream
    localStream.getTracks().forEach(track => {
        pc.addTrack(track, localStream);
    });
    
    // Monitor ICE candidates
    pc.onicecandidate = event => {
        if (event.candidate) {
            // Send ICE candidate to peer
            signalingChannel.send({
                type: 'ice-candidate',
                candidate: event.candidate
            });
        }
    };
    
    // Monitor remote stream
    pc.ontrack = event => {
        remoteVideo.srcObject = event.streams[0];
    };

    SDP Exchange Process

    1. Initiator:
      • Create offer: pc.createOffer()
      • Set local description: pc.setLocalDescription(offer)
      • Send offer to peer
    2. Receiver:
      • Set remote description: pc.setRemoteDescription(offer)
      • Create answer: pc.createAnswer()
      • Set local description: pc.setLocalDescription(answer)
      • Send answer to initiator

    ICE Candidate Exchange

    1. Collect ICE Candidates:
      • Local candidates: pc.onicecandidate event
      • Send to peer via signaling server
    2. Add ICE Candidates:
      • Peer receives candidates: pc.addIceCandidate(candidate)

    Common Issue Troubleshooting

    1. Unable to Acquire Media Stream:
      • Check if the browser requested permissions.
      • Ensure devices are connected and functioning.
      • Check the browser console for errors.
    2. Connection Cannot Be Established:
      • Verify that the signaling server is operational.
      • Ensure STUN/TURN server configurations are correct.
      • Check if firewall settings are blocking the connection.
    3. Choppy Video or Audio:
      • Check network bandwidth.
      • Try reducing video resolution or frame rate.
      • Investigate packet loss or high latency.
    Share your love