Перейти к основному содержимому

Руководство по WebRTC

· 12 мин. чтения

1. Обзор

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

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

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

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

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

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

./9dec81af6a72d2a20659d1f5c74d9354.png

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

./4c866b204e5d02c3976f7637ffbea7ad.png

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

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

./1516cc5b074d2db9adc10f82ab42bce1.png

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

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 :

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

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

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

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

@Configuration
@EnableWebSocket
public class WebSocketConfiguration implements WebSocketConfigurer {

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

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

5.2. Создание обработчика сообщений на Signaling Server

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

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

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

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

@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);
}
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

9. Установление соединения с ICE

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

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

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

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

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

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

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

9.2. Работа с кандидатами ICE

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

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

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

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

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

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

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

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

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

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

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

peerConnection.addIceCandidate(new RTCIceCandidate(candidate));

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

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

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

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

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

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

При этом WebRTC устанавливает успешное соединение.

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

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

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

dataChannel.send(“message”);

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

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

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

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

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

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

Когда 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 */ });

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

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

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

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

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

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

peerConnection.addStream(stream);

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

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

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

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

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

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

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

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

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

  1. ОШЕЛОМЛЕНИЕ
  2. ПОВЕРНУТЬ

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

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

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

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

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

14. Использование ПОВОРОТ

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

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

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

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

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

{
'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'
}
]
}

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

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

Мы также обсудили шаги, связанные с созданием и установлением соединения WebRTC.

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

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