Help us understand the problem. What is going on with this article?

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

Alexa Skills Kitにはカメラスキルというものがあり、これに対応したデバイスがあります。
このカメラスキルは、RTSPかWebRTCが使うことが出来、前回書いた記事「Raspberry PiのカメラでAmazon Kinesis Video StreamsのWebRTCを使ってみる」のようにAmazon Kinesis Video StreamsがWebRTCに対応したので、今回はWebRTCで試してみたいと思います。

デモ動画

Alexa Camera skill sample

事前準備

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

こちらに詳しくあるので参考にしてください。
スマートホームスキルの作成手順

アカウントリンキング

スマートホームスキルではアカウントリンキングが必要となるので、詳しくはこちらのドキュメントを参考にしてください。
アカウントリンクとは


ここまでで、以下のものについて終わっている前提で進めます。(作り方は上で紹介したリンクと、前回の記事を参考にしてください)

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

WebRTCサンプルの修正

少しハマったのが、 trickle ICEがAlexa デバイスでは対応していないとここに書かれていたのですが、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こちらに書かれているように3つのディレクティブに対応する必要があります。

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

Lambdaのサンプルソース

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

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で使うクレデンシャルの取得方法や、音声の利用など作り込むと、より実用的なものになるんじゃないかと思います。

免責

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

参考資料

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした