WebRTC - Signalisation

La plupart des applications WebRTC ne sont pas seulement capables de communiquer par vidéo et audio. Ils ont besoin de nombreuses autres fonctionnalités. Dans ce chapitre, nous allons construire un serveur de signalisation de base.

Signalisation et négociation

Pour vous connecter à un autre utilisateur, vous devez savoir où il se trouve sur le Web. L'adresse IP de votre appareil permet aux appareils compatibles Internet d'envoyer des données directement entre eux. L' objet RTCPeerConnection en est responsable. Dès que les appareils savent comment se trouver sur Internet, ils commencent à échanger des données sur les protocoles et codecs pris en charge par chaque appareil.

Pour communiquer avec un autre utilisateur, vous devez simplement échanger des informations de contact et le reste sera fait par WebRTC. Le processus de connexion à l'autre utilisateur est également appelé signalisation et négociation. Il se compose de quelques étapes -

  • Créez une liste de candidats potentiels pour une connexion homologue.

  • L'utilisateur ou une application sélectionne un utilisateur avec lequel établir une connexion.

  • La couche de signalisation informe un autre utilisateur que quelqu'un souhaite se connecter à lui. Il peut accepter ou refuser.

  • Le premier utilisateur est informé de l'acceptation de l'offre.

  • Le premier utilisateur lance RTCPeerConnection avec un autre utilisateur.

  • Les deux utilisateurs échangent des informations logicielles et matérielles via le serveur de signalisation.

  • Les deux utilisateurs échangent des informations de localisation.

  • La connexion réussit ou échoue.

La spécification WebRTC ne contient aucune norme sur l'échange d'informations. Gardez donc à l'esprit que ce qui précède n'est qu'un exemple de la façon dont la signalisation peut se produire. Vous pouvez utiliser n'importe quel protocole ou technologie de votre choix.

Construire le serveur

Le serveur que nous allons construire pourra relier deux utilisateurs qui ne se trouvent pas sur le même ordinateur. Nous créerons notre propre mécanisme de signalisation. Notre serveur de signalisation permettra à un utilisateur d'en appeler un autre. Une fois qu'un utilisateur en a appelé un autre, le serveur transmet l'offre, la réponse, les candidats ICE entre eux et établit une connexion WebRTC.

Le diagramme ci-dessus est le flux de messagerie entre les utilisateurs lors de l'utilisation du serveur de signalisation. Tout d'abord, chaque utilisateur s'inscrit auprès du serveur. Dans notre cas, ce sera un nom d'utilisateur de chaîne simple. Une fois que les utilisateurs se sont inscrits, ils peuvent s'appeler. L'utilisateur 1 fait une offre avec l'identifiant de l'utilisateur qu'il souhaite appeler. L'autre utilisateur doit répondre. Enfin, les candidats ICE sont envoyés entre les utilisateurs jusqu'à ce qu'ils puissent établir une connexion.

Pour créer une connexion WebRTC, les clients doivent pouvoir transférer des messages sans utiliser de connexion homologue WebRTC. C'est là que nous utiliserons HTML5 WebSockets - une connexion socket bidirectionnelle entre deux points de terminaison - un serveur Web et un navigateur Web. Commençons maintenant à utiliser la bibliothèque WebSocket. Créez le fichier server.js et insérez le code suivant -

//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 première ligne nécessite la bibliothèque WebSocket que nous avons déjà installée. Ensuite, nous créons un serveur socket sur le port 9090. Ensuite, nous écoutons l' événement de connexion . Ce code sera exécuté lorsqu'un utilisateur établit une connexion WebSocket avec le serveur. Nous écoutons ensuite tous les messages envoyés par l'utilisateur. Enfin, nous envoyons une réponse à l'utilisateur connecté en disant «Bonjour du serveur».

Exécutez maintenant le serveur de nœuds et le serveur devrait commencer à écouter les connexions socket.

Pour tester notre serveur, nous utiliserons l' utilitaire wscat que nous avons également déjà installé. Cet outil permet de se connecter directement au serveur WebSocket et de tester les commandes. Exécutez notre serveur dans une fenêtre de terminal, puis ouvrez une autre et exécutez la commande wscat -c ws: // localhost: 9090 . Vous devriez voir ce qui suit du côté client -

Le serveur doit également enregistrer l'utilisateur connecté -

Enregistrement de l'utilisateur

Dans notre serveur de signalisation, nous utiliserons un nom d'utilisateur basé sur une chaîne pour chaque connexion afin que nous sachions où envoyer les messages. Modifions un peu notre gestionnaire de connexion -

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

De cette façon, nous n'acceptons que les messages JSON. Ensuite, nous devons stocker tous les utilisateurs connectés quelque part. Nous utiliserons pour cela un simple objet Javascript. Changer le haut de notre fichier -

//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 = {};

Nous allons ajouter un champ de type pour chaque message provenant du client. Par exemple, si un utilisateur souhaite se connecter, il envoie le message de type de connexion . Définissons-le -

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 l'utilisateur envoie un message avec le type de connexion , nous -

  • Vérifiez si quelqu'un s'est déjà connecté avec ce nom d'utilisateur

  • Si tel est le cas, dites à l'utilisateur qu'il ne s'est pas connecté avec succès

  • Si personne n'utilise ce nom d'utilisateur, nous ajoutons le nom d'utilisateur comme clé à l'objet de connexion.

  • Si une commande n'est pas reconnue, nous envoyons une erreur.

Le code suivant est une fonction d'assistance pour l'envoi de messages à une connexion. Ajoutez-le au fichier server.js -

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

La fonction ci-dessus garantit que tous nos messages sont envoyés au format JSON.

Lorsque l'utilisateur se déconnecte, nous devons nettoyer sa connexion. Nous pouvons supprimer l'utilisateur lorsque l' événement de fermeture est déclenché. Ajoutez le code suivant au gestionnaire de connexion -

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

Testons maintenant notre serveur avec la commande login. Gardez à l'esprit que tous les messages doivent être encodés au format JSON. Exécutez notre serveur et essayez de vous connecter. Vous devriez voir quelque chose comme ça -

Passer un appel

Après une connexion réussie, l'utilisateur souhaite en appeler un autre. Il doit faire une offre à un autre utilisateur pour y parvenir. Ajouter le gestionnaire d' offres -

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;

Premièrement, nous obtenons la connexion de l'utilisateur que nous essayons d'appeler. S'il existe, nous lui envoyons les détails de l' offre . Nous ajoutons également otherName à l' objet de connexion . Ceci est fait pour la simplicité de le trouver plus tard.

Répondre

Répondre à la réponse a un modèle similaire que nous avons utilisé dans le gestionnaire d' offres . Notre serveur passe simplement tous les messages en réponse à un autre utilisateur. Ajoutez le code suivant après le paiement de l' offre -

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;

Vous pouvez voir comment cela est similaire au gestionnaire d' offres . Notez que ce code suit les fonctions createOffer et createAnswer sur l' objet RTCPeerConnection .

Nous pouvons maintenant tester notre mécanisme offre / réponse. Connectez deux clients en même temps et essayez de faire une offre et de répondre. Vous devriez voir ce qui suit -

Dans cet exemple, offer et answer sont de simples chaînes, mais dans une application réelle, elles seront remplies avec les données SDP.

Candidats ICE

La dernière partie concerne la gestion des candidats ICE entre les utilisateurs. Nous utilisons la même technique en passant simplement des messages entre utilisateurs. La principale différence est que les messages candidats peuvent apparaître plusieurs fois par utilisateur dans n'importe quel ordre. Ajouter le gestionnaire de candidats -

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;

Il devrait fonctionner de la même manière que les gestionnaires d' offres et de réponses .

Quitter la connexion

Pour permettre à nos utilisateurs de se déconnecter d'un autre utilisateur, nous devons implémenter la fonction de raccrochage. Il indiquera également au serveur de supprimer toutes les références utilisateur. Ajouter leleave gestionnaire -

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;

Cela enverra également à l'autre utilisateur l' événement de congé afin qu'il puisse déconnecter sa connexion homologue en conséquence. Nous devons également gérer le cas où un utilisateur abandonne sa connexion depuis le serveur de signalisation. Modifions notre gestionnaire de fermeture -

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

Maintenant, si la connexion se termine, nos utilisateurs seront déconnectés. L' événement de fermeture sera déclenché lorsqu'un utilisateur ferme la fenêtre de son navigateur alors que nous sommes toujours dans l' offre , la réponse ou l' état de candidat .

Serveur de signalisation complet

Voici le code complet de notre serveur de signalisation -

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

Le travail est donc terminé et notre serveur de signalisation est prêt. N'oubliez pas que faire des choses dans le désordre lors de l'établissement d'une connexion WebRTC peut causer des problèmes.

Sommaire

Dans ce chapitre, nous avons construit un serveur de signalisation simple et direct. Nous avons parcouru le processus de signalisation, l'enregistrement des utilisateurs et le mécanisme d'offre / réponse. Nous avons également mis en place l'envoi de candidats entre utilisateurs.