Amazon Kinesis Video StreamsのWebRTCとAlexaを組み合わせてドアホンみたいのを作る

Alexa Skills Kitには[カメラスキル](https://developer.amazon.com/ja-JP/docs/alexa/smarthome/build-smart-home-camera-skills.html)というものがあり、これに[対応したデバイス](https://www.amazon.co.jp/b/ref=s9_acss_bw_cg_SHtoprt_2d1_cta_w?node=5364384051&pf_rd_m=A3P5ROKL5A1OLE&pf_rd_s=merchandised-search-11&pf_rd_r=NRJZ8SGZVRPSP8ERJMTK&pf_rd_t=101&pf_rd_p=941344a9-0109-4a06-a924-b6a756b7ec9e&pf_rd_i=5364379051)があります。
このカメラスキルは、RTSPかWebRTCが使うことが出来、前回書いた記事「[Raspberry PiのカメラでAmazon Kinesis Video StreamsのWebRTCを使ってみる](https://qiita.com/sparkgene/items/3e3b71a7a211048e55bf)」のようにAmazon Kinesis Video StreamsがWebRTCに対応したので、今回はWebRTCで試してみたいと思います。

## デモ動画

[![Alexa Camera skill sample](http://img.youtube.com/vi/2SupeLYiV5Q/3.jpg)](http://www.youtube.com/watch?v=2SupeLYiV5Q)

## 事前準備

### スマートホームスキルの作り方

こちらに詳しくあるので参考にしてください。
[スマートホームスキルの作成手順](https://developer.amazon.com/ja-JP/docs/alexa/smarthome/steps-to-build-a-smart-home-skill.html)

#### アカウントリンキング

スマートホームスキルではアカウントリンキングが必要となるので、詳しくはこちらのドキュメントを参考にしてください。
[アカウントリンクとは](https://developer.amazon.com/ja-JP/docs/alexa/account-linking/understand-account-linking.html)

---

ここまでで、以下のものについて終わっている前提で進めます。（作り方は上で紹介したリンクと、[前回の記事](https://qiita.com/sparkgene/items/3e3b71a7a211048e55bf)を参考にしてください）

- スマートホームスキル用のLambdaの作成(中身は空でOK)
- スマートホームスキルの作成
- Amazon Kinesis Video StreamsのWebRTC Masterが動いているRaspberry Pi

## WebRTCサンプルの修正

少しハマったのが、 trickle ICEがAlexa デバイスでは対応していないと[ここに](https://awslabs.github.io/amazon-kinesis-video-streams-webrtc-sdk-js/examples/index.html)書かれていたのですが、GStreamerのサンプルだとデフォルトで有効となっているため、それを利用しないように修正が必要でした。

ここの`createSampleConfiguration `の3つ目の引数は`TRUE`ではなく`NULL`に修正して再度ビルド。
https://github.com/awslabs/amazon-kinesis-video-streams-webrtc-sdk-c/blob/master/samples/kvsWebRTCClientMasterGstreamerSample.c#L291

## Lambdaの実装

WebRTCのAlexa Skillでは、[Discovery](https://developer.amazon.com/ja-JP/docs/alexa/device-apis/alexa-rtcsessioncontroller.html#discovery)と[こちら](https://developer.amazon.com/ja-JP/docs/alexa/device-apis/alexa-rtcsessioncontroller.html#directives)に書かれているように3つのディレクティブに対応する必要があります。

- InitiateSessionWithOffer
- SessionConnected
  - ただし、これは毎回必ず送られてくるわけではないと書かれていたので、今回は実装していない
- SessionDisconnected

### Lambdaのサンプルソース

このサンプルをスマートホームスキルの作成時に作ったLambdaに貼り付けます。
サンプルのため、シグナリングチャンネルは固定にしています。
RaspPi側のサンプルを実行すると、シグナリングチャンネルが作成されているので、Kinesis Video Streamsのマネージメントコンソールから作成されたシグナリングチャネルの詳細画面を開きARNをコピーして、サンプルソースのCHANNEL_ARNに指定します。

```python
import base64
import boto3
import json
import logging
import time
import uuid

CHANNEL_ARN = "<<WebRTCのシグナリングチャネルのARNを指定する>>"

logger = logging.getLogger()
logger.setLevel(logging.INFO)

def get_uuid():
    return str(uuid.uuid4())

def lambda_handler(request, context):
    logger.info("Request ->")
    logger.info(json.dumps(request, indent=2, sort_keys=True))

    if request["directive"]["header"]["name"] == "Discover":
        response = handle_discovery(request)
    elif request["directive"]["header"]["name"] == "InitiateSessionWithOffer":
        response = handle_initiate_session(request)
    elif request["directive"]["header"]["name"] == "SessionDisconnected":
        response = handle_session_disconnected(request)
    else:
        raise Exception("not implemented")

    logger.info("Response ->")
    logger.info(json.dumps(response, indent=2, sort_keys=True))

    return response

def handle_discovery(request):

    response = {
        "event": {
            "header": {
                "namespace":"Alexa.Discovery",
                "name":"Discover.Response",
                "payloadVersion": "3",
                "messageId": get_uuid()
            },
            "payload":{
                "endpoints":[
                    {
                        "endpointId": "kvs_webrtc_test",
                        "manufacturerName": "sparkgene.com",
                        "description": "test kvs webrtc camera",
                        "friendlyName": "玄関のカメラ",
                        "displayCategories": [ "CAMERA" ],
                        "cookie": {},
                        "capabilities": [
                            {
                                "type": "AlexaInterface",
                                "interface": "Alexa.RTCSessionController",
                                "version": "3",
                                "configuration": {
                                    "isFullDuplexAudioSupported": True
                                }
                            },
                            {
                                "type": "AlexaInterface",
                                "interface": "Alexa",
                                "version": "3"
                            }
                        ]
                    }
                ]
            }
        }
    }

    return response

def handle_initiate_session(request):
    kv_client = boto3.client('kinesisvideo', region_name="ap-northeast-1")
    res = kv_client.get_signaling_channel_endpoint(
        ChannelARN=CHANNEL_ARN,
        SingleMasterChannelEndpointConfiguration={
            "Protocols": [
                "HTTPS",
            ],
            "Role": "VIEWER"
        }
    )
    kvs_client = boto3.client('kinesis-video-signaling',
                            region_name="ap-northeast-1",
                            endpoint_url=res["ResourceEndpointList"][0]["ResourceEndpoint"])
    offer = {
        "type":"offer",
        "sdp": request["directive"]["payload"]["offer"]["value"]
    }

    answer_res = kvs_client.send_alexa_offer_to_master(
            ChannelARN=CHANNEL_ARN,
            SenderClientId="ProducerMaster",
            MessagePayload=base64.b64encode( json.dumps(offer).encode() ).decode()
        )
#    logger.debug(answer_res)
    if "Answer" in answer_res:
        sdp_answer = json.loads(base64.b64decode( answer_res["Answer"] ).decode('utf-8'))["sdp"]
    else:
        sdp_answer = None
    response = {
        "event": {
            "header": {
                "namespace": "Alexa.RTCSessionController",
                "name": "AnswerGeneratedForSession",
                "messageId": get_uuid(),
                "correlationToken": request["directive"]["header"]["correlationToken"],
                "payloadVersion": "3"
            },
            "endpoint": {
                "scope": {
                    "type": "BearerToken",
                    "token": request["directive"]["endpoint"]["scope"]["token"]
                },
                "endpointId": request["directive"]["endpoint"]["endpointId"]
            },
            "payload": {
                "answer": {
                    "format" : "SDP",
                    "value" : sdp_answer
                }
            }
        }
    }
#    logger.info(response)
    return response

def handle_session_disconnected(request):
    response = {
        "event": {
            "header": {
                "namespace": "Alexa.RTCSessionController",
                "name": "SessionDisconnected",
                "messageId": get_uuid(),
                "correlationToken": request["directive"]["header"]["correlationToken"],
                "payloadVersion": "3"
            },
            "endpoint": {
                "scope": {
                    "type": "BearerToken",
                    "token": request["directive"]["endpoint"]["scope"]["token"]
                },
                "endpointId": request["directive"]["endpoint"]["endpointId"]
            },
            "payload": {
                "sessionId": request["directive"]["payload"]["sessionId"]
            }
        }
    }

#    logger.info(response)
    return response
```

## 動作確認

最初に、Raspberry Pi上のサンプルを実行しておきます。

次に、Alexaのアカウントで作成したスマートホームスキルを有効にし、自分のEchoデバイスから呼び出せるようにします。

このサンプルソースを使うと、Discoveryの結果として`event.payload.endpoints.friendlyName`に`玄関のカメラ`を指定しているので、スキルを有効にしてデバイスの検索が終わると、 「アレクサ、玄関のカメラを見せて」と話しかける事によって、このスキルを読み出すことが出来ます。

スキルが呼ばれると(handle_initiate_sessionの処理)、Raspberry Pi上で実行済みのWebRTC Masterに対してofferが送られ、そのanswerをAlexa側に返すことで、Echo Showなどの画面があるデバイスと、Raspberry Piの接続が確立され、Raspberry Piにつながっているカメラからの映像をEcho側で確認することが出来ます。

実際試してみると、Raspberry Piだとエンコーディングが重いようで、遅延が結構ありブロックノイズが出るような映像となってしまいます。
そこで、カメラの画像サイズをそのまま送らずに、解像度を下げて(1280x720 -> 320x180)に落としています。

具体的には、 
https://github.com/awslabs/amazon-kinesis-video-streams-webrtc-sdk-c/blob/master/samples/kvsWebRTCClientMasterGstreamerSample.c#L132
ここの行を以下のような内容に変更しています。(ついでに時間も表示)

```
"autovideosrc num-buffers=1000 ! queue ! videoconvert ! clockoverlay time-format="%Y-%m-%d %H:%M:%S" font-desc="Sans 28" draw-outline=false ! videoscale ! video/x-raw,width=320,height=180,framerate=30/1 ! x264enc bframes=0 speed-preset=veryfast key-int-max=30 bitrate=512 ! "
```

## まとめ

マネージドサービスを使うので運用がかなり楽になりそうですね。
あとは実際に作り込むとしたら、Kinesis Video Streamsで使うクレデンシャルの取得方法や、音声の利用など作り込むと、より実用的なものになるんじゃないかと思います。

## 免責

発言内容は個人的な意見であり、所属する企業を代表するものではありません。
掲載しているソースコードは、サンプルレベルの物ですので動作を保証するものではありません。

## 参考資料

- boto3 KinesisVideo
  - https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/kinesisvideo.html
- boto3 KinesisVideoSignalingChannels
  - https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/kinesis-video-signaling.html
- スマートホームカメラスキル
  - https://developer.amazon.com/ja-JP/docs/alexa/smarthome/build-smart-home-camera-skills.html
- スマートホームスキルの作成手順
  - https://developer.amazon.com/ja-JP/docs/alexa/smarthome/steps-to-build-a-smart-home-skill.html
- アカウントリンクとは
  - https://developer.amazon.com/ja-JP/docs/alexa/account-linking/understand-account-linking.html