LoginSignup
14
8

More than 3 years have passed since last update.

Amazon Kinesis Video Streams WebRTC を使った LINE 風ビデオ通話 Web アプリを簡単に作れる AWS Amplify CLI Plugin を作った(あるいは作ろうとした)話

Last updated at Posted at 2019-12-20

はじめに

なろう小説ばりに長いタイトルですみません。この記事は AWS Amplify Advent Calendar 2019 の20日目の記事です。
タイトルそのままですが、今月発表された Amazon Kinesis Video Streams の WebRTC 対応を利用して、LINE 風のビデオ通話 Web アプリをより簡単に作るための AWS Amplify Plugin を作りました。

この記事では、このプラグインをどうやって作ったか、どうやって使うかを解説します。最後には Amplify とプラグインと Vue.js と Vuetify を使って超省エネで(所要時間20分ほどで)実際にアプリを作成したいと思います。(ただ残念ながら iPhone での挙動が非常に不安定 & マイクの挙動も不安定で怪しい感じです。)
前振りやプラグインの仕組みなんかどうでもいいんだ、とにかく LINE 風ビデオ通話 Web アプリが作りたいんだ、という方は後半だけ読んでもらえれば OK です。

Amazon Kinesis Video Streams の WebRTC 対応

今月の上旬に AWS より、Kinesis Video Streams という動画のストリーミングサービスが WebRTC に対応したことが発表されました。ここでは多くを語りませんがこの対応により、かなり低遅延(ほぼリアルタイム)の双方向のメディア通信が可能となりました。
詳細についてはすでに Qiita にかなりしっかりとした解説記事が投稿されているので、こちらを参照下さい。

また、そもそも WebRTC とは何か?という点については、以下のサイトを参照下さい。私もいつもお世話になっております。

さて、私は1-2年ほど前に AWS 上で動画のストリーミング配信の仕組みを構築する必要に迫られました。その際は Elemental Media 系のサービスを駆使して仕組みを作ったのですが、タイムラグがかなり大きく結局構築は頓挫してしまいました。その際色々調べたところ、どうやら WebRTC という仕組みを使えばリアルタイムに動画配信ができるらしいこと、そして残念ながらその仕組みは素人が手を出すにはハードルが高いことがわかりました。そうして、WebRTC のことなどすっかり忘れて日々を暮らしていたのですが、上述の通り AWS が WebRTC に対応したというニュースが入ってきました。
AWS が対応したということは面倒な部分、素人には難しい部分をうまく AWS 側で巻き取ってくれているはず。実に2年越しで WebRTC に挑戦することにしました。

LINE 風ビデオ通話 Web アプリを Amplify で

Amazon Kinesis Video Streams WebRTC を試してみるにあたり、公式からサンプルが提供されていますが、(実際にやってみた方はお分かりになると思いますが、)少し物足りないです。
リアルタイムかつ双方向の通信と言うのであれば、やはりビデオ通話ぐらいはやってみたい。そこで、ようやく主役である AWS Amplify の登場です。Amplify のユースケースとして、リアルタイムのチャットアプリがよく紹介されていますので、これをさらに一歩踏み込んで ビデオ通話アプリを作ってみたいと思います。

…というのが、当初のこの記事のテーマになる予定だったのですが、実際に作ってみると元々の Amplify の手軽さに加え、Kinesis Video Streams WebRTC の対応も難しいところがほぼ無く、あっさり終わってしまいました。
「これでは記事にならん。そう言えば、先日の re:Invent で同僚が Amplify のプラグインを作る Builder's Session に出たと言っていたな。そもそもプラグインなんか作れるんだ。ちょっとやってみるか。」

…というワケでようやく本題に入ります。

Kinesis Video Streams WebRTC のための Amplify CLI Plugin

Amplify CLI を使えば Amplify api add で AppSync か API Gateway + Lambda で API を追加したり、Amplify storage add で S3 か DynamoDB にデータストレージを作ることができます。リソースを作るだけでなく、クレデンシャルや ARN などの面倒な設定も、CLI が作ってくれる設定ファイルから、読み込むだけで非常に簡単になります。
Amazon Kinesis Video Streams WebRTC のサンプルをご覧になった方はわかると思いますが、作らなければいけない AWS 上のリソースは Kinesis Video Streams の Signaling Channel だけで楽ですが、その後の呼び出し時に色々とソースコードでおまじないを書かなくてはいけません。プラグインを使うことで、この複雑なソースコードを隠ぺいできるようになると便利です。

そこで今回作成するプラグインでは、以下の実現を目指しました。
1. Amplify CLI の操作で Kinesis Video Streams の Signaling Channel を作成する
2. 同じく Amplify CLI から作成する Cognito と連携して、ログインユーザのみリソースを利用できるようにする
3. Signaling Channel 利用のためのソースコードを可能な限り隠ぺいする

Amplify CLI Plugin 入門

Amplify CLI のプラグインについてですが、あまり世に出回っておらずなかなか参考になるものがありません。私は以下3つのうち、当初は最後の video プラグインを参考に自分のものを作り始めました。

https://github.com/wizage/amplify-category-template
https://github.com/wizage/amplify-category-example
https://github.com/awslabs/amplify-video

video プラグインは他の2つに比べて、かなりハードに作りこまれており非常に勉強にはなりましたが、自分がやりたい内容に比べると完全に行き過ぎでした。私は結局公式サイトの説明をしっかり読んだ後に1から作り直すハメなったので、初心者の方はまずサイトを読むことから始めることをお勧めします。ちなみに私はサイトの読み込みが甘く、そもそもプラグインについての記載があることも全然気づきませんでした。公式のプラグインについての解説は以下です。上が「プラグインとは何か」「どうやって使うか」の解説、下がプラグインのアーキテクチャについての解説です。自分で作るためには下を(おぼろげにでも)理解することが非常に重要でした。

https://aws-amplify.github.io/docs/cli-toolchain/plugins?sdk=js
https://aws-amplify.github.io/docs/cli-toolchain/usage#architecture

Amplify Plugin には Category, Provider, Frontend, Util の4つのタイプが存在します。標準の Amplify CLI をインストールしただけで、amplify storage add, amplify auth add などのコマンドが利用できるようになりますが、これはそれぞれamplify-category-storage, amplify-category-authといったプラグインが同時にインストールされるからです。それ以外にも、amplify-frontend-javascript, amplify-provider-awscloudformation といったプラグインが標準でインストールされており、通常 Amplify CLI を使う際に特にプラグインだと意識せずに使っていると思います。

今回のプラグインでは、標準のamplify storage addと同じようにamplify webrtc addとコマンドを叩けば AWS 上にリソースができるようにしたいので、当初素直に考えて Category タイプのプラグインを作ろうとしました。先にあげた参考元もすべて Category タイプだったので、当初はそれ以外の選択肢がそもそもありませんでした。が、最初によくよく確認せずに Category タイプにしたことが後のデカい手戻りの要因になりました。
image.png
上記は公式の Amplify Plugin アーキテクチャの説明(上記リンクの下)より引用した図です。この図の通り、Category Plugins はあくまで Project Metadata を作成するだけの役割であり、AWS 上にリソースを作るには、Provider Plugins を利用する必要があります。ここでネックになるのが Provider Plugins には amplify-provider-awscloudformation しか存在しないのに、Kinesis Video Streams は現在 Cloud Formation に対応していないということです。

Kinesis Video Streams が Cloud Formation に対応していないことには早い段階で気付いており、まぁ代わりに AWS SDK でコマンド叩けば何とかなるだろうとタカを括っていたのですが、これが甘かったです。
Category タイプのプラグインを作ると、Provider との連携を強制されるようです。前述の Video プラグインを参考に色々といじったのですが、どうしてもamplify pushもしくはamplify webrtc pushしたあと Provider 連携時にエラーになる事象が回避できませんでした。

四苦八苦した挙句、Category タイプで作ることは諦め、Util タイプで作り直すことにしました。Util タイプはその名の通り便利系のプラグインであり、Amplify CLI のコア部分との連携が少なくて気軽に作成することができます。皆さんもプラグイン作成に挑戦する際は、まず Util タイプから入ることをお勧めします。
なお、以下で引用している公式ドキュメントによると、本来 Util タイプはクラウド上のリソースは管理しないことになっているようです。今回私が作ったプラグインはこの公式のコンセプトを完全に無視してしまっていますので一応留意ください。

a util purpose plugin does not manage any backend resources in the cloud, but provides certain CLI commands and/or certain functionalities for the CLI core, and other plugins.

amplify-util-webrtc 解説

このプラグインをインストールすると、amplify webrtc add, amplify webrtc removeというコマンドが実行できるようになります。add の方を叩くと標準の他の add の処理と同様に対話形式で、リソースの名前、設定を決定し、プロジェクトのルートディレクトリ配下の/amplify/backendwebrtcというディレクトリを作成しonLocal.jsonという設定ファイルを作成します。remove を叩くとこの設定ファイルを消します。add も remove も ローカルのファイルを操作するだけで AWS 上のリソース作成・削除は行いません。

リソースの操作はイベントハンドラーで行います。Amplify Plugin は Amplify 本体のamplify init, amplify pushといったイベントをキャッチして、その前後に処理を追加することができます。今回はamplify pushのイベントをキャッチして、標準のamplify push処理が終わったあとに、amplify webrtc addで作成した設定ファイルをもとに AWS SDK 経由で AWS 上のリソースの作成もしくは削除を行います。

今回作成したプラグインのファイル構成はこのようになっています。

amplify-util-webrtc
├─commands
| ├─add.js
│ └─remove.js
├─event-handlers
| ├─handle-PostPush.js
│ └─handle-PrePush.js
├─questions
│ └─questions.json
├─scripts
│ └─post-install.js
├─utils
│ └─aws-webrtc-exports.js.ejs
├─amplify-plugin.json
├─index.js
(後略-package.jsonなど)

amplify-plugin.json

このプラグインの設定ファイルです。名称、タイプ、実行可能なコマンド、ハンドリングするイベントを記述します。プラグインの詳細を確認するためのコマンドamplify plugin listで表示されるのもこのファイルの内容です。

amplify-plugin.json
{
    "name": "webrtc",
    "type": "util",
    "commands": [
        "add",
        "remove"
    ],
    "eventHandlers": [
        "PrePush",
        "PostPush"
    ]
}

index.js

プラグインの親ファイルです。特に難しいことはやっていません。amplify webrtc xxxが実行されると、executeAmplifyCommandが、amplify コアの方で何かのイベントが起きるとhandleAmplifyEventが実行されるようです。handleAmplifyEventでどのイベントが取り扱えるか正確にはわかっていませんが、試してみたところamplify push, amplify initは検知可能、amplify statusは検知不可のようです。

index.js
const path = require('path');

async function executeAmplifyCommand(context) {
  try {
    const commandsDirPath = path.normalize(path.join(__dirname, 'commands'));
    const commandPath = path.join(commandsDirPath, context.input.command);
    const commandModule = require(commandPath);
    await commandModule.run(context);
  } catch(err) {
    console.error(err)
  }
}

async function handleAmplifyEvent(context, args) {
  try {
    const eventHandlersDirPath = path.normalize(path.join(__dirname, 'event-handlers'));
    const eventHandlerPath = path.join(eventHandlersDirPath, `handle-${args.event}`);
    const eventHandlerModule = require(eventHandlerPath);
    await eventHandlerModule.run(context, args)
  } catch(err) {
    console.error(err)
  }
}

module.exports = {
  executeAmplifyCommand,
  handleAmplifyEvent,
};

commands

amplify webrtc xxx が実行されたときに実際に動く処理の部分です。今回はamplify-plugin.jsonaddremoveを定義したので、commands 配下にそれぞれadd.jsremove.jsを配置します。

add.jsでは前述の通り、対話形式で作成する Signaling Channel の設定値を決定します。といっても、Signaling Channel に設定できるのはチャンネル名と TTL の2つだけなので、質問も2つだけです。質問の内容はquestions/questions.jsonから読み込みます。

特筆すべきは処理冒頭にまず Auth カテゴリの存在有無を確認し、なければ先に Auth の作成フローに入る処理です。このプラグインで作成する Signaling Channel には Cognito の認証済みユーザが必須になるためこの処理を入れています。amplify storage addでも同様の処理があるので、amplify-category-storageのソースコードを参考に処理を追加しました。処理の途中でamplify-category-authをインポートして、addを実行する部分が確認できると思います。これだけで簡単に Auth のフローに入れるので便利です。

commands/add.js

const inquirer = require('inquirer');
const fs = require('fs');
const chalk = require('chalk');
const AWS = require('aws-sdk')
const questions = require('../questions/questions.json');

let projectMeta;

module.exports = {
  name: 'add',
  run: async (context) => {
    projectMeta = context.amplify.getProjectMeta();
    const backendDir = context.amplify.pathManager.getBackendDirPath();
    const paramDir = `${backendDir}/webrtc`;
    const localParamJson = `${paramDir}/onLocal.json`

    if (fs.existsSync(localParamJson)){
      console.log(chalk.yellow('WebRTC already exists. If you want to change, remove and add again.'));
      return
    }

    // Auth が追加されていなければまず Auth を作成する
    while (!checkIfAuthExists(context)) {
      if (
        await context.amplify.confirmPrompt.run(
          'You need to add auth (Amazon Cognito) to your project in order to add WebRTC. Do you want to add auth now?'
        )
      ) {
        try {
          const { add } = require('amplify-category-auth');
          await add(context);
        } catch (e) {
          context.print.error('The Auth plugin is not installed in the CLI. You need to install it to use this feature');
          break;
        }
        break;
      } else {
        process.exit(0);
      }
    }
    const result = await serviceQuestions(context);

    if (!fs.existsSync(paramDir)) {
      fs.mkdirSync(paramDir)
    } 

    try {
      fs.writeFileSync(localParamJson, JSON.stringify(result, null, 4));
      console.log(chalk.green('Successfully added resource.'));
    } catch(err) {
      console.log(chalk.red(err));
    }
  },
};
// (後略)

remove.jsの方はadd.jsで作成した設定ファイルを削除するだけです。コードも実行確認をプロンプトに出すだけの簡単なものです。

remove.js

const fs = require('fs');
const chalk = require('chalk');

module.exports = {
  name: 'remove',
  run: async (context) => {
    projectMeta = context.amplify.getProjectMeta();
    const backendDir = context.amplify.pathManager.getBackendDirPath();
    const paramDir = `${backendDir}/webrtc/`;
    const localParamJson = `${paramDir}/onLocal.json`

    if (!fs.existsSync(localParamJson)){
      console.log(chalk.yellow('WebRTC does not exist.'));
      return
    }

    const answer = await context.amplify.confirmPrompt.run('Are you sure you want to delete the resource? This action deletes all files related to this resource from the backend directory.')
    if (answer) {
      try {
        fs.unlinkSync(localParamJson)
        console.log(chalk.green('Successfully removed resource.'));
      } catch(err) {
        console.log(chalk.red(err));
      }
    }
  }
};

event-handlers

Amplify CLI のイベントをトリガに動くコードです。このプラグインでは、amplify pushの前後に処理を行います。
amplify push後に動くhandler-PostPush.jsでは、amplify webrtc addで作成したonLocal.jsonの内容をもとに実際に AWS 上にリソースを作成します。具体的には Kinesis Video Streams の Signaling Channel、その Channel を利用するために必要な IAM ポリシーを作成し、IAM ポリシーを Cognito ユーザプールの authed roll にアタッチします。
リソース作成後に2つのファイルを出力します。1つは onLocal.json とセットになる onCloud.json で、ここには作成したリソースの ARN などを書き込みます。次にまたamplify pushが実行された際に、onLocal.json と onCloud.json を比較し、内容に相違があったら AWS 上のリソースを作り直す、onLocal が無く onCloud だけある状態であれば、AWS 上のリソースを削除するなどの動きをします。
もう1つのファイルはaws-webrtc-exports.json で、これは標準の CLI が作成するaws-exports.jsonに相当するものです。この json をフロントエンド側でインポートすることにより、開発者がいちいちリソースの ARN などを気にする必要が無くなります。

ソースコードは長いので、リソース作成する部分だけ引用します。

event-handlers/handle-PostPush.js
async function createWebRTC(context, localParam, cloudParamJson){
  const awsConfig = getAWSConfig(context)
  const kinesisvideo = new AWS.KinesisVideo(awsConfig);
  const iam = new AWS.IAM(awsConfig)

  try {
    const params = {
      ChannelName: localParam.channelName,
      ChannelType: 'SINGLE_MASTER',
      SingleMasterConfiguration: {
        MessageTtlSeconds: localParam.ttl
      }
    }
    const kinesisRes = await kinesisvideo.createSignalingChannel(params).promise()

    const policyJson = {
      "Version": "2012-10-17",
      "Statement": [
        {
          "Effect": "Allow",
          "Action": [
            "kinesisvideo:GetSignalingChannelEndpoint",
            "kinesisvideo:ConnectAsMaster",
            "kinesisvideo:GetIceServerConfig",
            "kinesisvideo:ConnectAsViewer"
          ],
          "Resource": kinesisRes.ChannelARN
        }
      ]
    }
    const createPolicyParams = {
      PolicyDocument: JSON.stringify(policyJson),
      PolicyName: localParam.channelName + '-policy'
    }
    const createRes = await iam.createPolicy(createPolicyParams).promise()

    const attachPolicyParams = {
      PolicyArn: createRes.Policy.Arn,
      RoleName: projectMeta.providers.awscloudformation.AuthRoleName
    }
    await iam.attachRolePolicy(attachPolicyParams).promise()

    const cloudParam = {
      channelName: localParam.channelName,
      ttl: localParam.ttl,
      channelArn: kinesisRes.ChannelARN,
      policyName: localParam.channelName + '-policy',
      policyArn: createRes.Policy.Arn
    }
    fs.writeFileSync(cloudParamJson, JSON.stringify(cloudParam, null, 4));
    await generateAWSExportsWebRTC(context, cloudParam.channelArn)

  } catch(err) {
    console.error(err)
  }
}

handle-PrePush.jsの方は前述のonLocal.json, onCloud.jsonの有無を確認し、このまま Push 処理を続けると、WebRTC 関連リソースがどうなるかの情報を出力する処理です。簡易な内容なので解説は省略します。

utils/aws-webrtc-exports.js.ejs

handle-PostPush.jsで作成するaws-webrtc-exports.jsのひな型です。実はこのプラグインの中で一番ゴリゴリコードが書かれているのがこのファイルです。aws-xxx-exports.jsは AWS リソースの設定値などが入るだけのファイルなので本来はコードが書かれることはないと思います。
このプラグインでは目的の一つである「Signaling Channel 利用のためのソースコードを可能な限り隠ぺいする」ために、Signaling Channel 利用時の面倒な処理を強引に全部このひな形に押し込んでいます。本来はフロントエンド用に別のライブラリなりを用意するべきなんでしょうが、ちょっと面倒だったのでこの手法を取ることにしました。

以上でプラグインの解説は終わりです。ここからはこのプラグインを使って実際にビデオ通話アプリを作っていきたいと思います。

プラグインを使って実際にアプリを作る

今回作成したプラグインを使って、LINE 風ビデオ通話 Web アプリを作ります。Amplify CLI + Amplify WebRTC Util で Cognito ユーザプール、Kinesis Video Streams の Signaling Channel、アクセスに必要な IAM ポリシーを作成し、Vue + Vuetify でフロント画面を作成し、Amplify Console + Code Commit で静的サイトホスティングと配信を行います。
Web でカメラを使うために必要になるgetUserMediaは、HTTPS が必須になりますが、これも簡単に実現できます。そう、Amplify ならね!

なお、amplify hostting add でホスティングすることも可能ですが、私個人としてはホスティングに関しては、Amplify CLI より Amplify Console の方が簡単に感じるので Console の方を利用します。

準備

(npm と git はすでにインストールされている前提です。入っていない方は別途インストールしてください。)

Amplify CLI のインストール
npm install -g @aws-amplify/cli
Amplify WebRTC Util Plugin のインストール

今回の主題である プラグインを GitHub から適当な場所にクローンしてマニュアルインストールします。

$ git clone https://github.com/rtaguchi/amplify-util-webrtc.git
$ cd amplify-util-webrtc
$ npm install -g
Vue CLI のインストール
$ npm install -g @vue/cli
プロジェクトの作成

今回はプロジェクト名をmobile-video-chatとしました。対話形式で何か質問されますが、そのまま変更無しで default のまま続行します。

vue create mobile-video-chat`
Vuetify のインストール

プロジェクトのルートディレクトリに移動して、Vuetify をインストールします。プリセットについて質問されますが、default のままで OK です。

cd mobile-video-chat
vue add vuetify
Amplify 初期化

プロジェクトのルートディレクトリでamplify initを実行しプロジェクトを初期化します。色々聞かれますが、基本はそのまま Enter で大丈夫です。

$ amplify init
Scanning for plugins...
Plugin scan successful
Note: It is recommended to run this command from the root of your app directory
? Enter a name for the project mobile-video-chat
? Enter a name for the environment dev
? Choose your default editor: Visual Studio Code
? Choose the type of app that you're building javascript
Please tell us about your project
? What javascript framework are you using vue
? Source Directory Path:  src
? Distribution Directory Path: dist
? Build Command:  npm run-script build
? Start Command: npm run-script serve
Using default provider  awscloudformation

For more information on AWS Profiles, see:
https://docs.aws.amazon.com/cli/latest/userguide/cli-multiple-profiles.html
Code Commit の設定

Code Commit でリポジトリを作成し、リモートリポジトリの設定をします。(リポジトリの URL は Code Commit のリポジトリの画面で右上の「URL のクローン」からコピーできます。)
設定ができたら一度コミットしてリポジトリに Push しておきます。

git remote add origin <リポジトリのURL>
git add .
git commit -m "first commit."
git push origin master
Amplify Console の設定

AWS コンソール方から AWS Amplify にアクセスし、Amplify Console の設定を行います。アプリのリポジトリに先ほど作成した Code Commit のリポジトリを選択します。ビルドの設定などはデフォルトのままで大丈夫です。
細かいコンソール上の操作については以下の記事を参考にして下さい。
https://qiita.com/nakayama_cw/items/6d3514b51c5fbf9ba450#%E3%83%87%E3%83%97%E3%83%AD%E3%82%A4

途中経過

ここまでの作業でhttps://<ブランチ名>.<アプリID>.amplifyapp.com/に以下の表示がされると思います。現在はまだ Vuetify のデフォルト画面があるだけです。バックエンドにもリソースはありません。

image.png

ここから LINE 風ビデオ通話 Web アプリを作成していきます。

実装

バックエンドリソースの作成

プロジェクトのルートディレクトリでamplify webrtc addを実行します。
まず Auth カテゴリのリソースを作るように言われるので、Yes を選択し Cognito, Kinesis Video Streams の順でリソースを作成していきます。途中の質問は特にこだわりが無ければ全てデフォルトのまま Enter で問題ありません。

$ amplify webrtc add
? You need to add auth (Amazon Cognito) to your project in order to add WebRTC. D
o you want to add auth now? Yes
Using service: Cognito, provided by: awscloudformation

 The current configured provider is Amazon Cognito. 

 Do you want to use the default authentication and security configuration? Defaul
t configuration
 Warning: you will not be able to edit these selections. 
 How do you want users to be able to sign in? Username
 Do you want to configure advanced settings? No, I am done.
Successfully added auth resource
? Provide a friendly name for your signaling channel: amplify-mobile-video-chat-d
ev-71008-SC
? Specify TTL value of an existing signaling channnel:  60
Successfully added resource.

add が完了したら、そのままamplify push でバックエンドリソースを AWS 上に作成します。Cognito を作成するのに多少時間がかかります。

$ amplify push

This action creates WebRTC settings on the cloud.
✔ Successfully pulled backend environment dev from the cloud.

Current Environment: dev

| Category | Resource name           | Operation | Provider plugin   |
| -------- | ----------------------- | --------- | ----------------- |
| Auth     | mobilevideochate1eb608b | Create    | awscloudformation |
? Are you sure you want to continue? Yes

(中略:Cloud Formation の実行ログ)

Creating WebRTC settings on the cloud.
Successfully created WebRTC settings on the cloud.

これでバックエンドリソースの準備が整いました。

フロントエンドの実装

まずフロントに必要なライブラリをインストールします。

npm install aws-sdk aws-amplify aws-amplify-vue amazon-kinesis-video-streams-webrtc

プロジェクトのフォルダ構成は以下となっていると思います。この中からsrcフォルダの配下のplugins/vuetify.js, App.vue, main.jsを変更していきます。単純に WebRTC を試す目的なら以降の記述は全てデザインの話なので、全部コピペで大丈夫です。
なお、assets, components は利用しないので中のファイルを消しても問題ありません。

mobile-video-chat
├─amplify
├─node-modules
├─public
├─src
| ├─assets
| ├─components
| ├─plugins
| │ └─vuetify.js
| ├─App.vue
| ├─aws-exports.js
| ├─aws-webrtc-exports.js
│ └─main.js
(後略)

main.jsでは追加したライブラリの読み込み処理を追加します。

main.js
import Vue from 'vue'
import App from './App.vue'
import vuetify from './plugins/vuetify';
import Amplify, * as AmplifyModules from 'aws-amplify'
import aws_exports from './aws-exports'
import { AmplifyPlugin } from 'aws-amplify-vue'

Amplify.configure(aws_exports)
Vue.use(AmplifyPlugin, AmplifyModules)
Vue.config.productionTip = false

new Vue({
  vuetify,
  render: h => h(App)
}).$mount('#app')

plugins/vuetify.jsではテーマを dark にします。(何かそっちの方がカッコいいので。)

plugins/vuetify.js
import Vue from 'vue';
import Vuetify from 'vuetify/lib';

Vue.use(Vuetify);

export default new Vuetify({
    theme:{
        dark: true
    }
});

App.vueがメインで変更する部分になります。今回 WebRTC を試すのが目的ですので、デザインや UI の機能は最低限にします。そのため、components なども分けずに App.vue にべた書きします。

ごく単純な1画面構成のアプリなのでテンプレート部には特筆するところはありません。ログイン用の画面パーツには、amplify-vue のコンポネントを使っています。あとは 自分側の映像を出力する video タグと相手側の映像を出力する video タグが一つずつ。画面下部に Mater か Viewer を選択するためのボタンというデザインです。

App.vue(template)
<template>
  <v-app>
    <v-snackbar
      top
      v-model="snack.display"
      :color="snack.color"
      :timeout="1000"
      class="snack"
    >      
      {{ snack.text }}
    </v-snackbar>

    <v-progress-linear
      v-if="progress"
      indeterminate
      color="info"
      height="15"
      rounded
    ></v-progress-linear>

    <v-fade-transition>
      <amplify-authenticator class="auth" :authConfig="authConfig" ></amplify-authenticator>
    </v-fade-transition>

    <video
      id="local"
      :srcObject.prop="localStream"
      autoplay muted playsinline
      @click="playVideo(local)"
    />
    <video
      id="remote"      
      :srcObject.prop="remoteStream"
      autoplay playsinline
      @click="playVideo(remote)"
    />

    <v-scroll-y-reverse-transition>
      <v-bottom-navigation
        :value="activeBtn"
        fixed
        horizontal
        color="orange"
      >
        <v-row align="center" justify="center">
          <v-btn @click="selectRole('Master')">
            <span>Master</span>
            <v-icon large>mdi-account</v-icon>
          </v-btn>

          <v-btn @click="selectRole('Viewer')">
            <span>Viewer</span>
            <v-icon large>mdi-account-group-outline</v-icon>
          </v-btn>

          <v-btn @click="stopAndSignOut()">
            <v-icon>mdi-stop-circle</v-icon>
          </v-btn>
        </v-row>
      </v-bottom-navigation>
    </v-scroll-y-reverse-transition>

  </v-app>
</template>

スクリプト部分では、ほとんど Kinesis Video Streams 絡みの処理を書く必要がありません。それらの処理は、./aws-webrtc-exports.jsに記述されているため、そこから関数を呼び出すのみです。記述しなくてはいけないのは、外だしされたロジックから受け取ったストリーム情報の出力の処理と、Signaling Channel との情報送受信などのイベント発生時の処理です。

App.vue(script)
<script>
import { connectSignalingChannel, generateSignalingClientMaster, generateSignalingClientViewer } from './aws-webrtc-exports.js';
import { AmplifyEventBus, components } from 'aws-amplify-vue'

export default {
  name: 'App',

  components: {
    ...components
  },

  data: () => ({
    authConfig: {
      signInConfig: {
        isSignUpDisplayed : false,
      },
    },
    progress: false,
    snack: {
      display: false,
      color: 'info',
      text: null,
    },
    activeBtn: 3,
    isMaster: false,

    signalingClient: null,
    localStream: null,
    remoteStream: null,
    peerConnection: null,
  }),

  created: async function(){
    AmplifyEventBus.$on('authState', info => {
      if (info == 'signedOut') {
        this.credentials = null
        this.displayInfo('Signed out.')
      } else if (info === 'signedIn') {
        this.displayInfo('Signed In.')
      }
    });
  },

  methods: {
    displayInfo: function(text){
      this.snack.display = true
      this.snack.text = text
    },
    stopAndSignOut: function(){
      this.clearConnection()
      this.$Amplify.Auth.signOut()
        .then(()=>{
          return AmplifyEventBus.$emit('authState', 'signedOut')
        })
    },
    selectRole: function(role){
      this.isMaster = (role=='Master')
      this.clearConnection()
      this.startConnect(this.isMaster)
    },
    playVideo: function(id){
      this.displayInfo('Starting remote stream.')
      const remoteVideo = document.getElementById(id)
      remoteVideo.play()
    },
    clearConnection: function(){
      // 接続停止
      if (this.signalingClient) {
        this.signalingClient.close();
        this.peerConnection.close();
      }
      // カメラの停止
      if (this.localStream){
        let tracks = this.localStream.getTracks();
        tracks.forEach(function(track){
          track.stop()
        })
      }
      this.progress = false
      this.signalingClient = null
      this.localStream = null
      this.remoteStream = null
      this.peerConnection = null
    },
    startConnect: async function(isMaster){
      const role = isMaster ? 'MASTER' : 'VIEWER'
      this.peerConnection = await connectSignalingChannel(role)
      if (!this.peerConnection) {
        this.displayInfo('Please sign in.')
        return
      }

      this.displayInfo('Getting peer connection.')
      this.generateSignalingClient(isMaster)
    },
    generateSignalingClient(isMaster){
      const mediaConf = { 
        audio: true, 
        video: {
          width: {min: 320, max: 640},
          facingMode: {
            exact: "user"
          }
        }
      }
      const generateFunc = isMaster ? generateSignalingClientMaster : generateSignalingClientViewer
      const { signalingClient, scEmitter } = generateFunc(mediaConf, this.peerConnection)
      this.signalingClient = signalingClient

      scEmitter.on('open', (localStream) => {
        this.progress = false
        this.displayInfo('Connected to signaling service.')
        this.localStream = localStream
      });
      scEmitter.on('sdpOffer', ()=>this.displayInfo('Sending SDP offer.'))
      scEmitter.on('sdpAnswer', ()=>this.displayInfo('Received SDP answer.'))
      scEmitter.on('icecandidate', ()=>this.displayInfo('Sending ICE candidate to client.'))
      scEmitter.on('track', (remoteStream) => {
        this.displayInfo('Recieved remote track.')
        this.remoteStream = remoteStream
      })
      scEmitter.on('iceCandidate', ()=>this.displayInfo('Received ICE candidate from client.'))
      scEmitter.on('close', ()=>{})
      scEmitter.on('error', ()=>this.displayInfo('Signaling client error.'))

      const rolename = isMaster ? 'master' : 'viewer' 
      this.displayInfo('Starting ' + rolename + ' connection.')
      this.signalingClient.open()
      this.progress = true
    }
  }
}
</script>

スタイル部はお世辞にも出来の良い CSS とは言えませんが、とりあえずコレで目的の表示になるのでコレで良しとしています。

App.vue(style)
<style>
div {
  padding: 0;
}

video#local {
  top:2px;
  left:2px;
  width: 20%;
  height: auto;
  position: absolute;
  z-index: 100;
}

video#remote {
  width: 100%;
  height: auto;
  position: relative;
  z-index: 1;
}

.auth div {
  min-width: 100% !important;
  background-color: transparent;
  color: white
}

.snack {
  max-width: 80% !important;
  text-align: center;
}
</style>

以上でフロントエンドの実装も完了です。リポジトリにプッシュするとAmplify Console がビルド・デプロイを自動で行ってくれます。

git add .
git commit -m "second commit."
git push origin master

最終形

(このアプリの利用には Cognito のユーザが必要になりますので、あらかじめ適当なユーザを作っておきます。)

Amplify Console のデプロイが終わって、URL にアクセスすると以下の表示となっているはずです。
image.png
作っておいた Cognito ユーザでログインし、下部のボタンで WebChat の Master か Viewer か、選択します。同様に通話の相手にもログインしてもらい、自分と異なるロールを選択してもらうと、以下のように LINE 風にビデオ(通話は怪しい…)が可能となります。(が冒頭で書いたように残念ながら iPhone での挙動が非常に怪しいです。もし画像が表示されても動かないようでしたら、画像をタップしてみて下さい。それで動くようになる場合もありました。)
Line.png
なお、Kinesis Video Streams の現在の仕様では、Master 1 に対し、Viewer 10 がぶら下がれるのですが、このアプリはそこまで高機能ではなく1対1での通信しかできません。また、排他も聞いていないので、誰かが Master を選択していても 別の人物が Master のボタンを押すことができてしまいます。つまり作り込みがだいぶ甘いですがご了承下さい。

反省・課題

ネーミング

プラグイン作っている途中から気付いていたのですが、「amplify-util-webrtc」というネーミングが良くないです。WebRTC はあくまで仕組みの名称なんで、他のプラグインの、例えば「Storage」「Web Push」「Analytics」とかって名称と全然レベル感が合わないな、と。もっと抽象的というか、仕組みではなく目的にふさわしいネーミングを使うべきでした。が、結局最後まで思いつかず今も思いつかないので結局そのままになってしまいました。「video-chat」だと前述の「video」と若干かぶっちゃうし。

android, iOS 未対応

確認していないので正確には不明なのですが、おそらくこのプラグイン android, iOS では役に立たないと思います。他のプラグインのソースを読むときちんと、プラットフォームごとの対応が入っていてさすがだなと思います。マルチプラットフォームを実現するのは本当に大変ですね。

チーム作業、ローカル作業 未対応

Amplify CLI ではローカルリポジトリを開発者独自の AWS 環境にデプロイして動作確認、テストすることができます。きちんと確認していないのですが、今回作ったプラグインはこの仕組みに対応できていません。Amplify CLI は クラウド上のリソースは全て CloudFormation(もしくは Terraform)で作る前提で出来ているみたいなので、今回のように無理やり AWS SDK からリソース作っているパターンだとどこかで破綻するように思います。

フロント側のライブラリ対応

今回はフロント側のライブラリを別に作ることをせず、プラグインが生成したソースコードを import する形を取りました。そのおかげで、自動生成したソースコードで使っているライブラリを別途ユーザが手動で npm install しなくてはならないことになってしまい、スマートさに欠けるカタチになってしまいました。
公式の aws-amplifyaws-amplify-vue のように、aws-amplify-webrtc があると尚良かったように思います。

さいごに

プラグイン、最初は Node.js の理解が中途半端なことも手伝って、かなり苦労しましたが慣れてくると楽しく作ることができました。CloudFormation に対応しているサービスであれば、今回のような強引なことをせずに作ることができると思います。機会があれば、ぜひ皆さんも挑戦してみて下さい。(そしてコードを公開して参考にさせて下さい。)
それでは、皆さんより良い Amplify ライフを!

14
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14
8