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:
- Acquire Media Stream: Use
getUserMediato obtain camera and microphone permissions. - Create RTCPeerConnection: Establish a peer-to-peer connection object.
- Exchange SDP: Exchange session descriptions via a signaling server.
- Exchange ICE Candidates: Exchange network information via the signaling server.
- 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
- Initiator:
- Create offer:
pc.createOffer() - Set local description:
pc.setLocalDescription(offer) - Send offer to peer
- Create offer:
- Receiver:
- Set remote description:
pc.setRemoteDescription(offer) - Create answer:
pc.createAnswer() - Set local description:
pc.setLocalDescription(answer) - Send answer to initiator
- Set remote description:
ICE Candidate Exchange
- Collect ICE Candidates:
- Local candidates:
pc.onicecandidateevent - Send to peer via signaling server
- Local candidates:
- Add ICE Candidates:
- Peer receives candidates:
pc.addIceCandidate(candidate)
- Peer receives candidates:
Common Issue Troubleshooting
- Unable to Acquire Media Stream:
- Check if the browser requested permissions.
- Ensure devices are connected and functioning.
- Check the browser console for errors.
- 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.
- Choppy Video or Audio:
- Check network bandwidth.
- Try reducing video resolution or frame rate.
- Investigate packet loss or high latency.



