¿Te gustaría colaborar en un proyecto de software libre? Conoce P2PSP

Chat Multiusuario con WebRTC

  • Escrito por Cristóbal Medina López
  • Publicado el 29 Marzo 2014

Para entender un poco mejor como vamos a conseguir transmitir vídeo en el navegador mediante P2P sin necesidad de plugins gracias a WebRTC, primero necesitamos entender cómo funcionan los elementos básicos de la API que nos permitirán hacer esto, hablamos de RTCPeerConnection y RTCDataChannel. Para ello, vamos a construir un pequeño ejemplo de un chat multiusuario con el objetivo de entender como trabajar con la API WebRTC.

El chat estará compuesto por los siguientes elementos:

  • Peer: Es la parte del cliente, cada usuario ejecutará un peer en su navegador. El peer lo vamos a escribir en HTML5+JavaScript+WebRTC
  • Server (SignalingServer): Es la parte servidora, su misión es simple, hacer que los peer se conozcan entre sí. Sólo eso, los mensajes nunca pasarán por el servidor, estos serán intercambiados directamente entre los peers. El Server lo vamos a escribir en Python.

Nota: Si todo esto te suena a chino te recomiendo que eches un vistazo a WebRTC Comunicación en tiempo real sin plugins antes de seguir leyendo.

¡Manos a la obra!

El Server (SignalingServer)

Para que los clientes (peer) se conozca entre si es necesario hacer uso de un servidor de señalización que nos de la información necesaria de cada uno para poder establecer una comunicación directa entre ellos. Necesitamos una forma de comunicarnos con los peer que se están ejecutando en el navegador de cada usuario, algo que nos permita intercambiar la información entre el servidor y cada uno de los clientes. La forma más sencilla y directa es usar WebSocket, para ello haremos uso de la siguiente librería SimpleWebSocketServer by opiate, es software libre y nos facilita su implementación en Python.

Partiendo del módulo sólo tenemos que reescribir los meéodos handleMessage, handleConnected y handleClose para que hagan lo que nosotros búscamos, en este caso es muy sencillo, intercambiar la información de cada peer con el resto de peers.

#hanleMessage se ejecuta cuando un mensaje es recibido
def handleMessage(self):
      #recibimos un mensaje (json)
    datos=str(self.data) 
 
        #decodificamos el mensaje (json)
        try:
        decoded = json.loads(datos) 
    except (ValueError, KeyError, TypeError):
        print "JSON format error"
 
        #Reenviamos el mensaje a resto de clientes
    for client in self.server.connections.itervalues():
            if client != self:
              try:
                client.sendMessage(str(self.data))
              except Exception as n:
                  print n
 
#handleConnected se ejecuta cuando un nuevo cliente se conecta
def handleConnected(self):
    global nextid
 
    try:
          #enviamos al cliente su id de peer
      self.sendMessage(str('{"numpeer":"'+str(nextid)+'"}'))
            #enviamos al cliente la lista de peer actual
      self.sendMessage(str('{"peerlist":"'+str(peerlist)+'"}'))
            #agregamos el nuevo peer a la lista
      peerlist.append(nextid)
      peeridlist[self]=nextid
      nextid=nextid+1
    except Exception as n:
      print n
 
#handleClose se ejecuta cuando un cliente se desconecta
def handleClose(self):
      #eliminamos el peer de la lista
    peerlist.remove(peeridlist[self]);

Nota: El código completo del SignalingServer está actualizado y disponible en Launchpad -> P2PSP > Experimentos > ChatMultiusuario > Server.py

El Peer

Como ya hemos dicho, la parte del cliente se ejecuta en el navegador y toda la funcionalidad vamos a escribirla en JavaScript. A continuación sólo vamos a comentar algunas partes del código que son interesantes. Recuerda que puedes acceder al código fuente completo de este experimento en Launchpad -> WebRTCMultiPeerChat

Primero definimos el SignalingServer que será la URL donde se está ejecutando Server.py y configuration para RTCPeerConnection que será el servidor STUN que se encarga de proporcionarnos la información de red externa (IP, Puerto, etc).

var signalingChannel = new WebSocket("ws://127.0.0.1:9876/");
var configuration = {iceServers: [{ url: 'stun:stun.l.google.com:19302' }]}

A continuación se muestra el resto del código necesario para el peer, está comentado para una comprensión más sencilla:

// Inicializamos las conexión
// isInitiator = true o false
// i= id del peer
function start(isInitiator,i) {
     //Inicializamos RTCPeerConnection para el peer i.
  pcs[i] = new webkitRTCPeerConnection(configuration, {optional: []});
 
 
  // Enviar cualquier ICE candidate a los otros peer.
  pcs[i].onicecandidate = function (evt) {
    if (evt.candidate){
      signalingChannel.send(JSON.stringify({ "candidate": evt.candidate , "idtransmitter":'"'+idpeer+'"', "idreceiver":'"'+i+'"'}));
    }
  };
 
 
  // dejar a "negotiationneeded" generar ofertas (Offer)
  pcs[i].onnegotiationneeded = function () {
    pcs[i].createOffer(function(desc){localDescCreated(desc,pcs[i],i);});
    console.log("Create and send OFFER");
  }
 
  if (isInitiator) {
    // Crear el datachannel para ese peer
    channel[i] = pcs[i].createDataChannel("chat"+i);
    setupChat(i);
  } else {
      // Establecer el datachannel para ese peer
    pcs[i].ondatachannel = function (evt) {
      channel[i] = evt.channel;
      setupChat(i);
    };
  }  
  console.log("Saved in slot: "+i+" PeerConection: "+pcs[i]);
}
 
//Establecer localDescriction y y enviarla a los otros peer (ellos la estableceran como remoteDescription)
function localDescCreated(desc,pc,i) {
    pc.setLocalDescription(desc, function () {
  console.log("localDescription is Set");
        signalingChannel.send(JSON.stringify({ "sdp": pc.localDescription , "idtransmitter":'"'+idpeer+'"', "idreceiver":'"'+i+'"'}));
    }, logError);
}
 
//Se ejecuta cuando se recibe un mensaje desde el signalingChannel (WebSocket)
signalingChannel.onmessage = function (evt) {
  handleMessage(evt);
}
 
//Manipular el mensaje
function handleMessage(evt){
  var message = JSON.parse(evt.data);
 
    //Si es el ide del peer se almacena
    if (message.numpeer){    
    idpeer=message.numpeer;
    console.log('Peer ID: '+idpeer);
    return;    
    }  
 
    //Si es la lista de peer se almacena
  if (message.peerlist){    
    console.log('Peer List '+message.peerlist);
    peerlist=JSON.parse(message.peerlist);
    for (i in peerlist){
      console.log("Peer: "+peerlist[i]);
    }
    return;    
    }  
 
    //guardamos el id del que envia el mensaje y el id del que debe recibirlo
    var id=(message.idtransmitter).split('"').join(''); 
  var idreceiver=(message.idreceiver).split('"').join(''); 
    console.log("Received from: "+id+" and send to: "+idreceiver);
 
    //Si es nuevo para este peer se configura la RTCPeerConection y se añade a la lista de peer.
    if (!pcs[id]) { 
    console.log('%cCreate a new PeerConection','background: #222; color: #bada55');
    peerlist.push(id);
    console.log("PEER LIST UPDATE: "+peerlist);
    start(false,id);
    }   
 
    //Si el mensaje va dirigido a mi y es SDP (informacion de la sesion)
    if (message.sdp && idreceiver==idpeer){
        //Estableco la información de la conexión remota (remoteDescription)
    pcs[id].setRemoteDescription(new RTCSessionDescription(message.sdp), function () {
      console.log("remoteDescription is Set");
        // Si recibimos una oferta enviamos una respuesta.
      if (pcs[id].remoteDescription.type == "offer"){
        console.log("Create and send ANSWER");
            pcs[id].createAnswer(function(desc){localDescCreated(desc,pcs[id],id);});
        }
        });
    }
 
    //Si el mensaje va dirigido a mi y es un ICE candidate
    if (message.candidate && idreceiver==idpeer){
        //agrego el candidato a la lista de candidatos de ese peer.
    console.log("Received ice candidate: "+ message.candidate.candidate); 
    pcs[id].addIceCandidate(new RTCIceCandidate(message.candidate));
    }
 
}
 
//Configuracion del chat
function setupChat(i) {
    //Cuando se abra el dataChannel con ese peer se habilita el boton enviar.
    channel[i].onopen = function () {
        btnSend.disabled=false;
  document.getElementById("chatcontrols").style.display="inline";
    };
 
    //Cuando se reciba un mensaje por DataChannel (WebRTC) se muestra el mensaje.
    channel[i].onmessage = function (evt) {
       document.getElementById("receive").innerHTML+="<br />"+evt.data;
    };
}
 
//Enviar un mensaje por DataChannel (al resto de peer)
function sendChatMessage() {
    document.getElementById("receive").innerHTML+="<br />"+document.getElementById("login").value+ ": "+msg.value;
  //Para cada peer de la lista...
    for (i in peerlist){  
    if (peerlist[i]!=idpeer){
      console.log("send to "+peerlist[i]);  
            //Se envial el mensaje con el nombre de usuario y el texto a enviar.
      try{
        channel[peerlist[i]].send(document.getElementById("login").value+ ": "+msg.value);
      }catch(e){
        console.log(i+" said bye!");
      }
 
    }
  }
}

Nota: El código completo del Peer está actualizado y disponible en Launchpad -> P2PSP > Experimentos > ChatMultiusuario > Peer.js

Una versión de este experimento está disponible para probarlo en http://www.p2psp.org/chat. Si al introducir un nickname aparece el mensaje "Conecting..."  pero no aparece el cuadro de texto con el botón "send" es posible que no haya ningún otro peer (o esté detrás de un NAT simétrico), para comprobar que funciona correctamente puedes abrir otra vez la misma URL en otra pestaña de tu navegador o incluso en otro equipo de tu red.

Si tienes cualquier duda o sugerencia, usa los comentarios :-)

Este experimento, por el momento, sólo funciona en Google Chrome (incluido Chrome for Android)

Entendiendo el protocolo P2PSP

  • Escrito por Cristóbal Medina López
  • Publicado el 22 Febrero 2014

Llevo unas semanas desconectado del proyecto por los exámenes, aunque he dedicado algún tiempo a probar WebRTC y buscar la forma de adaptar el P2PSP para conseguir implementarlo con éxito en el navegador. Lo primero, entender como funciona el protocolo.

El Protocolo P2PSP es un conjunto de reglas de transmisión y comportamiento que ayuda a aumentar la calidad de servicio en los sistemas de transmisión de streaming en tiempo real. El protocolo está diseñado por módulos, cada uno de ellos está definido por un conjunto de reglas que provee diferentes funcionalidades. Los módulos que forman el P2PSP son: Data Broadcasting Set (DBS) of rules, Lost chunks Recovery Set (LRS) of rules, Adaptive Chunk-rate Set (ACS) of rules, End-point Masquerading Set (EMS) of rules, NAT Traversal Set (NTS) of rules, Multi-Channel Set (MCS) of rules, Data Integrity Set (DIS) of rules, Data Privacy Set (DPS) of rules. 

El módulo principal e indispensable para el funcionamiento básico del protocolo es el DBS, el resto son opcionales, por lo tanto, nos centraremos en implementar este módulo en WebRTC para conseguir streaming de vídeo en tiempo real en el navegador.

Las entidades que serán necesarias para el funcionamiento del módulo DBS son:

  • Source: Es el que produce el stream que será transmitido a la red P2PSP. Normalmente es un servidor de video HTTP como Icecast
  • Player: Es el encargado de decodificar y reproducir el stream.
  • Splitter: Esta entidad recibe el stream desde el Source, lo divide en trozos del mismo tamaño y envía los trozos a los peers.
  • Peer: Recibe los trozos desde el splitter y desde otros peers, ensambla el stream y lo envia al Player

Sin embargo, para la versión en WebRTC debemos tener en cuenta que el Player y el Peer estarán incrustados en el navegador Web y el Splitter además de su función en la red P2PSP hará también la función de Signaling Server de WebRTC

Un posible escenario del P2PSP implementado con WebRTC se muestra a continuación:

Escenario WebRTC+P2PSP

Nótese que en la imagen anterior el player forma parte del Client+Peer y el Source se ha divido en Streaming Server, que hace la función del Source original descrito en el White Paper del protocolo P2PSP y en el Video Source, que es un dispositivo encargado de capturar el vídeo y enviarlo al Streaming Server.

El conjunto de reglas necesario para implementar el módulo DBS está descrito en la Web oficial del protocolo en la sección The Data Broadcasting Set of rules

WebRTC, P2PSP y el CUSL

  • Escrito por Cristóbal Medina López
  • Publicado el 27 Octubre 2013

Concurso Universitario de Software LibreFue en mayo de 2012 cuando empecé a aportar mi granito de arena a la comunidad de software libre.  Por aquel entonces, el profesor de redes de computadores de la Universidad, Vicente González, decidió que la mejor forma de entender los socket era entrando de lleno con ellos. Nos animó a colaborar en un proyecto de software libre que aún se estaba gestando, el protocolo P2PSP. Para mí aquello fue una gran oportunidad, podría colaborar en un proyecto de software libre, poner en práctica lo aprendido en clase, aprender y divertirme al mismo tiempo. Por supuesto no lo dudé un instante y solicité formar parte del P2PSP Team en launchpad. Después de 18 meses, 58 revisiones, cambios de nombre, varias correcciones de bug y algunos cambios significativos en el protocolo, el P2PSP es una realidad, incluso tiene su propia web: www.p2psp.org. Como en cualquier proyecto de software libre que se precie, seguimos trabajando para mejorarlo entre todos y nos encantaría que te unieras al proyecto. Aún queda mucho por hacer.

Alguno estará pensando, ¿A qué viene esto ahora?, la respuesta es sencilla: Me presento al concurso universitario de software libre (CUSL). Para los que aún no lo conozcan, es un concurso de desarrollo de software, hardware y documentación técnica libre en el que puede participar cualquier estudiante, el objetivo es contribuir a la consolidación de la comunidad de software libre en la universidad. Para el concurso he decidido implementar el protocolo P2PSP con WebRTC, de forma que se ejecute íntegramente en el navegador Web sin necesidad de plugins.

A partir de ahora escribiré una serie de post dedicados a informar sobre todo lo que rodea al proyecto. Estarán accesibles desde el Blog o directamente en Mi participación en el CUSL, todos los post se podrán encontrar por la etiqueta CUSL. Ya sólo queda desear suerte a todos los participantes. ¡A disfrutar!

 

¿Por qué una implementación del P2PSP para el Navegador?

  • Escrito por Cristóbal Medina López
  • Publicado el 16 Noviembre 2013

Motivación

En la actualidad millones de personas se conectan diariamente a Internet para consumir contenido multimedia, y la mayoría de estas personas lo hace mediante dispositivos móviles. Cada vez son más las compañias que ofrecen servicios de acceso a Internet con tarifas reducidas, y todas ellas tienen algo en común: se tarifica por bits descargados sin tener en cuenta la cantidad de información enviada a la red. Esta capacidad está siendo desaprovechada por los actuales sistemas comerciales de streaming de vídeo, porque en estos, toda la carga de la red recae sobre el servidor, de modo que el servidor tiene que servir repetidamente el mismo contenido a todos los clientes que lo están solicitando al mismo tiempo.

Como mejora al modelo cliente-servidor para streaming tradicional, surge la tecnología P2P (Peer-To-Peer) y el protocolo P2PSP (P2P Straightforward Protocol) es un ejemplo concreto. Gracias al P2PSP, unido a otras tecnologías, podemos confeccionar clientes de streaming P2P que aprovechen ese ancho de banda que es desperdiciado, enviando una copia de su contenido al resto de clientes. Así conseguimos que todos los clientes compartan tanto como reciben a la par que liberamos al servidor de una gran carga computacional y de transmisión, de forma que estamos ante un sistema de streaming escalable capaz de soportar un mayor número de usuarios. Por otra parte, hacer un sistema capaz de ser ejecutado en múltiples plataformas, sobre todo en dispositivos móviles, sería un avance en el campo de la transmisión de vídeo por Internet, ya que permitiría a cualquier usuario independientemente de la plataforma conectarse a un clúster P2PSP para comenzar a recibir y compartir contenido multimedia. 

P2PSP + WEBRTC

¿Por qué el Navegador Web?

El navegador Web es una herramienta indispensable en cualquier equipo porque es la forma más común de acceder a la información disponible en Internet para un usuario. Esto se debe a que el navegador es fácil de usar, y para acceder a un recurso es suficiente con recordar el nombre de dominio donde se aloja o llegar hasta él mediante motores de búsqueda disponibles en la red.

Por otra parte, HTML5 es una opción ideal para implementar un cliente P2PSP. Una implementación en HTML5 permitiría a cualquier usuario conectarse a un cluster del P2PSP y comenzar a recibir (y compartir) el contenido multimedia. HTML5 incluye la etiqueta <video> para incrustar el contenido multimedia, pero esto no es suficiente para ejecutar un cliente P2PSP, para ello necesitamos hacer uso de protocolos de transporte ligeros. HTML5 nos brinda la oportunidad de usar sockets TCP para permitir una comunicación bidireccional entre el navegador y el servidor en distintos puertos. Sin embargo, existe un inconveniente en esta solución ya que la transmisión de bloques de vídeo necesitamos realizarla sobre UDP entre los navegadores. Por suerte, este problema podemos  solventarlo haciendo uso de una nueva API que ahora mismo está en proceso de estandarización por la W3C y la IETF. Concretamente, dicha API, que se denomina WebRTC, es ideal para nosotros porque define un recurso llamado interface DataChannel que permite establecer un canal de datos bidireccional entre los navegadores mediante socket sobre UDP. 

WebRTC Comunicación en tiempo real sin plugin

  • Escrito por Cristóbal Medina López
  • Publicado el 17 Septiembre 2013

WebRTC logo

¿Qué es WebRTC?

WebRTC (Web Real-Time Communication) es un proyecto de software libre que permite a los navegadores mantener comunicaciones en tiempo real a través de una API Javascript sin necesidad de software adicional. Es un proyecto mantenido por Google, Mozilla y Opera que actualmente está en proceso de estandarización en el IETF y en el W3C. 

Componentes Principales

  • MediaStream o getUserMedia: Permite al navegador acceder a la cámara y al micrófono. Representa un stream de audio/video y puede contener varios canales.
  • RTCPeerConnection: Permite establecer una conexión p2p con otros navegadores, procesar la señal, manejo de codec, gestión de ancho de banda, etc.
  • RTCDataChannel: Permite al navegador una comunicación de datos bidireccional entre peers.

Proceso de señalización (Signaling)

Para conseguir conectar los peer es necesario intercambiar un objeto 'session description' (SDP) que contiene: 

  • Formatos soportados y que información desea enviar
  • Información de la red para iniciar la conexión p2p

Ejemplo de SDP:

Suscripción RSS

feed-image Blog