Getting Started with WebRTC: Real‑Time P2P Communication in Browsers and Mobile Apps
This article introduces WebRTC, compares it with WebSocket, explains its architecture and core APIs, shows how to build a FastAPI‑based signaling server, demonstrates a full JavaScript P2P connection setup with TURN configuration, and provides code for screen sharing and audio/video controls.
1. WebRTC Overview
WebRTC (Web Real‑Time Communication) is an open‑source project that enables browsers and mobile apps to exchange audio, video, screen, and data directly without plugins.
WebRTC Core Capabilities
┌─────────────────────────────────────────────────────────────┐
│ Audio Call Video Call Screen Share │
│ P2P Data File Transfer Game Communication │
└─────────────────────────────────────────────────────────────┘1.2 WebRTC vs WebSocket
Connection method: WebRTC uses P2P (may traverse TURN); WebSocket uses server relay.
Latency: WebRTC offers ultra‑low latency via direct connection; WebSocket has moderate latency.
Server load: WebRTC offloads after direct connection; WebSocket keeps all traffic on the server.
Typical use‑case: WebRTC for audio/video calls; WebSocket for chat and real‑time data.
NAT traversal: WebRTC requires ICE; WebSocket does not.
2. WebRTC Architecture
2.1 Overall Architecture
WebRTC Architecture
┌─────────────────────────────────────────────────────────────┐
│ Web API │
│ getUserMedia RTCPeerConnection RTCDataChannel │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ WebRTC Engine │
│ ICE SRTP DTLS RTP SDP │
│ Network (UDP/TCP) │
└─────────────────────────────────────────────────────────────┘2.2 Core APIs
getUserMedia()– acquire audio/video streams. RTCPeerConnection – manage the P2P connection. RTCDataChannel – send arbitrary data over the P2P link. getDisplayMedia() – capture screen for sharing.
3. Signaling Server
3.1 Role of the Signaling Server
Signaling Server Workflow
User A → Server → User B
SDP Offer → → SDP Offer
← SDP Answer ← ← SDP Answer
ICE Candidates ↔ ICE Candidates
Direct P2P connection established3.2 Implementation with FastAPI and Socket.IO
from fastapi import FastAPI
from fastapi_socketio import SocketManager
import json
app = FastAPI()
sio = SocketManager(app, cors_allowed_origins="*")
@sio.on("join")
async def handle_join(sid, data):
"""User joins a room"""
room = data.get("room")
await sio.enter_room(sid, room)
await sio.emit("user_joined", {"sid": sid}, room=room)
@sio.on("offer")
async def handle_offer(sid, data):
"""Forward SDP Offer"""
await sio.emit("offer", {"sdp": data["sdp"], "from_sid": sid},
room=data["room"], skip_sid=sid)
@sio.on("answer")
async def handle_answer(sid, data):
"""Forward SDP Answer"""
await sio.emit("answer", {"sdp": data["sdp"], "from_sid": sid},
room=data["room"], skip_sid=sid)
@sio.on("ice_candidate")
async def handle_ice_candidate(sid, data):
"""Forward ICE candidate"""
await sio.emit("ice_candidate", {"candidate": data["candidate"], "room": data["room"]},
room=data["room"], skip_sid=sid)
@sio.on("leave")
async def handle_leave(sid, data):
"""User leaves"""
room = data.get("room")
await sio.leave_room(sid, room)
await sio.emit("user_left", {"sid": sid}, room=room)4. Establishing a P2P Connection
4.1 Full JavaScript Implementation
class WebRTCClient {
constructor() {
this.peerConnection = null;
this.localStream = null;
this.roomId = location.hash.slice(1);
}
async initialize() {
// 1. Get local media
this.localStream = await navigator.mediaDevices.getUserMedia({video:true, audio:true});
document.getElementById('localVideo').srcObject = this.localStream;
// 2. Connect to signaling server
this.socket = io();
// 3. Set up socket handlers
this.setupSocketHandlers();
// 4. Join room
this.socket.emit('join', {room: this.roomId});
}
createPeerConnection() {
// Create RTCPeerConnection
this.peerConnection = new RTCPeerConnection({
iceServers: [
{urls: 'stun:stun.l.google.com:19302'},
{urls: 'stun:stun1.l.google.com:19302'}
]
});
// Add local tracks
this.localStream.getTracks().forEach(track => {
this.peerConnection.addTrack(track, this.localStream);
});
// ICE candidate handling
this.peerConnection.onicecandidate = (event) => {
if (event.candidate) {
this.socket.emit('ice_candidate', {
candidate: event.candidate,
room: this.roomId
});
}
};
// Remote stream handling
this.peerConnection.ontrack = (event) => {
document.getElementById('remoteVideo').srcObject = event.streams[0];
};
return this.peerConnection;
}
async call() {
this.createPeerConnection();
// Create SDP Offer
const offer = await this.peerConnection.createOffer();
await this.peerConnection.setLocalDescription(offer);
// Send Offer
this.socket.emit('offer', {sdp: offer, room: this.roomId});
}
async handleOffer(sdp) {
this.createPeerConnection();
await this.peerConnection.setRemoteDescription(new RTCSessionDescription(sdp));
// Create Answer
const answer = await this.peerConnection.createAnswer();
await this.peerConnection.setLocalDescription(answer);
// Send Answer
this.socket.emit('answer', {sdp: answer, room: this.roomId});
}
async handleAnswer(sdp) {
await this.peerConnection.setRemoteDescription(new RTCSessionDescription(sdp));
}
async handleIceCandidate(candidate) {
try {
await this.peerConnection.addIceCandidate(new RTCIceCandidate(candidate));
} catch (e) {
console.error('Error adding ICE candidate:', e);
}
}
setupSocketHandlers() {
this.socket.on('user_joined', (data) => {
console.log('User joined:', data.sid);
if (data.sid !== this.socket.id) {
this.call();
}
});
this.socket.on('offer', (data) => {
this.handleOffer(data.sdp);
});
this.socket.on('answer', (data) => {
this.handleAnswer(data.sdp);
});
this.socket.on('ice_candidate', (data) => {
this.handleIceCandidate(data.candidate);
});
this.socket.on('user_left', (data) => {
console.log('User left:', data.sid);
});
}
}4.2 TURN Server Configuration
const config = {
iceServers: [
// STUN servers
{urls: 'stun:stun.l.google.com:19302'},
{urls: 'stun:stun1.l.google.com:19302'},
// TURN server (for strict NAT)
{
urls: 'turn:your-turn-server.com:3478',
username: 'user',
credential: 'password'
}
]
};5. Media Stream Handling
5.1 Screen Sharing
async function startScreenShare() {
try {
const screenStream = await navigator.mediaDevices.getDisplayMedia({
video: {cursor: 'always'},
audio: false
});
// Replace video track
const videoTrack = screenStream.getVideoTracks()[0];
const sender = peerConnection.getSenders().find(s => s.track.kind === 'video');
if (sender) {
await sender.replaceTrack(videoTrack);
}
// Handle stop sharing
videoTrack.onended = () => {
stopScreenShare();
};
document.getElementById('screenShareVideo').srcObject = screenStream;
} catch (error) {
console.error('Error sharing screen:', error);
}
}
async function stopScreenShare() {
// Restore camera
const cameraStream = await navigator.mediaDevices.getUserMedia({video:true});
const cameraTrack = cameraStream.getVideoTracks()[0];
const sender = peerConnection.getSenders().find(s => s.track.kind === 'video');
if (sender) {
await sender.replaceTrack(cameraTrack);
}
}5.2 Audio/Video Control
// Mute / Unmute
function toggleAudio() {
const audioTrack = localStream.getAudioTracks()[0];
audioTrack.enabled = !audioTrack.enabled;
document.getElementById('audioBtn').textContent =
audioTrack.enabled ? 'Mute' : 'Unmute';
}
// Enable / Disable video
function toggleVideo() {
const videoTrack = localStream.getVideoTracks()[0];
videoTrack.enabled = !videoTrack.enabled;
document.getElementById('videoBtn').textContent =
videoTrack.enabled ? 'Stop Video' : 'Start Video';
}Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Long Ge's Treasure Box
I'm Long Ge, and this is my treasure chest—packed with cutting‑edge tech insights, career‑advancement tips, and a touch of relaxation you crave. Open it daily for a new surprise.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
