使用Amazon IVS聊天创建交互式白板
#aws #javascript #amazonivs #chat

在我的最后几篇文章中,我们一直在扩展主持Amazon Interactive Video Service(Amazon IVS)聊天室。两周前,我们学会了如何执行automated chat moderation with AWS Lambda functions,上周我们看到了如何进行manually moderate chat rooms。在这篇文章中,我想切换齿轮,并谈论亚马逊IVS聊天室的有趣用例 - 现场,交互式白板。

实时流媒体以与娱乐相关的流(例如游戏或体育运动)相关的流而闻名,但它也是提供教育内容的理想工具。我之前在流上关于screen sharing and overlaying canvas elements的博客,但是白板是提供广播员的独特方法,观众是一种现场,交互式的方式,可视化甚至在给定主题上进行协作。这篇文章中的演示非常基本 - 只是徒手绘图的“笔”工具 - 但它有可能扩展形状和图像,使其成为创建自己的白板体验以增强您的Amazon IVS Live的理想起点流。

试试看!

在我们进入如何构建白板之前,请在下面查看其工作原理。您需要为两个唯一用户的现有Amazon IVS聊天室生成聊天令牌,并在两个Codepen嵌入中输入令牌和各自的user-id值。在生产中,您将使用其中一个AWS SDK来生成令牌,但是对于此演示,您可以使用AWS CLI生成它们以查看其工作原理(有关更多信息,请参见this post

$ aws ivschat create-chat-token \
  —room-identifier [CHAT ARN] \
  —user-id "1" \
  —capabilities "SEND_MESSAGE" \
  —query "token" \
  —output text 

使用上面的CLI命令,生成并输入用户“ 1”的令牌,然后在第一个Codepen中输入该user-id。然后,在下面的第二个Codepen中重复用户“ 2”的过程。如果您的聊天室不是在us-east-1中创建的,请更新Endpoint值以匹配您的聊天室的区域。连接两个用户后,您可以在一个画布上绘制并观察另一个画布上的图纸。

如果您没有任何Amazon IVS聊天室来测试 - 为什么不呢?当然只是在开玩笑。这是一个在行动中显示的GIF,您可以看到我的惊人艺术技能:

Amazon IVS Whiteboard Demo

设置东西

对于此演示,我正在通过<form>收集用户ID,笔颜色,聊天令牌和聊天端点。在生产中,您将拥有专用的用户ID,并且您的聊天令牌和端点将来自对服务器(或无服务器功能)的调用。跟踪用户ID很重要,以防止在当前正在绘制的用户的<canvas>上复制图形。这是收集表和图形画布的HTML标记。我已经删除了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
}

初始化聊天连接

现在我们拥有userIdchatTokenchatEndpoint,我们可以通过将其添加到init()函数来初始化与Amazon IVS聊天室的连接:

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>元素中添加三个侦听器:mousedownmousemovemouseup。这些事件的处理程序将需要做两件事:处理当前用户的画布上的绘制,并通过Websocket与其他用户的连接发布事件,以便可以在所有连接的客户端上复制绘图。

考虑使用指针事件而不是鼠标事件(pointerdownpointermovepointerup)来使您的白板响应触摸和鼠标事件。

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的上下文,开始了一条路径,然后移至适当的xy坐标。

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(),它绘制了当前的xy。因为我们在调用onMouseDown()之前将全局变量isDrawing设置为true,所以此方法将被连续调用,直到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 经常。如果我们看一下service quotas for Amazon IVS chat,我们可以看到我们每秒仅限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对象包含whiteboardtype,我们可以用来将白板消息与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()方法,并将我们对mousemove处理程序中handleQueue()的呼叫限制为每50-100ms一次称为每50-100ms。在我的测试中,我发现这是一个可接受的范围,两者都可以防止达到服务配额,并在另一个客户的<canvas>上提供了相当不错的事件序列。

window.throttlePause;

const throttle = (callback, time) => {
  if (window.throttlePause) return;
  window.throttlePause = true;
  setTimeout(() => {
    callback();
    window.throttlePause = false;
  }, time);
};

实现此抽样的唯一要做的事情是修改mousemove处理程序以每50ms排队每50ms排队。

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键,我们在发布消息以区分这些事件和“正常”聊天消息时使用。如果那个typewhiteboard,我们将解析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

在这里,我们用管道(|)和每个事件中的数据划分了每个事件。该事件的第一个字符(012)代表事件类型(0 for mousedown1mousemove2 for mouseup)。第二个字符和第三个字符分别是xy位置。第四个字符,仅是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聊天消息,使聊天开发人员能够将白板添加到其交互式实时流媒体应用程序中。我们还讨论了改善生产应用程序的一些潜在方法。如果您有任何疑问,请在Twitter上发表评论或与我联系。

Adrian的图像来自Pixabay