Integrating VoIP Calls via WebRTC on Website
WebRTC (Web Real-Time Communication) — browser standard for peer-to-peer media streams. Allows organizing audio and video calls directly in browser without plugins. Scenarios: visitor calling support, video consultation, voice chat in game.
WebRTC Call Components
Signaling server — not part of WebRTC standard, implemented separately. Exchanges SDP (Session Description Protocol) and ICE candidates between clients. Usually via WebSocket.
STUN server — helps clients determine external IP and port (NAT traversal). Can use free Google STUN: stun:stun.l.google.com:19302.
TURN server — relay server for cases when direct P2P impossible (symmetric NAT, corporate firewall). Mandatory for production. Recommended coturn.
Basic Two-Browser Call
// ICE server configuration
const configuration = {
iceServers: [
{ urls: 'stun:stun.l.google.com:19302' },
{
urls: 'turn:turn.yourserver.ru:3478',
username: 'user',
credential: 'pass'
}
]
};
// Create connection
const pc = new RTCPeerConnection(configuration);
// Capture microphone
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
stream.getTracks().forEach(track => pc.addTrack(track, stream));
// Handle incoming media stream
pc.ontrack = (event) => {
const remoteAudio = document.getElementById('remote-audio');
remoteAudio.srcObject = event.streams[0];
};
// Create offer (call initiator)
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
// Send offer via signaling (WebSocket)
signalingSocket.send(JSON.stringify({
type: 'offer',
sdp: offer,
to: targetUserId
}));
// Handle ICE candidates
pc.onicecandidate = (event) => {
if (event.candidate) {
signalingSocket.send(JSON.stringify({
type: 'ice_candidate',
candidate: event.candidate,
to: targetUserId
}));
}
};
Signaling Server on Node.js
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });
const clients = new Map(); // userId → WebSocket
wss.on('connection', (ws, req) => {
const userId = extractUserId(req);
clients.set(userId, ws);
ws.on('message', (data) => {
const message = JSON.parse(data);
const { type, to } = message;
if (['offer', 'answer', 'ice_candidate'].includes(type)) {
const target = clients.get(to);
if (target && target.readyState === WebSocket.OPEN) {
target.send(JSON.stringify({ ...message, from: userId }));
}
}
});
ws.on('close', () => clients.delete(userId));
});
Call Functions in React
function CallButton({ targetUserId }) {
const [callState, setCallState] = useState('idle'); // idle | calling | connected
const pcRef = useRef(null);
const startCall = async () => {
setCallState('calling');
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
pcRef.current = new RTCPeerConnection(configuration);
stream.getTracks().forEach(t => pcRef.current.addTrack(t, stream));
// ... (create offer, send via signaling)
};
const endCall = () => {
pcRef.current?.close();
setCallState('idle');
};
return (
<button onClick={callState === 'idle' ? startCall : endCall}>
{callState === 'idle' ? '📞 Call' : '❌ End'}
</button>
);
}
Connection Quality
- OPUS codec — standard WebRTC audio codec, good for speech
- Adaptive bitrate — WebRTC automatically adjusts quality to channel
-
Echo cancellation — built-in to browser (
echoCancellation: truein constraints) - Noise suppression — likewise built-in
Quality Monitoring
// WebRTC Stats API
const stats = await pc.getStats();
stats.forEach(report => {
if (report.type === 'inbound-rtp' && report.kind === 'audio') {
console.log('Jitter:', report.jitter);
console.log('Packet loss:', report.packetsLost / report.packetsReceived);
console.log('RTT:', report.roundTripTime);
}
});
TURN Server (coturn)
# /etc/turnserver.conf
listening-port=3478
tls-listening-port=5349
fingerprint
lt-cred-mech
user=user:password
realm=yourserver.ru
Without TURN about 15–20% calls won't establish due to network restrictions. With TURN — nearly 100%.
Development timeline: 3–5 weeks for complete implementation with signaling server, TURN and call interface.







