Учебники

WebRTC — Голосовая демонстрация

В этой главе мы собираемся создать клиентское приложение, которое позволит двум пользователям на отдельных устройствах общаться с помощью аудиопотоков WebRTC. Наше приложение будет иметь две страницы. Один для входа в систему, а другой для совершения аудиовызова другому пользователю.

Логин и аудиозвонок.

Две страницы будут тегами div . Большая часть ввода осуществляется через простые обработчики событий.

Сигнальный сервер

Чтобы создать соединение WebRTC, клиенты должны иметь возможность передавать сообщения без использования однорангового соединения WebRTC. Здесь мы будем использовать HTML5 WebSockets — двунаправленное сокетное соединение между двумя конечными точками — веб-сервером и веб-браузером. Теперь давайте начнем использовать библиотеку WebSocket. Создайте файл server.js и вставьте следующий код —

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

Первая строка требует библиотеки WebSocket, которую мы уже установили. Затем мы создаем сервер сокетов на порту 9090. Затем мы прослушиваем событие подключения . Этот код будет выполнен, когда пользователь устанавливает соединение WebSocket с сервером. Затем мы слушаем любые сообщения, отправленные пользователем. Наконец, мы отправляем ответ подключенному пользователю, говоря «Привет с сервера».

На нашем сервере сигнализации мы будем использовать имя пользователя на основе строки для каждого соединения, чтобы мы знали, куда отправлять сообщения. Давайте немного изменим наш обработчик соединения

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

Таким образом, мы принимаем только сообщения JSON. Далее нам нужно где-то хранить всех подключенных пользователей. Мы будем использовать простой объект Javascript для него. Изменить верх нашего файла —

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

Мы собираемся добавить поле типа для каждого сообщения от клиента. Например, если пользователь хочет войти, он отправляет сообщение типа входа . Давайте определим это —

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

Если пользователь отправляет сообщение с типом логина , мы —

  • Проверьте, если кто-то уже вошел в систему с этим именем пользователя.
  • Если это так, то сообщите пользователю, что он не вошел в систему.
  • Если никто не использует это имя пользователя, мы добавляем имя пользователя в качестве ключа к объекту подключения.
  • Если команда не распознана, мы отправляем ошибку.

Следующий код является вспомогательной функцией для отправки сообщений в соединение. Добавьте его в файл server.js

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

Когда пользователь отключается, мы должны очистить его соединение. Мы можем удалить пользователя при возникновении события закрытия . Добавьте следующий код в обработчик соединения :

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

После успешного входа в систему пользователь хочет позвонить другому. Он должен сделать предложение другому пользователю для достижения этого. Добавить обработчик предложения

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;

Во-первых, мы получаем соединение с пользователем, которого пытаемся вызвать. Если он существует, мы отправляем ему детали предложения . Мы также добавляем otherName к объекту соединения . Это сделано для простоты поиска позже.

Ответ на ответ имеет аналогичную схему, которую мы использовали в обработчике предложений . Наш сервер просто проходит через все сообщения как ответ другому пользователю. Добавьте следующий код после обработчика предложения

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;

Заключительная часть — обработка кандидата ICE между пользователями. Мы используем ту же технику, просто передавая сообщения между пользователями. Основное отличие состоит в том, что сообщения-кандидаты могут появляться несколько раз на пользователя в любом порядке. Добавьте обработчик кандидата

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

Ниже приведен полный код нашего сервера сигнализации.

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

Клиентское приложение

Один из способов протестировать это приложение — открыть две вкладки браузера и попытаться сделать аудиозвонок друг другу.

Прежде всего, нам нужно установить библиотеку начальной загрузки . Bootstrap — это интерфейс для разработки веб-приложений. Вы можете узнать больше на http://getbootstrap.com/. Создайте папку с именем, например, «audiochat». Это будет наша корневая папка приложения. Внутри этой папки создайте файл package.json (он необходим для управления зависимостями npm) и добавьте следующее:

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

Затем запустите npm install bootstrap . Это установит библиотеку начальной загрузки в папку audiochat / node_modules .

Теперь нам нужно создать простую HTML-страницу. Создайте файл index.html в корневой папке со следующим кодом —

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

Эта страница должна быть вам знакома. Мы добавили файл начальной загрузки css. Мы также определили две страницы. Наконец, мы создали несколько текстовых полей и кнопок для получения информации от пользователя. Вы должны увидеть два аудиоэлемента для локальных и удаленных аудиопотоков. Обратите внимание, что мы добавили ссылку на файл client.js .

Теперь нам нужно установить соединение с нашим сервером сигнализации. Создайте файл client.js в корневой папке со следующим кодом —

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

Теперь запустите наш сервер сигнализации через сервер узлов . Затем внутри корневой папки запустите статическую команду и откройте страницу в браузере. Вы должны увидеть следующий вывод консоли —

Запустить сервер сигнализации

Следующим шагом является реализация входа пользователя с уникальным именем пользователя. Мы просто отправляем имя пользователя на сервер, который затем сообщает нам, занято оно или нет. Добавьте следующий код в ваш файл 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 
      //**********************
		         
   } 
	
};

Во-первых, мы выбираем несколько ссылок на элементы на странице. Мы скрываем страницу вызова. Затем мы добавляем прослушиватель событий на кнопку входа. Когда пользователь щелкает по нему, мы отправляем его имя пользователя на сервер. Наконец, мы реализуем обратный вызов handleLogin. Если вход был успешным, мы показываем страницу вызова и начинаем устанавливать одноранговое соединение.

Чтобы начать одноранговое соединение нам нужно —

  • Получить аудиопоток с микрофона
  • Создайте объект RTCPeerConnection

Добавьте следующий код в «блок селекторов пользовательского интерфейса» —

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

var yourConn; 
var stream;

Изменить функцию 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); 
      }); 
		
   } 
};

Теперь, если вы запустите код, страница должна позволить вам войти в систему и отобразить ваш локальный аудиопоток на странице.

Разрешить войти

Теперь мы готовы инициировать вызов. Сначала мы отправляем предложение другому пользователю. Как только пользователь получает предложение, он создает ответ и начинает торговать кандидатами ICE. Добавьте следующий код в файл 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)); 
};

Мы добавляем обработчик кликов к кнопке Call, которая инициирует предложение. Затем мы реализуем несколько обработчиков, ожидаемых обработчиком onmessage . Они будут обрабатываться асинхронно, пока оба пользователя не установят соединение.

Последний шаг — реализация функции зависания. Это прекратит передачу данных и скажет другому пользователю закрыть вызов. Добавьте следующий код —

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

Когда пользователь нажимает кнопку Hang Up —

  • Он отправит сообщение «оставить» другому пользователю
  • Это закроет RTCPeerConnection и уничтожит соединение локально

Теперь запустите код. Вы должны иметь возможность войти на сервер, используя две вкладки браузера. Затем вы можете сделать аудиозвонок на вкладку и повесить трубку.

Страница авторизацииЗвоните и вешайте страницу

Ниже приведен весь файл client.js