Twilio WebRTCハンズオン（WebRTC Go 編）

:::note alert
2023年5月1日を持ちまして、株式会社KDDIウェブコミュニケーションズのTwilioリセール事業が終了したため、本記事に記載されている内容は正確ではないことを予めご了承ください。
:::

# はじめに

本資料は、過去に作成した「[Twilio WebRTC ハンズオン 2020年版](https://qiita.com/mobilebiz/items/bdace9699ce77dbc7279)」の2020年リメイク版になります。

:::note info
最新情報に更新（2022-11-15）
:::

### 対象者

- Twilio が初めての方
- 名前は知っているけど、触ったことがない方
- WebRTC に興味のある方

# カリキュラム

- ハンズオンの準備と Twilio Video の概要
- Twilio Videoの実装
- 参考情報
- まとめ

## 自己紹介

![katsumi_small_1.jpg](https://qiita-image-store.s3.amazonaws.com/0/86046/1ef9874d-ccb6-d0bb-b56f-eca8d5e6af50.jpeg)

### 髙橋克己（たかはしかつみ）

（株）KDDI ウェブコミュニケーションズ
Twilio 事業部　エバンジェリスト
Facebook: [katsumi.takahashi](https://www.facebook.com/katsumi.takahashi)
GitHub: [mobilebiz](https://github.com/mobilebiz)

###### katsumi.takahashi@kddi-web.com

## ハンズオン用ソースコード

[こちら](https://github.com/mobilebiz/twilio-video-handson-webrtc-go/raw/main/contents/video.zip)からtwilio-video.zipをローカルにダウンロードして展開して下さい。

# ハンズオンの準備と Twilio Video の概要

このハンズオンの内容はトライアルアカウントでも利用できます。
トライアルアカウントの作成は、以下の URL から行ってください。
[https://cloudapi.kddi-web.com/signup](https://cloudapi.kddi-web.com/signup)

### 手順1. Twilio の管理コンソールにログインします。

- ブラウザで[ログイン画面](https://jp.twilio.com/login/kddi-web)を表示し、ログインをします。

## Programmable Video

- **WebRTC** をベースに構築された音声・動画・データ通信サービス
- JavaScript SDK / iOS SDK / Android SDK を提供
- アクセストークン（JWT）を使った認証
- ルームベースでの実装（<font color='red'>呼び出し機能はありません</font>）
- P2Pベースの「Go」、「Peer-to-peer」と、SFUベースの「Group」「Group-small」の4種類のルームタイプが利用可能
- 「Go」は最大2名、「Peer-to-peer」は最大10名まで（3名までを推奨）、「Group-small」は最大4名まで、「Group」は最大50名までの参加者を収容可能
- TURN/STUN サービスも提供
- 画面共有に対応（Chrome と Firefox、Edge）
- サーバーサイドでの録画に対応（「Group」「Group-small」のみ）
- メディアコーデックには、Opus（音声）、VP8 / H.264（映像）に対応

ルームの作成方法には、最初の参加者が参加したときに動的に生成される Ad-hoc 方式と、REST API を使って予めルームを作成しておく方式があります。Ad-hoc 方式では、管理コンソール上で設定しておいたルームタイプが自動的に選択されますが、REST API を使った方式では、都度ルームタイプを指定することができます。
今回のハンズオンでは、Ad-hoc 方式を使いますが、都度ルームを作成したい場合は以下の記事を参考にしてください。
[Twilio Videoで都度Roomの設定を行う方法](https://qiita.com/mobilebiz/items/b300861475409fee8199)

### Twilio Video の基本概念

以下の図は、Twilio Video を使って、ビデオ会議を行うときの基本的な概念を図にしたものです。

![Untitled (3).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/86046/65f22968-86ef-1c4f-9d26-b9b96af2cfa8.png)

1. ビデオを開始したいデバイスは、まずはルームに入るためのトークンを取得する必要があります。
1. 次に、取得したトークンを使ってルームに接続をします。このときに、自分自身のトラック情報を渡します。
1. 接続が成功すると、ルームに入室している他の参加者の情報を取得することができます。
1. 他の参加者のトラックを購読し、自身の画面上に表示させるようにします。
1. ルームで発生する各種イベント通知（参加者の入退室など）を取得し、処理を行います。

### <参考> WebRTC とは

WebRTC とは、Web の技術（わかりやすく言えばブラウザの機能）を使って、お互いにリアルタイム性のある情報交換をするための技術の総称になります。リアルタイムな情報交換データとして、音声や映像などが挙げられます。
WebRTC では情報交換する相手とどのような手法でデータを交換するかを決める必要があり、その作業をシグナリングと呼んでいます。
また、それぞれの端末はファイアーウォールやルータの内側にいることが多く、外部から自分自身を探してもらうための仕組みなどもシグナリングには含まれます。

![スクリーンショット 2022-11-15 16.51.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/86046/85651452-9933-8e71-d99d-fbd9f17c0094.png)

### <参考> P2P 方式と SFU 方式

- P2P(Peer to Peer)方式
    - ブラウザ同士が直接映像などをやり取りする
    - もっとも高解像度で自由度が高い
    - WebRTC が前提としている接続方式でリアルタイム通信に向いている
    - 接続する相手が増えてくるとブラウザ側の負担が大きくなる（特にモバイル）
    - Twilio Video では「WebRTC Go」と「Peer-to-peer」で利用される方式 
- SFU（Selective Forwarding Unit）方式
    - 映像や音声を中継するサーバーを配置
    - 録画などの付加機能を実施することが可能
    - 上りのコネクションは１本、下りは相手の数分必要
    - ブラウザが増えた場合に、ブラウザ側の負担が P2P に比べて少ない
    - SFU 側では画像の加工などは行わないため、SFU 自体の負担は低い
    - Twilio Video では「Group」「Group-small」で利用される方式

![上級編SFUとP2P.png](https://qiita-image-store.s3.amazonaws.com/0/86046/b3a6f8d2-e8f1-5c5e-0e44-169352427987.png)

### <参考> TURN/STUN

- NAT（Network Address Translation） 越えをするための仕組み
- ICE（Interactive Connectivity Establishment）として説明されることもある
- TURN
 - 直接通信できない場合に中継するサーバー 
- STUN
 - 自分が外部に出たときに、外部から見える情報を取得する
- Twilioは、Network Traversal Service として TURN/STUN を単独でも提供（TURN のみ有料）
 - TURN/STUN の利用料は Twilio Video の利用料の含まれる ~~（「Go」のみ、25GB/月以降は有料）~~
 - Twilio の TURN サーバーは9つのリージョンから最も近い場所を利用可能

![スクリーンショット 2018-07-15 20.37.21.png](https://qiita-image-store.s3.amazonaws.com/0/86046/4ec7bf6f-1805-94fc-1f42-ac5e697396dd.png)

Twilio Video が利用できるかを確認するためには、以下のテストサイトに接続してみます。

https://networktest.twilio.com/?region=jp1

### Twilio Video SDK が対応するブラウザ

|  | CHROME | MS EDGE ※ | FIREFOX | SAFARI | WebView
|:--|:--|:--|:--|:--|:--|
| Android | ✓ | - | ✓ | - | - |
| iOS | ✓ | - | - | ✓ | ✓ |
| Linux | ✓ | - | ✓ | - | -|
| macOS | ✓ | ✓ | ✓ | ✓ | -|
| Windows | ✓ | ✓ | ✓ | - | -|

※ [Chromium-based Edge](https://www.microsoftedgeinsider.com/)ブラウザにのみ対応しています。 

### ルームの設定を変更する

ここでは、同時参加者数が2名に限定されますが、無料で使える WebRTC Go の設定を行っていきます。この設定を行うことにより、以後 Ad-hoc 方式で作成されるルームは、「Go」タイプとなります。

- Twilio管理コンソールにログインします。
- **Explore Products**から**Video**を選択します。
- さらに、**Manage** > **Room Settings**を選択します。
- **Room Topology**を以下のように設定します。

|設定項目|設定値|備考|
|:--|:--|:--|
|Room Type|Go|WebRTC Goを選択|
|Maximum Participants|2|最大参加者数を2に制限|
|Maximum Participant Duration|14400|最大滞在時間を14400秒（4時間）に制限|

![スクリーンショット 2022-11-16 8.01.17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/86046/cce6c24a-facc-b139-77e5-3ba01bf8c06f.png)

- **Save**ボタンを押します。

### Video APIの認証

- ユーザがルームに入るためには、**アクセストークン**が必要（電車の切符のようなもの）
- アクセストークンは、以下の情報を使って生成する必要あり（デフォルトの有効期限は1時間）
    - ACCOUNT SID （Twilioのアカウントに紐付いたID）
    - API KEY
    - API SECRET
    - IDENTITY （セッション中にユーザを識別するための値）
- アクセストークンに対して、接続可能なルームを指定しておくことも可能
- アクセストークンは、通常はAPI経由で生成する（一時的なトークンでよければ、管理コンソール上でも生成が可能）

## アクセストークンの生成プログラム（API経由）を作る

では最初に、アクセストークンを作成する部分を作ります。

#### シナリオ

Programmable Video で利用するアクセストークンを生成するプログラムを作りましょう。
今回は、実際にブラウザから動的に生成できるように、Twilioのサーバーレス環境であるTwilio Functionsを利用します。

### 手順1. APIキーを生成する

- 管理コンソールの上部にある**Account**プルダウンから、**API Keys & tokens** を選択します。
![スクリーンショット 2022-11-16 8.04.40.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/86046/9a7491ee-f05c-9cc1-eb90-1ab0e2954e34.png)

:::note info
**Authenticate to make changes**が表示されたら、赤いボタンを押して認証します。
:::

- **Create API key**ボタンを押して、新しい API Key を作成します。
- **Friendly name**に「Video」と入力し、**Region**は「US1」、**Key type**は「Standard」を選択します。

![スクリーンショット 2022-11-16 8.09.05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/86046/302d4984-d30c-1edf-b544-9fe23afe8b7f.png)

- **Create API Key**ボタンを押します。
- 表示される**SID**（SK から始まる文字列）と、**SECRET**に表示されている文字列の両方を**メモ帳に保存**します（**SECRET** については、このタイミングでしか確認することができません）。
- **Got it! 〜** のチェックボックスにチェックを入れて、**Done**ボタンを押します。

![スクリーンショット 2022-11-16 8.12.06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/86046/103c686a-ea12-96d7-be24-778fd3f59a6c.png)

:::note alert
API Key と API Secret が外部に漏洩すると、第三者から不正な利用をされてしまう可能性がありますので、取り扱いには十分注意してください。
:::

### 手順2. アクセストークン発行用Functionの作成

Programmable Video を利用するためには、先程作成した API キーと SECRET を使って、アクセストークンを動的に生成する必要があります。
今回は、Twilio Functions を使って、特定のルームにだけアクセス可能なアクセストークンを生成してみましょう。

- **Explore Products**から、**Functions and Assets**を選択します。
- Overview 画面にある、青い **Create Service** ボタンを押します。
- ダイアログ内の **Service Name** に「video」と入力ます。
![スクリーンショット 2022-11-16 8.17.05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/86046/58e1ae3c-b9b3-a4fc-4c7d-a6602dd446ec.png)

- **Next** ボタンを押します。
- 作成したサービスの Editor ページが表示されるので、**Add +** ボタンを押して、表示されるリストから **Add Function** を選択します。
![スクリーンショット 2022-11-16 8.17.57.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/86046/f12df426-3bb6-c3aa-cc84-33871e38d693.png)
- パス名を入れる状態になるので、「/video-token」と入力してエンターキーを押します。
![スクリーンショット 2022-11-16 8.18.42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/86046/bccfcbc4-abf3-c422-b610-3d1d466edb0d.png)
- **Protected** が選択されているリストを選択し、 **Public** を選択し直します。
![スクリーンショット 2022-11-16 8.19.17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/86046/3a154c37-d58e-3a29-f572-57cbea7e213e.png)
- 作成されたコードをすべて削除し、以下のコードで置き換えましょう。

```javascript:video-token.js
const uuid = require('uuid');
exports.handler = function(context, event, callback) {
    // ルーム名を取得
    const ROOM_NAME = event.roomName || '';
    if (ROOM_NAME === '') callback(new Error('roomName parameter was not set.'))

    // 環境変数から各種情報をセット
    const ACCOUNT_SID = context.ACCOUNT_SID;
    const API_KEY = context.TWILIO_VIDEO_KEY;
    const API_SECRET = context.TWILIO_VIDEO_SECRET;

    const IDENTITY = uuid.v4();  // ランダムにクライアント名を生成

    const AccessToken = Twilio.jwt.AccessToken;
    const VideoGrant = AccessToken.VideoGrant;

    // 参加できるルームをトークンで限定
    const videoGrant = new VideoGrant({
        room: ROOM_NAME
    });

    const accessToken = new AccessToken(
        ACCOUNT_SID,
        API_KEY,
        API_SECRET
    );

    accessToken.addGrant(videoGrant);
    accessToken.identity = IDENTITY;
    callback(null, {
        token: accessToken.toJwt()
    });
};
``` 

- 最後に忘れずに、 **Save** ボタンを押してコードを保存します。

- 次に、Editor 画面の **Settings** の中にある、**Environment Variables** を選択します。

![スクリーンショット 2020-11-22 11.46.57.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/86046/9c2142cc-6e37-d46a-b538-1871c34d7e47.png)

- **KEY** と **VALUE** に以下の２つの項目を追加します。それぞれの値を入力した後、**Add** ボタンを押すと追加されます。

|KEY|VALUE|
|:--|:--|
|TWILIO_VIDEO_KEY|先程メモしておいた API KEY （SKから始まる文字列）|
|TWILIO_VIDEO_SECRET|先程メモしておいた SECRET|

![スクリーンショット 2022-11-16 8.22.07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/86046/561fc20f-d122-8aa7-5741-584dcf61b1f4.png)

- 次に、Editor 画面の **Settings** の中にある、**Dependencies** を選択します。
- **MODULE** と **VERSION** に以下の２つの項目を追加します。それぞれの値を入力した後、**Add** ボタンを押すと追加されます。

|MODULE|VERSION|
|:--|:--|
|uuid|9.0.0|

![スクリーンショット 2022-11-16 8.24.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/86046/52981639-b39a-a47b-cbdc-e821fbf275ca.png)

### 手順3. デプロイ

ではここまでの状態で、一度デプロイを行います。デプロイというのは、サーバー上で実行できる環境を構築する作業です。コードを修正してもデプロイするまではサーバー上では実行できません。

- Editor 画面の下の方にある、青い **Deploy All** ボタンを押します。
- デプロイ中のログが画面上に表示され、しばらくしてデプロイが完了すると Functions の `/video-token` に緑のチェックが入ります。

![スクリーンショット 2020-11-22 11.58.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/86046/a6d69eb7-da2d-8b9e-9625-353a7f0febc2.png)

では次に、正しくトークンが発行されるかをテストしてみたいと思います。

- `video-token` の右側にあるドットが3つあるアイコンを押して、プルダウンから **Copy URL** を選択します。

![スクリーンショット 2020-11-22 12.00.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/86046/3a1fefdb-229d-6f01-09d7-8cc4c6ee3ecb.png)

- ブラウザで新しいタブを開いて、コピーしたURLを貼り付け、さらに `?roomName=hoge` パラメータを付加します（ドメインのXXXXはご自分の環境によって異なります）。

`https://video-XXXX.twil.io/video-token?roomName=hoge`


画面上に、次のような JSON が戻れば成功です。

```json
{"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImN0eSI6InR3aWxpby1mcGE7dj0xIn0.eyJqdGkiOiJTSzE2YmMyNzI1MWNkNTE5NjQ0MzJkYWYxNTFiMmU4MDAzLTE2MDYwODEzNzkiLCJncmFudHMiOnsiaWRlbnRpdHkiOiI4NmI4ZTRjZi0xMGY5LTRmYjAtYTlkMS1hYWUzNzNiYmE1ZTciLCJ2aWRlbyI6eyJyb29tIjoieHh4In19LCJpYXQiOjE2MDYwODEzNzksImV4cCI6MTYwNjA4NDk3OSwiaXNzIjoiU0sxNmJjMjcyNTFjZDUxOTY0NDMyZGFmMTUxYjJlODAwMyIsInN1YiI6IkFDYTBlNTA0ZDVmOWVlZWRkZmJiNWQ3NzMyMWIxYzc0OTYifQ.zxj4EpyRIjZRO3XqziW03BnNR54sOALS8SIMITQyBho"}
```

このトークンの有効期限は1時間なので、通常はルームに参加する直前にこの URL を呼び出してトークンを取得します。

:::note info
ルームに入室している間にトークンの有効期限が切れても、ルームから退出されることはありません。
:::

# Twilio Videoの実装

ここからは、実際にHTMLとJavaScriptを使って、Twilio VideoのJavaScript SDKを使った実装をしていきます。

## 手順1. HTMLとJSを設定する

- [こちら](https://github.com/mobilebiz/twilio-video-handson-webrtc-go/raw/main/contents/video.zip)からtwilio-video.zipをローカルにダウンロードして展開して下さい。
- ダウンロードした ZIP ファイルを解凍し、２つのファイルがあることを確認します。
    - video.html
    - video.js
- 管理コンソールの **Functions** 先程作成した **video** サービスを選択し、Editor 画面を開きます。
- **Add +** ボタンを押します。
- **Upload File** を選択します。
- 先程解凍した2つのファイル（`video.js` と `video.html`）を選択します。
- **VISIBILITY** を両ファイルとも、**Public** に設定して、画面下にある **Upload** ボタンを押します。

![スクリーンショット 2020-11-23 10.00.37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/86046/cb9b89f6-e11b-a2e8-bf56-3f10dbac490b.png)

- アップロードが完了すると、**Assets** 内に2つのファイルが見えるようになります。

![スクリーンショット 2020-11-23 10.06.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/86046/bfa8aeed-0701-fa1d-a3ed-4ae4d228c222.png)

この時点ではまだデプロイがされていないため、ファイル名の左側が●になっていることがわかります。
では早速デプロイをしてみましょう。

- 画面下にある **Deploy All** ボタンを押します。
- しばらくしてデプロイが完了すると、先程の2つのファイルに緑色のチェックが表示されます。

![スクリーンショット 2020-11-23 10.10.55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/86046/61313fa2-638c-7868-d948-db62a2ea88b0.png)

:::note info
Assets は Twilio が提供しているストレージサービスです。
Public でアップロードすると、外部からもアクセスが可能になります。Proteced を指定すると、アクセスする際に認証が必要になります。
:::

## 手順2. HTML ファイルを修正して、JavaScript や Twilio Video SDK などを読み込む

`Assets` 内の `video.html` を選択すると、以下のような HTML ソースを見ることができます。

![スクリーンショット 2020-11-23 10.38.57.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/86046/f4e7a61b-ec86-4095-2bc1-8e23f26ac486.png)

この時点では、ボタンが２つ表示されるだけの HTML となっています。

### STEP 1. 自分自身の画像を表示するエレメントを追加する

まずは、自分自身のカメラ映像を表示する \<video> タグを追加したいと思います。

- 9行目と10行目の間に以下のコードを記載します。

`      <video id="myStream" autoplay muted="true"></video>`

![スクリーンショット 2020-11-23 10.43.03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/86046/085ab27f-debc-3fe0-6347-55b1156cc954.png)

\<video> タグを使うことで、映像再生に対応するメディアプレイヤーを埋め込むことができます。
\<video> タグの詳細については、[こちら](https://developer.mozilla.org/ja/docs/Web/HTML/Element/video)をご覧ください。

### STEP 2. HTML に外部スクリプトを読み込む

つぎに、video.js ファイルや Twilio Video SDKなどの外部 JavaScript ファイルを読み込む設定を行います。今回読み込むのは以下の3つのファイルです。

1. Twilio Video JS SDK : Twilio が提供する Video 用のSDK
2. axios : アクセストークンを JavaScript 内から呼び出すための HTTP クライアントライブラリ
3. video.js : 今回作成する JavaScript ファイル

- 14行目と15行目の間に以下の3行を追加します。

```html
  <script src="https://sdk.twilio.com/js/video/releases/2.22.1/twilio-video.min.js"></script>
  <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
  <script src="./video.js?version=1.01"></script>
```
![スクリーンショット 2022-11-16 8.35.13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/86046/bdc7f85c-a969-472e-d23e-7506c7ab7fa5.png)

これで HTML ファイルの修正は完了となります。

- **Save** ボタンを押して変更を保存します。

## 手順3. video.js を修正して、ビデオ会議ができるようにする

- Editor 画面の **Assets** 内にある、`video.js` ファイルを選択します。

![スクリーンショット 2020-11-23 11.02.17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/86046/a079798c-e991-881c-dbb5-8ede0df7e629.png)

### STEP 1. プレビュー画面の表示

自分自身の映像を、\<video> タグに表示する部分を作ります。

// STEP 1. から // STEP 1. End の間に以下のコードを記載します。

```javascript
    // STEP 1. プレビュー画面の表示
    localTracks = await Video.createLocalTracks();
    const localVideo = document.getElementById('myStream');
    localTracks.forEach((track) => {
        if (track.kind === 'video' || track.kind === 'audio') {
        track.attach(localVideo);
        }
    });
    // STEP 1. End
```

`Video.createLocalTracks()` を利用することで、自身のPCに接続されているカメラとマイクを使うすることができます。
取得したローカルトラックを \<video> タグのvideoとaudioトラックに割り当てること（12〜16行目）で、自身の映像が再生されます。

- **Save** ボタンを押して変更を保存します。
- 画面下の **Deploy All** ボタンを押して、デプロイします。
- デプロイが完了したら、`/video.html` （`video.js` ではないので注意）の右側のメニューアイコン（ドットが3つ）から、**Copy URL** を選択します。

![スクリーンショット 2020-11-23 11.30.57.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/86046/491e2779-0cd4-aa24-e908-7e02654e826b.png)

- ブラウザの新しいタブを開いて、コピーした URL を貼り付けて実行します。
- 初回は、マイクとカメラの許可ダイアログが表示されるので、**許可**を選択します。

![スクリーンショット 2020-11-23 11.32.17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/86046/7dbbb7e6-3390-f79c-c557-f35ebe91f9bf.png)

- 自分自身のカメラ映像が表示されることを確認します。

![スクリーンショット 2020-11-24 7.22.50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/86046/80900311-7fda-0328-9213-af584873cbec.png)

- カメラが複数台あったり、仮想カメラをセットアップしている場合などは、そちらが選択されてしまうことがあります。その場合は、ブラウザのアドレス欄にあるカメラアイコンをクリックして、さらに**管理**ボタンを押します。

![スクリーンショット 2020-11-23 11.36.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/86046/663ce756-1503-5b0f-839c-8adf2083fc5e.png)

- **権限**の中の**カメラ**や**マイク**を選択すると、デフォルトのデバイスが選択できます。

### STEP 2. アクセストークンを取得

ここまでは、Twilio Video は利用されていません。ここからいよいよ Twilio Video を使って、ビデオルームに接続する部分を作っていきましょう。

`video.js` の // STEP 2. から // STEP 2. End の間に以下のコードを記載します。

```javascript
        // STEP 2. アクセストークンを取得
        axios.get(`${document.location.protocol}//${TWILIO_DOMAIN}/video-token?roomName=${ROOM_NAME}`)
        .then(async body => {
            const token = body.data.token;
            console.log(`Token got. ${token}`); // 本番環境ではコメントアウトしましょう

            connectRoom(token); // ルームに接続
        });
        // STEP 2. End
```

ここでは、**入室**ボタンを押したときの処理として記述します。
先ほど作成したサーバー上の `video-token` Function を Axios 経由で取得します。取得時にルーム名を指定していて、取得されたトークンでは、そのルームにしか入れないように制限をかけています。
取得したアクセストークンは JWT(JSON) 形式で返ってきます。

- **Save** ボタンを押して修正を保存します。
- **Deploy All** ボタンを押してデプロイします。
- 先程開いたカメラ映像のタブに移動します。
- ブラウザのアドレスバーの右端にあるメニューアイコンを押して、**その他のツール** > **デベロッパー ツール** を選択します。

![スクリーンショット 2020-11-23 11.50.32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/86046/9a423210-2205-da8e-e619-854757ca4466.png)
- **Console** タブを開いておきます。
- ブラウザを強制リロードします（Editor 画面でリロードしないように気をつけてください）。
- **入室**ボタンを押します。
- コンソール画面上に以下のようなトークン情報が表示されれば、ここまでは成功です。

![スクリーンショット 2020-11-23 11.55.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/86046/2801ba8d-27d7-d4a7-c83b-0ea077f387ab.png)

ここで、403エラーが発生する場合は、`video-token` Function が `Public` になっていない可能性がありますので確認しましょう。

### STEP 3. 部屋に入室

では次に、取得したアクセストークンを使って、ルームに入ってみましょう。

`video.js` の // STEP 3. から // STEP 3. End の間に以下のコードを記載します。

```javascript
        // STEP 3. 部屋に入室
        Video.connect(token, { name: ROOM_NAME, tracks: localTracks })
        .then(room => {
            console.log(`Connected to Room ${room.name}`);
            videoRoom = room;

            // すでに入室している参加者を表示
            room.participants.forEach(participantConnected);

            // 誰かが入室してきたときの処理
            room.on('participantConnected', participantConnected);

            // 誰かが退室したときの処理
            room.on('participantDisconnected', participantDisconnected);

            // 自分が退室したときの処理
            room.once('disconnected', error => room.participants.forEach(participantDisconnected));
        
            btnJoinRoom.disabled = true;
            btnLeaveRoom.disabled = false;
        })
        .catch(err => console.error(err));
        // STEP 3. End
```

ルームに入るためのメソッドは、`Video.connect()` です。第一オプションのアクセストークンは必須です。第2オプションでは、下記のパラメータを渡すことが可能です。

- `audio` および `video` : これらが有効時（デフォルト）には、ルームに接続すると直ちにローカルのカメラおよびマイクからのオーディオおよびビデオのトラックが作成および公開されます。
- `region` : シグナリングサーバのロケーションを指定します。日本リージョンを利用する場合は、`jp1` を指定します。省略すると `gll` 最も遅延が少ないリージョンが自動選択されます。
- `name` : 参加したいルームの名前を動的に指定できます。
- `iceServers` および `iceTransportPolicy` : ルーム接続時に TURN/STUN サーバーを強制的に利用することができます。
オプションの詳細については、[こちら](https://media.twiliocdn.com/sdk/js/video/releases/2.7.3/docs/global.html#ConnectOptions)をご覧ください。

ルームの名前には、参加したいルームを指定します。その名前のルームがまだ存在していない場合、接続に先立って作成されます（Ad-hoc 方式）。 ルームがすでにアクティブな場合は、自動的にそのルームに接続され、同じルームに接続されている他の参加者（`participant`）からの通知を受信します。ルーム名はアカウント内で一意です。
ルームに参加すると、`room` オブジェクトの中に、`participants` リストが返りますので、それを使ってすでに参加しているユーザの情報を取得しています（51行目）。

![スクリーンショット 2022-11-16 9.13.49.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/86046/998f7696-1c11-b13d-5555-a6ab6d565d43.png)

また、ルームへの接続が完了するとルームのイベントが取得できるようになるので、イベントハンドラを指定します。

- `participantConnected` イベント： 自分以外の参加者がルームに参加したとき（54行目）
- `participantDisconnected` イベント: 自分以外の参加者がルームから退出したとき（57行目）
- `disconnected` イベント: 接続が切れてしまったとき（60行目）

62〜63行目は、ボタンの有効化/無効化を指定しています。ルームに正しく入れたので、**入室**ボタンを無効化して、**退室**ボタンを有効化しています。

では、ここまでの状態で一度テストをしましょう。

- **Save** ボタンを押して修正を保存します。
- **Deploy All** ボタンを押してデプロイします。
- 先程開いたカメラ映像のタブに移動します。
- ブラウザを強制リロードします。
- **入室**ボタンを押してルームに入ります。
- 以下のようにコンソールに表示されれば、無事にルームに接続できています（`Connected to Room VideoRoom` が表示されます）。

![スクリーンショット 2020-11-23 13.26.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/86046/6d80016f-9116-ae80-a4e3-b36b086272bc.png)

### STEP 4. 部屋から退室

STEP 3で指定した各イベントの処理は一旦おいておいて、まずは**退出**ボタンを押したときの処理を記述しておきましょう。

`video.js` の // STEP 4. から // STEP 4. End の間に以下のコードを記載します。

```javascript
        // STEP 4. 部屋から退室
        videoRoom.disconnect();
        console.log(`Disconnected to Room ${videoRoom.name}`);
        btnJoinRoom.disabled = false;
        btnLeaveRoom.disabled = true;
        // STEP 4. End
```

ルームから退室するには、`disconnect()` メソッドを呼ぶだけです（39行目）。

![スクリーンショット 2022-11-16 9.15.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/86046/f1a1f20c-3e30-0a74-dda9-94c26e14fec1.png)

`disconnect()` をコールすると、STEP 3で指定した `disconnect` イベントが発火しますので、画面周りの処理はそちらに任せています。
41〜42行目は、ボタンの有効化/無効化の処理です。

では再び、ここまでの状態で一度テストをしましょう。

- **Save** ボタンを押して修正を保存します。
- **Deploy All** ボタンを押してデプロイします。
- 先程開いたカメラ映像のタブに移動します。
- ブラウザを強制リロードします。
- **入室**ボタンを押してルームに入ります。
- **退室**ボタンを押してルームから抜けます。
- コンソール上に `Disconnected to Room VideoRoom` が表示されれば退室が完了しています。

![スクリーンショット 2020-11-23 13.38.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/86046/a6554584-c904-62bc-e20b-b89c48ec5bf0.png)

テストをしたときに、上記以外に `Participant xxxxxx connected` のメッセージが出ることがあります。これは、入室した状態でブラウザをリロードしてしまったため、サーバ上ではルームに入室した状態が残ってしまっており、それを検知して先に入室しているユーザがいるように反応してしまったためです。しばらくすると最初のユーザがサーバー上から消えます（`Participant xxxxxx disconnected.` が表示されます）。

### STEP 5. 参加者を表示する

では次に、別の参加者の映像を画面に表示させてみたいと思います。

`video.js` の // STEP 5. から // STEP 5. End の間に以下のコードを記載します。

```javascript
        // STEP 5. 参加者を表示する 
        const div = document.createElement('div');
        div.id = participant.sid;

        // 参加者のトラック（映像、音声など）を処理
        participant.tracks.forEach(publication => {
            if (publication.isSubscribed) {
                trackSubscribed(div, publication.track);
            }
        });
        
        // 参加者の映像が届いたとき
        participant.on('trackSubscribed', track => trackSubscribed(div, track));

        // 参加者の映像が切れたとき
        participant.on('trackUnsubscribed', trackUnsubscribed);
    
        document.body.appendChild(div);
        // STEP 5. End
```

新しい参加者を表示するための \<div> タグを生成します（78行目）。
削除するときの識別子として利用するため、`id` に参加者（`participant`）の `id` を指定しています（79行目）。

![スクリーンショット 2022-11-16 9.18.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/86046/c1e178aa-db4b-af30-5124-e701fa4dcb8f.png)

82〜86行目は、参加者のトラック（`tracks`）リストを使って、パブリケーション（`publication`）オブジェクトを取り出し、そこに含まれるトラックを使って画面の表示を行う処理です（画面の表示自体は、後述のSTEP 7で行います）。

WebRTC では、音声や映像をトラックと呼ばれる配送経路上で送受信します。すなわち、１参加者は複数のトラックを持つことになります。
**Participant** > **Tracks** > **Track**

この時点では、画面上への表示の処理を記述していないので、実行しないで先にすすみましょう。

### STEP 6. 他の参加者の画面を削除する

次は他の参加者が退出したときの処理です。具体的には、`participantDisconnected` イベントが発火したときに動作する処理となります。

`video.js` の // STEP 6. から // STEP 6. End の間に以下のコードを記載します。

```javascript
        // STEP 6. 他の参加者の画面を削除する
        document.getElementById(participant.sid).remove();
        // STEP 6. End
```

STEP 5で、参加者用に作成した \<div> タグの `id` を指定して、`remove()` メソッドを呼びます。これにより、\<div> タグ自体がドキュメントから削除されるので、画面上から消えます。

### STEP 7. トラックをアタッチする

では次に、先程の STEP 5でペンディングにしていた、トラックを画面上の \<div> タグと結びつける処理を記述します。

`video.js` の // STEP 7. から // STEP 7. End の間に以下のコードを記載します。

```javascript
        // STEP 7. トラックをアタッチする
        div.appendChild(track.attach());
        // STEP 7. End
```

コード自体は非常にシンプルです。
実際に、音声や映像のトラックを \<div> タグにアタッチすると、以下のように \<audio> タグや、\<video> タグが追加されます。

![スクリーンショット 2020-11-23 14.16.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/86046/b9f81e29-7597-4887-5d7f-27ac825d4bb2.png)

では再び、ここまでの状態で一度テストをしましょう。

- **Save** ボタンを押して修正を保存します。
- **Deploy All** ボタンを押してデプロイします。
- 先程開いたカメラ映像のタブに移動します。
- ブラウザを強制リロードします。
- **入室**ボタンを押してルームに入ります。
- 別のタブを開いて、同じURLで接続してみます。

![スクリーンショット 2020-11-24 7.23.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/86046/3b4a5701-9cc1-62e4-f425-2ee7712e4091.png)

このように相手側（今回は自分のカメラなので同じ画像です）が表示されれば成功です。
環境によっては、相手側の画像のサイズがだんだん大きくなるかもしれません。これは、Twilio が通信状況をリアルタイムで検知しながら最適なサイズを自動で選択するためです。

- それぞれのタブで、**退室**ボタンを押してルームから抜けます。

### STEP 8. トラックのデタッチ

では最後に、参加者が退出したときの処理を記述しましょう。

`video.js` の // STEP 8. から // STEP 8. End の間に以下のコードを記載します。

```javascript
        // STEP 8. トラックのデタッチ
        track.detach().forEach(element => element.remove());
        // STEP 8 End
```

すでに、STEP 6で参加者が退出したときの \<div> タグを削除する処理を記述しているので、このコード自体はあまり意味がないように見えますが、たとえば参加者の音声トラックだけが削除された場合（音声ミュート）などは、`disconnect` イベントは発生せず、`trackUnsubscribed` イベントのみが発火するため、こちらのコードが重要になります。

これでコードはすべて完成です。
完成したコードを載せておきます。

```javascript:video.js
(async() => {
    'use strict';

    const TWILIO_DOMAIN = location.host;    // 現在のURL
    const ROOM_NAME = 'VideoRoom';          // 部屋の名前
    const Video = Twilio.Video;             // Twilio Video JS SDK
    let videoRoom, localTracks;

    // STEP 1. プレビュー画面の表示
    localTracks = await Video.createLocalTracks();
    const localVideo = document.getElementById('myStream');
    localTracks.forEach((track) => {
        if (track.kind === 'video' || track.kind === 'audio') {
        track.attach(localVideo);
        }
    });
    // STEP 1. End
        
    // ボタンの準備
    const btnJoinRoom = document.getElementById("button-join");
    const btnLeaveRoom = document.getElementById("button-leave");

    // 入室ボタンが押されたときの処理
    btnJoinRoom.onclick = (() => {
        // STEP 2. アクセストークンを取得
        axios.get(`${document.location.protocol}//${TWILIO_DOMAIN}/video-token?roomName=${ROOM_NAME}`)
        .then(async body => {
            const token = body.data.token;
            console.log(`Token got. ${token}`); // 本番環境ではコメントアウトしましょう

            connectRoom(token); // ルームに接続
        });
        // STEP 2. End
    });

    // 退出ボタンが押されたときの処理
    btnLeaveRoom.onclick = (() => {
        // STEP 4. 部屋から退室
        videoRoom.disconnect();
        console.log(`Disconnected to Room ${videoRoom.name}`);
        btnJoinRoom.disabled = false;
        btnLeaveRoom.disabled = true;
        // STEP 4. End
    });

    // ルームに接続
    const connectRoom = (token) => {
        // STEP 3. 部屋に入室
        Video.connect(token, { name: ROOM_NAME, tracks: localTracks })
        .then(room => {
            console.log(`Connected to Room ${room.name}`);
            videoRoom = room;

            // すでに入室している参加者を表示
            room.participants.forEach(participantConnected);

            // 誰かが入室してきたときの処理
            room.on('participantConnected', participantConnected);

            // 誰かが退室したときの処理
            room.on('participantDisconnected', participantDisconnected);

            // 自分が退室したときの処理
            room.once('disconnected', error => room.participants.forEach(participantDisconnected));
        
            btnJoinRoom.disabled = true;
            btnLeaveRoom.disabled = false;
        })
        .catch(err => console.error(err));
        // STEP 3. End
    };

    // 他の参加者が入室したとき
    const participantConnected = (participant) => {
        console.log(`Participant ${participant.identity} connected'`);

        // STEP 5. 参加者を表示する 
        const div = document.createElement('div');
        div.id = participant.sid;

        // 参加者のトラック（映像、音声など）を処理
        participant.tracks.forEach(publication => {
            if (publication.isSubscribed) {
                trackSubscribed(div, publication.track);
            }
        });
        
        // 参加者の映像が届いたとき
        participant.on('trackSubscribed', track => trackSubscribed(div, track));

        // 参加者の映像が切れたとき
        participant.on('trackUnsubscribed', trackUnsubscribed);
    
        document.body.appendChild(div);
        // STEP 5. End
    }

    // 他の参加者が退室したとき
    const participantDisconnected = (participant) => {
        console.log(`Participant ${participant.identity} disconnected.`);

        // STEP 6. 他の参加者の画面を削除する
        document.getElementById(participant.sid).remove();
        // STEP 6. End
    }

    // トラックの購読
    const trackSubscribed = (div, track) => {
        // STEP 7. トラックをアタッチする
        div.appendChild(track.attach());
        // STEP 7. End
    }

    // トラックの非購読
    const trackUnsubscribed = (track) => {
        // STEP 8. トラックのデタッチ
        track.detach().forEach(element => element.remove());
        // STEP 8 End
    }

})();
```

先程と同じように再度デプロイをしてテストをしてみましょう。

# 参考情報

今まで紹介してこなかったTwilio Videoの機能について、簡単に説明します。

- データトラック
- 画面共有
- 録画
- 料金

## データトラック

- 音声や動画以外に、データ通信用のトラックを利用することできます。
- チャットのような文字列のやり取りや、マウスの動きを送信して共同編集が可能です。
- データトラック用に、[DataTrack API](https://jp.twilio.com/docs/video/using-datatrack-api)が用意されています。
- ネットワークの状態によって、パケットのロスが発生することがあります。これを調整するためのパラメータとして、以下の２つを用意しています。
 - maxPacketLifeTime : パケットの生存時間（この時間内であれば再送の対象とする。単位はms）
 - maxRetransmits : パケットの再送回数（この回数を越えても送信できない場合はドロップ）  

## 画面共有

- Chrome、 Firefox、 MS Edge （chromium 版）を利用する場合、画面共有が可能です。
- Firefoxはバージョン52以上で利用可能です。
- 画面共有のついては、[こちら](https://jp.twilio.com/docs/video/screen-capture-chrome)をご参照ください。

![twilioVideo-screen-share2.gif](https://qiita-image-store.s3.amazonaws.com/0/86046/c1336273-5674-0605-f72e-1d0dcc3d0e2f.gif)

## 録画（Video Recording）

:::note warn
録画機能については、Peer-to-Peerルームと、WebRTC GOルームでは使えません。
:::

- 録画をするためには、グループルームの初期値で録画を有効にするか、REST APIでルームの作成時に録画を有効にします。
- 録画ファイルは、参加者ごとに、音声（ mka ファイル）と動画（ mkv ファイル）の２つが作成されます。
- メディアファイルのダウンロードは、管理サイトから手動で行うか、以下のURLを使ってダウンロードすることができます。
 - `https://video.twilio.com/v1/Recordings/RTXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX/Media`
 - 上記URLにアクセスするためには、APIキーとSECRETによるBASIC認証が必要です。
- メディアファイルを AWS S3 に直接保存させることも可能です(Enterprise Edition の契約が必要)。
- Twilio 側にメディアファイルを保存する場合は、ユーザの秘密鍵で暗号化することができます(Enterprise Edition の契約が必要)。
- これらの設定は、管理コンソール > Programmable Video > 録音 > 設定で行います。
- [Compositions API](https://jp.twilio.com/docs/video/api/compositions-resource)を利用することで、複数の参加者のメディアデータをまとめて、１つの mp4 もしくは webm に加工することができます。

## Programmable Video の料金（2022/11月現在）

|ルームタイプ|課金単位|料金（税込み）|
|:--|:--|:--|
|Group|1 分あたり|0.54 円|
|Group-small|1 分あたり|0.54 円|
|Peer-to-peer|1 分あたり|0.21 円|
|WebRTC Go||無料|
|録画料|分/参加者あたり|0.54 円|
|録画データ保存料|1GB 毎/日あたり|0.25 円|

`Compositions API` を利用する場合には追加料金が発生します。

データトラック機能は、ルーム料金に含まれます。

# まとめ

Twilio Video は、WebRTC をベースに作られているため、ブラウザの機能をうまく組み合わせて色々なことができます。
今回の記事では触れませんが、字幕をつけたり、画像解析を入れるなど、通常のビデオ会議では実現できないような機能をもたせることも可能です。
WebRTC Go は、参加者あたりの料金がかからないプランとなっているため、ぜひ色々とチャレンジしてみてください。

## <参考> Twilio Video 関連の記事

https://qiita.com/mobilebiz/items/0d6f763b47fdeeb4740d

https://qiita.com/mobilebiz/items/96140ca2b4ab506959ef

https://qiita.com/mobilebiz/items/7bbfa8e794bd97d6f927

https://qiita.com/mobilebiz/items/c5f8d9e035871982b325

https://qiita.com/mobilebiz/items/3d9fe846c456807c82b5

https://qiita.com/mobilebiz/items/c779d8f93ac597eafdff

---
### Twilio（トゥイリオ）とは

https://cloudapi.kddi-web.com
Twilioは音声通話、メッセージング（SMS/チャット）、ビデオなどの 様々なコミュニケーション手段をアプリケーションやビジネスへ容易に組み込むことのできるクラウドAPIサービスです。初期費用不要な従量課金制で、各種開発言語に対応しているため、多くのハッカソンイベントやスタートアップなどにも、ご利用いただいております。

自己紹介|　
:--|:--
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/86046/b51a8b88-abd5-ce5e-195a-de3564ba2ae6.png" height='200' width='150'/>|高橋克己（Katsumi Takahashi） 自称「<font color='red'>赤い芸人</font>」<br />グローバル・インターネット・ジャパン株式会社　代表取締役<br />株式会社KDDIウェブコミュニケーションズ　Twilio事業部エバンジェリスト<br /><br />2001年より大手通信事業者の法人サービスの教育に携わり、企業における電話のしくみや重要性を研究。2016年よりTwilio事業部にジョインし、Twilioを使ったスマートコミュニケーションの普及活動を精力的に行っている。<br />2019 Twilio Champions