WebRTC - Sinalização

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 configura 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 WebSockets HTML5 - uma conexão de soquete bidirecional entre dois pontos de extremidade - 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 na conexão direta com o servidor WebSocket e testa 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 = {}; 
   } 
	
});

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

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, elas 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 candidatos -

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