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

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

    Оставить комментарий