В нескольких последних сообщениях мы подробно рассмотрели модерирование чатов Amazon Interactive Video Service (Amazon IVS). Две недели назад мы узнали, как выполнять автоматическую модерацию чата с помощью функций AWS Lambda, а на прошлой неделе мы увидели, как вручную модерировать чаты. В этом посте я хотел бы переключиться и рассказать об интересном примере использования чатов Amazon IVS — живых интерактивных досках.

Прямая трансляция в основном известна развлекательными потоками, такими как игры или спорт, но это также идеальный инструмент для доставки образовательного контента. Я уже писал в блоге о совместном использовании экрана и наложении элементов холста на поток, но интерактивная доска — это уникальный способ предоставить вещателю и зрителям живой интерактивный способ визуализации и даже совместной работы над заданной темой. Демонстрация в этом посте очень проста — это всего лишь инструмент «ручка» для рисования от руки — но его можно расширить для фигур и изображений, что делает его идеальной отправной точкой для создания собственной интерактивной доски для улучшения вашего Amazon IVS в прямом эфире. потоки.


Попробуйте!

Прежде чем мы перейдем к тому, как построить доску, посмотрите, как она работает ниже. Вам потребуется сгенерировать токен чата для существующей комнаты чата Amazon IVS для двух уникальных пользователей и ввести токены и соответствующие user-id значения в двух встраиваниях CodePen. В производственной среде вы бы использовали один из AWS SDK для создания своих токенов, но для этой демонстрации вы можете сгенерировать их с помощью интерфейса командной строки AWS, чтобы увидеть, как это работает (см. этот пост для получения дополнительной информации).

$ aws ivschat create-chat-token \
  —room-identifier [CHAT ARN] \
  —user-id "1" \
  —capabilities "SEND_MESSAGE" \
  —query "token" \
  —output text 
Войти в полноэкранный режим

Выйти из полноэкранного режима

С помощью приведенной выше команды CLI сгенерируйте и введите токен для пользователя «1» и введите его. user-id в первом CodePen. Затем повторите процесс для пользователя «2» во втором CodePen ниже. Если ваш чат не был создан в us-east-1обновить Endpoint значение, соответствующее региону вашего чата. Когда оба пользователя подключены, вы можете рисовать на одном холсте и наблюдать за рисованием на другом холсте.

Если у вас нет чатов Amazon IVS для тестирования — ПОЧЕМУ НЕТ?? Шучу, конечно. Вот GIF, показывающий это в действии, где вы можете увидеть мои удивительные художественные навыки:

Демонстрация интерактивной доски Amazon IVS


Настройка вещей

Для этой демонстрации я собираю идентификатор пользователя, цвет пера, токен чата и конечную точку чата через <form>. В рабочей среде у вас будет выделенный идентификатор пользователя, а ваш токен чата и конечная точка будут исходить от вызова сервера (или бессерверной функции). Важно отслеживать идентификатор пользователя, чтобы предотвратить копирование рисунка на <canvas> пользователя, который в данный момент рисует. Вот разметка HTML для формы коллекции и холста для рисования. Я удалил классы Bootstrap, используемые в CodePen, чтобы код было легче читать.

<div id="settings">
  <div>Settings</div>
  <div>
    <div>
      <label for="chat-userid">Chat UserId</label>
      <div>
        <input type="text" id="chat-userid" required />
      </div>
    </div>
    <div>
      <label for="pen-color">Pen Color</label>
      <div>
        <input type="color" id="pen-color" required />
      </div>
    </div>
    <div>
      <label for="chat-token">Chat Token</label>
      <div>
        <input type="text" id="chat-token" required />
      </div>
    </div>
    <div>
      <label for="chat-endpoint">Endpoint</label>
      <div>
        <input type="text" id="chat-endpoint" required />
      </div>
    </div>
    <div>
      <div>
        <button type="button" id="submit-settings">Submit</button>
      </div>
    </div>
  </div>
</div>
<div id="whiteboard-container">
  <canvas id="whiteboard">
    Sorry, your browser does not support HTML5 canvas technology.
  </canvas>
</div>
Войти в полноэкранный режим

Выйти из полноэкранного режима

Далее я добавил DOMContentLoaded слушатель, чтобы установить случайный цвет пера и слушать Представлять на рассмотрение щелчок кнопки.

document.addEventListener('DOMContentLoaded', () => {
  document.getElementById('pen-color').value = `#${Math.floor(Math.random()*16777215).toString(16)}`;
  document.getElementById('submit-settings').addEventListener('click', () => {
    init();
  })
});
Войти в полноэкранный режим

Выйти из полноэкранного режима

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

const init = () => {
  window.chatEndpoint = document.getElementById('chat-endpoint').value;
  window.userId = document.getElementById('chat-userid').value;
  window.chatToken = document.getElementById('chat-token').value;
  window.penColor = document.getElementById('pen-color').value;
  if (!window.chatEndpoint || !window.chatToken || !window.userId) {
    alert('Chat Endpoint, Token and UserId are required!');
    return;
  }
  document.getElementById('settings').classList.add('d-none');

  // init chat connection
}
Войти в полноэкранный режим

Выйти из полноэкранного режима


Инициализация чат-соединения

Теперь, когда у нас есть userId, chatTokenа также chatEndpointмы можем инициализировать наше соединение WebSocket с чат-комнатой Amazon IVS, добавив его в init() функция:

window.connection = new WebSocket(window.chatEndpoint, window.chatToken);
window.connection.addEventListener('message', (e) => {
  // todo: handle message
});
Войти в полноэкранный режим

Выйти из полноэкранного режима

Мы немного заполним обработчик сообщений. А пока давайте посмотрим, как рисовать на холсте.


Рисование на холсте

Прежде чем мы сможем рисовать на холсте, мы добавим немного конфигурации к элементу холста в init() функция.

const whiteboardContainer = document.getElementById('whiteboard-container');
const canvasEl = document.getElementById('whiteboard');
canvasEl.width = whiteboardContainer.offsetWidth;
canvasEl.height = whiteboardContainer.offsetHeight;
const ctx = canvasEl.getContext('2d');
ctx.lineWidth = 5;
ctx.fillStyle = '#fff';
ctx.fillRect(0, 0, canvasEl.width, canvasEl.height);
Войти в полноэкранный режим

Выйти из полноэкранного режима

Фрагмент выше устанавливает ширину и высоту <canvas> и устанавливает цвет фона по умолчанию.


Обработка событий мыши

Мы добавим трех слушателей к нашему <canvas> элемент: mousedown, mousemoveа также mouseup. Обработчики этих событий должны будут делать две вещи: обрабатывать рисунок на холсте текущего пользователя и публиковать событие через соединение WebSocket для других пользователей, чтобы рисунок можно было реплицировать на всех подключенных клиентах.

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

canvasEl.addEventListener('mousedown', (e) => {
  window.isDrawing = true;
  const evt = { x: e.offsetX, y: e.offsetY, type: 'mousedown' };
  onMouseDown(evt);
  // queue event for publishing
});
canvasEl.addEventListener('mousemove', (e) => {
  if (window.isDrawing) {
    const evt = { x: e.offsetX, y: e.offsetY, type: 'mousemove' };
    // queue event for publishing
    onMouseMove(evt);
  }
});
canvasEl.addEventListener('mouseup', (e) => {
  window.isDrawing = false;
  onMouseUp({});
  // queue event for publishing
});
Войти в полноэкранный режим

Выйти из полноэкранного режима

Теперь давайте посмотрим на каждую из функций, которые выполняют фактическое рисование холста. Первый, onMouseDown() который получает 2d контекст для холста, начинает путь и перемещается к нужному x а также y координаты.

const onMouseDown = (e) => {
  const canvasEl = document.getElementById('whiteboard');
  const ctx = canvasEl.getContext('2d');
  ctx.beginPath();
  const x = e.x;
  const y = e.y;
  ctx.moveTo(x, y);
};
Войти в полноэкранный режим

Выйти из полноэкранного режима

Следующий, onMouseMove()который рисует линию к текущему x а также y. Поскольку мы устанавливаем глобальную переменную isDrawing к true прежде чем звонить onMouseDown()этот метод будет вызываться непрерывно до тех пор, пока mouseup событие устанавливает isDrawing флаг обратно в false. Это означает, что onMouseMove() будет рисовать линию, пока мы не отпустим кнопку мыши.

const onMouseMove = (e, color) => {
  const canvasEl = document.getElementById('whiteboard');
  const ctx = canvasEl.getContext('2d');
  const x = e.x;
  const y = e.y;
  ctx.lineTo(x, y);
  ctx.strokeStyle = color || window.penColor;
  ctx.stroke();
};
Войти в полноэкранный режим

Выйти из полноэкранного режима

Окончательно, onMouseUp() закрывает путь, который мы начали в onMouseUp().

const onMouseUp = (e) => {
  const canvasEl = document.getElementById('whiteboard');
  const ctx = canvasEl.getContext('2d');
  ctx.closePath();
}
Войти в полноэкранный режим

Выйти из полноэкранного режима

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


Публикация событий рисования

Нет гарантии того, как часто будет срабатывать событие мыши, но большинство браузеров будут срабатывать. mousemove частенько. Если мы посмотрим на сервисные квоты для чата Amazon IVS, мы видим, что мы ограничены 10 транзакциями в секунду. Было бы довольно легко достичь пределов квоты, если бы мы попытались публиковать каждый mousemove событие для всех подключенных пользователей чата. Чтобы обойти это, мы можем сделать две вещи: отправить события в пакете и опубликовать только образец mousemove события другим подключенным клиентам.


Постановка событий мыши в очередь

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

window.queue = [];
window.maxQueueSize = 20;
Войти в полноэкранный режим

Выйти из полноэкранного режима

Далее мы создадим handleQueue() метод создания пакета событий. Когда размер пакета больше нашего настроенного window.maxQueueSize (или когда mouseup событие получено), мы отправим текущую партию.

const handleQueue = (event) => {
  if (window.queue.length <= window.maxQueueSize) {
    window.queue.push(event);
  }
  if (window.queue.length === window.maxQueueSize || event.type == 'mouseup') {
    sendEvents();
  }
};
Войти в полноэкранный режим

Выйти из полноэкранного режима


Публикация событий мыши

sendEvents() метод создает полезную нагрузку, содержащую сериализованную версию JSON очереди событий, и отправляет ее так же, как мы обычно отправляем сообщение чата в комнату чата с SEND_MESSAGE. Обратите внимание на Attribute объект содержит type из whiteboard которые мы можем использовать, чтобы отличить сообщения интерактивной доски от обычных сообщений чата в message обработчик (мы рассмотрим этот обработчик ниже).

const sendEvents = () => {
  const payload = {
    'Action': 'SEND_MESSAGE',
    'Content': '[whiteboard event]',
    'Attributes': {
      'type': 'whiteboard',
      'color': window.penColor,
      'events': JSON.stringify(window.queue),
    }
  }
  try {
    window.connection.send(JSON.stringify(payload));
    window.queue = [];
  }
  catch (e) {
    console.error(e);
  }
}
Войти в полноэкранный режим

Выйти из полноэкранного режима

Теперь мы можем изменить mouse обработчики событий для вызова handleQueue().

canvasEl.addEventListener('mousedown', (e) => {
  window.isDrawing = true;
  const evt = { x: e.offsetX, y: e.offsetY, type: 'mousedown' };
  onMouseDown(evt);
  handleQueue(evt);
});
canvasEl.addEventListener('mousemove', (e) => {
  if (window.isDrawing) {
    const evt = { x: e.offsetX, y: e.offsetY, type: 'mousemove' };  
  handleQueue(evt);
  onMouseMove(evt);
  }
});
canvasEl.addEventListener('mouseup', (e) => {
  window.isDrawing = false;
  onMouseUp({});
  handleQueue(evt);
});
Войти в полноэкранный режим

Выйти из полноэкранного режима


Выборка событий перемещения мыши

Если бы мы запустили это приложение в этот момент, мы, вероятно, быстро исчерпали бы нашу квоту службы, даже если мы отправляем события пакетами. Как упоминалось выше, мы можем попробовать mousemove мероприятие, чтобы предотвратить это. мы добавим throttle() метод и ограничим наш вызов handleQueue() в mousemove обработчик вызывается каждые 50-100 мс. В своих тестах я обнаружил, что это приемлемый диапазон, который предотвращает попадание в квоту службы и обеспечивает достаточно хорошее воссоздание последовательности событий на другом клиенте. <canvas>.

window.throttlePause;

const throttle = (callback, time) => {
  if (window.throttlePause) return;
  window.throttlePause = true;
  setTimeout(() => {
    callback();
    window.throttlePause = false;
  }, time);
};
Войти в полноэкранный режим

Выйти из полноэкранного режима

Единственное, что осталось сделать для реализации этой выборки, это изменить mousemove обработчик только для постановки события в очередь каждые 50 мс.

canvasEl.addEventListener('mousemove', (e) => {
  if (window.isDrawing) {
    const evt = { x: e.offsetX, y: e.offsetY, type: 'mousemove' };
    throttle(() => {
      handleQueue(evt);
    }, 50);
    onMouseMove(evt);
  }
});
Войти в полноэкранный режим

Выйти из полноэкранного режима


Обработка входящих событий мыши

Теперь, когда мы реализовали локальный рисунок и логику для постановки в очередь и публикации событий, нам просто нужно добавить наш message обработчик соединения WebSocket для обработки опубликованных событий и воссоздания рисунков от других подключенных клиентов. Назад внутри нашего init() функция, сразу после того, как мы создадим WebSocket подключение, добавьте следующее:

window.connection.addEventListener('message', (e) => {
  const data = JSON.parse(e.data);
  const msgType = data.Attributes.type;

  if(msgType == 'whiteboard') {
    const events = JSON.parse(data.Attributes.events);
    const color = data.Attributes.color;
    const eventUserId = data.Sender.UserId;
    events.forEach(e => {
      const type = e.type;
      if(eventUserId != window.userId) {
        switch(type){
          case 'mousedown':
            onMouseDown({x: e.x, y: e.y});
            break;
          case 'mousemove':
            onMouseMove({x: e.x, y: e.y}, color);
            break;
          case 'mouseup':
            onMouseUp({});
            break;
        };  
      }      
    });  
  }

  // otherwise, handle as an incoming chat...
});
Войти в полноэкранный режим

Выйти из полноэкранного режима

В обработчике выше мы сначала проверяем Attributes объект для type ключ, который мы использовали при публикации сообщений, чтобы отличать эти события от «обычных» сообщений чата. Если это type является whiteboardмы разбираем events Строка JSON, которая содержит массив событий и зацикливается на массиве. Внутри цикла мы проверяем, eventUserId — лицо, публикующее событие — не является текущим userId. Если нет, мы воссоздаем операцию рисования на локальном <canvas> путем вызова соответствующей функции.


Возможные улучшения

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


Повышение производительности

Поскольку эта демонстрация использует JSON для публикации событий, полезная нагрузка, которую мы публикуем, больше, чем должна быть. Чтобы увеличить количество событий, которые могут быть опубликованы в одном сообщении, ваше приложение может публиковать события в текстовом формате с разделителями и использовать идентификаторы для событий. Например, вместо такого массива JSON, который публикует полезную нагрузку размером 94 байта:

[
  {
    "x": 100,
    "y": 100,
    "type": "mousedown"
  },
  {
    "x": 100,
    "y": 100,
    "type": "mousemove"
  },
  {
    "type": "mouseup"
  }
]
Войти в полноэкранный режим

Выйти из полноэкранного режима

Вместо этого вы можете отформатировать данные следующим образом:

0,100,100|1,100,100,#ff9911|2
Войти в полноэкранный режим

Выйти из полноэкранного режима

Здесь мы разделили каждое событие вертикальной чертой (|) и данные внутри каждого события через запятую. Первый персонаж события (0, 1, 2) представляет тип события (0 для mousedown, 1 за mousemoveа также 2 за mouseup). Второй и третий символы являются x а также y позиции соответственно. Четвертый символ, необходимый только для mousemove события, это цвет пера. В этом формате полезная нагрузка составляет 31 байт, что означает уменьшение размера на 67%.

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

payload = '0,100,100|1,100,100,#ff9911|2';
events = payload.split('|');
events.forEach((e) => {
  let type = e[0];
  let x = e[1];
  let y = e[2];
  let color = e[3];
  switch(type){
    case 0:
      onMouseDown({x, y});
      break;
    case 1:
      onMouseMove({x,y}, color);
      break;
    case 2:
      onMouseUp({});
      break;
  }
})
Войти в полноэкранный режим

Выйти из полноэкранного режима


Резюме

В этом посте мы создали базовое доказательство концепции, в которой используются сообщения чата Amazon IVS, чтобы дать разработчикам чатов возможность добавлять интерактивную доску в свои интерактивные приложения для потоковой передачи в реальном времени. Мы также обсудили некоторые возможные способы улучшения приложения в производственной среде. Если у вас есть какие-либо вопросы, оставьте комментарий или свяжитесь со мной по Твиттер.

Автор изображения Адриан из Pixabay