WebRTC - Guia Rápido

A Web não é mais estranha à comunicação em tempo real, pois WebRTC (Web Real-Time Communication)entra em jogo. Embora tenha sido lançado em maio de 2011, ainda está em desenvolvimento e seus padrões estão mudando. Um conjunto de protocolos é padronizado pela comunicação em tempo real no grupo de trabalho de navegadores WEB emhttp://tools.ietf.org/wg/rtcweb/ do IETF (Internet Engineering Task Force)enquanto novos conjuntos de APIs são padronizados pelo Web Real-Time Communications Working Groupe emhttp://www.w3.org/2011/04/webrtc/ do W3C (World Wide Web Consortium). Com o surgimento do WebRTC, os aplicativos modernos da web podem facilmente transmitir conteúdo de áudio e vídeo para milhões de pessoas.

Esquema Básico

O WebRTC permite que você configure conexões ponto a ponto com outros navegadores da web de forma rápida e fácil. Para construir tal aplicativo do zero, você precisaria de uma variedade de estruturas e bibliotecas que lidam com problemas típicos, como perda de dados, queda de conexão e passagem de NAT. Com o WebRTC, tudo isso vem embutido no navegador pronto para uso. Esta tecnologia não precisa de plug-ins ou software de terceiros. É open-source e seu código-fonte está disponível gratuitamente emhttp://www.webrtc.org/.

A API WebRTC inclui captura de mídia, codificação e decodificação de áudio e vídeo, camada de transporte e gerenciamento de sessão.

Captura de mídia

A primeira etapa é obter acesso à câmera e ao microfone do dispositivo do usuário. Detectamos o tipo de dispositivos disponíveis, obtemos permissão do usuário para acessar esses dispositivos e gerenciar o fluxo.

Codificação e decodificação de áudio e vídeo

Não é uma tarefa fácil enviar um fluxo de dados de áudio e vídeo pela Internet. É aqui que a codificação e a decodificação são usadas. Este é o processo de dividir os quadros de vídeo e as ondas de áudio em pedaços menores e compactá-los. Este algoritmo é chamadocodec. Existe uma quantidade enorme de codecs diferentes, que são mantidos por empresas diferentes com objetivos de negócios diferentes. Existem também muitos codecs dentro do WebRTC, como H.264, iSAC, Opus e VP8. Quando dois navegadores se conectam, eles escolhem o codec compatível mais adequado entre dois usuários. Felizmente, o WebRTC faz a maior parte da codificação nos bastidores.

Camada de Transporte

A camada de transporte gerencia a ordem dos pacotes, lida com a perda de pacotes e se conecta a outros usuários. Novamente, a API WebRTC nos dá um acesso fácil a eventos que nos informam quando há problemas com a conexão.

Gestão de Sessão

O gerenciamento de sessão lida com o gerenciamento, abertura e organização de conexões. Isso é comumente chamadosignaling. Se você transferir fluxos de áudio e vídeo para o usuário, também fará sentido transferir dados colaterais. Isso é feito peloRTCDataChannel API.

Engenheiros de empresas como Google, Mozilla, Opera e outras fizeram um ótimo trabalho para trazer essa experiência em tempo real para a web.

Compatibilidade do navegador

Os padrões WebRTC são um dos que evoluem mais rapidamente na web, então isso não significa que todos os navegadores suportem todos os mesmos recursos ao mesmo tempo. Para verificar se o seu navegador suporta WebRTC ou não, você pode visitarhttp://caniuse.com/#feat=rtcpeerconnection. Ao longo de todos os tutoriais, recomendo que você use o Chrome para todos os exemplos.

Experimentando WebRTC

Vamos começar a usar WebRTC agora. Navegue no seu navegador para o site de demonstração emhttps://apprtc.appspot.com/

Clique no botão “PARTICIPAR”. Você deve ver uma notificação suspensa.

Clique no botão “Permitir” para iniciar o streaming de seu vídeo e áudio para a página da web. Você deve ver um stream de vídeo de você mesmo.

Agora abra a URL que você está atualmente em uma nova aba do navegador e clique em “PARTICIPAR”. Você deve ver dois streams de vídeo - um de seu primeiro cliente e outro do segundo.

Agora você deve entender porque o WebRTC é uma ferramenta poderosa.

Casos de Uso

A web em tempo real abre a porta para uma nova gama de aplicativos, incluindo chat baseado em texto, compartilhamento de tela e arquivo, jogos, chat de vídeo e muito mais. Além da comunicação, você pode usar WebRTC para outros fins, como -

  • marketing em tempo real
  • publicidade em tempo real
  • comunicações de back office (CRM, ERP, SCM, FFM)
  • gestão de RH
  • rede social
  • serviços de namoro
  • consultas médicas online
  • serviços financeiros
  • surveillance
  • jogos multijogador
  • transmissão ao vivo
  • e-learning

Resumo

Agora você deve ter uma compreensão clara do termo WebRTC. Você também deve ter uma idéia de quais tipos de aplicativos podem ser construídos com WebRTC, como você já experimentou em seu navegador. Para resumir, WebRTC é uma tecnologia bastante útil.

A arquitetura geral do WebRTC possui um grande nível de complexidade.

Aqui você pode encontrar três camadas diferentes -

  • API for web developers - essa camada contém todas as APIs necessárias para o desenvolvedor da Web, incluindo objetos RTCPeerConnection, RTCDataChannel e MediaStrean.

  • API para fabricantes de navegadores

  • API substituível, que os fabricantes de navegadores podem conectar.

Os componentes de transporte permitem estabelecer conexões entre vários tipos de redes, enquanto os mecanismos de voz e vídeo são estruturas responsáveis ​​por transferir fluxos de áudio e vídeo de uma placa de som e câmera para a rede. Para desenvolvedores da Web, a parte mais importante é a API WebRTC.

Se olharmos para a arquitetura WebRTC do lado cliente-servidor, podemos ver que um dos modelos mais comumente usados ​​é inspirado no Trapézio SIP (Session Initiation Protocol).

Neste modelo, ambos os dispositivos estão executando um aplicativo da web de servidores diferentes. O objeto RTCPeerConnection configura streams para que eles possam se conectar um ao outro, ponto a ponto. Essa sinalização é feita via HTTP ou WebSockets.

Mas o modelo mais comumente usado é o Triângulo -

Neste modelo, os dois dispositivos usam o mesmo aplicativo da web. Ele dá ao desenvolvedor da web mais flexibilidade ao gerenciar as conexões do usuário.

A API WebRTC

Consiste em alguns objetos javascript principais -

  • RTCPeerConnection
  • MediaStream
  • RTCDataChannel

O objeto RTCPeerConnection

Este objeto é o principal ponto de entrada para a API WebRTC. Isso nos ajuda a nos conectar a pares, inicializar conexões e anexar fluxos de mídia. Ele também gerencia uma conexão UDP com outro usuário.

A principal tarefa do objeto RTCPeerConnection é configurar e criar uma conexão de mesmo nível. Podemos facilmente conectar pontos-chave da conexão porque esse objeto dispara um conjunto de eventos quando eles aparecem. Esses eventos fornecem acesso à configuração de nossa conexão -

O RTCPeerConnection é um objeto javascript simples, que você pode simplesmente criar desta forma -

[code] 
var conn = new RTCPeerConnection(conf); 

conn.onaddstream = function(stream) { 
   // use stream here 
}; 

[/code]

O objeto RTCPeerConnection aceita um parâmetro conf , que abordaremos mais tarde nestes tutoriais. O evento onaddstream é disparado quando o usuário remoto adiciona um fluxo de vídeo ou áudio à conexão de mesmo nível.

API MediaStream

Os navegadores modernos fornecem ao desenvolvedor acesso à API getUserMedia , também conhecida como API MediaStream . Existem três pontos principais de funcionalidade -

  • Ele dá ao desenvolvedor acesso a um objeto de fluxo que representa fluxos de vídeo e áudio

  • Ele gerencia a seleção de dispositivos de entrada do usuário, caso um usuário tenha várias câmeras ou microfones em seu dispositivo

  • Ele fornece um nível de segurança perguntando ao usuário o tempo todo ele deseja buscar o fluxo

Para testar esta API, vamos criar uma página HTML simples. Ele mostrará um único elemento <video>, solicitará permissão do usuário para usar a câmera e mostrará uma transmissão ao vivo da câmera na página. Crie um arquivo index.html e adicione -

[code] 
<html>
 
   <head> 
      <meta charset = "utf-8"> 
   </head>
	
   <body> 
      <video autoplay></video> 
      <script src = "client.js"></script> 
   </body> 
	 
</html> 
[/code]

Em seguida, adicione um arquivo client.js -

[code] 
//checks if the browser supports WebRTC 

function hasUserMedia() { 
   navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia 
      || navigator.mozGetUserMedia || navigator.msGetUserMedia; 
   return !!navigator.getUserMedia; 
}
 
if (hasUserMedia()) { 
   navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia
      || navigator.mozGetUserMedia || navigator.msGetUserMedia;
		
   //get both video and audio streams from user's camera 
   navigator.getUserMedia({ video: true, audio: true }, function (stream) { 
      var video = document.querySelector('video'); 
		
      //insert stream into the video tag 
      video.src = window.URL.createObjectURL(stream); 
   }, function (err) {}); 
	
}else {
   alert("Error. WebRTC is not supported!"); 
}
[/code]

Agora abra o index.html e você deverá ver o stream de vídeo exibindo seu rosto.

Mas tome cuidado, porque o WebRTC funciona apenas no lado do servidor. Se você simplesmente abrir esta página com o navegador, ele não funcionará. Você precisa hospedar esses arquivos nos servidores Apache ou Node, ou em qual você preferir.

O objeto RTCDataChannel

Além de enviar fluxos de mídia entre pares, você também pode enviar dados adicionais usando a API DataChannel . Esta API é tão simples quanto a API MediaStream. A principal tarefa é criar um canal proveniente de um objeto RTCPeerConnection existente -

[code] 
var peerConn = new RTCPeerConnection(); 

//establishing peer connection 
//... 
//end of establishing peer connection 
var dataChannel = peerConnection.createDataChannel("myChannel", dataChannelOptions); 

// here we can start sending direct messages to another peer 
[/code]

Isso é tudo que você precisava, apenas duas linhas de código. Todo o resto é feito na camada interna do navegador. Você pode criar um canal em qualquer conexão de mesmo nível até que o objeto RTCPeerConnection seja fechado.

Resumo

Agora você deve ter um domínio firme da arquitetura WebRTC. Também cobrimos as APIs MediaStream, RTCPeerConnection e RTCDataChannel. A API WebRTC é um alvo móvel, portanto, sempre se mantenha atualizado com as especificações mais recentes.

Antes de começarmos a construir nossos aplicativos WebRTC, devemos definir nosso ambiente de codificação. Em primeiro lugar, você deve ter um editor de texto ou IDE onde possa editar HTML e Javascript. É possível que você já tenha escolhido o preferido enquanto está lendo este tutorial. Quanto a mim, estou usando o WebStorm IDE. Você pode baixar sua versão de teste emhttps://www.jetbrains.com/webstorm/. Também estou usando o Linux Mint como sistema operacional de escolha.

O outro requisito para aplicativos WebRTC comuns é ter um servidor para hospedar os arquivos HTML e Javascript. O código não funcionará apenas com um clique duplo nos arquivos porque o navegador não tem permissão para se conectar a câmeras e microfones, a menos que os arquivos estejam sendo servidos por um servidor real. Isso é feito obviamente devido a questões de segurança.

Existem muitos servidores web diferentes, mas neste tutorial, vamos usar Node.js com node-static -

  • Visita https://nodejs.org/en/ e baixe a versão mais recente do Node.js.

  • Descompacte-o no diretório / usr / local / nodejs.

  • Abra o arquivo /home/YOUR_USERNAME/.profile e adicione a seguinte linha ao final - export PATH = $ PATH: / usr / local / nodejs / bin

  • Em seguida, você pode reiniciar o computador ou executar a fonte /home/YOUR_USERNAME/.profile

  • Agora, o comando do deve estar disponível na linha de comando. O comando npm também está disponível. NMP é o gerenciador de pacotes para Node.js. Você pode aprender mais emhttps://www.npmjs.com/.

  • Abra um terminal e execute sudo npm install -g node-static . Isso instalará o servidor da web estático para Node.js.

  • Agora navegue para qualquer diretório que contenha os arquivos HTML e execute o comando estático dentro do diretório para iniciar o seu servidor web.

  • Você pode navegar para http://localhost:8080 para ver seus arquivos.

Existe outra maneira de instalar o nodejs. Basta executar sudo apt-get install nodejs na janela do terminal.

Para testar a instalação do Node.js, abra o terminal e execute o comando node . Digite alguns comandos para verificar como funciona -

Node.js executa arquivos Javascript, bem como comandos digitados no terminal. Crie um arquivo index.js com o seguinte conteúdo -

console.log(“Testing Node.js”);

Em seguida, execute o comando de índice do . Você verá o seguinte -

Ao construir nosso servidor de sinalização, usaremos uma biblioteca WebSockets para Node.js. Para instalar em run npm instale ws no terminal.

Para testar nosso servidor de sinalização, usaremos o utilitário wscat. Para instalá-lo, execute npm install -g wscat na janela do seu terminal.

S.No Protocolos e descrição
1 Protocolos WebRTC

Os aplicativos WebRTC usam UDP (User Datagram Protocol) como protocolo de transporte. A maioria das aplicações web hoje são construídas com o uso do TCP (Transmission Control Protocol)

2 Protocolo de Descrição de Sessão

O SDP é uma parte importante do WebRTC. É um protocolo que se destina a descrever as sessões de comunicação de mídia.

3 Encontrar uma rota

Para se conectar a outro usuário, você deve encontrar um caminho claro em torno de sua própria rede e da rede do outro usuário. Mas há chances de que a rede que você está usando tenha vários níveis de controle de acesso para evitar problemas de segurança.

4 Protocolo de transmissão de controle de fluxo

Com a conexão de mesmo nível, podemos enviar rapidamente dados de vídeo e áudio. O protocolo SCTP é usado hoje para enviar dados de blob além de nossa conexão de mesmo nível configurada ao usar o objeto RTCDataChannel.

Resumo

Neste capítulo, cobrimos várias das tecnologias que permitem conexões de mesmo nível, como UDP, TCP, STUN, TURN, ICE e SCTP. Agora você deve ter uma compreensão superficial de como o SDP funciona e seus casos de uso.

A API MediaStream foi projetada para acessar facilmente os fluxos de mídia de câmeras e microfones locais. O método getUserMedia () é a principal forma de acessar dispositivos de entrada locais.

A API tem alguns pontos-chave -

  • Um stream de mídia em tempo real é representado por um objeto de stream na forma de vídeo ou áudio

  • It provides a security level through user permissions asking the user before a web application can start fetching a stream

  • The selection of input devices is handled by the MediaStream API (for example, when there are two cameras or microphones connected to the device)

Each MediaStream object includes several MediaStreamTrack objects. They represent video and audio from different input devices.

Each MediaStreamTrack object may include several channels (right and left audio channels). These are the smallest parts defined by the MediaStream API.

There are two ways to output MediaStream objects. First, we can render output into a video or audio element. Secondly, we can send output to the RTCPeerConnection object, which then send it to a remote peer.

Using the MediaStream API

Let's create a simple WebRTC application. It will show a video element on the screen, ask the user permission to use the camera, and show a live video stream in the browser. Create an index.html file −

<!DOCTYPE html> 
<html lang = "en">
 
   <head> 
      <meta charset = "utf-8" /> 
   </head> 
	
   <body> 
      <video autoplay></video> 
      <script src = "client.js"></script> 
   </body>
	
</html>

Then create the client.js file and add the following;

function hasUserMedia() { 
   //check if the browser supports the WebRTC 
   return !!(navigator.getUserMedia || navigator.webkitGetUserMedia || 
      navigator.mozGetUserMedia); 
} 

if (hasUserMedia()) { 
   navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia
      || navigator.mozGetUserMedia; 
		
   //enabling video and audio channels 
   navigator.getUserMedia({ video: true, audio: true }, function (stream) { 
      var video = document.querySelector('video'); 
		
      //inserting our stream to the video tag     
      video.src = window.URL.createObjectURL(stream); 
   }, function (err) {}); 
} else { 
   alert("WebRTC is not supported"); 
}

Here we create the hasUserMedia() function which checks whether WebRTC is supported or not. Then we access the getUserMedia function where the second parameter is a callback that accept the stream coming from the user's device. Then we load our stream into the video element using window.URL.createObjectURL which creates a URL representing the object given in parameter.

Now refresh your page, click Allow, and you should see your face on the screen.

Remember to run all your scripts using the web server. We have already installed one in the WebRTC Environment Tutorial.

MediaStream API

Properties

  • MediaStream.active (read only) − Returns true if the MediaStream is active, or false otherwise.

  • MediaStream.ended (read only, deprecated) − Return true if the ended event has been fired on the object, meaning that the stream has been completely read, or false if the end of the stream has not been reached.

  • MediaStream.id (read only) − A unique identifier for the object.

  • MediaStream.label (read only, deprecated) − A unique identifier assigned by the user agent.

You can see how the above properties look in my browser −

Event Handlers

  • MediaStream.onactive − A handler for an active event that is fired when a MediaStream object becomes active.

  • MediaStream.onaddtrack − A handler for an addtrack event that is fired when a new MediaStreamTrack object is added.

  • MediaStream.onended (deprecated) − A handler for an ended event that is fired when the streaming is terminating.

  • MediaStream.oninactive − A handler for an inactive event that is fired when a MediaStream object becomes inactive.

  • MediaStream.onremovetrack − A handler for a removetrack event that is fired when a MediaStreamTrack object is removed from it.

Methods

  • MediaStream.addTrack() − Adds the MediaStreamTrack object given as argument to the MediaStream. If the track has already been added, nothing happens.

  • MediaStream.clone() − Returns a clone of the MediaStream object with a new ID.

  • MediaStream.getAudioTracks() − Returns a list of the audio MediaStreamTrack objects from the MediaStream object.

  • MediaStream.getTrackById() − Returns the track by ID. If the argument is empty or the ID is not found, it returns null. If several tracks have the same ID, it returns the first one.

  • MediaStream.getTracks() − Returns a list of all MediaStreamTrack objects from the MediaStream object.

  • MediaStream.getVideoTracks() − Returns a list of the video MediaStreamTrack objects from the MediaStream object.

  • MediaStream.removeTrack() − Removes the MediaStreamTrack object given as argument from the MediaStream. If the track has already been removed, nothing happens.

To test the above APIs change change the index.html in the following way −

<!DOCTYPE html> 
<html lang = "en">
 
   <head> 
      <meta charset = "utf-8" /> 
   </head>
	
   <body> 
      <video autoplay></video> 
      <div><button id = "btnGetAudioTracks">getAudioTracks()
         </button></div> 
      <div><button id = "btnGetTrackById">getTrackById()
         </button></div> 
      <div><button id = "btnGetTracks">getTracks()</button></div> 
      <div><button id = "btnGetVideoTracks">getVideoTracks()
         </button></div> 
      <div><button id = "btnRemoveAudioTrack">removeTrack() - audio
         </button></div> 
      <div><button id = "btnRemoveVideoTrack">removeTrack() - video
         </button></div> 
      <script src = "client.js"></script> 
   </body> 
	
</html>

We added a few buttons to try out several MediaStream APIs. Then we should add event handlers for our newly created button. Modify the client.js file this way −

var stream;
  
function hasUserMedia() { 
   //check if the browser supports the WebRTC 
   return !!(navigator.getUserMedia || navigator.webkitGetUserMedia || 
      navigator.mozGetUserMedia); 
} 
 
if (hasUserMedia()) {
   navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia
      || navigator.mozGetUserMedia; 
		
   //enabling video and audio channels 
   navigator.getUserMedia({ video: true, audio: true }, function (s) { 
      stream = s; 
      var video = document.querySelector('video'); 
		
      //inserting our stream to the video tag     
      video.src = window.URL.createObjectURL(stream); 
   }, function (err) {}); 
} else { 
   alert("WebRTC is not supported"); 
}
  
btnGetAudioTracks.addEventListener("click", function(){ 
   console.log("getAudioTracks"); 
   console.log(stream.getAudioTracks()); 
});
  
btnGetTrackById.addEventListener("click", function(){ 
   console.log("getTrackById"); 
   console.log(stream.getTrackById(stream.getAudioTracks()[0].id)); 
});
  
btnGetTracks.addEventListener("click", function(){ 
   console.log("getTracks()"); 
   console.log(stream.getTracks()); 
});
 
btnGetVideoTracks.addEventListener("click", function(){ 
   console.log("getVideoTracks()"); 
   console.log(stream.getVideoTracks()); 
});

btnRemoveAudioTrack.addEventListener("click", function(){ 
   console.log("removeAudioTrack()"); 
   stream.removeTrack(stream.getAudioTracks()[0]); 
});
  
btnRemoveVideoTrack.addEventListener("click", function(){ 
   console.log("removeVideoTrack()"); 
   stream.removeTrack(stream.getVideoTracks()[0]); 
});

Now refresh your page. Click on the getAudioTracks() button, then click on the removeTrack() - audio button. The audio track should now be removed. Then do the same for the video track.

If you click the getTracks() button you should see all MediaStreamTracks (all connected video and audio inputs). Then click on the getTrackById() to get audio MediaStreamTrack.

Summary

In this chapter, we created a simple WebRTC application using the MediaStream API. Now you should have a clear overview of the various MediaStream APIs that make WebRTC work.

The RTCPeerConnection API is the core of the peer-to-peer connection between each of the browsers. To create the RTCPeerConnection objects simply write

var pc = RTCPeerConnection(config);

where the config argument contains at least on key, iceServers. It is an array of URL objects containing information about STUN and TURN servers, used during the finding of the ICE candidates. You can find a list of available public STUN servers at code.google.com

Depending upon whether you are the caller or the callee the RTCPeerConnection object is used in a slightly different way on each side of the connection.

Here is an example of the user's flow −

  • Register the onicecandidate handler. It sends any ICE candidates to the other peer, as they are received.

  • Register the onaddstream handler. It handles the displaying of the video stream once it is received from the remote peer.

  • Register the message handler. Your signaling server should also have a handler for messages received from the other peer. If the message contains the RTCSessionDescription object, it should be added to the RTCPeerConnection object using the setRemoteDescription() method. If the message contains the RTCIceCandidate object, it should be added to the RTCPeerConnection object using the addIceCandidate() method.

  • Utilize getUserMedia() to set up your local media stream and add it to the RTCPeerConnection object using the addStream() method.

  • Start offer/answer negotiation process. This is the only step where the caller's flow is different from the callee's one. The caller starts negotiation using the createOffer() method and registers a callback that receives the RTCSessionDescription object. Then this callback should add this RTCSessionDescription object to your RTCPeerConnection object using setLocalDescription(). And finally, the caller should send this RTCSessionDescription to the remote peer using the signaling server. The callee, on the other, registers the same callback, but in the createAnswer() method. Notice that the callee flow is initiated only after the offer is received from the caller.

RTCPeerConnection API

Properties

  • RTCPeerConnection.iceConnectionState (read only) − Returns an RTCIceConnectionState enum that describes the state of the connection. An iceconnectionstatechange event is fired when this value changes. The possible values −

    • new − the ICE agent is waiting for remote candidates or gathering addresses

    • checking − the ICE agent has remote candidates, but it has not found a connection yet

    • connected − the ICE agent has found a usable connection, but is still checking more remote candidate for better connection.

    • completed − the ICE agent has found a usable connection and stopped testing remote candidates.

    • failed − the ICE agent has checked all the remote candidates but didn't find a match for at least one component.

    • disconnected − at least one component is no longer alive.

    • closed − the ICE agent is closed.

  • RTCPeerConnection.iceGatheringState (read only) − Returns a RTCIceGatheringState enum that describes the ICE gathering state for the connection −

    • new − the object was just created.

    • gathering − the ICE agent is in the process of gathering candidates

    • complete the ICE agent has completed gathering.

  • RTCPeerConnection.localDescription (read only) − Returns an RTCSessionDescription describing the local session. It can be null if it has not yet been set.

  • RTCPeerConnection.peerIdentity (read only) − Returns an RTCIdentityAssertion. It consists of an idp(domain name) and a name representing the identity of the remote peer.

  • RTCPeerConnection.remoteDescription (read only) − Return an RTCSessionDescription describing the remote session. It can be null if it has not yet been set.

  • RTCPeerConnection.signalingState (read only) − Returns an RTCSignalingState enum that describes the signaling state of the local connection. This state describes the SDP offer. A signalingstatechange event is fired when this value changes. The possible values −

    • stable − The initial state. There is no SDP offer/answer exchange in progress.

    • have-local-offer − the local side of the connection has locally applied a SDP offer.

    • have-remote-offer − the remote side of the connection has locally applied a SDP offer.

    • have-local-pranswer − a remote SDP offer has been applied, and a SDP pranswer applied locally.

    • have-remote-pranswer − a local SDP has been applied, and a SDP pranswer applied remotely.

    • closed − the connection is closed.

Event Handlers

S.No. Event Handlers & Description
1

RTCPeerConnection.onaddstream

This handler is called when the addstream event is fired. This event is sent when a MediaStream is added to this connection by the remote peer.

2

RTCPeerConnection.ondatachannel

This handler is called when the datachannel event is fired. This event is sent when a RTCDataChannel is added to this connection.

3

RTCPeerConnection.onicecandidate

This handler is called when the icecandidate event is fired. This event is sent when a RTCIceCandidate object is added to the script.

4

RTCPeerConnection.oniceconnectionstatechange

This handler is called when the iceconnectionstatechange event is fired. This event is sent when the value of iceConnectionState changes.

5

RTCPeerConnection.onidentityresult

This handler is called when the identityresult event is fired. This event is sent when an identity assertion is generated during the creating of an offer or an answer of via getIdentityAssertion().

6

RTCPeerConnection.onidpassertionerror

This handler is called when the idpassertionerror event is fired. This event is sent when the IdP (Identitry Provider) finds an error while generating an identity assertion.

7

RTCPeerConnection.onidpvalidation

This handler is called when the idpvalidationerror event is fired. This event is sent when the IdP (Identitry Provider) finds an error while validating an identity assertion.

8

RTCPeerConnection.onnegotiationneeded

This handler is called when the negotiationneeded event is fired. This event is sent by the browser to inform the negotiation will be required at some point in the future.

9

RTCPeerConnection.onpeeridentity

This handler is called when the peeridentity event is fired. This event is sent when a peer identity has been set and verified on this connection.

10

RTCPeerConnection.onremovestream

This handler is called when the signalingstatechange event is fired. This event is sent when the value of signalingState changes.

11

RTCPeerConnection.onsignalingstatechange

This handler is called when the removestream event is fired. This event is sent when a MediaStream is removed from this connection.

Methods

S.No. Methods & Description
1

RTCPeerConnection()

Returns a new RTCPeerConnection object.

2

RTCPeerConnection.createOffer()

Creates an offer(request) to find a remote peer. The two first parameters of this method are success and error callbacks. The optional third parameter are options, like enabling audio or video streams.

3

RTCPeerConnection.createAnswer()

Creates an answer to the offer received by the remote peer during the offer/answer negotiation process. The two first parameters of this method are success and error callbacks. The optional third parameter are options for the answer to be created.

4

RTCPeerConnection.setLocalDescription()

Changes the local connection description. The description defines the properties of the connection. The connection must be able to support both old and new descriptions. The method takes three parameters, RTCSessionDescription object, callback if the change of description succeeds, callback if the change of description fails.

5

RTCPeerConnection.setRemoteDescription()

Changes the remote connection description. The description defines the properties of the connection. The connection must be able to support both old and new descriptions. The method takes three parameters, RTCSessionDescription object, callback if the change of description succeeds, callback if the change of description fails.

6

RTCPeerConnection.updateIce()

Updates the ICE agent process of pinging remote candidates and gathering local candidates.

7

RTCPeerConnection.addIceCandidate()

Provides a remote candidate to the ICE agent.

8

RTCPeerConnection.getConfiguration()

Returns a RTCConfiguration object. It represents the configuration of the RTCPeerConnection object.

9

RTCPeerConnection.getLocalStreams()

Returns an array of local MediaStream connection.

10

RTCPeerConnection.getRemoteStreams()

Returns an array of remote MediaStream connection.

11

RTCPeerConnection.getStreamById()

Returns local or remote MediaStream by the given ID.

12

RTCPeerConnection.addStream()

Adds a MediaStream as a local source of video or audio.

13

RTCPeerConnection.removeStream()

Removes a MediaStream as a local source of video or audio.

14

RTCPeerConnection.close()

Closes a connection.

15

RTCPeerConnection.createDataChannel()

Creates a new RTCDataChannel.

16

RTCPeerConnection.createDTMFSender()

Creates a new RTCDTMFSender, associated to a specific MediaStreamTrack. Allows to send DTMF (Dual-tone multifrequency) phone signaling over the connection.

17

RTCPeerConnection.getStats()

Creates a new RTCStatsReport that contains statistics concerning the connection.

18

RTCPeerConnection.setIdentityProvider()

Sets the IdP. Takes three parameters − the name, the protocol used to communicate and an optional username.

19

RTCPeerConnection.getIdentityAssertion()

Gathers an identity assertion. It is not expected to deal with this method in the application. So you may call it explicitly only to anticipate the need.

Establishing a Connection

Now let's create an example application. Firstly, run the signaling server we created in the “signaling server” tutorial via “node server”.

There will be two text inputs on the page, one for a login and one for a username we want to connect to. Create an index.html file and add the following code −

<html lang = "en"> 
   <head> 
      <meta charset = "utf-8" /> 
   </head>
	
   <body> 
	
      <div> 
         <input type = "text" id = "loginInput" /> 
         <button id = "loginBtn">Login</button> 
      </div> 
	
      <div> 
         <input type = "text" id = "otherUsernameInput" />
         <button id = "connectToOtherUsernameBtn">Establish connection</button> 
      </div> 
		
      <script src = "client2.js"></script>
		
   </body>
	
</html>

You can see that we've added the text input for a login, the login button, the text input for the other peer username, and the connect-to-him button. Now create a client.js file and add the following code −

var connection = new WebSocket('ws://localhost:9090'); 
var name = ""; 
 
var loginInput = document.querySelector('#loginInput'); 
var loginBtn = document.querySelector('#loginBtn'); 
var otherUsernameInput = document.querySelector('#otherUsernameInput'); 
var connectToOtherUsernameBtn = document.querySelector('#connectToOtherUsernameBtn'); 
var connectedUser, myConnection;
  
//when a user clicks the login button 
loginBtn.addEventListener("click", function(event){ 
   name = loginInput.value; 
	
   if(name.length > 0){ 
      send({ 
         type: "login", 
         name: name 
      }); 
   } 
	
});
  
//handle messages from the server 
connection.onmessage = function (message) { 
   console.log("Got message", message.data);
   var data = JSON.parse(message.data); 
	
   switch(data.type) { 
      case "login": 
         onLogin(data.success); 
         break; 
      case "offer": 
         onOffer(data.offer, data.name); 
         break; 
      case "answer": 
         onAnswer(data.answer); 
         break; 
      case "candidate": 
         onCandidate(data.candidate); 
         break; 
      default: 
         break; 
   } 
};
  
//when a user logs in 
function onLogin(success) { 

   if (success === false) { 
      alert("oops...try a different username"); 
   } else { 
      //creating our RTCPeerConnection object 
		
      var configuration = { 
         "iceServers": [{ "url": "stun:stun.1.google.com:19302" }] 
      }; 
		
      myConnection = new webkitRTCPeerConnection(configuration); 
      console.log("RTCPeerConnection object was created"); 
      console.log(myConnection); 
  
      //setup ice handling
      //when the browser finds an ice candidate we send it to another peer 
      myConnection.onicecandidate = function (event) { 
		
         if (event.candidate) { 
            send({ 
               type: "candidate", 
               candidate: event.candidate 
            }); 
         } 
      }; 
   } 
};
  
connection.onopen = function () { 
   console.log("Connected"); 
};
  
connection.onerror = function (err) { 
   console.log("Got error", err); 
};
  
// Alias for sending messages in JSON format 
function send(message) { 

   if (connectedUser) { 
      message.name = connectedUser; 
   } 
	
   connection.send(JSON.stringify(message)); 
};

You can see that we establish a socket connection to our signaling server. When a user clicks on the login button the application sends his username to the server. If login is successful the application creates the RTCPeerConnection object and setup onicecandidate handler which sends all found icecandidates to the other peer. Now open the page and try to login. You should see the following console output −

The next step is to create an offer to the other peer. Add the following code to your client.js file −

//setup a peer connection with another user 
connectToOtherUsernameBtn.addEventListener("click", function () { 
 
   var otherUsername = otherUsernameInput.value; 
   connectedUser = otherUsername;
	
   if (otherUsername.length > 0) { 
      //make an offer 
      myConnection.createOffer(function (offer) { 
         console.log(); 
         send({ 
            type: "offer", 
            offer: offer 
         });
			
         myConnection.setLocalDescription(offer); 
      }, function (error) { 
         alert("An error has occurred."); 
      }); 
   } 
}); 
 
//when somebody wants to call us 
function onOffer(offer, name) { 
   connectedUser = name; 
   myConnection.setRemoteDescription(new RTCSessionDescription(offer)); 
	
   myConnection.createAnswer(function (answer) { 
      myConnection.setLocalDescription(answer); 
      send({ 
         type: "answer", 
         answer: answer 
      }); 
		
   }, function (error) { 
      alert("oops...error"); 
   }); 
}
  
//when another user answers to our offer 
function onAnswer(answer) { 
   myConnection.setRemoteDescription(new RTCSessionDescription(answer)); 
} 
 
//when we got ice candidate from another user 
function onCandidate(candidate) { 
   myConnection.addIceCandidate(new RTCIceCandidate(candidate)); 
}

You can see that when a user clicks the “Establish connection” button the application makes an SDP offer to the other peer. We also set onAnswer and onCandidate handlers. Reload your page, open it in two tabs, login with two users and try to establish a connection between them. You should see the following console output −

Now the peer-to-peer connection is established. In the next tutorials, we will add video and audio streams as well as text chat support.

WebRTC is not only good at transferring audio and video streams, but any arbitrary data we might have. This is where the RTCDataChannel object comes into play.

RTCDataChannel API

Properties

  • RTCDataChannel.label (read only) − Returns a string containing the data channel name.

  • RTCDataChannel.ordered (read only) − Returns true if the order of delivery of the messages is guaranteed or false if it is not guaranteed.

  • RTCDataChannel.protocol (read only) − Returns a string containing subprotocol name used for this channel.

  • RTCDataChannel.id (read only) − Returns a unique id for the channel which is set at the creation of the RTCDataChannel object.

  • RTCDataChannel.readyState (read only) − Returns the RTCDataChannelState enum representing the state of the connection. The possible values −

    • connecting − Indicates that the connection is not yet active. This is the initial state.

    • open − Indicates that the connection is running.

    • closing − Indicates that the connection is in the process of shutting down. The cached messages are in the process of being sent or received, but no newly created task is accepting.

    • closed − Indicates that the connection could not be established or has been shut down.

  • RTCDataChannel.bufferedAmount (read only) − Returns the amount of bytes that have been queued for sending. This is the amount of data that has not been sent yet via RTCDataChannel.send().

  • RTCDataChannel.bufferedAmountLowThreshold − Returns the number of bytes at which the RTCDataChannel.bufferedAmount is taken up as low. When the RTCDataChannel.bufferedAmount decreases below this threshold, the bufferedamountlow event is fired.

  • RTCDataChannel.binaryType − Returns the type of the binary data transmitted by the connection. Can be “blob” or “arraybuffer”.

  • RTCDataChannel.maxPacketLifeType (read only) − Returns an unsigned short that indicates the length in milliseconds of the window in when messaging is going in unreliable mode.

  • RTCDataChannel.maxRetransmits (read only) − Returns an unsigned short that indicates the maximum number of times a channel will retransmit data if it is not delivered.

  • RTCDataChannel.negotiated (read only) − Returns a boolean that indicates if the channel has been negotiated by the user-agent, or by the application.

  • RTCDataChannel.reliable (read only) − Returns a boolean that indicates of the connection can send messages in unreliable mode.

  • RTCDataChannel.stream (read only) − Synonym for RTCDataChannel.id

Event Handlers

  • RTCDataChannel.onopen − This event handler is called when the open event is fired. This event is sent when the data connection has been established.

  • RTCDataChannel.onmessage − This event handler is called when the message event is fired. The event is sent when a message is available on the data channel.

  • RTCDataChannel.onbufferedamountlow − This event handler is called when the bufferedamoutlow event is fired. This event is sent when RTCDataChannel.bufferedAmount decreases below the RTCDataChannel.bufferedAmountLowThreshold property.

  • RTCDataChannel.onclose − This event handler is called when the close event is fired. This event is sent when the data connection has been closed.

  • RTCDataChannel.onerror − This event handler is called when the error event is fired. This event is sent when an error has been encountered.

Métodos

  • RTCDataChannel.close() - Fecha o canal de dados.

  • RTCDataChannel.send()- Envia os dados do parâmetro pelo canal. Os dados podem ser um blob, uma string, um ArrayBuffer ou um ArrayBufferView.

Agora vamos criar um exemplo simples. Em primeiro lugar, execute o servidor de sinalização que criamos no tutorial “servidor de sinalização” via “servidor de nó”.

Haverá três entradas de texto na página, uma para um login, uma para um nome de usuário e uma para a mensagem que queremos enviar ao outro par. Crie um arquivo index.html e adicione o seguinte código -

<html lang = "en"> 
   <head> 
      <meta charset = "utf-8" /> 
   </head>
	
   <body> 
      <div> 
         <input type = "text" id = "loginInput" /> 
         <button id = "loginBtn">Login</button> 
      </div> 
		
      <div> 
         <input type = "text" id = "otherUsernameInput" /> 
         <button id = "connectToOtherUsernameBtn">Establish connection</button> 
      </div> 
		
      <div> 
         <input type = "text" id = "msgInput" /> 
         <button id = "sendMsgBtn">Send text message</button> 
      </div> 
		
      <script src = "client.js"></script>
   </body>
	
</html>

Também adicionamos três botões para login, estabelecendo uma conexão e enviando uma mensagem. Agora crie um arquivo client.js e adicione o seguinte código -

var connection = new WebSocket('ws://localhost:9090'); 
var name = "";

var loginInput = document.querySelector('#loginInput'); 
var loginBtn = document.querySelector('#loginBtn'); 

var otherUsernameInput = document.querySelector('#otherUsernameInput'); 
var connectToOtherUsernameBtn = document.querySelector('#connectToOtherUsernameBtn'); 
var msgInput = document.querySelector('#msgInput'); 
var sendMsgBtn = document.querySelector('#sendMsgBtn'); 
var connectedUser, myConnection, dataChannel;
  
//when a user clicks the login button 
loginBtn.addEventListener("click", function(event) { 
   name = loginInput.value; 
	
   if(name.length > 0) { 
      send({ 
         type: "login", 
         name: name 
      }); 
   } 
}); 
 
//handle messages from the server 
connection.onmessage = function (message) { 
   console.log("Got message", message.data); 
   var data = JSON.parse(message.data); 
	
   switch(data.type) { 
      case "login": 
         onLogin(data.success); 
         break; 
      case "offer": 
         onOffer(data.offer, data.name); 
         break; 
      case "answer":
         onAnswer(data.answer); 
         break; 
      case "candidate": 
         onCandidate(data.candidate); 
         break; 
      default: 
         break; 
   } 
}; 
 
//when a user logs in 
function onLogin(success) { 

   if (success === false) { 
      alert("oops...try a different username"); 
   } else { 
      //creating our RTCPeerConnection object 
      var configuration = { 
         "iceServers": [{ "url": "stun:stun.1.google.com:19302" }] 
      }; 
		
      myConnection = new webkitRTCPeerConnection(configuration, { 
         optional: [{RtpDataChannels: true}] 
      }); 
		
      console.log("RTCPeerConnection object was created"); 
      console.log(myConnection); 
  
      //setup ice handling 
      //when the browser finds an ice candidate we send it to another peer 
      myConnection.onicecandidate = function (event) { 
		
         if (event.candidate) { 
            send({ 
               type: "candidate", 
               candidate: event.candidate 
            });
         } 
      }; 
		
      openDataChannel();
		
   } 
};
  
connection.onopen = function () { 
   console.log("Connected"); 
}; 
 
connection.onerror = function (err) { 
   console.log("Got error", err); 
};
  
// Alias for sending messages in JSON format 
function send(message) { 
   if (connectedUser) { 
      message.name = connectedUser; 
   }
	
   connection.send(JSON.stringify(message)); 
};

Você pode ver que estabelecemos uma conexão de soquete com nosso servidor de sinalização. Quando um usuário clica no botão de login, o aplicativo envia seu nome de usuário para o servidor. Se o login for bem-sucedido, o aplicativo cria o objeto RTCPeerConnection e configura o manipulador onicecandidate, que envia todos os icecandidates encontrados para o outro par. Ele também executa a função openDataChannel (), que cria um dataChannel. Observe que ao criar o objeto RTCPeerConnection, o segundo argumento no construtor opcional: [{RtpDataChannels: true}] é obrigatório se você estiver usando o Chrome ou Opera. A próxima etapa é criar uma oferta para o outro par. Adicione o seguinte código ao seu arquivo client.js−

//setup a peer connection with another user 
connectToOtherUsernameBtn.addEventListener("click", function () {
  
   var otherUsername = otherUsernameInput.value;
   connectedUser = otherUsername;
	
   if (otherUsername.length > 0) { 
      //make an offer 
      myConnection.createOffer(function (offer) { 
         console.log(); 
         send({ 
            type: "offer", 
            offer: offer 
         }); 
			
         myConnection.setLocalDescription(offer); 
      }, function (error) { 
         alert("An error has occurred."); 
      }); 
   } 
});
  
//when somebody wants to call us 
function onOffer(offer, name) { 
   connectedUser = name; 
   myConnection.setRemoteDescription(new RTCSessionDescription(offer));
	
   myConnection.createAnswer(function (answer) { 
      myConnection.setLocalDescription(answer); 
		
      send({ 
         type: "answer", 
         answer: answer 
      }); 
		
   }, function (error) { 
      alert("oops...error"); 
   }); 
}

//when another user answers to our offer 
function onAnswer(answer) { 
   myConnection.setRemoteDescription(new RTCSessionDescription(answer)); 
}
  
//when we got ice candidate from another user 
function onCandidate(candidate) { 
   myConnection.addIceCandidate(new RTCIceCandidate(candidate)); 
}

Você pode ver que quando um usuário clica no botão “Estabelecer conexão”, o aplicativo faz uma oferta SDP ao outro par. Nós também definir onAnswer e onCandidate manipuladores. Finalmente, vamos implementar a função openDataChannel () que cria nosso dataChannel. Adicione o seguinte código ao seu arquivo client.js -

//creating data channel 
function openDataChannel() { 

   var dataChannelOptions = { 
      reliable:true 
   }; 
	
   dataChannel = myConnection.createDataChannel("myDataChannel", dataChannelOptions);
	
   dataChannel.onerror = function (error) { 
      console.log("Error:", error); 
   };
	
   dataChannel.onmessage = function (event) { 
      console.log("Got message:", event.data); 
   };  
}
  
//when a user clicks the send message button 
sendMsgBtn.addEventListener("click", function (event) { 
   console.log("send message");
   var val = msgInput.value; 
   dataChannel.send(val); 
});

Aqui, criamos o dataChannel para nossa conexão e adicionamos o manipulador de eventos para o botão “enviar mensagem”. Agora abra esta página em duas abas, faça o login com dois usuários, estabeleça uma conexão e tente enviar mensagens. Você deve vê-los na saída do console. Observe que o exemplo acima foi testado no Opera.

Agora você pode ver que RTCDataChannel é uma parte extremamente poderosa da API WebRTC. Existem muitos outros casos de uso para este objeto, como jogos ponto a ponto ou compartilhamento de arquivos baseado em torrent.

A maioria dos aplicativos WebRTC não são apenas capazes de se comunicar por meio de vídeo e áudio. Eles precisam de muitos outros recursos. Neste capítulo, vamos construir um servidor básico de sinalização.

Sinalização e Negociação

Para se conectar a outro usuário, você deve saber onde ele está localizado na web. O endereço IP do seu dispositivo permite que dispositivos habilitados para Internet enviem dados diretamente entre si. O objeto RTCPeerConnection é responsável por isso. Assim que os dispositivos sabem como se encontrar na Internet, eles começam a trocar dados sobre quais protocolos e codecs cada dispositivo suporta.

Para se comunicar com outro usuário basta trocar informações de contato e o resto será feito pelo WebRTC. O processo de conexão com o outro usuário também é conhecido como sinalização e negociação. Consiste em algumas etapas -

  • Crie uma lista de candidatos potenciais para uma conexão de par.

  • O usuário ou um aplicativo seleciona um usuário com o qual fazer uma conexão.

  • A camada de sinalização notifica outro usuário de que alguém deseja se conectar a ele. Ele pode aceitar ou recusar.

  • O primeiro usuário é notificado da aceitação da oferta.

  • O primeiro usuário inicia RTCPeerConnection com outro usuário.

  • Ambos os usuários trocam informações de software e hardware por meio do servidor de sinalização.

  • Ambos os usuários trocam informações de localização.

  • A conexão é bem-sucedida ou falha.

A especificação WebRTC não contém nenhum padrão sobre troca de informações. Portanto, lembre-se de que o texto acima é apenas um exemplo de como a sinalização pode acontecer. Você pode usar qualquer protocolo ou tecnologia que desejar.

Construindo o Servidor

O servidor que vamos construir será capaz de conectar dois usuários que não estão no mesmo computador. Vamos criar nosso próprio mecanismo de sinalização. Nosso servidor de sinalização permitirá que um usuário chame outro. Depois que um usuário chama outro, o servidor passa a oferta, a resposta e os candidatos ICE entre eles e estabelece uma conexão WebRTC.

O diagrama acima é o fluxo de mensagens entre os usuários ao usar o servidor de sinalização. Em primeiro lugar, cada usuário se registra no servidor. Em nosso caso, será um nome de usuário de string simples. Depois que os usuários se cadastram, eles podem ligar uns para os outros. O usuário 1 faz uma oferta com o identificador de usuário que deseja chamar. O outro usuário deve responder. Finalmente, os candidatos ICE são enviados entre os usuários até que eles possam fazer uma conexão.

Para criar uma conexão WebRTC, os clientes devem ser capazes de transferir mensagens sem usar uma conexão de mesmo nível WebRTC. É aqui que usaremos HTML5 WebSockets - uma conexão de soquete bidirecional entre dois endpoints - um servidor web e um navegador web. Agora vamos começar a usar a biblioteca WebSocket. Crie o arquivo server.js e insira o seguinte código -

//require our websocket library 
var WebSocketServer = require('ws').Server; 

//creating a websocket server at port 9090 
var wss = new WebSocketServer({port: 9090}); 
 
//when a user connects to our sever 
wss.on('connection', function(connection) { 
   console.log("user connected");
	
   //when server gets a message from a connected user 
   connection.on('message', function(message){ 
      console.log("Got message from a user:", message); 
   }); 
	
   connection.send("Hello from server"); 
});

A primeira linha requer a biblioteca WebSocket que já instalamos. Em seguida, criamos um servidor de soquete na porta 9090. Em seguida, ouvimos o evento de conexão . Este código será executado quando um usuário fizer uma conexão WebSocket com o servidor. Em seguida, ouvimos todas as mensagens enviadas pelo usuário. Por fim, enviamos uma resposta ao usuário conectado dizendo “Olá do servidor”.

Agora execute o servidor de nó e o servidor deve começar a escutar as conexões de soquete.

Para testar nosso servidor, usaremos o utilitário wscat que também já instalamos. Esta ferramenta ajuda a conectar-se diretamente ao servidor WebSocket e testar comandos. Execute nosso servidor em uma janela de terminal, abra outra e execute o comando wscat -c ws: // localhost: 9090 . Você deve ver o seguinte no lado do cliente -

O servidor também deve registrar o usuário conectado -

Registro do usuário

Em nosso servidor de sinalização, usaremos um nome de usuário baseado em string para cada conexão, para que saibamos para onde enviar mensagens. Vamos mudar nosso gerenciador de conexão um pouco -

connection.on('message', function(message) { 
   var data; 
	
   //accepting only JSON messages 
   try { 
      data = JSON.parse(message); 
   } catch (e) { 
      console.log("Invalid JSON"); 
      data = {}; 
   } 
	
});

Desta forma, aceitamos apenas mensagens JSON. Em seguida, precisamos armazenar todos os usuários conectados em algum lugar. Usaremos um objeto Javascript simples para isso. Mude a parte superior do nosso arquivo -

//require our websocket library 
var WebSocketServer = require('ws').Server;
 
//creating a websocket server at port 9090 
var wss = new WebSocketServer({port: 9090}); 

//all connected to the server users
var users = {};

Vamos adicionar um campo de tipo para cada mensagem vinda do cliente. Por exemplo, se um usuário deseja fazer o login, ele envia a mensagem do tipo de login . Vamos definir -

connection.on('message', function(message){
   var data; 
	
   //accepting only JSON messages 
   try { 
      data = JSON.parse(message); 
   } catch (e) { 
      console.log("Invalid JSON"); 
      data = {}; 
   }
	
   //switching type of the user message 
   switch (data.type) { 
      //when a user tries to login 
      case "login": 
         console.log("User logged:", data.name); 
			
         //if anyone is logged in with this username then refuse 
         if(users[data.name]) { 
            sendTo(connection, { 
               type: "login", 
               success: false 
            }); 
         } else { 
            //save user connection on the server 
            users[data.name] = connection; 
            connection.name = data.name; 
				
            sendTo(connection, { 
               type: "login", 
               success: true 
            });
				
         } 
			
         break;
					 
      default: 
         sendTo(connection, { 
            type: "error", 
            message: "Command no found: " + data.type 
         }); 
			
         break; 
   } 
	
});

Se o usuário enviar uma mensagem com o tipo de login , nós -

  • Verifique se alguém já fez login com este nome de usuário

  • Se sim, diga ao usuário que ele não fez o login com sucesso

  • Se ninguém estiver usando este nome de usuário, adicionamos nome de usuário como uma chave para o objeto de conexão.

  • Se um comando não for reconhecido, enviamos um erro.

O código a seguir é uma função auxiliar para enviar mensagens a uma conexão. Adicione-o ao arquivo server.js -

function sendTo(connection, message) { 
   connection.send(JSON.stringify(message)); 
}

A função acima garante que todas as nossas mensagens sejam enviadas no formato JSON.

Quando o usuário desconecta, devemos limpar sua conexão. Podemos excluir o usuário quando o evento close for disparado. Adicione o seguinte código ao gerenciador de conexão -

connection.on("close", function() { 
   if(connection.name) { 
      delete users[connection.name]; 
    } 
});

Agora vamos testar nosso servidor com o comando login. Lembre-se de que todas as mensagens devem ser codificadas no formato JSON. Execute nosso servidor e tente fazer o login. Você deveria ver algo assim -

Fazer uma chamada

Após o login bem-sucedido, o usuário deseja ligar para outro. Ele deve fazer uma oferta a outro usuário para alcançá-lo. Adicione o gerenciador de oferta -

case "offer": 
   //for ex. UserA wants to call UserB 
   console.log("Sending offer to: ", data.name); 
	
   //if UserB exists then send him offer details 
   var conn = users[data.name]; 
	
   if(conn != null){ 
      //setting that UserA connected with UserB 
      connection.otherName = data.name; 
		
      sendTo(conn, { 
         type: "offer", 
         offer: data.offer, 
         name: connection.name 
      }); 
   }
	
   break;

Em primeiro lugar, obtemos a conexão do usuário que estamos tentando chamar. Se existir, enviamos-lhe os detalhes da oferta . Também adicionamos outroNome ao objeto de conexão . Isso é feito pela simplicidade de encontrá-lo mais tarde.

Respondendo

Responder à resposta tem um padrão semelhante ao que usamos no manipulador de ofertas . Nosso servidor apenas passa por todas as mensagens como resposta a outro usuário. Adicione o seguinte código após o manipulador de oferta -

case "answer": 
   console.log("Sending answer to: ", data.name); 
	
   //for ex. UserB answers UserA 
   var conn = users[data.name]; 
	
   if(conn != null) { 
      connection.otherName = data.name; 
      sendTo(conn, { 
         type: "answer", 
         answer: data.answer 
      }); 
   }
	
   break;

Você pode ver como isso é semelhante ao manipulador de ofertas . Observe que este código segue as funções createOffer e createAnswer no objeto RTCPeerConnection .

Agora podemos testar nosso mecanismo de oferta / resposta. Conecte dois clientes ao mesmo tempo e tente fazer uma oferta e uma resposta. Você deve ver o seguinte -

Neste exemplo, offer e answer são strings simples, mas em um aplicativo real serão preenchidas com os dados SDP.

Candidatos ICE

A parte final é lidar com o candidato ICE entre os usuários. Usamos a mesma técnica apenas para passar mensagens entre os usuários. A principal diferença é que as mensagens candidatas podem ocorrer várias vezes por usuário em qualquer ordem. Adicione o manipulador de candidato -

case "candidate": 
   console.log("Sending candidate to:",data.name); 
   var conn = users[data.name]; 
	
   if(conn != null) {
      sendTo(conn, { 
         type: "candidate", 
         candidate: data.candidate 
      }); 
   }
	
   break;

Deve funcionar de forma semelhante aos manipuladores de ofertas e respostas .

Saindo da conexão

Para permitir que nossos usuários se desconectem de outro usuário, devemos implementar a função de desligar. Também dirá ao servidor para excluir todas as referências do usuário. Adicione oleave manipulador -

case "leave": 
   console.log("Disconnecting from", data.name); 
   var conn = users[data.name]; 
   conn.otherName = null; 
	
   //notify the other user so he can disconnect his peer connection 
   if(conn != null) { 
      sendTo(conn, { 
         type: "leave" 
      }); 
   } 
	
   break;

Isso também enviará ao outro usuário o evento leave para que ele possa desconectar sua conexão de par de acordo. Também devemos lidar com o caso em que um usuário interrompe sua conexão do servidor de sinalização. Vamos modificar nosso manipulador de fechamento -

connection.on("close", function() { 

   if(connection.name) { 
      delete users[connection.name]; 
		
      if(connection.otherName) { 
         console.log("Disconnecting from ", connection.otherName); 
         var conn = users[connection.otherName]; 
         conn.otherName = null;
			
         if(conn != null) { 
            sendTo(conn, { 
               type: "leave" 
            }); 
         }  
      } 
   } 
});

Agora, se a conexão for encerrada, nossos usuários serão desconectados. O evento de fechamento será disparado quando um usuário fechar a janela do navegador enquanto ainda estamos em oferta , resposta ou estado de candidato .

Servidor de sinalização completo

Aqui está todo o código do nosso servidor de sinalização -

//require our websocket library 
var WebSocketServer = require('ws').Server;
 
//creating a websocket server at port 9090 
var wss = new WebSocketServer({port: 9090}); 

//all connected to the server users 
var users = {};
  
//when a user connects to our sever 
wss.on('connection', function(connection) {
  
   console.log("User connected");
	
   //when server gets a message from a connected user
   connection.on('message', function(message) { 
	
      var data; 
      //accepting only JSON messages 
      try {
         data = JSON.parse(message); 
      } catch (e) { 
         console.log("Invalid JSON"); 
         data = {}; 
      } 
		
      //switching type of the user message 
      switch (data.type) { 
         //when a user tries to login 
			
         case "login": 
            console.log("User logged", data.name); 
				
            //if anyone is logged in with this username then refuse 
            if(users[data.name]) { 
               sendTo(connection, { 
                  type: "login", 
                  success: false 
               }); 
            } else { 
               //save user connection on the server 
               users[data.name] = connection; 
               connection.name = data.name; 
               sendTo(connection, { 
                  type: "login", 
                  success: true 
               }); 
            } 
				
            break; 
				
         case "offer": 
            //for ex. UserA wants to call UserB 
            console.log("Sending offer to: ", data.name); 
				
            //if UserB exists then send him offer details 
            var conn = users[data.name];
				
            if(conn != null) { 
               //setting that UserA connected with UserB 
               connection.otherName = data.name; 
					
               sendTo(conn, { 
                  type: "offer", 
                  offer: data.offer, 
                  name: connection.name 
               }); 
            } 
				
            break;  
				
         case "answer": 
            console.log("Sending answer to: ", data.name); 
            //for ex. UserB answers UserA 
            var conn = users[data.name]; 
				
            if(conn != null) { 
               connection.otherName = data.name; 
               sendTo(conn, { 
                  type: "answer", 
                  answer: data.answer 
               }); 
            } 
				
            break;  
				
         case "candidate": 
            console.log("Sending candidate to:",data.name); 
            var conn = users[data.name];  
				
            if(conn != null) { 
               sendTo(conn, { 
                  type: "candidate", 
                  candidate: data.candidate 
               });
            } 
				
            break;  
				
         case "leave": 
            console.log("Disconnecting from", data.name); 
            var conn = users[data.name]; 
            conn.otherName = null; 
				
            //notify the other user so he can disconnect his peer connection 
            if(conn != null) { 
               sendTo(conn, { 
                  type: "leave" 
               }); 
            }  
				
            break;  
				
         default: 
            sendTo(connection, { 
               type: "error", 
               message: "Command not found: " + data.type 
            }); 
				
            break; 
      }  
   });  
	
   //when user exits, for example closes a browser window 
   //this may help if we are still in "offer","answer" or "candidate" state 
   connection.on("close", function() { 
	
      if(connection.name) { 
      delete users[connection.name]; 
		
         if(connection.otherName) { 
            console.log("Disconnecting from ", connection.otherName);
            var conn = users[connection.otherName]; 
            conn.otherName = null;  
				
            if(conn != null) { 
               sendTo(conn, { 
                  type: "leave" 
               });
            }  
         } 
      } 
   });  
	
   connection.send("Hello world"); 
	
});  

function sendTo(connection, message) { 
   connection.send(JSON.stringify(message)); 
}

Então o trabalho está feito e nosso servidor de sinalização está pronto. Lembre-se de que fazer coisas fora de ordem ao fazer uma conexão WebRTC pode causar problemas.

Resumo

Neste capítulo, construímos um servidor de sinalização simples e direto. Percorremos o processo de sinalização, registro do usuário e mecanismo de oferta / resposta. Também implementamos o envio de candidatos entre usuários.

A Web está se movendo muito rápido e está sempre melhorando. Novos padrões são criados todos os dias. Os navegadores permitem que as atualizações sejam instaladas sem que o usuário saiba, portanto, você deve acompanhar o que está acontecendo no mundo da Web e do WebRTC. Aqui está uma visão geral do que isso significa hoje.

Suporte de navegador

Cada navegador não tem todos os mesmos recursos WebRTC ao mesmo tempo. Navegadores diferentes podem estar à frente da curva, o que faz com que alguns recursos do WebRTC funcionem em um navegador e não em outro. O suporte atual para WebRTC no navegador é mostrado na imagem a seguir.

Você pode verificar um status de suporte WebRTC atualizado em http://caniuse.com/#feat=rtcpeerconnection.

Chrome, Firefox e Opera

As versões mais recentes do Chrome, Firefox e Opera nos principais sistemas operacionais de PC, como Mac OS X, Windows e Linux, oferecem suporte para WebRTC pronto para uso. E o mais importante, os engenheiros das equipes de desenvolvedores do Chrome e do Firefox têm trabalhado juntos para corrigir problemas para que esses dois navegadores possam se comunicar facilmente.

Sistema operacional Android

Em sistemas operacionais Android, os aplicativos WebRTC para Chrome e Firefox devem funcionar imediatamente. Eles são capazes de trabalhar com outros navegadores após a versão Android Ice Cream Sandwich (4.0). Isso ocorre devido ao compartilhamento de código entre as versões desktop e móvel.

maçã

A Apple ainda não fez nenhum anúncio sobre seus planos de oferecer suporte a WebRTC no Safari no OS X. Uma das possíveis soluções alternativas para aplicativos iOS nativos híbridos é incorporar o código WebRTC diretamente no aplicativo e carregar esse aplicativo em um WebView.

Internet Explorer

A Microsoft não oferece suporte a WebRTC em desktops. Mas eles confirmaram oficialmente que irão implementar ORTC (Object Realtime Communications) em futuras versões do IE (Edge). Eles não planejam oferecer suporte ao WebRTC 1.0. Eles rotularam seu ORTC como WebRTC 1.1, embora seja apenas um aprimoramento da comunidade e não o padrão oficial. Recentemente, eles adicionaram o suporte ORTC à versão mais recente do Microsoft Edge. Você pode aprender mais emhttps://blogs.windows.com/msedgedev/2015/09/18/ortc-api-is-now-available-in-microsoftedge/.

Resumo

Observe que WebRTC é uma coleção de APIs e protocolos, não uma única API. O suporte para cada um deles está se desenvolvendo em navegadores e sistemas operacionais diferentes em um nível diferente. Uma ótima maneira de verificar o nível de suporte mais recente é por meiohttp://canisue.com.Ele rastreia a adoção de APIs modernas em vários navegadores. Você também pode encontrar as informações mais recentes sobre o suporte do navegador, bem como demonstrações WebRTC emhttp://www.webrtc.org, que é compatível com Mozilla, Google e Opera.

No mundo móvel, o suporte WebRTC não está no mesmo nível que em desktops. Os dispositivos móveis têm seus próprios caminhos, então WebRTC também é algo diferente nas plataformas móveis.

Ao desenvolver um aplicativo WebRTC para desktop, consideramos o uso do Chrome, Firefox ou Opera. Todos eles suportam WebRTC pronto para uso. Em geral, você só precisa de um navegador e não se preocupa com o hardware do desktop.

No mundo móvel, existem três modos possíveis para WebRTC hoje -

  • O aplicativo nativo
  • O aplicativo do navegador
  • O navegador nativo

Android

Em 2013, o navegador Firefox para Android foi apresentado com suporte WebRTC fora da caixa. Agora você pode fazer chamadas de vídeo em dispositivos Android usando o navegador Firefox para celular.

Possui três componentes principais WebRTC -

  • PeerConnection - permite chamadas entre navegadores

  • getUserMedia - fornece acesso à câmera e ao microfone

  • DataChannels - fornece transferência de dados ponto a ponto

O Google Chrome para Android também oferece suporte WebRTC. Como você já percebeu, os recursos mais interessantes geralmente aparecem pela primeira vez no Chrome.

No ano passado, o navegador móvel Opera apareceu com suporte WebRTC. Portanto, para Android, você tem Chrome, Firefox e Opera. Outros navegadores não suportam WebRTC.

iOS

Infelizmente, WebRTC não é compatível com iOS agora. Embora WebRTC funcione bem no Mac ao usar Firefox, Opera ou Chrome, não é compatível com iOS.

Hoje em dia, seu aplicativo WebRTC não funciona em dispositivos móveis da Apple fora da caixa. Mas existe um navegador - Bowser. É um navegador da Web desenvolvido pela Ericsson e oferece suporte ao WebRTC pronto para uso. Você pode verificar sua página inicial emhttp://www.openwebrtc.org/bowser/.

Hoje, é a única maneira amigável de oferecer suporte ao seu aplicativo WebRTC no iOS. Outra maneira é desenvolver você mesmo um aplicativo nativo.

Telefones Windows

A Microsoft não oferece suporte a WebRTC em plataformas móveis. Mas eles confirmaram oficialmente que irão implementar ORTC (Object Realtime Communications) em versões futuras do IE. Eles não planejam oferecer suporte ao WebRTC 1.0. Eles rotularam seu ORTC como WebRTC 1.1, embora seja apenas um aprimoramento da comunidade e não o padrão oficial.

Portanto, hoje os usuários do Windows Phone não podem usar aplicativos WebRTC e não há como superar essa situação.

Amora

Os aplicativos WebRTC também não são suportados no Blackberry, de forma alguma.

Usando um navegador nativo WebRTC

O caso mais conveniente e confortável para usuários utilizarem WebRTC é usar o navegador nativo do dispositivo. Neste caso, o dispositivo está pronto para realizar quaisquer configurações adicionais.

Hoje, apenas dispositivos Android com a versão 4 ou superior fornecem esse recurso. A Apple ainda não mostra nenhuma atividade com suporte WebRTC. Portanto, os usuários do Safari não podem usar aplicativos WebRTC. A Microsoft também não o introduziu no Windows Phone 8.

Usando WebRTC por meio de aplicativos de navegador

Isso significa usar aplicativos de terceiros (navegadores da web não nativos) para fornecer os recursos WebRTC. Por enquanto, existem dois desses aplicativos de terceiros. Bowser, que é a única maneira de trazer recursos WebRTC para o dispositivo iOS e Opera, que é uma boa alternativa para a plataforma Android. O restante dos navegadores móveis disponíveis não oferece suporte a WebRTC.

Aplicativos móveis nativos

Como você pode ver, o WebRTC ainda não tem um grande suporte no mundo móvel. Portanto, uma das soluções possíveis é desenvolver aplicativos nativos que utilizem a API WebRTC. Mas não é a melhor escolha porque o principal recurso WebRTC é uma solução de plataforma cruzada. De qualquer forma, em alguns casos, esta é a única maneira porque um aplicativo nativo pode utilizar funções ou recursos específicos do dispositivo que não são suportados por navegadores HTML5.

Restringindo o fluxo de vídeo para dispositivos móveis e desktop

O primeiro parâmetro da API getUserMedia espera um objeto de chaves e valores informando ao navegador como processar fluxos. Você pode verificar o conjunto completo de restrições emhttps://tools.ietf.org/html/draft-alvestrand-constraints-resolution-03. Você pode configurar a proporção de vídeo, frameRate e outros parâmetros opcionais.

O suporte a dispositivos móveis é uma das maiores dores, porque os dispositivos móveis têm espaço de tela limitado junto com recursos limitados. Você pode querer que o dispositivo móvel capture apenas uma resolução de 480x320 ou um fluxo de vídeo menor para economizar energia e largura de banda. Usar a string do agente do usuário no navegador é uma boa maneira de testar se o usuário está em um dispositivo móvel ou não. Vamos ver um exemplo. Crie o arquivo index.html -

<!DOCTYPE html> 
<html lang = "en">
 
   <head> 
      <meta charset = "utf-8" /> 
   </head> 
	
   <body> 
      <video autoplay></video> 
      <script src = "client.js"></script> 
   </body>
	
</html>

Em seguida, crie o seguinte arquivo client.js -

//constraints for desktop browser 
var desktopConstraints = { 

   video: { 
      mandatory: { 
         maxWidth:800,
         maxHeight:600   
      }  
   }, 
	
   audio: true 
}; 
 
//constraints for mobile browser 
var mobileConstraints = { 

   video: { 
      mandatory: { 
         maxWidth: 480, 
         maxHeight: 320, 
      } 
   }, 
	
   audio: true 
}
  
//if a user is using a mobile browser 
if(/Android|iPhone|iPad/i.test(navigator.userAgent)) { 
   var constraints = mobileConstraints;   
} else { 
   var constraints = desktopConstraints; 
}
  
function hasUserMedia() { 
   //check if the browser supports the WebRTC 
   return !!(navigator.getUserMedia || navigator.webkitGetUserMedia || 
      navigator.mozGetUserMedia); 
}
  
if (hasUserMedia()) {
  
   navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || 
      navigator.mozGetUserMedia;
	
   //enabling video and audio channels 
   navigator.getUserMedia(constraints, function (stream) { 
      var video = document.querySelector('video');
		
      //inserting our stream to the video tag     
      video.src = window.URL.createObjectURL(stream);
		
   }, function (err) {}); 
} else { 
   alert("WebRTC is not supported"); 
}

Execute o servidor da web usando o comando estático e abra a página. Você verá que é 800x600. Em seguida, abra esta página em uma janela de visualização móvel usando ferramentas do Chrome e verifique a resolução. Deve ser 480x320.

As restrições são a maneira mais fácil de aumentar o desempenho de seu aplicativo WebRTC.

Resumo

Neste capítulo, aprendemos sobre os problemas que podem ocorrer durante o desenvolvimento de aplicativos WebRTC para dispositivos móveis. Descobrimos diferentes limitações de suporte à API WebRTC em plataformas móveis. Também lançamos um aplicativo de demonstração em que definimos diferentes restrições para navegadores de desktop e móveis.

Neste capítulo, vamos construir um aplicativo cliente que permite que dois usuários em dispositivos separados se comuniquem usando WebRTC. Nosso aplicativo terá duas páginas. Um para login e outro para chamar outro usuário.

As duas páginas serão as tags div . A maioria das entradas é feita por meio de manipuladores de eventos simples.

Servidor de Sinalização

Para criar uma conexão WebRTC, os clientes devem ser capazes de transferir mensagens sem usar uma conexão de mesmo nível WebRTC. É aqui que usaremos HTML5 WebSockets - uma conexão de soquete bidirecional entre dois endpoints - um servidor web e um navegador web. Agora vamos começar a usar a biblioteca WebSocket. Crie o arquivo server.js e insira o seguinte código -

//require our websocket library 
var WebSocketServer = require('ws').Server; 

//creating a websocket server at port 9090 
var wss = new WebSocketServer({port: 9090});
  
//when a user connects to our sever 
wss.on('connection', function(connection) { 
   console.log("user connected"); 
	
   //when server gets a message from a connected user 
   connection.on('message', function(message) { 
      console.log("Got message from a user:", message); 
   }); 
	
   connection.send("Hello from server");
});

A primeira linha requer a biblioteca WebSocket que já instalamos. Em seguida, criamos um servidor de soquete na porta 9090. Em seguida, ouvimos o evento de conexão . Este código será executado quando um usuário fizer uma conexão WebSocket com o servidor. Em seguida, ouvimos todas as mensagens enviadas pelo usuário. Por fim, enviamos uma resposta ao usuário conectado dizendo “Olá do servidor”.

Em nosso servidor de sinalização, usaremos um nome de usuário baseado em string para cada conexão, para que saibamos para onde enviar mensagens. Vamos mudar nosso gerenciador de conexão um pouco -

connection.on('message', function(message) { 
   var data; 
	
   //accepting only JSON messages 
   try { 
      data = JSON.parse(message); 
   } catch (e) { 
      console.log("Invalid JSON"); 
      data = {}; 
   } 
});

Desta forma, aceitamos apenas mensagens JSON. Em seguida, precisamos armazenar todos os usuários conectados em algum lugar. Usaremos um objeto Javascript simples para isso. Mude a parte superior do nosso arquivo -

//require our websocket library 
var WebSocketServer = require('ws').Server; 

//creating a websocket server at port 9090 
var wss = new WebSocketServer({port: 9090}); 

//all connected to the server users 
var users = {};

Vamos adicionar um campo de tipo para cada mensagem vinda do cliente. Por exemplo, se um usuário deseja fazer o login, ele envia a mensagem do tipo de login . Vamos definir -

connection.on('message', function(message) { 
   var data; 
	
   //accepting only JSON messages 
   try { 
      data = JSON.parse(message);
   } catch (e) { 
      console.log("Invalid JSON"); 
      data = {}; 
   } 
	
   //switching type of the user message 
   switch (data.type) { 
      //when a user tries to login 
      case "login": 
         console.log("User logged:", data.name); 
			
         //if anyone is logged in with this username then refuse 
         if(users[data.name]) { 
            sendTo(connection, { 
               type: "login", 
               success: false 
            }); 
         } else { 
            //save user connection on the server 
            users[data.name] = connection; 
            connection.name = data.name; 
				
            sendTo(connection, { 
               type: "login", 
               success: true 
            }); 
         } 
			
         break;
			
      default: 
         sendTo(connection, { 
            type: "error", 
            message: "Command no found: " + data.type 
         }); 
			
         break;
   }  
});

Se o usuário enviar uma mensagem com o tipo de login , nós -

  • Verifique se alguém já fez login com este nome de usuário

  • Se sim, diga ao usuário que ele não fez o login com sucesso

  • Se ninguém estiver usando este nome de usuário, adicionamos nome de usuário como uma chave para o objeto de conexão.

  • Se um comando não for reconhecido, enviamos um erro.

O código a seguir é uma função auxiliar para enviar mensagens a uma conexão. Adicione-o ao arquivo server.js -

function sendTo(connection, message) { 
   connection.send(JSON.stringify(message)); 
}

Quando o usuário desconecta, devemos limpar sua conexão. Podemos excluir o usuário quando o evento close for disparado. Adicione o seguinte código ao gerenciador de conexão -

connection.on("close", function() { 
   if(connection.name) { 
      delete users[connection.name]; 
   } 
});

Após o login bem-sucedido, o usuário deseja ligar para outro. Ele deve fazer uma oferta a outro usuário para alcançá-lo. Adicione o gerenciador de oferta -

case "offer": 
   //for ex. UserA wants to call UserB 
   console.log("Sending offer to: ", data.name);
	
   //if UserB exists then send him offer details 
   var conn = users[data.name]; 
	
   if(conn != null) { 
      //setting that UserA connected with UserB 
      connection.otherName = data.name; 
		
      sendTo(conn, { 
         type: "offer",
         offer: data.offer, 
         name: connection.name 
      }); 
		
   }  
	
   break;

Em primeiro lugar, obtemos a conexão do usuário que estamos tentando chamar. Se existir, enviamos-lhe os detalhes da oferta . Também adicionamos outroNome ao objeto de conexão . Isso é feito pela simplicidade de encontrá-lo mais tarde.

Responder à resposta tem um padrão semelhante ao que usamos no manipulador de ofertas . Nosso servidor apenas passa por todas as mensagens como resposta a outro usuário. Adicione o seguinte código após o manipulador de oferta -

case "answer": 
   console.log("Sending answer to: ", data.name); 
	
   //for ex. UserB answers UserA 
   var conn = users[data.name]; 
	
   if(conn != null) { 
      connection.otherName = data.name; 
		
      sendTo(conn, { 
         type: "answer", 
         answer: data.answer 
      }); 
   } 
	
   break;

A parte final é lidar com o candidato ICE entre os usuários. Usamos a mesma técnica apenas para passar mensagens entre os usuários. A principal diferença é que as mensagens candidatas podem ocorrer várias vezes por usuário em qualquer ordem. Adicione o manipulador de candidato -

case "candidate": 
   console.log("Sending candidate to:",data.name); 
   var conn = users[data.name];
	
   if(conn != null) { 
      sendTo(conn, { 
         type: "candidate", 
         candidate: data.candidate 
      }); 
   } 
	
   break;

Para permitir que nossos usuários se desconectem de outro usuário, devemos implementar a função de desligar. Também dirá ao servidor para excluir todas as referências do usuário. Adicione o manipulador de licença -

case "leave": 
   console.log("Disconnecting from", data.name); 
   var conn = users[data.name]; 
   conn.otherName = null; 
	
   //notify the other user so he can disconnect his peer connection 
   if(conn != null) { 
      sendTo(conn, { 
         type: "leave" 
      }); 
   } 
	
   break;

Isso também enviará ao outro usuário o evento leave para que ele possa desconectar sua conexão de par de acordo. Também devemos lidar com o caso em que um usuário interrompe sua conexão do servidor de sinalização. Vamos modificar nosso manipulador de fechamento -

connection.on("close", function() { 

   if(connection.name) { 
      delete users[connection.name]; 
		
      if(connection.otherName) { 
         console.log("Disconnecting from ", connection.otherName); 
         var conn = users[connection.otherName]; 
         conn.otherName = null;  
			
         if(conn != null) { 
            sendTo(conn, { 
               type: "leave" 
            });
         }
			
      } 
   } 
});

A seguir está o código completo do nosso servidor de sinalização -

//require our websocket library 
var WebSocketServer = require('ws').Server; 

//creating a websocket server at port 9090 
var wss = new WebSocketServer({port: 9090}); 

//all connected to the server users 
var users = {};
  
//when a user connects to our sever 
wss.on('connection', function(connection) {
  
   console.log("User connected");
	
   //when server gets a message from a connected user 
   connection.on('message', function(message) { 
	
      var data; 
		
      //accepting only JSON messages 
      try { 
         data = JSON.parse(message); 
      } catch (e) { 
         console.log("Invalid JSON"); 
         data = {}; 
      }
		
      //switching type of the user message 
      switch (data.type) { 
         //when a user tries to login
         case "login": 
            console.log("User logged", data.name); 
				
            //if anyone is logged in with this username then refuse 
            if(users[data.name]) { 
               sendTo(connection, { 
                  type: "login", 
                  success: false 
               }); 
            } else { 
               //save user connection on the server 
               users[data.name] = connection; 
               connection.name = data.name; 
					
               sendTo(connection, { 
                  type: "login", 
                  success: true 
               }); 
            } 
				
            break;
				
         case "offer": 
            //for ex. UserA wants to call UserB 
            console.log("Sending offer to: ", data.name);
				
            //if UserB exists then send him offer details 
            var conn = users[data.name]; 
				
            if(conn != null) { 
               //setting that UserA connected with UserB 
               connection.otherName = data.name; 
					
               sendTo(conn, { 
                  type: "offer", 
                  offer: data.offer, 
                  name: connection.name 
               }); 
            }
				
            break;
				
         case "answer": 
            console.log("Sending answer to: ", data.name); 
            //for ex. UserB answers UserA 
            var conn = users[data.name]; 
				
            if(conn != null) { 
               connection.otherName = data.name; 
               sendTo(conn, { 
                  type: "answer", 
                  answer: data.answer 
               }); 
            } 
				
            break; 
				
         case "candidate": 
            console.log("Sending candidate to:",data.name); 
            var conn = users[data.name];
				
            if(conn != null) { 
               sendTo(conn, { 
                  type: "candidate", 
                  candidate: data.candidate 
               }); 
            } 
				
            break;
				
         case "leave": 
            console.log("Disconnecting from", data.name); 
            var conn = users[data.name]; 
            conn.otherName = null; 
				
            //notify the other user so he can disconnect his peer connection 
            if(conn != null) {
               sendTo(conn, { 
                  type: "leave" 
              }); 
            }
				
            break;
				
         default: 
            sendTo(connection, { 
               type: "error", 
               message: "Command not found: " + data.type 
            }); 
				
            break; 
      }
		
   }); 
	
   //when user exits, for example closes a browser window 
   //this may help if we are still in "offer","answer" or "candidate" state 
   connection.on("close", function() { 
	
      if(connection.name) { 
         delete users[connection.name]; 
			
         if(connection.otherName) { 
            console.log("Disconnecting from ", connection.otherName); 
            var conn = users[connection.otherName]; 
            conn.otherName = null;
				
            if(conn != null) { 
               sendTo(conn, { 
                  type: "leave" 
               }); 
            }
         } 
      }
		
   });  
	
   connection.send("Hello world");  
});
  
function sendTo(connection, message) { 
   connection.send(JSON.stringify(message)); 
}

Aplicativo do cliente

Uma maneira de testar este aplicativo é abrir duas guias do navegador e tentar chamar um ao outro.

Em primeiro lugar, precisamos instalar a biblioteca bootstrap . Bootstrap é uma estrutura de front-end para o desenvolvimento de aplicativos da web. Você pode aprender mais emhttp://getbootstrap.com/.Crie uma pasta chamada, por exemplo, “videochat”. Esta será a nossa pasta de aplicativo raiz. Dentro desta pasta, crie um arquivo package.json (é necessário para gerenciar dependências npm) e adicione o seguinte -

{ 
   "name": "webrtc-videochat", 
   "version": "0.1.0", 
   "description": "webrtc-videochat", 
   "author": "Author", 
   "license": "BSD-2-Clause" 
}

Em seguida, execute npm install bootstrap . Isso instalará a biblioteca de bootstrap na pasta videochat / node_modules .

Agora precisamos criar uma página HTML básica. Crie um arquivo index.html na pasta raiz com o seguinte código -

<html> 
 
   <head> 
      <title>WebRTC Video Demo</title>
      <link rel = "stylesheet" href = "node_modules/bootstrap/dist/css/bootstrap.min.css"/>
   </head>
	
   <style>
	
      body { 
         background: #eee; 
         padding: 5% 0; 
      } 
		
      video { 
         background: black; 
         border: 1px solid gray; 
      }
		
      .call-page { 
         position: relative; 
         display: block; 
         margin: 0 auto; 
         width: 500px; 
         height: 500px; 
      } 
		
      #localVideo { 
         width: 150px; 
         height: 150px; 
         position: absolute; 
         top: 15px; 
         right: 15px; 
      }
		
      #remoteVideo { 
         width: 500px; 
         height: 500px; 
      }
		
   </style>
	
   <body>
	
   <div id = "loginPage" class = "container text-center"> 
	
      <div class = "row"> 
         <div class = "col-md-4 col-md-offset-4">
			
            <h2>WebRTC Video Demo. Please sign in</h2> 
            <label for = "usernameInput" class = "sr-only">Login</label> 
            <input type = "email" id = "usernameInput" c
               lass = "form-control formgroup" placeholder = "Login" 
               required = "" autofocus = ""> 
            <button id = "loginBtn" class = "btn btn-lg btn-primary btnblock">
               Sign in</button>
				
         </div> 
      </div> 
		
   </div>
	
   <div id = "callPage" class = "call-page"> 
      <video id = "localVideo" autoplay></video> 
      <video id = "remoteVideo" autoplay></video>
		
      <div class = "row text-center"> 
         <div class = "col-md-12"> 
            <input id = "callToUsernameInput" type = "text"
               placeholder = "username to call" /> 
            <button id = "callBtn" class = "btn-success btn">Call</button> 
            <button id = "hangUpBtn" class = "btn-danger btn">Hang Up</button> 
         </div>	
      </div> 
		
   </div>
	
   <script src = "client.js"></script> 
	
   </body>
	
</html>

Esta página deve ser familiar para você. Adicionamos o arquivo css de bootstrap . Também definimos duas páginas. Por fim, criamos diversos campos de texto e botões para obter informações do usuário. Você deve ver os dois elementos de vídeo para streams de vídeo locais e remotos. Observe que adicionamos um link para um arquivo client.js .

Agora precisamos estabelecer uma conexão com nosso servidor de sinalização. Crie o arquivo client.js na pasta raiz com o seguinte código -

//our username 
var name; 
var connectedUser;
  
//connecting to our signaling server 
var conn = new WebSocket('ws://localhost:9090');
  
conn.onopen = function () { 
   console.log("Connected to the signaling server"); 
};
  
//when we got a message from a signaling server 
conn.onmessage = function (msg) { 
   console.log("Got message", msg.data);
	
   var data = JSON.parse(msg.data);
	
   switch(data.type) { 
      case "login": 
         handleLogin(data.success);
         break; 
      //when somebody wants to call us 
      case "offer": 
         handleOffer(data.offer, data.name); 
         break; 
      case "answer": 
         handleAnswer(data.answer); 
         break; 
      //when a remote peer sends an ice candidate to us 
      case "candidate": 
         handleCandidate(data.candidate); 
         break; 
      case "leave": 
         handleLeave(); 
         break; 
      default: 
         break; 
   } 
};
  
conn.onerror = function (err) { 
   console.log("Got error", err); 
};
  
//alias for sending JSON encoded messages 
function send(message) { 
   //attach the other peer username to our messages 
   if (connectedUser) { 
      message.name = connectedUser; 
   } 
	
   conn.send(JSON.stringify(message)); 
};

Agora execute nosso servidor de sinalização via servidor de nó . Em seguida, dentro da pasta raiz execute o comando estático e abra a página dentro do navegador. Você deve ver a seguinte saída do console -

A próxima etapa é implementar um login de usuário com um nome de usuário exclusivo. Nós simplesmente enviamos um nome de usuário para o servidor, que nos informa se ele foi usado ou não. Adicione o seguinte código ao seu arquivo client.js -

//****** 
//UI selectors block 
//****** 

var loginPage = document.querySelector('#loginPage'); 
var usernameInput = document.querySelector('#usernameInput'); 
var loginBtn = document.querySelector('#loginBtn'); 

var callPage = document.querySelector('#callPage'); 
var callToUsernameInput = document.querySelector('#callToUsernameInput');
var callBtn = document.querySelector('#callBtn'); 

var hangUpBtn = document.querySelector('#hangUpBtn');
  
//hide call page 
callPage.style.display = "none"; 
 
// Login when the user clicks the button 
loginBtn.addEventListener("click", function (event) { 
   name = usernameInput.value; 
	
   if (name.length > 0) { 
      send({ 
         type: "login", 
         name: name 
      }); 
   } 
	
});
 
function handleLogin(success) { 

   if (success === false) { 
      alert("Ooops...try a different username"); 
   } else { 
      //display the call page if login is successful 
      loginPage.style.display = "none"; 
      callPage.style.display = "block";  
      //start peer connection 
   } 
};

Primeiramente, selecionamos algumas referências aos elementos da página. Em seguida, ocultamos a página de chamada. Em seguida, adicionamos um ouvinte de evento no botão de login. Quando o usuário clica nele, enviamos seu nome de usuário para o servidor. Finalmente, implementamos o callback handleLogin. Se o login for bem-sucedido, mostramos a página de chamada e iniciamos a configuração de uma conexão de par.

Para iniciar uma conexão de par, precisamos -

  • Obtenha um stream da webcam.
  • Crie o objeto RTCPeerConnection.

Adicione o seguinte código ao “bloco de seletores de IU” -

var localVideo = document.querySelector('#localVideo'); 
var remoteVideo = document.querySelector('#remoteVideo');
 
var yourConn; 
var stream;

Modifique a função handleLogin -

function handleLogin(success) { 

   if (success === false) { 
      alert("Ooops...try a different username"); 
   } else { 
      loginPage.style.display = "none"; 
      callPage.style.display = "block";
		  
      //********************** 
      //Starting a peer connection 
      //********************** 
		
      //getting local video stream 
      navigator.webkitGetUserMedia({ video: true, audio: true }, function (myStream) { 
         stream = myStream; 
			
         //displaying local video stream on the page 
         localVideo.src = window.URL.createObjectURL(stream);
			
         //using Google public stun server 
         var configuration = { 
            "iceServers": [{ "url": "stun:stun2.1.google.com:19302" }] 
         }; 
			
         yourConn = new webkitRTCPeerConnection(configuration);
			
         // setup stream listening 
         yourConn.addStream(stream); 
			
         //when a remote user adds stream to the peer connection, we display it 
         yourConn.onaddstream = function (e) { 
            remoteVideo.src = window.URL.createObjectURL(e.stream); 
         };
			
         // Setup ice handling 
         yourConn.onicecandidate = function (event) {
			
            if (event.candidate) { 
               send({ 
                  type: "candidate", 
                  candidate: event.candidate 
               }); 
            } 
				
         };
			
      }, function (error) { 
         console.log(error); 
      }); 
   } 
};

Agora, se você executar o código, a página deve permitir que você faça login e exiba seu stream de vídeo local na página.

Agora estamos prontos para iniciar uma chamada. Em primeiro lugar, enviamos uma oferta para outro usuário. Depois que o usuário recebe a oferta, ele cria uma resposta e começa a negociar candidatos ICE. Adicione o seguinte código ao arquivo client.js -

//initiating a call 
callBtn.addEventListener("click", function () { 
   var callToUsername = callToUsernameInput.value; 
	
   if (callToUsername.length > 0) {
	
      connectedUser = callToUsername;
		
      // create an offer
      yourConn.createOffer(function (offer) { 
         send({ 
            type: "offer", 
            offer: offer 
         }); 
			
         yourConn.setLocalDescription(offer); 
			
      }, function (error) { 
         alert("Error when creating an offer"); 
      });  
   } 
});
  
//when somebody sends us an offer 
function handleOffer(offer, name) { 
   connectedUser = name; 
   yourConn.setRemoteDescription(new RTCSessionDescription(offer));
	
   //create an answer to an offer 
   yourConn.createAnswer(function (answer) { 
      yourConn.setLocalDescription(answer); 
		
      send({ 
         type: "answer", 
         answer: answer 
      }); 
		
   }, function (error) { 
      alert("Error when creating an answer"); 
   }); 
};
  
//when we got an answer from a remote user 
function handleAnswer(answer) { 
   yourConn.setRemoteDescription(new RTCSessionDescription(answer));
}; 
 
//when we got an ice candidate from a remote user 
function handleCandidate(candidate) { 
   yourConn.addIceCandidate(new RTCIceCandidate(candidate)); 
};

Adicionamos um manipulador de cliques ao botão Chamar, que inicia uma oferta. Em seguida, implementamos vários manipuladores esperados pelo manipulador onmessage . Eles serão processados ​​de forma assíncrona até que ambos os usuários tenham feito uma conexão.

A última etapa é implementar o recurso de desligamento. Isso interromperá a transmissão de dados e instruirá o outro usuário a fechar a chamada. Adicione o seguinte código -

//hang up 
hangUpBtn.addEventListener("click", function () { 

   send({ 
      type: "leave" 
   });
	
   handleLeave(); 
});
  
function handleLeave() { 
   connectedUser = null; 
   remoteVideo.src = null; 
	
   yourConn.close(); 
   yourConn.onicecandidate = null; 
   yourConn.onaddstream = null; 
};

Quando o usuário clica no botão Desligar -

  • Ele enviará uma mensagem de "deixe" para o outro usuário
  • Ele fechará o RTCPeerConnection e destruirá a conexão localmente

Agora execute o código. Você deve ser capaz de fazer login no servidor usando duas guias do navegador. Você pode então ligar para a guia e desligar a ligação.

A seguir está o arquivo client.js inteiro -

//our username 
var name; 
var connectedUser;
  
//connecting to our signaling server
var conn = new WebSocket('ws://localhost:9090');
  
conn.onopen = function () { 
   console.log("Connected to the signaling server"); 
};
  
//when we got a message from a signaling server 
conn.onmessage = function (msg) { 
   console.log("Got message", msg.data);
	
   var data = JSON.parse(msg.data); 
	
   switch(data.type) { 
      case "login": 
         handleLogin(data.success); 
         break; 
      //when somebody wants to call us 
      case "offer": 
         handleOffer(data.offer, data.name); 
         break; 
      case "answer": 
         handleAnswer(data.answer); 
         break; 
      //when a remote peer sends an ice candidate to us 
      case "candidate": 
         handleCandidate(data.candidate); 
         break; 
      case "leave": 
         handleLeave(); 
         break; 
      default: 
         break; 
   }
};
  
conn.onerror = function (err) { 
   console.log("Got error", err); 
};
  
//alias for sending JSON encoded messages 
function send(message) { 
   //attach the other peer username to our messages 
   if (connectedUser) { 
      message.name = connectedUser; 
   } 
	
   conn.send(JSON.stringify(message)); 
};
  
//****** 
//UI selectors block 
//******
 
var loginPage = document.querySelector('#loginPage'); 
var usernameInput = document.querySelector('#usernameInput'); 
var loginBtn = document.querySelector('#loginBtn'); 

var callPage = document.querySelector('#callPage'); 
var callToUsernameInput = document.querySelector('#callToUsernameInput');
var callBtn = document.querySelector('#callBtn'); 

var hangUpBtn = document.querySelector('#hangUpBtn');
  
var localVideo = document.querySelector('#localVideo'); 
var remoteVideo = document.querySelector('#remoteVideo'); 

var yourConn; 
var stream;
  
callPage.style.display = "none";

// Login when the user clicks the button 
loginBtn.addEventListener("click", function (event) { 
   name = usernameInput.value;
	
   if (name.length > 0) { 
      send({ 
         type: "login", 
         name: name 
      }); 
   }
	
});
  
function handleLogin(success) { 
   if (success === false) { 
      alert("Ooops...try a different username"); 
   } else { 
      loginPage.style.display = "none"; 
      callPage.style.display = "block";
		
      //********************** 
      //Starting a peer connection 
      //********************** 
		
      //getting local video stream 
      navigator.webkitGetUserMedia({ video: true, audio: true }, function (myStream) { 
         stream = myStream; 
			
         //displaying local video stream on the page 
         localVideo.src = window.URL.createObjectURL(stream);
			
         //using Google public stun server 
         var configuration = { 
            "iceServers": [{ "url": "stun:stun2.1.google.com:19302" }]
         }; 
			
         yourConn = new webkitRTCPeerConnection(configuration); 
			
         // setup stream listening 
         yourConn.addStream(stream); 
			
         //when a remote user adds stream to the peer connection, we display it 
         yourConn.onaddstream = function (e) { 
            remoteVideo.src = window.URL.createObjectURL(e.stream); 
         };
			
         // Setup ice handling 
         yourConn.onicecandidate = function (event) { 
            if (event.candidate) { 
               send({ 
                  type: "candidate", 
                  candidate: event.candidate 
               }); 
            } 
         };  
			
      }, function (error) { 
         console.log(error); 
      }); 
		
   } 
};
  
//initiating a call 
callBtn.addEventListener("click", function () { 
   var callToUsername = callToUsernameInput.value;
	
   if (callToUsername.length > 0) { 
	
      connectedUser = callToUsername;
		
      // create an offer 
      yourConn.createOffer(function (offer) { 
         send({ 
            type: "offer", 
            offer: offer 
         }); 
			
         yourConn.setLocalDescription(offer); 
      }, function (error) { 
         alert("Error when creating an offer"); 
      });
		
   } 
});
  
//when somebody sends us an offer 
function handleOffer(offer, name) { 
   connectedUser = name; 
   yourConn.setRemoteDescription(new RTCSessionDescription(offer));
	
   //create an answer to an offer 
   yourConn.createAnswer(function (answer) { 
      yourConn.setLocalDescription(answer); 
		
      send({ 
         type: "answer", 
         answer: answer 
      }); 
		
   }, function (error) { 
      alert("Error when creating an answer"); 
   }); 
};
  
//when we got an answer from a remote user
function handleAnswer(answer) { 
   yourConn.setRemoteDescription(new RTCSessionDescription(answer)); 
};
  
//when we got an ice candidate from a remote user 
function handleCandidate(candidate) { 
   yourConn.addIceCandidate(new RTCIceCandidate(candidate)); 
};
   
//hang up 
hangUpBtn.addEventListener("click", function () { 

   send({ 
      type: "leave" 
   });  
	
   handleLeave(); 
});
  
function handleLeave() { 
   connectedUser = null; 
   remoteVideo.src = null; 
	
   yourConn.close(); 
   yourConn.onicecandidate = null; 
   yourConn.onaddstream = null; 
};

Resumo

Esta demonstração fornece uma linha de base de recursos que todo aplicativo WebRTC precisa. Para melhorar esta demonstração, você pode adicionar a identificação do usuário através de plataformas como Facebook ou Google, lidar com a entrada do usuário para dados inválidos. Além disso, a conexão WebRTC pode falhar devido a vários motivos, como não suportar a tecnologia ou não ser capaz de atravessar firewalls. Valeu a pena trabalhar para tornar estável qualquer aplicativo WebRTC.

Neste capítulo, vamos construir um aplicativo cliente que permite que dois usuários em dispositivos separados se comuniquem usando fluxos de áudio WebRTC. Nosso aplicativo terá duas páginas. Um para login e outro para fazer uma chamada de áudio para outro usuário.

As duas páginas serão as tags div . A maioria das entradas é feita por meio de manipuladores de eventos simples.

Servidor de Sinalização

Para criar uma conexão WebRTC, os clientes devem ser capazes de transferir mensagens sem usar uma conexão de mesmo nível WebRTC. É aqui que usaremos HTML5 WebSockets - uma conexão de soquete bidirecional entre dois endpoints - um servidor web e um navegador web. Agora vamos começar a usar a biblioteca WebSocket. Crie o arquivo server.js e insira o seguinte código -

//require our websocket library 
var WebSocketServer = require('ws').Server; 

//creating a websocket server at port 9090 
var wss = new WebSocketServer({port: 9090});
  
//when a user connects to our sever 
wss.on('connection', function(connection) { 
   console.log("user connected"); 
	
   //when server gets a message from a connected user 
   connection.on('message', function(message) { 
      console.log("Got message from a user:", message); 
   }); 
	
   connection.send("Hello from server"); 
});

A primeira linha requer a biblioteca WebSocket que já instalamos. Em seguida, criamos um servidor de soquete na porta 9090. Em seguida, ouvimos o evento de conexão . Este código será executado quando um usuário fizer uma conexão WebSocket com o servidor. Em seguida, ouvimos todas as mensagens enviadas pelo usuário. Por fim, enviamos uma resposta ao usuário conectado dizendo “Olá do servidor”.

Em nosso servidor de sinalização, usaremos um nome de usuário baseado em string para cada conexão, para que saibamos para onde enviar mensagens. Vamos mudar nosso gerenciador de conexão um pouco -

connection.on('message', function(message) { 
   var data; 
	
   //accepting only JSON messages 
   try { 
      data = JSON.parse(message); 
   } catch (e) { 
      console.log("Invalid JSON");
      data = {}; 
   } 
});

Desta forma, aceitamos apenas mensagens JSON. Em seguida, precisamos armazenar todos os usuários conectados em algum lugar. Usaremos um objeto Javascript simples para isso. Mude a parte superior do nosso arquivo -

//require our websocket library 
var WebSocketServer = require('ws').Server; 

//creating a websocket server at port 9090 
var wss = new WebSocketServer({port: 9090}); 

//all connected to the server users 
var users = {};

Vamos adicionar um campo de tipo para cada mensagem vinda do cliente. Por exemplo, se um usuário deseja fazer o login, ele envia a mensagem do tipo de login . Vamos definir -

connection.on('message', function(message) {
  
   var data; 
   //accepting only JSON messages 
   try { 
      data = JSON.parse(message); 
   } catch (e) { 
      console.log("Invalid JSON"); 
      data = {}; 
   } 
	
   //switching type of the user message 
   switch (data.type) { 
      //when a user tries to login 
      case "login": 
         console.log("User logged:", data.name); 
			
         //if anyone is logged in with this username then refuse 
         if(users[data.name]) { 
            sendTo(connection, { 
               type: "login",
               success: false 
            }); 
         } else { 
            //save user connection on the server 
            users[data.name] = connection; 
            connection.name = data.name; 
				
            sendTo(connection, { 
               type: "login", 
               success: true 
            }); 
         } 
			
         break;
			
      default: 
         sendTo(connection, { 
            type: "error", 
            message: "Command no found: " + data.type 
         }); 
			
         break; 
   }  
});

Se o usuário enviar uma mensagem com o tipo de login , nós -

  • Verifique se alguém já fez login com este nome de usuário.
  • Nesse caso, diga ao usuário que ele não fez o login com sucesso.
  • Se ninguém estiver usando este nome de usuário, adicionamos nome de usuário como uma chave para o objeto de conexão.
  • Se um comando não for reconhecido, enviamos um erro.

O código a seguir é uma função auxiliar para enviar mensagens a uma conexão. Adicione-o ao arquivo server.js -

function sendTo(connection, message) { 
   connection.send(JSON.stringify(message)); 
}

Quando o usuário desconecta, devemos limpar sua conexão. Podemos excluir o usuário quando o evento close for disparado. Adicione o seguinte código ao gerenciador de conexão -

connection.on("close", function() { 
   if(connection.name) { 
      delete users[connection.name]; 
   } 
});

Após o login bem-sucedido, o usuário deseja ligar para outro. Ele deve fazer uma oferta a outro usuário para alcançá-lo. Adicione o gerenciador de oferta -

case "offer": 
   //for ex. UserA wants to call UserB 
   console.log("Sending offer to: ", data.name); 
	
   //if UserB exists then send him offer details 
   var conn = users[data.name]; 
	
   if(conn != null) { 
      //setting that UserA connected with UserB 
      connection.otherName = data.name; 
      sendTo(conn, { 
         type: "offer", 
         offer: data.offer, 
         name: connection.name 
      });
   }		
	
   break;

Em primeiro lugar, obtemos a conexão do usuário que estamos tentando chamar. Se existir, enviamos-lhe os detalhes da oferta . Também adicionamos outroNome ao objeto de conexão . Isso é feito pela simplicidade de encontrá-lo mais tarde.

Responder à resposta tem um padrão semelhante ao que usamos no manipulador de ofertas . Nosso servidor apenas passa por todas as mensagens como resposta a outro usuário. Adicione o seguinte código após o manipulador de oferta -

case "answer": 
   console.log("Sending answer to: ", data.name); 
   //for ex. UserB answers UserA
   var conn = users[data.name]; 
	
   if(conn != null) { 
      connection.otherName = data.name;
		
      sendTo(conn, { 
         type: "answer", 
         answer: data.answer 
      }); 
   } 
	
   break;

A parte final é lidar com o candidato ICE entre os usuários. Usamos a mesma técnica apenas para passar mensagens entre os usuários. A principal diferença é que as mensagens candidatas podem ocorrer várias vezes por usuário em qualquer ordem. Adicione o manipulador de candidato -

case "candidate": 
   console.log("Sending candidate to:",data.name); 
   var conn = users[data.name];
	
   if(conn != null) { 
      sendTo(conn, { 
         type: "candidate", 
         candidate: data.candidate 
      }); 
   } 
	
   break;

Para permitir que nossos usuários se desconectem de outro usuário, devemos implementar a função de desligar. Também dirá ao servidor para excluir todas as referências do usuário. Adicione o manipulador de licença -

case "leave": 
   console.log("Disconnecting from", data.name); 
   var conn = users[data.name]; 
   conn.otherName = null; 
	
   //notify the other user so he can disconnect his peer connection 
   if(conn != null) { 
      sendTo(conn, {
         type: "leave" 
      }); 
   }  
	
   break;

Isso também enviará ao outro usuário o evento leave para que ele possa desconectar sua conexão de par de acordo. Também devemos lidar com o caso em que um usuário interrompe sua conexão do servidor de sinalização. Vamos modificar nosso manipulador de fechamento -

connection.on("close", function() { 

   if(connection.name) { 
      delete users[connection.name]; 
		
      if(connection.otherName) { 
         console.log("Disconnecting from ", connection.otherName); 
         var conn = users[connection.otherName]; 
         conn.otherName = null;
			
         if(conn != null) { 
            sendTo(conn, { 
               type: "leave" 
            }); 
         }
			
      } 
   } 
});

A seguir está o código completo do nosso servidor de sinalização -

//require our websocket library 
var WebSocketServer = require('ws').Server; 

//creating a websocket server at port 9090 
var wss = new WebSocketServer({port: 9090}); 

//all connected to the server users 
var users = {};

//when a user connects to our sever 
wss.on('connection', function(connection) {
  
   console.log("User connected");
	
   //when server gets a message from a connected user 
   connection.on('message', function(message) { 
	
      var data;
		
      //accepting only JSON messages 
      try { 
         data = JSON.parse(message); 
      } catch (e) { 
         console.log("Invalid JSON"); 
         data = {}; 
      }
		
      //switching type of the user message 
      switch (data.type) { 
         //when a user tries to login 
         case "login": 
            console.log("User logged", data.name); 
				
            //if anyone is logged in with this username then refuse 
            if(users[data.name]) { 
               sendTo(connection, { 
                  type: "login", 
                  success: false 
               }); 
            } else { 
               //save user connection on the server 
               users[data.name] = connection; 
               connection.name = data.name;
               sendTo(connection, { 
                  type: "login", 
                  success: true 
               }); 
            } 
				
            break;
				
         case "offer": 
            //for ex. UserA wants to call UserB 
            console.log("Sending offer to: ", data.name); 
				
            //if UserB exists then send him offer details 
            var conn = users[data.name]; 
				
            if(conn != null) { 
               //setting that UserA connected with UserB 
               connection.otherName = data.name; 
               sendTo(conn, { 
                  type: "offer", 
                  offer: data.offer, 
                  name: connection.name 
               }); 
            } 
				
            break;
				
         case "answer": 
            console.log("Sending answer to: ", data.name); 
            //for ex. UserB answers UserA 
            var conn = users[data.name]; 
				
            if(conn != null) { 
               connection.otherName = data.name; 
               sendTo(conn, { 
                  type: "answer", 
                  answer: data.answer 
               });
            } 
				
            break;
				
         case "candidate": 
            console.log("Sending candidate to:",data.name); 
            var conn = users[data.name];  
				
            if(conn != null) { 
               sendTo(conn, { 
                  type: "candidate", 
                  candidate: data.candidate 
               }); 
            } 
				
            break;
				
         case "leave": 
            console.log("Disconnecting from", data.name); 
            var conn = users[data.name]; 
            conn.otherName = null; 
				
            //notify the other user so he can disconnect his peer connection 
            if(conn != null) { 
               sendTo(conn, { 
                  type: "leave" 
               }); 
            }  
				
            break;
				
         default: 
            sendTo(connection, { 
               type: "error", 
               message: "Command not found: " + data.type 
            });
				
            break; 
      }  
   });
	
   //when user exits, for example closes a browser window 
   //this may help if we are still in "offer","answer" or "candidate" state 
   connection.on("close", function() { 
	
      if(connection.name) { 
         delete users[connection.name]; 
			
         if(connection.otherName) { 
            console.log("Disconnecting from ", connection.otherName); 
            var conn = users[connection.otherName]; 
            conn.otherName = null;  
				
            if(conn != null) { 
               sendTo(conn, { 
                  type: "leave" 
              }); 
            }  
         } 
      } 
   });  
	
   connection.send("Hello world"); 
}); 
 
function sendTo(connection, message) { 
   connection.send(JSON.stringify(message)); 
}

Aplicativo do cliente

Uma forma de testar esse aplicativo é abrir duas guias do navegador e tentar fazer uma chamada de áudio entre eles.

Em primeiro lugar, precisamos instalar a biblioteca bootstrap . Bootstrap é uma estrutura de front-end para o desenvolvimento de aplicativos da web. Você pode aprender mais emhttp://getbootstrap.com/.Crie uma pasta chamada, por exemplo, “audiochat”. Esta será a nossa pasta de aplicativo raiz. Dentro desta pasta, crie um arquivo package.json (é necessário para gerenciar dependências npm) e adicione o seguinte -

{ 
   "name": "webrtc-audiochat", 
   "version": "0.1.0", 
   "description": "webrtc-audiochat", 
   "author": "Author", 
   "license": "BSD-2-Clause" 
}

Em seguida, execute npm install bootstrap . Isso instalará a biblioteca de bootstrap na pasta audiochat / node_modules .

Agora precisamos criar uma página HTML básica. Crie um arquivo index.html na pasta raiz com o seguinte código -

<html>
 
   <head> 
      <title>WebRTC Voice Demo</title> 
      <link rel = "stylesheet" href = "node_modules/bootstrap/dist/css/bootstrap.min.css"/> 
   </head>
 
   <style> 
      body { 
         background: #eee; 
         padding: 5% 0; 
      } 
   </style>
	
   <body> 
      <div id = "loginPage" class = "container text-center"> 
		
         <div class = "row"> 
            <div class = "col-md-4 col-md-offset-4">
				
               <h2>WebRTC Voice Demo. Please sign in</h2>
				
               <label for = "usernameInput" class = "sr-only">Login</label> 
               <input type = "email" id = "usernameInput" 
                  class = "form-control formgroup"
                  placeholder = "Login" required = "" autofocus = ""> 
               <button id = "loginBtn" class = "btn btn-lg btn-primary btnblock">
                  Sign in</button> 
            </div> 
         </div> 
			
      </div>
		
      <div id = "callPage" class = "call-page">
		
         <div class = "row"> 
			
            <div class = "col-md-6 text-right"> 
               Local audio: <audio id = "localAudio" 
               controls autoplay></audio> 
            </div>
				
            <div class = "col-md-6 text-left"> 
               Remote audio: <audio id = "remoteAudio" 
                  controls autoplay></audio> 
            </div> 
				
         </div> 
			
         <div class = "row text-center"> 
            <div class = "col-md-12"> 
               <input id = "callToUsernameInput" 
                  type = "text" placeholder = "username to call" /> 
               <button id = "callBtn" class = "btn-success btn">Call</button> 
               <button id = "hangUpBtn" class = "btn-danger btn">Hang Up</button> 
            </div> 
         </div>
			
      </div> 
		
      <script src = "client.js"></script> 
		
   </body>
	
</html>

Esta página deve ser familiar para você. Adicionamos o arquivo css de bootstrap . Também definimos duas páginas. Por fim, criamos diversos campos de texto e botões para obter informações do usuário. Você deve ver os dois elementos de áudio para fluxos de áudio locais e remotos. Observe que adicionamos um link para um arquivo client.js .

Agora precisamos estabelecer uma conexão com nosso servidor de sinalização. Crie o arquivo client.js na pasta raiz com o seguinte código -

//our username 
var name; 
var connectedUser;
  
//connecting to our signaling server 
var conn = new WebSocket('ws://localhost:9090');
  
conn.onopen = function () { 
   console.log("Connected to the signaling server"); 
}; 
 
//when we got a message from a signaling server 
conn.onmessage = function (msg) { 
   console.log("Got message", msg.data);  
   var data = JSON.parse(msg.data);  
	
   switch(data.type) { 
      case "login": 
         handleLogin(data.success); 
         break; 
      //when somebody wants to call us 
      case "offer": 
         handleOffer(data.offer, data.name); 
         break; 
      case "answer": 
         handleAnswer(data.answer); 
         break; 
      //when a remote peer sends an ice candidate to us 
      case "candidate": 
         handleCandidate(data.candidate); 
         break;
      case "leave": 
         handleLeave(); 
         break; 
      default: 
         break; 
   } 
};
  
conn.onerror = function (err) { 
   console.log("Got error", err); 
};
  
//alias for sending JSON encoded messages 
function send(message) { 
   //attach the other peer username to our messages
   if (connectedUser) { 
      message.name = connectedUser; 
   } 
	
   conn.send(JSON.stringify(message)); 
};

Agora execute nosso servidor de sinalização via servidor de nó . Em seguida, dentro da pasta raiz execute o comando estático e abra a página dentro do navegador. Você deve ver a seguinte saída do console -

A próxima etapa é implementar um login de usuário com um nome de usuário exclusivo. Nós simplesmente enviamos um nome de usuário para o servidor, que nos informa se ele foi usado ou não. Adicione o seguinte código ao seu arquivo client.js -

//****** 
//UI selectors block 
//******

var loginPage = document.querySelector('#loginPage'); 
var usernameInput = document.querySelector('#usernameInput'); 
var loginBtn = document.querySelector('#loginBtn');
 
var callPage = document.querySelector('#callPage'); 
var callToUsernameInput = document.querySelector('#callToUsernameInput');
var callBtn = document.querySelector('#callBtn');
 
var hangUpBtn = document.querySelector('#hangUpBtn');
  
callPage.style.display = "none";
  
// Login when the user clicks the button 
loginBtn.addEventListener("click", function (event) { 
   name = usernameInput.value;
	
   if (name.length > 0) { 
      send({
         type: "login", 
         name: name 
      }); 
   } 
	
}); 
 
function handleLogin(success) { 
   if (success === false) { 
      alert("Ooops...try a different username"); 
   } else { 
      loginPage.style.display = "none"; 
      callPage.style.display = "block"; 
		
      //********************** 
      //Starting a peer connection 
      //**********************
		         
   } 
	
};

Primeiramente, selecionamos algumas referências aos elementos da página. Em seguida, ocultamos a página de chamada. Em seguida, adicionamos um ouvinte de evento no botão de login. Quando o usuário clica nele, enviamos seu nome de usuário para o servidor. Finalmente, implementamos o callback handleLogin. Se o login for bem-sucedido, mostramos a página de chamada e iniciamos a configuração de uma conexão de par.

Para iniciar uma conexão de par, precisamos -

  • Obtenha um stream de áudio de um microfone
  • Crie o objeto RTCPeerConnection

Adicione o seguinte código ao “bloco de seletores de IU” -

var localAudio = document.querySelector('#localAudio'); 
var remoteAudio = document.querySelector('#remoteAudio'); 

var yourConn; 
var stream;

Modifique a função handleLogin -

function handleLogin(success) { 
   if (success === false) { 
      alert("Ooops...try a different username"); 
   } else { 
      loginPage.style.display = "none"; 
      callPage.style.display = "block";
		
      //********************** 
      //Starting a peer connection 
      //********************** 
		
      //getting local audio stream 
      navigator.webkitGetUserMedia({ video: false, audio: true }, function (myStream) { 
         stream = myStream; 
			
         //displaying local audio stream on the page
         localAudio.src = window.URL.createObjectURL(stream);
			
         //using Google public stun server 
         var configuration = { 
            "iceServers": [{ "url": "stun:stun2.1.google.com:19302" }] 
         }; 
			
         yourConn = new webkitRTCPeerConnection(configuration); 
			
         // setup stream listening 
         yourConn.addStream(stream); 
			
         //when a remote user adds stream to the peer connection, we display it 
         yourConn.onaddstream = function (e) { 
            remoteAudio.src = window.URL.createObjectURL(e.stream); 
         }; 
			
         // Setup ice handling 
         yourConn.onicecandidate = function (event) { 
            if (event.candidate) { 
               send({ 
                  type: "candidate", 
               }); 
            } 
         };  
			
      }, function (error) { 
         console.log(error); 
      }); 
		
   } 
};

Agora, se você executar o código, a página deve permitir que você faça login e exiba seu stream de áudio local na página.

Agora estamos prontos para iniciar uma chamada. Em primeiro lugar, enviamos uma oferta para outro usuário. Depois que o usuário recebe a oferta, ele cria uma resposta e começa a negociar candidatos ICE. Adicione o seguinte código ao arquivo client.js -

//initiating a call 
callBtn.addEventListener("click", function () { 
   var callToUsername = callToUsernameInput.value; 
	
   if (callToUsername.length > 0) { 
      connectedUser = callToUsername; 
		
      // create an offer 
      yourConn.createOffer(function (offer) { 
         send({ 
            type: "offer", 
            offer: offer 
         }); 
			
         yourConn.setLocalDescription(offer); 
			
      }, function (error) { 
         alert("Error when creating an offer"); 
      }); 
   } 
	
});
 
//when somebody sends us an offer 
function handleOffer(offer, name) { 
   connectedUser = name; 
   yourConn.setRemoteDescription(new RTCSessionDescription(offer)); 
	
   //create an answer to an offer 
   yourConn.createAnswer(function (answer) { 
      yourConn.setLocalDescription(answer); 
		
      send({ 
         type: "answer",
         answer: answer 
      }); 
		
   }, function (error) { 
      alert("Error when creating an answer"); 
   }); 
	
};
 
//when we got an answer from a remote user 
function handleAnswer(answer) { 
   yourConn.setRemoteDescription(new RTCSessionDescription(answer)); 
};
 
//when we got an ice candidate from a remote user 
function handleCandidate(candidate) { 
   yourConn.addIceCandidate(new RTCIceCandidate(candidate)); 
};

Adicionamos um manipulador de cliques ao botão Chamar, que inicia uma oferta. Em seguida, implementamos vários manipuladores esperados pelo manipulador onmessage . Eles serão processados ​​de forma assíncrona até que ambos os usuários tenham feito uma conexão.

A última etapa é implementar o recurso de desligamento. Isso interromperá a transmissão de dados e instruirá o outro usuário a fechar a chamada. Adicione o seguinte código -

//hang up 
hangUpBtn.addEventListener("click", function () { 
   send({ 
      type: "leave" 
   });  
	
   handleLeave(); 
});
  
function handleLeave() { 
   connectedUser = null; 
   remoteAudio.src = null;
	
   yourConn.close(); 
   yourConn.onicecandidate = null; 
   yourConn.onaddstream = null;
};

Quando o usuário clica no botão Desligar -

  • Ele enviará uma mensagem de "deixe" para o outro usuário
  • Ele fechará o RTCPeerConnection e destruirá a conexão localmente

Agora execute o código. Você deve ser capaz de fazer login no servidor usando duas guias do navegador. Você pode então fazer uma chamada de áudio para a guia e desligar a chamada.

A seguir está o arquivo client.js inteiro -

//our username 
var name; 
var connectedUser;
 
//connecting to our signaling server 
var conn = new WebSocket('ws://localhost:9090');
 
conn.onopen = function () { 
   console.log("Connected to the signaling server"); 
};
 
//when we got a message from a signaling server 
conn.onmessage = function (msg) { 
   console.log("Got message", msg.data); 
   var data = JSON.parse(msg.data); 
	
   switch(data.type) { 
      case "login": 
         handleLogin(data.success); 
         break; 
      //when somebody wants to call us 
      case "offer": 
         handleOffer(data.offer, data.name); 
         break; 
      case "answer": 
         handleAnswer(data.answer); 
         break; 
      //when a remote peer sends an ice candidate to us 
      case "candidate": 
         handleCandidate(data.candidate); 
         break; 
      case "leave": 
         handleLeave(); 
         break; 
      default: 
         break; 
   } 
}; 

conn.onerror = function (err) { 
   console.log("Got error", err); 
};
 
//alias for sending JSON encoded messages 
function send(message) { 
   //attach the other peer username to our messages 
   if (connectedUser) { 
      message.name = connectedUser; 
   } 
	
   conn.send(JSON.stringify(message)); 
};
 
//****** 
//UI selectors block 
//****** 

var loginPage = document.querySelector('#loginPage'); 
var usernameInput = document.querySelector('#usernameInput'); 
var loginBtn = document.querySelector('#loginBtn');

var callPage = document.querySelector('#callPage'); 
var callToUsernameInput = document.querySelector('#callToUsernameInput');
var callBtn = document.querySelector('#callBtn'); 

var hangUpBtn = document.querySelector('#hangUpBtn'); 
var localAudio = document.querySelector('#localAudio'); 
var remoteAudio = document.querySelector('#remoteAudio'); 

var yourConn; 
var stream; 

callPage.style.display = "none";
 
// Login when the user clicks the button 
loginBtn.addEventListener("click", function (event) { 
   name = usernameInput.value; 
	
   if (name.length > 0) { 
      send({ 
         type: "login", 
         name: name 
      }); 
   } 
	
});
 
function handleLogin(success) { 
   if (success === false) { 
      alert("Ooops...try a different username"); 
   } else { 
      loginPage.style.display = "none"; 
      callPage.style.display = "block"; 
		
      //********************** 
      //Starting a peer connection 
      //********************** 
		
      //getting local audio stream 
      navigator.webkitGetUserMedia({ video: false, audio: true }, function (myStream) { 
         stream = myStream; 
			
         //displaying local audio stream on the page 
         localAudio.src = window.URL.createObjectURL(stream);
			
         //using Google public stun server 
         var configuration = { 
            "iceServers": [{ "url": "stun:stun2.1.google.com:19302" }] 
         }; 
			
         yourConn = new webkitRTCPeerConnection(configuration); 
			
         // setup stream listening 
         yourConn.addStream(stream); 
			
         //when a remote user adds stream to the peer connection, we display it 
         yourConn.onaddstream = function (e) { 
            remoteAudio.src = window.URL.createObjectURL(e.stream); 
         }; 
			
         // Setup ice handling 
         yourConn.onicecandidate = function (event) { 
            if (event.candidate) { 
               send({ 
                  type: "candidate", 
                  candidate: event.candidate 
               }); 
            } 
         }; 
			
      }, function (error) { 
         console.log(error); 
      }); 
		
   } 
};
 
//initiating a call 
callBtn.addEventListener("click", function () { 
   var callToUsername = callToUsernameInput.value; 
	
   if (callToUsername.length > 0) { 
      connectedUser = callToUsername; 
		
      // create an offer 
      yourConn.createOffer(function (offer) { 
         send({
            type: "offer", 
            offer: offer 
         }); 
			
         yourConn.setLocalDescription(offer); 
      }, function (error) { 
         alert("Error when creating an offer"); 
      }); 
   } 
});
 
//when somebody sends us an offer 
function handleOffer(offer, name) { 
   connectedUser = name; 
   yourConn.setRemoteDescription(new RTCSessionDescription(offer)); 
	
   //create an answer to an offer 
   yourConn.createAnswer(function (answer) { 
      yourConn.setLocalDescription(answer); 
		
      send({ 
         type: "answer", 
         answer: answer 
      });
		
   }, function (error) { 
      alert("Error when creating an answer"); 
   }); 
	
};
 
//when we got an answer from a remote user 
function handleAnswer(answer) { 
   yourConn.setRemoteDescription(new RTCSessionDescription(answer)); 
};
 
//when we got an ice candidate from a remote user 
function handleCandidate(candidate) { 
   yourConn.addIceCandidate(new RTCIceCandidate(candidate)); 
};
 
//hang up
hangUpBtn.addEventListener("click", function () { 
   send({ 
      type: "leave" 
   }); 
	
   handleLeave(); 
});
 
function handleLeave() { 
   connectedUser = null; 
   remoteAudio.src = null; 
	
   yourConn.close(); 
   yourConn.onicecandidate = null; 
   yourConn.onaddstream = null; 
};

Neste capítulo, vamos construir um aplicativo cliente que permite que dois usuários em dispositivos separados enviem mensagens um ao outro usando WebRTC. Nosso aplicativo terá duas páginas. Um para login e outro para enviar mensagens a outro usuário.

As duas páginas serão as tags div . A maioria das entradas é feita por meio de manipuladores de eventos simples.

Servidor de Sinalização

Para criar uma conexão WebRTC, os clientes devem ser capazes de transferir mensagens sem usar uma conexão de mesmo nível WebRTC. É aqui que usaremos HTML5 WebSockets - uma conexão de soquete bidirecional entre dois endpoints - um servidor web e um navegador web. Agora vamos começar a usar a biblioteca WebSocket. Crie o arquivo server.js e insira o seguinte código -

//require our websocket library 
var WebSocketServer = require('ws').Server; 

//creating a websocket server at port 9090 
var wss = new WebSocketServer({port: 9090});
  
//when a user connects to our sever 
wss.on('connection', function(connection) { 
   console.log("user connected"); 
	
   //when server gets a message from a connected user 
   connection.on('message', function(message) { 
      console.log("Got message from a user:", message); 
   }); 
	
   connection.send("Hello from server"); 
});

A primeira linha requer a biblioteca WebSocket que já instalamos. Em seguida, criamos um servidor de soquete na porta 9090. Em seguida, ouvimos o evento de conexão . Este código será executado quando um usuário fizer uma conexão WebSocket com o servidor. Em seguida, ouvimos todas as mensagens enviadas pelo usuário. Por fim, enviamos uma resposta ao usuário conectado dizendo “Olá do servidor”.

Em nosso servidor de sinalização, usaremos um nome de usuário baseado em string para cada conexão, para que saibamos para onde enviar mensagens. Vamos mudar nosso gerenciador de conexão um pouco -

connection.on('message', function(message) { 
   var data; 
	
   //accepting only JSON messages 
   try { 
      data = JSON.parse(message); 
   } catch (e) { 
      console.log("Invalid JSON");
      data = {}; 
   } 
});

Desta forma, aceitamos apenas mensagens JSON. Em seguida, precisamos armazenar todos os usuários conectados em algum lugar. Usaremos um objeto Javascript simples para isso. Mude a parte superior do nosso arquivo -

//require our websocket library 
var WebSocketServer = require('ws').Server; 

//creating a websocket server at port 9090 
var wss = new WebSocketServer({port: 9090}); 

//all connected to the server users 
var users = {};

Vamos adicionar um campo de tipo para cada mensagem vinda do cliente. Por exemplo, se um usuário deseja fazer o login, ele envia a mensagem do tipo de login . Vamos definir -

connection.on('message', function(message) { 
   var data; 
	
   //accepting only JSON messages 
   try { 
      data = JSON.parse(message); 
   } catch (e) { 
      console.log("Invalid JSON"); 
      data = {}; 
   }
	
   //switching type of the user message 
   switch (data.type) { 
      //when a user tries to login 
      case "login": 
         console.log("User logged:", data.name); 
			
         //if anyone is logged in with this username then refuse 
         if(users[data.name]) { 
            sendTo(connection, { 
               type: "login", 
               success: false
            }); 
         } else { 
            //save user connection on the server 
            users[data.name] = connection; 
            connection.name = data.name; 
				
            sendTo(connection, { 
               type: "login", 
               success: true 
            }); 
         } 
			
         break;
			
      default: 
         sendTo(connection, { 
            type: "error", 
            message: "Command no found: " + data.type 
         }); 
			
         break;
   } 
});

Se o usuário enviar uma mensagem com o tipo de login , nós -

  • Verifique se alguém já fez login com este nome de usuário.
  • Nesse caso, diga ao usuário que ele não fez o login com sucesso.
  • Se ninguém estiver usando este nome de usuário, adicionamos nome de usuário como uma chave para o objeto de conexão.
  • Se um comando não for reconhecido, enviamos um erro.

O código a seguir é uma função auxiliar para enviar mensagens a uma conexão. Adicione-o ao arquivo server.js -

function sendTo(connection, message) { 
   connection.send(JSON.stringify(message)); 
}

Quando o usuário desconecta, devemos limpar sua conexão. Podemos excluir o usuário quando o evento close for disparado. Adicione o seguinte código ao gerenciador de conexão -

connection.on("close", function() { 
   if(connection.name) { 
      delete users[connection.name]; 
   } 
});

Após o login bem-sucedido, o usuário deseja ligar para outro. Ele deve fazer uma oferta a outro usuário para alcançá-lo. Adicione o gerenciador de oferta -

case "offer": 
   //for ex. UserA wants to call UserB 
   console.log("Sending offer to: ", data.name); 
	
   //if UserB exists then send him offer details 
   var conn = users[data.name]; 
	
   if(conn != null){ 
   //setting that UserA connected with UserB 
   connection.otherName = data.name; 
	
      sendTo(conn, { 
         type: "offer", 
         offer: data.offer, 
         name: connection.name 
      }); 
     
   break;

Em primeiro lugar, obtemos a conexão do usuário que estamos tentando chamar. Se existir, enviamos-lhe os detalhes da oferta . Também adicionamos outroNome ao objeto de conexão . Isso é feito pela simplicidade de encontrá-lo mais tarde.

Responder à resposta tem um padrão semelhante ao que usamos no manipulador de ofertas . Nosso servidor apenas passa por todas as mensagens como resposta a outro usuário. Adicione o seguinte código após o manipulador de oferta -

case "answer": 
   console.log("Sending answer to: ", data.name); 
	
   //for ex. UserB answers UserA 
   var conn = users[data.name]; 
	
   if(conn != null) { 
      connection.otherName = data.name;
      sendTo(conn, { 
         type: "answer", 
         answer: data.answer 
      }); 
   } 
	
   break;

A parte final é lidar com o candidato ICE entre os usuários. Usamos a mesma técnica apenas para passar mensagens entre os usuários. A principal diferença é que as mensagens candidatas podem ocorrer várias vezes por usuário em qualquer ordem. Adicione o manipulador de candidato -

case "candidate": 
   console.log("Sending candidate to:",data.name); 
   var conn = users[data.name];
	
   if(conn != null) { 
      sendTo(conn, { 
         type: "candidate", 
         candidate: data.candidate 
      }); 
   } 
	
   break;

Para permitir que nossos usuários se desconectem de outro usuário, devemos implementar a função de desligar. Também dirá ao servidor para excluir todas as referências do usuário. Adicione o manipulador de licença -

case "leave": 
   console.log("Disconnecting from", data.name); 
   var conn = users[data.name]; 
   conn.otherName = null;
	
   //notify the other user so he can disconnect his peer connection 
   if(conn != null) { 
      sendTo(conn, { 
         type: "leave" 
      }); 
   }
	
   break;

Isso também enviará ao outro usuário o evento leave para que ele possa desconectar sua conexão de par de acordo. Também devemos lidar com o caso em que um usuário interrompe sua conexão do servidor de sinalização. Vamos modificar nosso manipulador de fechamento -

connection.on("close", function() { 

   if(connection.name) { 
      delete users[connection.name]; 
		
      if(connection.otherName) { 
         console.log("Disconnecting from ", connection.otherName); 
         var conn = users[connection.otherName]; 
         conn.otherName = null;  
			
         if(conn != null) { 
            sendTo(conn, { 
               type: "leave" 
            }); 
         }  
      } 
   } 
});

A seguir está o código completo do nosso servidor de sinalização -

//require our websocket library 
var WebSocketServer = require('ws').Server;
 
//creating a websocket server at port 9090 
var wss = new WebSocketServer({port: 9090}); 

//all connected to the server users 
var users = {};
  
//when a user connects to our sever 
wss.on('connection', function(connection) {
  
   console.log("User connected");
	 
   //when server gets a message from a connected user 
   connection.on('message', function(message) {
	 
      var data; 
      //accepting only JSON messages 
      try { 
         data = JSON.parse(message); 
      } catch (e) { 
         console.log("Invalid JSON"); 
         data = {}; 
      }
		  
      //switching type of the user message 
      switch (data.type) { 
         //when a user tries to login 
         case "login": 
            console.log("User logged", data.name); 
            //if anyone is logged in with this username then refuse 
            if(users[data.name]) { 
               sendTo(connection, { 
                  type: "login", 
                  success: false 
               }); 
            } else { 
               //save user connection on the server 
               users[data.name] = connection; 
               connection.name = data.name; 
					
                  sendTo(connection, { 
                     type: "login", 
                     success: true 
                  }); 
            }
				
            break;
				
         case "offer": 
            //for ex. UserA wants to call UserB 
            console.log("Sending offer to: ", data.name); 
				
            //if UserB exists then send him offer details 
            var conn = users[data.name]; 
				
            if(conn != null) { 
               //setting that UserA connected with UserB 
               connection.otherName = data.name; 
					
               sendTo(conn, { 
                  type: "offer", 
                  offer: data.offer, 
                  name: connection.name 
               }); 
            } 
				
            break;
				
         case "answer": 
            console.log("Sending answer to: ", data.name); 
            //for ex. UserB answers UserA 
            var conn = users[data.name]; 
				
            if(conn != null) { 
               connection.otherName = data.name; 
               sendTo(conn, { 
                  type: "answer", 
                  answer: data.answer 
               }); 
            } 
				
            break;
				
         case "candidate": 
            console.log("Sending candidate to:",data.name);
            var conn = users[data.name];  
				
            if(conn != null) { 
               sendTo(conn, { 
                  type: "candidate", 
                  candidate: data.candidate 
               }); 
            } 
				
            break;
				
         case "leave": 
            console.log("Disconnecting from", data.name); 
            var conn = users[data.name]; 
            conn.otherName = null; 
				
            //notify the other user so he can disconnect his peer connection 
            if(conn != null) { 
               sendTo(conn, { 
                  type: "leave"
               });
            }  
				
            break;
				
         default: 
            sendTo(connection, { 
               type: "error", 
               message: "Command not found: " + data.type 
            }); 
				
            break;
				
      }  
   });
	
   //when user exits, for example closes a browser window 
   //this may help if we are still in "offer","answer" or "candidate" state 
   connection.on("close", function() { 
	
      if(connection.name) { 
         delete users[connection.name]; 
			
         if(connection.otherName) { 
            console.log("Disconnecting from ", connection.otherName); 
            var conn = users[connection.otherName]; 
            conn.otherName = null;
				
            if(conn != null) { 
               sendTo(conn, { 
                  type: "leave" 
               }); 
            }  
         } 
      } 
   });
	
   connection.send("Hello world");
	
});
  
function sendTo(connection, message) { 
   connection.send(JSON.stringify(message)); 
}

Aplicativo do cliente

Uma forma de testar este aplicativo é abrir duas guias do navegador e tentar enviar uma mensagem um ao outro.

Em primeiro lugar, precisamos instalar a biblioteca bootstrap . Bootstrap é uma estrutura de front-end para o desenvolvimento de aplicativos da web. Você pode aprender mais emhttp://getbootstrap.com/.Crie uma pasta chamada, por exemplo, “textchat”. Esta será a nossa pasta de aplicativo raiz. Dentro desta pasta, crie um arquivo package.json (é necessário para gerenciar dependências npm) e adicione o seguinte -

{ 
   "name": "webrtc-textochat", 
   "version": "0.1.0", 
   "description": "webrtc-textchat", 
   "author": "Author", 
   "license": "BSD-2-Clause" 
}

Em seguida, execute npm install bootstrap . Isso instalará a biblioteca de bootstrap na pasta textchat / node_modules .

Agora precisamos criar uma página HTML básica. Crie um arquivo index.html na pasta raiz com o seguinte código -

<html> 

   <head> 
      <title>WebRTC Text Demo</title> 
      <link rel = "stylesheet" href = "node_modules/bootstrap/dist/css/bootstrap.min.css"/> 
   </head>
 
   <style> 
      body { 
         background: #eee; 
         padding: 5% 0; 
      } 
   </style>
	
   <body> 
      <div id = "loginPage" class = "container text-center"> 
		
         <div class = "row"> 
            <div class = "col-md-4 col-md-offset-4"> 
               <h2>WebRTC Text Demo. Please sign in</h2> 
               <label for = "usernameInput" class = "sr-only">Login</label> 
               <input type = "email" id = "usernameInput" 
                  class = "form-control formgroup" placeholder = "Login" 
                  required = "" autofocus = "">
               <button id = "loginBtn" class = "btn btn-lg btn-primary btnblock">
                  Sign in</button> 
            </div> 
         </div> 
			
      </div>
		
      <div id = "callPage" class = "call-page container">
		
         <div class = "row"> 
            <div class = "col-md-4 col-md-offset-4 text-center"> 
               <div class = "panel panel-primary"> 
                  <div class = "panel-heading">Text chat</div> 
                  <div id = "chatarea" class = "panel-body text-left"></div> 
               </div> 
            </div> 
         </div>
			
         <div class = "row text-center form-group"> 
            <div class = "col-md-12"> 
               <input id = "callToUsernameInput" type = "text" 
                  placeholder = "username to call" /> 
               <button id = "callBtn" class = "btn-success btn">Call</button> 
               <button id = "hangUpBtn" class = "btn-danger btn">Hang Up</button> 
            </div> 
         </div> 
			
         <div class = "row text-center"> 
            <div class = "col-md-12"> 
               <input id = "msgInput" type = "text" placeholder = "message" /> 
               <button id = "sendMsgBtn" class = "btn-success btn">Send</button> 
            </div> 
         </div>
			
      </div> 
		
      <script src = "client.js"></script> 
		
   </body>
 
</html>

Esta página deve ser familiar para você. Adicionamos o arquivo css de bootstrap . Também definimos duas páginas. Por fim, criamos diversos campos de texto e botões para obter informações do usuário. Na página “chat” você deverá ver a tag div com o ID “chatarea” onde todas as nossas mensagens serão exibidas. Observe que adicionamos um link para um arquivo client.js .

Agora precisamos estabelecer uma conexão com nosso servidor de sinalização. Crie o arquivo client.js na pasta raiz com o seguinte código -

//our username 
var name; 
var connectedUser;
  
//connecting to our signaling server 
var conn = new WebSocket('ws://localhost:9090');
  
conn.onopen = function () { 
   console.log("Connected to the signaling server"); 
}; 
 
//when we got a message from a signaling server 
conn.onmessage = function (msg) { 
   console.log("Got message", msg.data);
	
   var data = JSON.parse(msg.data);
	
   switch(data.type) { 
      case "login": 
         handleLogin(data.success); 
         break; 
      //when somebody wants to call us 
      case "offer": 
         handleOffer(data.offer, data.name); 
         break; 
      case "answer": 
         handleAnswer(data.answer); 
         break; 
      //when a remote peer sends an ice candidate to us 
      case "candidate":
         handleCandidate(data.candidate); 
         break; 
      case "leave": 
         handleLeave(); 
         break; 
      default: 
         break; 
   } 
};
  
conn.onerror = function (err) { 
   console.log("Got error", err); 
};
  
//alias for sending JSON encoded messages 
function send(message) { 
   //attach the other peer username to our messages 
   if (connectedUser) { 
      message.name = connectedUser; 
   } 
	
   conn.send(JSON.stringify(message)); 
};

Agora execute nosso servidor de sinalização via servidor de nó . Em seguida, dentro da pasta raiz execute o comando estático e abra a página dentro do navegador. Você deve ver a seguinte saída do console -

A próxima etapa é implementar um login de usuário com um nome de usuário exclusivo. Nós simplesmente enviamos um nome de usuário para o servidor, que nos informa se ele foi usado ou não. Adicione o seguinte código ao seu arquivo client.js -

//****** 
//UI selectors block
//****** 

var loginPage = document.querySelector('#loginPage'); 
var usernameInput = document.querySelector('#usernameInput'); 
var loginBtn = document.querySelector('#loginBtn'); 

var callPage = document.querySelector('#callPage'); 
var callToUsernameInput = document.querySelector('#callToUsernameInput'); 
var callBtn = document.querySelector('#callBtn'); 

var hangUpBtn = document.querySelector('#hangUpBtn'); 
callPage.style.display = "none"; 

// Login when the user clicks the button 
loginBtn.addEventListener("click", function (event) { 
   name = usernameInput.value;
	
   if (name.length > 0) { 
      send({ 
         type: "login", 
         name: name 
      }); 
   } 
	
});
 
function handleLogin(success) { 

   if (success === false) { 
      alert("Ooops...try a different username"); 
   } else { 
      loginPage.style.display = "none"; 
      callPage.style.display = "block"; 
		
      //********************** 
      //Starting a peer connection 
      //********************** 
   } 
	
};

Primeiramente, selecionamos algumas referências aos elementos da página. Em seguida, ocultamos a página de chamada. Em seguida, adicionamos um ouvinte de evento no botão de login. Quando o usuário clica nele, enviamos seu nome de usuário para o servidor. Finalmente, implementamos o callback handleLogin. Se o login for bem-sucedido, mostramos a página de chamada, configuramos uma conexão de par e criamos um canal de dados.

Para iniciar uma conexão de par com um canal de dados, precisamos -

  • Crie o objeto RTCPeerConnection
  • Crie um canal de dados dentro de nosso objeto RTCPeerConnection

Adicione o seguinte código ao “bloco de seletores de IU” -

var msgInput = document.querySelector('#msgInput'); 
var sendMsgBtn = document.querySelector('#sendMsgBtn'); 
var chatArea = document.querySelector('#chatarea'); 

var yourConn; 
var dataChannel;

Modifique a função handleLogin -

function handleLogin(success) { 
   if (success === false) { 
      alert("Ooops...try a different username"); 
   } else { 
      loginPage.style.display = "none"; 
      callPage.style.display = "block";
		
      //********************** 
      //Starting a peer connection 
      //**********************
		
      //using Google public stun server 
      var configuration = { 
         "iceServers": [{ "url": "stun:stun2.1.google.com:19302" }] 
      }; 
		
      yourConn = new webkitRTCPeerConnection(configuration, {optional: [{RtpDataChannels: true}]});
		
      // Setup ice handling 
      yourConn.onicecandidate = function (event) { 
         if (event.candidate) { 
            send({
               type: "candidate", 
               candidate: event.candidate 
            }); 
         } 
      };
		
      //creating data channel 
      dataChannel = yourConn.createDataChannel("channel1", {reliable:true}); 
		
      dataChannel.onerror = function (error) { 
         console.log("Ooops...error:", error); 
      };
		
      //when we receive a message from the other peer, display it on the screen 
      dataChannel.onmessage = function (event) { 
         chatArea.innerHTML += connectedUser + ": " + event.data + "<br />"; 
      };
		
      dataChannel.onclose = function () { 
         console.log("data channel is closed"); 
      };  
   } 
};

Se o login for bem-sucedido, o aplicativo cria o objeto RTCPeerConnection e configura o manipulador onicecandidate que envia todos os icecandidates encontrados para o outro par. Ele também cria um dataChannel. Observe que, ao criar o objeto RTCPeerConnection, o segundo argumento no construtor opcional: [{RtpDataChannels: true}] é obrigatório se você estiver usando o Chrome ou Opera. A próxima etapa é criar uma oferta para o outro par. Depois que o usuário recebe a oferta, ele cria uma resposta e começa a negociar candidatos ICE. Adicione o seguinte código ao arquivo client.js -

//initiating a call
callBtn.addEventListener("click", function () { 
   var callToUsername = callToUsernameInput.value;
	
   if (callToUsername.length > 0) {
	
      connectedUser = callToUsername;
		
      // create an offer 
      yourConn.createOffer(function (offer) { 
		
         send({ 
            type: "offer", 
            offer: offer 
         }); 
			
         yourConn.setLocalDescription(offer); 
			
      }, function (error) { 
         alert("Error when creating an offer"); 
      });  
   } 
});
  
//when somebody sends us an offer 
function handleOffer(offer, name) { 
   connectedUser = name; 
   yourConn.setRemoteDescription(new RTCSessionDescription(offer));
	
   //create an answer to an offer 
   yourConn.createAnswer(function (answer) { 
      yourConn.setLocalDescription(answer); 
		
      send({ 
         type: "answer", 
         answer: answer 
      }); 
		
   }, function (error) { 
      alert("Error when creating an answer"); 
   });
};
  
//when we got an answer from a remote user 
function handleAnswer(answer) { 
   yourConn.setRemoteDescription(new RTCSessionDescription(answer)); 
};
  
//when we got an ice candidate from a remote user 
function handleCandidate(candidate) { 
   yourConn.addIceCandidate(new RTCIceCandidate(candidate)); 
};

Adicionamos um manipulador de cliques ao botão Chamar, que inicia uma oferta. Em seguida, implementamos vários manipuladores esperados pelo manipulador onmessage . Eles serão processados ​​de forma assíncrona até que ambos os usuários tenham feito uma conexão.

A próxima etapa é implementar o recurso de desligamento. Isso interromperá a transmissão de dados e dirá ao outro usuário para fechar o canal de dados. Adicione o seguinte código -

//hang up 
hangUpBtn.addEventListener("click", function () { 
   send({ 
      type: "leave" 
   }); 
	
   handleLeave(); 
}); 
 
function handleLeave() { 
   connectedUser = null; 
   yourConn.close(); 
   yourConn.onicecandidate = null; 
};

Quando o usuário clica no botão Desligar -

  • Ele enviará uma mensagem “deixe” para o outro usuário.
  • Ele fechará o RTCPeerConnection e também o canal de dados.

A última etapa é enviar uma mensagem a outro par. Adicione o manipulador de “clique” ao botão “enviar mensagem” -

//when user clicks the "send message" button 
sendMsgBtn.addEventListener("click", function (event) { 
   var val = msgInput.value; 
   chatArea.innerHTML += name + ": " + val + "<br />"; 
	
   //sending a message to a connected peer 
   dataChannel.send(val); 
   msgInput.value = ""; 
});

Agora execute o código. Você deve ser capaz de fazer login no servidor usando duas guias do navegador. Você pode então configurar uma conexão de par para o outro usuário e enviar-lhe uma mensagem, bem como fechar o canal de dados clicando no botão “Desligar”.

A seguir está o arquivo client.js inteiro -

//our username 
var name; 
var connectedUser; 

//connecting to our signaling server 
var conn = new WebSocket('ws://localhost:9090'); 

conn.onopen = function () { 
   console.log("Connected to the signaling server");
};
 
//when we got a message from a signaling server 
conn.onmessage = function (msg) { 
   console.log("Got message", msg.data); 
   var data = JSON.parse(msg.data); 
	
   switch(data.type) { 
      case "login": 
         handleLogin(data.success); 
         break; 
      //when somebody wants to call us 
      case "offer": 
         handleOffer(data.offer, data.name); 
         break; 
      case "answer": 
         handleAnswer(data.answer); 
         break; 
      //when a remote peer sends an ice candidate to us 
      case "candidate": 
         handleCandidate(data.candidate); 
         break; 
      case "leave": 
         handleLeave(); 
         break; 
      default: 
         break; 
   } 
}; 

conn.onerror = function (err) { 
   console.log("Got error", err); 
}; 

//alias for sending JSON encoded messages 
function send(message) { 

   //attach the other peer username to our messages
   if (connectedUser) { 
      message.name = connectedUser; 
   } 
	
   conn.send(JSON.stringify(message)); 
};
 
//****** 
//UI selectors block 
//****** 

var loginPage = document.querySelector('#loginPage'); 
var usernameInput = document.querySelector('#usernameInput'); 
var loginBtn = document.querySelector('#loginBtn'); 

var callPage = document.querySelector('#callPage'); 
var callToUsernameInput = document.querySelector('#callToUsernameInput');
var callBtn = document.querySelector('#callBtn'); 

var hangUpBtn = document.querySelector('#hangUpBtn'); 
var msgInput = document.querySelector('#msgInput'); 
var sendMsgBtn = document.querySelector('#sendMsgBtn'); 

var chatArea = document.querySelector('#chatarea'); 
var yourConn; 
var dataChannel; 
callPage.style.display = "none"; 

// Login when the user clicks the button 
loginBtn.addEventListener("click", function (event) { 
   name = usernameInput.value; 
	
   if (name.length > 0) { 
      send({ 
         type: "login", 
         name: name 
      }); 
   } 
	
});
 
function handleLogin(success) { 

   if (success === false) {
      alert("Ooops...try a different username"); 
   } else { 
      loginPage.style.display = "none"; 
      callPage.style.display = "block"; 
		
      //********************** 
      //Starting a peer connection 
      //********************** 
		
      //using Google public stun server 
      var configuration = { 
         "iceServers": [{ "url": "stun:stun2.1.google.com:19302" }] 
      }; 
		
      yourConn = new webkitRTCPeerConnection(configuration, {optional: [{RtpDataChannels: true}]}); 
		
      // Setup ice handling 
      yourConn.onicecandidate = function (event) { 
         if (event.candidate) { 
            send({ 
               type: "candidate", 
               candidate: event.candidate 
            }); 
         } 
      }; 
		
      //creating data channel 
      dataChannel = yourConn.createDataChannel("channel1", {reliable:true}); 
		
      dataChannel.onerror = function (error) { 
         console.log("Ooops...error:", error); 
      }; 
		
      //when we receive a message from the other peer, display it on the screen 
      dataChannel.onmessage = function (event) { 
         chatArea.innerHTML += connectedUser + ": " + event.data + "<br />"; 
      }; 
		
      dataChannel.onclose = function () { 
         console.log("data channel is closed"); 
      };
		
   } 
};
 
//initiating a call 
callBtn.addEventListener("click", function () { 
   var callToUsername = callToUsernameInput.value; 
	
   if (callToUsername.length > 0) { 
      connectedUser = callToUsername; 
      // create an offer 
      yourConn.createOffer(function (offer) { 
         send({ 
            type: "offer", 
            offer: offer 
         }); 
         yourConn.setLocalDescription(offer); 
      }, function (error) { 
         alert("Error when creating an offer"); 
      }); 
   } 
	
});
 
//when somebody sends us an offer 
function handleOffer(offer, name) { 
   connectedUser = name; 
   yourConn.setRemoteDescription(new RTCSessionDescription(offer)); 
	
   //create an answer to an offer 
   yourConn.createAnswer(function (answer) { 
      yourConn.setLocalDescription(answer); 
      send({ 
         type: "answer", 
         answer: answer 
      }); 
   }, function (error) { 
      alert("Error when creating an answer"); 
   });
	
};
 
//when we got an answer from a remote user 
function handleAnswer(answer) { 
   yourConn.setRemoteDescription(new RTCSessionDescription(answer)); 
};
 
//when we got an ice candidate from a remote user 
function handleCandidate(candidate) { 
   yourConn.addIceCandidate(new RTCIceCandidate(candidate)); 
};
 
//hang up 
hangUpBtn.addEventListener("click", function () { 
   send({ 
      type: "leave" 
   }); 
	
   handleLeave(); 
}); 

function handleLeave() { 
   connectedUser = null; 
   yourConn.close(); 
   yourConn.onicecandidate = null; 
};
 
//when user clicks the "send message" button 
sendMsgBtn.addEventListener("click", function (event) { 
   var val = msgInput.value; 
   chatArea.innerHTML += name + ": " + val + "<br />"; 
	
   //sending a message to a connected peer 
   dataChannel.send(val); 
   msgInput.value = ""; 
});

Neste capítulo, vamos adicionar recursos de segurança ao servidor de sinalização que criamos no capítulo “Sinalização WebRTC”. Haverá dois aprimoramentos -

  • Autenticação de usuário usando banco de dados Redis
  • Ativando conexão de soquete seguro

Em primeiro lugar, você deve instalar o Redis.

  • Baixe a última versão estável em http://redis.io/download(3,05 no meu caso)

  • Descompacte-o

  • Dentro da pasta baixada, execute sudo make install

  • Após o término da instalação, execute make test para verificar se tudo está funcionando corretamente.

O Redis tem dois comandos executáveis ​​-

  • redis-cli - interface de linha de comando para Redis (parte cliente)

  • redis-server - Armazenamento de dados Redis

Para executar o servidor Redis, digite redis-server no console do terminal. Você deve ver o seguinte -

Agora abra uma nova janela de terminal e execute redis-cli para abrir um aplicativo cliente.

Basicamente, o Redis é um banco de dados de valores-chave. Para criar uma chave com um valor de string, você deve usar o comando SET. Para ler o valor da chave, você deve usar o comando GET. Vamos adicionar dois usuários e senhas para eles. As chaves serão os nomes de usuário e os valores dessas chaves serão as senhas correspondentes.

Agora devemos modificar nosso servidor de sinalização para adicionar uma autenticação de usuário. Adicione o seguinte código ao topo do arquivo server.js -

//require the redis library in Node.js 
var redis = require("redis");
 
//creating the redis client object 
var redisClient = redis.createClient();

No código acima, exigimos a biblioteca Redis para Node.js e a criação de um cliente redis para nosso servidor.

Para adicionar a autenticação, modifique o manipulador de mensagens no objeto de conexão -

//when a user connects to our sever 
wss.on('connection', function(connection) { 
   console.log("user connected");
	
   //when server gets a message from a connected user 
   connection.on('message', function(message) { 
	
      var data; 
      //accepting only JSON messages 
      try { 
         data = JSON.parse(message); 
      } catch (e) { 
         console.log("Invalid JSON"); 
         data = {}; 
      }
		
      //check whether a user is authenticated 
      if(data.type != "login") { 
		
         //if user is not authenticated 
         if(!connection.isAuth) { 
            sendTo(connection, { 
               type: "error", 
               message: "You are not authenticated" 
            }); 
            return; 
         } 
      } 
		
      //switching type of the user message 
      switch (data.type) { 
         //when a user tries to login 
         case "login": 
            console.log("User logged:", data.name); 
            //get password for this username from redis database 
				
            redisClient.get(data.name, function(err, reply) {  
               //check if password matches with the one stored in redis 
               var loginSuccess = reply === data.password;
				
               //if anyone is logged in with this username or incorrect password 
                  then refuse 
               if(users[data.name] || !loginSuccess) { 
                  sendTo(connection, { 
                     type: "login", 
                     success: false 
                  }); 
               } else { 
                  //save user connection on the server 
                  users[data.name] = connection; 
                  connection.name = data.name;
                  connection.isAuth = true; 
						
                  sendTo(connection, { 
                     type: "login", 
                     success: true 
                  }); 
               }  
            }); 
				
            break;
      }
   });
	
}		
		
//... 
//*****other handlers*******

No código acima se um usuário tenta fazer o login obtemos do Redis sua senha, verificamos se ela coincide com a armazenada, e se obtiver sucesso armazenamos seu nome de usuário no servidor. Também adicionamos o sinalizador isAuth à conexão para verificar se o usuário está autenticado. Observe este código -

//check whether a user is authenticated 
if(data.type != "login") { 

   //if user is not authenticated 
   if(!connection.isAuth) { 
      sendTo(connection, { 
         type: "error", 
         message: "You are not authenticated" 
      });
		
      return; 
   } 
}

Se um usuário não autenticado tentar enviar a oferta ou sair da conexão, simplesmente enviamos um erro de volta.

A próxima etapa é habilitar uma conexão de soquete segura. É altamente recomendado para aplicativos WebRTC. PKI (Public Key Infrastructure) é uma assinatura digital de uma CA (Autoridade de Certificação). Os usuários então verificam se a chave privada usada para assinar um certificado corresponde à chave pública do certificado da CA. Para fins de desenvolvimento. usaremos um certificado de segurança autoassinado.

Usaremos o openssl. É uma ferramenta de código aberto que implementa os protocolos SSL (Secure Sockets Layer) e TLS (Transport Layer Security). Geralmente é instalado por padrão em sistemas Unix. Execute o openssl version -a para verificar se ele está instalado.

Para gerar chaves de certificado de segurança pública e privada, você deve seguir as etapas abaixo -

  • Generate a temporary server password key

openssl genrsa -des3 -passout pass:x -out server.pass.key 2048

  • Generate a server private key

openssl rsa -passin pass:12345 -in server.pass.key -out server.key

  • Generate a signing request. You will be asked additional questions about your company. Just hit the “Enter” button all the time.

openssl req -new -key server.key -out server.csr

  • Generate the certificate

openssl x509 -req -days 1095 -in server.csr -signkey server.key -out server.crt

Agora você tem dois arquivos, o certificado (server.crt) e a chave privada (server.key). Copie-os para a pasta raiz do servidor de sinalização.

Para habilitar a conexão de soquete seguro, modifique nosso servidor de sinalização.

//require file system module 
var fs = require('fs'); 
var httpServ = require('https');
  
//https://github.com/visionmedia/superagent/issues/205 
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
  
//out secure server will bind to the port 9090 
var cfg = { 
   port: 9090, 
   ssl_key: 'server.key', 
   ssl_cert: 'server.crt' 
};
  
//in case of http request just send back "OK" 
var processRequest = function(req, res) { 
   res.writeHead(200); 
   res.end("OK"); 
};
  
//create our server with SSL enabled 
var app = httpServ.createServer({ 
   key: fs.readFileSync(cfg.ssl_key), 
   cert: fs.readFileSync(cfg.ssl_cert) 
}, processRequest).listen(cfg.port);
	
//require our websocket library 
var WebSocketServer = require('ws').Server; 

//creating a websocket server at port 9090 
var wss = new WebSocketServer({server: app}); 

//all connected to the server users 
var users = {};
  
//require the redis library in Node.js
var redis = require("redis"); 

//creating the redis client object 
var redisClient = redis.createClient(); 
 
//when a user connects to our sever 
wss.on('connection', function(connection){ 
//...other code

No código acima, exigimos que a biblioteca fs leia a chave privada e o certificado, crie o objeto cfg com a porta de ligação e os caminhos para a chave privada e o certificado. Em seguida, criamos um servidor HTTPS com nossas chaves junto com o servidor WebSocket na porta 9090.

Agora aberto https://localhost:9090no Opera. Você deve ver o seguinte -

Clique no botão “continuar assim mesmo”. Você deverá ver a mensagem “OK”.

Para testar nosso servidor de sinalização seguro, modificaremos o aplicativo de chat que criamos no tutorial “WebRTC Text Demo”. Só precisamos adicionar um campo de senha. A seguir está o arquivo index.html completo -

<html>
  
   <head> 
      <title>WebRTC Text Demo</title>  
      <link rel = "stylesheet" href = "node_modules/bootstrap/dist/css/bootstrap.min.css"/>  
   </head> 

   <style>  
      body { 
         background: #eee; 
         padding: 5% 0; 
      }  
   </style>
	
   <body>  
      <div id = "loginPage" class = "container text-center"> 
		
         <div class = "row"> 
            <div class = "col-md-4 col-md-offset-4">  
               <h2>WebRTC Text Demo. Please sign in</h2> 
               <label for = "usernameInput" class = "sr-only">Login</label> 
               <input type = "email" id = "usernameInput" 
                  class = "form-control formgroup" placeholder = "Login" 
                  required = "" autofocus = ""> 
               <input type = "text" id = "passwordInput" 
                  class = "form-control form-group" placeholder = "Password"
                  required = "" autofocus = ""> 
               <button id = "loginBtn" class = "btn btn-lg btn-primary btnblock"
                  >Sign in</button>  
            </div> 
         </div> 
			
      </div> 
		
      <div id = "callPage" class = "call-page container">
		
         <div class = "row"> 
            <div class = "col-md-4 col-md-offset-4 text-center"> 
               <div class = "panel panel-primary"> 
                  <div class = "panel-heading">Text chat</div> 
                  <div id = "chatarea" class = "panel-body text-left"></div> 
               </div> 
            </div> 
         </div>
			
         <div class = "row text-center form-group"> 
            <div class = "col-md-12"> 
               <input id = "callToUsernameInput" type = "text" 
                  placeholder = "username to call" /> 
               <button id = "callBtn" class = "btn-success btn">Call</button> 
               <button id = "hangUpBtn" class = "btn-danger btn">Hang Up</button> 
            </div> 
         </div>
			
         <div class = "row text-center"> 
            <div class = "col-md-12"> 
               <input id = "msgInput" type = "text" placeholder = "message" /> 
               <button id = "sendMsgBtn" class = "btn-success btn">Send</button> 
            </div> 
         </div>
			
      </div>  
		
      <script src = "client.js"></script> 
  
   </body> 
	
</html>

Também precisamos habilitar uma conexão de soquete segura no arquivo client.js por meio desta linha var conn = new WebSocket ('wss: // localhost: 9090'); . Observe o protocolo wss . Então, o manipulador do botão de login deve ser modificado para enviar a senha junto com o nome de usuário -

loginBtn.addEventListener("click", function (event) { 
   name = usernameInput.value; 
   var pwd = passwordInput.value;
	
   if (name.length > 0) { 
      send({ 
         type: "login", 
         name: name, 
         password: pwd 
      }); 
   } 
	
});

A seguir está o arquivo client.js inteiro -

//our username 
var name; 
var connectedUser;
  
//connecting to our signaling server 
var conn = new WebSocket('wss://localhost:9090');
  
conn.onopen = function () { 
   console.log("Connected to the signaling server"); 
};
  
//when we got a message from a signaling server 
conn.onmessage = function (msg) { 
   console.log("Got message", msg.data);
	
   var data = JSON.parse(msg.data);
	
   switch(data.type) { 
      case "login": 
         handleLogin(data.success); 
         break; 
      //when somebody wants to call us 
      case "offer":
         handleOffer(data.offer, data.name); 
         break; 
      case "answer": 
         handleAnswer(data.answer); 
         break; 
      //when a remote peer sends an ice candidate to us 
      case "candidate": 
         handleCandidate(data.candidate); 
         break; 
      case "leave": 
         handleLeave(); 
         break; 
      default: 
         break; 
   } 
};
  
conn.onerror = function (err) { 
   console.log("Got error", err); 
};  

//alias for sending JSON encoded messages 
function send(message) { 
   //attach the other peer username to our messages 
   if (connectedUser) { 
      message.name = connectedUser; 
   } 
	
   conn.send(JSON.stringify(message)); 
}; 
 
//****** 
//UI selectors block 
//******

var loginPage = document.querySelector('#loginPage'); 
var usernameInput = document.querySelector('#usernameInput'); 
var passwordInput = document.querySelector('#passwordInput'); 
var loginBtn = document.querySelector('#loginBtn'); 

var callPage = document.querySelector('#callPage'); 
var callToUsernameInput = document.querySelector('#callToUsernameInput');
var callBtn = document.querySelector('#callBtn'); 
var hangUpBtn = document.querySelector('#hangUpBtn');
  
var msgInput = document.querySelector('#msgInput'); 
var sendMsgBtn = document.querySelector('#sendMsgBtn'); 
var chatArea = document.querySelector('#chatarea'); 

var yourConn; 
var dataChannel;
  
callPage.style.display = "none";
  
// Login when the user clicks the button 
loginBtn.addEventListener("click", function (event) { 
   name = usernameInput.value; 
   var pwd = passwordInput.value;  
	
   if (name.length > 0) { 
      send({ 
         type: "login", 
         name: name, 
         password: pwd 
      }); 
   } 
	
}); 
 
function handleLogin(success) { 
   if (success === false) {
      alert("Ooops...incorrect username or password"); 
   } else { 
      loginPage.style.display = "none"; 
      callPage.style.display = "block";
		
      //********************** 
      //Starting a peer connection 
      //********************** 
		
      //using Google public stun server 
      var configuration = { 
         "iceServers": [{ "url": "stun:stun2.1.google.com:19302" }] 
      }; 
		
      yourConn = new webkitRTCPeerConnection(configuration, {optional: [{RtpDataChannels: true}]}); 
		
      // Setup ice handling 
      yourConn.onicecandidate = function (event) { 
         if (event.candidate) { 
            send({ 
               type: "candidate", 
               candidate: event.candidate 
            }); 
         } 
      };
		
      //creating data channel 
      dataChannel = yourConn.createDataChannel("channel1", {reliable:true}); 
		
      dataChannel.onerror = function (error) { 
         console.log("Ooops...error:", error); 
      };
		
      //when we receive a message from the other peer, display it on the screen 
      dataChannel.onmessage = function (event) { 
         chatArea.innerHTML += connectedUser + ": " + event.data + "<br />"; 
      };  
      dataChannel.onclose = function () { 
         console.log("data channel is closed"); 
      };  
   } 
	
};
  
//initiating a call 
callBtn.addEventListener("click", function () { 
   var callToUsername = callToUsernameInput.value;
	
   if (callToUsername.length > 0) {
	
      connectedUser = callToUsername;
		
      // create an offer 
      yourConn.createOffer(function (offer) { 
         send({ 
            type: "offer", 
            offer: offer 
         }); 
			
         yourConn.setLocalDescription(offer); 
			
      }, function (error) { 
         alert("Error when creating an offer"); 
      });  
   } 
});
 
//when somebody sends us an offer 
function handleOffer(offer, name) { 
   connectedUser = name; 
   yourConn.setRemoteDescription(new RTCSessionDescription(offer));
	
   //create an answer to an offer 
   yourConn.createAnswer(function (answer) { 
      yourConn.setLocalDescription(answer); 
		
      send({ 
         type: "answer", 
         answer: answer 
      }); 
		
   }, function (error) { 
      alert("Error when creating an answer"); 
   }); 
	
};
  
//when we got an answer from a remote user 
function handleAnswer(answer) { 
   yourConn.setRemoteDescription(new RTCSessionDescription(answer)); 
};
  
//when we got an ice candidate from a remote user 
function handleCandidate(candidate) { 
   yourConn.addIceCandidate(new RTCIceCandidate(candidate)); 
};
   
//hang up 
hangUpBtn.addEventListener("click", function () { 

   send({ 
      type: "leave"
   }); 
	
   handleLeave(); 
});
  
function handleLeave() { 
   connectedUser = null; 
   yourConn.close(); 
   yourConn.onicecandidate = null; 
}; 
 
//when user clicks the "send message" button 
sendMsgBtn.addEventListener("click", function (event) { 
   var val = msgInput.value; 
   chatArea.innerHTML += name + ": " + val + "<br />"; 
	
   //sending a message to a connected peer 
   dataChannel.send(val); 
   msgInput.value = ""; 
});

Agora execute nosso servidor de sinalização seguro via servidor de nó . Execute node static dentro da pasta modificada de demonstração de chat. Abrirlocalhost:8080em duas guias do navegador. Tente fazer o login. Lembre-se de que apenas “usuário1” com “senha1” e “usuário2” com “senha2” têm permissão para fazer o login. Em seguida, estabeleça o RTCPeerConnection (chame outro usuário) e tente enviar uma mensagem.

A seguir está todo o código do nosso servidor de sinalização seguro -

//require file system module 
var fs = require('fs'); 
var httpServ = require('https');

//https://github.com/visionmedia/superagent/issues/205 
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
  
//out secure server will bind to the port 9090 
var cfg = { 
   port: 9090, 
   ssl_key: 'server.key', 
   ssl_cert: 'server.crt' 
};
  
//in case of http request just send back "OK" 
var processRequest = function(req, res){ 
   res.writeHead(200); 
   res.end("OK"); 
};
  
//create our server with SSL enabled 
var app = httpServ.createServer({ 
   key: fs.readFileSync(cfg.ssl_key), 
   cert: fs.readFileSync(cfg.ssl_cert) 
}, processRequest).listen(cfg.port);
  
//require our websocket library 
var WebSocketServer = require('ws').Server; 

//creating a websocket server at port 9090 
var wss = new WebSocketServer({server: app}); 

//all connected to the server users 
var users = {};
  
//require the redis library in Node.js 
var redis = require("redis"); 

//creating the redis client object 
var redisClient = redis.createClient();

//when a user connects to our sever 
wss.on('connection', function(connection) { 
   console.log("user connected"); 
	
   //when server gets a message from a connected user 
   connection.on('message', function(message) {  
	
      var data; 
      //accepting only JSON messages 
      try { 
         data = JSON.parse(message); 
      } catch (e) { 
         console.log("Invalid JSON"); 
         data = {}; 
      } 
		
      //check whether a user is authenticated 
      if(data.type != "login") { 
         //if user is not authenticated 
         if(!connection.isAuth) { 
            sendTo(connection, { 
               type: "error", 
               message: "You are not authenticated" 
            }); 
				
            return; 
         } 
      }
		
      //switching type of the user message 
      switch (data.type) { 
         //when a user tries to login 
         case "login":
            console.log("User logged:", data.name); 
            //get password for this username from redis database 
            redisClient.get(data.name, function(err, reply) {
			  
               //check if password matches with the one stored in redis 
               var loginSuccess = reply === data.password;
				  
               //if anyone is logged in with this username or incorrect password 
                  then refuse 
               if(users[data.name] || !loginSuccess) { 
                  sendTo(connection, { 
                     type: "login", 
                     success: false 
                  }); 
               } else { 
                  //save user connection on the server 
                  users[data.name] = connection; 
                  connection.name = data.name; 
                  connection.isAuth = true; 
						
                  sendTo(connection, { 
                     type: "login", 
                     success: true 
                  }); 
               }  
            }); 
				
            break;
				
         case "offer": 
            //for ex. UserA wants to call UserB 
            console.log("Sending offer to: ", data.name); 
				
            //if UserB exists then send him offer details 
            var conn = users[data.name];
				
            if(conn != null) { 
               //setting that UserA connected with UserB 
               connection.otherName = data.name;
					
               sendTo(conn, { 
                  type: "offer", 
                  offer: data.offer, 
                  name: connection.name 
               }); 
            } 
				
            break;
				
         case "answer": 
            console.log("Sending answer to: ", data.name); 
            //for ex. UserB answers UserA 
            var conn = users[data.name]; 
				
            if(conn != null) { 
               connection.otherName = data.name;
					
               sendTo(conn, { 
                  type: "answer", 
                  answer: data.answer 
               }); 
            } 
				
            break;
				
         case "candidate": 
            console.log("Sending candidate to:",data.name); 
            var conn = users[data.name];
				
            if(conn != null) { 
               sendTo(conn, { 
                  type: "candidate", 
                  candidate: data.candidate 
               });
            } 
				
            break;
				
         case "leave": 
            console.log("Disconnecting from", data.name); 
            var conn = users[data.name]; 
            conn.otherName = null; 
				
            //notify the other user so he can disconnect his peer connection 
            if(conn != null) { 
               sendTo(conn, { 
                  type: "leave" 
               }); 
            }  
				
            break;
				
         connection.on("close", function() {
			
            if(connection.name) { 
               delete users[connection.name]; 
					
               if(connection.otherName) { 
                  console.log("Disconnecting from ", connection.otherName); 
                  var conn = users[connection.otherName]; 
                  conn.otherName = null;  
						
                  if(conn != null) { 
                     sendTo(conn, { 
                        type: "leave" 
                    }); 
                  } 
						
               } 
            } 
         });
			
         default: 
            sendTo(connection, { 
               type: "error", 
               message: "Command no found: " + data.type 
            }); 
				
            break; 
      }  
   });
	
   //when user exits, for example closes a browser window 
   //this may help if we are still in "offer","answer" or "candidate" state 
   connection.on("close", function() { 
      if(connection.name) { 
         delete users[connection.name]; 
      } 
   });
	
   connection.send("Hello from server"); 
});
  
function sendTo(connection, message) { 
   connection.send(JSON.stringify(message)); 
}

Resumo

Neste capítulo, adicionamos a autenticação do usuário ao nosso servidor de sinalização. Também aprendemos como criar certificados SSL autoassinados e usá-los no escopo de aplicativos WebRTC.