Alexa Skills Kitにはカメラスキルというものがあり、これに対応したデバイスがあります。
このカメラスキルは、RTSPかWebRTCが使うことが出来、前回書いた記事「Raspberry PiのカメラでAmazon Kinesis Video StreamsのWebRTCを使ってみる」のようにAmazon Kinesis Video StreamsがWebRTCに対応したので、今回はWebRTCで試してみたいと思います。
デモ動画
事前準備
スマートホームスキルの作り方
こちらに詳しくあるので参考にしてください。
スマートホームスキルの作成手順
アカウントリンキング
スマートホームスキルではアカウントリンキングが必要となるので、詳しくはこちらのドキュメントを参考にしてください。
アカウントリンクとは
ここまでで、以下のものについて終わっている前提で進めます。(作り方は上で紹介したリンクと、前回の記事を参考にしてください)
- スマートホームスキル用の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で使うクレデンシャルの取得方法や、音声の利用など作り込むと、より実用的なものになるんじゃないかと思います。
免責
発言内容は個人的な意見であり、所属する企業を代表するものではありません。
掲載しているソースコードは、サンプルレベルの物ですので動作を保証するものではありません。
参考資料
- boto3 KinesisVideo
- boto3 KinesisVideoSignalingChannels
- スマートホームカメラスキル
- スマートホームスキルの作成手順
- アカウントリンクとは