WebRTC (Web Real-Time Communication) è un insieme di API e protocolli che permette ai browser e alle applicazioni di comunicare in tempo reale tramite audio, video e dati, senza richiedere plugin esterni. È particolarmente usato per videochiamate, chat vocali, condivisione schermo e applicazioni di collaborazione in tempo reale.
L'obiettivo principale di WebRTC è stabilire una connessione diretta (peer-to-peer) tra due client, riducendo la latenza e il carico sui server. I server rimangono comunque fondamentali per la fase di segnalazione (signaling) e, in alcuni casi, per l'instradamento dei media.
2. Componenti principali di WebRTC
- MediaDevices/getUserMedia: API che permette di accedere a microfono, webcam e schermo dell'utente, previa autorizzazione.
- RTCPeerConnection: oggetto che gestisce la connessione peer-to-peer, il trasporto dei flussi audio/video e la negoziazione ICE.
- RTCDataChannel: canale dati affidabile o non affidabile per scambiare messaggi arbitrari (testo, binario) tra i peer.
-
ICE, STUN, TURN:
- ICE (Interactive Connectivity Establishment) definisce come trovare il percorso migliore tra due peer.
- STUN (Session Traversal Utilities for NAT) aiuta a scoprire l'indirizzo pubblico del client dietro NAT.
- TURN (Traversal Using Relays around NAT) inoltra il traffico attraverso un server quando una connessione diretta non è possibile.
3. Flusso generale di una connessione WebRTC
- Ogni peer ottiene i propri media (audio/video) con
getUserMedia. - Ogni peer crea un oggetto
RTCPeerConnectioncon la configurazione ICE (STUN/TURN). - Il primo peer (chiamante) crea un offer SDP e lo invia all'altro peer tramite un canale di signaling (per esempio WebSocket).
- Il secondo peer (chiamato) riceve l'offer, crea un answer SDP e lo restituisce al chiamante via signaling.
- Entrambi i peer scambiano candidate ICE via signaling finché non trovano un percorso di rete funzionante.
- Quando la connessione viene stabilita, i media e i dati fluiscono direttamente tra i peer.
4. HTML minimo per una videochiamata
Un esempio di struttura HTML essenziale con due elementi video: uno per il flusso locale e uno per il remoto.
<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="UTF-8" />
<title>Demo WebRTC</title>
</head>
<body>
<h1>Demo WebRTC</h1>
<video id="localVideo" autoplay playsinline muted></video>
<video id="remoteVideo" autoplay playsinline></video>
<button id="startCall">Avvia chiamata</button>
<button id="hangup">Termina</button>
<script src="app.js"></script>
</body>
</html>
5. Ottenere l'accesso a microfono e webcam
Il primo passo in molte applicazioni WebRTC è ottenere un flusso audio/video locale. In JavaScript
utilizziamo l'API navigator.mediaDevices.getUserMedia:
const constraints = {
audio: true,
video: { width: 1280, height: 720 }
};
let localStream;
async function initLocalMedia() {
try {
localStream = await navigator.mediaDevices.getUserMedia(constraints);
const localVideo = document.getElementById("localVideo");
localVideo.srcObject = localStream;
} catch (err) {
console.error("Errore nell'ottenere i media locali:", err);
}
}
initLocalMedia();
È importante gestire gli errori: l'utente potrebbe negare i permessi o il dispositivo potrebbe non avere una webcam o un microfono disponibili.
6. Creazione di una RTCPeerConnection
Per creare la connessione WebRTC definiamo una configurazione ICE, tipicamente con uno o più server STUN (ed eventualmente TURN):
const iceConfig = {
iceServers: [
{ urls: "stun:stun.l.google.com:19302" }
// Qui, in produzione, inseriresti anche i server TURN
]
};
let pc;
function createPeerConnection() {
pc = new RTCPeerConnection(iceConfig);
// Aggiungo i flussi locali alla connessione
localStream.getTracks().forEach(track => {
pc.addTrack(track, localStream);
});
// Gestione delle tracce remote
pc.addEventListener("track", event => {
const [remoteStream] = event.streams;
const remoteVideo = document.getElementById("remoteVideo");
remoteVideo.srcObject = remoteStream;
});
// Gestione delle candidate ICE generate localmente
pc.addEventListener("icecandidate", event => {
if (event.candidate) {
// Invia la candidate al peer remoto tramite signaling
sendToSignalingServer({
type: "candidate",
candidate: event.candidate
});
}
});
}
La funzione sendToSignalingServer è una funzione personalizzata che dovrai implementare usando,
ad esempio, WebSocket o un'API REST. WebRTC non specifica il meccanismo di signaling, lasciando la scelta
allo sviluppatore.
7. Creazione dell'offer e dell'answer
Una volta creata la RTCPeerConnection, il peer chiamante genera un'offer SDP. Il peer chiamato
genera un'answer SDP. Entrambe vengono scambiate tramite il server di signaling.
async function startCall() {
createPeerConnection();
try {
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
// Invia l'offer al peer remoto via signaling
sendToSignalingServer({
type: "offer",
sdp: offer
});
} catch (err) {
console.error("Errore nella creazione dell'offer:", err);
}
}
// Gestione dei messaggi in arrivo dal signaling
async function handleSignalingMessage(message) {
switch (message.type) {
case "offer":
await handleOffer(message.sdp);
break;
case "answer":
await handleAnswer(message.sdp);
break;
case "candidate":
await handleCandidate(message.candidate);
break;
}
}
async function handleOffer(offer) {
if (!pc) {
createPeerConnection();
}
await pc.setRemoteDescription(new RTCSessionDescription(offer));
const answer = await pc.createAnswer();
await pc.setLocalDescription(answer);
sendToSignalingServer({
type: "answer",
sdp: answer
});
}
async function handleAnswer(answer) {
await pc.setRemoteDescription(new RTCSessionDescription(answer));
}
async function handleCandidate(candidate) {
try {
await pc.addIceCandidate(new RTCIceCandidate(candidate));
} catch (err) {
console.error("Errore nell'aggiunta della candidate:", err);
}
}
Il flusso base è:
- Il chiamante crea l'offer, la imposta come descrizione locale e la invia al chiamato.
- Il chiamato imposta l'offer come descrizione remota, crea l'answer, la imposta come descrizione locale e la invia al chiamante.
- Entrambi impostano l'answer come descrizione remota sul proprio
RTCPeerConnection.
8. Aggiungere un RTCDataChannel
Oltre a audio e video, WebRTC permette di scambiare dati arbitrari tramite RTCDataChannel.
Questo è utile per chat testuali, sincronizzazione di stato tra client, trasferimento file e molto altro.
let dataChannel;
function createDataChannel() {
dataChannel = pc.createDataChannel("chat");
dataChannel.addEventListener("open", () => {
console.log("DataChannel aperto");
});
dataChannel.addEventListener("message", event => {
console.log("Messaggio ricevuto:", event.data);
});
}
// Sul lato chiamato, ascoltiamo l'evento datachannel
pc.addEventListener("datachannel", event => {
dataChannel = event.channel;
dataChannel.addEventListener("open", () => {
console.log("DataChannel aperto (lato chiamato)");
});
dataChannel.addEventListener("message", event => {
console.log("Messaggio ricevuto:", event.data);
});
});
// Per inviare un messaggio
function sendChatMessage(text) {
if (dataChannel && dataChannel.readyState === "open") {
dataChannel.send(text);
}
}
9. Signaling: concetto e implementazione
WebRTC non definisce come due peer debbano trovarsi e scambiarsi offer, answer e candidate ICE. Questo compito è affidato al sistema di signaling, che puoi implementare in vari modi:
- WebSocket (molto comune per applicazioni in tempo reale).
- REST API + polling o long polling.
- Qualsiasi altro canale bidirezionale affidabile.
Un server di signaling tipico:
- Gestisce le stanze o le sessioni (per esempio stanze numerate o con codice univoco).
- Riceve i messaggi da un peer (offer, answer, candidate) e li inoltra agli altri peer della stessa stanza.
- Non vede i media: si limita a inoltrare metadati e messaggi di controllo.
10. Gestione degli stati e chiusura della chiamata
Una buona applicazione WebRTC deve gestire correttamente la chiusura della chiamata e gli stati della connessione:
function hangup() {
if (pc) {
pc.getSenders().forEach(sender => sender.track && sender.track.stop());
pc.close();
pc = null;
}
if (dataChannel) {
dataChannel.close();
dataChannel = null;
}
}
// Ascolta i cambiamenti di stato
if (pc) {
pc.addEventListener("connectionstatechange", () => {
console.log("Stato connessione:", pc.connectionState);
if (pc.connectionState === "failed" || pc.connectionState === "disconnected") {
hangup();
}
});
}
11. Sicurezza e permessi
WebRTC è strettamente legato alla sicurezza del browser:
- L'accesso a microfono e webcam richiede sempre il consenso esplicito dell'utente.
- Le API WebRTC funzionano solo su contesti sicuri (HTTPS o
localhost). - È importante gestire correttamente i permessi negati e informare l'utente su come attivarli.
12. Ottimizzazione e considerazioni avanzate
Per applicazioni di produzione, ci sono ulteriori aspetti da considerare:
- Scalabilità: per chiamate tra molti partecipanti, è comune usare un server SFU (Selective Forwarding Unit) per instradare i flussi media invece di connessioni completamente peer-to-peer.
- Qualità del servizio: gestire bitrate, risoluzione e codec per adattarsi alla banda disponibile.
- Registrazione: per registrare una sessione video si possono usare sia WebRTC lato server, sia le API di registrazione media lato client.
- Compatibilità: sebbene il supporto a WebRTC sia molto diffuso, è sempre bene verificare le differenze tra browser e piattaforme.
13. Conclusione
WebRTC in JavaScript offre una potente piattaforma per costruire applicazioni di comunicazione in tempo reale direttamente nel browser. Comprendendo i concetti chiave (getUserMedia, RTCPeerConnection, ICE, STUN/TURN, DataChannel) e progettando con attenzione il sistema di signaling, puoi creare videochat, sistemi di collaborazione, giochi multiplayer e molto altro con latenza ridotta e alta flessibilità.