Приветствую всех читателей Хабра. В этом году довелось писать модуль видеосвязи для одного учебного портала для созвона по видеосвязи прямо на сайте учителя с учеником. Раннее такую задачу решать не приходилось. После недолгих поисков обнаружил, что есть 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 уникальный индекс. В рабочем проекте я это организовал так:
- После загрузки страницы создается объект Peer
- Его peerID записывается в mysql базу
- При нажатии кнопки Вызов 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-сервер (Вопрос, где его взять, оказался не самым простым. Нам пришлось поднимать свой:
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