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

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

https://github.com/rtaguchi/amplify-util-webrtc

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


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

https://qiita.com/k0bya4/items/4fef5d4abd17eed18f0a

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

https://gist.github.com/voluntas/67e5a26915751226fdcf

さて、私は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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/91332/f6f5e9d0-3900-074a-5e22-143322d60e77.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/backend`に`webrtc`というディレクトリを作成し`onLocal.json`という設定ファイルを作成します。remove を叩くとこの設定ファイルを消します。add も remove も ローカルのファイルを操作するだけで AWS 上のリソース作成・削除は行いません。

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

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

```bash
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`で表示されるのもこのファイルの内容です。

```json: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`は検知不可のようです。

```javascript: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.json`に`add`と`remove`を定義したので、commands 配下にそれぞれ`add.js`と`remove.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 のフローに入れるので便利です。

```javascript: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`で作成した設定ファイルを削除するだけです。コードも実行確認をプロンプトに出すだけの簡単なものです。

```javascript: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 などを気にする必要が無くなります。

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

```javascript: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 のインストール

```bash
npm install -g @aws-amplify/cli
```

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

```bash
$ git clone https://github.com/rtaguchi/amplify-util-webrtc.git
$ cd amplify-util-webrtc
$ npm install -g
```

##### Vue CLI のインストール

```bash
$ npm install -g @vue/cli
```
##### プロジェクトの作成
今回はプロジェクト名を`mobile-video-chat`としました。対話形式で何か質問されますが、そのまま変更無しで default のまま続行します。

```bash
vue create mobile-video-chat`
```

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

```bash
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 しておきます。

```bash
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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/91332/01566efd-45eb-5043-c018-1e257660b994.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.
```

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

##### フロントエンドの実装
まずフロントに必要なライブラリをインストールします。

``` bash
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` は利用しないので中のファイルを消しても問題ありません。

```bash
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`では追加したライブラリの読み込み処理を追加します。

```javascript: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 にします。（何かそっちの方がカッコいいので。）

```javascript: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 を選択するためのボタンというデザインです。

```html: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 との情報送受信などのイベント発生時の処理です。

```javascript: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 とは言えませんが、とりあえずコレで目的の表示になるのでコレで良しとしています。

```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 がビルド・デプロイを自動で行ってくれます。

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

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

Amplify Console のデプロイが終わって、URL にアクセスすると以下の表示となっているはずです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/91332/2f7578fc-b3ec-d296-4d6f-376a3967e74e.png)
作っておいた Cognito ユーザでログインし、下部のボタンで WebChat の Master か Viewer か、選択します。同様に通話の相手にもログインしてもらい、自分と異なるロールを選択してもらうと、以下のように LINE 風にビデオ（通話は怪しい…）が可能となります。（が冒頭で書いたように残念ながら iPhone での挙動が非常に怪しいです。もし画像が表示されても動かないようでしたら、画像をタップしてみて下さい。それで動くようになる場合もありました。）
![Line.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/91332/d8d741b8-92d2-a3d0-3876-f9ba9389a06a.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-amplify`、`aws-amplify-vue` のように、`aws-amplify-webrtc` があると尚良かったように思います。

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