Добро пожаловать во вторую часть серии демистифицированных WebRTC. В этой статье мы создадим простое приложение для видеочата с нуля, используя Angular и PeerJS, библиотеку оболочки JavaScript WebRTC. Если сейчас вы потеете и паникуете из-за того, что не знаете, что такое WebRTC, во-первых, успокойтесь, а во-вторых, посмотрите часть 1 этой серии, и я встречусь с вами здесь

Вот что мы будем строить сегодня:

Вы можете найти рабочую демонстрацию здесь и репозиторий GitHub здесь. Вы в восторге? Если да, давай начнем вечеринку 🚀

Предварительные требования

  1. Базовые знания Angular
  2. Компьютер с установленным Angular
  3. Умение читать / писать

Этап 0: настройка

Начнем с создания нового проекта Angular:

ng new angular-peerjs

После того, как проект настроен (нет необходимости в маршрутизации, и я использую scss для стилизации), давайте войдем во вновь созданный каталог проекта и добавим Angular Material, чтобы придать нашему пользовательскому интерфейсу минимальное эстетическое достоинство:

ng add @angular/material

Теперь мы можем продолжить и установить наши 2 зависимости для проекта:

npm i --save peerjs uuid

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

Хотите прочитать эту историю позже? Сохранить в журнале.

Этап 1. Макет и стиль

Давайте сделаем наш базовый макет, открыв app.component.html file. Мы создадим простую страницу, которая выглядит следующим образом.

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

В приведенном выше шаблоне мы используем наблюдаемый isCallStarted$ для включения / отключения кнопок (мы не можем начать вызов, если мы уже участвуем в нем, и мы не можем завершить вызов, если мы нет). Тег видео #remoteVideo будет использоваться для воспроизведения потока, передаваемого удаленным узлом, а #localVideo будет использоваться для воспроизведения потока, захваченного нашей веб-камерой. У последнего флаг muted установлен на true, иначе вы услышите себя дважды.

Теперь давайте добавим базовый стиль в файл app.component.scss, чтобы наше локальное видео было меньше и привязано к правой нижней части экрана:

Фаза 2: модальный запуск / присоединение к вызову

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

ng g c callinfo-dialog

Давайте посмотрим на компонент callinfo-dialog. Шаблон определяется следующим образом:

Если мы присоединяемся к вызову, текстовое поле позволит нам указать идентификатор удаленного узла, тогда как, если мы начинаем вызов, тот же ввод будет отключен, поскольку он будет отображать идентификатор локального узла. Мы используем директиву cdkCopyToClipboard, чтобы разрешить копирование идентификатора партнера, просто нажав на ввод текста. Когда мы закрываем модальное окно, мы хотим передать наш идентификатор партнера, если мы начинаем вызов, или идентификатор удаленного узла, если мы присоединяемся к нему, в основной app.component.ts (который мы определим в фазе 3). Компонент довольно прост и выглядит так:

Как видите, здесь ничего особенного не происходит. Мы сообщаем конструктору, что ожидаем объект data, предоставляемый основным app.component.ts, который мы рассмотрим позже, который содержит два свойства: идентификатор партнера и логическое значение, определяющее, будем ли мы собирается присоединиться к разговору или начать его.

Фаза 3: Главный компонент

Давайте посмотрим на app.component.ts файл, в котором мы свяжем логику кнопок с только что созданным модальным окном.

Этот компонент основан на call.service.ts, который мы проанализируем в следующем и последнем разделе. В конструкторе мы получаем ссылку на наблюдаемый объект isCallStarted$, который будет отвечать за отключение кнопок для запуска / присоединения к вызову, пока вызов уже выполняется. Также мы инициализируем нашего однорангового узла и получаем наш уникальный идентификатор однорангового узла, который присваиваем переменной peerId.

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

В методе showModal, который запускается кнопками вызова запуска / присоединения, мы открываем модальное окно и передаем две переменные: если мы запускаем вызов, мы устанавливаем для joinCall значение false и мы передаем идентификатор нашего партнера (чтобы мы могли его скопировать и передать удаленному партнеру), если мы пытаемся присоединиться к вызову, мы устанавливаем joinCall в значение true и peerId в значение null. Когда мы закрываем модальное окно, мы либо разрешаем нашему одноранговому узлу отвечать на вызовы - в случае, если мы его запустили, - либо пытаемся установить вызов с удаленным одноранговым узлом, чей идентификатор был предоставлен пользователем в модальном окне.

Этап 4: Служба звонков

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

Вначале мы определяем переменные, которые будем использовать: наш одноранговый узел, ссылка на фактическое соединение вызова, две пары поведения субъект / наблюдаемый для управления и предоставления внешнего мира локальный и удаленный поток и другой субъект / наблюдаемое поведение. пара, чтобы показать состояние вызова (начат или закончился). В конструкторе мы инструктируем Angular DI внедрить MatSnackBar, который мы будем использовать для отображения сообщений об ошибках. Если вы получаете сообщение об ошибке при импорте peerjs, откройте файл tsconfig.json и установите «allowSyntheticDefaultImports":true

В этом методе мы определяем конфигурацию для нашего партнера. PeerJSOption позволяет нам настраивать ICE: как видите, мы используем два бесплатных сервера STUN, предоставленных Google (чтобы узнать больше о ICE и STUN, обратитесь к моей предыдущей статье этой серии). Если бы у нас был TURN-сервер, мы могли бы добавить его в список. После того, как мы настроили наш peerJSOption, мы генерируем новый уникальный идентификатор и создаем экземпляр нашего однорангового узла. Теперь наш клиент подключится к общедоступному серверу PeerJS (также может быть развернут локально), который позаботится о сигнализации для нас. Если все идет хорошо, мы возвращаем идентификатор партнера.

Теперь, когда наш партнер был правильно инициализирован, пора заняться реальными делами 🚀

Одноранговый узел, который инициирует вызов, вызывает establishMediaCallmethod, который принимает идентификатор удаленного однорангового узла в качестве входного параметра и выполняет следующие действия:

  1. Пытается получить аудио / видеопоток с локальных устройств, таких как веб-камеры и микрофоны, после получения он передается субъекту localStreamBsbehavior, который, в свою очередь, делает его доступным для всех зарегистрированных наблюдателей.
  2. Пытается установить соединение с удаленным узлом
  3. Как только соединение с одноранговым узлом установлено, начинается звонок.
  4. Если на вызов ответили - this.mediaCall.on('stream') обратный вызов - remoteStreamBs передается с удаленным потоком, передавая его всем зарегистрированным наблюдателям.
  5. Если возникают какие-либо ошибки, они регистрируются, и пользователь уведомляется о том, что пошло не так.

Одноранговый узел, который отвечает на вызов, вызывает метод enableCallAnswer. Этот метод выполняет следующие действия:

  1. Пытается получить аудио / видеопоток с локальных устройств, таких как веб-камеры и микрофоны, после получения он передается субъекту localStreamBsbehavior, который, в свою очередь, делает его доступным для всех наблюдателей, зарегистрированных для прослушивания.
  2. Обратный вызов call однорангового узла зарегистрирован. Как только входящий вызов получен, метод автоматически отвечает на него (что дает указание партнеру начать отправку другой стороне локального потока) и начинает прослушивать удаленный поток, который передается субъекту remoteStreamBsbehavior.
  3. Если есть ошибки, запишите их и уведомите пользователя о том, что пошло не так.

Теперь, когда логика для запуска / ответа на вызовы определена, последнее, что нам нужно сделать, это обработать конец вызова.

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

Вот и все! Теперь мы готовы ng serve наше приложение и получайте удовольствие!

PS: в случае появления следующей ошибки: parcelRequire is not defined откройте файл index.html и поместите следующее в тег заголовка

<script>var parcelRequire;</script>

Заключение

В этой серии из двух статей мы рассмотрели, как WebRTC работает под капотом, а затем создали изящное небольшое приложение для видеочата, используя только наши две руки. И действительно мощный фреймворк JavaScript. И библиотека, которая позаботилась обо всем, что касается WebRTC. Но мы сделали это 🔥 так что похлопайте себя по спине и до скорой встречи 🚀

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

ETH: 0xe00ef8a6d7c43bd164d41332d5e577be9bd830b6

📝 Сохраните эту историю в Журнале.