WebRTC: Comunicazioni Real Time per il Web

21 marzo 2013

Una semplice chat peer-to-peer

Chiamiamo index.html il file che conterrà il codice della parte client del nostro progetto e cominciamo e definirne il markup; per gestire la comunicazione bilaterale saranno sufficienti due elementi video, uno contenente lo stream della nostra webcam e uno con quello della webcam del peer:

<!doctype html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Una semplice video chat p2p</title>
  </head>
  <body>

    <video width="300" id="video_locale" autoplay="autoplay"></video>
    <video width="300" id="video_remoto" autoplay="autoplay"></video>

    <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.8.3/jquery.min.js"></script>
    <script src="js/application.js"></script>
  </body>
</html>

Ora passiamo alla parte JavaScript: creiamo un file application.js in una cartella js e soffermiamoci un poco per pianificare il flusso che dovrà seguire l’intera connessione:

  1. L’utente che vuole iniziare una chat peer-to-peer accede ad index.html da un browser;
  2. la pagina web apre una connessione websocket e genera una chiave randomica come identificativo della connessione. Per comodità questa chiave viene aggiunta automaticamente in coda all’url nella barra degli indirizzi (es: http://miowebserver/webrtc/index.html#403 );
  3. la pagina invia attraverso la connessione websocket il messaggio c:x, dove x è la chiave randomica del punto precedente. In questo modo il websocket crea un canale di comunicazione identificato da x e vi aggiunge il client.
  4. la pagina si mette in ascolto di eventuali messaggi in arrivo attraverso il canale websocket appena creato.

Ora l’utente deve invitare un partner alla chat, per farlo può mandare tramite e-mail l’indirizzo della pagina web, comprensivo della chiave randomica. Il partner segue il link ricevuto e atterra sul medesimo index.html dell’utente con la differenza che in questo caso l’url contiene già una chiave.

L’applicazione web quindi può utilizzare e inviare al server websocket l’identificativo già presente nell’url invece che crearne uno nuovo. Quest’operazione mette di fatto entrambi i client sullo stesso canale di comunicazione websocket e li abilita in definitiva allo scambio di messaggi.

Vediamo il codice (da inserire in application.js) per realizzare questa prima parte:

  // --- gestione del prefisso sperimentale ---
  window.RTCPeerConnection = window.webkitRTCPeerConnection;
  navigator.getUserMedia = navigator.webkitGetUserMedia ;
  window.URL = window.URL || window.webkitURL;

  // --- variabili iniziali ---
  var video_locale = document.getElementById("video_locale"),
      video_remoto = document.getElementById("video_remoto"),
      ws = new WebSocket('ws://__IP_SERVER_WEBSOCKET__:8080'),
      peer;

  if(location.hash === ""){
    // in questo caso l'URL non contiene la chiave e devo crearne una
    var stanza = Math.round(Math.random()*1000),
        chiamante = 0;
    location.hash = stanza;
  }else{
    // in questo caso l'URL contiene già la chiave
    var stanza = location.hash.substr(1),
        chiamante = 1;
  }

  // --- init dall'applicazione all'apertura del canale websocket ---
  ws.addEventListener('open', function(){ 
    // invio al server websocket l'identificativo del canale di 
    // comunicazione al quale mi voglio connettere.
    ws.send("c:" + stanza);
    inizializza_video();  // dobbiamo ancora scrivere questa funzione.
  }, false);

La variabile chiamante è valorizzata sulla base della presenza o meno della chiave univoca ed è utilizzata nella prossima parte del codice per distinguere se l’utente sta aprendo una chat o sta cercando di accedere ad una chat esistente. Nel primo caso tutto ciò che è richiesto dalle API è rimanere in attesa della richiesta di connessione da parte del partner, nel secondo caso invece l’applicazione deve creare per prima un set di candidati ICE ed una offerta SDP ed inviare il tutto tramite il canale di comunicazione attraverso il server websocket.

Ora definiamo inizializza_video; i compiti di questa funzione sono molteplici:

  • Ottenere, tramite la chiamata getUserMedia, l’accesso alla webcam e al microfono dell’utente;
  • visualizzare lo stream video proveniente dalla webcam dell’utente nell’elemento con id video_locale;
  • istanziare una RTCPeerConnection ed associare una funzione all’evento icecandidate della stessa;
  • associare all’evento addstream una funzione che visualizzi lo stream remoto nell’elemento con id video_remoto;
  • aggiungere lo stream locale all’istanza RTCPeerConnection e, se l’applicazione è già in possesso di una chiave alla quale connettersi (chiamante = 1), invocare la funzione per la creazione dell’offerta SDP da inviare al partner.

Ed ecco il codice della funzione, da aggiungere all’interno di application.js

// --- configurazione --- 
var mediaConstraints = {'mandatory': {'OfferToReceiveAudio':true, 'OfferToReceiveVideo':true }};
var peer_config = {"iceServers": [{"url": "stun:stunserver.org"}]};

// --- richiesta accesso webcam e microfono e init della connessione P2P ---
function inizializza_video() {
  navigator.getUserMedia( {'audio':true, 'video':true}, 
    function(stream) {

      video_locale.src = URL.createObjectURL(stream);
      peer = new RTCPeerConnection(peer_config);
      peer.onicecandidate = onIceCandidate;
      peer.onaddstream = function(event){
        video_remoto.src = URL.createObjectURL(event.stream);
      };

      peer.addStream(stream);
      if (chiamante)
        peer.createOffer(sdpcreato, null, mediaConstraints);
    }
  );
}

Bene, a questo punto dobbiamo creare le due funzioni che abbiamo definito come callbacks nel listato precedente: onIceCandidate e sdpcreato. La prima delle due funzioni deve inviare il nuovo candidato ICE al partner mentre la seconda deve inviare sempre al partner il descrittore SDP prodotto, ecco il codice da accodare in application.js:

  // --- invio l'SDP al peer ---
  function sdpcreato(sdp) {
    peer.setLocalDescription(sdp);
    messaggio_da_inviare(sdp);
  }

  // --- invio il candidato ICE al peer ---
  function onIceCandidate(event) {
    if (event.candidate) {
      messaggio_da_inviare(event.candidate);
    } 
  }

Ora, per concludere, è necessario definire la funzione messaggio_da_inviare che, ricordiamo, deve inviare il messaggio attraverso il server websocket nel formato m:x:txt dove x è la chiave del canale di comunicazione attraverso il quale deve transitare il messaggio. Anche lo stesso messaggio txt è a sua volta strutturato in frammenti nel formato y:json dove y assume lo stesso valore della variabile chiamante mentre json è la serializzazione JSON del messaggio, che di volta in volta potrebbe essere un candidato ICE o il descrittore SDP.

Ecco la funzione, sempre da aggiungere ad application.js

  // --- invio messaggi al websocket ---
  function messaggio_da_inviare(msg) {
    var msgjson = JSON.stringify(msg);
    ws.send("m:"+ stanza + ":" + chiamante + ":" + msgjson);
  }

Gestire i messaggi dal server

L’ultima parte del progetto riguarda la gestione dei messaggi in arrivo dal server WebSocket, in questo caso le attività da fare sono:

  • Ignorare tutti i messaggi (ricordiamo nel formato m:x:y:json) il cui valore y sia lo stesso della variabile chiamante in quanto sono gli stessi messaggi inviati dall’applicazione al partner remoto. Questo accade perché l’implementazione del server websocket utilizzata invia il messaggio a tutti gli appartenenti al canale di comunicazione identificato compreso lo stesso mittente del messaggio.
  • Deserializzare i messaggi provenienti dal partner e valutarne la tipologia:
    • se la tipologia è di tipo offer si tratta di una offerta SDP, è quindi necessario produrre una corrispondente answer ed inviarla al partner;
    • se la tipologia è di tipo answer dobbiamo soltanto memorizzare la risposta;
    • in ogni altro caso il messaggio è sicuramente un candidato ICE inviatoci dal partner e per questo va incapsulato in una istanza di RTCIceCandidate e processato dall’istanza di RTCPeerConnection dell’applicazione.

Ed ora completiamo il progetto aggiungendo questa ultima porzione di codice ad application.js:

  // --- ricezione messaggi dal websocket ---
  ws.addEventListener('message', function(evt){
    var msg = evt.data;
    if(parseInt(msg.substr(0,1),10) !== chiamante){
      processa(msg.substr(2));
    }
  });  

  // --- interpretazione dei messaggi ricevuti per stabilire la connessione P2P ---
  function processa(messaggio) {
    var msg = JSON.parse(messaggio);
    if (msg.type === 'offer') {
      peer.setRemoteDescription(new RTCSessionDescription(msg));
      peer.createAnswer(sdpcreato, null, mediaConstraints);
    } else if (msg.type === 'answer') {
      peer.setRemoteDescription(new RTCSessionDescription(msg));
    } else {
      var candidate = new RTCIceCandidate(msg);
      peer.addIceCandidate(candidate);
    }
  }

Bene, sincerandoci di aver eseguito webserver.rb possiamo testare la nostra realizzazione navigando con Chrome alla pagina index.htm che abbiamo sviluppato, dando il consenso all’utilizzo di microfono e webcam e condividendo il link generato con un amico o un collega all’interno della nostra stessa LAN (a meno di non aver pubblicato il server websocket in Internet, in qual caso l’invito è estendibile a chiunque sia connesso alla rete).

Se vuoi aggiornamenti su WebRTC: Comunicazioni Real Time per il Web inserisci la tua e-mail nel box qui sotto:
 
X
Se vuoi aggiornamenti su WebRTC: Comunicazioni Real Time per il Web

inserisci la tua e-mail nel box qui sotto:

Ho letto e acconsento l'informativa sulla privacy

Acconsento al trattamento di cui al punto 3 dell'informativa sulla privacy