WebRTC (Web Real-Time Communication) technology enables direct real-time audio and video communication between browsers without requiring plugins or intermediate servers (except for signaling servers). This document provides a detailed guide on implementing one-to-one real-time communication using WebRTC, including a complete technical implementation, code examples, and key considerations.
One-to-One Communication Architecture Design
Basic Communication Flow
The basic flow for one-to-one WebRTC communication is as follows:
- Signaling Exchange Phase:
- Both parties exchange SDP (Session Description Protocol) information
- Exchange ICE (Interactive Connectivity Establishment) candidate addresses
- Connection Establishment Phase:
- Establish a direct connection using ICE candidate addresses
- Complete DTLS-SRTP handshake for encryption
- Media Transmission Phase:
- Directly transmit audio and video streams
- Optional data channel transmission
Communication Flow Diagram:
[User A] ---Signaling Server---> [User B]
| |
|--SDP Exchange--> |--SDP Exchange-->
| |
|--ICE Candidate Exchange--> |--ICE Candidate Exchange-->
| |
[Direct Media Stream] <--------> [Direct Media Stream]
Role Definitions
In one-to-one communication, there are typically two roles:
- Initiator:
- Creates the offer SDP
- Initiates connection establishment
- Responder:
- Receives the offer SDP
- Creates the answer SDP
In practical applications, these roles are dynamic, depending on which party initiates the call request.
Complete Code Implementation
HTML Structure
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WebRTC One-to-One Video Call</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;
}
#status {
margin-top: 10px;
padding: 5px;
border-radius: 3px;
}
.connected {
background-color: #dff0d8;
color: #3c763d;
}
.disconnected {
background-color: #f2dede;
color: #a94442;
}
</style>
</head>
<body>
<h1>WebRTC One-to-One Video Call</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>
<div id="status" class="disconnected">Disconnected</div>
<script src="app.js"></script>
</body>
</html>JavaScript Implementation (app.js)
// Global variables
let localStream;
let peerConnection;
let roomId;
let isInitiator = false;
let signalingChannel;
let remoteUserId = null;
// 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');
const statusDiv = document.getElementById('status');
// Join room button click event
joinBtn.addEventListener('click', () => {
roomId = roomIdInput.value.trim();
if (!roomId) {
alert('Please enter a room ID');
return;
}
// Create signaling channel (simulated with WebSocket)
signalingChannel = createSignalingChannel();
// Enable start button
startBtn.disabled = false;
});
// Start call button click event
startBtn.addEventListener('click', async () => {
try {
// Get 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 to peer via signaling channel
signalingChannel.send({
type: 'offer',
sdp: peerConnection.localDescription,
roomId: roomId,
from: 'user_' + Math.random().toString(36).substr(2, 9) // Generate temporary user ID
});
startBtn.disabled = true;
hangupBtn.disabled = false;
updateStatus('Call request initiated', 'connected');
} catch (err) {
console.error('Failed to start call:', err);
alert('Failed to get 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;
updateStatus('Call ended', 'disconnected');
remoteUserId = null;
});
// Create signaling channel (simulated WebSocket)
function createSignalingChannel() {
// In production, this should create a WebSocket connection
// Here, we simulate a signaling channel with an object
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,
from: 'user_' + Math.random().toString(36).substr(2, 9)
};
onSignalingMessage(simulatedAnswer);
// Simulate ICE candidate exchange
setTimeout(() => {
const simulatedCandidate = {
type: 'ice-candidate',
candidate: { /* Simulated ICE candidate */ },
roomId: data.roomId,
from: 'user_' + Math.random().toString(36).substr(2, 9)
};
onSignalingMessage(simulatedCandidate);
}, 100);
}, 100);
} else if (data.type === 'ice-candidate') {
// Simulate receiving ICE candidate
setTimeout(() => {
onSignalingMessage(data);
}, 100);
}
}, 100);
},
onmessage: function(message) {
// In production, this would receive messages from WebSocket
// Here, we directly call the handler
onSignalingMessage(message);
}
};
}
// Handle signaling messages
async function onSignalingMessage(message) {
console.log('Received signaling message:', message);
if (message.roomId !== roomId) {
// Ignore messages not for the current room
return;
}
if (message.from === remoteUserId) {
// Messages from the remote user
switch (message.type) {
case 'offer':
// If responder
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,
from: 'user_' + Math.random().toString(36).substr(2, 9)
});
remoteUserId = message.from;
updateStatus('Call request received', 'connected');
}
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;
}
} else {
// Messages from other users (possible in multi-room scenarios)
console.log('Ignoring message from other user');
}
}
// Create RTCPeerConnection
function createPeerConnection() {
// Configure ICE servers (using Google's public STUN server)
const configuration = {
iceServers: [
{ urls: 'stun:stun.l.google.com:19302' }
// In production, you may need to add TURN servers
// { urls: 'turn:your-turn-server.com', username: 'user', credential: 'pass' }
]
};
peerConnection = new RTCPeerConnection(configuration);
// Listen for ICE candidates
peerConnection.onicecandidate = event => {
if (event.candidate) {
signalingChannel.send({
type: 'ice-candidate',
candidate: event.candidate,
roomId: roomId,
from: 'user_' + Math.random().toString(36).substr(2, 9)
});
}
};
// Listen for remote stream
peerConnection.ontrack = event => {
remoteVideo.srcObject = event.streams[0];
};
// Listen for ICE connection state changes
peerConnection.oniceconnectionstatechange = () => {
console.log('ICE connection state:', peerConnection.iceConnectionState);
switch (peerConnection.iceConnectionState) {
case 'connected':
console.log('Connection established');
updateStatus('Call connected', 'connected');
break;
case 'disconnected':
console.log('Connection disconnected (may recover)');
updateStatus('Connection disconnected (may recover)', 'disconnected');
break;
case 'failed':
console.log('Connection failed (needs restart)');
updateStatus('Connection failed', 'disconnected');
// Add reconnection logic here if needed
break;
case 'closed':
console.log('Connection closed');
updateStatus('Connection closed', 'disconnected');
break;
}
};
// Listen for errors
peerConnection.onerror = error => {
console.error('RTCPeerConnection error:', error);
updateStatus('Connection error', 'disconnected');
};
}
// Update status display
function updateStatus(text, className) {
statusDiv.textContent = text;
statusDiv.className = className;
}
// Get media stream (example)
async function getMediaStream() {
try {
localStream = await navigator.mediaDevices.getUserMedia({
video: true,
audio: true
});
localVideo.srcObject = localStream;
return localStream;
} catch (err) {
console.error('Failed to get media stream:', err);
throw err;
}
}Key Technical Points Analysis
SDP Exchange Mechanism
The Session Description Protocol (SDP) is used in WebRTC to describe media session parameters:
- Offer/Answer Model:
- Initiator creates offer SDP
- Responder creates answer SDP
- Both exchange SDP via signaling server
- SDP Content:
- Media types (video/audio)
- Codec preferences
- Network transport parameters
- ICE candidate information
SDP Exchange Flow:
- Initiator calls
createOffer()to generate offer SDP - Initiator calls
setLocalDescription(offer)to set local description - Initiator sends offer to responder via signaling server
- Responder receives offer and calls
setRemoteDescription(offer)to set remote description - Responder calls
createAnswer()to generate answer SDP - Responder calls
setLocalDescription(answer)to set local description - Responder sends answer to initiator via signaling server
- Initiator receives answer and calls
setRemoteDescription(answer)to set remote description
ICE Candidate Collection and Exchange
Interactive Connectivity Establishment (ICE) protocol is used to establish direct connections in complex network environments:
- Candidate Types:
- Host candidate (local IP address)
- Reflexive candidate (public IP from STUN server)
- Relay candidate (TURN server relay)
- Collection Process:
- Local candidates (immediately available)
- STUN candidates (require network requests)
- TURN candidates (require network requests)
- Exchange Process:
- Both parties exchange candidates via signaling server
- Perform connectivity checks
- Select optimal path
ICE State Transitions:
new: Initial statechecking: Checking candidatesconnected: Connection establishedcompleted: All candidates checkedfailed: Connection faileddisconnected: Connection disruptedclosed: Connection closed
DTLS-SRTP Encryption Mechanism
WebRTC uses a DTLS-SRTP combination for end-to-end encryption:
- DTLS (Datagram Transport Layer Security):
- Handles handshake and key exchange
- Provides transport-layer security
- SRTP (Secure Real-time Transport Protocol):
- Encrypts actual media streams
- Ensures media stream security
Encryption Flow:
- Negotiate encryption parameters via DTLS handshake
- Derive media encryption keys
- Encrypt media streams using SRTP
- Encrypt data channels using DTLS
Practical Deployment Considerations
Signaling Server Implementation
In production, a reliable signaling server is required:
- WebSocket Server:
- Node.js with ws library
- Socket.io
- Other WebSocket implementations
- Functional Requirements:
- Message routing (ensure messages reach correct users)
- Room management
- Connection state monitoring
Simple Node.js Signaling Server Example:
const WebSocket = require('ws');
const http = require('http');
const server = http.createServer();
const wss = new WebSocket.Server({ server });
// Room management
const rooms = {};
wss.on('connection', (ws) => {
// Assign temporary ID on user connection
const userId = 'user_' + Math.random().toString(36).substr(2, 9);
ws.userId = userId;
console.log(`User ${userId} connected`);
// Message handling
ws.on('message', (message) => {
try {
const data = JSON.parse(message);
// Handle different message types
switch (data.type) {
case 'join':
// Join room
if (!rooms[data.roomId]) {
rooms[data.roomId] = [];
}
rooms[data.roomId].push(ws);
ws.roomId = data.roomId;
console.log(`User ${userId} joined room ${data.roomId}`);
break;
case 'offer':
case 'answer':
case 'ice-candidate':
// Forward signaling messages to other users in the room
if (ws.roomId && rooms[ws.roomId]) {
rooms[ws.roomId].forEach(client => {
if (client !== ws && client.readyState === WebSocket.OPEN) {
client.send(message);
}
});
}
break;
}
} catch (err) {
console.error('Message processing error:', err);
}
});
// Connection closure
ws.on('close', () => {
console.log(`User ${userId} disconnected`);
if (ws.roomId && rooms[ws.roomId]) {
rooms[ws.roomId] = rooms[ws.roomId].filter(client => client !== ws);
if (rooms[ws.roomId].length === 0) {
delete rooms[ws.roomId];
}
}
});
// Error handling
ws.on('error', (err) => {
console.error('WebSocket error:', err);
});
});
server.listen(8080, () => {
console.log('Signaling server running on port 8080');
});NAT Traversal and TURN Servers
In complex network environments, TURN servers may be necessary:
- STUN Servers:
- Used to obtain public IP addresses
- Free public STUN servers available
- TURN Servers:
- Used to relay media streams
- Requires server resources
- Paid services or self-hosted
Configuration Example:
const configuration = {
iceServers: [
{ urls: 'stun:stun.l.google.com:19302' }, // Free STUN server
{
urls: 'turn:your-turn-server.com:3478',
username: 'your-username',
credential: 'your-password'
} // Self-hosted or paid TURN server
]
};Multi-Device and Multi-Room Support
Extending one-to-one communication to multi-device or multi-room scenarios:
- Multi-Room Management:
- Room ID assignment
- User join/leave room
- Room state monitoring
- Multi-Device Support:
- Multiple device connections for the same user
- State synchronization across devices
- Media stream distribution
Testing and Debugging
Testing Strategies
- Local Testing:
- Test within the same network
- Test across different browsers
- Cross-Network Testing:
- Test with different public IPs
- NAT traversal testing
- Stress Testing:
- Multi-room concurrent testing
- Long-duration connection testing
Debugging Tools
- Browser Developer Tools:
- WebRTC internal state monitoring (chrome://webrtc-internals)
- Network panel analysis
- Network Packet Capture Tools:
- Wireshark
- tcpdump
- Logging Systems:
- Client-side logs
- Server-side logs
Using webrtc-internals:
- Enter
chrome://webrtc-internalsin Chrome’s address bar - Locate the entry for your page tab
- Monitor key metrics:
- ICE connection state
- DTLS handshake state
- Bandwidth usage
- Packet loss rate
Common Issues and Solutions
Connection Establishment Failure
Possible Causes:
- NAT traversal failure
- Firewall blocking
- Incorrect STUN/TURN server configuration
Solutions:
- Check ICE connection state
- Verify STUN/TURN server reachability
- Try adding TURN servers
- Check firewall settings
Media Stream Not Displaying
Possible Causes:
- Device permission issues
- Codec mismatch
- Insufficient network bandwidth
Solutions:
- Check browser permissions
- Verify media stream status
- Adjust video resolution
- Check network conditions
High Latency or Stuttering
Possible Causes:
- Insufficient network bandwidth
- Poor routing path
- Improper codec settings
Solutions:
- Reduce video resolution
- Adjust frame rate
- Check network routing
- Optimize codec parameters
Extensions and Optimizations
Performance Optimization
- Bandwidth Adaptation:
- Dynamically adjust video quality
- Bitrate control based on network conditions
- Hardware Acceleration:
- Enable GPU acceleration
- Optimize codec settings
- Connection Optimization:
- Adjust ICE candidate priorities
- Multi-path transmission
Feature Extensions
- Screen Sharing:
- Use
getDisplayMediaAPI - Optimize screen content encoding
- Use
- File Transfer:
- Transmit via data channel
- Chunk large files for transfer
- Multi-Party Calls:
- Implement SFU architecture
- Implement MCU architecture
Conclusion
Implementing one-to-one real-time communication with WebRTC involves multiple technical components, from signaling exchange to media transmission and security encryption. This document has covered:
- Complete Implementation Flow: From HTML structure to JavaScript code
- Key Technical Points: SDP exchange, ICE candidates, DTLS-SRTP encryption
- Practical Deployment Considerations: Signaling server, TURN server configuration
- Testing and Debugging Methods: Developer tools, network packet capture
- Common Issue Resolutions: Connection issues, media issues, performance issues
By mastering these concepts, developers can build stable and efficient one-to-one WebRTC communication applications. As WebRTC technology continues to evolve, future advancements will support more advanced features, such as improved NAT traversal and more efficient codecs.



