«1. Обзор

Когда двум браузерам необходимо обмениваться данными, им обычно требуется промежуточный сервер для координации связи и передачи сообщений между ними. Но наличие сервера посередине приводит к задержке связи между браузерами.

В этом руководстве мы узнаем о WebRTC, проекте с открытым исходным кодом, который позволяет браузерам и мобильным приложениям напрямую взаимодействовать друг с другом в режиме реального времени. Затем мы увидим его в действии, написав простое приложение, которое создает одноранговое соединение для обмена данными между двумя HTML-клиентами.

Мы будем использовать HTML, JavaScript и библиотеку WebSocket вместе со встроенной поддержкой WebRTC в веб-браузерах для создания клиента. И мы будем создавать сервер сигнализации с Spring Boot, используя WebSocket в качестве протокола связи. Наконец, мы увидим, как добавить видео- и аудиопотоки к этому соединению.

2. Основы и концепции WebRTC

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

Предположим, у нас есть два браузера, и Браузер 1 должен отправить сообщение Браузеру 2. Браузер 1 сначала отправляет его на Сервер:

После того, как Сервер получает сообщение, он обрабатывает его, находит Браузер 2 , и отправляет ему сообщение:

Поскольку сервер должен обработать сообщение перед его отправкой в ​​браузер 2, обмен данными происходит практически в режиме реального времени. Конечно, хотелось бы, чтобы это было в реальном времени.

WebRTC решает эту проблему, создавая прямой канал между двумя браузерами, устраняя необходимость в сервере:

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

3. Поддержка WebRTC и встроенных функций

WebRTC поддерживается основными браузерами, такими как Chrome, Firefox, Opera и Microsoft Edge, а также такими платформами, как Android и iOS.

WebRTC не требует установки каких-либо внешних плагинов в наш браузер, так как решение поставляется в комплекте с браузером.

Кроме того, в типичном приложении реального времени, включающем передачу видео и аудио, мы должны сильно зависеть от библиотек C++, и нам приходится решать множество проблем, в том числе:

Маскировка потери пакетов Подавление эха Адаптивность полосы пропускания Динамическая буферизация джиттера Автоматическая регулировка усиления Шумоподавление и подавление «очистка» изображения

Но WebRTC решает все эти проблемы скрытно, упрощая связь между клиентами в реальном времени.

4. Одноранговое соединение

    В отличие от связи клиент-сервер, где известен адрес сервера, а клиент уже знает адрес сервера для связи, в P2P (одноранговой одноранговое) соединение, ни один из одноранговых узлов не имеет прямого адреса к другому одноранговому узлу.

Чтобы установить одноранговое соединение, необходимо выполнить несколько шагов, чтобы клиенты могли:

сделать себя доступными для связи, идентифицировать друг друга и обмениваться информацией, связанной с сетью, обмениваться и согласовывать формат данных, режим и задействованные протоколы обмениваются данными

WebRTC определяет набор API и методологий для выполнения этих шагов.

Чтобы клиенты могли обнаруживать друг друга, делиться сведениями о сети, а затем обмениваться форматом данных, WebRTC использует механизм, называемый сигнализацией.

    5. Сигнализация

Сигнализация относится к процессам, связанным с обнаружением сети, созданием сеанса, управлением сеансом и обменом метаданными о возможностях мультимедиа.

Это очень важно, так как клиенты должны знать друг друга заранее, чтобы начать общение.

Чтобы достичь всего этого, WebRTC не определяет стандарт для сигнализации и оставляет его на усмотрение разработчика. Таким образом, это дает нам возможность использовать WebRTC на различных устройствах с любой технологией и поддерживающим протоколом.

«5.1. Создание сервера сигнализации

Для сервера сигнализации мы создадим сервер WebSocket с использованием Spring Boot. Мы можем начать с пустого проекта Spring Boot, сгенерированного из Spring Initializr.

Чтобы использовать WebSocket для нашей реализации, давайте добавим зависимость в наш pom.xml:

Мы всегда можем найти последнюю версию для использования в Maven Central.

Реализация сигнального сервера проста — мы создадим конечную точку, которую клиентское приложение сможет использовать для регистрации в качестве соединения WebSocket.

Чтобы сделать это в Spring Boot, давайте напишем класс @Configuration, который расширяет WebSocketConfigurer и переопределяет метод registerWebSocketHandlers:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-websocket</artifactId>
    <version>2.4.0</version>
</dependency>

Обратите внимание, что мы определили /socket как URL-адрес, который мы будем регистрировать из client, который мы создадим на следующем шаге. Мы также передали SocketHandler в качестве аргумента методу addHandler — на самом деле это обработчик сообщений, который мы создадим далее.

5.2. Создание обработчика сообщений на сервере сигнализации

Следующим шагом является создание обработчика сообщений для обработки сообщений WebSocket, которые мы будем получать от нескольких клиентов.

@Configuration
@EnableWebSocket
public class WebSocketConfiguration implements WebSocketConfigurer {

    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(new SocketHandler(), "/socket")
          .setAllowedOrigins("*");
    }
}

Это необходимо для облегчения обмена метаданными между разными клиентами для установления прямого соединения WebRTC.

Здесь, для простоты, когда мы получаем сообщение от клиента, мы отправляем его всем другим клиентам, кроме самого себя.

Для этого мы можем расширить TextWebSocketHandler из библиотеки Spring WebSocket и переопределить оба метода handleTextMessage и afterConnectionEstablished:

Как мы видим в методе afterConnectionEstablished, мы добавляем полученную сессию в список сессий так что мы можем отслеживать всех клиентов.

И когда мы получаем сообщение от любого из клиентов, как видно из handleTextMessage, мы перебираем все клиентские сеансы в списке и отправляем сообщение всем другим клиентам, кроме отправителя, сравнивая идентификатор сеанса отправитель и сеансы в списке.

6. Обмен метаданными

@Component
public class SocketHandler extends TextWebSocketHandler {

    List<WebSocketSession>sessions = new CopyOnWriteArrayList<>();

    @Override
    public void handleTextMessage(WebSocketSession session, TextMessage message)
      throws InterruptedException, IOException {
        for (WebSocketSession webSocketSession : sessions) {
            if (webSocketSession.isOpen() && !session.getId().equals(webSocketSession.getId())) {
                webSocketSession.sendMessage(message);
            }
        }
    }

    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        sessions.add(session);
    }
}

В P2P-соединении клиенты могут сильно отличаться друг от друга. Например, Chrome на Android может подключаться к Mozilla на Mac.

Следовательно, мультимедийные возможности этих устройств могут сильно различаться. Следовательно, для рукопожатия между одноранговыми узлами важно согласовать типы мультимедиа и кодеки, используемые для связи.

На этом этапе WebRTC использует SDP (протокол описания сеанса) для согласования метаданных между клиентами.

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

Соединение устанавливается, когда этот процесс завершен.

7. Настройка клиента

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

Мы начнем с создания файла HTML с именем index.html и файла JavaScript с именем client.js, который будет использовать index.html.

Чтобы подключиться к нашему сигнальному серверу, мы создаем к нему соединение WebSocket. Предполагая, что созданный нами сигнальный сервер Spring Boot работает на http://localhost:8080, мы можем создать соединение:

Чтобы отправить сообщение на сигнальный сервер, мы создадим метод отправки, который будет использоваться для передачи сообщения на следующих шагах:

8. Настройка простого RTCDataChannel

После настройки клиента в client.js нам нужно создать объект для класса RTCPeerConnection:

var conn = new WebSocket('ws://localhost:8080/socket');

В этом примере целью объекта конфигурации является передача серверов STUN (утилиты обхода сеанса для NAT) и TURN (обход с использованием ретрансляторов вокруг NAT) и других конфигураций, которые мы будем обсуждать в Последняя часть этого руководства. Для этого примера достаточно передать null.

function send(message) {
    conn.send(JSON.stringify(message));
}

Теперь мы можем создать dataChannel для передачи сообщений:

Затем мы можем создать прослушиватели для различных событий в канале данных:

configuration = null;
var peerConnection = new RTCPeerConnection(configuration);

«

9. Установление соединения с помощью ICE

var dataChannel = peerConnection.createDataChannel("dataChannel", { reliable: true });

Следующий шаг в установлении соединения WebRTC включает протоколы ICE (установление интерактивного соединения) и SDP, где описания сеансов пиров обмениваются и принимаются на обоих пирах.

dataChannel.onerror = function(error) {
    console.log("Error:", error);
};
dataChannel.onclose = function() {
    console.log("Data channel is closed");
};

Сервер сигнализации используется для отправки этой информации между узлами. Это включает в себя серию шагов, когда клиенты обмениваются метаданными соединения через сигнальный сервер.

9.1. Создание предложения

Во-первых, мы создаем предложение и устанавливаем его как локальное описание peerConnection. Затем мы отправляем предложение другому узлу:

Здесь метод отправки вызывает сигнальный сервер для передачи информации о предложении.

Обратите внимание, что мы можем реализовать логику метода send с любой серверной технологией.

peerConnection.createOffer(function(offer) {
    send({
        event : "offer",
        data : offer
    });
    peerConnection.setLocalDescription(offer);
}, function(error) {
    // Handle error here
});

9.2. Обработка кандидатов ICE

Во-вторых, нам нужно обработать кандидатов ICE. WebRTC использует протокол ICE (интерактивное установление соединения) для обнаружения пиров и установления соединения.

Когда мы устанавливаем локальное описание для peerConnection, оно запускает событие icecandidate.

Это событие должно передать кандидата удаленному партнеру, чтобы удаленный партнер мог добавить его в свой набор удаленных кандидатов.

Для этого мы создаем прослушиватель для события onicecandidate:

Событие icecandidate запускается снова с пустой строкой кандидата, когда все кандидаты собраны.

Мы также должны передать этот объект-кандидат удаленному партнеру. Мы передаем эту пустую строку кандидата, чтобы убедиться, что удаленный узел знает, что все объекты icecandidate собраны.

peerConnection.onicecandidate = function(event) {
    if (event.candidate) {
        send({
            event : "candidate",
            data : event.candidate
        });
    }
};

Кроме того, это же событие запускается снова, чтобы указать, что сбор кандидатов ICE завершен, а значение объекта-кандидата установлено равным null в событии. Это не нужно передавать удаленному узлу.

9.3. Получение кандидата ICE

В-третьих, нам нужно обработать кандидата ICE, отправленного другим узлом.

Удаленный партнер, получив этого кандидата, должен добавить его в свой пул кандидатов:

9.4. Получение предложения

После этого, когда другой узел получает предложение, он должен установить его в качестве удаленного описания. Кроме того, он должен генерировать ответ, который отправляется инициирующему узлу:

peerConnection.addIceCandidate(new RTCIceCandidate(candidate));

9.5. Получение ответа

Наконец, инициирующий узел получает ответ и устанавливает его в качестве удаленного описания:

peerConnection.setRemoteDescription(new RTCSessionDescription(offer));
peerConnection.createAnswer(function(answer) {
    peerConnection.setLocalDescription(answer);
        send({
            event : "answer",
            data : answer
        });
}, function(error) {
    // Handle error here
});

Таким образом, WebRTC устанавливает успешное соединение.

Теперь мы можем отправлять и получать данные между двумя одноранговыми узлами напрямую, без сигнального сервера.

handleAnswer(answer){
    peerConnection.setRemoteDescription(new RTCSessionDescription(answer));
}

10. Отправка сообщения

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

Аналогично, чтобы получить сообщение на другом peer, давайте создадим прослушиватель для события onmessage:

Чтобы получить сообщение по каналу данных, мы также должны добавить обратный вызов для объекта peerConnection:

dataChannel.send(“message”);

На этом шаге у нас есть создали полнофункциональный канал данных WebRTC. Теперь мы можем отправлять и получать данные между клиентами. Кроме того, мы можем добавить к этому видео- и аудиоканалы.

dataChannel.onmessage = function(event) {
    console.log("Message:", event.data);
};

11. Добавление видео- и аудиоканалов

peerConnection.ondatachannel = function (event) {
    dataChannel = event.channel;
};

Когда WebRTC устанавливает P2P-соединение, мы можем легко передавать аудио- и видеопотоки напрямую.

11.1. Получение медиапотока

Во-первых, нам нужно получить медиапоток из браузера. WebRTC предоставляет API для этого:

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

Объект ограничения также позволяет указать камеру, используемую в случае мобильных устройств:

const constraints = {
    video: true,audio : true
};
navigator.mediaDevices.getUserMedia(constraints).
  then(function(stream) { /* use the stream */ })
    .catch(function(err) { /* handle the error */ });

Кроме того, значение faceMode может быть установлено как «environment» вместо «user», если мы хотим включить задняя камера.

11.2. Отправка потока

var constraints = {
    video : {
        frameRate : {
            ideal : 10,
            max : 15
        },
        width : 1280,
        height : 720,
        facingMode : "user"
    }
};

Во-вторых, мы должны добавить поток в объект однорангового соединения WebRTC:

Добавление потока в одноранговое соединение вызывает событие addstream на подключенных одноранговых узлах.

11.3. Получение потока

peerConnection.addStream(stream);

«В-третьих, чтобы получать поток на удаленном пире, мы можем создать прослушиватель.

Давайте установим этот поток в элемент видео HTML:

12. Проблемы с NAT

В реальном мире брандмауэр и устройства NAT (обход сетевых адресов) соединяют наши устройства с общедоступным Интернетом.

peerConnection.onaddstream = function(event) {
    videoElement.srcObject = event.stream;
};

NAT предоставляет устройству IP-адрес для использования в локальной сети. Таким образом, этот адрес недоступен за пределами локальной сети. Без публичного адреса сверстники не могут общаться с нами.

Для решения этой проблемы WebRTC использует два механизма:

13. Использование STUN

STUN — самый простой подход к этой проблеме. Прежде чем поделиться сетевой информацией с узлом, клиент делает запрос на STUN-сервер. Ответственность сервера STUN заключается в том, чтобы вернуть IP-адрес, с которого он получает запрос.

  1. STUN
  2. TURN

Итак, запрашивая STUN-сервер, мы получаем наш собственный общедоступный IP-адрес. Затем мы передаем этот IP-адрес и информацию о порте узлу, к которому хотим подключиться. Другие одноранговые узлы могут сделать то же самое, чтобы поделиться своими общедоступными IP-адресами.

Чтобы использовать сервер STUN, мы можем просто передать URL-адрес в объекте конфигурации для создания объекта RTCPeerConnection:

14. Использование TURN не удается установить P2P-соединение. Роль сервера TURN заключается в прямой передаче данных между узлами. В этом случае фактический поток данных проходит через серверы TURN. Используя реализации по умолчанию, серверы TURN также действуют как серверы STUN.

Серверы TURN общедоступны, и клиенты могут получить к ним доступ, даже если они находятся за брандмауэром или прокси-сервером.

var configuration = {
    "iceServers" : [ {
        "url" : "stun:stun2.1.google.com:19302"
    } ]
};

Но использование сервера TURN на самом деле не является P2P-соединением, так как присутствует промежуточный сервер.

Примечание: TURN — это последнее средство, когда мы не можем установить P2P-соединение. Поскольку данные проходят через сервер TURN, требуется большая полоса пропускания, и в этом случае мы не используем P2P.

Подобно STUN, мы можем указать URL-адрес сервера TURN в том же объекте конфигурации:

15. Заключение

В этом руководстве мы обсудили, что такое проект WebRTC, и представили его основные концепции. Затем мы создали простое приложение для обмена данными между двумя HTML-клиентами.

Мы также обсудили этапы создания и установления соединения WebRTC.

{
  'iceServers': [
    {
      'urls': 'stun:stun.l.google.com:19302'
    },
    {
      'urls': 'turn:10.158.29.39:3478?transport=udp',
      'credential': 'XXXXXXXXXXXXX',
      'username': 'XXXXXXXXXXXXXXX'
    },
    {
      'urls': 'turn:10.158.29.39:3478?transport=tcp',
      'credential': 'XXXXXXXXXXXXX',
      'username': 'XXXXXXXXXXXXXXX'
    }
  ]
}

Кроме того, мы рассмотрели использование серверов STUN и TURN в качестве резервного механизма в случае сбоя WebRTC.

Вы можете ознакомиться с примерами, представленными в этой статье, на GitHub.

«

Furthermore, we looked into the use of STUN and TURN servers as a fallback mechanism when WebRTC fails.

You can check out the examples provided in this article over on GitHub.