Как-то в одной из старинных и уже заброшенных статей я писал о том, как легко и непринужденно можно транслировать видео с canvas через websockets. В той статье поверхностно рассказывал о том, как захватить видео с камеры и звук с микрофона посредством MediaStream API, как полученный поток кодировать и отправлять через websockets на сервер. Однако в реальности так не делают, для трансляций используют либо специальный софт, который нужно устанавливать и настраивать: навскидку это может быть Open Broadcast Software, либо задействуют WebRTC, который работает прямо из коробки, то есть не требует установки никаких плагинов аля flash player, который вот уже в декабре выпилят из Chromium браузера.

Сегодня поговорим о WebRTC.


Web Real-Time Communication (WebRTC) это не один протокол, это целая коллекция стандартов, протоколов и JavaScript API, которые все вместе обеспечивают peer-to-peer видео-аудио коммуникации в реальном времени, а также могут быть использованы для передачи любых бинарных данных. Обычно пирами выступают браузеры, но это может быть и мобильное приложение, например. Для того чтобы организовать p2p общение между клиентами требуется поддержка браузером различных видов кодирования видео и аудио, поддержка множества сетевых протоколов, обеспечение взаимодействия железа с браузером (через слои ОС): вебкамер, звуковых карт. Вся эта мешанина технологий скрыта за абстракцией JavaScript API для удобства разработчика.

Все сводится в итоге к трем API:

  • MediaStream API - разбирали в прошлый раз, сегодня еще немного напишу про него. Служит для получения видео/аудио потоков от "железа"

  • RTCPeerConnection - обеспечивает коммуникации между двумя клиентами (p2p)

  • RTCDataChannel - служит для передачи произвольных данных между двумя клиентами

Подготовка аудио и видео потоков к передаче

Все начинается с "захвата" медиапотоков вебкамеры и микрофона. Сырые потоки конечно же не подходят для организации телеконференции, каждый поток необходимо обработать: улучшить качество, синхронизировать аудио с видео, расставить метки синхронизации в видеопотоке, обеспечить соответствующий постоянно меняющейся широте пропускания канала битрейт. Браузер все это берет на себя, разработчику даже не надо беспокоиться об обеспечении кодирования медиа-потоков. Внутри современного браузера уже присутствуют программные слои захвата, улучшения качества (убрать эхо и шум из звука, улучшить картинку), кодирования видео и аудио. Схема слоев показана рис. 1:

Рис. 1. Слои аудио и видео обработки в браузере
Рис. 1. Слои аудио и видео обработки в браузере

Вся обработка происходит прямо в самом браузере, никаких доп. плагинов не требуется. Однако все еще не столь радужно на 2020 год. Остались браузеры, которые пока еще не поддерживают полностью MediaStream API, вы можете перейти по ссылке и в самом низу посмотреть таблицу совместимости. В частности IE снова разочаровывает.

С полученными потоками можно делать очень интересные вещи: можно клонировать, менять разрешение видео, манипулировать качеством аудио, можно взять и "прицепить" Media Stream поток к <video> тегу и смотреть на себя любимого на страничке html. А можно поток и на canvas отрисовать, и натравить WebGL или CSS3, и накладывать различные фильтры на видео, захватывать обработанное видео с canvas и далее уже отправлять по сети на сервер, транскодить и публиковать всем желающим (привет bigo live, twitch и прочие). Здесь я не буду разбирать как делаются такие вещи, приведу пару примеров, найденных на просторах сети:

https://jeeliz.com/ - ребята занимаются realtime CV на Javascript. У них есть целый арсенал различных js-библиотек для работы с видеопотоком на canvas: детектирование лиц, объектов, наложение фильтров (масок, как в инсте) и пр. Отличный пример того, как без дополнительных плагинов можно прямо в браузере обрабатывать видео в реальном времени.

Canvas captureStream API - документация API по захвату видеопотока с canvas. Уже поддерживается в Chrome, Opera и Firefox

RTCPeerConnection

Вот мы и подошли к тому, а как собственно передать видео другому пользователю? На первый план выступает RTCPeerConnection. Если говорить кратко, практически на этом шаге вам необходимо создать объект RTCPeerConnection:

const peerConnection = new RTCPeerConnection({
  iceServers: [{
    urls: 'stun:stun.l.google.com:19302'
  }]
});

Одной из опций указываем iceServers - это сервер, который помогает обеспечивать соединение между двумя браузерами, находящимися за NAT'ом. То есть здесь решается проблема: как узнать ip собеседника, если он находится за NAT его провайдера? На помощь приходит ICE протокол, на самом деле, ICE вообще не относится к WebRTC, но об этом позже.

Ранее мы получили Usermedia потоки:

navigator.mediaDevices.getUserMedia({ video: true, audio: true }).then(stream => {
  // Usermedia-потоки, обычно это видео и аудио 
  const tracks = stream.getTracks();

   for (const track of tracks) {
     // каждый трек присоединяем к peerConnection
     peerConnection.addTrack(track);
   }
}).catch(console.error);

Далее на peerConnection срабатывает событие onnegotiationneeded, в обработчике которого мы должны создать offer (в терминах SDP - Session Description Protocol) и назначить в peerConnection через метод setLocalDescription. Об SDP - что это такое и о форматах offer и answer - поговорим далее.

После назначения LocalDescription peerConnection, браузер "собирает" ice-кандидатов, то есть находит различные пути для коммуникации через NAT. Срабатывает событие onicegatheringstatechange. В обработчике onicegatheringstatechange разрешаем соединение с webrtc-signaling-сервером stream для обмена Session Description между пирами:

peerConnection.oniceconnectionstatechange = (event) => {
      console.log('Connection state: ', peerConnection.iceConnectionState);

      if (peerConnection.iceConnectionState === 'connected') {
        // Можем активировать кнопку Start broadcast
        setBroadcasting(true);
        setBroadcastingBtnActive(true);
      }
    };

// Событие срабатывает сразу, как только добавился медаиапоток в peerConnection
peerConnection.onnegotiationneeded = (event) => {
      // Создаем и назначаем SDP offer
      peerConnection.createOffer().
        then((offer) => peerConnection.setLocalDescription(offer)).
        catch(console.error);
    };

// Событие срабатывает каждый раз, как появляется ICE кандидат
peerConnection.onicegatheringstatechange = (ev) => {
      let connection = ev.target;

      // Now we can activate broadcast button
      if (connection.iceGatheringState === 'complete') {
        let delay = 50;
        let tries = 0;
        let maxTries = 3;

        let timerId = setTimeout(function allowStreaming() {
          if (isOnline) {
            setBroadcastingBtnActive(true);
            return;
          }

          if (tries < maxTries) {
            tries += 1;
            delay *= 2;
            timerId = setTimeout(allowStreaming, delay);
          } else {
            // TODO: show user notification
            console.error("Can't connect to server");

            alert("Can't connect to server");
          }
        }, delay);
      }
    };

webrtc-signaling-сервер - это сервер, необходимый для обеспечения обмена session description между двумя пирами, это может быть простейший websocket или xhr-сервер на любом ЯП. Его задача проста: принять session description от одного пира и передать другому.

После обмена Session descriptions обе стороны готовы транслировать и принимать видеопотоки, на стороне, которая принимает видеопоток срабатывает событие ontrack на peerConnection, в обработчике которого, получаемые треки можно назначить на <video> и смотреть на любимого собеседника. Далее теория и подробности.

Ссылки и литература:

https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection - документация RTCPeerConnection

https://github.com/pion/webrtc - реализация протоколов WebRTC на go

https://webrtcforthecurious.com/ - книжечка от создателей pion

https://hpbn.co/ - книга High Perfomance Browser Networking. В подробностях разбираются вопросы обеспечения высокой производительности web-приложений. В конце описывается WebRTC. Книжка конечно старая (2013), но не теряет своей актуальности.

В следующей части хочу дать еще немного порции теории и на практике разобрать прием и обработку видеопотока на сервере с помощью pion, транскодинг в HLS через ffmpeg для последующей трансляции зрителям в браузере.

Для нетерпеливых: мой очень сырой прототип трансляции видео с вебки на react через сервер на основе pion в twitch (это просто эксперимент).