2
Help us understand the problem. What are the problem?

こちらは「ライブ配信/ビデオ通話SDK(Agora)を使用したサービスのアイデアを大募集!【PR】V-CUBE Advent Calendar 2021」 13日目の記事になります。

参加条件の①こんなライブ配信があったら面白いのでは?を考えて居たはずが
いつの間にか②AgoraSDKを触ってみて、感想を投稿下さい!を書き始めていました。

はじめに

先日私が所属する会社でリモート忘年会のコンテンツ募集がありました。
そこから色々あり参加表明する形になったので、
何かしら同時参加型で楽しめるコンテンツを作れないかボンヤリ思っていました。

同時参加型なのでwebRTC1を使った何か、、、
ただ実装期間も数日しかない中、なるべく実装工数が少ない方法を探していたところ
協賛にあった「Agora」というビデオ通話SDKを使えば簡単にできそうだと感じて制作に乗り出しました。

Agoraとは

  • Agora(agora io sdk)は、スマートフォン・PCアプリやWebサイトに、カスタマイズしたビデオ・音声通話やライブ配信をかんたんに実装できるSDK2
    • Android, iOS/Mac, Windows, Web, Electron... など様々なプラットフォームと開発言語に対応3
  • 料金はビデオ通話(HD画質)で$3.99 per/1,000 minutesとなる4
    • トライアルもあり、無料時間として10,000分提供されている5

作ったもの

一言にするなら「表情ビンゴゲーム」です。

挑戦者はwebカメラを通して真ん中のパネルに自身の表情を映します。
そして表情映像をAIが分析して、各表情に対応した8つのパネルが埋まっていきます。
すべて揃えるとタイマーストップして勝者が決まる、そんなシンプルなゲームです。

もちろん他の人がプレイ風景を見れないと面白くないので
画面共有機能用の映像ストリームをAgoraで開くことで、
閲覧者用ページで最大4人の挑戦者映像を見える形にしました。

文字にしても良く分からないと思うので、実際のプレイ風景を元に概要図を書きました↓

モザイクが付いていない人は、
今回快く顔出しOKしてくれた同じチームメンバーの masatomasato1224さんです。ありがとう。

all_conv.png

webカメラの映像が中心に映っており、各パネルにはそれぞれ達成するべき表情が設定されています。
そしてAIが満足するスコアを出した時に、各パネルが埋まるようになっています。

↓ 実際に動いている様子
mm_conv.gif

配信機能を除いたゲームデモを用意したので良かったら遊んでみてください。
ただ何も制御せずにAgoraを利用すると御顔が最大100万人6に配信されてしまう恐れがあるので、
デモゲームでは配信機能を取り除きました。

今回は検証・実装の手間を省くためにchromeブラウザのみ動作保証しております。
GoogleAnalyticsでPV取得する以外、データ取得・通信してないのでご安心ください。

使った技術・環境

表情判定を外部サービスに任せたくなかったのでface-api.jsを用いクライアント処理。

また費用と実装コストを抑えるためにAmplifyを使い、Next.jsで開発しました。

今回主軸の映像配信について何より実装コストを考えAgoraを選択したという形です。

実装解説

今回実装したコードはgithubに上がってますので忘年会等で使ってみてください。

また今回自分が担当したのは画面配信部分と顔認識および描画部分で、
「開始ボタン・タイマー」についてはmasatomasato1224が実装してくれました。
顔出し協力含めありがとう🌝

また.tsなのに型定義されてない部分もありますが、そのあたりは手抜きです。

ここからが解説です。
構成要素を大きく分けると挑戦者画面閲覧者画面の2つになるので順番に説明していきます。

挑戦者画面

配信者画面には9つのパネル(CellComponent)とパネルをまとめたゲームボード(BoardComponent)、
そしてそれらの画面を配信する機能を実装しました。

基本的な機能はほとんど Board.tsx に含めています。

カメラ初期化と顔認識準備

画像認識用モデルをロードした後、webカメラをアクティブにします。
準備ができたら真ん中のパネルにカメラストリームを接続します。

const startCam = async () :Promise<void> => {
  await faceapi.nets.ssdMobilenetv1.loadFromUri("/models");    // モデルロード①
  await faceapi.nets.faceExpressionNet.loadFromUri("/models"); // モデルロード②
  await navigator.mediaDevices
    .getUserMedia({ // Webカメラ取得
      video: true,
      audio: false,
    })
    .then((stream) => {
      faceVideoElm.current.srcObject = stream; //真ん中パネルにstream設定
      faceVideoElm.current.play();  // Webカメラ映像取り込み
    })
    .catch((errorMsg) => {
      console.log(errorMsg);
    });
  join();  // 後述のAgora初期化処理
};

表情判定とスナップショット

表情判定については face-api.jsのドキュメントにあるような基本機能しか使っていないです。

const expressionResult:WithFaceExpressions<WithFaceDetection<{}>> = await faceapi
  .detectSingleFace(faceVideoElm.current) // 顔判定
  .withFaceExpressions(); // ラベル情報と共に取り出す

判定結果が一定しきい値を超えた場合のみRefを通して各パネルに描画していきます。

  const drawCaptureFace = (squareIndex:number) => {
    const captureTmpCanvas = document.createElement("canvas"); // 映像を保持する2次キャンバス
    const captureWidth = faceVideoElm.current.clientWidth;
    const captureHeight = faceVideoElm.current.clientHeight;

    captureTmpCanvas.width = captureWidth;
    captureTmpCanvas.height = captureHeight;
    captureTmpCanvas
      .getContext("2d")
      .drawImage(faceVideoElm.current, 0, 0, captureWidth, captureHeight); // 2次キャンバスに映像描画

    cellRefs.current[squareIndex].current.updateSetImage( // 該当セルのimgSrc更新
      captureTmpCanvas.toDataURL() // 2次キャンバスに映像をdataURLに変換
    );
  };

以上でゲームの基本要素は完成です。

いざ映像配信

アカウント作成 → SDKを用いた実装 → 配信テストの流れで説明していきます。

まずはAgora利用の事前準備

事前に必要なAgoraの利用申請AgoraWebSDKの準備
公式ガイドに超丁寧にあるので、そちらを参考にすれば特に困らないかと思います。

AgoraWebSDKを用いた実装

AgoraWebSDKをロードして初期化します。

import AgoraRTC from 'agora-rtc-sdk-ng'
...
const agoraClient = AgoraRTC.createClient({ mode: "rtc", codec: "vp8" });

あとは公式APIリファレンスを見ながら必要なパラメータを設定していきます。

let localTracks = {
  videoTrack: null
};
const options = {
  appid: process.env.NEXT_PUBLIC_AGORA_APP_ID,          // アプリID。Agoraのダッシュボードに表示されてるもの。
  channel: process.env.NEXT_PUBLIC_AGORA_CHANNEL_NAME,  // チャンネル名。このチャンネルに対して動画配信される。
  uid: null,                                            // NULLでOK
  token: process.env.NEXT_PUBLIC_AGORA_TEMP_TOKEN       // Agoraのダッシュボードから作成するtoken (有効期限24時間)
};

上記トークンについては管理画面から下記手順で作成7します。

赤枠リンクを選択 チャンネル名を書いてGenerateTempToken
pic3.png pic4.png

そして今回の配信方法となる画面共有メソッドを指定して、先程のオプション値を設定。
なんとこれだけ! 超お手軽ですね。

[ options.uid, localTracks.videoTrack ] = await Promise.all([
  agoraClient.join(options.appid, options.channel, options.token || null),
  AgoraRTC.createScreenVideoTrack({}, "disable") // 画面共有取得
]);
localTracks.videoTrack.play("local-player");
await agoraClient.publish(Object.values(localTracks));

今回はゲームボードを共有したかったので
画面共有用メソッドcreateScreenVideoTrackを利用しましたが、他にも色々あるようです。

以上で「顔認識してパネルが埋まるゲーム」をプレイしてる画面を配信する準備ができました。

残りはこの挑戦者画面が並んだ画面を見る閲覧者画面の作成です。

閲覧者画面

動きとしては誰も配信していない時はLoading画面が出ており、
1人でも配信開始すると画面へ順番に動画が出てきて、誰もいなくなるとLoading画面に再度戻る仕様です。

基本的な機能はほとんどViewer.tsxに含めています。

閲覧者画面再掲
mm_conv.gif

映像を受信する

まずは配信と同様にSDK初期化します。

実は送受信コードのベース部分はほとんど公式デモのコードを使っています。資料が豊富。

import AgoraRTC from 'agora-rtc-sdk-ng'
...
const agoraClient = AgoraRTC.createClient({ mode: "rtc", codec: "vp8" });

あとは受信チャンネル名を指定してpublishunpublishイベントを監視するメソッドを作成します。

agoraClient.on("user-published", handleUserPublished);
agoraClient.on("user-unpublished", handleUserUnpublished);
[ options.uid ] = await Promise.all([
  agoraClient.join(options.appid, options.channel, options.token || null)
]);

チャンネルに対してpublishを検知したら該当メディア情報を取得し、特定IDを持つ要素に描画します。

const handleUserPublished = (user, mediaType) => {
  setRemoteUsers((remoteUsers) => [...remoteUsers, user.uid]); // 挑戦者IDリストを管理する内部state. Agoraとは直接関係しない
  subscribe(user, mediaType);
}

const subscribe = async (user, mediaType) => {
  await agoraClient.subscribe(user, mediaType);
  if (mediaType === 'video') {
    user.videoTrack.play(`player-${user.uid}`); // ここで <div id="{`player-${remoteUser}`}" />に対して描画している
  }
}

また配信が終了したら不要Component削除をしたいので下記処理でreactの内部stateを更新しています。

const handleUserUnpublished = (user) => {
  // 挑戦者IDリストを管理する内部state. Agoraとは直接関係しない
  setRemoteUsers((remoteUsers) => remoteUsers.filter(remoteUser => remoteUser !== user.uid))
}

これで閲覧者はページにアクセスして放置しておくだけで、勝手に表情筋デスマッチが開催されます。

デプロイ

今回はなるべくメイン開発ライン以外は楽をしたかったのでAmplifyを使いました。
社内のイベントなのでBASIC認証を手軽に掛けたかったのも理由です。

Amplifyの利用方法については非常に簡単でここで解説しなくとも、
公式チュートリアルAWSでReactアプリケーションの構築を見たほうが早いかと思うので割愛します。
上記リンクの①まで進めれば大体今回の要件を満たせます。

後はAmplifyから払い出されたURLにアクセスするだけでゲーム開始です。

余談: Agoraの管理画面について

管理画面ではトークンの発行以外にも、API使用状況も簡単にわかる様になっているので安心ですね。

pic2.png

上記は今回トライアル用に使ったアカウントの画面ですが
12/7(火)から触り始めて12/9(木)には実装を終え12/10(金)に本番投入してるので
如何に導入しやすいかがわかるかと思います。

ただトライアルが気前よく10,000分5なので実際にはAPI利用状況画面をほぼ見ていないです↓

pic0.png

感想

ブラウザ上で遊べるコンテンツを作ったとして、どうやって複数人で同時に楽しめるかが悩みでしたが
Agoraを利用することでバックエンドを気にせず簡単に作成することができました。

また個人的にはアカウントの作成敷居が低いトライアル使用量が多いドキュメントが豊富という点が助かりました。

触りやすいだけでなく同時閲覧人数もかなり余裕があり6
実際のサービス利用としても十分に選択肢として上がってくる性能という印象でした。

また今回作ったシステムでは共有するコンテンツ自体を変更すれば何でもできるので、
他の4人対戦コンテンツ配信するときにも活用でき、何気に用途は広いという感想を持ちました。

あとは例のごとく顔を利用した検証が大変で、深夜の共同デバッグでは狂気じみたものがありました🌝

楽しかったです。

参考文献・関連資料


  1. 厳密にはAgoraはwebRTC互換 

  2. 公式サイト引用 https://jp.vcube.com/service/agora 

  3. 公式サイト参照 https://jp.vcube.com/service/agora/developer/sdk 2021/12/13時点 

  4. 公式サイト参照 https://www.agora.io/en/pricing/ 2021/12/13時点 

  5. 2021/12/07登録時点 

  6. 最大100万人に届けられる。そう、Agoraならね。 2021/12/14時点 

  7. 今回はテストモードで配信していますが、productionモードで配信する際は随時トークンの生成処理も実装する必要があります。またテストモードではトークン有効期限が24時間なので注意が必要です。 

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Sign upLogin
2
Help us understand the problem. What are the problem?