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

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

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

Спецификация WebRTC включает API для связи с сервером ICE (установления подключения к Интернету), но компонент сигнализации не является ее частью. Сигнализация необходима для того, чтобы два узла могли совместно использовать способ подключения. Обычно это решается с помощью обычного веб-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 означает «Утилиты обхода сеанса для NAT» и обычно используется косвенно в большинстве приложений WebRTC.

TURN (обход с использованием 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

,

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

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

Спецификация WebRTC включает API для связи с сервером ICE (установления подключения к Интернету), но компонент сигнализации не является ее частью. Сигнализация необходима для того, чтобы два узла могли совместно использовать способ подключения. Обычно это решается с помощью обычного веб-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 означает «Утилиты обхода сеанса для NAT» и обычно используется косвенно в большинстве приложений WebRTC.

TURN (обход с использованием 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