WebRTC Overview
WebRTC (Web Real-Time Communication) enables peer-to-peer audio and video streaming directly between browsers without server intermediation for media transport.
Key Concepts
- Peer Connection: Direct connection between two participants
- Media Tracks: Individual audio or video streams
- ICE (Interactive Connectivity Establishment): NAT traversal protocol
- SDP (Session Description Protocol): Metadata for media capabilities
- STUN/TURN: Servers for NAT traversal and relay
Architecture Components
PeerConnectionManager
The PeerConnectionManager class manages all peer-to-peer connections for a meeting participant.
Location: client/src/lib/webrtc/PeerConnection.ts:35
export class PeerConnectionManager {
private peerConnections: Map<string, RTCPeerConnection> = new Map();
private localStream: MediaStream | null = null;
private config: PeerConnectionConfig;
// Callbacks for signaling
public onIceCandidate?: (peerId: string, candidate: RTCIceCandidate) => void;
public onRemoteStream?: (peerId: string, stream: MediaStream) => void;
public onConnectionStateChange?: (peerId: string, state: RTCPeerConnectionState) => void;
}
Each participant maintains a separate RTCPeerConnection for every other participant. In a 4-person meeting, each peer has 3 active peer connections.
The MediaManager class handles local media device access and track management.
Location: client/src/lib/webrtc/MediaManager.ts:12
export class MediaManager {
private localStream: MediaStream | null = null;
private screenStream: MediaStream | null = null;
async getLocalStream(constraints?: MediaConstraints): Promise<MediaStream>
async startScreenShare(): Promise<MediaStream>
toggleAudio(enabled: boolean): void
toggleVideo(enabled: boolean): void
}
ICE Configuration
STUN Servers
STUN (Session Traversal Utilities for NAT) servers help peers discover their public IP addresses and port mappings.
Default Configuration: client/src/lib/webrtc/PeerConnection.ts:24-33
const DEFAULT_ICE_SERVERS: RTCIceServer[] = [
// Google's free STUN servers
{ urls: "stun:stun.l.google.com:19302" },
{ urls: "stun:stun1.l.google.com:19302" },
{ urls: "stun:stun2.l.google.com:19302" },
{ urls: "stun:stun3.l.google.com:19302" },
{ urls: "stun:stun4.l.google.com:19302" },
];
STUN servers are used for NAT traversal in most network environments. They work for symmetric NAT and most corporate networks.
TURN Servers (Optional)
TURN (Traversal Using Relays around NAT) servers relay media when direct peer-to-peer connection fails.
Configuration: client/src/lib/webrtc/PeerConnection.ts:6-22
const getTurnServers = (): RTCIceServer[] => {
const turnUrl = import.meta.env.VITE_TURN_URL;
const turnUsername = import.meta.env.VITE_TURN_USERNAME;
const turnCredential = import.meta.env.VITE_TURN_CREDENTIAL;
if (turnUrl && turnUsername && turnCredential) {
return [
{ urls: turnUrl, username: turnUsername, credential: turnCredential },
{
urls: turnUrl.replace("turn:", "turns:").replace(":3478", ":5349"),
username: turnUsername,
credential: turnCredential,
},
];
}
return [];
};
Environment Variables: .env.example:25-28
# TURN Server (optional - for production)
TURN_SERVER_URL=
TURN_USERNAME=
TURN_PASSWORD=
TURN servers consume significant bandwidth as they relay all media. Only needed for ~10% of connections behind strict firewalls. Popular TURN server providers include Twilio, Xirsys, and self-hosted coturn.
Peer Connection Lifecycle
1. Initialization
When a participant joins a room, peer connections are created for existing participants.
Reference: client/src/hooks/useWebRTC.ts:165-170
// Add existing participants and create offers
data.participants.forEach((p: Participant) => {
addParticipant(p);
// Create offer to existing participants
createOfferForPeer(p.socketId);
});
2. Creating an Offer
The new participant creates SDP offers for each existing peer.
Reference: client/src/lib/webrtc/PeerConnection.ts:132-150
async createOffer(peerId: string): Promise<RTCSessionDescriptionInit | null> {
let pc = this.peerConnections.get(peerId);
if (!pc) {
pc = await this.createPeerConnection(peerId);
}
try {
const offer = await pc.createOffer({
offerToReceiveAudio: true,
offerToReceiveVideo: true,
});
await pc.setLocalDescription(offer);
return offer;
} catch (error) {
console.error("Error creating offer:", error);
return null;
}
}
3. Handling an Offer
The receiving peer creates an SDP answer.
Reference: client/src/lib/webrtc/PeerConnection.ts:152-171
async handleOffer(
peerId: string,
offer: RTCSessionDescriptionInit,
): Promise<RTCSessionDescriptionInit | null> {
let pc = this.peerConnections.get(peerId);
if (!pc) {
pc = await this.createPeerConnection(peerId);
}
try {
await pc.setRemoteDescription(new RTCSessionDescription(offer));
const answer = await pc.createAnswer();
await pc.setLocalDescription(answer);
return answer;
} catch (error) {
console.error("Error handling offer:", error);
return null;
}
}
4. ICE Candidate Exchange
Peers exchange ICE candidates to establish the optimal connection path.
Reference: client/src/lib/webrtc/PeerConnection.ts:99-103
// Handle ICE candidates
pc.onicecandidate = (event) => {
if (event.candidate) {
this.onIceCandidate?.(peerId, event.candidate);
}
};
Reference: client/src/hooks/useWebRTC.ts:65-70
pcManager.current.onIceCandidate = (peerId, candidate) => {
socketClient.emit("ice-candidate", {
targetId: peerId,
candidate: candidate.toJSON(),
});
};
ICE candidates represent possible network paths. The WebRTC engine tests all candidates and selects the best one (lowest latency, highest bandwidth).
5. Connection Established
Once ICE negotiation completes, media tracks are received.
Reference: client/src/lib/webrtc/PeerConnection.ts:106-111
// Handle remote tracks
pc.ontrack = (event) => {
const stream = event.streams[0];
if (stream) {
this.onRemoteStream?.(peerId, stream);
}
};
Reference: client/src/lib/webrtc/MediaManager.ts:16-49
async getLocalStream(constraints?: MediaConstraints): Promise<MediaStream> {
// Reuse existing stream if it's still active
if (this.localStream && this.localStream.active) {
const videoTracks = this.localStream.getVideoTracks();
const audioTracks = this.localStream.getAudioTracks();
if (videoTracks.length > 0 || audioTracks.length > 0) {
return this.localStream;
}
}
const defaultConstraints: MediaConstraints = {
video: {
width: { ideal: 1280 },
height: { ideal: 720 },
facingMode: "user",
},
audio: {
echoCancellation: true,
noiseSuppression: true,
autoGainControl: true,
},
};
try {
this.localStream = await navigator.mediaDevices.getUserMedia(
constraints || defaultConstraints,
);
return this.localStream;
} catch (error) {
console.error("Error getting local stream:", error);
throw error;
}
}
Audio Processing
Neuron Meet enables audio processing constraints for better call quality:
- Echo Cancellation: Prevents audio feedback loops
- Noise Suppression: Reduces background noise
- Auto Gain Control: Normalizes volume levels
Reference: client/src/lib/webrtc/MediaManager.ts:33-37
Video Quality
Default video resolution is 720p (1280x720). This balances quality and bandwidth.
Reference: client/src/lib/webrtc/MediaManager.ts:28-32
For low-bandwidth environments, consider implementing adaptive bitrate by adjusting video constraints based on network conditions.
Screen Sharing
Screen sharing replaces the video track in peer connections.
Starting Screen Share
Reference: client/src/lib/webrtc/MediaManager.ts:68-89
async startScreenShare(): Promise<MediaStream> {
try {
this.screenStream = await navigator.mediaDevices.getDisplayMedia({
video: true,
// Capturing system/tab audio is a frequent source of echo loops in calls.
audio: false,
});
// Handle when user stops sharing via browser UI
this.screenStream.getVideoTracks()[0].onended = () => {
this.stopScreenShare();
window.dispatchEvent(new CustomEvent("screenshare-ended"));
};
return this.screenStream;
} catch (error) {
if ((error as Error).name === "NotAllowedError") {
throw new Error("Screen sharing permission denied");
}
throw error;
}
}
Track Replacement
When screen sharing starts, the camera video track is replaced.
Reference: client/src/lib/webrtc/PeerConnection.ts:209-220
async replaceVideoTrack(newTrack: MediaStreamTrack): Promise<void> {
const promises: Promise<void>[] = [];
this.peerConnections.forEach((pc) => {
const sender = pc.getSenders().find((s) => s.track?.kind === "video");
if (sender) {
promises.push(sender.replaceTrack(newTrack));
}
});
await Promise.all(promises);
}
Screen sharing audio is disabled by default to prevent echo loops. If enabled, implement acoustic echo cancellation on the receiving end.Reference: client/src/lib/webrtc/MediaManager.ts:72-73
Muting/Unmuting Audio
Reference: client/src/lib/webrtc/MediaManager.ts:118-124
toggleAudio(enabled: boolean): void {
if (this.localStream) {
this.localStream.getAudioTracks().forEach((track) => {
track.enabled = enabled;
});
}
}
Toggling Video
Reference: client/src/lib/webrtc/MediaManager.ts:126-132
toggleVideo(enabled: boolean): void {
if (this.localStream) {
this.localStream.getVideoTracks().forEach((track) => {
track.enabled = enabled;
});
}
}
Toggling track.enabled mutes locally without renegotiating peer connections. The track continues flowing but sends silence/black frames.
Connection Monitoring
Connection State Changes
Reference: client/src/lib/webrtc/PeerConnection.ts:114-121
// Monitor connection state
pc.onconnectionstatechange = () => {
this.onConnectionStateChange?.(peerId, pc.connectionState);
// Clean up failed connections
if (pc.connectionState === "failed" || pc.connectionState === "closed") {
this.closePeerConnection(peerId);
}
};
Connection States
- new: Just created, no ICE processing yet
- connecting: ICE candidates being exchanged
- connected: Successfully connected, media flowing
- disconnected: Connection temporarily lost (e.g., network switch)
- failed: Connection failed permanently
- closed: Connection closed intentionally
Bandwidth Usage
For a 720p video call:
- Video: ~1-2 Mbps per peer
- Audio: ~50-100 Kbps per peer
- Total: In a 4-person meeting, each peer sends 1 stream and receives 3 streams (~3-6 Mbps down, 1-2 Mbps up)
CPU Usage
WebRTC encoding/decoding is hardware-accelerated when available:
- Modern devices: Negligible CPU usage
- Older devices: May struggle with multiple 720p streams
For better performance on low-end devices, implement simulcast (sending multiple quality levels) or consider switching to an SFU architecture.
Troubleshooting
Connection Issues
-
ICE Connection Failed: Usually NAT/firewall issues
- Solution: Configure TURN servers
- Check firewall allows UDP traffic
-
No Audio/Video: Permission or device issues
- Check browser permissions
- Verify device availability
- Check for conflicts with other apps
-
One-Way Audio/Video: Asymmetric NAT
- Solution: TURN server required
Chrome DevTools provides WebRTC internals:
- Navigate to
chrome://webrtc-internals/
- View connection stats, ICE candidates, and bandwidth usage
Integration with Signaling
WebRTC requires a signaling channel to exchange connection metadata. Neuron Meet uses Socket.io for this.
Key Events:
offer: Send SDP offer to peer
answer: Send SDP answer to peer
ice-candidate: Exchange ICE candidates
Reference: See Signaling Architecture for details.
Related Pages