Создайте приложение для видеочата с помощью Twilio Video и React, используя только функциональные компоненты.

Мы видели видеочат, построенный с помощью React в блоге Twilio, но с тех пор, в версии 16.8, React выпустил Hooks. Хуки позволяют использовать состояние или другие функции React внутри функциональных компонентов вместо написания компонента класса.

В этом посте мы собираемся создать приложение для видеочата с использованием Twilio Video и React только с функциональными компонентами, используя хуки useState, useCallback, useEffect и useRef.

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

Чтобы создать это приложение для видеочата, вам понадобится следующее:

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

Начиная

Итак, мы можем сразу перейти к приложению React, мы можем начать с начального приложения React and Express, созданного мной. Загрузите или клонируйте ветвь twilio начального приложения, перейдите в новый каталог и установите зависимости:

git clone -b twilio [email protected]:philnash/react-express-starter.git twilio-video-react-hooks
cd twilio-video-react-hooks
npm install

Скопируйте файл .env.example в .env.

cp .env.example .env

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

npm run dev

Вы должны увидеть эту страницу в браузере:

Подготовка учетных данных Twilio

Чтобы подключиться к Twilio Video, нам потребуются некоторые учетные данные. Из вашей консоли Twilio скопируйте SID своей учетной записи и введите его в .env файле как TWILIO_ACCOUNT_SID.

Вам также понадобятся ключ и секрет API, вы можете создать их в Программируемых видео инструментах в вашей консоли. Создайте пару ключей и добавьте SID и секрет как TWILIO_API_KEY и TWILIO_API_SECRET в файл .env.

Добавление стиля

В этом посте мы не будем касаться CSS, но давайте добавим немного, чтобы результат не выглядел ужасно! Возьмите CSS из этого URL и замените им содержимое src/App.css.

Теперь мы готовы приступить к сборке.

Планирование наших компонентов

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

Внутри компонента VideoChat мы хотим показать компонент Lobby, в котором пользователь может ввести свое имя и комнату, в которую он хочет присоединиться.

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

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

Сборка компонентов

Компонент приложения

Откройте src/App.js, здесь много кода из исходного примера приложения, который мы можем удалить.

Кроме того, компонент App - это компонент на основе классов. Мы сказали, что создадим все приложение с функциональными компонентами, поэтому лучше это изменить.

Из импорта удалите Component и импорт logo.svg. Замените весь класс App функцией, которая отображает скелет нашего приложения. Весь файл должен выглядеть так:

Компонент VideoChat

Этот компонент будет отображать вестибюль или комнату в зависимости от того, ввел ли пользователь имя пользователя и имя комнаты. Создайте новый файл компонента src/VideoChat.js и начните его со следующего шаблона:

import React from 'react';

const VideoChat = () => {
  return <div></div> // we'll build up our response later
};

export default VideoChat;

Компонент VideoChat будет компонентом верхнего уровня для обработки данных о чате.

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

Мы создадим форму для ввода некоторых из этих данных в следующем компоненте.

В React Hooks мы используем useState Hook для хранения этих данных.

useState

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

Мы деструктурируем этот массив, чтобы получить две разные переменные, такие как state и setState. Мы собираемся использовать setState для отслеживания имени пользователя, имени комнаты и токена в нашем компоненте.

Начните с импорта useState из React и настройте состояния для имени пользователя, имени комнаты и токена:

Затем нам нужны две функции для обработки обновления username и roomName, когда пользователь вводит их в соответствующие элементы ввода.

Хотя это будет работать, мы можем оптимизировать наш компонент, используя здесь еще один React Hook; useCallback.

useCallback

Каждый раз, когда вызывается этот функциональный компонент, handleXXX функции переопределяются. Они должны быть частью компонента, потому что полагаются на функции setUsername и setRoomName, но каждый раз они будут одинаковыми.

useCallback - это React Hook, который позволяет нам запоминать функции. То есть, если они одинаковы между вызовами функций, они не будут переопределены.

useCallback принимает два аргумента: функцию, которая должна быть запомнена, и массив зависимостей функции. Если какая-либо из зависимостей функции изменяется, это означает, что мемоизированная функция устарела, а затем функция переопределяется и снова запоминается.

В этом случае между этими двумя функциями нет зависимостей, поэтому будет достаточно пустого массива (setState функции из useState Hook считаются постоянными внутри функции).

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

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

Мы будем использовать Fetch API, чтобы отправить данные в виде JSON на конечную точку, получить и проанализировать ответ, а затем использовать setToken для сохранения токена в нашем состоянии.

Мы также заключим эту функцию в useCallback, но в этом случае функция будет зависеть от username и roomName, поэтому мы добавляем их как зависимости к useCallback.

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

const handleLogout = useCallback(event => {
    setToken(null);
  }, []);

  return <div></div> // we'll build up our response later
};

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

Давайте создадим компонент Lobby, который отображает форму, которая запрашивает имя пользователя и название комнаты.

Компонент Лобби

Основная задача компонента Lobby - визуализировать форму с использованием этих свойств, например:

Давайте обновим компонент VideoChat, чтобы отобразить Lobby, если у нас нет token, иначе мы будем отображать username, roomName и token.

Нам нужно будет импортировать Lobby компонент в верхней части файла и отобразить JSX в нижней части функции компонента:

Чтобы это отображалось на странице, нам также необходимо импортировать компонент VideoChat в компонент App и отрендерить его. Снова откройте src/App.js и внесите следующие изменения:

Убедитесь, что приложение все еще работает (или перезапустите его с помощью npm run dev), откройте его в браузере, и вы увидите форму.

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

Компонент "Комната"

Теперь, когда мы добавили в приложение имя пользователя и название комнаты, мы можем использовать их для присоединения к комнате чата Twilio Video. Для работы со службой Twilio Video нам понадобится JS SDK, установите его с помощью:

npm install twilio-video --save

Создайте новый файл в каталоге src с именем Room.js. Начните со следующего шаблона.

В этом компоненте мы будем использовать Twilio Video SDK, а также хуки useState и useEffect. Мы также получим roomName, token и handleLogout в качестве свойств родительского компонента VideoChat:

import React, { useState, useEffect } from 'react';
import Video from 'twilio-video';

const Room = ({ roomName, token, handleLogout }) => {

});

export default Room;

Первое, что сделает компонент, это подключится к сервису Twilio Video, используя токен и roomName. При подключении мы получим room объект, который захотим сохранить.

В комнате также есть список участников, который со временем будет меняться, поэтому мы его тоже будем хранить. Мы будем использовать useState для их хранения, начальные значения будут null для комнаты и пустой массив для участников:

const Room = ({ roomName, token, handleLogout }) => {
  const [room, setRoom] = useState(null);
  const [participants, setParticipants] = useState([]);
});

Прежде чем мы перейдем к объединению с комнатой, давайте что-нибудь отрендерим для этого компонента. Мы нанесем карту на массив участников, чтобы показать личность каждого участника, а также показать личность местного участника в комнате:

Давайте обновим компонент VideoChat, чтобы отобразить этот компонент Room вместо информации-заполнителя, которая у нас была ранее.

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

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

Мы также хотим выйти из комнаты после того, как компонент будет уничтожен (нет смысла поддерживать соединение WebRTC в фоновом режиме). Это оба побочных эффекта.

С компонентами на основе классов здесь вы можете использовать методы жизненного цикла componentDidMount и componentWillUnmount. В React Hooks мы будем использовать useEffect Hook.

useEffect

useEffect - это функция, которая принимает метод и запускает его после рендеринга компонента.

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

Давайте начнем создавать наш Hook, добавив этот код перед JSX в Room.js:

При этом используются token и roomName для подключения к сервису Twilio Video.

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

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

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

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

Обратите внимание, что здесь мы используем версию обратного вызова функции setRoom, которую мы получили от useState ранее.

Если вы передадите функцию в setRoom, она будет вызываться с предыдущим значением, в данном случае с существующей комнатой, которую мы назовем currentRoom, и она установит состояние, которое вы вернете.

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

Как и useCallback, мы делаем это, передавая массив переменных, от которых зависит эффект. Если переменные изменились, мы хотим сначала очистить, а затем снова запустить эффект. Если они не изменились, нет необходимости запускать эффект снова.

Глядя на функцию, мы видим, что если бы roomName или token изменились, мы ожидали бы подключения к другой комнате или в качестве другого пользователя. Давайте также передадим эти переменные в виде массива в useEffect:

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

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

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

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

Последний кусок головоломки - отрендерить участников видеозвонка, добавив их видео и аудио на страницу.

Компонент участника

Создайте новый компонент в src под названием Participant.js. Мы начнем с обычного шаблона, хотя в этом компоненте мы собираемся использовать три обработчика: useState и useEffect, которые мы видели, и useRef.

Мы также будем передавать participant объект в props и отслеживать видео- и аудиодорожки участника с помощью useState:

Когда мы получаем от участника видеопоток или аудиопоток, нам нужно прикрепить его к элементу <video> или <audio>.

Поскольку JSX декларативен, мы не получаем прямого доступа к DOM (объектной модели документа), поэтому нам нужно получить ссылку на элемент HTML каким-либо другим способом.

React предоставляет доступ к DOM через refs и useRef Hook. Чтобы использовать ссылки, мы объявляем их заранее, а затем ссылаемся на них в JSX. Мы создаем наши ссылки с помощью useRef Hook, прежде чем что-либо отрендерить:

А пока давайте вернем JSX, который нам нужен. Чтобы подключить элемент JSX к ссылке, мы используем атрибут ref.

Я также установил атрибуты тегов <video> и <audio> на автовоспроизведение (чтобы они воспроизводились, как только у них есть поток мультимедиа) и без звука (чтобы я не оглушал себя отзывами во время тестирования, вы меня благодарите за это, если вы когда-нибудь совершите эту ошибку).

Этот компонент пока мало что делает, так как нам нужно использовать некоторые эффекты. На самом деле мы будем использовать useEffect Hook в этом компоненте трижды, скоро вы поймете, почему.

Первый useEffect Hook установит видео- и аудиодорожки в состояние и настроит слушателей для объекта-участника, когда треки добавляются или удаляются. Также потребуется очистить и удалить эти прослушиватели и очистить состояние, когда компонент отключен.

В наш первый useEffect Hook мы добавим две функции, которые будут запускаться либо при добавлении дорожки, либо при удалении ее из участника.

Эти функции проверяют, является ли дорожка звуковой или видеодорожкой, а затем добавляют или удаляют ее из состояния с помощью соответствующей функции состояния.

Затем мы используем объект participant для установки начальных значений для аудио- и видеодорожек, настраиваем слушателей для событий trackSubscribed и trackUnsubscribed с помощью только что написанных функций, а затем выполняем очистку в возвращенной функции:

Обратите внимание, что ловушка зависит только от объекта participant и не будет очищена и запущена повторно, если участник не изменится.

Нам также понадобится useEffect Hook, чтобы прикрепить видео- и аудиодорожки к DOM. Я покажу только одну из них, видео-версию, но звук останется таким же, если вы замените аудио на видео.

Хук получит первую видеодорожку из состояния и, если она существует, присоединит ее к узлу DOM, который мы захватили с помощью ссылки ранее. Вы можете ссылаться на текущий узел DOM в ссылке, используя videoRef.current.

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

Повторите эту ловушку для audioTracks, и мы готовы визуализировать наш Participant компонент из Room компонента.

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

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

Заключение

Сборка с помощью Twilio Video в React требует немного больше работы, потому что приходится иметь дело со всевозможными побочными эффектами.

От запроса на получение токена, подключения к видеосервису и манипулирования DOM для соединения элементов <video> и <audio>, есть немало вещей, которые нужно разобраться.

В этом посте мы увидели, как использовать useState, useCallback, useEffect и useRef для управления этими побочными эффектами и создания нашего приложения, используя только функциональные компоненты.

Надеюсь, это поможет вам понять как Twilio Video, так и React Hooks. Весь исходный код этого приложения доступен на GitHub, чтобы вы могли разобрать его и собрать.

Для дальнейшего чтения по React Hooks взгляните на официальную документацию, которая очень подробно описывает эту визуализацию мышления с помощью хуков, и ознакомьтесь с глубоким погружением Дэна Абрамова в useEffect (это длинный пост, но того стоит, обещаю).

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

Если вы создадите эти или другие интересные функции видеочата в React, дайте мне знать в комментариях.