Начало работы с одноранговыми соединениями

Одноранговые соединения — это часть спецификации WebRTC, которая касается соединения двух приложений на разных компьютерах для связи с использованием однорангового протокола. Связь между одноранговыми узлами может быть видео-, аудио- или произвольными двоичными данными (для клиентов, поддерживающих RTCDataChannel API). Чтобы узнать, как два одноранговых узла могут подключиться, оба клиента должны предоставить конфигурацию ICE Server. Это либо STUN, либо TURN-сервер, и их роль заключается в предоставлении ICE-кандидатов каждому клиенту, который затем передается удаленному узлу. Эта передача кандидатов ICE обычно называется сигнализацией.

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

Спецификация WebRTC включает API для связи с сервером ICE (Internet Connectivity Establishment), но компонент сигнализации не является его частью. Сигнализация необходима для того, чтобы два одноранговых узла могли совместно использовать способ подключения. Обычно это решается с помощью обычного веб-API на основе HTTP (то есть службы REST или другого механизма RPC), где веб-приложения могут передавать необходимую информацию до того, как будет инициировано одноранговое соединение.

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

// Set up an asynchronous communication channel that will be
// used during the peer connection setup
const signalingChannel = new SignalingChannel(remoteClientId);
signalingChannel.addEventListener('message', message => {
    // New message from remote client received
});

// Send an asynchronous message to the remote client
signalingChannel.send('Hello!');

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

Инициирование одноранговых соединений

Каждое одноранговое соединение обрабатывается объектом RTCPeerConnection . Конструктор этого класса принимает в качестве параметра один объект RTCConfiguration . Этот объект определяет, как устанавливается одноранговое соединение, и должен содержать информацию об используемых серверах ICE.

После создания RTCPeerConnection нам нужно создать предложение или ответ SDP, в зависимости от того, являемся ли мы вызывающим или принимающим узлом. После создания предложения или ответа SDP его необходимо отправить удаленному узлу по другому каналу. Передача объектов SDP удаленным одноранговым узлам называется сигнализацией и не рассматривается в спецификации WebRTC.

Чтобы инициировать установку однорангового соединения с вызывающей стороны, мы создаем объект RTCPeerConnection , а затем вызываем createOffer() для создания объекта RTCSessionDescription . Это описание сеанса устанавливается как локальное описание с помощью setLocalDescription() и затем отправляется по нашему сигнальному каналу на принимающую сторону. Мы также настроили прослушиватель для нашего сигнального канала, когда от принимающей стороны будет получен ответ на предлагаемое нами описание сеанса.

async function makeCall() {
    const configuration = {'iceServers': [{'urls': 'stun:stun.l.google.com:19302'}]}
    const peerConnection = new RTCPeerConnection(configuration);
    signalingChannel.addEventListener('message', async message => {
        if (message.answer) {
            const remoteDesc = new RTCSessionDescription(message.answer);
            await peerConnection.setRemoteDescription(remoteDesc);
        }
    });
    const offer = await peerConnection.createOffer();
    await peerConnection.setLocalDescription(offer);
    signalingChannel.send({'offer': offer});
}

На принимающей стороне мы ждем входящего предложения, прежде чем создавать наш экземпляр RTCPeerConnection . Как только это будет сделано, мы установим полученное предложение, используя setRemoteDescription() . Далее мы вызываем createAnswer() для создания ответа на полученное предложение. Этот ответ устанавливается как локальное описание с помощью setLocalDescription() и затем отправляется вызывающей стороне через наш сигнальный сервер.

const peerConnection = new RTCPeerConnection(configuration);
signalingChannel.addEventListener('message', async message => {
    if (message.offer) {
        peerConnection.setRemoteDescription(new RTCSessionDescription(message.offer));
        const answer = await peerConnection.createAnswer();
        await peerConnection.setLocalDescription(answer);
        signalingChannel.send({'answer': answer});
    }
});

Как только два одноранговых узла установили описания локального и удаленного сеанса, они узнают возможности удаленного однорангового узла. Это не означает, что соединение между узлами готово. Чтобы это работало, нам нужно собрать кандидатов ICE на каждом узле и передать (по сигнальному каналу) другому узлу.

ICE кандидаты

Прежде чем два одноранговых узла смогут общаться с помощью WebRTC, им необходимо обменяться информацией о подключении. Поскольку условия сети могут меняться в зависимости от ряда факторов, для обнаружения возможных кандидатов на подключение к узлу обычно используется внешний сервис. Эта служба называется ICE и использует сервер STUN или TURN. STUN расшифровывается как Session Traversal Utilities для NAT и обычно косвенно используется в большинстве приложений WebRTC.

TURN (Traversal Using Relay NAT) — это более продвинутое решение, которое включает в себя протоколы STUN, и большинство коммерческих сервисов на основе WebRTC используют сервер TURN для установления соединений между одноранговыми узлами. API WebRTC напрямую поддерживает как STUN, так и TURN, и он объединяется под более полным термином «Установление подключения к Интернету». При создании WebRTC-соединения мы обычно предоставляем один или несколько ICE-серверов в конфигурации для объекта RTCPeerConnection .

Струйка льда

После создания объекта RTCPeerConnection базовая структура использует предоставленные серверы ICE для сбора кандидатов на установление соединения (кандидатов ICE). Событие icegatheringstatechange на RTCPeerConnection сигнализирует о том, в каком состоянии находится сбор ICE ( new , gathering или complete ).

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

Чтобы собрать кандидатов ICE, просто добавьте прослушиватель для события icecandidate . RTCPeerConnectionIceEvent , созданное этим прослушивателем, будет содержать свойство candidate , представляющее нового кандидата, который должен быть отправлен удаленному узлу (см. Сигнализация).

// Listen for local ICE candidates on the local RTCPeerConnection
peerConnection.addEventListener('icecandidate', event => {
    if (event.candidate) {
        signalingChannel.send({'new-ice-candidate': event.candidate});
    }
});

// Listen for remote ICE candidates and add them to the local RTCPeerConnection
signalingChannel.addEventListener('message', async message => {
    if (message.iceCandidate) {
        try {
            await peerConnection.addIceCandidate(message.iceCandidate);
        } catch (e) {
            console.error('Error adding received ice candidate', e);
        }
    }
});

Соединение установлено

Как только кандидаты ICE получены, мы должны ожидать, что состояние нашего однорангового соединения в конечном итоге изменится на подключенное состояние. Чтобы обнаружить это, мы добавляем прослушиватель в наш RTCPeerConnection , где мы прослушиваем события connectionstatechange .

// Listen for connectionstatechange on the local RTCPeerConnection
peerConnection.addEventListener('connectionstatechange', event => {
    if (peerConnection.connectionState === 'connected') {
        // Peers connected!
    }
});

Документация по API RTCPeerConnection