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 cuenta con varias APIs de JavaScript. Haz clic en los vínculos para ver las demostraciones.

¿Dónde puedo usar WebRTC?

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

¿Qué es la señalización?

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 y protocolos de señalización. En este codelab, usarás Socket.IO para la mensajería, pero existen muchas alternativas.

¿Qué son STUN y TURN?

WebRTC está diseñado para funcionar entre pares, por lo que los usuarios pueden conectarse por la ruta más directa posible. Sin embargo, WebRTC está diseñado para funcionar con las redes del mundo real: las aplicaciones cliente deben atravesar puertas de enlace NAT y firewalls, y las redes entre pares necesitan resguardos 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 los servidores TURN funcionan como servidores de retransmisión en caso de que falle la comunicación entre pares. (WebRTC en el mundo real explica 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 mecanismos de señalización no están definidos por los estándares de WebRTC, por lo que depende de ti asegurarte de usar protocolos seguros.

2. Descripción general

Compila una app para obtener videos y tomar instantáneas con tu cámara web y compartirlos entre pares a través de WebRTC. Durante el proceso, aprenderás a usar las principales APIs de WebRTC y a configurar un servidor de mensajería con Node.js.

Qué aprenderás

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

Requisitos

  • Chrome 47 o versiones posteriores
  • Web Server para Chrome o usa 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 estás familiarizado con Git, puedes clonarlo desde GitHub para descargar el código de este codelab:

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

Harás todo el trabajo de codificación en el directorio llamado work.

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

Instala y verifica el servidor web

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

6ddeb4aee53c0f0e.png

Después de instalar la app de Servidor web para Chrome, haz clic en el acceso directo a las Apps de Chrome desde la barra de favoritos, una página Nueva pestaña o el Selector de aplicaciones:

1d2b4aa977ab7e24.png

Haz clic en el ícono de Web Server:

27fce4494f641883.png

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

Captura de pantalla 2016-02-18 a las 11.48.14 AM.png

Haz clic en el botón ELEGIR CARPETA y selecciona la carpeta de trabajo 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 Servidor web en la sección URL del servidor web.

En Opciones, marca la casilla junto a Mostrar automáticamente index.html, como se muestra a continuación:

Captura de pantalla 2016-02-18 a las 11.56.30 AM.png

Luego, detén y reinicia el servidor deslizando el botón de activación etiquetado Web Server: STARTED hacia la izquierda y, luego, de nuevo hacia la derecha.

Captura de pantalla 2016-02-18 a las 12.22.18 PM.png

Ahora visita tu sitio de trabajo en tu navegador web haciendo clic en la URL destacada del servidor web. Deberías ver una página como la siguiente, que corresponde a work/index.html:

18a705cb6ccc5181.png

Desde luego, esta app todavía no está haciendo nada interesante. Hasta ahora, solo se trata de un esqueleto mínimo que utilizamos para asegurarnos de que tu servidor web funcione correctamente. Agregarás funciones y características de diseño en los pasos posteriores.

4. Transmitir videos desde tu cámara web

Qué aprenderás

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

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

En la carpeta step-01 se encuentra una versión completa de este paso.

Un poco de HTML...

Agrega un elemento video y un elemento script a index.html en el 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 un poco 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 el navegador. Deberías ver algo como esto (con la vista de tu cámara web, por supuesto):

9297048e43ed0f3d.png

Cómo funciona

Después de la llamada a getUserMedia(), el navegador solicita permiso al usuario para acceder a la cámara (si es la primera vez que se solicita acceso a la cámara para el origen actual). Si se ejecuta de forma correcta, se muestra un MediaStream que puede usar un elemento multimedia 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é contenido multimedia deseas obtener. En este ejemplo, solo 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
    }
  }
}

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

Si getUserMedia() funciona correctamente, la transmisión de video por Internet 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 alcance global, por lo que puedes inspeccionarlo desde la consola del navegador: abre la consola, escribe stream y presiona Intro. (Para ver la consola en Chrome, presiona Ctrl + Mayúsculas + J o, si usas una Mac, Comando + Opción +).
  • ¿Qué muestra localStream.getVideoTracks()?
  • Llama a localStream.getVideoTracks()[0].stop().
  • Observa el objeto constraints: ¿qué sucede cuando lo cambias a {audio: true, video: true}?
  • ¿De qué tamaño es el elemento de video? ¿Cómo puedes obtener el tamaño natural del video a partir de JavaScript, en lugar del tamaño de visualización? Usa las herramientas para desarrolladores de Chrome para comprobarlo.
  • Prueba agregar filtros CSS al elemento de video. Por ejemplo:
video {
  filter: blur(4px) invert(1) opacity(0.5);
}
  • Intenta agregar filtros de SVG. Por ejemplo:
video {
   filter: hue-rotate(180deg) saturate(200%);
 }

Qué aprendiste

En este paso, aprendiste a hacer lo siguiente:

  • Obtén un video de tu cámara web.
  • Establece restricciones de contenido multimedia.
  • Desordenar el elemento de video

En la carpeta step-01 se encuentra una versión completa de este paso.

Sugerencias

  • No olvides el atributo autoplay en el elemento video. Sin ella, solo verás un marco.
  • Hay muchas más opciones para las restricciones de getUserMedia(). Observa 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 supere el tamaño del 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 un video, pero ¿cómo lo transmites? Descúbrelo en el siguiente paso.

5. Transmite videos con RTCPeerConnection

Qué aprenderás

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

  • Abstrae las diferencias del navegador con la corrección de compatibilidad de WebRTC, adapter.js.
  • Usa la API de RTCPeerConnection para transmitir videos.
  • Controla la captura y la transmisión de contenido multimedia.

En la carpeta step-2 se encuentra una versión completa de este paso.

¿Qué es RTCPeerConnection?

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

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

No tiene un uso muy práctico, pero es útil para comprender cómo funciona RTCPeerConnection.

Cómo 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 mediante RTCPeerconnection. (En una aplicación real, un elemento de video mostraría la transmisión local y el otro la transmisión remota).

Agrega la corrección de compatibilidad de adaptador.js

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

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

Index.html 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 RTCPeerConnection

Reemplaza main.js con la versión 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 con apps similares. 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 tiene muchas ventajas...

WebRTC usa la API de RTCPeerConnection para configurar una conexión de transmisión de video entre clientes de WebRTC, lo que se conoce como intercambios de tráfico.

En este ejemplo, los dos objetos RTCPeerConnection están en la misma página: pc1 y pc2. No es muy práctico, pero sirve para demostrar cómo funcionan las APIs.

Configurar una llamada entre pares de WebRTC implica tres tareas:

  • Crea una RTCPeerConnection para cada final de la llamada y, en cada extremo, agrega la transmisión local de getUserMedia().
  • Obtén y comparte información de la red: los extremos de conexión potenciales se conocen como candidatos ICE.
  • Obtiene y comparte descripciones locales y remotas: metadatos sobre medios locales en formato SDP.

Imagina que Alicia y Roberto quieren usar RTCPeerConnection para configurar un chat de video.

En primer lugar, Alice y Bob intercambian información de la red. La expresión “encontrar candidatos” se refiere al proceso de encontrar 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 le pasa:
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 los candidatos de la red están disponibles.
  2. Alice envía datos serializados de candidatos a Bob. En una aplicación real, este proceso (conocido como indicación) se realiza a través de un servicio de mensajería. 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 Roberto recibe un mensaje candidato de Alice, llama a addIceCandidate() para agregar el candidato a la descripción de 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 necesitan encontrar e intercambiar información de medios de audio y video locales y remotos, como capacidades de resolución y códec. La señalización para intercambiar información de configuración de medios se realiza intercambiando BLOB de metadatos, conocidos como offer y answer, mediante el formato de protocolo de descripción de sesión, conocido como SDP:

  1. Alice ejecuta el método createOffer() de RTCPeerConnection. La promesa que se muestra proporciona una RTCSessionDescription: la descripción de la sesión local de Alice:
trace('localPeerConnection createOffer start.');
localPeerConnection.createOffer(offerOptions)
  .then(createdOffer).catch(setSessionDescriptionError);
  1. Si se ejecuta de forma correcta, Alice establece la descripción local con setLocalDescription() y, luego, envía la descripción de esta sesión a Bob a través del canal de señalización.
  2. Roberto establece la descripción que Alicia le envió como descripción remota mediante setRemoteDescription().
  3. Roberto ejecuta el método createAnswer() de RTCPeerConnection y le pasa la descripción remota que obtuvo de Alice para que se pueda generar una sesión local compatible con la suya. La promesa createAnswer() pasa una RTCSessionDescription: Bob la establece como descripción local y se la envía a Alice.
  4. Cuando Alice obtiene 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. Echa un vistazo a chrome://webrtc-internals. Proporciona estadísticas y datos de depuración de WebRTC. (Puedes encontrar una lista completa de las URL de Chrome en chrome://about).
  2. Diseña la página con CSS:
  • Coloca los videos uno al lado del otro.
  • Haga que los botones tengan el mismo ancho y el texto sea 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, consulta 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 la corrección de compatibilidad de WebRTC, adapter.js.
  • Usa la API de RTCPeerConnection para transmitir videos.
  • Controla la captura y la transmisión de contenido multimedia.
  • Compartir información multimedia y de red entre pares para habilitar una llamada de WebRTC

En la carpeta step-2 se encuentra una versión completa de este paso.

Sugerencias

  • Hay mucho que aprender en este paso. Para encontrar otros recursos que explican RTCPeerConnection con más detalle, visita webrtc.org. En esta página, se incluyen sugerencias para los frameworks de JavaScript, en caso de que quieras usar WebRTC, pero no deseas derivar las APIs.
  • Obtén más información sobre la corrección de compatibilidad de adaptador.js en el repositorio de GitHub de adapter.js.
  • ¿Quieres conocer la mejor app de videochat del mundo? Observa AppRTC, la app canónica del proyecto WebRTC para llamadas de WebRTC: app, code. 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 promesas 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 videos entre pares, pero este codelab también se trata de datos.

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

6. Cómo usar RTCDataChannel para intercambiar datos

Qué aprenderás

  • Cómo intercambiar datos entre extremos de WebRTC (intercambios de tráfico)

En la carpeta step-03 se encuentra una versión completa de este paso.

Actualiza tu HTML

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

Quita de index.html los elementos de video y botón, 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>

Una área de texto será para ingresar texto, en la otra se mostrará el texto tal como se transmite entre pares.

El archivo index.html 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 con el contenido de step-03/js/main.js.

Prueba la transmisión de datos entre apps similares. Para ello, abre index.html, presiona Iniciar para configurar la conexión con apps similares, ingresa texto en el textarea de la izquierda y, luego, haz clic en Enviar para transferir el texto usando 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 la misma que la 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 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 sobre el rendimiento. Puedes encontrar más información sobre las opciones en Mozilla Developer Network.

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, aunque eso implique perder algunos datos?
  2. Utiliza CSS para mejorar el diseño de la página y agrega un atributo de marcador de posición a "dataChannelReciba" en el área de texto.
  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 intercambios de tráfico de WebRTC.
  • Intercambiar datos de texto entre apps similares.

En la carpeta step-03 se encuentra una versión completa de este paso.

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 lo haces entre diferentes máquinas? Primero, debes configurar un canal de señalización para intercambiar mensajes de metadatos. En el siguiente paso, descubre cómo hacerlo.

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

Qué aprenderás

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

  • Usa npm para instalar dependencias del proyecto como se especifica en package.json.
  • Ejecuta un servidor Node.js y usa node-static para entregar archivos estáticos.
  • Configurar un servicio de mensajería en Node.js mediante Socket.IO
  • Usar esa información para crear "salas" e intercambiar mensajes.

En la carpeta step-04 se encuentra una versión completa de este paso.

Conceptos

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

  • Información del candidato (red).
  • Mensajes de Offer y answer que proporcionan información sobre contenido multimedia, como la resolución y los códecs.

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

En los pasos anteriores, los objetos RTCPeerConnection del emisor y del receptor están en la misma página, por lo que se indica es simplemente pasar metadatos entre objetos.

En una aplicación real, las RTCPeerConnections de envío y recepción se ejecutan en páginas web en dispositivos diferentes, y necesitas una forma de que comuniquen los metadatos.

Para ello, debes usar un servidor de señalización: un servidor que puede pasar mensajes entre clientes de WebRTC (intercambios de tráfico). Los mensajes reales son texto sin formato: objetos JavaScript en cadena.

Requisito previo: instalar Node.js

Para ejecutar los siguientes 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 siguientes pasos (ejecutar npm install), así como ejecutar un servidor localhost pequeño para ejecutar el codelab (con node index.js). Estos comandos se indicarán más adelante, cuando sean necesarios.

Acerca de la aplicación

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

En este paso, compilarás un servidor simple de señalización de Node.js con el módulo Socket.IO para Node.js y la biblioteca de JavaScript para mensajería. Será útil tener experiencia con Node.js y Socket.IO, pero no crucial. 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 aplicación web) se implementa en index.html.

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

Primero, actúa como una retransmisión de mensajes:

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

En segundo lugar, administra las "salas" de videochats 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 WebRTC permitirá que un máximo de dos pares compartan una sala.

HTML y JavaScript

Actualiza el archivo 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: todo el registro se realiza en la consola del navegador. (Para ver la consola en Chrome, presiona Ctrl + Mayúsculas + J o, si usas una Mac, Comando + Opción +).

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, puedes haber visto que estás usando 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 debe instalar.

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

npm install

Deberías ver un registro de instalación que termina 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 el navegador, abre localhost:8080.

Cada vez que abras esta URL, se te pedirá que ingreses un nombre para la sala. Para unirse a la misma sala, elija siempre el mismo nombre, 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.

Revisa la consola en cada una de las pestañas: deberías ver el registro de JavaScript anterior.

Puntos adicionales

  1. ¿Qué mecanismos alternativos de mensajería podrían ser posibles? ¿Qué problemas podrías encontrar al usar ¿ WebSocket?
  2. ¿Qué problemas podrían surgir con el escalamiento de esta aplicación? ¿Puede desarrollar un método para probar miles o millones de solicitudes de salas de reuniones simultáneas?
  3. Esta app usa un mensaje de JavaScript para obtener el nombre de una habitación. Busca una manera de obtener el nombre de la sala de la URL. Por ejemplo, localhost:8080/foo le daría el nombre de sala foo.

Qué aprendiste

En este paso, aprendiste a hacer lo siguiente:

  • Usa npm para instalar dependencias del proyecto como se especifica en package.json
  • Ejecutar un servidor Node.js a un servidor de archivos estáticos
  • Configura un servicio de mensajería en Node.js con socket.io.
  • Usar esa información para crear "salas" e intercambiar mensajes.

En la carpeta step-04 se encuentra una versión completa de este paso.

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 intercambio de tráfico.

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

Qué aprenderás

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

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

En la carpeta step-05 se encuentra una versión completa de este paso.

Cómo reemplazar HTML y JavaScript

Reemplaza el contenido de index.html por 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 con el contenido de step-05/js/main.js.

Ejecuta el servidor Node.js

Si no estás siguiendo este codelab desde tu directorio work, es posible que debas instalar las dependencias de la carpeta step-05 o 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. Si deseas obtener más información sobre el E/S de nodo y socket, consulta la sección “Configura un servicio de señalización para intercambiar mensajes”.

En el navegador, abre localhost:8080.

Vuelve a abrir localhost:8080 en una pestaña o ventana nueva. En un elemento de video, se mostrará la transmisión local de getUserMedia() y en el otro, se mostrará el control remoto. de video transmitido mediante RTCPeerconnection.

Visualiza los registros en la consola del navegador.

Puntos adicionales

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

Qué aprendiste

En este paso, aprendiste a hacer lo siguiente:

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

En la carpeta step-05 se encuentra una versión completa de este paso.

Sugerencias

  • Las estadísticas y los datos de depuración de WebRTC están disponibles en chrome://webrtc-internals.
  • test.webrtc.org se puede usar para verificar tu entorno local y probar tu cámara y micrófono.
  • Si tienes problemas extraños con el almacenamiento en caché, prueba lo siguiente:
  • Para realizar una actualización forzada, mantén presionada la tecla Ctrl y haz clic en el botón Volver a cargar.
  • 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 las imágenes y compartirlos con colegas 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 con el elemento lienzo.
  • Intercambiar datos de imagen con un usuario remoto

En la carpeta step-06 se encuentra una versión completa de este paso.

Cómo funciona

Anteriormente, aprendiste a intercambiar mensajes de texto con RTCDataChannel.

Este paso permite compartir archivos enteros; en este ejemplo, fotos tomadas con getUserMedia().

Los componentes centrales de este paso son los siguientes:

  1. Establecer un canal de datos Ten en cuenta que no agregarás ninguna transmisión de contenido multimedia a la conexión de intercambio de tráfico en este paso.
  2. Captura la transmisión de video por Internet 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 la transmisión de video por Internet 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 haga 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 lado receptor vuelve los bytes de mensajes del canal de datos a una imagen y le muestra la imagen 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 con 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 estás siguiendo este codelab desde tu directorio work, es posible que debas instalar las dependencias de la carpeta step-06 o tu carpeta de trabajo actual. Solo debes ejecutar 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 implemente Socket.IO y recuerda reiniciar tu servidor Node.js si realizas cambios. Si deseas obtener más información sobre el E/S de nodo y socket, 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 aplicación use tu cámara web.

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

Haz clic en el botón Ajustar y Enviar y, luego, observa el área Entrantes en la otra pestaña que se encuentra en la parte inferior de la página. La app transfiere fotos entre pestañas.

Debería ver algo como esto:

911b40f36ba6ba8.png

Puntos adicionales

  1. ¿Cómo puedes modificar el código para que sea posible compartir cualquier tipo de archivo?

Más información

Qué aprendiste

  • Cómo tomar una foto y obtener los datos de ella con el elemento lienzo
  • Cómo intercambiar esos datos con un usuario remoto

En la carpeta step-06 se encuentra una versión completa de este paso.

10. Felicitaciones

Creaste una app para transmitir video en tiempo real e intercambiar datos.

Qué aprendiste

En este codelab aprendiste a hacer lo siguiente:

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

Próximos pasos

Más información

  • En webrtc.org, encontrarás una gran variedad de recursos para comenzar a usar WebRTC.