Comunicação em tempo real com o WebRTC

1. Introdução

O WebRTC é um projeto de código aberto que possibilita a comunicação em tempo real de áudio, vídeo e dados em apps nativos e da Web.

O WebRTC tem várias APIs JavaScript. Clique nos links para ver as demonstrações.

Onde posso usar o WebRTC?

No Firefox, no Opera e no Chrome para computador e Android. O WebRTC também está disponível para apps nativos no iOS e no Android.

O que é sinalização?

O WebRTC usa o RTCPeerConnection para comunicar dados de streaming entre navegadores, mas também precisa de um mecanismo para coordenar a comunicação e enviar mensagens de controle, um processo conhecido como sinalização. Os métodos e protocolos de sinalização não são especificados pelo WebRTC. Neste codelab, você usará o Socket.IO para enviar mensagens, mas há muitas alternativas (link em inglês).

O que são STUN e TURN?

O WebRTC foi projetado para funcionar ponto a ponto, para que os usuários possam se conectar pela rota mais direta possível. No entanto, o WebRTC foi criado para lidar com as redes do mundo real: os aplicativos clientes precisam passar por gateways NAT e firewalls, e a rede ponto a ponto precisa de fallbacks caso a conexão direta falhe. Como parte desse processo, as APIs WebRTC usam servidores STUN para conseguir o endereço IP do seu computador e os servidores TURN funcionam como servidores de redirecionamento em caso de falha na comunicação ponto a ponto. O WebRTC no mundo real explica isso em mais detalhes.

O WebRTC é seguro?

A criptografia é obrigatória para todos os componentes WebRTC, e as APIs JavaScript só podem ser usadas de origens seguras (HTTPS ou localhost). Os mecanismos de sinalização não são definidos por padrões WebRTC. Portanto, cabe a você usar protocolos seguros.

2. Visão geral

Crie um app para gerar vídeos, criar capturas de tela com a webcam e compartilhá-las ponto a ponto via WebRTC. Ao longo do caminho, você vai aprender a usar as principais APIs WebRTC e configurar um servidor de mensagens usando Node.js.

O que você vai aprender

  • Obter vídeo de sua webcam
  • Transmitir vídeo com RTCPeerConnection
  • Transmitir dados com o RTCDataChannel
  • Configurar um serviço de sinalização para trocar mensagens
  • Combinar sinalização e conexão de peering
  • Tirar uma foto e compartilhá-la em um canal de dados

O que é necessário

  • Chrome 47 ou superior
  • Web Server for Chrome ou use seu próprio servidor da Web de preferência.
  • Código de amostra
  • Um editor de texto
  • Noções básicas de HTML, CSS e JavaScript

3. Fazer o download do exemplo de código

Fazer o download do código

Se você já conhece o git, clone o código para este codelab no GitHub:

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

Se preferir, clique no botão a seguir para fazer o download de um arquivo ZIP do código:

Abra o arquivo ZIP transferido por download. Isso vai descompactar uma pasta de projeto (adaptive-web-media) que contém uma pasta para cada etapa deste codelab, além de todos os recursos necessários.

Você vai fazer todo o trabalho de programação no diretório work.

As pastas step-nn contêm uma versão concluída para cada etapa deste codelab. Elas servem como referência.

Instalar e verificar o servidor da Web

Você pode usar seu próprio servidor da Web, mas este codelab foi projetado para funcionar bem com o Chrome Web Server. Se você ainda não tem esse app instalado, faça isso pela Chrome Web Store.

6ddeb4aee53c0f0e.png

Depois de instalar o app Web Server for Chrome, clique no atalho de apps do Chrome na barra de favoritos, na página "Nova guia" ou no Acesso rápido aos apps:

1d2b4aa977ab7e24.png

Clique no ícone do servidor da Web:

27fce4494f641883.png

Em seguida, você verá esta caixa de diálogo, que permite configurar seu servidor da Web local:

Captura de tela 2016-02-18 às 11.48.14 AM.png

Clique no botão ESCOLHER PASTA e selecione a pasta de trabalho que você acabou de criar. Isso permitirá que você veja seu trabalho em andamento no Chrome pelo URL destacado na caixa de diálogo "Servidor da Web", na seção URLs do servidor da Web.

Em Opções, marque a caixa ao lado de Mostrar index.html automaticamente, conforme mostrado abaixo:

Captura de tela 2016-02-18 às 11.56.30 AM.png

Em seguida, interrompa e reinicie o servidor deslizando o botão Web Server: STARTED para a esquerda e depois de volta para a direita.

Captura de tela 2016-02-18 às 12.22.18 PM.png

Agora visite seu site de trabalho no navegador da Web, clicando no URL do servidor da Web destacado. Uma página semelhante a esta, que corresponde a work/index.html, será exibida:

18a705cb6fc5181.png

Obviamente, esse app ainda não está fazendo nada interessante. Até agora, é apenas um esqueleto mínimo que estamos usando para garantir que seu servidor da Web esteja funcionando corretamente. Você vai adicionar funcionalidades e recursos de layout nas próximas etapas.

4. Fazer streaming de vídeo com sua webcam

O que você vai aprender

Nesta etapa, você descobrirá como:

  • Obtenha um stream de vídeo de sua webcam.
  • Manipular a reprodução do stream.
  • Use CSS e SVG para manipular vídeos.

Uma versão completa desta etapa está na pasta step-01.

Uma pitada de HTML...

Adicione um elemento video e um script ao arquivo index.html no seu diretório 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>

...e uma pitada de JavaScript

Adicione o seguinte ao arquivo main.js na sua pasta 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);

Faça um teste

Abra index.html em seu navegador e você verá algo assim (com a visualização da webcam, é claro):

9297048e43ed0f3d.png

Como funciona

Após a chamada getUserMedia(), o navegador solicita permissão do usuário para acessar a câmera (se esta for a primeira vez que o acesso à câmera foi solicitado para a origem atual). Se for bem-sucedido, um MediaStream será retornado, que pode ser usado por um elemento de mídia por meio do atributo srcObject:

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


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

O argumento constraints permite especificar qual mídia será acessada. Neste exemplo, somente vídeo, já que o áudio está desativado por padrão:

const mediaStreamConstraints = {
  video: true,
};

Você pode usar restrições para requisitos adicionais, como resolução de vídeo:

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

A especificação MediaTrackConstraints lista todos os possíveis tipos de restrição, embora nem todas as opções sejam compatíveis com todos os navegadores. Se a câmera selecionada não oferecer suporte à resolução solicitada, getUserMedia() será rejeitado com um OverconstrainedError, e o usuário não vai receber uma solicitação para conceder permissão de acesso à câmera.

Se getUserMedia() for bem-sucedido, o stream de vídeo da webcam será definido como a origem do elemento de vídeo:

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

Pontos de bônus

  • O objeto localStream transmitido para getUserMedia() está no escopo global. Portanto, é possível inspecioná-lo no console do navegador: abra o console, digite stream e pressione Enter. Para visualizar o console no Chrome, pressione Ctrl-Shift-J ou Command-Option-J se estiver em um Mac.
  • O que o localStream.getVideoTracks() retorna?
  • Tente chamar localStream.getVideoTracks()[0].stop().
  • Observe o objeto de restrições: o que acontece quando você o muda para {audio: true, video: true}?
  • Qual é o tamanho do elemento de vídeo? Como é possível descobrir o tamanho natural do vídeo pelo JavaScript em vez do tamanho de exibição? Use as Ferramentas para desenvolvedores do Chrome para verificar.
  • Tente adicionar filtros de CSS ao elemento de vídeo. Exemplo:
video {
  filter: blur(4px) invert(1) opacity(0.5);
}
  • Tente adicionar filtros SVG. Exemplo:
video {
   filter: hue-rotate(180deg) saturate(200%);
 }

O que você aprendeu

Nesta etapa, você aprendeu a:

  • Obter vídeo de sua webcam.
  • Defina restrições de mídia.
  • Mexa com o elemento de vídeo.

Uma versão completa desta etapa está na pasta step-01.

Dicas

  • Não se esqueça do atributo autoplay no elemento video. Sem isso, você só verá um único frame.
  • Há muito mais opções para restrições de getUserMedia(). Confira a demonstração em webrtc.github.io/samples/src/content/peerconnection/constraints (em inglês). Como você verá, há muitos exemplos interessantes de WebRTC no site.

Prática recomendada

  • Confira se o elemento de vídeo não está além do contêiner. Adicionamos width e max-width para definir um tamanho preferido e um tamanho máximo para o vídeo. O navegador calculará a altura automaticamente:
video {
  max-width: 100%;
  width: 320px;
}

A seguir

Você tem vídeos, mas como transmiti-los? Descubra na próxima etapa.

5. Transmitir vídeo com RTCPeerConnection

O que você vai aprender

Nesta etapa, você descobrirá como:

  • Abstrair diferenças do navegador com o paliativo WebRTC, adapter.js.
  • Use a API RTCPeerConnection para fazer streaming de vídeo.
  • Controle a captura e o streaming de mídia.

Uma versão completa desta etapa está na pasta step-2.

O que é RTCPeerConnection?

O RTCPeerConnection é uma API que faz chamadas WebRTC para transmitir vídeo e áudio, além de trocar dados.

Este exemplo configura uma conexão entre dois objetos RTCPeerConnection (conhecidos como pares) na mesma página.

Não há muito uso prático, mas é bom para entender como o RTCPeerConnection funciona.

Adicionar elementos de vídeo e botões de controle

Em index.html, substitua o único elemento de vídeo por dois elementos de vídeo e três botões:

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

Um elemento de vídeo exibirá o stream de getUserMedia(), e o outro mostrará o mesmo vídeo transmitido via RTCPeerconnection. Em um aplicativo real, um elemento de vídeo exibiria o stream local e o outro, o stream remoto.

Adicionar o shim do adaptador.js

Adicione um link para a versão atual do adapter.js acima do link para main.js:

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

O Index.html ficará assim:

<!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>

Instalar o código RTCPeerConnection

Substitua main.js pela versão na pasta step-02.

Fazer a chamada

Abra index.html, clique no botão Iniciar para receber o vídeo da webcam e clique em Ligar para fazer a conexão de peering. Você verá o mesmo vídeo (da webcam) nos dois elementos de vídeo. Acesse o console do navegador para ver os registros WebRTC.

Como funciona

Esta etapa faz muitas coisas...

O WebRTC usa a API RTCPeerConnection para configurar uma conexão e fazer streaming de vídeo entre clientes WebRTC, conhecidos como peers.

Neste exemplo, os dois objetos RTCPeerConnection estão na mesma página: pc1 e pc2. Não tem muito uso prático, mas é bom para demonstrar como as APIs funcionam.

A configuração de uma chamada entre pares WebRTC envolve três tarefas:

  • Crie um RTCPeerConnection para cada fim da chamada e, em cada extremidade, adicione o stream local de getUserMedia().
  • Receber e compartilhar informações da rede: endpoints de conexão em potencial são conhecidos como candidatos ICE.
  • Receba e compartilhe descrições locais e remotas: metadados sobre a mídia local no formato SDP.

Imagine que Alice e Bob querem usar o RTCPeerConnection para configurar um chat por vídeo.

Primeiro, Alice e Bob trocam informações de rede. A expressão "encontrando candidatos" refere-se ao processo de encontrar interfaces e portas de rede usando o framework ICE (em inglês).

  1. Alice cria um objeto RTCPeerConnection com um gerenciador onicecandidate (addEventListener('icecandidate')). Isso corresponde ao seguinte código do main.js:
let localPeerConnection;
localPeerConnection = new RTCPeerConnection(servers);
localPeerConnection.addEventListener('icecandidate', handleConnection);
localPeerConnection.addEventListener(
    'iceconnectionstatechange', handleConnectionChange);
  1. Alice chama getUserMedia() e adiciona o stream transmitido a ele:
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. O gerenciador onicecandidate da etapa 1 é chamado quando os candidatos à rede ficam disponíveis.
  2. Alice envia dados candidatos serializados para Bob. Em um aplicativo real, esse processo (conhecido como sinalização) ocorre por meio de um serviço de mensagens. Você aprenderá a fazer isso em uma etapa posterior. É claro que, nesta etapa, os dois objetos RTCPeerConnection estão na mesma página e podem se comunicar diretamente sem a necessidade de mensagens externas.
  3. Quando Bob recebe uma mensagem de candidato de Alice, ele chama addIceCandidate() para adicionar a candidata à descrição do colega 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}.`);
  }
}

Os pares WebRTC também precisam descobrir e trocar informações de mídia local e remota de áudio e vídeo, como resolução e recursos de codec. A sinalização para troca de informações de configuração de mídia ocorre com a troca de blobs de metadados, conhecidos como oferta e resposta, usando o formato do Protocolo de descrição da sessão, conhecido como SDP:

  1. Alice executa o método RTCPeerConnection createOffer(). A promessa retornada fornece uma RTCSessionDescription: Descrição da sessão local de Alice:
trace('localPeerConnection createOffer start.');
localPeerConnection.createOffer(offerOptions)
  .then(createdOffer).catch(setSessionDescriptionError);
  1. Se conseguir, Alice define a descrição local usando setLocalDescription() e envia a descrição da sessão para Beto pelo canal de sinalização.
  2. Bob define a descrição que Alice enviou a ele como a descrição remota usando setRemoteDescription().
  3. Bob executa o método createAnswer() RTCPeerConnection, transmitindo a descrição remota que ele recebeu de Alice para que uma sessão local seja gerada e compatível com a dela. A promessa createAnswer() transmite uma RTCSessionDescription: Bob define isso como a descrição local e a envia para Alice.
  4. Quando Alice recebe a descrição da sessão de Beto, ela a define como a descrição remota com 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!

Pontos de bônus

  1. Acesse chrome://webrtc-internals. Fornece dados de depuração e estatísticas WebRTC. Você encontra uma lista completa de URLs do Chrome em chrome://about.
  2. Personalize a página com CSS:
  • Coloque os vídeos lado a lado.
  • Deixe os botões da mesma largura e com texto maior.
  • Verifique se o layout funciona em dispositivos móveis.
  1. No console do Chrome Dev Tools, verifique localStream, localPeerConnection e remotePeerConnection.
  2. No console, consulte localPeerConnectionpc1.localDescription. Como é o formato SDP?

O que você aprendeu

Nesta etapa, você aprendeu a:

  • Abstrair diferenças do navegador com o paliativo WebRTC, adapter.js.
  • Use a API RTCPeerConnection para fazer streaming de vídeo.
  • Controle a captura e o streaming de mídia.
  • Compartilhe informações de mídia e rede entre pares para ativar uma chamada WebRTC.

Uma versão completa desta etapa está na pasta step-2.

Dicas

  • Há muito o que aprender nesta etapa. Para encontrar outros recursos que explicam o RTCPeerConnection em mais detalhes, confira webrtc.org (link em inglês). Esta página inclui sugestões para frameworks JavaScript, caso você queira usar o WebRTC, mas não queira modificar as APIs.
  • Saiba mais sobre o shim do adaptador.js no repositório do GitHub do adaptador.js.
  • Quer ver como é o melhor aplicativo de chat por vídeo do mundo? Confira o AppRTC, o app canônico do projeto WebRTC para chamadas WebRTC: app, code. O tempo de configuração da chamada é inferior a 500 ms.

Prática recomendada

  • Para preparar seu código para o futuro, use as novas APIs baseadas em promessas e ative a compatibilidade com navegadores que não têm suporte a elas usando o adapter.js.

A seguir

Esta etapa mostra como usar o WebRTC para fazer streaming de vídeo entre pares, mas este codelab também é sobre dados.

Na próxima etapa, você vai descobrir como transmitir dados arbitrários usando o RTCDataChannel.

6. Usar o RTCDataChannel para trocar dados

O que você vai aprender

  • Como trocar dados entre endpoints (peers) WebRTC.

Uma versão completa desta etapa está na pasta step-03.

Atualizar o HTML

Nesta etapa, você vai usar canais de dados WebRTC para enviar texto entre dois elementos textarea na mesma página. Isso não é muito útil, mas demonstra como o WebRTC pode ser usado para compartilhar dados e fazer streaming de vídeo.

Remova os elementos de vídeo e botão de index.html e substitua-os pelo seguinte 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>

Uma área de texto será usada para inserir texto, a outra exibirá o texto transmitido entre pares.

index.html ficará assim:

<!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>

Atualizar o JavaScript

Substitua main.js pelo conteúdo de step-03/js/main.js.

Teste o streaming de dados entre pares: abra index.html, pressione Iniciar para configurar a conexão de peering, digite um texto em textarea à esquerda e clique em Enviar para transferir o texto usando canais de dados WebRTC.

Como funciona

Esse código usa RTCPeerConnection e RTCDataChannel para permitir a troca de mensagens de texto.

Grande parte do código desta etapa é igual ao do exemplo RTCPeerConnection.

As funções sendData() e createConnection() têm a maior parte do novo código:

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);
}

A sintaxe do RTCDataChannel é deliberadamente semelhante à WebSocket, com um método send() e um evento message.

Observe o uso de dataConstraint. Os canais de dados podem ser configurados para ativar diferentes tipos de compartilhamento de dados, por exemplo, priorizando entrega confiável em vez de desempenho. Saiba mais sobre essas opções na Mozilla Developer Network.

Pontos de bônus

  1. Com o SCTP, o protocolo usado pelos canais de dados WebRTC é ativado por padrão na entrega de dados confiáveis e ordenados. Quando o RTCDataChannel pode precisar fornecer entrega confiável de dados e quando o desempenho pode ser mais importante, mesmo que isso signifique perder alguns dados?
  2. Use CSS para melhorar o layout da página e adicione um atributo de marcador de posição ao campo "dataChannelReceber" área de texto.
  3. Teste a página em um dispositivo móvel.

O que você aprendeu

Nesta etapa, você aprendeu a:

  • Estabeleça uma conexão entre dois pares WebRTC.
  • Trocar dados de texto entre os apps semelhantes.

Uma versão completa desta etapa está na pasta step-03.

Saiba mais

A seguir

Você já aprendeu a trocar dados entre pares na mesma página, mas como fazer isso entre máquinas diferentes? Primeiro, é preciso configurar um canal de sinalização para trocar mensagens de metadados. Descubra como na próxima etapa.

7. Configurar um serviço de sinalização para trocar mensagens

O que você vai aprender

Nesta etapa, você descobrirá como:

  • Use npm para instalar as dependências do projeto, conforme especificado em package.json.
  • Execute um servidor Node.js e use a estática do nó para disponibilizar arquivos estáticos.
  • Configurar um serviço de mensagens no Node.js usando o Socket.IO.
  • Use-o para criar "salas" e trocar mensagens.

Uma versão completa desta etapa está na pasta step-04.

Conceitos

Para configurar e manter uma chamada WebRTC, os clientes WebRTC (peering) precisam trocar metadados:

  • Informações do candidato (rede).
  • Mensagens de oferta e resposta com informações sobre mídia, como resolução e codecs.

Em outras palavras, é necessária uma troca de metadados antes que ocorra um streaming ponto a ponto de áudio, vídeo ou dados. Esse processo é chamado de sinalização.

Nas etapas anteriores, os objetos RTCPeerConnection do remetente e do receptor estão na mesma página. Portanto, a "sinalização" é uma questão de transmitir metadados entre objetos.

Em um aplicativo real, os RTCPeerConnections do remetente e do receptor são executados em páginas da Web em dispositivos diferentes, e você precisa que eles comuniquem metadados.

Para isso, você usa um servidor de sinalização, que pode transmitir mensagens entre clientes WebRTC (peering). As mensagens reais são texto simples: objetos JavaScript em string.

Pré-requisito: instalar o Node.js

Para executar as próximas etapas deste codelab (pastas step-04 e step-06), será necessário executar um servidor em localhost usando Node.js.

Faça o download e instale o Node.js neste link ou usando o gerenciador de pacotes de sua preferência.

Depois de instalado, você poderá importar as dependências necessárias para as próximas etapas (executando npm install), bem como executar um pequeno servidor localhost para executar o codelab (executando node index.js). Esses comandos serão indicados posteriormente, quando forem necessários.

Sobre o app

O WebRTC usa uma API JavaScript do lado do cliente, mas para uso real também requer um servidor de sinalização (mensagens), bem como os servidores STUN e TURN. Saiba mais neste link.

Nesta etapa, você vai criar um servidor de sinalização Node.js simples, usando o módulo Node.js do Socket.IO e a biblioteca JavaScript para mensagens. Ter experiência com Node.js e Socket.IO será útil, mas não crucial. os componentes das mensagens são muito simples.

Neste exemplo, o servidor (o aplicativo Node.js) está implementado em index.js, e o cliente executado nele (o app da Web) está implementado em index.html.

O aplicativo Node.js nesta etapa tem duas tarefas.

Primeiro, ele funciona como um redirecionamento de mensagens:

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

Segundo, ele gerencia "salas" de chat por vídeo 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);
}

Nosso aplicativo WebRTC simples permite que no máximo dois pares compartilhem uma sala.

HTML e JavaScript

Atualize index.html para que fique assim:

<!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>

Você não verá nada na página nesta etapa: todo o registro é feito no console do navegador. Para visualizar o console no Chrome, pressione Ctrl-Shift-J ou Command-Option-J se estiver em um Mac.

Substitua js/main.js pelo seguinte:

'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);
});

Configurar o Socket.IO para ser executado no Node.js

No arquivo HTML, você pode ter visto que está usando um arquivo Socket.IO:

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

No nível superior do diretório work, crie um arquivo chamado package.json com o seguinte conteúdo:

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

Esse é um manifesto do app que informa ao Gerenciador de pacotes do nó (npm) quais dependências do projeto vão ser instaladas.

Para instalar dependências (como /socket.io/socket.io.js), execute o seguinte no terminal de linha de comando, no diretório work:

npm install

Você verá um registro de instalação que termina com algo assim:

3ab06b7bcc7664b9.png

Como você pode ver, npm instalou as dependências definidas em package.json.

Crie um novo arquivo index.js no nível superior do diretório work (não no diretório js) e adicione o seguinte 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);
        }
      });
    }
  });

});

No terminal da linha de comando, execute o seguinte comando no diretório work:

node index.js

No navegador, abra localhost:8080.

Sempre que você abrir esse URL, precisará inserir o nome de um ambiente. Para participar da mesma sala, escolha sempre o mesmo nome, como "foo".

Abra uma página nova guia e abra localhost:8080 novamente. Escolha o mesmo nome de ambiente.

Abra localhost:8080 em uma terceira guia ou janela. Escolha o mesmo nome de ambiente novamente.

Verifique o console em cada uma das guias: você vai encontrar o registro do JavaScript acima.

Pontos de bônus

  1. Quais mecanismos alternativos de mensagem podem ser possíveis? Que problemas você pode encontrar usando "puro" WebSocket?
  2. Que problemas podem estar envolvidos no escalonamento desse aplicativo? Você consegue desenvolver um método para testar milhares ou milhões de solicitações de sala simultâneas?
  3. Este app usa um comando JavaScript para receber o nome de um ambiente. Encontre uma maneira de encontrar o nome da sala usando o URL. Por exemplo, localhost:8080/foo daria o nome da sala foo.

O que você aprendeu

Nesta etapa, você aprendeu a:

  • Use o npm para instalar as dependências do projeto, conforme especificado em package.json.
  • Executar um servidor Node.js para servidores de arquivos estáticos.
  • Configurar um serviço de mensagens no Node.js usando socket.io.
  • Use isso para criar "salas" e trocar mensagens.

Uma versão completa desta etapa está na pasta step-04.

Saiba mais

A seguir

Saiba como usar a sinalização para permitir que dois usuários estabeleçam uma conexão de peering.

8. Combinar sinalização e conexão de peering

O que você vai aprender

Nesta etapa, você descobrirá como:

  • Executar um serviço de sinalização WebRTC usando o Socket.IO em execução no Node.js
  • Use esse serviço para trocar metadados WebRTC entre pares.

Você encontra uma versão completa desta etapa na pasta step-05.

Substituir HTML e JavaScript

Substitua o conteúdo de index.html pelo seguinte:

<!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>

Substitua js/main.js pelo conteúdo de step-05/js/main.js.

Executar o servidor Node.js

Se você não estiver seguindo este codelab do seu diretório work, talvez seja necessário instalar as dependências para a pasta step-05 ou para a pasta de trabalho atual. Execute o seguinte comando no seu diretório de trabalho:

npm install

Depois de instalado, se o servidor Node.js não estiver em execução, inicie-o chamando o seguinte comando no diretório work:

node index.js

Verifique se você está usando a versão do index.js da etapa anterior que implementa o Socket.IO. Para mais informações sobre E/S de nó e soquete, consulte a seção "Configurar um serviço de sinalização para trocar mensagens".

No navegador, abra localhost:8080.

Abra localhost:8080 novamente em uma nova guia ou janela. Um elemento de vídeo mostra o stream local de getUserMedia(), e o outro mostra o "remoto" transmitido via RTCPeerconnection.

Veja os registros no console do navegador.

Pontos de bônus

  1. Este aplicativo suporta apenas bate-papo por vídeo entre duas pessoas. Como você pode mudar o design para permitir que mais de uma pessoa compartilhe a mesma sala de chat por vídeo?
  2. O exemplo tem o nome do ambiente foo codificado. Qual seria a melhor maneira de ativar outros nomes de ambiente?
  3. Como os usuários compartilhariam o nome da sala? Tente criar uma alternativa ao compartilhamento dos nomes das salas.
  4. Como você mudaria o app

O que você aprendeu

Nesta etapa, você aprendeu a:

  • Execute um serviço de sinalização WebRTC usando o Socket.IO em execução no Node.js.
  • Use esse serviço para trocar metadados WebRTC entre pares.

Você encontra uma versão completa desta etapa na pasta step-05.

Dicas

  • As estatísticas e os dados de depuração do WebRTC estão disponíveis em chrome://webrtc-internals.
  • test.webrtc.org (link em inglês) pode ser usado para verificar o ambiente local e testar a câmera e o microfone.
  • Se você tiver problemas com o armazenamento em cache, tente o seguinte:
  • Faça uma atualização forçada segurando a tecla Ctrl e clicando no botão Atualizar.
  • Reinicie o navegador.
  • Execute npm cache clean na linha de comando.

A seguir

Aprenda a tirar uma foto, acessar os dados de imagens e compartilhar com colegas remotos.

9. Tirar uma foto e compartilhá-la em um canal de dados

O que você vai aprender

Nesta etapa, você vai aprender a:

  • Tire uma foto e receba os dados dela usando o elemento canvas.
  • Trocar dados de imagem com um usuário remoto.

Uma versão completa desta etapa está na pasta step-06.

Como funciona

Anteriormente, você aprendeu a trocar mensagens de texto usando o RTCDataChannel.

Esta etapa possibilita o compartilhamento de arquivos inteiros. Neste exemplo, fotos capturadas usando o getUserMedia().

As principais partes desta etapa são:

  1. Estabelecer um canal de dados. Observe que você não adiciona streams de mídia à conexão de peering nesta etapa.
  2. Capture o stream de vídeo da webcam do usuário com 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. Quando o usuário clicar no botão Snap, extraia um snapshot (um frame de vídeo) do stream de vídeo e mostre-o em um 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. Quando o usuário clicar no botão Enviar, converta a imagem em bytes e envie-a por um canal de dados:
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. O lado de recebimento converte bytes de mensagens do canal de dados de volta em uma imagem e mostra a imagem ao usuário:
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);
}

Acessar o código

Substitua o conteúdo da pasta work pelo conteúdo da etapa-06. Seu arquivo index.html no work agora terá esta aparência**:**

<!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>

Se você não estiver seguindo este codelab do seu diretório work, talvez seja necessário instalar as dependências para a pasta step-06 ou para a pasta de trabalho atual. Basta executar o seguinte comando no seu diretório de trabalho:

npm install

Depois de instalado, se o servidor Node.js não estiver em execução, inicie-o chamando o seguinte comando no diretório work:

node index.js

Verifique se você está usando a versão do index.js que implementa o Socket.IO e não se esqueça de reiniciar o servidor Node.js se fizer alterações. Para mais informações sobre E/S de nó e soquete, consulte a seção "Configurar um serviço de sinalização para trocar mensagens".

Se necessário, clique no botão Allow para permitir que o app use a webcam.

O app criará um ID de ambiente aleatório e o adicionará ao URL. Abra o URL na barra de endereço em uma nova guia ou janela do navegador.

Clique no botão Ajustar e Enviar e olhe para a área "Recebidos" na outra guia na parte inferior da página. O app transfere fotos entre guias.

Você verá algo como:

911b40f36ba6ba8.png

Pontos de bônus

  1. Como você pode alterar o código para possibilitar o compartilhamento de qualquer tipo de arquivo?

Saiba mais

O que você aprendeu

  • Como tirar uma foto e receber os dados dela usando o elemento canvas.
  • Como trocar esses dados com um usuário remoto.

Uma versão completa desta etapa está na pasta step-06.

10. Parabéns

Você criou um app para fazer streaming de vídeo e troca de dados em tempo real.

O que você aprendeu

Neste codelab, você aprendeu a:

  • Obter vídeo de sua webcam.
  • Faça streaming de vídeo com o RTCPeerConnection.
  • Transmitir dados com o RTCDataChannel.
  • Configurar um serviço de sinalização para trocar mensagens.
  • Combinar sinalização e conexão de peering.
  • Tirar uma foto e compartilhá-la por meio de um canal de dados.

Próximas etapas

Saiba mais

  • Vários recursos para começar a usar o WebRTC estão disponíveis em webrtc.org.