habrahabr

WebRTC. Делаем приложение с блекджеком и видеозвонками

  • среда, 20 мая 2020 г. в 00:32:42
https://habr.com/ru/post/502726/
  • JavaScript


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

Представим ситуацию: у нас есть чат-платформа и нам необходимо прикрутить к ней видеозвонки, то есть в онлайне сидит некий Вася и он хочет позвонить Пете, для реализации такой фичи нам понадобится технология WebSocket.

Что ж, давайте поднимем наш WebSocket сервер, нам в этом поможет node.js;
Создадим файл sockets.js и запишем туда код сокет сервера:

const WebSocketServer = require('websocket').server;
const http = require('http');

const server = http.createServer(function(request, response) {
 //здесь мы ничего не пишем,потому что мы используем сокеты,а не http
});
server.listen(1337, function() {});

// создаем вебсокет сервер
const wsServer = new WebSocketServer({
  httpServer: server
});

wsServer.on('request', function(request) {
  let connection = request.accept(null, request.origin);
//принимаем подключение к сокету
})

Создадим файл index.html и запишем туда код для открытия сокет-соединения:

     <video autoplay muted height='300' width='300' style="position:fixed;bottom:0;left:0;z-index: 9999;" src="" id='my'>
          </video> <!-- наше видео --> 

          <video autoplay height='300' width='300' style="position:fixed;bottom:0;left:300px;z-index: 999999;" src="" id='not_my'>
          </video> <!--видео нашего собеседника-->

Теперь создадим и подключим файл script.js к нашему html файлу:

  let connection = new WebSocket('ws://127.0.0.1:1337');//подключаемся к нашему сокет-серверу
  connection.onopen = function(){
//если вам необходимо,можете отправлять какие-либо данные на сокет при его открытии
 }
connection.onmessage = function(message){
//функция которая будет выполняться при приходе сообщения от сокета


}
connection.onerror = function (error) {
        console.error(error)
       //функция которая выполнится,если будет ошибка соединения
      };

Итак, вернемся к нашему Васе и Пете


image

Это начальный этап на котором Вася с Петей просто обмениваются JSON, о том будем ли принимать звонок или нет.То есть при заходе на нашу страницу мы обязательно должны открывать WebSocket соединение для связи с нашим сокет сервером.

  1. Мы шлем сначала на сокет сервер JSON с тем, что мы хотим позвонить Пете с аккаунта Васи

    connection.send(JSON.stringify({
    //данные для инициализации
    }))
    
  2. На сокет сервере мы должны этот JSON принять, после того как мы получили request, мы на наш сервер навешиваем событие message:

    connection.on('message',function(message){
    //можем работать с message и отсылать кому угодно,но прежде переведем все из JSON в JS
    let self = JSON.parse(message.utf8Data);
    })
    

Пообщавшись и решив, что оба пользователя готовы к разговору, мы должны разобраться как работает видеосвязь в браузере, в js встроен модуль для этого — RTCPeerConnection


image

Нам необходимо открыть RTCPeerConnection, с помощью которого мы сможем сгененрировать offer и отправить его нужному нам пользователю опять же через наш сокет-сервер, который получив его сгенерирует нам answer и отправит обратно, после чего мы начинаем обмениваться ice пакетами, в которых содержится информация об окружении данного компьютера, которая необходима для успешного установления видеосвязи.

Генерируем и отправляем offer


var pc = new RTCPeerConnection();
            
            var peerConnectionConfig = {
              iceServers: [
                  {
                      urls: 'stun:stun.l.google.com:19302'
                  }
                ]
            }
            pc.onicecandidate = function (event) {
              console.log('new ice candidate', event.candidate);

              if (event.candidate !== null) {
                connection.send(JSON.stringify({
                 //отправляем json и ice пакеты
                }))
              }
          };

            navigator.getUserMedia = navigator.getUserMedia ||
                         navigator.webkitGetUserMedia ||
                         navigator.mozGetUserMedia;
                      

            navigator.getUserMedia({video: true,audio:true}, function(stream) {
              // Добавление локального потока не вызовет onaddstream обратного вызова,
              // так называют его вручную.
            var my_video = document.getElementById('my')
            my_video.srcObject = stream

              pc.onaddstream = e => {
                document.getElementById('not_my').srcObject = e.stream;
                console.log('not stream is added')
              }
              pc.addStream(stream);   
            
              pc.createOffer(function(offer) {
                pc.setLocalDescription(offer, function() {
                  //отправляем наш offer на сокет
                  }))
                }, e=> console.log(e));
              }, e=> console.log(e));
            },function (){console.warn("Error getting audio stream from getUserMedia")});

            // функция помощник
            function endCall() {
              var videos = document.getElementsByTagName("video");
              for (var i = 0; i < videos.length; i++) {
                videos[i].pause();
              }
            
              pc.close();
            }

            function error(err) {
              endCall();
            }
 

Обрабатываем offer и генерируем answer


var pc = new RTCPeerConnection();//создаем connection
var peerConnectionConfig = { //конфигурация ice server
              iceServers: [
                  {
                      urls: 'stun:stun.l.google.com:19302'
                  }
                ]
            }
            pc.onicecandidate = function (event) {
              console.log('new ice candidate', event.candidate);

              if (event.candidate !== null) {
                //отправляем ice пакеты
              }
          };


            navigator.getUserMedia = navigator.getUserMedia ||
                         navigator.webkitGetUserMedia ||
                         navigator.mozGetUserMedia;

            navigator.getUserMedia({video: true,audio:true}, function(stream) {//подключаем у пользователя видеосвязь и аудиосвязь и втыкаем ее на страницу
              var my_video = document.getElementById('my')
              my_video.srcObject = stream
              console.log('stream is added while offering')

              pc.onaddstream = e => {
                console.log('not my stream is added while offering')
                document.getElementById('not_my').srcObject = e.stream;
                
              }
              pc.addStream(stream);
            
              pc.setRemoteDescription(new RTCSessionDescription(data.offer), function() {
                pc.createAnswer(function(answer) {
                  pc.setLocalDescription(answer, function() {
                    //отправляем ответ
                  }, e => console.log(e));
                }, e => console.log(e));
              }, e => console.log(e));
            },function (){console.warn("Error getting audio stream from getUserMedia")});
          }
        }

Принимаем answer и создаем видеопоток


pc.setRemoteDescription(new RTCSessionDescription(data.answer), function() { }, error);

И последнее что нам нужно сделать — обработать ice пакеты


pc.addIceCandidate(new RTCIceCandidate(ice))//тут ice -  тот,который пришел нам от пользователя