WebRTC (Интернет-связь в реальном времени) — это технология с набором функций, которые позволяют пользователю получать аудио- и видеоматериалы и передавать эту информацию при одноранговой связи. С помощью этого соединения также можно отправлять любые данные, такие как текст или файлы.

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

Структура проекта

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

Клиент представляет собой простой HTML-код для получения видео/аудиопотока и ввода для отправки сообщений чата. Связь WebRTC реализована в файле Javascript, импортированном этим HTML.

Ресурсы WebRTC

  • MediaStream: представляет поток мультимедийного контента с дорожками для аудио и видео. Вы можете получить объект MediaStream с помощью функции navigator.mediaDevices.getUserMedia().
  • RTCPeerConnection: представляет соединение между двумя одноранговыми узлами. Он используется для отправки потока между клиентами.
  • RTCDataChannel: представляет двунаправленный канал данных между двумя парами соединения. Он используется для отправки сообщений чата между клиентами.

Покажи мне код

Начнем с кода сервера. Сначала мы начнем проект NodeJS.

yarn init -y

Установите необходимые зависимости. Express для создания сервера и socket.io для включения связи WebSocket.

yarn add express socket.io

Создайте server.js, чтобы запустить наш сервер, и поместите следующий код:

const express = require('express');
const socketio = require('socket.io');
const cors = require('cors');
const http = require('http');

// Create server
const app = express();
const server = http.Server(app);

// Enable Cors to Socket IO
app.use(cors());

// Init Socket IO Server
const io = socketio(server);

// Called whend a client start a socket connection
io.on('connection', (socket) => {

});

// Start server in port 3000 or the port passed at "PORT" env variable
server.listen(process.env.PORT || 3000,
  () => console.log('Server Listen On: *:', process.env.PORT || 3000));

Начальная структура проекта должна быть примерно такой:]

Структура веб-сокета

Цель веб-сокета - сделать так, чтобы клиент не знал друг друга без соединения WebRTC.

Соединение WebRTC устанавливается в несколько шагов, описанных ниже. Все эти шаги объясняются в разделе реализации клиента.

  1. Создайте экземпляр RTCPeerConnection;
  2. Создайте Предложение для подключения;
  3. Отправьте Ответ на запрос предложения;
  4. Сигнализация между клиентами.

Итак, для реализации этого необходимо добавить некоторые события в socket.

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

Ниже у нас есть полный код этой реализации.

// Array to map all clients connected in socket
let connectedUsers = [];

// Called whend a client start a socket connection
io.on('connection', (socket) => {
  // It's necessary to socket knows all clients connected
  connectedUsers.push(socket.id);

  // Emit to myself the other users connected array to start a connection with each them
  const otherUsers = connectedUsers.filter(socketId => socketId !== socket.id);
  socket.emit('other-users', otherUsers);

  // Send Offer To Start Connection
  socket.on('offer', (socketId, description) => {
    socket.to(socketId).emit('offer', socket.id, description);
  });

  // Send Answer From Offer Request
  socket.on('answer', (socketId, description) => {
    socket.to(socketId).emit('answer', description);
  });

  // Send Signals to Establish the Communication Channel
  socket.on('candidate', (socketId, signal) => {
    socket.to(socketId).emit('candidate', signal);
  });

  // Remove client when socket is disconnected
  socket.on('disconnect', () => {
    connectedUsers = connectedUsers.filter(socketId => socketId !== socket.id);
  });
});

Код клиента

Сначала создайте папку с именем public и добавьте файлы index.html и main.js. Окончательная структура проекта должна выглядеть так:

  • HTML-код
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>WebRTC Example</title>

  <style>
    #video-grid {
      display: none;
      grid-template-columns: repeat(auto-fill, 400px);
      grid-auto-rows: 400px;
    }

    video {
      width: 100%;
      height: 100%;
    }
  </style>

  <script src="/socket.io/socket.io.js"></script>
  <script src="/main.js" type="module"></script>
</head>
<body>
  <h1>Hello!</h1>

  <!-- My Video and Remote Video from connection -->
  <div id="video-grid">
    <video playsinline autoplay muted id="local-video"></video>
    <video playsinline autoplay id="remote-video"></video>
  </div>

  <!-- Input to send messages -->
  <div>
    <span style="font-weight: bold">Message: </span>
    <input type="text" id="message-input" title="Message to Send!">
    <button id="message-button">Send</button>
  </div>

  <!-- Area to Print Images -->
  <div class="messages"></div>
</body>
</html>

В файле main.js первым шагом является запуск MediaStream, например:

console.log('Main JS!');

// Map All HTML Elements
const videoGrid = document.getElementById('video-grid');
const messagesEl = document.querySelector('.messages');
const messageInput = document.getElementById('message-input');
const sendButton = document.getElementById('message-button');
const localVideo = document.getElementById('local-video');
const remoteVideo = document.getElementById('remote-video');

// Open Camera To Capture Audio and Video
navigator.mediaDevices.getUserMedia({ video: true, audio: true })
  .then(stream => {
    // Show My Video
    videoGrid.style.display = 'grid';
    localVideo.srcObject = stream;

    // Start a Peer Connection to Transmit Stream
    initConnection(stream);
  })
  .catch(error => console.log(error));

Результат примерно такой: ваше видео находится в области local-video.

Следующие шаги — запуск сокетного соединения и инициализация RTCPeerConnectin для подключения друг к другу пользователей. При получении события сокета other-users клиент инициирует соединение с каждым из них.

const initConnection = (stream) => {
  const socket = io('/');
  let localConnection;
  let remoteConnection;

  // Start a RTCPeerConnection to each client
  socket.on('other-users', (otherUsers) => {
    // Ignore when not exists other users connected
    if (!otherUsers || !otherUsers.length) return;

    const socketId = otherUsers[0];

    // Ininit peer connection
    localConnection = new RTCPeerConnection();

    // Add all tracks from stream to peer connection
    stream.getTracks().forEach(track => localConnection.addTrack(track, stream));

    // Send Candidtates to establish a channel communication to send stream and data
    localConnection.onicecandidate = ({ candidate }) => {
      candidate && socket.emit('candidate', socketId, candidate);
    };

    // Receive stream from remote client and add to remote video area
    localConnection.ontrack = ({ streams: [ stream ] }) => {
      remoteVideo.srcObject = stream;
    };

    // Create Offer, Set Local Description and Send Offer to other users connected
    localConnection
      .createOffer()
      .then(offer => localConnection.setLocalDescription(offer))
      .then(() => {
        socket.emit('offer', socketId, localConnection.localDescription);
      });
  });
}

ВАЖНО: в реальных условиях RTCPeerConnection должен быть инициализирован с настройками iceServers с серверами STUN и TURN, это необходимо получить реальный IP-адрес для подключения к Интернету и избежать блоков NAT в сети. Подробнее об этом смотрите в RTCPeerConnection и WebRTC в реальном мире

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

// Receive Offer From Other Client
socket.on('offer', (socketId, description) => {
    // Ininit peer connection
    remoteConnection = new RTCPeerConnection();

    // Add all tracks from stream to peer connection
    stream.getTracks().forEach(track => remoteConnection.addTrack(track, stream));

    // Send Candidtates to establish a channel communication to send stream and data
    remoteConnection.onicecandidate = ({ candidate }) => {
      candidate && socket.emit('candidate', socketId, candidate);
    };

    // Receive stream from remote client and add to remote video area
    remoteConnection.ontrack = ({ streams: [ stream ] }) => {
      remoteVideo.srcObject = stream;
    };

    // Set Local And Remote description and create answer
    remoteConnection
      .setRemoteDescription(description)
      .then(() => remoteConnection.createAnswer())
      .then(answer => remoteConnection.setLocalDescription(answer))
      .then(() => {
        socket.emit('answer', socketId, remoteConnection.localDescription);
      });
  });

Наконец, первый клиент получает ответ и устанавливает Удаленное описание. Итак, запустите кандидатов на отправку, чтобы создать канал связи для отправки потока.

// Receive Answer to establish peer connection
socket.on('answer', (description) => {
  localConnection.setRemoteDescription(description);
});

// Receive candidates and add to peer connection
socket.on('candidate', (candidate) => {
  // GET Local or Remote Connection
  const conn = localConnection || remoteConnection;
  conn.addIceCandidate(new RTCIceCandidate(candidate));
});

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

Ссылка

https://developer.mozilla.org/pt-BR/docs/Web/API/WebRTC_API

https://www.html5rocks.com/en/tutorials/webrtc/infrastructure/

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

Вы можете посмотреть весь код в GitHub

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

Первоначально опубликовано на https://dev.to 11 ноября 2020 г.