使用Amazon IVS在浏览器中创建实时多主机视频聊天
#aws #javascript #amazonivs #livestreaming

我们最近通过一个名为Amazon Interactive Video Service(Amazon IVS)的新虚拟资源(Amazon Interactive Video Service(Amazon IVS))来支持协作实时流。此功能打开了过去实施的多种可能性,这些可能性并不容易(甚至可能)。此功能使Live Streamers能够邀请客人参与他们的流,与其他内容创建者合作,甚至可以与Audio参与者一起创建“呼叫”样式表演。在这篇文章中,我们将研究如何开始使用Web广播SDK的多个主机创建Web应用程序。与往常一样,docs是一个很棒的起点,但是它总是有助于浏览演示应用程序,因此我们将重点关注这篇文章。

对于此演示,我们将构建一个Web应用程序,该应用程序允许多个参与者互相查看和交谈。在以后的文章中,我们将添加将对话广播到Amazon IVS频道的能力,其他人可以在其中查看对话。

在高级别上,我们将要采取以下步骤来创建虚拟“阶段”应用程序,以进行多个主机之间的实时协作。

  • 通过SDK创建stage
  • 舞台参与者的问题令牌
  • 连接到客户端上的stage
  • 参与者加入阶段时的音频和视频

服务器端

我们将利用JavaScript(V3)的AWS SDK的新的@aws-sdk/client-ivs-realtime模块(docs)来创建舞台资源和参与者令牌。这是您可以用来处理这些步骤的example of a serverless application,但是对于我的演示,我决定简化一些内容,并创建了一个可以托管API和我的前端的Express应用程序。

通过JavaScript(V3)的AWS SDK创建舞台资源

在我的Express应用程序中,我创建了一个名为IvsRealtimeService的服务。此服务将现有的stagesstageParticipants缓存到内存中,以避免需要数据库持续存在。

在生产中,您需要将这些值存储在持久的数据存储中,以避免缓存内存中值的波动和局限性(再次,请参阅serverless demo6,以了解有关使用Amazon Dynamodb持久阶段和表象的更多信息)。

我的应用程序有一条称为/ivs-stage的路线,可用于POST a级name。该路线使用我的IvsRealtimeService检查以该名称检查现有阶段,然后返回现有阶段或创建新阶段。

router.post('/ivs-stage', async function (req, res, next) {
  const body = req.body;
  const stageName = body.name;
  let stage = 
    ivsRealtimeService.stageExists(stageName) ? 
      ivsRealtimeService.getStage(stageName) : 
      await ivsRealtimeService.createStage(stageName);
  res.json(stage);
});

stageExists()getStage()方法看起来像:

import { CreateParticipantTokenCommand, CreateStageCommand, IVSRealTimeClient } from "@aws-sdk/client-ivs-realtime";

const config = {
  credentials: {
    accessKeyId: process.env.ACCESS_KEY,
    secretAccessKey: process.env.SECRET_KEY,
  }
};

export default class IvsRealtimeService {
  ivsRealtimeClient = new IVSRealTimeClient(config);
  stages = {};
  stageParticipants = {};

  getStage(name) {
    return this.stages[name];
  }

  stageExists(name) {
    return this.stages.hasOwnProperty(name);
  }
}

createStage()方法通过ivsRealtimeClient发送CreateStageCommand

async createStage(name) {
  const createStageRequest = new CreateStageCommand({ name });
  const createStageResponse = await this.ivsRealtimeClient.send(createStageRequest);
  this.stages[name] = createStageResponse.stage;
  this.stageParticipants[name] = [];
  return this.stages[name];
};

产生舞台参与者令牌

我在应用程序中创建了另一条路由,以处理名为/ivs-stage-token的生成参与者令牌。

router.post('/ivs-stage-token', async function (req, res, next) {
  const body = req.body;
  const username = body.username;
  const stageName = body.stageName;
  const userId = uuid4();
  let token = await ivsRealtimeService.createStageParticipantToken(stageName, userId, username);
  res.json(token);
});

请注意,此端点需要一个usernamestageNameuserId并调用ivsRealtimeService.createStageParticipantToken()

async createStageParticipantToken(stageName, userId, username, duration = 60) {
  let stage;
  if (!this.stageExists(stageName)) {
    stage = await this.createStage(stageName)
  }
  else {
    stage = this.getStage(stageName);
  }
  const stageArn = stage.arn;

  const createStageTokenRequest = new CreateParticipantTokenCommand({
    attributes: {
      username,
    },
    userId,
    stageArn,
    duration,
  });
  const createStageTokenResponse = await this.ivsRealtimeClient.send(createStageTokenRequest);
  const participantToken = createStageTokenResponse.participantToken;
  this.stageParticipants[stageName].push(participantToken);
  return participantToken;
};

在此方法中,我们正在创建一个CreateParticipantTokenCommand,该CreateParticipantTokenCommand期望一个包含userIdstageArn,令牌持续时间(默认值:60分钟)的输入对象和可用于存储任意应用程序特定值的attributes对象(就我而言,是username)。 attributes将在我们创建前端时以后可用,因此这是包含参与者特定信息的好方法。但是,就像我们的文档所说:

该领域暴露于所有阶段参与者,不应用于个人识别,机密或敏感信息。

现在,我们已经创建了一些端点来帮助我们创建阶段资源和参与者令牌,让我们看创建Web应用程序。

构建Web应用程序

前端将是一个直接的,香草JavaScript和HTML应用程序,可以使事情变得简单,并专注于学习Web广播SDK。对于此演示,我们可以添加返回HTML文件的路由。在该文件中,包括Web广播SDK(版本1.3.1)。

包括和标记

<script src="https://web-broadcast.live-video.net/1.3.1/amazon-ivs-web-broadcast.js"></script>

由于这个虚拟阶段的参与者数量是动态的(最多12个),因此创建一个包含<video>标签以及我们需要的任何其他按钮,标签或标记的<template>是有意义的。这是看起来的样子。

<template id="stages-guest-template">
  <video class="participant-video" autoplay></video>
  <div>
    <small class="participant-name"></small>
  </div>
  <div>
    <button type="button" class="settings-btn">Cam/Mic Settings</button>
  </div>
</template>

我也有一个空的<div>,该29将用于渲染参与者加入虚拟阶段。

<div id="participants"></div>

JavaScript

现在,我们已经拥有Web广播SDK依赖性,并准备好标记了,我们可以查看加入虚拟“阶段”并培养参与者所需的JavaScript。这涉及几个步骤,但是我们可以将它们分解为可管理的功能,以使事情变得简单。 DOM准备就绪时,我们可以调用以下功能(我们将在下面查看每个功能)。

let
  audioDevices,
  videoDevices,
  selectedAudioDeviceId,
  selectedVideoDeviceId,
  videoStream,
  audioStream,
  username,
  stageConfig,
  username = '[USERNAME]',
  stageName = '[STAGE NAME]',
  stageParticipantToken,
  stage;

document.addEventListener('DOMContentLoaded', async () => {
  await handlePermissions();
  await getDevices();
  await createVideoStream();
  await createAudioStream();
  stage = await getStageConfig(stageName);
  stageParticipantToken = await getStageParticipantToken(stage.name, username);
  await initStage();
});

设备和权限

如果您过去与Amazon IVS Web广播SDK合作,则第一个4个方法调用(handlePermissions()getDevices()getDevices()createVideoStream()createAudioStream())应该看起来很熟悉。我们将快速查看下面的每个功能,但是您始终可以参考docs以获取更多信息。

首先,handlePermissions()让我们提示用户许可访问其网络摄像头和麦克风。

const handlePermissions = async () => {
  let permissions;
  try {
    const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });
    for (const track of stream.getTracks()) {
      track.stop();
    }
    permissions = { video: true, audio: true };
  }
  catch (err) {
    permissions = { video: false, audio: false };
    console.error(err.message);
  }
  if (!permissions.video) {
    console.error('Failed to get video permissions.');
  } else if (!permissions.audio) {
    console.error('Failed to get audio permissions.');
  }
};

接下来,getDevices()检索网络摄像头和麦克风列表,并存储它们。在此演示中,我将所选的视频和音频设备默认为第一个可用的设备,但是在您的应用程序中,您可能会在<select>中显示这些设备,以使用户选择他们想要使用的广播设备。<<<<<<<<<<< br>

const getDevices = async () => {
  const devices = await navigator.mediaDevices.enumerateDevices();
  videoDevices = devices.filter((d) => d.kind === 'videoinput');
  audioDevices = devices.filter((d) => d.kind === 'audioinput');
  selectedVideoDeviceId = videoDevices[0].deviceId;
  selectedAudioDeviceId = audioDevices[0].deviceId;
};

现在我们列出了设备,我们可以从它们创建视频和音频流。对于多主机视频,我们应该确保牢记帧和视频尺寸的recommended limits

const createVideoStream = async () => {
  videoStream = await navigator.mediaDevices.getUserMedia({
    video: {
      deviceId: {
        exact: selectedVideoDeviceId
      },
      width: {
        ideal: 1280,
        max: 1280,
      },
      height: {
        ideal: 720,
        max: 720,
      },
      frameRate: {
        max: 30,
      },
    },
  });
};

const createAudioStream = async () => {
  audioStream = await navigator.mediaDevices.getUserMedia({
    audio: {
      deviceId: selectedAudioDeviceId
    },
  });
};

配置和加入舞台

现在,我们已经有了权限,设备和流,我们可以专注于虚拟阶段。首先,声明一些必要的变量。

const { Stage, SubscribeType, LocalStageStream, StageEvents, StreamType } = IVSBroadcastClient;

现在,我们可以调用getStageConfig()方法,该方法调用我们在上面创建的API端点以创建(或检索)阶段资源。阶段name的价值此处是我们的应用程序如何允许多个参与者加入相同的虚拟阶段,因此我们可能希望以某种方式通过(也许是通过URL变量或从后端检索到)。

const getStageConfig = async (name) => {
  const stageRequest = await fetch('/ivs-stage', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ name })
  });
  const stage = await stageRequest.json();
  return stage;
};

接下来,getStageParticipantToken()从我们上面创建的其他API端点中检索一个令牌。 stageName变量是上面从getStageConfig()返回的stage对象的属性,而username取决于您的应用程序逻辑(也许您可以访问用户属性登录的当前电流)。

const getStageParticipantToken = async (stageName, username) => {
  const stageTokenRequest = await fetch('/ivs-stage-token', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ username, stageName }),
  });
  const token = await stageTokenRequest.json();
  return token;
};

现在,我们准备好配置并加入舞台。 initStage()方法将创建一个Stage对象的实例,该对象期望阶段参与者令牌和strategy对象。 strategy对象包含三个表达阶段所需状态的函数(有关策略的所有不同可能性,请参阅docs)。在initStage()方法中,我们可以像这样定义它。

const initStage = async () => {
  const strategy = {
    shouldSubscribeToParticipant: (participant) => {
      return SubscribeType.AUDIO_VIDEO;
    },
    shouldPublishParticipant: (participant) => {
      return true;
    },
    stageStreamsToPublish: () => {
      const videoTrack = videoStream.getVideoTracks()[0]
      const audioTrack = audioStream.getAudioTracks()[0]
      const streamsToPublish = [
        new LocalStageStream(videoTrack),
        new LocalStageStream(audioTrack)
      ];
      return streamsToPublish;
    },
  };
}

在我们继续使用initStage()方法的其余部分之前,让我们分解策略对象。第一个功能(shouldSubscribeToParticipant())表示如何处理加入应用程序的每个参与者。这使我们可以自由添加可以充当主持人(NONE),仅听众的参与者(AUDIO)或具有完整视频和音频的参与者(AUDIO_VIDEO)的参与者(

)。

接下来,shouldPublishParticipant()表示是否应发布参与者。您可能需要根据单击按钮或复选框检查参与者的状态,以使参与者能够保持未发表直到他们准备就绪。

最后,stageStreamsToPublish()表达了一系列LocalStageStream对象,其中包含应发布的MediaStream元素。在此演示中,我们将使用上面创建的videoStreamaudioStream来生成这些59。

接下来,在initStage()方法内,我们创建了一个Stage类的实例,将其传递给参与者令牌和策略。

stage = new Stage(stageParticipantToken.token, strategy);

现在我们有了一个Stage实例,我们可以将听众附加到舞台上播放的各种事件中。参见docs for all of the possible events。在此演示中,我们将聆听添加或删除参与者时。

当添加参与者时,我们将使参与者。

stage.on(StageEvents.STAGE_PARTICIPANT_STREAMS_ADDED, (participant, streams) => {
  renderParticipant(participant, streams);
});

renderParticipant()方法定义在initStage()方法之外,克隆我们上面定义并为给定参与者定义并自定义的<template>。请注意,participant对象包含一个布尔值isLocal;我们可以使用它来为当前本地参与者添加视频流,以避免回荡自己的声音。

const renderParticipant = (participant, streams) => {
  // clone the <template>
  const guestTemplate = document.getElementById('stagesGuestTemplate');
  const newGuestEl = guestTemplate.content.cloneNode(true);

  // populate the template values
  newGuestEl.querySelector('.participant-col').setAttribute('data-participant-id', participant.id);
  newGuestEl.querySelector('.participant-name').textContent = participant.attributes.username;

  // get a list of streams to add
  let streamsToDisplay = streams;
  if (participant.isLocal) {
    streamsToDisplay = streams.filter(stream => stream.streamType === StreamType.VIDEO)
  }

  // add all audio/video streams to the <video>
  const videoEl = newGuestEl.querySelector('.participant-video');
  videoEl.setAttribute('id', `${participant.id}-video`);
  const mediaStream = new MediaStream();
  streamsToDisplay.forEach(stream => {
    mediaStream.addTrack(stream.mediaStreamTrack);
  });
  videoEl.srcObject = mediaStream;

  // add the cloned template to the list of participants
  document.getElementById('participants').appendChild(newGuestEl);
};

回到initStage(),我们可以在参与者离开舞台时聆听,以便我们可以从DOM中删除他们的视频。

stage.on(StageEvents.STAGE_PARTICIPANT_STREAMS_REMOVED, (participant, streams) => {
  const videoId = `${participant.id}-video`
  document.getElementById(videoId).closest('.participant-col').remove();
});

对于此演示,我们不会添加任何其他听众。您的业​​务需求将决定其他听众,并且您的申请可以根据需要对这些听众做出响应。例如,如果我们想通过当前连接状态更新客户端的指示器,则可以收听StageEvents.STAGE_CONNECTION_STATE_CHANGED并在调用处理程序时设置状态指示器。

initStage()内部的最后一步是加入舞台。

try {
   await stage.join();
} 
catch (error) {
   // handle join exception
}

离开舞台

这不是完全必要的,但是我们可以通过明确离开参与者退出应用程序的阶段来改善用户体验。这将确保其余的参与者UI会更早更新。为此,我们可以使用beforeunload处理程序调用stage.leave()来确保干净的断开连接。

const cleanUp = () => {
  if (stage) stage.leave();
};

document.addEventListener("beforeunload", cleanUp);

现在我们的应用程序准备好测试了。运行该应用程序为我们提供了最多12名参与者之间的实时视频聊天体验。

Multi-host video chat

概括

在这篇文章中,我们学会了如何为多达12名参与者创建实时视频聊天体验。在我们的下一篇文章中,我们将学习如何采取下一步并将实时聊天广播到Amazon IVS频道,以便最终的观众可以以高质量和低潜伏期观看对话。