Включить связь в реальном времени с помощью WebRTC

1. Прежде чем начать

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

Предпосылки

  • Базовые знания HTML, CSS и JavaScript

Что вы будете строить

  • Получите видео с веб-камеры.
  • Потоковое видео с RTCPeerConnection .
  • Потоковая передача данных с помощью RTCDataChannel .
  • Настройте службу сигнализации для обмена сообщениями.
  • Объедините одноранговое соединение и сигнализацию.
  • Сделайте снимок и используйте канал данных, чтобы поделиться им.

Что вам понадобится

  • Chrome 47 или выше
  • Веб-сервер для Chrome или веб-сервер по вашему выбору
  • Текстовый редактор на ваш выбор
  • Node.js

2. Получите пример кода

Скачать код

  1. Если вы знакомы с Git, запустите эту команду, чтобы клонировать код для этой кодовой лаборатории с GitHub:
git clone https://github.com/googlecodelabs/webrtc-web

Или щелкните эту ссылку, чтобы загрузить zip-файл с кодом:

  1. Откройте загруженный zip-файл, чтобы распаковать папку проекта с именем webrtc-web-master , которая содержит одну папку для каждого шага этой лаборатории кода и все необходимые ресурсы.

Вы выполняете всю свою работу с кодом в каталоге с именем work .

Папки step-nn содержат готовую версию для каждого шага этой лаборатории кода. Они там для справки.

Установить и проверить веб-сервер

Хотя вы можете использовать свой собственный веб-сервер, эта лаборатория кода хорошо работает с веб-сервером для Chrome.

  1. Если у вас нет веб-сервера для Chrome, щелкните эту ссылку, чтобы установить его из Интернет-магазина Chrome:

d0a4649b4920cf3.png

  1. Нажмите Добавить в Chrome , чтобы установить веб-сервер для Chrome и автоматически открыть приложения Google в новой вкладке.
  2. Нажмите Веб-сервер :

27fce4494f641883.png

Появится диалоговое окно, позволяющее настроить локальный веб-сервер:

a300381a486b9e22.png

  1. Щелкните Выбрать папку.
  2. Выберите work папку, которую вы создали.

В разделе URL- адреса веб-сервера вы видите URL-адрес, по которому вы можете просмотреть свою текущую работу в

Хром.

  1. В разделе « Параметры» (может потребоваться перезагрузка) установите флажок « Автоматически показывать index.html» .
  2. Переключение веб-сервера: запускался дважды, чтобы остановить и перезапустить сервер.

f23cafb3993dfac1.png

  1. Щелкните URL-адрес в разделе URL-адреса веб-сервера , чтобы увидеть свою работу в веб-браузере.

Вы должны увидеть страницу, которая выглядит так, что соответствует work/index.html :

18a705cb6ccc5181.png

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

3. Потоковое видео с веб-камеры

Полная версия этого шага находится в папке step-01 .

Добавьте штрих HTML

Скопируйте этот код и вставьте его в файл index.html в вашем work каталоге, чтобы добавить video и элемент script :

<!DOCTYPE html>
<html>

<head>

  <title>Real-time communication with WebRTC</title>

  <link rel="stylesheet" href="css/main.css" />

</head>

<body>

  <h1>Real-time communication with WebRTC</h1>

  <video autoplay playsinline></video>

  <script src="js/main.js"></script>

</body>

</html>

Добавьте щепотку JavaScript

Скопируйте этот код и вставьте его в файл main.js в вашей папке js :

'use strict';

// In this codelab, you  only stream video, not audio (video: true).
const mediaStreamConstraints = {
  video: true,
};

// The video element where the stream is displayed
const localVideo = document.querySelector('video');

// The local stream that's displayed on the video
let localStream;

// Handle success and add the MediaStream to the video element
function gotLocalMediaStream(mediaStream) {
  localStream = mediaStream;
  localVideo.srcObject = mediaStream;
}

// Handle error and log a message to the console with the error message
function handleLocalMediaStreamError(error) {
  console.log('navigator.getUserMedia error: ', error);
}

// Initialize media stream
navigator.mediaDevices.getUserMedia(mediaStreamConstraints)
  .then(gotLocalMediaStream).catch(handleLocalMediaStreamError);

Попытайся

Откройте файл index.html в своем браузере, и вы должны увидеть что-то вроде этого, но, конечно, с видом с вашей веб-камеры:

9297048e43ed0f3d.png

Как это работает

После getUserMedia() браузер запрашивает разрешение на доступ к вашей камере, если это первый запрос на доступ к камере для текущего источника.

В случае успеха возвращается MediaStream , который media -элемент может использовать через атрибут srcObject :

navigator.mediaDevices.getUserMedia(mediaStreamConstraints)
  .then(gotLocalMediaStream).catch(handleLocalMediaStreamError);


}
function gotLocalMediaStream(mediaStream) {
  localVideo.srcObject = mediaStream;
}

Аргумент constraints позволяет вам указать, какой носитель нужно получить. В этом примере мультимедиа — это только видео, потому что аудио по умолчанию отключено:

const mediaStreamConstraints = {
  video: true,
};

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

const hdConstraints = {
  video: {
    width: {
      min: 1280
    },
    height: {
      min: 720
    }
  }
}

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

В случае getUserMedia() видеопоток с веб-камеры устанавливается в качестве источника видеоэлемента:

function gotLocalMediaStream(mediaStream) {
  localVideo.srcObject = mediaStream;
}

Набрать бонусные баллы

  • Объект localStream , переданный в getUserMedia() , находится в глобальной области видимости, поэтому вы можете проверить его из консоли браузера. Откройте консоль, введите stream, и нажмите Enter ( Return на Mac). Чтобы просмотреть консоль в Chrome, нажмите Control+Shift+J (или Command+Option+J на Mac).
  • Что localStream.getVideoTracks() ?
  • Вызовите localStream.getVideoTracks()[0].stop() .
  • Посмотрите на объект ограничений. Что произойдет, если вы измените его на {audio: true, video: true} ?
  • Какого размера элемент видео? Как вы можете получить естественный размер видео из JavaScript, а не размер дисплея? Используйте инструменты разработчика Google Chrome для проверки.
  • Добавьте фильтры CSS к элементу видео, например:
video {
  filter: blur(4px) invert(1) opacity(0.5);
}
  • Добавьте фильтры SVG, например:
video {
   filter: hue-rotate(180deg) saturate(200%);
 }

Советы

Лучшая практика

Убедитесь, что ваш элемент видео не переполняет контейнер. Эта лаборатория кода добавила width и max-width , чтобы установить предпочтительный размер и максимальный размер видео. Ваш браузер вычисляет высоту автоматически.

video {
  max-width: 100%;
  width: 320px;
}

4. Потоковое видео с API RTCPeerConnection

Полная версия этого шага находится в папке step-2 .

Добавьте видеоэлементы и кнопки управления

В файле index.html замените один элемент video двумя элементами video и тремя элементами button :

<video id="localVideo" autoplay playsinline></video>
<video id="remoteVideo" autoplay playsinline></video>


<div>
  <button id="startButton">Start</button>
  <button id="callButton">Call</button>
  <button id="hangupButton">Hang Up</button>
</div>

Один элемент видео отображает поток от getUserMedia() , а другой показывает то же самое видео, переданное через RTCPeerconnection . (В реальном приложении один video будет отображать локальный поток, а другой — удаленный.)

Добавьте прокладку адаптера.js

Скопируйте этот элемент сценария и вставьте его над элементом сценария для main.js :

<script src="https://webrtc.github.io/adapter/adapter-latest.js"></script>

Теперь ваш файл index.html должен выглядеть так:

<!DOCTYPE html>
<html>

<head>
  <title>Real-time communication with WebRTC</title>
  <link rel="stylesheet" href="css/main.css" />
</head>

<body>
  <h1>Real-time communication with WebRTC</h1>

  <video id="localVideo" autoplay playsinline></video>
  <video id="remoteVideo" autoplay playsinline></video>

  <div>
    <button id="startButton">Start</button>
    <button id="callButton">Call</button>
    <button id="hangupButton">Hang Up</button>
  </div>

  <script src="https://webrtc.github.io/adapter/adapter-latest.js"></script>
  <script src="js/main.js"></script>
</body>
</html>

Установите код RTCPeerConnection

Замените main.js версией в папке step-02 .

Сделать звонок

  1. Откройте файл index.html .
  2. Нажмите «Пуск» , чтобы получить видео с веб-камеры.
  3. Нажмите « Позвонить» , чтобы установить одноранговое соединение.

Вы должны увидеть одно и то же видео с веб-камеры в обоих video .

  1. Откройте консоль браузера, чтобы просмотреть ведение журнала WebRTC.

Как это работает

Этот шаг делает многое.

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

В этом примере два объекта RTCPeerConnection находятся на одной странице: pc1 и pc2 .

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

  1. Создайте RTCPeerConnection для каждого конца вызова и на каждом конце добавьте локальный поток из getUserMedia() .
  2. Получайте и делитесь информацией о сети.

Потенциальные конечные точки подключения известны как кандидаты ICE .

  1. Получайте и делитесь локальными и удаленными описаниями.

Метаданные о локальном носителе представлены в формате протокола описания сеанса (SDP).

Представьте, что Алиса и Боб хотят использовать RTCPeerConnection для настройки видеочата.

Сначала Алиса и Боб обмениваются сетевой информацией. Выражение « поиск кандидатов » относится к процессу поиска сетевых интерфейсов и портов с использованием фреймворка ICE .

  1. Алиса создает объект RTCPeerConnection с onicecandidate (addEventListener('icecandidate')) .

Это соответствует следующему коду из main.js :

let localPeerConnection;
localPeerConnection = new RTCPeerConnection(servers);
localPeerConnection.addEventListener('icecandidate', handleConnection);
localPeerConnection.addEventListener(
    'iceconnectionstatechange', handleConnectionChange);
  1. Алиса вызывает getUserMedia() и добавляет к нему переданный поток:
navigator.mediaDevices.getUserMedia(mediaStreamConstraints).
  then(gotLocalMediaStream).
  catch(handleLocalMediaStreamError);
function gotLocalMediaStream(mediaStream) {
  localVideo.srcObject = mediaStream;
  localStream = mediaStream;
  trace('Received local stream.');
  callButton.disabled = false;  // Enable call button.
}
localPeerConnection.addStream(localStream);
trace('Added local stream to localPeerConnection.');
  1. Обработчик onicecandidate из первого шага вызывается, когда становятся доступными сетевые кандидаты.
  2. Алиса отправляет сериализованные данные кандидата Бобу.

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

  1. Когда Боб получает сообщение кандидата от Алисы, он вызывает addIceCandidate() , чтобы добавить кандидата в описание удаленного узла:
function handleConnection(event) {
  const peerConnection = event.target;
  const iceCandidate = event.candidate;

  if (iceCandidate) {
    const newIceCandidate = new RTCIceCandidate(iceCandidate);
    const otherPeer = getOtherPeer(peerConnection);

    otherPeer.addIceCandidate(newIceCandidate)
      .then(() => {
        handleConnectionSuccess(peerConnection);
      }).catch((error) => {
        handleConnectionFailure(peerConnection, error);
      });

    trace(`${getPeerName(peerConnection)} ICE candidate:\n` +
          `${event.candidate.candidate}.`);
  }
}

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

  1. Алиса запускает RTCPeerConnection createOffer() .

Возвращаемое обещание предоставляет RTCSessionDescription — описание локального сеанса Алисы:

trace('localPeerConnection createOffer start.');
localPeerConnection.createOffer(offerOptions)
  .then(createdOffer).catch(setSessionDescriptionError);
  1. В случае успеха Алиса устанавливает локальное описание с помощью setLocalDescription() а затем отправляет это описание сеанса Бобу через их сигнальный канал.
  2. Боб устанавливает описание, отправленное ему Алисой, как удаленное описание с помощью setRemoteDescription() .
  3. Боб запускает метод RTCPeerConnection createAnswer() и передает ему удаленное описание, полученное от Алисы, чтобы сгенерировать локальный сеанс, совместимый с ее сеансом.
  4. createAnswer() передает RTCSessionDescription , которое Боб устанавливает как локальное описание и отправляет Алисе.
  5. Когда Алиса получает описание сеанса Боба, она устанавливает его как удаленное описание с помощью setRemoteDescription() .
// Logs offer creation and sets peer connection session descriptions
function createdOffer(description) {
  trace(`Offer from localPeerConnection:\n${description.sdp}`);

  trace('localPeerConnection setLocalDescription start.');
  localPeerConnection.setLocalDescription(description)
    .then(() => {
      setLocalDescriptionSuccess(localPeerConnection);
    }).catch(setSessionDescriptionError);

  trace('remotePeerConnection setRemoteDescription start.');
  remotePeerConnection.setRemoteDescription(description)
    .then(() => {
      setRemoteDescriptionSuccess(remotePeerConnection);
    }).catch(setSessionDescriptionError);

  trace('remotePeerConnection createAnswer start.');
  remotePeerConnection.createAnswer()
    .then(createdAnswer)
    .catch(setSessionDescriptionError);
}

// Logs answer to offer creation and sets peer-connection session descriptions
function createdAnswer(description) {
  trace(`Answer from remotePeerConnection:\n${description.sdp}.`);

  trace('remotePeerConnection setLocalDescription start.');
  remotePeerConnection.setLocalDescription(description)
    .then(() => {
      setLocalDescriptionSuccess(remotePeerConnection);
    }).catch(setSessionDescriptionError);

  trace('localPeerConnection setRemoteDescription start.');
  localPeerConnection.setRemoteDescription(description)
    .then(() => {
      setRemoteDescriptionSuccess(localPeerConnection);
    }).catch(setSessionDescriptionError);
}

Набрать бонусные баллы

  1. Перейдите к chrome://webrtc-internals.

На этой странице представлена ​​статистика WebRTC и данные отладки. (Вы можете найти полный список URL-адресов Chrome по адресу chrome://about.)

  1. Стилизовать страницу с помощью CSS:
  2. Поместите видео рядом.
  3. Сделайте кнопки одинаковой ширины с более крупным текстом.
  4. Убедитесь, что макет работает на мобильных устройствах.
  5. В консоли инструментов разработчика Chrome просмотрите localStream , localPeerConnection и remotePeerConnection .
  6. В консоли посмотрите на localPeerConnectionpc1.localDescription .

Как выглядит формат SDP?

Советы

  • Дополнительные сведения о прокладке adapter.js см. в GitHub-репозитории adapter.js .
  • Взгляните на AppRTC и его код , каноническое приложение проекта WebRTC для вызовов WebRTC. Время установления вызова составляет менее 500 мс.

Лучшая практика

Чтобы защитить свой код в будущем, используйте новые API-интерфейсы на основе Promise и включите совместимость с браузерами, которые их не поддерживают, с помощью adapter.js .

5. Используйте канал данных для обмена данными

Полная версия этого шага находится в папке step-03 .

Обновите свой HTML

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

Удалите элементы video и button из index.html, и замените их следующим HTML-кодом:

<textarea id="dataChannelSend" disabled
    placeholder="Press Start, enter some text, then press Send."></textarea>
<textarea id="dataChannelReceive" disabled></textarea>

<div id="buttons">
  <button id="startButton">Start</button>
  <button id="sendButton">Send</button>
  <button id="closeButton">Stop</button>
</div>

Одно textarea предназначено для ввода текста, а другое — для отображения текста в потоковом режиме между одноранговыми узлами.

Теперь ваш файл index.html должен выглядеть так:

<!DOCTYPE html>
<html>

<head>

  <title>Real-time communication with WebRTC</title>

  <link rel="stylesheet" href="css/main.css" />

</head>

<body>

  <h1>Real-time communication with WebRTC</h1>

  <textarea id="dataChannelSend" disabled
    placeholder="Press Start, enter some text, then press Send."></textarea>
  <textarea id="dataChannelReceive" disabled></textarea>

  <div id="buttons">
    <button id="startButton">Start</button>
    <button id="sendButton">Send</button>
    <button id="closeButton">Stop</button>
  </div>

  <script src="https://webrtc.github.io/adapter/adapter-latest.js"></script>
  <script src="js/main.js"></script>

</body>

</html>

Обновите свой JavaScript

  1. Замените main.js содержимым step-03/js/main.js .
  1. Попробуйте передавать данные между узлами:
  2. Откройте index.html .
  3. Нажмите Start , чтобы настроить одноранговое соединение.
  4. Введите текст в textarea область слева.
  5. Нажмите « Отправить» , чтобы передать текст по каналу данных WebRTC.

Как это работает

Этот код использует RTCPeerConnection и RTCDataChannel для обеспечения обмена текстовыми сообщениями.

Большая часть кода на этом шаге такая же, как и в примере RTCPeerConnection . Функции sendData() и createConnection() содержат большую часть нового кода:

function createConnection() {
  dataChannelSend.placeholder = '';
  var servers = null;
  pcConstraint = null;
  dataConstraint = null;
  trace('Using SCTP based data channels');
  // For SCTP, reliable and ordered delivery is true by default.
  // Add localConnection to global scope to make it visible
  // from the browser console.
  window.localConnection = localConnection =
      new RTCPeerConnection(servers, pcConstraint);
  trace('Created local peer connection object localConnection');

  sendChannel = localConnection.createDataChannel('sendDataChannel',
      dataConstraint);
  trace('Created send data channel');

  localConnection.onicecandidate = iceCallback1;
  sendChannel.onopen = onSendChannelStateChange;
  sendChannel.onclose = onSendChannelStateChange;

  // Add remoteConnection to global scope to make it visible
  // from the browser console.
  window.remoteConnection = remoteConnection =
      new RTCPeerConnection(servers, pcConstraint);
  trace('Created remote peer connection object remoteConnection');

  remoteConnection.onicecandidate = iceCallback2;
  remoteConnection.ondatachannel = receiveChannelCallback;

  localConnection.createOffer().then(
    gotDescription1,
    onCreateSessionDescriptionError
  );
  startButton.disabled = true;
  closeButton.disabled = false;
}

function sendData() {
  var data = dataChannelSend.value;
  sendChannel.send(data);
  trace('Sent Data: ' + data);
}

Синтаксис RTCDataChannel намеренно похож на WebSocket с методом send() и событием message .

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

Набрать бонусные баллы

  1. С SCTP , протоколом, используемым каналами данных WebRTC, надежная и упорядоченная доставка данных включена по умолчанию. Когда может RTCDataChannel для обеспечения надежной доставки данных, а когда производительность может быть важнее, даже если это означает потерю некоторых данных?
  2. Используйте CSS для улучшения макета страницы и добавьте атрибут-заполнитель в текстовую textarea dataChannelReceive .
  3. Протестируйте страницу на мобильном устройстве.

Узнать больше

6. Настройте сигнальный сервис для обмена сообщениями

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

Полная версия этого шага находится в папке step-04 .

О приложении

WebRTC использует API JavaScript на стороне клиента, но для реального использования также требуется сервер сигнализации (обмена сообщениями), а также серверы STUN и TURN. Вы можете узнать больше здесь .

На этом шаге вы создадите простой сигнальный сервер Node.js, используя модуль Socket.IO Node.js и библиотеку JavaScript для обмена сообщениями.

В этом примере сервер (приложение Node.js) реализован в index.js , а работающий на нем клиент (веб-приложение) реализован в index.html .

Приложение Node.js на этом этапе выполняет две задачи.

Во-первых, он действует как ретранслятор сообщений:

socket.on('message', function (message) {
  log('Got message: ', message);
  socket.broadcast.emit('message', message);
});

Во-вторых, он управляет видеочатами WebRTC:

if (numClients === 0) {
  socket.join(room);
  socket.emit('created', room, socket.id);
} else if (numClients === 1) {
  socket.join(room);
  socket.emit('joined', room, socket.id);
  io.sockets.in(room).emit('ready');
} else { // max two clients
  socket.emit('full', room);
}

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

HTML и JavaScript

  1. Обновите index.html , чтобы он выглядел следующим образом:
<!DOCTYPE html>
<html>

<head>

  <title>Real-time communication with WebRTC</title>

  <link rel="stylesheet" href="css/main.css" />

</head>

<body>

  <h1>Real-time communication with WebRTC</h1>

  <script src="/socket.io/socket.io.js"></script>
  <script src="js/main.js"></script>
  
</body>

</html>

На этом шаге вы ничего не увидите на странице. Все журналирование выполняется в консоли браузера. Чтобы просмотреть консоль в Chrome, нажмите Control+Shift+J (или Command+Option+J на Mac).

  1. Замените js/main.js следующим:
'use strict';

var isInitiator;

window.room = prompt("Enter room name:");

var socket = io.connect();

if (room !== "") {
  console.log('Message from client: Asking to join room ' + room);
  socket.emit('create or join', room);
}

socket.on('created', function(room, clientId) {
  isInitiator = true;
});

socket.on('full', function(room) {
  console.log('Message from client: Room ' + room + ' is full :^(');
});

socket.on('ipaddr', function(ipaddr) {
  console.log('Message from client: Server IP address is ' + ipaddr);
});

socket.on('joined', function(room, clientId) {
  isInitiator = false;
});

socket.on('log', function(array) {
  console.log.apply(console, array);
});

Настройте файл Socket.IO для работы на Node.js

В HTML-файле вы могли заметить, что используете файл Socket.IO:

<script src="/socket.io/socket.io.js"></script>
  1. На верхнем уровне вашего work каталога создайте файл с именем package.json со следующим содержимым:
{
  "name": "webrtc-codelab",
  "version": "0.0.1",
  "description": "WebRTC codelab",
  "dependencies": {
    "node-static": "^0.7.10",
    "socket.io": "^1.2.0"
  }
}

Это манифест приложения, который сообщает Node Package Manager ( npm ), какой проект

зависимости для установки.

  1. Чтобы установить зависимости, такие как /socket.io/socket.io.js , запустите следующую команду из терминала командной строки в work каталоге:
npm install

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

3ab06b7bcc7664b9.png

Как видите, npm установил зависимости, определенные в package.json .

  1. Создайте новый файл index.js на верхнем уровне вашего work каталога (не каталога js ) и добавьте следующий код:
'use strict';

var os = require('os');
var nodeStatic = require('node-static');
var http = require('http');
var socketIO = require('socket.io');

var fileServer = new(nodeStatic.Server)();
var app = http.createServer(function(req, res) {
  fileServer.serve(req, res);
}).listen(8080);

var io = socketIO.listen(app);
io.sockets.on('connection', function(socket) {

  // Convenience function to log server messages on the client
  function log() {
    var array = ['Message from server:'];
    array.push.apply(array, arguments);
    socket.emit('log', array);
  }

  socket.on('message', function(message) {
    log('Client said: ', message);
    // For a real app, would be room-only (not broadcast)
    socket.broadcast.emit('message', message);
  });

  socket.on('create or join', function(room) {
    log('Received request to create or join room ' + room);

    var clientsInRoom = io.sockets.adapter.rooms[room];
    var numClients = clientsInRoom ? Object.keys(clientsInRoom.sockets).length : 0;

    log('Room ' + room + ' now has ' + numClients + ' client(s)');

    if (numClients === 0) {
      socket.join(room);
      log('Client ID ' + socket.id + ' created room ' + room);
      socket.emit('created', room, socket.id);

    } else if (numClients === 1) {
      log('Client ID ' + socket.id + ' joined room ' + room);
      io.sockets.in(room).emit('join', room);
      socket.join(room);
      socket.emit('joined', room, socket.id);
      io.sockets.in(room).emit('ready');
    } else { // max two clients
      socket.emit('full', room);
    }
  });

  socket.on('ipaddr', function() {
    var ifaces = os.networkInterfaces();
    for (var dev in ifaces) {
      ifaces[dev].forEach(function(details) {
        if (details.family === 'IPv4' && details.address !== '127.0.0.1') {
          socket.emit('ipaddr', details.address);
        }
      });
    }
  });

});
  1. В терминале командной строки выполните следующую команду в work каталоге:
node index.js
  1. В браузере перейдите по адресу http://localhost:8080 .

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

Чтобы присоединиться к одной и той же комнате, каждый раз вводите одно и то же имя комнаты, например, foo .

  1. Откройте новую вкладку, снова перейдите по адресу http://localhost:8080 и снова введите то же имя комнаты.
  2. Откройте еще одну новую вкладку, снова перейдите по адресу http://localhost:8080 и снова введите то же имя комнаты.
  3. Проверьте консоль на каждой из вкладок.

Вы должны увидеть журнал из JavaScript.

Набрать бонусные баллы

  • Какие альтернативные механизмы обмена сообщениями могут быть возможны? С какими проблемами вы можете столкнуться при использовании чистого WebSocket?
  • Какие проблемы могут возникнуть при масштабировании этого приложения? Можете ли вы разработать метод тестирования тысяч или миллионов одновременных запросов на комнату?
  • Это приложение использует подсказку JavaScript для получения имени комнаты. Выясните, как получить имя комнаты из URL-адреса. Например, http://localhost:8080/foo даст имя комнаты foo .

Узнать больше

7. Комбинируйте одноранговое соединение и сигнализацию

Полная версия этого шага находится в папке step-05 .

Замените HTML и JavaScript

  1. Замените содержимое index.html следующим:
<!DOCTYPE html>
<html>

<head>

  <title>Real-time communication with WebRTC</title>

  <link rel="stylesheet" href="/css/main.css" />

</head>

<body>

  <h1>Real-time communication with WebRTC</h1>

  <div id="videos">
    <video id="localVideo" autoplay muted></video>
    <video id="remoteVideo" autoplay></video>
  </div>

  <script src="/socket.io/socket.io.js"></script>
  <script src="https://webrtc.github.io/adapter/adapter-latest.js"></script>
  <script src="js/main.js"></script>
  
</body>

</html>
  1. Замените js/main.js содержимым step-05/js/main.js .

Запустите сервер Node.js

Если вы не следуете этой лаборатории кода из своего work каталога, вам может потребоваться установить зависимости для папки step-05 или вашей текущей рабочей папки.

  1. Запустите следующую команду из вашего рабочего каталога:
npm install
  1. После установки, если ваш сервер Node.js не запущен, запустите его, выполнив следующую команду в work каталоге:
node index.js

Убедитесь, что вы используете версию index.js из предыдущего шага, которая реализует Socket.IO. Дополнительные сведения об узлах и сокетах ввода-вывода см. в разделе Настройка службы сигнализации для обмена сообщениями.

  1. В браузере перейдите по адресу http://localhost:8080.
  2. Откройте новую вкладку и снова перейдите по адресу http://localhost:8080.

Один элемент video отображает локальный поток от getUserMedia() , а другой показывает удаленное видео, переданное через RTCPeerconnection .

  1. Просмотр журнала в консоли браузера.

Набирайте бонусные баллы

  • Это приложение поддерживает только видеочат один на один. Как бы вы изменили дизайн, чтобы несколько человек могли пользоваться одним и тем же видеочатом?
  • В примере имя комнаты foo жестко запрограммировано. Как лучше всего включить другие имена комнат?
  • Как пользователи будут делиться названием комнаты? Попробуйте создать альтернативу совместному использованию имен комнат.
  • Как вы могли бы изменить приложение?

Советы

  • Найдите статистику WebRTC и данные отладки на chrome://webrtc-internals.
  • Используйте средство устранения неполадок WebRTC , чтобы проверить локальную среду и протестировать камеру и микрофон.
  • Если у вас возникли странные проблемы с кэшированием, попробуйте следующее:
  1. Нажмите Control и щелкните Перезагрузить эту страницу .
  2. Перезапустите браузер.
  3. Запустите npm cache clean из командной строки.

8. Сделайте снимок и поделитесь им через канал данных

Полная версия этого шага находится в папке step-06 .

Как это работает

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

Основные части этого этапа следующие:

  1. Установить канал данных.

На этом шаге вы не добавляете никаких медиапотоков к одноранговому соединению.

  1. Захватите видеопоток с веб-камеры с помощью getUserMedia() :
var video = document.getElementById('video');

function grabWebCamVideo() {
  console.log('Getting user media (video) ...');
  navigator.mediaDevices.getUserMedia({
    video: true
  })
  .then(gotStream)
  .catch(function(e) {
    alert('getUserMedia() error: ' + e.name);
  });
}
  1. Нажмите Snap , чтобы получить снимок (видеокадр) из видеопотока и отобразить его в элементе canvas :
var photo = document.getElementById('photo');
var photoContext = photo.getContext('2d');

function snapPhoto() {
  photoContext.drawImage(video, 0, 0, photo.width, photo.height);
  show(photo, sendBtn);
}
  1. Нажмите « Отправить », чтобы преобразовать изображение в байты и отправить их по каналу данных:
function sendPhoto() {
  // Split the data-channel message in chunks of this byte length.
  var CHUNK_LEN = 64000;
  var img = photoContext.getImageData(0, 0, photoContextW, photoContextH),
    len = img.data.byteLength,
    n = len / CHUNK_LEN | 0;

  console.log('Sending a total of ' + len + ' byte(s)');
  dataChannel.send(len);

  // Split the photo and send in chunks of approximately 64KB.
  for (var i = 0; i < n; i++) {
    var start = i * CHUNK_LEN,
      end = (i + 1) * CHUNK_LEN;
    console.log(start + ' - ' + (end - 1));
    dataChannel.send(img.data.subarray(start, end));
  }

  // Send the reminder, if applicable.
  if (len % CHUNK_LEN) {
    console.log('last ' + len % CHUNK_LEN + ' byte(s)');
    dataChannel.send(img.data.subarray(n * CHUNK_LEN));
  }
}

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

function receiveDataChromeFactory() {
  var buf, count;

  return function onmessage(event) {
    if (typeof event.data === 'string') {
      buf = window.buf = new Uint8ClampedArray(parseInt(event.data));
      count = 0;
      console.log('Expecting a total of ' + buf.byteLength + ' bytes');
      return;
    }

    var data = new Uint8ClampedArray(event.data);
    buf.set(data, count);

    count += data.byteLength;
    console.log('count: ' + count);

    if (count === buf.byteLength) {
      // we're done: all data chunks have been received
      console.log('Done. Rendering photo.');
      renderPhoto(buf);
    }
  };
}

function renderPhoto(data) {
  var canvas = document.createElement('canvas');
  canvas.width = photoContextW;
  canvas.height = photoContextH;
  canvas.classList.add('incomingPhoto');
  // The trail is the element that holds the incoming images.
  trail.insertBefore(canvas, trail.firstChild);

  var context = canvas.getContext('2d');
  var img = context.createImageData(photoContextW, photoContextH);
  img.data.set(data);
  context.putImageData(img, 0, 0);
}

Получить код

  1. Замените содержимое work папки содержимым step-06 .

Ваш work файл index.html теперь должен выглядеть так**:**

<!DOCTYPE html>
<html>

<head>

  <title>Real-time communication with WebRTC</title>

  <link rel="stylesheet" href="/css/main.css" />

</head>

<body>

  <h1>Real-time communication with WebRTC</h1>

  <h2>
    <span>Room URL: </span><span id="url">...</span>
  </h2>

  <div id="videoCanvas">
    <video id="camera" autoplay></video>
    <canvas id="photo"></canvas>
  </div>

  <div id="buttons">
    <button id="snap">Snap</button><span> then </span><button id="send">Send</button>
    <span> or </span>
    <button id="snapAndSend">Snap &amp; Send</button>
  </div>

  <div id="incoming">
    <h2>Incoming photos</h2>
    <div id="trail"></div>
  </div>

  <script src="/socket.io/socket.io.js"></script>
  <script src="https://webrtc.github.io/adapter/adapter-latest.js"></script>
  <script src="js/main.js"></script>

</body>

</html>
  1. Если вы не следуете этой лаборатории кода из своего work каталога, вам может потребоваться установить зависимости для папки step-06 или вашей текущей рабочей папки. Просто запустите следующую команду из своего рабочего каталога:
npm install
  1. После установки, если ваш сервер Node.js не запущен, запустите его, выполнив следующую команду из work каталога:
node index.js
    Make sure that you're using the version of `index.js` that implements Socket.IO and 

не забудьте перезапустить сервер Node.js, если вы вносите изменения.

Дополнительные сведения о Node и Socket.IO см. в разделе Настройка сигнализации.

Сервис для обмена сообщениями.

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

Приложение создает случайный идентификатор комнаты и добавляет идентификатор к URL-адресу.

  1. Откройте URL-адрес из адресной строки в новой вкладке или окне браузера.
  2. Нажмите «Создать и отправить » и посмотрите « Входящие фотографии » на другой вкладке в нижней части страницы.

Приложение передает фотографии между вкладками.

Вы должны увидеть что-то вроде этого:

911b40f36ba6ba8.png

Набрать бонусные баллы

Как вы можете изменить код, чтобы сделать возможным совместное использование файлов любого типа?

Узнать больше

9. Поздравления

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

Учить больше