WebRTC - Señalización

La mayoría de las aplicaciones WebRTC no solo se pueden comunicar a través de video y audio. Necesitan muchas otras funciones. En este capítulo, vamos a construir un servidor de señalización básico.

Señalización y negociación

Para conectarse con otro usuario debe saber dónde se encuentra en la Web. La dirección IP de su dispositivo permite que los dispositivos habilitados para Internet se envíen datos directamente entre sí. El objeto RTCPeerConnection es responsable de esto. Tan pronto como los dispositivos saben cómo encontrarse entre sí a través de Internet, comienzan a intercambiar datos sobre qué protocolos y códecs admite cada dispositivo.

Para comunicarse con otro usuario, simplemente necesita intercambiar información de contacto y el resto lo hará WebRTC. El proceso de conexión con el otro usuario también se conoce como señalización y negociación. Consta de unos pocos pasos:

  • Cree una lista de posibles candidatos para una conexión entre pares.

  • El usuario o una aplicación selecciona un usuario para establecer una conexión.

  • La capa de señalización notifica a otro usuario que alguien quiere conectarse con él. Puede aceptarlo o rechazarlo.

  • Se notifica al primer usuario la aceptación de la oferta.

  • El primer usuario inicia RTCPeerConnection con otro usuario.

  • Ambos usuarios intercambian información de software y hardware a través del servidor de señalización.

  • Ambos usuarios intercambian información de ubicación.

  • La conexión tiene éxito o falla.

La especificación WebRTC no contiene ningún estándar sobre el intercambio de información. Así que tenga en cuenta que lo anterior es solo un ejemplo de cómo puede ocurrir la señalización. Puede utilizar cualquier protocolo o tecnología que desee.

Construyendo el servidor

El servidor que vamos a construir podrá conectar a dos usuarios juntos que no estén ubicados en la misma computadora. Crearemos nuestro propio mecanismo de señalización. Nuestro servidor de señalización permitirá que un usuario llame a otro. Una vez que un usuario ha llamado a otro, el servidor pasa la oferta, responde, candidatos ICE entre ellos y establece una conexión WebRTC.

El diagrama anterior es el flujo de mensajes entre usuarios cuando utilizan el servidor de señalización. En primer lugar, cada usuario se registra en el servidor. En nuestro caso, será un nombre de usuario de cadena simple. Una vez que los usuarios se han registrado, pueden llamarse entre sí. El usuario 1 hace una oferta con el identificador de usuario al que desea llamar. El otro usuario debería responder. Finalmente, los candidatos de ICE se envían entre usuarios hasta que puedan establecer una conexión.

Para crear una conexión WebRTC, los clientes deben poder transferir mensajes sin utilizar una conexión de pares WebRTC. Aquí es donde usaremos HTML5 WebSockets, una conexión de socket bidireccional entre dos puntos finales, un servidor web y un navegador web. Ahora comencemos a usar la biblioteca WebSocket. Cree el archivo server.js e inserte el siguiente 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"); 
});

La primera línea requiere la biblioteca WebSocket que ya hemos instalado. Luego creamos un servidor de socket en el puerto 9090. Luego, escuchamos el evento de conexión . Este código se ejecutará cuando un usuario establezca una conexión WebSocket al servidor. Luego escuchamos los mensajes enviados por el usuario. Finalmente, enviamos una respuesta al usuario conectado diciendo "Hola desde el servidor".

Ahora ejecute el servidor de nodo y el servidor debería comenzar a escuchar conexiones de socket.

Para probar nuestro servidor, usaremos la utilidad wscat que también tenemos instalada. Esta herramienta ayuda a conectarse directamente al servidor WebSocket y a probar los comandos. Ejecute nuestro servidor en una ventana de terminal, luego abra otra y ejecute el comando wscat -c ws: // localhost: 9090 . Debería ver lo siguiente en el lado del cliente:

El servidor también debe registrar al usuario conectado:

registro de usuario

En nuestro servidor de señalización, usaremos un nombre de usuario basado en cadenas para cada conexión para que sepamos dónde enviar mensajes. Cambiemos un poco nuestro controlador de conexión :

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

De esta forma solo aceptamos mensajes JSON. A continuación, debemos almacenar a todos los usuarios conectados en algún lugar. Usaremos un objeto Javascript simple para ello. Cambie la parte superior de nuestro archivo -

//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 a agregar un campo de tipo para cada mensaje proveniente del cliente. Por ejemplo, si un usuario desea iniciar sesión, envía el mensaje de tipo de inicio de sesión . Vamos a definirlo -

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

Si el usuario envía un mensaje con el tipo de inicio de sesión , nosotros:

  • Compruebe si alguien ya ha iniciado sesión con este nombre de usuario

  • Si es así, dígale al usuario que no ha iniciado sesión correctamente.

  • Si nadie está usando este nombre de usuario, agregamos el nombre de usuario como clave para el objeto de conexión.

  • Si no se reconoce un comando, enviamos un error.

El siguiente código es una función auxiliar para enviar mensajes a una conexión. Añadirlo a la server.js archivo -

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

La función anterior asegura que todos nuestros mensajes se envíen en formato JSON.

Cuando el usuario se desconecta debemos limpiar su conexión. Podemos eliminar al usuario cuando se dispara el evento de cierre . Agregue el siguiente código al controlador de conexión :

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

Ahora probemos nuestro servidor con el comando de inicio de sesión. Tenga en cuenta que todos los mensajes deben estar codificados en formato JSON. Ejecute nuestro servidor e intente iniciar sesión. Deberías ver algo como esto:

Haciendo una llamada

Después de iniciar sesión correctamente, el usuario desea llamar a otro. Debería hacer una oferta a otro usuario para lograrlo. Agregar el controlador de ofertas :

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;

En primer lugar, obtenemos la conexión del usuario al que intentamos llamar. Si existe le enviamos los detalles de la oferta . También agregamos otherName al objeto de conexión . Esto se hace por la simplicidad de encontrarlo más tarde.

Respondiendo

Responder a la respuesta tiene un patrón similar al que usamos en el controlador de ofertas . Nuestro servidor simplemente pasa todos los mensajes como respuesta a otro usuario. Agregue el siguiente código después de la mano 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;

Puede ver cómo esto es similar al controlador de ofertas . Observe que este código sigue a las funciones createOffer y createAnswer en el objeto RTCPeerConnection .

Ahora podemos probar nuestro mecanismo de oferta / respuesta. Conecte dos clientes al mismo tiempo e intente hacer una oferta y una respuesta. Debería ver lo siguiente:

En este ejemplo, offer y answer son cadenas simples, pero en una aplicación real se completarán con los datos SDP.

Candidatos ICE

La parte final es el manejo del candidato ICE entre usuarios. Usamos la misma técnica simplemente pasando mensajes entre usuarios. La principal diferencia es que los mensajes candidatos pueden aparecer varias veces por usuario en cualquier orden. Agregar el controlador 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;

Debería funcionar de manera similar a los controladores de ofertas y respuestas .

Dejando la conexión

Para permitir que nuestros usuarios se desconecten de otro usuario, debemos implementar la función de colgar. También le indicará al servidor que elimine todas las referencias de usuario. Añade elleave manejador -

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;

Esto también enviará al otro usuario el evento de permiso para que pueda desconectar su conexión de igual en consecuencia. También deberíamos manejar el caso cuando un usuario pierde su conexión desde el servidor de señalización. Modifiquemos nuestro controlador cercano -

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

Ahora, si la conexión termina, nuestros usuarios serán desconectados. El evento de cierre se activará cuando un usuario cierre la ventana de su navegador mientras todavía estamos en estado de oferta , respuesta o candidato .

Servidor de señalización completo

Aquí está el código completo de nuestro servidor de señalización:

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

Así que el trabajo está hecho y nuestro servidor de señalización está listo. Recuerde que hacer cosas fuera de orden al realizar una conexión WebRTC puede causar problemas.

Resumen

En este capítulo, creamos un servidor de señalización simple y directo. Recorrimos el proceso de señalización, el registro de usuarios y el mecanismo de oferta / respuesta. También implementamos el envío de candidatos entre usuarios.