React: WebRTC Media Call

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


Привет, друзья!


В этой статье я покажу вам, как разработать приложение для совершения аудио/видео звонков с помощью WebRTC.


Функционал нашего приложения будет следующим:


  • при запуске приложения пользователь А получает уникальный идентификатор;
  • он передает этот идентификатор пользователю Б;
  • пользователь Б использует идентификатор пользователя А для совершения аудио или видео звонка;
  • пользователь А получает уведомление о звонке пользователя Б и может ответить на него с видео или без либо отклонить звонок;
  • в процессе соединения пользователи имеют возможность включать/выключать аудио и видео;
  • после завершения звонка выполняется перезагрузка WebRTC для обеспечения возможности совершения нового звонка.

Демо приложения.


Репозиторий с исходным кодом.


Основной источник вдохновения.


Если вам это интересно, прошу под кат.


Для установки зависимостей мы будем использовать Yarn.


Для создания шаблона приложения — Vite.


Для стилизации — Sass.


Для инициализации медиасессии (signalling — сигнализации) между браузерами — веб-сокеты в лице Socket.io.


Основные спецификации, на которые мы будем опираться при разработке приложения:


  • WebRTC 1.0: Real-Time Communication Between Browsers — набор API для обмена медиа и другими данными между браузерами, в которых реализован соответствующий набор протоколов, в режиме реального времени;
  • An Offer/Answer Model with the Session Description Protocol (SDP) — механизм, позволяющий браузерам устанавливать соединение с помощью протокола описания сессии (Session Description Protocol, SDP);
  • Media Capture and Streams — набор API, позволяющих браузеру получать доступ к медиапотоку с устройств пользователя.

Ссылки на соответствующие разделы MDN будут приводиться по мере необходимости.


Фактически наше приложение будет продвинутой реализацией примеров, описанных здесь и здесь.


Подготовка и настройка проекта


Создаем директорию, переходим в нее и создаем шаблон React-приложения:


mkdir react-webrtc
cd react-webrtc

yarn create vite client --template react

Пока создается шаблон, поговорим о процессе установки P2P-соединения (peer-to-peer — равный к равному, одноранговая сеть), об интерфейсах и методах, задействованных в этом процессе.


Вот что происходит на самом высоком уровне:


  • браузер пользователя А (далее — А) захватывает (capture) медиапоток с устройств пользователя (видеокамера и микрофон). Для этого используется метод getUserMedia:

// ограничения или требования к потоку
// https://w3c.github.io/mediacapture-main/#constrainable-interface
const config = {
 audio: true,
 video: true
}

// https://w3c.github.io/mediacapture-main/#dom-mediadevices-getusermedia
const localStream = await navigator.mediaDevices.getUserMedia(config)

// медиапоток состоит из 1 или более медиатреков
// https://w3c.github.io/mediacapture-main/#dom-mediastream-gettracks
const localTracks = localStream.getTracks()

  • А создает экземпляр RTCPeerConnection с помощью одноименного конструктора:

// https://w3c.github.io/webrtc-pc/#dom-rtcconfiguration
const config = { iceServers: [{ urls: ['stun:stun.l.google.com:19302'] }] }

// https://w3c.github.io/webrtc-pc/#interface-definition
const pc = new RTCPeerConnection(config)

  • А добавляет захваченные треки и поток в экземпляр RTCPeerConnection с помощью метода addTrack:

stream.getTracks().forEach((track) => {
 // https://w3c.github.io/webrtc-pc/#dom-rtcpeerconnection-addtrack
 pc.addTrack(track, stream)
})

  • А генерирует предложение (offer) об установке соединения с помощью метода createOffer. Данный метод возвращает RTCLocalSessionDescriptionInit:

// https://w3c.github.io/webrtc-pc/#dom-rtcpeerconnection-createoffer
const offer = await pc.createOffer()

  • А вызывает метод setLocalDescription, передавая ему предложение:

// https://w3c.github.io/webrtc-pc/#dom-peerconnection-setlocaldescription
pc.setLocalDescription(offer)

  • предложение передается браузеру пользователя Б (далее — Б) любым доступным способом, например, через веб-сокеты (сигнализация):

socket.emit('call', {
 // идентификатор Б
 to: remoteId,
 // предложение
 sdp: offer
})

  • Б создает экземпляр RTCPeerConnection и добавляет в него предложение в виде RTCSessionDescription, с помощью метода setRemoteDescription. RTCSessionDescription (описание сессии) создается с помощью одноименного конструктора:

// https://w3c.github.io/webrtc-pc/#rtcsessiondescription-class
const sdp = new RTCSessionDescription(desc)
// https://w3c.github.io/webrtc-pc/#dom-peerconnection-setremotedescription
pc.setRemoteDescription(sdp)

  • при добавлении А треков и потока в экземпляр RTCPeerConnection на стороне Б возникает событие track, которое обрабатывается с помощью ontrack:

// событие содержит медиапотоки, полученные от другой стороны, в виде массива
// в нашем случае в массиве будет только один такой поток
// https://w3c.github.io/webrtc-pc/#rtctrackevent
// https://w3c.github.io/webrtc-pc/#dom-rtcpeerconnection-ontrack
pc.ontrack = ({ streams }) => {
 remoteStream = streams[0]
}

  • Б захватывает медиапоток с устройств пользователя, добавляет треки и поток в экземпляр RTCPeerConnection, генерирует ответ на предложение об установке соединения (answer) с помощью метода createAnswer, вызывает setLocalDescription с ответом и передает ответ А:

// https://w3c.github.io/webrtc-pc/#dom-rtcpeerconnection-createanswer
const answer = await pc.createAnswer()

pc.setLocalDescription(answer)

// сигнализация
socket.emit('call', {
 // идентификатор А
 to: remoteId,
 sdp: answer
})

  • А, в свою очередь, также вызывает setRemoteDescription и регистрирует ontrack;


  • в это же время (после вызова setLocalDescription) происходит подбор кандидатов для установки интерактивного соединения (ICE gathering). Возникает событие icecandidate, которое обрабатывается с помощью onicecandidate:



// https://w3c.github.io/webrtc-pc/#dom-rtcpeerconnection-onicecandidate
// https://w3c.github.io/webrtc-pc/#dom-rtcpeerconnectioniceevent
pc.onicecandidate = ({ candidate }) => {
 // событие содержит `RTCIceCandidateInit`
 // https://w3c.github.io/webrtc-pc/#dom-rtcicecandidateinit
 // передаем "кандидата" другой стороне
 socket.emit('call', {
   to: remoteId,
   candidate
 })
}

  • при получении "кандидата" другой стороной, она создает экземпляр RTCIceCandidate с помощью одноименного конструктора и вызывает метод addIceCandidate:

// https://w3c.github.io/webrtc-pc/#rtcicecandidate-interface
const _candidate = new RTCIceCandidate(candidate)

// https://w3c.github.io/webrtc-pc/#dom-peerconnection-addicecandidate
pc.addIceCandidate(_candidate)

  • после этого стороны могут напрямую обмениваться медиаданными.

Более подробную информацию о том, как все это происходит на более низком уровне, в доступной форме можно получить здесь.


Сервер


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


Создаем директорию, переходим в нее, инициализируем Node.js-проект и устанавливаем зависимости:


mkdir server
cd server

yarn init -yp

yarn add express socket.io dotenv nanoid

yarn add -D nodemon

Зависимости:


  • express — Node.js-фреймворк для разработки веб-серверов;
  • socket.io — библиотека, облегчающая работу с веб-сокетами;
  • dotenv — утилита для работы с переменными среды окружения;
  • nanoid — утилита для генерации идентификаторов;
  • nodemon — утилита для запуска сервера для разработки.

Определяем тип кода сервера и команды для запуска серверов в package.json:


"type": "module",
"scripts": {
 "dev": "nodemon",
 "start": "node index.js"
}

Создаем файл index.js:


touch index.js

Импортируем библиотеки и утилиты:


import express from 'express'
import { createServer } from 'http'
import { join, dirname } from 'path'
import { fileURLToPath } from 'url'
import { Server } from 'socket.io'
import { config } from 'dotenv'
import initSocket from './utils/initSocket.js'

Получаем доступ к переменным среды окружения и формируем путь к текущей директории:


config()

const __dirname = dirname(fileURLToPath(import.meta.url))

Создаем экземпляры express и сервера:


const app = express()
const server = createServer(app)

Добавляем обслуживание статических файлов из сборки клиента:


app.use(express.static(join(__dirname, '../client/dist')))

Создаем экземпляр socket.io и добавляем обработку подключения:


const io = new Server(server, {
 cors: process.env.ALLOWED_ORIGIN,
 serveClient: false
})
io.on('connection', initSocket)

Определяем порт и запускаем сервер:


const port = process.env.PORT || 4000
server.listen(port, () => {
 console.log(`Server ready on port ${port} 						
Источник: https://habr.com/ru/company/timeweb/blog/649369/


Интересные статьи

Интересные статьи

Технология WebRTC как известно, основана на передачи данных - аудио, видео, текста, файлов - от одного конечного узла одноранговой сети другому. Существует термин, описывающий такую схему: p2p (peer t...
Данная статья - это не научный прорыв, а лишь помощник быстрее понять как работает стандартный функционал в BitrixДавайте представим, что в разделе каталога у нас 150 запросов к БД. Вроде бы немного п...
Как быстро определить, что на отдельно взятый сайт забили, и им никто не занимается? Если в подвале главной страницы в копирайте стоит не текущий год, а старый, то именно в этом году опека над са...
Эта статья для тех, кто собирается открыть интернет-магазин, но еще рассматривает варианты и думает по какому пути пойти, заказать разработку магазина в студии, у фрилансера или выбрать облачный серви...
Всем привет. На связи Омельницкий Сергей. Не так давно я вел стрим по реактивному программированию, где рассказывал про асинхронность в JavaScript. Сегодня я бы хотел законспектировать этот матер...