Amazon Kinesis Video Streams WebRTC を動かしてみた

# はじめに

2019年のre:Inventで、Amazon Kinesis Video Streams （以後KVSと表記) に WebRTCを使ったリアルタイム通信が加わりました。

ブラウザ(JavaScript)向けのSDKだけでなく、組み込み用途のC言語SDKや、iOS/Androidといったモバイルアプリ向けのSDKがあるのが特徴です。

- Web用SDK ... https://github.com/awslabs/amazon-kinesis-video-streams-webrtc-sdk-js
- 組み込みC用SDK ... https://github.com/awslabs/amazon-kinesis-video-streams-webrtc-sdk-c
- iOS用SDK ... https://github.com/awslabs/amazon-kinesis-video-streams-webrtc-sdk-ios
- Android用SDK ... https://github.com/awslabs/amazon-kinesis-video-streams-webrtc-sdk-android

今回はWeb用のSDKを使って動作を確認してみました。オマケとしてC用のSDKを覗いて分かったことを記載します。

※続編：[Amazon Kinesis Video Streams WebRTC で無理やり複数人のビデオチャットを作る]
(https://qiita.com/massie_g/items/4cdf475ab623757a2630)

# KVS WebRTCがサポートすること、しないこと

## サーバー機能 

KVS WebRTC のサーバーが提供する機能は、次の通りです。

- シグナリング ... Offer/Answer SDP の交換、ICE candidateの交換
- STUN/TURN ... P2P通信でNAT越えを行ったり、UDPが通らない場合でも通信を行うための仕組み

対してサポートしない機能もあります。

- メディアの送受信
- メディアの変換

もともとあったKVSの方では、サーバー側でメディアを受信して保存したり、HLSで配信する仕組みを持っていました。KVS WebRTCでは同様なことは現状行えないので、注意が必要です。


※TURNによる接続について
私が試せる環境（企業のFirewall内と外部）では、TURN経由の接続が成功していません。私の使い方が悪いのか、環境が厳しすぎる（UDPはブロック、80/TCP, 443/TCPしか通らない）のかは不明です。

## クライアント機能(SDK)

WebSocketを使ったシグナリングがKVS WebRTCの基本機能になります。

### Web (JavaScript)向けのSDK

- シグナリング ... WebSocketによるOffer/Answer SDP の交換、ICE candidateの交換

Web(JavaScript)向けのSDKでは、メディアの扱いやWebRTC通信そのものはブラウザがもっている機能を使うのが前提です。

### C言語用

こちらはシグナリングに加えて(S)RTP通信もサポートしているようです。

- シグナリング ... WebSocketによるOffer/Answer SDP の交換、ICE candidateの交換
- (S)RTP通信 ... RTCPeerConnection相当の通信

メディアの取得や再生は別のライブラリ(GStreamer等)を使う必要があります。




# KVS WebRTCの通信形態

- シグナリングチャネル（ルーム）ごとのシグナリング
    - 1つのMasterが参加
    - 1つ以上複数のViewerが参加可能（現在、最大10まで）
- P2P通信
    - メディアの通信
    - データの通信(DataChannel)
- 双方向、片方向はどちらも可能
    - Master --> Viewer
    - Master <-- Viewer
    - Master <--> Viewer

![kvs_master_viewer.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19403/c082324e-129d-293d-e0c5-3350a3576066.png)


MasterとViewerは1対1で使うことが自然だと思いますが、複数のViewerを1つのMasterに接続することも可能です。一方Viewer間の通信はできないため、3クライアント以上のフルメッシュ構造は取れません。

![kvs_topology.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19403/06fedd80-53df-674b-2595-9808f4cf284f.png)



## シグナリングチャネル

KVS WebRTCでシグナリングをするためには、あらかじめ「シグナリングチャネル」を作成しておく必要があります。これは、WebRTCのアプリケーションで良くある「ルーム」や「チャネル」に相当するものです。
同じ「シグナリングチャネル」に参加しているクライアント同士で通信を行うことができます。

シグナリングチャネルを作成するには、次の方法があります。

- AWSコンソールから作成
- AWSのコマンドラインツール（CLI）から作成
- AWSのSDKからAPIを利用して作成
- REST APIを利用して作成

シグナリングチャネルには「チャネルタイプ」がありますが、現在は"SINGLE_MASTER"のみ指定できます。（もしかしたら将来的には MULTI_MASTER や SFU_MASTER みたいな種類も提供される、かも知れません）

シグナリングチャネルを利用するには、あらかじめIAMでアプリ用のユーザを作っておく必要があります。[（後述）](https://qiita.com/massie_g/items/b6d3513d06a28ba89677#iamによるユーザー作成)


### AWSコンソールからの作成

AWSコンソールにログインし、希望リージョンのAmazon Kinesis Video Streamsのシグナリングチャネルのページから、新規にシグナリングチャネルを作成します。

![kvs_channel_list.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19403/dcbdf570-07c5-6ae4-2c15-10b7053638c5.png)

チャネル名と、必要に応じてTTL（シグナリングメッセージを保持する秒数）、タグを指定して作成します。

![kvs_new_channel.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19403/932027b0-c9bb-6fed-ce09-f05a8746c776.png)


### AWSのコマンドラインツール(CLI)からの作成

こちらの手順に従ってCLIツールをインストールします。

- [Installing the AWS CLI version 2 on Linux or macOS](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2-linux-mac.html)
- [Installing AWS CLI version 2 on Windows](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2-windows.html)

macOSの場合は次の手順です。

```
$ curl "https://d1vvhvl2y92vvt.cloudfront.net/awscli-exe-macos.zip" -o "awscliv2.zip"
$ unzip awscliv2.zip
$sudo ./aws/install
```

/usr/local/bin/aws2 としてインストールされました。

AWSで利用するアクセスキーとシークレットアクセスキーを設定します。ファイルで指定する方法と、環境変数で指定する方法があります。

- [設定ファイルと認証情報ファイルの設定](https://docs.aws.amazon.com/ja_jp/cli/latest/userguide/cli-configure-files.html)
- [AWS CLI を設定する環境変数](https://docs.aws.amazon.com/ja_jp/cli/latest/userguide/cli-configure-envvars.html)

macOSやLinuxでファイルを利用する場合は、~/.aws/credentials に下記のように指定します。

```
$ cat ~/.aws/credentials
[default]
aws_access_key_id=xxxxxx
aws_secret_access_key=xxxxxxxxx/xxxxxxx
```

CLIツールでシグナリングチャネル "channel02"を作るには、次のようにターミナルから操作します。

```
$ aws2 --region "us-west-2" kinesisvideo create-signaling-channel --channel-name channel02 --channel-type SINGLE_MASTER
{
    "ChannelARN": "arn:aws:kinesisvideo:us-west-2:xxxxxxxxxx:channel/channel02/xxxxxxxxx"
}
```

成功すると、上記のようにJSON形式でARNが返ってきます。

### AWSのSDKからAPIを利用した作成

AWSのSDKを用いて、プログラムからシグナリングチャネルを作成することも可能です。例えばNode.jsでは次のようなコードで作成できます。（[SDKのリファレンスはこちら](https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/KinesisVideo.html#createSignalingChannel-property))


```js:create_channel.js
const AWS = require("aws-sdk");
AWS.config.update({ region: 'us-west-2' });
 
AWS.config.getCredentials(function (err) {
 if (err) {
   // credentials not loaded
   console.log(err.stack);
   process.exit(1);
 }
 else {
   console.log("Access key:", AWS.config.credentials.accessKeyId);
   console.log("Secret access key:", AWS.config.credentials.secretAccessKey);
 }
});
 
const kinesisvideo = new AWS.KinesisVideo();
const channel = 'channel03'; 
const params = {
 ChannelName: channel, /* required */
 ChannelType: 'SINGLE_MASTER',
 SingleMasterConfiguration: {
   MessageTtlSeconds: 30
 }
};
 
kinesisvideo.createSignalingChannel(params, function (err, data) {
 if (err) console.log(err, err.stack); // an error occurred
 else console.log(data);           // successful response
});
```

実行には、CLIツールと同様に認証情報を予め設定しておく必要があります。作成に成功すると、次のようにARNが返ってきます。

```
$ node create_channel.js
{ ChannelARN:
   'arn:aws:kinesisvideo:us-west-2:xxxxxx:channel/channel03/xxxxx' }
```



### AWS REST APIによる作成

CLIやSDKを使わず、直接REST APIを呼び出すこともできます。

- [CreateSignalingChannel](https://docs.aws.amazon.com/kinesisvideostreams/latest/dg/API_CreateSignalingChannel.html)

us-west-2 リージョンに "channel04"を作るには、curlを使って次のような操作でできるハズです。が、Authenticationヘッダーに指定する文字列の生成の方法がまだ理解できおらず、成功していません。

```
$ curl -X POST https://kinesisvideo.us-west-2.amazonaws.com/createSignalingChannel \
 -H "Content-Type: application/json" \
 -d '{"ChannelName": "channel04", "ChannelType": "SINGLE_MASTER"}' \
 -H "Authorization: AWS AWSAccessKeyId:Signature"
```
## IAMによるユーザー作成

シグナリングチャネルを利用するには、あらかじめIAM(Identity and Access Managemenent)でユーザを作り、
AmazonKinesisVideoStreamsFullAccess の権限を与えておく必要があります。その際のアクセスキーおよびアクセスシークレットキーを記録しておき、次の利用時に指定します。

- アプリからシグナリングチャネルに接続するとき
- CLIツールや、API経由でシグナリングチャネルを作成するとき

AWSコンソールのIAMで、ユーザーを追加
<img width="1225" alt="IAM_1.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19403/900fe4bb-7a87-40fc-c9d2-3547f43fcea4.png">

ユーザー名を指定し、「プログラムによるアクセス」を指定
<img width="1119" alt="IAM_2.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19403/80bf663c-5b34-1688-a8bd-6a6fe4b7ae50.png">

権限として「AmazonKinesisVideoStreamsFullAccess」を指定
<img width="1069" alt="IAM_3.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19403/85967df7-1a41-ffd3-dc99-8ccfb6e914a5.png">

アクセスキー、シークレットアクセスキーを記録
<img width="1049" alt="IAM_4.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19403/5298295d-3ae3-75f7-a93c-e5baa3b9949e.png">



## Master/Viewerとシグナリング

現状はViewerがSDP Offer側、MasterがSDP Answer側になることが前提になっています。
シグナリングチャネル経由でSDPを送る際に、Offerは必ずMasterに届けられます。ViewerにOfferを送信することはできません。

また、シグナリングチャネルで送信できるのはSDPとICE Candidateのみになっています。アプリケーション独自のメッセージを送ることはできず、よく行われる切断の通知には利用できません。

# KVS WebRTC (for JavaScript)のサンプル

[公式のサンプル](https://github.com/awslabs/amazon-kinesis-video-streams-webrtc-sdk-js)があるのでそれを読めば良いのですが、ちょっと複雑です。なるべく最小限で動くようにしたものをこちらに上げておきます。

- [kvs_webrtc_example](https://github.com/mganeko/kvs_webrtc_example)

## 共通の処理

該当部分のコードはこちら
https://github.com/mganeko/kvs_webrtc_example/blob/master/master.html#L45

- AWS.KinesisVideoインスタンスを生成
    - リージョン、アクセスキー、シークレットアクセスキーを指定
- シグナリングチャネル用のエンドポイントを取得
    - WSS, HTTPSがある
- STUN/TURNサーバー(iceServers)の情報を取得
- KVSWebRTC.SignalingClient インスタンスを生成
    - シグナリングチャネルを利用するためのオブジェクト
    - 生成するときにロールを指定
        - KVSWebRTC.Role.MASTER または KVSWebRTC.Role.VIEWER
    - イベントに対応するハンドラを設定
        - open, close
        - sdpOffer, sdpAnswer
        - iceCandidate

```js:KinesisVideo,SignalingClientインタンス生成部分
    const kinesisVideoClient = new AWS.KinesisVideo({
      region,
      accessKeyId,
      secretAccessKey,
    });

    const getSignalingChannelEndpointResponse = await kinesisVideoClient
      .getSignalingChannelEndpoint({
        ChannelARN: channelARN,
        SingleMasterChannelEndpointConfiguration: {
          Protocols: ['WSS', 'HTTPS'],
          Role: KVSWebRTC.Role.VIEWER, // MASTER or VIEWER
        },
      })
      .promise();
    const endpointsByProtocol = getSignalingChannelEndpointResponse.ResourceEndpointList.reduce((endpoints, endpoint) => {
      endpoints[endpoint.Protocol] = endpoint.ResourceEndpoint;
      return endpoints;
    }, {});

    const kinesisVideoSignalingChannelsClient = new AWS.KinesisVideoSignalingChannels({
      region,
      accessKeyId,
      secretAccessKey,
      endpoint: endpointsByProtocol.HTTPS,
    });

    const getIceServerConfigResponse = await kinesisVideoSignalingChannelsClient
      .getIceServerConfig({
        ChannelARN: channelARN,
      })
      .promise();
    const iceServers = [
      { urls: `stun:stun.kinesisvideo.${region}.amazonaws.com:443` }
    ];
    getIceServerConfigResponse.IceServerList.forEach(iceServer =>
      iceServers.push({
        urls: iceServer.Uris,
        username: iceServer.Username,
        credential: iceServer.Password,
      }),
    );

    const signalingClient = new KVSWebRTC.SignalingClient({
      channelARN,
      channelEndpoint: endpointsByProtocol.WSS,
      clientId,
      role: KVSWebRTC.Role.VIEWER,
      region,
      credentials: {
        accessKeyId,
        secretAccessKey,
      },
    });
```

## Viewerの処理

コードはこちら
https://github.com/mganeko/kvs_webrtc_example/blob/master/viewer.html

- 自分のClientIDを決定する
    - シグナリングチャネル内で一意になるように
- シグナリングチャネルに接続
    - KVSWebRTC.SignalingClient.open()を呼ぶ
    - openイベントが発生したら、処理を続行する
- カメラ/マイク等のメディアを取得
- RTCPeerConnectionのインスタンスを生成
    - addTrack()でメディアを追加
- RTCPeerConnection.createOffer()でoffer SDPを生成
- KVSWebRTC.SignalingClient.sendSdpOffer()でoffer SDPを（Masterに）送信
    - あて先は指定できない。自動的にMasterに送信される
- Answer SDPを受け取ったら、RTCPeerConnection.setRemoteDescription()で設定
    - KVSWebRTC.SignalingClien の sdpAnswer イベント
- RTCPeerConnection で iceCandidate が生成されたら、Masterに送る
    - KVSWebRTC.SignalingClient.sendIceCandidate() で送信
    - あて先を省略すると、Masterに送信される
- iceCandidate を受け取ったら、RTCPeerConnection.addIceCandidate()で追加
    - KVSWebRTC.SignalingClien の iceCandidate イベント

```js:Offerを生成、送信する部分の抜粋
      // Create an SDP offer and send it to the master
      const offer = await peer.createOffer({
        offerToReceiveAudio: true,
        offerToReceiveVideo: true,
      });
      await peer.setLocalDescription(offer);
      console.log('send offer');
      signalingClient.sendSdpOffer(peer.localDescription);
```

```js:Answerを受け取った時の処理の抜粋
    // When the SDP answer is received back from the master, add it to the peer connection.
    signalingClient.on('sdpAnswer', async answer => {
      console.log('got answer');
      await globalKVS.peerConnection.setRemoteDescription(answer);
    });
```


## Masterの処理

コードはこちら
https://github.com/mganeko/kvs_webrtc_example/blob/master/master.html

- カメラ/マイク等のメディアを取得
- シグナリングチャネルに接続
    - KVSWebRTC.SignalingClient.open()を呼ぶ
    - ※Viewerよりも先に接続しておくと、処理がスムーズ
- Offer SDPとclientIDを受け取ったら、対応するRTCPeerConnectionを生成
    - KVSWebRTC.SignalingClien の sdpOffer イベント
    - Viewer側のclientIDを保持
    - 複数Viewerの場合は、clientIDと対応付けてRTCPeerConnectionのインスタンスを保持
    - RTCPeerConnection.addTrack()でメディアを追加
    - RTCPeerConnection.setRemoteDescription()でOfferを設定
- RTCPeerConnection.createAnswer()でanswer SDPを生成
- KVSWebRTC.SignalingClient.sendSdpAnswer()でanswer SDPをViewerに送信
    - あて先として、通信相手となるViewerのclientIDを指定
- RTCPeerConnection で iceCandidate が生成されたら、Viewerに送る
    - KVSWebRTC.SignalingClient.sendIceCandidate() で送信
    - あて先として、ViewerのclientIDを指定
- iceCandidate を受け取ったら、RTCPeerConnection.addIceCandidate()で追加
    - KVSWebRTC.SignalingClien の iceCandidate イベント

```js:Offer受信→Answer送信まで
    signalingClient.on('sdpOffer', async (offer, remoteClientId) => {
      // --- prepare new peer ---
      let peer = prepareNewPeer(globalKVS.iceServers, globalKVS.signalingClient, remoteClientId); // 関数内でRTCPeerConnetionを生成
      if (localStream) {
        localStream.getTracks().forEach(track => peer.addTrack(track, localStream));
      }
      else {
        // --- recvonly ---
        if (peer.addTransceiver) {
          peer.addTransceiver('video', { direction: 'recvonly' });
          peer.addTransceiver('audio', { direction: 'recvonly' });
        }
      }

      await peer.setRemoteDescription(offer);
      await peer.setLocalDescription(
        await peer.createAnswer({
          offerToReceiveAudio: true,
          offerToReceiveVideo: true,
        }),
      );

      // send back answer
      signalingClient.sendSdpAnswer(peer.localDescription, remoteClientId);
    });
```

# KVS WebRTC の利用料金

こちらのモデルケースが参考になります。

- https://aws.amazon.com/jp/kinesis/video-streams/pricing/

> 米国東部における WebRTC 料金は、アクティブなシグナリングチャネルが月額で 0.03 USD、シグナリングメッセージ 100 万件あたりで 2.25 USD、1,000 TURN ストリーミング時間 (分) あたりで 0.12 USD となっています。

> アクティブなシグナリングチャネル = 100 x (0.03 USD/月) = 3.0 USD
シグナリングメッセージ = 100 ユーザー x 1,500 シグナリングメッセージ/1,000,000 x (2.25 USD/100 万シグナリングメッセージ) = 0.34 USD

> TURN ストリーミング時間 (分) = 100 ユーザー x 400 TURN ストリーミング時間 (分) x (0.12 USD/1,000 TURN ストリーミング時間 (分)) = 4.8 USD

> 合計 = 8.14 USD

> 注意: インターネット経由で TURN ストリーミングから AWS 外の送信先にデータを送信する場合は、AWS の標準のデータ転送料金がかかります。

AWSのデータ転送料金は次の概算になりました。

- メディア通信が1Mbpsと仮定
- 40000 TURNストリーミング時間/月 → 300GB/月
- 転送料金 27 USD/月 (299GB分、us-west-2の場合）


# KVS WebRTC の感想

少し触っただけですが、個人的な感想をあげてみます。

- 現状はビデオチャットのようなコミュニケーションアプリを作るためのものではない
    - 1対1なら可能だが、Master（主催者）とViewer（参加者）がはっきり区別される
    - n対nは不可能
    - ただし今後SFUのようなサーバー側の仕組みが登場する可能性はある
- 1対少数の配信は可能だが、大規模配信はできない
    - それは(WebRTCではない)Kinesis Video Streams を使うのが筋が良さそう

個人的にはビデオチャットのプラットフォームとして利用するなら、他のクラウドサービスを利用する方がスムーズと感じました。（登場したばかりなので、今後どう発展するかは未知数ですが）
どちらかというと、C言語のSDKが提供されていることからも分かる通り、組み込みデバイスからの映像やセンサーデーターをモニターするような、IoT的な使い方がマッチしそうです。