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

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 è:

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:

Un server di signaling tipico:

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:

12. Ottimizzazione e considerazioni avanzate

Per applicazioni di produzione, ci sono ulteriori aspetti da considerare:

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à.