برنامه نویسی

ایجاد یک چت ویدیویی چند میزبان واقعی در یک مرورگر با آمازون IVS

ما اخیراً از طریق یک منبع مجازی جدید به نام استیج با سرویس ویدیوی تعاملی آمازون (Amazon IVS) پشتیبانی از پخش زنده مشترک را اعلام کردیم. این ویژگی امکانات زیادی را در اختیار شما قرار می دهد که در گذشته پیاده سازی آنها آسان (یا حتی ممکن) نبود. این ویژگی به پخش‌کنندگان زنده این امکان را می‌دهد که مهمان‌ها را به جریان خود دعوت کنند، با دیگر سازندگان محتوا همکاری کنند، یا حتی یک نمایش به سبک «تماسی» با شرکت‌کنندگان فقط صدا ایجاد کنند. در این پست، نحوه شروع ایجاد یک برنامه وب با میزبان های متعدد با Web Broadcast SDK را بررسی خواهیم کرد. مثل همیشه، اسناد یک نقطه شروع عالی هستند، اما همیشه کمک می کند تا از طریق یک برنامه آزمایشی عبور کنید، بنابراین این چیزی است که در این پست روی آن تمرکز خواهیم کرد.

برای این نسخه ی نمایشی، ما یک برنامه تحت وب می سازیم که به چندین شرکت کننده اجازه می دهد تا یکدیگر را مشاهده کرده و با یکدیگر صحبت کنند. در یک پست آینده، ما توانایی پخش مکالمه را به یک کانال IVS آمازون اضافه خواهیم کرد که در آن دیگران می توانند مکالمه را مشاهده کنند.

در سطح بالا، در اینجا مراحلی وجود دارد که ما برای ایجاد برنامه مجازی “مرحله” برای همکاری بلادرنگ بین چندین میزبان انجام خواهیم داد.

  • ایجاد یک stage از طریق SDK
  • برای شرکت کنندگان در مرحله، نشانه(های) صادر کنید
  • اتصال به stage روی مشتری
  • هنگام پیوستن به مرحله، صدا و تصویر شرکت‌کننده را رندر کنید

سمت سرور

ما از جدید استفاده خواهیم کرد @aws-sdk/client-ivs-realtime ماژول (اسناد) AWS SDK برای جاوا اسکریپت (v3) برای ایجاد منبع مرحله و توکن های شرکت کننده. در اینجا یک مثال از یک برنامه بدون سرور است که می توانید برای انجام این مراحل از آن استفاده کنید، اما برای نسخه ی نمایشی خود تصمیم گرفتم کمی کارها را ساده کنم و یک برنامه Express ایجاد کردم که می تواند برای میزبانی API و front-end من مورد استفاده قرار گیرد.

ایجاد یک منبع Stage از طریق AWS SDK برای جاوا اسکریپت (v3)

در برنامه Express خود، یک سرویس به نام ایجاد کردم IvsRealtimeService. این سرویس کش موجود است stages و stageParticipants در حافظه برای جلوگیری از نیاز به پایگاه داده برای حفظ آنها.

در مرحله تولید، می‌خواهید این مقادیر را در یک ذخیره‌گاه داده دائمی ذخیره کنید تا از نوسانات و محدودیت‌های ذخیره مقادیر در حافظه جلوگیری کنید (دوباره به نسخه آزمایشی بدون سرور مراجعه کنید تا در مورد استفاده از Amazon DynamoDB برای تداوم مراحل و نشانه‌ها بیشتر بدانید).

برنامه من یک مسیر به نام دارد /ivs-stage که می توان از آن استفاده کرد POST یک مرحله 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() روش a CreateStageCommand از طریق ivsRealtimeClient.

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);
});
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

توجه داشته باشید که این نقطه پایانی نیاز به a username، stageName، و الف userId و فراخوانی می کند 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 که انتظار دارد یک شی ورودی حاوی userId، stageArn، مدت زمان نشانه (پیش فرض: 60 دقیقه) و یک attributes شی ای که می تواند برای ذخیره مقادیر دلخواه خاص برنامه مورد استفاده قرار گیرد (در مورد من، the username). را attributes بعداً زمانی که صفحه جلویی خود را ایجاد کنیم در دسترس خواهد بود، بنابراین راه خوبی برای گنجاندن اطلاعات خاص شرکت‌کننده است. اما همانطور که اسناد ما می گویند:

این فیلد در معرض همه شرکت کنندگان مرحله است و نباید برای شناسایی شخصی، محرمانه یا اطلاعات حساس استفاده شود.

اکنون که چند نقطه پایانی برای کمک به ایجاد منابع مرحله و نشانه‌های شرکت‌کننده ایجاد کرده‌ایم، بیایید به ایجاد برنامه وب نگاهی بیندازیم.

ساخت اپلیکیشن وب

قسمت جلویی یک برنامه جاوا اسکریپت و HTML ساده و وانیلی برای ساده نگه داشتن کارها و تمرکز بر یادگیری SDK پخش وب خواهد بود. برای این دمو، می‌توانیم مسیری اضافه کنیم که یک فایل HTML را برمی‌گرداند. در آن فایل، Web Broadcast SDK (نسخه 1.3.1).

شامل و نشانه گذاری

<script src="https://web-broadcast.live-video.net/1.3.1/amazon-ivs-web-broadcast.js"></script>
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

از آنجایی که تعداد شرکت کنندگان در این مرحله مجازی پویا است (تا 12)، ایجاد یک منطقی است <template> که حاوی الف است <video> برچسب و هر دکمه، برچسب یا نشانه گذاری دیگری که نیاز داریم. در اینجا این است که چگونه ممکن است به نظر برسد.

<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> که برای رندر کردن شرکت کنندگان در هنگام پیوستن به مرحله مجازی استفاده می شود.

<div id="participants"></div>
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

جاوا اسکریپت

اکنون که وابستگی Web Broadcast SDK و نشانه‌گذاری را آماده کرده‌ایم، می‌توانیم به جاوا اسکریپت مورد نیاز برای پیوستن به یک “مرحله” مجازی نگاه کنیم و شرکت‌کنندگان را رندر کنیم. این شامل چندین مرحله است، اما می‌توانیم آن‌ها را به توابع قابل مدیریت تقسیم کنیم تا همه چیز ساده باشد. وقتی 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();
});
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

دستگاه ها و مجوزها

4 روش اول فراخوانی (handlePermissions()، getDevices()، createVideoStream()، createAudioStream()) اگر در گذشته با Amazon IVS Web Broadcast SDK کار کرده اید، باید آشنا به نظر برسد. ما به سرعت به هر تابع در زیر نگاه خواهیم کرد، اما همیشه می توانید برای اطلاعات بیشتر به اسناد مراجعه کنید.

اولین، 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> تا کاربر بتواند دستگاه پخش مورد نظر خود را انتخاب کند.

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;
};
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

اکنون که دستگاه‌های لیست شده را داریم، می‌توانیم یک جریان ویدیویی و صوتی از آنها ایجاد کنیم. برای ویدیوی چند میزبان، باید مطمئن شویم که محدودیت‌های پیشنهادی برای نرخ فریم و اندازه ویدیو را در نظر داشته باشیم.

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 متغیر یکی از ویژگی های است stage شیء برگشتی از getStageConfig() بلافاصله در بالا، و 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 شی شامل سه تابع است که حالت مورد نظر مرحله را بیان می کند (برای همه احتمالات مختلف برای استراتژی به اسناد مراجعه کنید). در داخل 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 عناصری که باید منتشر شوند در این نسخه ی نمایشی، ما از هر دو استفاده خواهیم کرد videoStream و audioStream که در بالا برای تولید اینها ایجاد کردیم.

بعد، در داخل initStage() روش، ما یک نمونه از را ایجاد می کنیم Stage کلاس، رمز شرکت کننده و استراتژی را به آن منتقل می کند.

stage = new Stage(stageParticipantToken.token, strategy);
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

اکنون که ما یک Stage به عنوان مثال، ما می توانیم شنوندگان را به رویدادهای مختلفی که روی صحنه پخش می شود متصل کنیم. برای همه رویدادهای احتمالی به اسناد مراجعه کنید. در این نسخه نمایشی، به زمانی که شرکت‌کنندگان اضافه یا حذف می‌شوند گوش می‌دهیم.

وقتی شرکت‌کنندگان اضافه شدند، شرکت‌کننده را رندر می‌کنیم.

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
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

ترک صحنه

این کاملاً ضروری نیست، اما می‌توانیم با ترک صریح مرحله‌ای که شرکت‌کننده از برنامه خارج می‌شود، تجربه کاربر را بهبود ببخشیم. این اطمینان حاصل می کند که رابط کاربری باقیمانده شرکت کنندگان زودتر به روز می شود. برای این کار می توانیم از a استفاده کنیم beforeunload کنترل کننده برای فراخوانی stage.leave() برای اطمینان از قطعی تمیز.

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

document.addEventListener("beforeunload", cleanUp);
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

اکنون برنامه ما آماده آزمایش است. اجرای برنامه به ما یک تجربه چت ویدیویی بی‌درنگ بین حداکثر 12 شرکت‌کننده می‌دهد.

چت تصویری چند میزبان

خلاصه

در این پست، ما یاد گرفتیم که چگونه یک تجربه چت ویدیویی بلادرنگ برای حداکثر 12 شرکت کننده ایجاد کنیم. در پست بعدی ما یاد خواهیم گرفت که چگونه گام بعدی را برداریم و یک چت بلادرنگ را در یک کانال IVS آمازون پخش کنیم تا بینندگان نهایی بتوانند مکالمه را با کیفیت بالا و تاخیر کم تماشا کنند.

نوشته های مشابه

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *

دکمه بازگشت به بالا