Comunicación en tiempo real con WebRTC

1. Introducción

WebRTC es un proyecto de código abierto que permite la comunicación en tiempo real de audio, video y datos en apps web y nativas.

WebRTC tiene varias APIs de JavaScript. Haz clic en los vínculos para ver demostraciones.

¿Dónde puedo usar WebRTC?

En Firefox, Opera y en Chrome para computadoras y Android WebRTC también está disponible para apps nativas en iOS y Android.

¿Qué es el envío de indicadores?

WebRTC usa RTCPeerConnection para comunicar datos de transmisión entre navegadores, pero también necesita un mecanismo para coordinar la comunicación y enviar mensajes de control, un proceso conocido como señalización. WebRTC no especifica los métodos ni los protocolos de señalización. En este codelab, usarás Socket.IO para la mensajería, pero hay muchas alternativas.

¿Qué son STUN y TURN?

WebRTC está diseñado para funcionar de par a par, de modo que los usuarios puedan conectarse por la ruta más directa posible. Sin embargo, WebRTC se diseñó para hacer frente a las redes del mundo real: las aplicaciones cliente deben atravesar puertas de enlace NAT y firewalls, y las redes de igual a igual necesitan alternativas en caso de que falle la conexión directa. Como parte de este proceso, las APIs de WebRTC usan servidores STUN para obtener la dirección IP de tu computadora y servidores TURN para funcionar como servidores de retransmisión en caso de que falle la comunicación de punto a punto. (WebRTC en el mundo real explica esto con más detalle).

¿WebRTC es seguro?

La encriptación es obligatoria para todos los componentes de WebRTC, y sus APIs de JavaScript solo se pueden usar desde orígenes seguros (HTTPS o localhost). Los estándares de WebRTC no definen los mecanismos de señalización, por lo que depende de ti asegurarte de usar protocolos seguros.

2. Descripción general

Crea una app para obtener videos y tomar instantáneas con tu cámara web, y compártelos de usuario a usuario a través de WebRTC. En el proceso, aprenderás a usar las APIs principales de WebRTC y a configurar un servidor de mensajería con Node.js.

Qué aprenderás

  • Cómo obtener video de tu cámara web
  • Transmite video con RTCPeerConnection
  • Transmite datos con RTCDataChannel
  • Configura un servicio de señalización para intercambiar mensajes
  • Combina la conexión entre pares y la señalización
  • Toma una foto y compártela a través de un canal de datos

Requisitos

  • Chrome 47 o versiones posteriores
  • Web Server for Chrome o tu propio servidor web.
  • El código de muestra
  • Un editor de texto
  • Conocimientos básicos de HTML, CSS y JavaScript

3. Obtén el código de muestra

Descarga el código

Si conoces git, puedes clonar el código de este codelab desde GitHub para descargarlo:

git clone https://github.com/googlecodelabs/webrtc-web

También puedes hacer clic en el siguiente botón para descargar un archivo .zip del código:

Descargar código fuente

Abre el archivo ZIP que descargaste. Con esto, se descomprimirá una carpeta de proyecto (adaptive-web-media) que contiene una carpeta para cada paso de este codelab, junto con todos los recursos que necesitarás.

Todo el trabajo de codificación se realizará en el directorio llamado work.

Las carpetas step-nn contienen una versión final para cada paso de este codelab. Están disponibles como referencia.

Instala y verifica el servidor web

Si bien puedes usar tu propio servidor web, este codelab está diseñado para funcionar bien con el servidor web de Chrome. Si aún no tienes esa app instalada, puedes instalarla desde Chrome Web Store.

6ddeb4aee53c0f0e.png

Después de instalar la app de Web Server for Chrome, haz clic en el acceso directo de Chrome Apps en la barra de marcadores, en una página de nueva pestaña o en el Selector de aplicaciones:

1d2b4aa977ab7e24.png

Haz clic en el ícono del servidor web:

27fce4494f641883.png

A continuación, verás este diálogo, que te permitirá configurar tu servidor web local:

Screen Shot 2016-02-18 at 11.48.14 AM.png

Haz clic en el botón ELEGIR CARPETA y selecciona la carpeta work que acabas de crear. Esto te permitirá ver tu trabajo en curso en Chrome a través de la URL destacada en el diálogo de servidor web en la sección Web Server URL(s).

En Options, marca la casilla junto a Automatically show index.html, como se muestra a continuación:

Screen Shot 2016-02-18 at 11.56.30 AM.png

Luego, detén y reinicia el servidor deslizando el interruptor etiquetado como Web Server: STARTED hacia la izquierda y, luego, hacia la derecha.

Screen Shot 2016-02-18 at 12.22.18 PM.png

Ahora, haz clic en la URL del servidor web destacada para visitar tu sitio de trabajo en el navegador web. Deberías ver una página similar a esta, que corresponde a work/index.html:

18a705cb6ccc5181.png

Obviamente, esta app aún no está haciendo nada interesante. Hasta ahora, es solo un esqueleto mínimo que usamos para asegurarnos de que tu servidor web funcione correctamente. Agregarás funciones y características de diseño en los pasos posteriores.

4. Transmite video desde tu cámara web

Qué aprenderás

En este paso, descubrirás cómo hacer lo siguiente:

  • Obtén una transmisión de video de tu cámara web.
  • Manipular la reproducción de la transmisión
  • Usa CSS y SVG para manipular videos.

Encontrarás una versión completa de este paso en la carpeta step-01.

Un toque de HTML…

Agrega un elemento video y un elemento script a index.html en tu directorio work:

<!DOCTYPE html>
<html>

<head>

  <title>Realtime communication with WebRTC</title>

  <link rel="stylesheet" href="css/main.css" />

</head>

<body>

  <h1>Realtime communication with WebRTC</h1>

  <video autoplay playsinline></video>

  <script src="js/main.js"></script>

</body>

</html>

…y una pizca de JavaScript

Agrega lo siguiente a main.js en tu carpeta js:

'use strict';

// On this codelab, you will be streaming only video (video: true).
const mediaStreamConstraints = {
  video: true,
};

// Video element where stream will be placed.
const localVideo = document.querySelector('video');

// Local stream that will be reproduced on the video.
let localStream;

// Handles success by adding the MediaStream to the video element.
function gotLocalMediaStream(mediaStream) {
  localStream = mediaStream;
  localVideo.srcObject = mediaStream;
}

// Handles error by logging a message to the console with the error message.
function handleLocalMediaStreamError(error) {
  console.log('navigator.getUserMedia error: ', error);
}

// Initializes media stream.
navigator.mediaDevices.getUserMedia(mediaStreamConstraints)
  .then(gotLocalMediaStream).catch(handleLocalMediaStreamError);

Probar

Abre index.html en tu navegador y deberías ver algo como lo siguiente (con la vista de tu cámara web, por supuesto):

9297048e43ed0f3d.png

Cómo funciona

Después de la llamada a getUserMedia(), el navegador le solicita permiso al usuario para acceder a su cámara (si es la primera vez que se solicita acceso a la cámara para el origen actual). Si la operación se realiza correctamente, se devuelve un objeto MediaStream, que un elemento multimedia puede usar a través del atributo srcObject:

navigator.mediaDevices.getUserMedia(mediaStreamConstraints)
  .then(gotLocalMediaStream).catch(handleLocalMediaStreamError);


}
function gotLocalMediaStream(mediaStream) {
  localVideo.srcObject = mediaStream;
}

El argumento constraints te permite especificar qué medios obtener. En este ejemplo, solo se muestra el video, ya que el audio está inhabilitado de forma predeterminada:

const mediaStreamConstraints = {
  video: true,
};

Puedes usar restricciones para requisitos adicionales, como la resolución de video:

const hdConstraints = {
  video: {
    width: {
      min: 1280
    },
    height: {
      min: 720
    }
  }
}

La especificación MediaTrackConstraints enumera todos los tipos de restricciones posibles, aunque no todos los navegadores admiten todas las opciones. Si la cámara seleccionada actualmente no admite la resolución solicitada, se rechazará getUserMedia() con un OverconstrainedError y no se le pedirá al usuario que otorgue permiso para acceder a su cámara.

Si getUserMedia() se ejecuta correctamente, la transmisión de video de la cámara web se establece como la fuente del elemento de video:

function gotLocalMediaStream(mediaStream) {
  localVideo.srcObject = mediaStream;
}

Puntos adicionales

  • El objeto localStream que se pasa a getUserMedia() está en el alcance global, por lo que puedes inspeccionarlo desde la consola del navegador: abre la consola, escribe stream y presiona Retorno. (Para ver la consola en Chrome, presiona Ctrl + Mayúsculas + J o Comando + Opción + J si usas una Mac).
  • ¿Qué devuelve localStream.getVideoTracks()?
  • Intenta llamar a localStream.getVideoTracks()[0].stop().
  • Observa el objeto de restricciones: ¿qué sucede cuando lo cambias a {audio: true, video: true}?
  • ¿Qué tamaño tiene el elemento de video? ¿Cómo puedes obtener el tamaño natural del video desde JavaScript, en lugar del tamaño de visualización? Usa las Herramientas para desarrolladores de Chrome para verificarlo.
  • Intenta agregar filtros CSS al elemento de video. Por ejemplo:
video {
  filter: blur(4px) invert(1) opacity(0.5);
}
  • Intenta agregar filtros SVG. Por ejemplo:
video {
   filter: hue-rotate(180deg) saturate(200%);
 }

Qué aprendiste

En este paso, aprendiste a hacer lo siguiente:

  • Obtener video de tu cámara web
  • Establece restricciones de medios.
  • Manipula el elemento de video.

Encontrarás una versión completa de este paso en la carpeta step-01.

Sugerencias

  • No olvides el atributo autoplay en el elemento video. Sin eso, solo verás un solo fotograma.
  • Hay muchas más opciones para las restricciones de getUserMedia(). Echa un vistazo a la demostración en webrtc.github.io/samples/src/content/peerconnection/constraints. Como verás, hay muchos ejemplos interesantes de WebRTC en ese sitio.

Práctica recomendada

  • Asegúrate de que el elemento de video no desborde su contenedor. Agregamos width y max-width para establecer un tamaño preferido y un tamaño máximo para el video. El navegador calculará la altura automáticamente:
video {
  max-width: 100%;
  width: 320px;
}

Cuál es el próximo paso

Tienes el video, pero ¿cómo lo transmites? Descúbrelo en el siguiente paso.

5. Transmite video con RTCPeerConnection

Qué aprenderás

En este paso, descubrirás cómo hacer lo siguiente:

  • Abstrae las diferencias del navegador con el shim de WebRTC, adapter.js.
  • Usa la API de RTCPeerConnection para transmitir video.
  • Controlar la captura y transmisión de contenido multimedia

Encontrarás una versión completa de este paso en la carpeta step-2.

¿Qué es RTCPeerConnection?

RTCPeerConnection es una API para realizar llamadas de WebRTC para transmitir video y audio, y para intercambiar datos.

En este ejemplo, se configura una conexión entre dos objetos RTCPeerConnection (conocidos como pares) en la misma página.

No tiene mucho uso práctico, pero es bueno para comprender cómo funciona RTCPeerConnection.

Agregar elementos de video y botones de control

En index.html, reemplaza el elemento de video único por dos elementos de video y tres botones:

<video id="localVideo" autoplay playsinline></video>
<video id="remoteVideo" autoplay playsinline></video>


<div>
  <button id="startButton">Start</button>
  <button id="callButton">Call</button>
  <button id="hangupButton">Hang Up</button>
</div>

Un elemento de video mostrará la transmisión de getUserMedia(), y el otro mostrará el mismo video transmitido a través de RTCPeerconnection. (En una aplicación del mundo real, un elemento de video mostraría la transmisión local y el otro, la transmisión remota).

Agrega el polyfill de adapter.js

Agrega un vínculo a la versión actual de adapter.js sobre el vínculo a main.js:

<script src="https://webrtc.github.io/adapter/adapter-latest.js"></script>

Index.html ahora debería verse de la siguiente manera:

<!DOCTYPE html>
<html>

<head>
  <title>Realtime communication with WebRTC</title>
  <link rel="stylesheet" href="css/main.css" />
</head>

<body>
  <h1>Realtime communication with WebRTC</h1>

  <video id="localVideo" autoplay playsinline></video>
  <video id="remoteVideo" autoplay playsinline></video>

  <div>
    <button id="startButton">Start</button>
    <button id="callButton">Call</button>
    <button id="hangupButton">Hang Up</button>
  </div>

  <script src="https://webrtc.github.io/adapter/adapter-latest.js"></script>
  <script src="js/main.js"></script>
</body>
</html>

Instala el código de RTCPeerConnection

Reemplaza main.js por la versión que se encuentra en la carpeta step-02.

Realizar la llamada

Abre index.html, haz clic en el botón Start para obtener el video de tu cámara web y haz clic en Call para establecer la conexión entre pares. Deberías ver el mismo video (de tu cámara web) en ambos elementos de video. Consulta la consola del navegador para ver el registro de WebRTC.

Cómo funciona

Este paso hace mucho…

WebRTC usa la API de RTCPeerConnection para configurar una conexión y transmitir video entre clientes de WebRTC, conocidos como pares.

En este ejemplo, los dos objetos RTCPeerConnection se encuentran en la misma página: pc1 y pc2. No tiene mucho uso práctico, pero es bueno para demostrar cómo funcionan las APIs.

La configuración de una llamada entre pares de WebRTC implica tres tareas:

  • Crea un objeto RTCPeerConnection para cada extremo de la llamada y, en cada extremo, agrega la transmisión local de getUserMedia().
  • Obtener y compartir información de la red: Los posibles extremos de conexión se conocen como candidatos ICE.
  • Obtener y compartir descripciones locales y remotas: Metadatos sobre contenido multimedia local en formato SDP

Imagina que Alice y Bob quieren usar RTCPeerConnection para configurar un videochat.

Primero, Alicia y Roberto intercambian información de la red. La expresión "búsqueda de candidatos" se refiere al proceso de búsqueda de interfaces y puertos de red con el framework ICE.

  1. Alice crea un objeto RTCPeerConnection con un controlador onicecandidate (addEventListener('icecandidate')). Esto corresponde al siguiente código de main.js:
let localPeerConnection;
localPeerConnection = new RTCPeerConnection(servers);
localPeerConnection.addEventListener('icecandidate', handleConnection);
localPeerConnection.addEventListener(
    'iceconnectionstatechange', handleConnectionChange);
  1. Alice llama a getUserMedia() y agrega la transmisión que se pasó a ese método:
navigator.mediaDevices.getUserMedia(mediaStreamConstraints).
  then(gotLocalMediaStream).
  catch(handleLocalMediaStreamError);
function gotLocalMediaStream(mediaStream) {
  localVideo.srcObject = mediaStream;
  localStream = mediaStream;
  trace('Received local stream.');
  callButton.disabled = false;  // Enable call button.
}
localPeerConnection.addStream(localStream);
trace('Added local stream to localPeerConnection.');
  1. Se llama al controlador onicecandidate del paso 1 cuando hay candidatos de red disponibles.
  2. Alice envía datos serializados de candidatos a Bob. En una aplicación real, este proceso (conocido como señalización) se lleva a cabo a través de un servicio de mensajería, y aprenderás a hacerlo en un paso posterior. Por supuesto, en este paso, los dos objetos RTCPeerConnection están en la misma página y pueden comunicarse directamente sin necesidad de mensajería externa.
  3. Cuando Bob recibe un mensaje de candidato de Alice, llama a addIceCandidate() para agregar el candidato a la descripción del par remoto:
function handleConnection(event) {
  const peerConnection = event.target;
  const iceCandidate = event.candidate;

  if (iceCandidate) {
    const newIceCandidate = new RTCIceCandidate(iceCandidate);
    const otherPeer = getOtherPeer(peerConnection);

    otherPeer.addIceCandidate(newIceCandidate)
      .then(() => {
        handleConnectionSuccess(peerConnection);
      }).catch((error) => {
        handleConnectionFailure(peerConnection, error);
      });

    trace(`${getPeerName(peerConnection)} ICE candidate:\n` +
          `${event.candidate.candidate}.`);
  }
}

Los pares de WebRTC también deben averiguar e intercambiar información de medios de audio y video locales y remotos, como la resolución y las capacidades de códec. La señalización para intercambiar información de configuración de medios se realiza intercambiando BLOB de metadatos, conocidos como oferta y respuesta, con el formato del Protocolo de descripción de sesión, conocido como SDP:

  1. Alice ejecuta el método createOffer() de RTCPeerConnection. La promesa devuelta proporciona un RTCSessionDescription: la descripción de la sesión local de Alice:
trace('localPeerConnection createOffer start.');
localPeerConnection.createOffer(offerOptions)
  .then(createdOffer).catch(setSessionDescriptionError);
  1. Si la operación se realiza correctamente, Alice establece la descripción local con setLocalDescription() y, luego, envía esta descripción de la sesión a Bob a través de su canal de señalización.
  2. Bob establece la descripción que le envió Alice como la descripción remota con setRemoteDescription().
  3. Roberto ejecuta el método createAnswer() de RTCPeerConnection y le pasa la descripción remota que obtuvo de Alicia para que se pueda generar una sesión local compatible con la de ella. La promesa createAnswer() pasa un RTCSessionDescription: Bob lo establece como la descripción local y se lo envía a Alice.
  4. Cuando Alice recibe la descripción de la sesión de Bob, la establece como la descripción remota con setRemoteDescription().
// Logs offer creation and sets peer connection session descriptions.
function createdOffer(description) {
  trace(`Offer from localPeerConnection:\n${description.sdp}`);

  trace('localPeerConnection setLocalDescription start.');
  localPeerConnection.setLocalDescription(description)
    .then(() => {
      setLocalDescriptionSuccess(localPeerConnection);
    }).catch(setSessionDescriptionError);

  trace('remotePeerConnection setRemoteDescription start.');
  remotePeerConnection.setRemoteDescription(description)
    .then(() => {
      setRemoteDescriptionSuccess(remotePeerConnection);
    }).catch(setSessionDescriptionError);

  trace('remotePeerConnection createAnswer start.');
  remotePeerConnection.createAnswer()
    .then(createdAnswer)
    .catch(setSessionDescriptionError);
}

// Logs answer to offer creation and sets peer connection session descriptions.
function createdAnswer(description) {
  trace(`Answer from remotePeerConnection:\n${description.sdp}.`);

  trace('remotePeerConnection setLocalDescription start.');
  remotePeerConnection.setLocalDescription(description)
    .then(() => {
      setLocalDescriptionSuccess(remotePeerConnection);
    }).catch(setSessionDescriptionError);

  trace('localPeerConnection setRemoteDescription start.');
  localPeerConnection.setRemoteDescription(description)
    .then(() => {
      setRemoteDescriptionSuccess(localPeerConnection);
    }).catch(setSessionDescriptionError);
}
  1. ¡Ping!

Puntos adicionales

  1. Consulta chrome://webrtc-internals. Esto proporciona estadísticas de WebRTC y datos de depuración. (En chrome://about, se encuentra una lista completa de las URLs de Chrome).
  2. Aplica un diseño a la página con CSS:
  • Coloca los videos uno al lado del otro.
  • Haz que los botones tengan el mismo ancho y un texto más grande.
  • Asegúrate de que el diseño funcione en dispositivos móviles.
  1. En la consola de Herramientas para desarrolladores de Chrome, observa localStream, localPeerConnection y remotePeerConnection.
  2. En la consola, observa localPeerConnectionpc1.localDescription. ¿Cómo se ve el formato SDP?

Qué aprendiste

En este paso, aprendiste a hacer lo siguiente:

  • Abstrae las diferencias del navegador con el shim de WebRTC, adapter.js.
  • Usa la API de RTCPeerConnection para transmitir video.
  • Controlar la captura y transmisión de contenido multimedia
  • Compartir información de medios y redes entre pares para habilitar una llamada de WebRTC

Encontrarás una versión completa de este paso en la carpeta step-2.

Sugerencias

  • Hay mucho que aprender en este paso. Para encontrar otros recursos que expliquen RTCPeerConnection con más detalle, consulta webrtc.org. En esta página, se incluyen sugerencias para los frameworks de JavaScript si deseas usar WebRTC, pero no quieres manipular las APIs.
  • Obtén más información sobre el shim de adapter.js en el repositorio de GitHub de adapter.js.
  • ¿Quieres ver cómo se ve la mejor app de chat de video del mundo? Consulta AppRTC, la app canónica del proyecto de WebRTC para llamadas de WebRTC: app, código. El tiempo de configuración de la llamada es inferior a 500 ms.

Práctica recomendada

  • Para preparar tu código para el futuro, usa las nuevas APIs basadas en Promises y habilita la compatibilidad con los navegadores que no las admiten usando adapter.js.

Cuál es el próximo paso

En este paso, se muestra cómo usar WebRTC para transmitir video entre pares, pero este codelab también trata sobre datos.

En el siguiente paso, descubrirás cómo transmitir datos arbitrarios con RTCDataChannel.

6. Usa RTCDataChannel para intercambiar datos

Qué aprenderás

  • Cómo intercambiar datos entre extremos de WebRTC (pares)

Encontrarás una versión completa de este paso en la carpeta step-03.

Actualiza tu código HTML

En este paso, usarás canales de datos de WebRTC para enviar texto entre dos elementos textarea en la misma página. Esto no es muy útil, pero demuestra cómo se puede usar WebRTC para compartir datos y transmitir video.

Quita los elementos de video y botón de index.html y reemplázalos por el siguiente código HTML:

<textarea id="dataChannelSend" disabled
    placeholder="Press Start, enter some text, then press Send."></textarea>
<textarea id="dataChannelReceive" disabled></textarea>

<div id="buttons">
  <button id="startButton">Start</button>
  <button id="sendButton">Send</button>
  <button id="closeButton">Stop</button>
</div>

Un área de texto será para ingresar texto y la otra mostrará el texto transmitido entre pares.

index.html ahora debería verse de la siguiente manera:

<!DOCTYPE html>
<html>

<head>

  <title>Realtime communication with WebRTC</title>

  <link rel="stylesheet" href="css/main.css" />

</head>

<body>

  <h1>Realtime communication with WebRTC</h1>

  <textarea id="dataChannelSend" disabled
    placeholder="Press Start, enter some text, then press Send."></textarea>
  <textarea id="dataChannelReceive" disabled></textarea>

  <div id="buttons">
    <button id="startButton">Start</button>
    <button id="sendButton">Send</button>
    <button id="closeButton">Stop</button>
  </div>

  <script src="https://webrtc.github.io/adapter/adapter-latest.js"></script>
  <script src="js/main.js"></script>

</body>

</html>

Actualiza tu código JavaScript

Reemplaza main.js por el contenido de step-03/js/main.js.

Prueba la transmisión de datos entre pares: abre index.html, presiona Start para configurar la conexión de pares, ingresa texto en el textarea de la izquierda y, luego, haz clic en Send para transferir el texto con los canales de datos de WebRTC.

Cómo funciona

Este código usa RTCPeerConnection y RTCDataChannel para permitir el intercambio de mensajes de texto.

Gran parte del código de este paso es el mismo que el del ejemplo de RTCPeerConnection.

Las funciones sendData() y createConnection() tienen la mayor parte del código nuevo:

function createConnection() {
  dataChannelSend.placeholder = '';
  var servers = null;
  pcConstraint = null;
  dataConstraint = null;
  trace('Using SCTP based data channels');
  // For SCTP, reliable and ordered delivery is true by default.
  // Add localConnection to global scope to make it visible
  // from the browser console.
  window.localConnection = localConnection =
      new RTCPeerConnection(servers, pcConstraint);
  trace('Created local peer connection object localConnection');

  sendChannel = localConnection.createDataChannel('sendDataChannel',
      dataConstraint);
  trace('Created send data channel');

  localConnection.onicecandidate = iceCallback1;
  sendChannel.onopen = onSendChannelStateChange;
  sendChannel.onclose = onSendChannelStateChange;

  // Add remoteConnection to global scope to make it visible
  // from the browser console.
  window.remoteConnection = remoteConnection =
      new RTCPeerConnection(servers, pcConstraint);
  trace('Created remote peer connection object remoteConnection');

  remoteConnection.onicecandidate = iceCallback2;
  remoteConnection.ondatachannel = receiveChannelCallback;

  localConnection.createOffer().then(
    gotDescription1,
    onCreateSessionDescriptionError
  );
  startButton.disabled = true;
  closeButton.disabled = false;
}

function sendData() {
  var data = dataChannelSend.value;
  sendChannel.send(data);
  trace('Sent Data: ' + data);
}

La sintaxis de RTCDataChannel es deliberadamente similar a la de WebSocket, con un método send() y un evento message.

Observa el uso de dataConstraint. Los canales de datos se pueden configurar para habilitar diferentes tipos de uso compartido de datos, por ejemplo, priorizar la entrega confiable por sobre el rendimiento. Puedes obtener más información sobre las opciones en la Red de desarrolladores de Mozilla.

Puntos adicionales

  1. Con SCTP, el protocolo que usan los canales de datos de WebRTC, la entrega de datos confiable y ordenada está activada de forma predeterminada. ¿Cuándo podría RTCDataChannel necesitar proporcionar una entrega confiable de datos y cuándo podría ser más importante el rendimiento, incluso si eso significa perder algunos datos?
  2. Usa CSS para mejorar el diseño de la página y agrega un atributo de marcador de posición al área de texto "dataChannelReceive".
  3. Prueba la página en un dispositivo móvil.

Qué aprendiste

En este paso, aprendiste a hacer lo siguiente:

  • Establece una conexión entre dos pares de WebRTC.
  • Intercambia datos de texto entre los pares.

Encontrarás una versión completa de este paso en la carpeta step-03.

Más información

Cuál es el próximo paso

Aprendiste a intercambiar datos entre pares en la misma página, pero ¿cómo se hace entre diferentes máquinas? Primero, debes configurar un canal de señalización para intercambiar mensajes de metadatos. Descubre cómo hacerlo en el siguiente paso.

7. Configura un servicio de señalización para intercambiar mensajes

Qué aprenderás

En este paso, descubrirás cómo hacer lo siguiente:

  • Usa npm para instalar las dependencias del proyecto según se especifica en package.json.
  • Ejecuta un servidor de Node.js y usa node-static para entregar archivos estáticos.
  • Configura un servicio de mensajería en Node.js con Socket.IO.
  • Úsalo para crear "salas" e intercambiar mensajes.

Encontrarás una versión completa de este paso en la carpeta step-04.

Conceptos

Para configurar y mantener una llamada de WebRTC, los clientes (pares) de WebRTC deben intercambiar metadatos:

  • Es la información del candidato (red).
  • Mensajes de oferta y respuesta que proporcionan información sobre los medios, como la resolución y los códecs.

En otras palabras, se requiere un intercambio de metadatos antes de que se pueda realizar la transmisión de audio, video o datos de punto a punto. Este proceso se denomina señalización.

En los pasos anteriores, los objetos RTCPeerConnection del emisor y el receptor se encuentran en la misma página, por lo que la "señalización" es simplemente una cuestión de pasar metadatos entre objetos.

En una aplicación del mundo real, los objetos RTCPeerConnection del emisor y el receptor se ejecutan en páginas web en diferentes dispositivos, y necesitas una forma de que comuniquen metadatos.

Para ello, se usa un servidor de señalización, es decir, un servidor que puede pasar mensajes entre clientes (pares) de WebRTC. Los mensajes reales son texto sin formato: objetos de JavaScript convertidos en cadenas.

Requisito previo: Instala Node.js

Para ejecutar los próximos pasos de este codelab (carpetas step-04 a step-06), deberás ejecutar un servidor en localhost con Node.js.

Puedes descargar e instalar Node.js desde este vínculo o a través del administrador de paquetes que prefieras.

Una vez instalado, podrás importar las dependencias necesarias para los próximos pasos (ejecutar npm install), así como ejecutar un pequeño servidor localhost para ejecutar el codelab (ejecutar node index.js). Estos comandos se indicarán más adelante, cuando sean necesarios.

Acerca de la aplicación

WebRTC usa una API de JavaScript del cliente, pero para el uso en el mundo real también requiere un servidor de señalización (mensajería), así como servidores STUN y TURN. Puedes obtener más información aquí.

En este paso, compilarás un servidor de señalización simple de Node.js con el módulo Socket.IO Node.js y la biblioteca de JavaScript para la mensajería. Será útil tener experiencia con Node.js y Socket.IO, pero no es fundamental, ya que los componentes de mensajería son muy simples.

En este ejemplo, el servidor (la aplicación de Node.js) se implementa en index.js, y el cliente que se ejecuta en él (la app web) se implementa en index.html.

La aplicación de Node.js de este paso tiene dos tareas.

En primer lugar, actúa como un retransmisor de mensajes:

socket.on('message', function (message) {
  log('Got message: ', message);
  socket.broadcast.emit('message', message);
});

En segundo lugar, administra las "salas" de chat de video de WebRTC:

if (numClients === 0) {
  socket.join(room);
  socket.emit('created', room, socket.id);
} else if (numClients === 1) {
  socket.join(room);
  socket.emit('joined', room, socket.id);
  io.sockets.in(room).emit('ready');
} else { // max two clients
  socket.emit('full', room);
}

Nuestra sencilla aplicación de WebRTC permitirá que un máximo de dos pares compartan una sala.

HTML y JavaScript

Actualiza index.html para que se vea de la siguiente manera:

<!DOCTYPE html>
<html>

<head>

  <title>Realtime communication with WebRTC</title>

  <link rel="stylesheet" href="css/main.css" />

</head>

<body>

  <h1>Realtime communication with WebRTC</h1>

  <script src="/socket.io/socket.io.js"></script>
  <script src="js/main.js"></script>
  
</body>

</html>

En este paso, no verás nada en la página: todos los registros se realizan en la consola del navegador. (Para ver la consola en Chrome, presiona Ctrl + Mayúsculas + J o Comando + Opción + J si usas una Mac).

Reemplaza js/main.js por lo siguiente:

'use strict';

var isInitiator;

window.room = prompt("Enter room name:");

var socket = io.connect();

if (room !== "") {
  console.log('Message from client: Asking to join room ' + room);
  socket.emit('create or join', room);
}

socket.on('created', function(room, clientId) {
  isInitiator = true;
});

socket.on('full', function(room) {
  console.log('Message from client: Room ' + room + ' is full :^(');
});

socket.on('ipaddr', function(ipaddr) {
  console.log('Message from client: Server IP address is ' + ipaddr);
});

socket.on('joined', function(room, clientId) {
  isInitiator = false;
});

socket.on('log', function(array) {
  console.log.apply(console, array);
});

Configura Socket.IO para que se ejecute en Node.js

En el archivo HTML, es posible que hayas visto que usas un archivo Socket.IO:

<script src="/socket.io/socket.io.js"></script>

En el nivel superior de tu directorio work, crea un archivo llamado package.json con el siguiente contenido:

{
  "name": "webrtc-codelab",
  "version": "0.0.1",
  "description": "WebRTC codelab",
  "dependencies": {
    "node-static": "^0.7.10",
    "socket.io": "^1.2.0"
  }
}

Este es un manifiesto de la app que le indica al administrador de paquetes de Node (npm) qué dependencias del proyecto instalar.

Para instalar las dependencias (como /socket.io/socket.io.js), ejecuta lo siguiente desde la terminal de línea de comandos, en tu directorio work:

npm install

Deberías ver un registro de instalación que finaliza de la siguiente manera:

3ab06b7bcc7664b9.png

Como puedes ver, npm instaló las dependencias definidas en package.json.

Crea un archivo nuevo index.js en el nivel superior de tu directorio work (no en el directorio js) y agrega el siguiente código:

'use strict';

var os = require('os');
var nodeStatic = require('node-static');
var http = require('http');
var socketIO = require('socket.io');

var fileServer = new(nodeStatic.Server)();
var app = http.createServer(function(req, res) {
  fileServer.serve(req, res);
}).listen(8080);

var io = socketIO.listen(app);
io.sockets.on('connection', function(socket) {

  // convenience function to log server messages on the client
  function log() {
    var array = ['Message from server:'];
    array.push.apply(array, arguments);
    socket.emit('log', array);
  }

  socket.on('message', function(message) {
    log('Client said: ', message);
    // for a real app, would be room-only (not broadcast)
    socket.broadcast.emit('message', message);
  });

  socket.on('create or join', function(room) {
    log('Received request to create or join room ' + room);

    var clientsInRoom = io.sockets.adapter.rooms[room];
    var numClients = clientsInRoom ? Object.keys(clientsInRoom.sockets).length : 0;

    log('Room ' + room + ' now has ' + numClients + ' client(s)');

    if (numClients === 0) {
      socket.join(room);
      log('Client ID ' + socket.id + ' created room ' + room);
      socket.emit('created', room, socket.id);

    } else if (numClients === 1) {
      log('Client ID ' + socket.id + ' joined room ' + room);
      io.sockets.in(room).emit('join', room);
      socket.join(room);
      socket.emit('joined', room, socket.id);
      io.sockets.in(room).emit('ready');
    } else { // max two clients
      socket.emit('full', room);
    }
  });

  socket.on('ipaddr', function() {
    var ifaces = os.networkInterfaces();
    for (var dev in ifaces) {
      ifaces[dev].forEach(function(details) {
        if (details.family === 'IPv4' && details.address !== '127.0.0.1') {
          socket.emit('ipaddr', details.address);
        }
      });
    }
  });

});

En la terminal de la línea de comandos, ejecuta el siguiente comando en el directorio work:

node index.js

En tu navegador, abre localhost:8080.

Cada vez que abras esta URL, se te pedirá que ingreses un nombre de sala. Para unirte a la misma sala, elige el mismo nombre cada vez, por ejemplo, "foo".

Abre una página Nueva pestaña y vuelve a abrir localhost:8080. Elige el mismo nombre de habitación.

Abre localhost:8080 en una tercera pestaña o ventana. Vuelve a elegir el mismo nombre de la habitación.

Consulta la consola en cada una de las pestañas: Deberías ver el registro del código JavaScript anterior.

Puntos adicionales

  1. ¿Qué mecanismos de mensajería alternativos podrían ser posibles? ¿Qué problemas podrías encontrar al usar WebSocket "puro"?
  2. ¿Qué problemas podrían surgir al escalar esta aplicación? ¿Puedes desarrollar un método para probar miles o millones de solicitudes de salas simultáneas?
  3. Esta app usa una instrucción de JavaScript para obtener el nombre de una sala. Busca una forma de obtener el nombre de la sala a partir de la URL. Por ejemplo, localhost:8080/foo proporcionaría el nombre de la sala foo.

Qué aprendiste

En este paso, aprendiste a hacer lo siguiente:

  • Usa npm para instalar las dependencias del proyecto según se especifica en package.json
  • Ejecuta un servidor de Node.js para entregar archivos estáticos.
  • Configura un servicio de mensajería en Node.js con socket.io.
  • Úsalo para crear "salas" e intercambiar mensajes.

Encontrarás una versión completa de este paso en la carpeta step-04.

Más información

Cuál es el próximo paso

Descubre cómo usar la señalización para permitir que dos usuarios establezcan una conexión de igual a igual.

8. Combina la conexión entre pares y la señalización

Qué aprenderás

En este paso, descubrirás cómo hacer lo siguiente:

  • Ejecuta un servicio de señalización de WebRTC con Socket.IO que se ejecuta en Node.js
  • Usa ese servicio para intercambiar metadatos de WebRTC entre pares.

Encontrarás una versión completa de este paso en la carpeta step-05.

Reemplaza HTML y JavaScript

Reemplaza el contenido de index.html con lo siguiente:

<!DOCTYPE html>
<html>

<head>

  <title>Realtime communication with WebRTC</title>

  <link rel="stylesheet" href="/css/main.css" />

</head>

<body>

  <h1>Realtime communication with WebRTC</h1>

  <div id="videos">
    <video id="localVideo" autoplay muted></video>
    <video id="remoteVideo" autoplay></video>
  </div>

  <script src="/socket.io/socket.io.js"></script>
  <script src="https://webrtc.github.io/adapter/adapter-latest.js"></script>
  <script src="js/main.js"></script>
  
</body>

</html>

Reemplaza js/main.js por el contenido de step-05/js/main.js.

Ejecuta el servidor Node.js

Si no sigues este codelab desde tu directorio work, es posible que debas instalar las dependencias de la carpeta step-05 o de tu carpeta de trabajo actual. Ejecuta el siguiente comando desde tu directorio de trabajo:

npm install

Una vez instalado, si tu servidor Node.js no se está ejecutando, inícialo llamando al siguiente comando en el directorio work:

node index.js

Asegúrate de usar la versión de index.js del paso anterior que implementa Socket.IO. Para obtener más información sobre Node y Socket IO, consulta la sección "Configura un servicio de señalización para intercambiar mensajes".

En tu navegador, abre localhost:8080.

Vuelve a abrir localhost:8080 en una pestaña o ventana nueva. Un elemento de video mostrará la transmisión local de getUserMedia() y el otro mostrará el video "remoto" transmitido a través de RTCPeerconnection.

Visualiza el registro en la consola del navegador.

Puntos adicionales

  1. Esta aplicación solo admite el chat de video uno a uno. ¿Cómo cambiarías el diseño para permitir que más de una persona comparta la misma sala de chat de video?
  2. El ejemplo tiene el nombre de la habitación foo codificado. ¿Cuál sería la mejor forma de habilitar otros nombres de habitaciones?
  3. ¿Cómo compartirían los usuarios el nombre de la sala? Intenta crear una alternativa para compartir los nombres de las habitaciones.
  4. Cómo podrías cambiar la app

Qué aprendiste

En este paso, aprendiste a hacer lo siguiente:

  • Ejecuta un servicio de señalización de WebRTC con Socket.IO que se ejecuta en Node.js.
  • Usa ese servicio para intercambiar metadatos de WebRTC entre pares.

Encontrarás una versión completa de este paso en la carpeta step-05.

Sugerencias

  • Las estadísticas de WebRTC y los datos de depuración están disponibles en chrome://webrtc-internals.
  • Puedes usar test.webrtc.org para verificar tu entorno local y probar la cámara y el micrófono.
  • Si tienes problemas extraños con el almacenamiento en caché, prueba lo siguiente:
  • Mantén presionada la tecla Ctrl y haz clic en el botón Volver a cargar para realizar una actualización forzada.
  • Reinicia el navegador
  • Ejecuta npm cache clean desde la línea de comandos.

Cuál es el próximo paso

Descubre cómo tomar una foto, obtener los datos de la imagen y compartirlos entre pares remotos.

9. Toma una foto y compártela a través de un canal de datos

Qué aprenderás

En este paso, aprenderás a hacer lo siguiente:

  • Toma una foto y obtén los datos de ella con el elemento canvas.
  • Intercambiar datos de imágenes con un usuario remoto

Encontrarás una versión completa de este paso en la carpeta step-06.

Cómo funciona

Anteriormente, aprendiste a intercambiar mensajes de texto con RTCDataChannel.

Este paso permite compartir archivos completos: en este ejemplo, fotos capturadas a través de getUserMedia().

Las partes principales de este paso son las siguientes:

  1. Establece un canal de datos. Ten en cuenta que no agregarás ningún flujo de medios a la conexión de pares en este paso.
  2. Captura la transmisión de video de la cámara web del usuario con getUserMedia():
var video = document.getElementById('video');

function grabWebCamVideo() {
  console.log('Getting user media (video) ...');
  navigator.mediaDevices.getUserMedia({
    video: true
  })
  .then(gotStream)
  .catch(function(e) {
    alert('getUserMedia() error: ' + e.name);
  });
}
  1. Cuando el usuario haga clic en el botón Snap, obtén una instantánea (un fotograma de video) de la transmisión de video y muéstrala en un elemento canvas:
var photo = document.getElementById('photo');
var photoContext = photo.getContext('2d');

function snapPhoto() {
  photoContext.drawImage(video, 0, 0, photo.width, photo.height);
  show(photo, sendBtn);
}
  1. Cuando el usuario hace clic en el botón Enviar, convierte la imagen en bytes y envíalos a través de un canal de datos:
function sendPhoto() {
  // Split data channel message in chunks of this byte length.
  var CHUNK_LEN = 64000;
  var img = photoContext.getImageData(0, 0, photoContextW, photoContextH),
    len = img.data.byteLength,
    n = len / CHUNK_LEN | 0;

  console.log('Sending a total of ' + len + ' byte(s)');
  dataChannel.send(len);

  // split the photo and send in chunks of about 64KB
  for (var i = 0; i < n; i++) {
    var start = i * CHUNK_LEN,
      end = (i + 1) * CHUNK_LEN;
    console.log(start + ' - ' + (end - 1));
    dataChannel.send(img.data.subarray(start, end));
  }

  // send the reminder, if any
  if (len % CHUNK_LEN) {
    console.log('last ' + len % CHUNK_LEN + ' byte(s)');
    dataChannel.send(img.data.subarray(n * CHUNK_LEN));
  }
}
  1. El receptor convierte los bytes del mensaje del canal de datos en una imagen y la muestra al usuario:
function receiveDataChromeFactory() {
  var buf, count;

  return function onmessage(event) {
    if (typeof event.data === 'string') {
      buf = window.buf = new Uint8ClampedArray(parseInt(event.data));
      count = 0;
      console.log('Expecting a total of ' + buf.byteLength + ' bytes');
      return;
    }

    var data = new Uint8ClampedArray(event.data);
    buf.set(data, count);

    count += data.byteLength;
    console.log('count: ' + count);

    if (count === buf.byteLength) {
      // we're done: all data chunks have been received
      console.log('Done. Rendering photo.');
      renderPhoto(buf);
    }
  };
}

function renderPhoto(data) {
  var canvas = document.createElement('canvas');
  canvas.width = photoContextW;
  canvas.height = photoContextH;
  canvas.classList.add('incomingPhoto');
  // trail is the element holding the incoming images
  trail.insertBefore(canvas, trail.firstChild);

  var context = canvas.getContext('2d');
  var img = context.createImageData(photoContextW, photoContextH);
  img.data.set(data);
  context.putImageData(img, 0, 0);
}

Obtén el código

Reemplaza el contenido de tu carpeta work por el contenido de step-06. Tu archivo index.html en work ahora debería verse de la siguiente manera**:**

<!DOCTYPE html>
<html>

<head>

  <title>Realtime communication with WebRTC</title>

  <link rel="stylesheet" href="/css/main.css" />

</head>

<body>

  <h1>Realtime communication with WebRTC</h1>

  <h2>
    <span>Room URL: </span><span id="url">...</span>
  </h2>

  <div id="videoCanvas">
    <video id="camera" autoplay></video>
    <canvas id="photo"></canvas>
  </div>

  <div id="buttons">
    <button id="snap">Snap</button><span> then </span><button id="send">Send</button>
    <span> or </span>
    <button id="snapAndSend">Snap &amp; Send</button>
  </div>

  <div id="incoming">
    <h2>Incoming photos</h2>
    <div id="trail"></div>
  </div>

  <script src="/socket.io/socket.io.js"></script>
  <script src="https://webrtc.github.io/adapter/adapter-latest.js"></script>
  <script src="js/main.js"></script>

</body>

</html>

Si no sigues este codelab desde tu directorio de trabajo, es posible que debas instalar las dependencias de la carpeta step-06 o de tu carpeta de trabajo actual. Simplemente ejecuta el siguiente comando desde tu directorio de trabajo:

npm install

Una vez instalado, si tu servidor Node.js no se está ejecutando, inícialo llamando al siguiente comando desde tu directorio work:

node index.js

Asegúrate de usar la versión de index.js que implementa Socket.IO y recuerda reiniciar el servidor de Node.js si realizas cambios. Para obtener más información sobre Node y Socket IO, consulta la sección "Configura un servicio de señalización para intercambiar mensajes".

Si es necesario, haz clic en el botón Permitir para permitir que la app use tu cámara web.

La app creará un ID de habitación aleatorio y lo agregará a la URL. Abre la URL de la barra de direcciones en una nueva pestaña o ventana del navegador.

Haz clic en el botón Snap & Send y, luego, mira el área Incoming en la otra pestaña en la parte inferior de la página. La app transfiere fotos entre pestañas.

Deberías ver algo como esto:

911b40f36ba6ba8.png

Puntos adicionales

  1. ¿Cómo puedes cambiar el código para que se pueda compartir cualquier tipo de archivo?

Más información

Qué aprendiste

  • Cómo tomar una foto y obtener sus datos con el elemento canvas
  • Cómo intercambiar esos datos con un usuario remoto

Encontrarás una versión completa de este paso en la carpeta step-06.

10. Felicitaciones

Compilaste una app para transmitir videos y realizar intercambios de datos en tiempo real.

Qué aprendiste

En este codelab aprendiste a hacer lo siguiente:

  • Obtener video de tu cámara web
  • Transmite video con RTCPeerConnection.
  • Transmite datos con RTCDataChannel.
  • Configura un servicio de señalización para intercambiar mensajes.
  • Combina la conexión entre pares y la señalización.
  • Toma una foto y compártela a través de un canal de datos.

Próximos pasos

Más información

  • En webrtc.org, se encuentra disponible una variedad de recursos para comenzar a usar WebRTC.