Spec-Zone.ru › Web APIs

Пример простого RTCDataChannel

Интерфейс RTCDataChannel — это функция API WebRTC, которая позволяет открыть канал связи между двумя участниками для отправки и получения произвольных данных. API намеренно похож на WebSocket API, чтобы для каждого из них можно было использовать одну и ту же модель программирования.

В этом примере мы откроем соединение RTCDataChannel, связывающее два элемента на одной странице. Хотя это, очевидно, искусственная ситуация, она полезна для демонстрации процесса подключения двух участников. Мы рассмотрим механизм подключения и передачи и получения данных, но сохраним информацию о поиске и подключении к удалённому компьютеру для другого примера.

HTML

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

<button id="connectButton" name="connectButton" class="buttonleft">
  Connect
</button>
<button
  id="disconnectButton"
  name="disconnectButton"
  class="buttonright"
  disabled>
  Disconnect
</button>

Затем есть блок, содержащий поле ввода текста, в которое пользователь может ввести сообщение для передачи, и кнопку для отправки введённого текста. Этот <div> будет первым участником в канале.

<div class="messagebox">
  <label for="message"
    >Enter a message:
    <input
      type="text"
      name="message"
      id="message"
      placeholder="Message text"
      inputmode="latin"
      size="60"
      maxlength="120"
      disabled />
  </label>
  <button id="sendButton" name="sendButton" class="buttonright" disabled>
    Send
  </button>
</div>

Наконец, есть небольшой блок, в который мы вставим сообщения. Этот <div> блок будет вторым участником.

<div class="messagebox" id="receive-box">
  <p>Messages received:</p>
</div>

Код JavaScript

Хотя вы можете просто посмотреть сам код на GitHub, ниже мы рассмотрим части кода, выполняющие основную работу.

Инициализация

При запуске скрипта мы создаём обработчик события load, чтобы после полной загрузки страницы вызывалась наша функция startup().

let connectButton = null;
let disconnectButton = null;
let sendButton = null;
let messageInputBox = null;
let receiveBox = null;

let localConnection = null; // RTCPeerConnection for our "local" connection
let remoteConnection = null; // RTCPeerConnection for the "remote"

let sendChannel = null; // RTCDataChannel for the local (sender)
let receiveChannel = null; // RTCDataChannel for the remote (receiver)

function startup() {
  connectButton = document.getElementById("connectButton");
  disconnectButton = document.getElementById("disconnectButton");
  sendButton = document.getElementById("sendButton");
  messageInputBox = document.getElementById("message");
  receiveBox = document.getElementById("receive-box");

  // Set event listeners for user interface widgets

  connectButton.addEventListener("click", connectPeers, false);
  disconnectButton.addEventListener("click", disconnectPeers, false);
  sendButton.addEventListener("click", sendMessage, false);
}

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

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

При нажатии пользователем кнопки «Подключиться» вызывается метод connectPeers(). Для большей ясности мы разделим его на части.

Примечание: Хотя оба конца нашего соединения будут на одной странице, мы будем называть начавшее соединение «локальным», а другое — «удалённым» концом.

Настройка локального участника

localConnection = new RTCPeerConnection();

sendChannel = localConnection.createDataChannel("sendChannel");
sendChannel.onopen = handleSendChannelStatusChange;
sendChannel.onclose = handleSendChannelStatusChange;

Первый шаг — создать «локальный» конец соединения. Это участник, который отправит запрос на подключение. Следующий шаг — создать RTCDataChannel, вызвав RTCPeerConnection.createDataChannel(), и настроить обработчики событий для мониторинга канала, чтобы мы знали, когда он открыт и закрыт (то есть, когда канал подключён или отключён в рамках этого соединения).

Важно помнить, что каждый конец канала имеет свой собственный объект RTCDataChannel.

Настройка удалённого участника

remoteConnection = new RTCPeerConnection();
remoteConnection.ondatachannel = receiveChannelCallback;

Удалённый конец настраивается аналогично, за исключением того, что нам не нужно явно создавать RTCDataChannel, так как мы будем подключены через созданный выше канал. Вместо этого мы настраиваем обработчик события datachannel; он вызывается при открытии канала данных; этот обработчик получит объект RTCDataChannel; вы увидите это ниже.

Настройка кандидатов ICE

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

Примечание: В реальной ситуации, когда два участника работают не в одном контексте, процесс немного сложнее; каждая сторона по очереди предлагает способ подключения (например, UDP, UDP с ретрансляцией, TCP и т. д.), вызывая RTCPeerConnection.addIceCandidate(), и они обмениваются предложениями до достижения согласия. Но здесь мы просто принимаем первое предложение с каждой стороны, так как никакой реальной сетевой работы нет.

localConnection.onicecandidate = (e) =>
  !e.candidate ||
  remoteConnection.addIceCandidate(e.candidate).catch(handleAddCandidateError);

remoteConnection.onicecandidate = (e) =>
  !e.candidate ||
  localConnection.addIceCandidate(e.candidate).catch(handleAddCandidateError);

Мы настраиваем каждый RTCPeerConnection с обработчиком события icecandidate.

Начать попытку соединения

Последнее, что нам нужно сделать для начала подключения наших участников, это создать предложение подключения.

localConnection
  .createOffer()
  .then((offer) => localConnection.setLocalDescription(offer))
  .then(() =>
    remoteConnection.setRemoteDescription(localConnection.localDescription),
  )
  .then(() => remoteConnection.createAnswer())
  .then((answer) => remoteConnection.setLocalDescription(answer))
  .then(() =>
    localConnection.setRemoteDescription(remoteConnection.localDescription),
  )
  .catch(handleCreateDescriptionError);

Давайте пройдёмся по каждой строке и расшифруем её значение.

  1. Сначала мы вызываем метод RTCPeerConnection.createOffer() для создания фрагмента SDP (протокол описания сессии), описывающего желаемое соединение. Этот метод принимает необязательный объект с ограничениями, которые должно удовлетворять соединение, например, поддерживает ли соединение аудио, видео или оба. В нашем простом примере ограничений нет.
  2. Если предложение создано успешно, мы передаём фрагмент SDP методу RTCPeerConnection.setLocalDescription() локального соединения. Это настраивает локальный конец соединения.
  3. Следующий шаг — подключить локального участника к удалённому, сообщив удалённому участнику о нём. Это делается путём вызова remoteConnection.setRemoteDescription(). Теперь удалённый участник знает о формируемом соединении. В реальном приложении для этого требуется сервер обмена сообщениями.
  4. Это означает, что настала очередь удалённого участника ответить. Он делает это, вызвав свой метод createAnswer(). Это генерирует фрагмент SDP, описывающий соединение, которое удалённый участник готов и способен установить. Эта конфигурация находится где-то в совокупности доступных обоим участникам опций.
  5. После создания ответа он передаётся в удалённое соединение путём вызова RTCPeerConnection.setLocalDescription() удалённого участника. Это устанавливает удалённый конец соединения (который для удалённого участника является локальным концом. Эта штука может быть запутанной, но вы к ней привыкнете). Опять же, обычно это происходит через сервер обмена сообщениями.
  6. Наконец, локальное соединение устанавливает удалённое описание, ссылающееся на удалённого участника, вызвав метод RTCPeerConnection.setRemoteDescription() локального соединения.
  7. Функция catch() вызывает процедуру обработки возникших ошибок.

Примечание: Ещё раз, это не реальная реализация; в обычном использовании есть два блока кода, работающих на двух машинах, взаимодействующих и договаривающихся о соединении. Для обмена описанием (которое находится в формате application/sdp) между двумя участниками обычно используется вспомогательный канал, обычно называемый «сервером обмена сообщениями».

Обработка успешного подключения участников

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

function handleCreateDescriptionError(error) {
  console.log(`Unable to create an offer: ${error.toString()}`);
}

function handleLocalAddCandidateSuccess() {
  connectButton.disabled = true;
}

function handleRemoteAddCandidateSuccess() {
  disconnectButton.disabled = false;
}

function handleAddCandidateError() {
  console.log("Oh noes! addICECandidate failed!");
}

Единственное, что мы делаем здесь, — это отключаем кнопку «Подключиться» при подключении локального участника и включаем кнопку «Отключиться» при подключении удалённого участника.

Подключение канала данных

После открытия RTCPeerConnection событие datachannel отправляется удалённому участнику для завершения процесса открытия канала данных; это вызывает нашу функцию receiveChannelCallback(), которая выглядит так:

function receiveChannelCallback(event) {
  receiveChannel = event.channel;
  receiveChannel.onmessage = handleReceiveMessage;
  receiveChannel.onopen = handleReceiveChannelStatusChange;
  receiveChannel.onclose = handleReceiveChannelStatusChange;
}

Событие datachannel содержит в своём свойстве channel ссылку на RTCDataChannel, представляющую удалённый конец канала. Это сохраняется, и мы настраиваем на канале обработчики событий для событий, которые хотим обработать. После этого наша функция handleReceiveMessage() будет вызываться каждый раз, когда удалённый участник получает данные, и функция handleReceiveChannelStatusChange() будет вызываться всякий раз, когда состояние соединения канала изменяется, поэтому мы можем реагировать, когда канал полностью открыт и когда он закрыт.

Обработка изменений статуса канала

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

Когда локальный узел испытывает событие открытия или закрытия, вызывается метод handleSendChannelStatusChange():

function handleSendChannelStatusChange(event) {
  if (sendChannel) {
    const state = sendChannel.readyState;

    if (state === "open") {
      messageInputBox.disabled = false;
      messageInputBox.focus();
      sendButton.disabled = false;
      disconnectButton.disabled = false;
      connectButton.disabled = true;
    } else {
      messageInputBox.disabled = true;
      sendButton.disabled = true;
      connectButton.disabled = false;
      disconnectButton.disabled = true;
    }
  }
}

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

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

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

function handleReceiveChannelStatusChange(event) {
  if (receiveChannel) {
    console.log(
      `Receive channel's status has changed to ${receiveChannel.readyState}`,
    );
  }
}

Метод handleReceiveChannelStatusChange() получает в качестве входного параметра произошедшее событие; это будет событие RTCDataChannelEvent.

Отправка сообщений

Когда пользователь нажимает кнопку «Отправить», вызывается метод sendMessage(), который мы установили как обработчик события click кнопки. Этот метод достаточно прост:

function sendMessage() {
  const message = messageInputBox.value;
  sendChannel.send(message);

  messageInputBox.value = "";
  messageInputBox.focus();
}

Во-первых, текст сообщения извлекается из атрибута value поля ввода. Затем это сообщение отправляется удаленному узлу с помощью вызова sendChannel.send(). Вот и всё! Остальная часть метода — это просто удобства для пользовательского интерфейса: поле ввода очищается и получает фокус, чтобы пользователь мог сразу начать печатать другое сообщение.

Получение сообщений

Когда на удаленном канале происходит событие «сообщение», наш метод handleReceiveMessage() вызывается как обработчик события.

function handleReceiveMessage(event) {
  const el = document.createElement("p");
  const textNode = document.createTextNode(event.data);

  el.appendChild(textNode);
  receiveBox.appendChild(el);
}

Этот метод выполняет некоторые базовые операции вставки в DOM; он создает новый элемент <p> (абзац), затем создает новый узел Text с текстом сообщения, который получен в свойстве data события. Этот узел текста добавляется как дочерний элемент нового элемента, который затем вставляется в блок receiveBox, тем самым вызывая его отображение в окне браузера.

Отключение узлов

Когда пользователь нажимает кнопку «Отключиться», вызывается метод disconnectPeers(), ранее установленный как обработчик этой кнопки.

function disconnectPeers() {
  // Close the RTCDataChannels if they're open.

  sendChannel.close();
  receiveChannel.close();

  // Close the RTCPeerConnections

  localConnection.close();
  remoteConnection.close();

  sendChannel = null;
  receiveChannel = null;
  localConnection = null;
  remoteConnection = null;

  // Update user interface elements

  connectButton.disabled = false;
  disconnectButton.disabled = true;
  sendButton.disabled = true;

  messageInputBox.value = "";
  messageInputBox.disabled = true;
}

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

Следующие шаги

Посмотрите на исходный код webrtc-simple-datachannel, доступный на GitHub.

См. также

  • Сигнальные сообщения и видеозвонки.
  • Шаблон Совершенное согласование.

© 2005–2024 MDN contributors.
Licensed under the Creative Commons Attribution-ShareAlike License v2.5 or later.
https://developer.mozilla.org/en-US/docs/Web/API/WebRTC_API/Simple_RTCDataChannel_sample

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API