Pull to refresh
151.63
Skillfactory
Учим работать в IT на курсах и в магистратурах

Видеочат с возможностью совместного редактирования текста при помощи Twilio Sync

Reading time12 min
Views1.4K
Original author: Mia Adjei

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


Подготовка

Что вам будет нужно:

  • Бесплатная учётная запись Twilio. Если вы зарегистрируетесь здесь, вы получите 10 долларов в качестве кредита Twilio при переходе на платную учётную запись.

  • Node.js (версия 14.16.1 или выше) и npm.

  • Код из этого репозитория.

Скачайте и запустите код видеоблокнота

Чтобы скачать код, выберите место на вашем компьютере, где вы хотите работать с проектом. Затем откройте окно терминала и выполните следующую команду, чтобы клонировать ветку start:

git clone -b start https://github.com/adjeim/video-note-collab.git

Затем перейдите в корневой каталог проекта и установите необходимые зависимости, выполнив следующие команды:

cd video-note-collab
npm install

Скопируйте файл .env для переменных среды такой командой:

cp .env.template .env 

Затем откройте файл .env в редакторе кода и замените значения переменных своими учётными данными:

TWILIO_ACCOUNT_SID
TWILIO_SYNC_SERVICE_SID
TWILIO_API_KEY_SID
TWILIO_API_KEY_SECRET

Найти свои учётные данные вы можете в консоли Twilio, на странице Twilio Sync Service и на странице Twilio API Keys. В качестве значения переменной TWILIO_SYNC_SERVICE_SID можно использовать SID по умолчанию. Теперь, когда вы ввели свои учётные данные, запустите сервер Express:

npm start

Если вы перейдёте по адресу http://localhost:3000/ в своём браузере, то увидите заметку:

Протестируйте приложение, набрав несколько слов в блокноте. При вводе пробела, клавиши ввода или знаков препинания запускается синхронизация. Если вы откроете второе окно браузера по адресу http://localhost:3000/ и одновременно посмотрите на них, вы должны увидеть, что текст, который вы вводите в одном окне, отображается и в другом:

Как это происходит? Это приложение использует Twilio Sync для синхронизации состояния и данных между браузерами и устройствами в режиме реального времени. Здесь у вас есть документ Sync, который называется notepad. Когда страница загружается, создаётся новый токен доступа, который передаётся Sync клиенту. Если документ существует, его содержимое загружается в <textarea>. Приложение отслеживает обновления документа, обновляя положение курсора и содержимое блокнота. Если вы откроете инструменты разработчика своего браузера, вы также сможете увидеть изменения в контенте, который записывается в консоль.

Идея здесь аналогична идее других инструментов совместной работы с заметками, таким как Google Docs или Notion. С помощью этого типа приложения вы можете вместе с другим человеком просматривать один и тот же документ и вместе редактировать его в режиме реального времени. Запустите приложение — и переходите к следующему шагу

Обновите макет приложения

Первое, что вам нужно сделать, — это немного обновить макет вашего приложения. Если вы прямо сейчас откроете public/index.html в редакторе кода, то увидите, что Tailwind CSS и Twilio Sync уже подключены в проект. Обновите элемент <head>, добавив в него библиотеку Twilio Video, как показано в приведённом ниже коде:

<html>
  <head>
    <meta name='viewport' content='width=device-width, initial-scale=1.0' />
    <link href='https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css' rel='stylesheet'>
    <script type="text/javascript" src="https://media.twiliocdn.com/sdk/js/sync/v2.0/twilio-sync.min.js"></script>
    <script src='https://sdk.twilio.com/js/video/releases/2.15.0/twilio-video.min.js'></script>
    <title>Video Collaboration with Notes</title>
  </head>

Обновите элемент <body>. Сначала добавьте элемент <form>, в котором пользователь может ввести своё имя и нажать кнопку, чтобы присоединиться к комнате.

  <body class='bg-grey-100 p-10 flex flex-wrap container'>
    <form id='login' class='w-full max-h-20 flex items-center py-2'>
        <input class='appearance-none bg-transparent border-b border-green-500 mr-3 py-1 px-2 focus:outline-none'
            id='identity' type='text' placeholder='Enter your name...' required>
        <button id='joinOrLeaveRoom' class='bg-green-500 hover:bg-green-700 text-white py-1 px-4 rounded' type='submit'>
          Join Video Call
        </button>
    </form>
    <textarea id='notepad' class='h-44 w-full shadow-lg border rounded-md p-3 sm:mx-auto sm:w-1/2'></textarea>

Замените элемент <textarea>. В этом проекте вы отключите взаимодействие с документом и установите для него серый цвет фона при первой загрузке приложения.

    </form>
    <textarea disabled id='notepad' class='bg-gray-200 h-140 w-6/12 shadow-lg border rounded-md p-3 sm:mx-auto sm:w-1/2'></textarea>

Затем чуть ниже <textarea> добавьте элемент <div> для вывода видео  участников:

    <textarea disabled id='notepad' class='bg-gray-200 h-140 w-6/12 shadow-lg border rounded-md p-3 sm:mx-auto sm:w-1/2'></textarea>
    <div id='container' class='w-5/12  bg-green-100'>
      <div id='participantsContainer'>
        <div id='localParticipant'>
          <div id='localVideoTrack' class='participant'></div>
        </div>
        <div id='remoteParticipants'>
          <!-- Remote participants will be added here as they join the call -->
        </div>
      </div>
    </div>

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

Обновите генерацию токена доступа

Если вы откроете index.js в своём редакторе кода, вы увидите, что у вас уже есть код для предоставления токена доступа и что к этим токенам доступа будет добавлен SyncGrant. Чтобы задействовать видео, вам также понадобится VideoGrant.

Под вашей константой для SyncGrant также укажите константу для VideoGrant. Пока вы в этой части кода, убедитесь, что ваше приложение может парсить JSON из тела запроса при помощи функции express.json():

const AccessToken = require('twilio').jwt.AccessToken;
const SyncGrant = AccessToken.SyncGrant;
const VideoGrant = AccessToken.VideoGrant;

app.use(express.json());

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

app.post('/token', async (req, res) => {
  if (!req.body.identity || !req.body.room) {
    return res.status(400);
  }

  // Get the user's identity from the request
  const identity = req.body.identity;

  // Create a 'grant' identifying the Sync service instance for this app.
  const syncGrant = new SyncGrant({
      serviceSid: process.env.TWILIO_SYNC_SERVICE_SID
  });

  // Create a video grant
  const videoGrant = new VideoGrant({
    room: req.body.room
  })

  // Create an access token which we will sign and return to the client,
  // containing the grant we just created and specifying their identity.
  const token = new AccessToken(
      process.env.TWILIO_ACCOUNT_SID,
      process.env.TWILIO_API_KEY_SID,
      process.env.TWILIO_API_KEY_SECRET,
  );

  token.addGrant(syncGrant);
  token.addGrant(videoGrant);
  token.identity = identity;

  // Serialize the token to a JWT string and include it in a JSON response
  res.send({
      identity: identity,
      token: token.toJwt()
  });
});


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

Подключение и вывод видео участников

Вернитесь в public/index.html в редакторе кода. Посмотрите на код в последнем теге <script> в конце файла. Вы увидите, что когда приложение загружается в браузере, этот код получает токен доступа из вашего запроса на токен, затем подключает клиент синхронизации и обновляет блокнот. Чтобы добавить видео в этот проект, нужно изменить этот код таким образом, чтобы приложение получило токен после того, как пользователь введёт своё имя и нажмёт кнопку «Join Video Call» в пользовательском интерфейсе. Затем этот токен будет использоваться для подключения пользователя к синхронизированному блокноту и видеозвонку. Чуть ниже вашей переменной notepad добавьте ещё несколько переменных, чтобы упростить обращение к другим элементам в вашем пользовательском интерфейсе:

<script>
  const notepad = document.getElementById('notepad');
  const localVideoTrack = document.getElementById('localVideoTrack');
  const login = document.getElementById('login');
  const identityInput = document.getElementById('identity');
  const joinLeaveButton = document.getElementById('joinOrLeaveRoom');
  const localParticipant = document.getElementById('localParticipant');
  const remoteParticipants = document.getElementById('remoteParticipants');

  let connected = false;
  let room;
  let syncDocument;
  let twilioSyncClient;

Теперь вы сможете обращаться к различным частям видеочата, а также отслеживать, подключён ли локальный пользователь к видеозвонку. Вы также сможете отслеживать, открыт ли синхронизированный документ, с которым пользователь может взаимодействовать. Чтобы сделать предварительный просмотр видеозвонка, добавьте функцию addLocalVideo чуть ниже списка переменных, которые вы обновили выше:

  const addLocalVideo = async () => {
    const videoTrack = await Twilio.Video.createLocalVideoTrack();
    const trackElement = videoTrack.attach();
    localVideoTrack.appendChild(trackElement);
  };

Вызовите эту функцию в конце файла, непосредственно перед закрывающим тегом <script>:

  addLocalVideo();
</script>

Затем создайте новую функцию с именем connectOrDisconnect, которая будет обрабатывать событие, когда пользователь нажимает кнопку «Join Video Call». Если пользователь ещё не подключён к звонку, эта функция подключит их. Если пользователь уже подключён, эта функция отключит его. Добавьте следующий код чуть ниже функции addLocalVideo:

  const connectOrDisconnect = async (event) => {
    event.preventDefault();
    if (!connected) {
      const identity = identityInput.value;
      joinLeaveButton.disabled = true;
      joinLeaveButton.innerHTML = 'Connecting...';

      try {
        await connect(identity);
      } catch (error) {
        console.log(error);
        alert('Failed to connect to video room.');
        joinLeaveButton.innerHTML = 'Join Video Call';
        joinLeaveButton.disabled = false;
      }
    }
    else {
      disconnect();
    }
  };

Непосредственно перед закрывающим тегом <script> добавьте ещё один слушатель событий, на этот раз в форму входа. Этот слушатель будет вызывать connectOrDisconnect всякий раз, когда пользователь отправляет форму:

  // Add listener
  notepad.addEventListener('keyup', (event) => {

    // Define array of triggers to sync (space, enter, and punctuation)
    // Otherwise sync will fire every time
    const syncKeys = [32, 13, 8, 188, 190];

    if (syncKeys.includes(event.keyCode)) {
      syncNotepad(twilioSyncClient);
    }
  })

  login.addEventListener('submit', connectOrDisconnect);

Затем замените блок fetch('/token') следующей функцией подключения, которая примет идентификатор, переданный в форму, получит токен для этого пользователя с сервера, загрузит и включит документ и подключит этого локального пользователя в видеочат:

  const connect = async (identity) => {
    const response = await fetch('/token', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({'identity': identity, room: 'My Video Room'})
    });

    const data = await response.json();
    const token = data.token;

    // Set up sync document
    twilioSyncClient = new Twilio.Sync.Client(token);
    notepad.disabled = false;
    notepad.classList.remove('bg-gray-200');

    syncDocument = await twilioSyncClient.document('notepad');

    // Load the existing Document
    notepad.value = syncDocument.data.content || '';

    // Listen to updates on the Document
    syncDocument.on('updated', (event) => {

      // Update the cursor position
      let cursorStartPos = notepad.selectionStart;
      let cursorEndPos = notepad.selectionEnd;

      notepad.value = event.data.content;

      // Reset the cursor position
      notepad.selectionEnd = cursorEndPos;

      console.log('Received Document update event. New value:', event.data.content);
    })

    // Set up the video room
    room = await Twilio.Video.connect(token);

    const identityDiv = document.createElement('div');
    identityDiv.setAttribute('class', 'identity');
    identityDiv.innerHTML = identity;
    localParticipant.appendChild(identityDiv);

    room.participants.forEach(participantConnected);
    room.on('participantConnected', participantConnected);
    room.on('participantDisconnected', participantDisconnected);
    connected = true;

    joinLeaveButton.innerHTML = 'Leave Video Call';
    joinLeaveButton.disabled = false;
    identityInput.style.display = 'none';
  };

Как только локальный участник подключится к звонку, эта видеокомната будет назначена глобальной переменной для room. Видео локального пользователя будет отображаться в пользовательском интерфейсе, и слушатели событий будут отслеживать, когда другие участники присоединяются к видеовстрече или выйдут из неё. Кнопка «Join Video Call» также изменится на «Leave Video Call», а поле ввода для имени будет скрыто. Теперь, когда вы добавили функцию подключения, добавьте функцию отключения чуть ниже:

const disconnect = () => {
    room.disconnect();

    let removeParticipants = remoteParticipants.getElementsByClassName('participant');

    while (removeParticipants[0]) {
      remoteParticipants.removeChild(removeParticipants[0]);
    }

    joinLeaveButton.innerHTML = 'Join Video Call';
    connected = false;
    identityInput.style.display = 'inline-block';
    localParticipant.removeChild(localParticipant.lastElementChild);
    
    syncDocument.close();
    twilioSyncClient = null;
    notepad.value = '';
    notepad.disabled = true;
    notepad.classList.add('bg-gray-200');
  };

Функция disconnect будет обрабатывать отключение участника от видеозвонка, когда он нажимает кнопку «Leave Video Call». Она также будет проходить через других участников видеозвонка и удалять видео из пользовательского интерфейса для отключённого участника. Состояние подключения снова устанавливается на false, поле ввода имени пользователя появляется снова, а кнопка «Leave Video Call» меняется на «Join Video Call». Кроме того, блокнот будет очищен и отключён для локального участника, как только кто-то отключится от видеозвонка, он также будет отключён от общего блокнота.

Затем вы захотите поработать с тем, что происходит, когда удалённые участники подключаются к звонку или отключаются от него. Начните с добавления функции ParticipantConnected, которая создаст новый <div> для подключённого участника, показывая имя пользователя участника в качестве его идентификатора и прикрепляя их видео и аудиодорожки к <div>, если локальный участник подписан на них.

Эта функция также создаёт обработчики событий для управления действиями по подписке, относящимися к аудио- и видеодорожкам участников. Если удалённый участник отключает свой аудио- или видеопоток, хочется иметь возможность реагировать на это событие и при необходимости подключать или отключать эти дорожки. Добавьте функцию participantConnected чуть ниже вашей функции отключения в public/index.html:

  const participantConnected = (participant) => {
    const participantDiv = document.createElement('div');
    participantDiv.setAttribute('id', participant.sid);
    participantDiv.setAttribute('class', 'participant');

    const tracksDiv = document.createElement('div');
    participantDiv.appendChild(tracksDiv);

    const identityDiv = document.createElement('div');
    identityDiv.setAttribute('class', 'identity');
    identityDiv.innerHTML = participant.identity;
    participantDiv.appendChild(identityDiv);

    remoteParticipants.appendChild(participantDiv);

    participant.tracks.forEach(publication => {
      if (publication.isSubscribed) {
        trackSubscribed(tracksDiv, publication.track);
      }
    });
    participant.on('trackSubscribed', track => trackSubscribed(tracksDiv, track));
    participant.on('trackUnsubscribed', trackUnsubscribed);
  };

Пришло время добавить функцию ParticipantDisconnected для ситуации, когда удалённый участник покидает видеозвонок. Это функция, которая находит участника по его sid (уникальному идентификатору) и удаляет его div из DOM. Добавьте функцию participantDisconnected сразу под вашей функцией ParticipantConnected:

  const participantDisconnected = (participant) => {
    document.getElementById(participant.sid).remove();
  };

А теперь добавим код для ситуации, когда локальный участник подписывается на аудио- или видеодорожки удалённого участника или отписывается от них. Добавьте следующие функции trackSubscribed и trackUnsubscribed в public/index.html чуть ниже вашего кода для participantDisconnected:

  const trackSubscribed = (div, track) => {
    const trackElement = track.attach();
    div.appendChild(trackElement);
  };

  const trackUnsubscribed = (track) => {
    track.detach().forEach(element => {
      element.remove()
    });
  };

У вас есть весь код проекта. Теперь протестируем блокнот с видео.

Тестирование вашего приложения

Перейдите по адресу http://localhost:3000/. Вы должны увидеть сайт, похожий на показанный ниже, с блокнотом слева и видеопотоком локального участника справа:

Введите своё имя в поле ввода и нажмите «Join Video Call». Вы увидите своё имя под видео. Если хотите, можете попробовать набрать текст в блокноте. Откройте другую вкладку браузера по адресу http://localhost:3000/ и присоединитесь к видеозвонку под другим именем. Как только вы это сделаете, вы увидите два изображения, а это означает, что вы можете общаться друг с другом и делиться блокнотом:

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

Что будет дальше с инструментом для совместной работы с заметками?

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

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

Узнайте, как прокачаться и в других специальностях или освоить их с нуля:

Другие профессии и курсы
Tags:
Hubs:
Total votes 5: ↑4 and ↓1+3
Comments0

Articles

Information

Website
www.skillfactory.ru
Registered
Founded
Employees
501–1,000 employees
Location
Россия
Representative
Skillfactory School