WebRTC in JavaScript

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

  1. Ogni peer ottiene i propri media (audio/video) con getUserMedia.
  2. Ogni peer crea un oggetto RTCPeerConnection con la configurazione ICE (STUN/TURN).
  3. Il primo peer (chiamante) crea un offer SDP e lo invia all'altro peer tramite un canale di signaling (per esempio WebSocket).
  4. Il secondo peer (chiamato) riceve l'offer, crea un answer SDP e lo restituisce al chiamante via signaling.
  5. Entrambi i peer scambiano candidate ICE via signaling finché non trovano un percorso di rete funzionante.
  6. 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à.

Torna su