Видеозвонок в браузере на PeerJS. Быстрый старт

в 13:01, , рубрики: javascript, Peer-to-Peer, peerjs, php, видеоконференция, видеоконференцсвязь, видеосвязь

Приветствую всех читателей Хабра. В этом году довелось писать модуль видеосвязи для одного учебного портала для созвона по видеосвязи прямо на сайте учителя с учеником. Раннее такую задачу решать не приходилось. После недолгих поисков обнаружил, что есть 2 пути: Flash и WebRTC. WebRTC в чистом виде оказался сложноват, и в общем-то это естественно, так как задача видеосвязи не является простой. Но потом я наткнулся на PeerJS, который является оберткой для WebRTC. В этой статье я расскажу, как быстро организовать свою браузерную звонилку.

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

Стартовая верстка будет выглядеть так:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title>Peer</title>
   <script src="https://unpkg.com/peerjs@1.0.0/dist/peerjs.min.js"></script>
</head>
<body>
	<p><h3>Мой ID: </h3><span id=myid ></span></p>
	<input id=otherPeerId type=text placeholder="otherPeerId" > <button onclick="callToNode(document.getElementById('otherPeerId').value)">Вызов</button>
	
	<br>
	<video id=myVideo muted="muted" width="400px" height="auto" ></video>
	<div id=callinfo ></div>
	<video id=remVideo width="400px" height="auto" ></video>
</body>

В секции head мы подключаем PeerJS удаленно. Есть также возможность скачать скрипт и подключать его локально.

input id=otherPeerId — прездназначен для ввода пира того, кому мы будем звонить (можно воспринимать это как индекс, или как номер телефона).

Два тега видео предназначены для отображения собственного видео и для видео собеседника соответственно.

Теперь немного о технологии WebRTC и о том, как происходит звонок. WebRTC выполняет звонок от клиента к клиенту напрямую, без участия сервера, поэтому на первом шаге 2 браузера должны найти друг друга. Для этого в классическом WebRTC необходим сигнальный сервер, то есть сервер, который сообщит одному браузеру параметры другого браузера, и в WebRTC такой сервер приходится организовывать самому. Однако разработчики PeerJS предоставляют собственный сигнальный сервер. Всё что нужно сделать, это передать потенциальному собеседнику peerID, то есть получаемый в системе PeerJS уникальный индекс. В рабочем проекте я это организовал так:

  1. После загрузки страницы создается объект Peer
  2. Его peerID записывается в mysql базу
  3. При нажатии кнопки Вызов peerID собеседника вытаскивается из БД и используется для установки соединения

В текущем тестовом примере мы будем вводить peerID собеседника в текстовое поле otherPeerId

Итак приступим к написанию кода

1. Создаем основной объект peer

var peer = new Peer();

2. При открытии пира мы получим заветный peerID, который нужно передать партнеру, чтоб он мог связаться с нами

peer.on('open', function(peerID) {
			document.getElementById('myid').innerHTML=peerID;			
		});

3. Для того чтобы принять звонок, вешаем обработчик на событие call

var peercall;
peer.on('call', function(call) {
		  // Answer the call, providing our mediaStream
			peercall=call;
			document.getElementById('callinfo').innerHTML="Входящий звонок <button onclick='callanswer()' >Принять</button><button onclick='callcancel()' >Отклонить</button>";
		});

При входящем звонке мы получаем объект call, который сохраним в глобальную переменную peercall. Также в информационном блоке будет выведено уведомление о входящем звонке и 2 кнопки: Принять и Отклонить

4. Пишем функцию для кнопки Принять

function callanswer() {
	navigator.mediaDevices.getUserMedia ({ audio: true, video: true }).then(function(mediaStream) {
	var video = document.getElementById('myVideo');		    				  
	 peercall.answer(mediaStream); // отвечаем на звонок и передаем свой медиапоток собеседнику
	//peercall.on ('close', onCallClose); //можно обработать закрытие-обрыв звонка
	 video.srcObject = mediaStream; //помещаем собственный медиапоток в объект видео (чтоб видеть себя)
	document.getElementById('callinfo').innerHTML="Звонок начат... <button onclick='callclose()' >Завершить звонок</button>"; //информируем, что звонок начат, и выводим кнопку Завершить
	video.onloadedmetadata = function(e) {//запускаем воспроизведение, когда объект загружен
		video.play();
	  };
	  setTimeout(function() {
                 //входящий стрим помещаем в объект видео для отображения
	        document.getElementById('remVideo').srcObject = peercall.remoteStream; 
		document.getElementById('remVideo').onloadedmetadata= function(e) {
// и запускаем воспроизведение когда объект загружен
				document.getElementById('remVideo').play();
		  };
	},1500);			  				  
				  
}).catch(function(err) { console.log(err.name + ": " + err.message); });
}

navigator.mediaDevices.getUserMedia — запрашивает доступ к камере и микрофону. В данных объекта, который передается в этот метод { audio: true, video: true } можно соответственно запросить доступ только к камере или только к микрофону. Дальнейшие комментарии добавил прямо в коде.

setTimeout был добавлен опытным путем: воспроизведение видео партнера не начиналось, а с тайм-аутом заработало.

5. Функция дозвона по кнопке Вызов

function callToNode(peerId) { //вызов
	  navigator.mediaDevices.getUserMedia ({ audio: true, video: true }).then(function(mediaStream) {
	  var video = document.getElementById('myVideo');				  
	  peercall = peer.call(peerId,mediaStream); //звоним, указав peerId-партнера и передав свой mediaStream
	  peercall.on('stream', function (stream) { //нам ответили, получим стрим
		  setTimeout(function() {
		  document.getElementById('remVideo').srcObject = peercall.remoteStream;
		  document.getElementById('remVideo').onloadedmetadata= function(e) {
			document.getElementById('remVideo').play();
		  };
	      },1500);	
     });
     //  peercall.on('close', onCallClose);				  
	video.srcObject = mediaStream;
	video.onloadedmetadata = function(e) {
		video.play();
	 };
   }).catch(function(err) { console.log(err.name + ": " + err.message); });
}

Также как и в предыдущем пункте запрашиваем свой медиапоток. После вызываем функцию call объекта peer, которая вернет нам объект звонка, сохраняем его в peercall. Обрабатываем событие stream, чтоб узнать, что нам ответили, и помещаем входящий стрим в соответствующий объект video

Вот собственно и всё, но…

Если оба звонящих находятся за NAT-ом звонок не пройдет. (Почему? Читайте здесь habr.com/ru/company/yandex/blog/419951 )
Для того, чтобы преодолеть эту преграду, необходимо при создании объекта peer указать TURN-сервер (Вопрос, где его взять, оказался не самым простым. Нам пришлось поднимать свой: VPS на Ubuntu 16.04. Установка командой

apt install coturn

)

Тогда создание пира будет выглядеть примерно так:

var callOptions={'iceServers': [
		{url: 'stun:95.xxx.xx.x9:3479',		
		username: "user",
		credential: "xxxxxxxxxx"},
		{ url: "turn:95.xxx.xx.x9:3478",		
		username: "user",
		credential: "xxxxxxxx"}]
	}; 
peer= new Peer({config: callOptions}); 

В завершение код целиком:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title>Peer</title>
   <script src="https://unpkg.com/peerjs@1.0.0/dist/peerjs.min.js"></script>
</head>
<body>
	<p><h3>Мой ID: </h3><span id=myid ></span></p>
	<input id=otherPeerId type=text placeholder="otherPeerId" > <button onclick="callToNode(document.getElementById('otherPeerId').value)">Вызов</button>
	
	<br>
	<video id=myVideo muted="muted" width="400px" height="auto" ></video>
	<div id=callinfo ></div>
	<video id=remVideo width="400px" height="auto" ></video>
<script>
var callOptions={'iceServers': [
		{url: 'stun:95.xxx.xx.x9:3479',		
		username: "user",
		credential: "xxxxxxxxxx"},
		{ url: "turn:95.xxx.xx.x9:3478",		
		username: "user",
		credential: "xxxxxxxx"}]
	}; 
peer= new Peer({config: callOptions}); 
peer.on('open', function(peerID) {
			document.getElementById('myid').innerHTML=peerID;			
		});
var peercall;
peer.on('call', function(call) {
		  // Answer the call, providing our mediaStream
			peercall=call;
			document.getElementById('callinfo').innerHTML="Входящий звонок <button onclick='callanswer()' >Принять</button><button onclick='callcancel()' >Отклонить</button>";
		});
function callanswer() {
			navigator.mediaDevices.getUserMedia ({ audio: true, video: true }).then(function(mediaStream) {
		    var video = document.getElementById('myVideo');		    				  
		  peercall.answer(mediaStream); // отвечаем на звонок и передаем свой медиапоток собеседнику
		  //peercall.on ('close', onCallClose); //можно обработать закрытие-обрыв звонка
		  video.srcObject = mediaStream; //помещаем собственный медиапоток в объект видео (чтоб видеть себя)
		  document.getElementById('callinfo').innerHTML="Звонок начат... <button onclick='callclose()' >Завершить звонок</button>"; //информируем, что звонок начат, и выводим кнопку Завершить
		  video.onloadedmetadata = function(e) {//запускаем воспроизведение, когда объект загружен
			video.play();
		  };
		  setTimeout(function() {
                          //входящий стрим помещаем в объект видео для отображения
			  document.getElementById('remVideo').srcObject = peercall.remoteStream; 
			  document.getElementById('remVideo').onloadedmetadata= function(e) {
// и запускаем воспроизведение когда объект загружен
						document.getElementById('remVideo').play();
					  };
					  },1500);			  
				  
				  
			}).catch(function(err) { console.log(err.name + ": " + err.message); });
		}
function callToNode(peerId) { //вызов
		  navigator.mediaDevices.getUserMedia ({ audio: true, video: true }).then(function(mediaStream) {
		  var video = document.getElementById('myVideo');				  
		  peercall = peer.call(peerId,mediaStream); 
		  peercall.on('stream', function (stream) { //нам ответили, получим стрим
				  setTimeout(function() {
				  document.getElementById('remVideo').srcObject = peercall.remoteStream;
					  document.getElementById('remVideo').onloadedmetadata= function(e) {
						document.getElementById('remVideo').play();
					  };
					  },1500);	
				  });
				//  peercall.on('close', onCallClose);				  
				  video.srcObject = mediaStream;
				  video.onloadedmetadata = function(e) {
					video.play();
				  };
			}).catch(function(err) { console.log(err.name + ": " + err.message); });
		}
</script>
</body>

Данное решение было успешно проверено под Windows7 и Ubuntu 18.04 в браузерах Chrome, Opera, Firefox. В Chrome работает также под Android и MacOS, но не работает для iPhone и iPad.

Автор: stitakov

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js