開発者コンソール

手順8: スキルをテストしてCloudWatchでログを確認する


手順8: スキルをテストしてCloudWatchでログを確認する

スキルのテストを実行して、AlexaからLambda関数に送信されるディレクティブを確認します。AlexaからLambda関数に送信されたディレクティブをCloudWatchで一覧表示して、やり取りを確認できます。Lambda関数がアプリに送信するレスポンスを確認するには、Android StudioでLogcatを開きます。

adbを使用してFire TVに接続する方法

ここまでの手順で、セキュリティプロファイルの構成が完了し、準備が整いました。この手順では、Fire TV対応アプリを実行し、すべてが正常に機能して承認が成功するかを確認します。Fire TVでアプリを実行する方法の詳細については、adbを使用してFire TVに接続する方法を参照してください。ここでは、adbの接続手順を簡単に説明します。

  1. Fire TVで、[設定] > [My Fire TV] > [開発者オプション] の順にクリックします。[ADBデバッグ][不明ソースからのアプリ] を有効にします(注: Fire TVの一部のモデルでは、[My Fire TV] ではなく [端末] と表示されます)。
  2. Fire TVで、[設定] > [My Fire TV] > [バージョン情報] > [ネットワーク] の順にクリックします。この画面に表示されるネットワークとIPアドレスをメモします。
  3. コンピューターがFire TVと同じWi-Fiネットワークに接続されていることを確認します。VPNに接続している場合は、接続を解除します。
  4. ターミナルウィンドウを開き、次のコマンドを実行します。

    adb connect <ipaddress>:5555
    

    次に例を示します。

    adb connect 192.168.1.23:5555
    

    Fire TVに接続できない場合は、同じWi-Fiネットワークに接続していることと、VPNに接続していないことを確認してください。それでも接続できない場合は、ポートで競合が発生している可能性があるため、コンピューターとFire TVを再起動してください([設定] > [My Fire TV] > [再起動])。

  5. adbを使用してFire TVに正常に接続したら、Android Studioを開きます。app構成を選択し、アプリを実行するFire TVデバイス(「Amazon AFTMM」など)も選択します(「AFTMM」は、Fire TVのビルドモデルを意味します)。

    Android Studioでの構成の選択
    Android Studioでの構成の選択
  6. [Run App] ボタン [Run App] をクリックしてアプリを実行します。

    まだFire TVデバイスを選択していない場合は、選択するよう求められます。Fire TVデバイスを選択し、[OK] をクリックします。

    対象のFire TVデバイスの選択
    対象のFire TVデバイスの選択

    アプリのビルドと実行中に、Android Studioの下部にある [Logcat] タブをクリックしてログメッセージをモニタリングします。アプリが実行されていれば、セキュリティプロファイルは正しく構成されていることになります。サンプルアプリのホーム画面は次のようになります。

    一般に利用可能なビデオが統合されたサンプルアプリ
    一般に利用可能なビデオが統合されたサンプルアプリ

この手順で問題が発生した場合は、次の方法でトラブルシューティングを行います。

  • エラーメッセージ(エントリ名の競合など)が表示された場合は、[Build] > [Rebuild Project] の順にクリックし、古いアーティファクトが消去されたことを確認してから、アプリを再度実行します。
  • インストールに競合が生じた場合は、Fire TVに同じパッケージ名のアプリがないことを確認します。必要に応じてアプリをアンインストールします([設定] > [アプリケーション] > [インストール済みアプリケーションを管理])。
  • アプリの画面が空白になる場合やその他の問題がある場合は、Logcatを開き、「Error」でフィルタリングします。 「Invalid API key」というエラーが表示された場合は、以前に構成したカスタムfiretv署名鍵を使用してアプリに署名していることを確認します。

テスト発話による確認

Fire TVのフォアグラウンドで実行中のアプリで、「アレクサ、スーパーマンを見せて」とEchoに話しかけます。tms(Gracenote)カタログが搭載されたサンプルアプリを使用している場合は、そのほかの映画やTV番組のタイトルでも構いません。

初めてこのように話しかけると、Alexaは「<ビデオスキル>でアレクサが有効になりました...」と応答します。 これは、ビデオスキルとFire TV対応アプリを自動的にペアリングしたことを示しています。次に、Alexaは「<ビデオスキル>からスーパーマンを取得しています」と応答します。 サンプルビデオの再生が開始されます。

サンプルアプリでは、すべての発話が同じサンプルビデオにマッピングされています。

Alexaは、(前の手順1でEchoをFire TVにリンクした場合とは異なり)「Fire TVから」ではなく、「ビデオスキルから」リスティングを取得します。

CloudWatchでのログの確認

CloudWatchを使用して、ビデオスキルから受信したイベントとLambda関数によって書き込まれたログを確認できます。これにより、さまざまなコンポーネント間で何が起きているかをより正確に把握できます。CloudWatchでログを表示するには、次の手順を実行します。

  1. AWSで、[サービス] をクリックし、CloudWatchに移動します(必要に応じて検索してください)。
  2. 左側のサイドバーで [ログ] > [ロググループ] の順にクリックします。
  3. 作成したLambda関数をクリックします。

    現在のリージョンに関連するLambda関数のみが表示されます。したがって、Lambda関数が表示されない場合は、現在のリージョンが正しいことを確認してください。

  4. 関数のログのリストが表示され、最新のロググループが上部に表示されます(Echoに向けてコマンドの発話を開始するまで、ログは表示されません)。

Lambdaログを見やすくするには、[テキスト] ラジオボタンをクリックします。

Lambda関数に移動してCloudWatchログにアクセスすることもできます。次のスクリーンショットに示すように、[モニタリング] タブをクリックし、[CloudWatchのログを表示] ボタンをクリックします。

Lambda関数画面にある、CloudWatchログへのリンク
Lambda関数画面にある、CloudWatchログへのリンク

ログの詳細については、以下のCloudWatchに表示される全体的なやり取りについてを参照してください。

発話のコンテキストと暗黙的/明示的なターゲット指定

Cloudwatchでのさまざまなテストケースや段階的説明を始める前に、発話を指定できるさまざまなコンテキストに注目してください。発話を使用できる主なコンテキストには次の2つがあります。

  • 暗黙的なターゲット指定: コンテンツプロバイダーやアプリ名を指定しない音声コマンド。例:「スーパーマンを見せて」
  • 明示的なターゲット指定: コンテンツプロバイダーやアプリ名を指定する音声コマンド。例:「Streamzでスーパーマンを見せて」

さまざまな発話をテストする場合、明示的なターゲット指定は、スキルをライブアプリテストにプッシュした後でなければ使用できないことに注意してください。Android Studioでアプリをサイドロードする場合、Alexaはスキルの明示的なターゲット指定を処理できません。そのため、フォアグラウンドになるようにアプリを開く必要があります。

アプリが(バックグラウンドではなく)フォアグラウンドにある場合、「スーパーマンを見せて」と発話すると、Alexaはすべてのカタログではなく、フォアグラウンドにあるカタログのアプリから一致するものを探します。アプリがフォアグラウンドにある間にコマンドを発話することは、明示的なターゲット指定と同等ですが、Android Studioからローカルにアプリをサイドロードしている場合でもサポートされます。

Alexa、Lambda、Fire TV対応アプリ間のインタラクションモデルについて

これで必要なものがすべて揃ったので、ここで、これらのコンポーネントの連携について詳しく説明します。Alexa搭載デバイスに話しかけたフレーズは、(クラウドにあるAlexa自然言語サービスを通じて)意味が解析され、ディレクティブに変換されます。

ディレクティブは、ビデオスキルAPIを介してAlexaからLambda関数に送信されて処理される情報ブロック(JSON形式)です。Fire TVで使用されるディレクティブは、APIリファレンスのセクションに記載されています(VSK Fire TVのAPIは、VSK Echo Showで使用されるAPIとは異なります)。

AlexaからLambda関数に送信されたディレクティブはCloudWatchで表示できます。CloudWatchは、Lambda(およびその他のサービス)のモニタリングデータと運用データをログ、指標、イベントの形で収集するAWSサービスです。Lambda関数は、Alexaからディレクティブを受け取るたびに、ディレクティブやその他のイベントをログに記録できます。これらのログはCloudWatchで確認できます。

ビデオスキルを構成する際には、AlexaからLambda関数に送信されるディレクティブを理解し、想定されるレスポンスをアプリに実装することが重要です。

また、Lambda関数は、ディレクティブの処理が成功したことを示す簡単な確認レスポンスをAlexaに返す必要があります。ただし、Lambda関数は、Alexaにレスポンスは返しますが、ユーザーに向けたカスタム音声フィードバックを提供することはできません。ユーザーとのやり取りは、Alexaが処理します。Lambda関数が実行できるアクションは、APIリファレンスで指定されているアクションだけです。

Lambda関数は、Amazon Device Messaging(ADM)を通じてFire TV対応アプリに情報を伝達します。サンプルアプリのLambda関数は、Alexaから受け取ったのと同じディレクティブをアプリにプッシュするだけです(処理後や別のデータソースの問い合わせ後など、必要に応じて情報をアプリに送信するようにLambda関数をカスタマイズできます)。 ADMからのメッセージを調べ、カタログと一致するメディア識別子を見つけて再生するロジックが、サンプルアプリには含まれています。受信メッセージは [Logcat] ペインで確認できます。このペインでは、検索したタイトルでフィルタリングすることができます。

CloudWatchに表示される「アイアンマン2を検索して」の全体的なやり取りについて

以下のテスト発話に進む前に、「アイアンマン2を検索して」という発話に関するサンプルのCloudWatchログについて詳しく説明しましょう。 「検索して」というキーワードを使用すると、AlexaはSearchAndDisplayResultsディレクティブを送信するよう求められます(「見せて」や「再生して」というキーワードを使用した場合は、SearchAndPlayディレクティブになります)。 サンプルアプリを使用している場合、CloudWatchログは次のようになります(あくまでもサンプルLambda関数の例です)。各ログメッセージの意味については、インラインコメントを参照してください。読みやすくするために、リクエストIDとタイムスタンプの一部が省略されています。

START RequestId: b9e15474 Version: $LATEST

リクエストが開始されます。

2020-03-18T04:28:49 b9e15474 INFO Lambda was invoked by Alexa

ログは、Alexaのリクエストイベントから始まります。Lambda関数が(この場合は「トリガー」として機能するビデオスキルによって)呼び出されると、AWS Lambdaはhandler関数を呼び出してコードの実行を開始します。ランタイムでは、次のようにhandlerメソッドに3つの引数を渡します。

exports.handler = (event, context, callback) =>

各引数の内容は次のとおりです。

  • event: 呼び出し元の情報が含まれています。この場合、呼び出し元はAlexaディレクティブです。Alexaでは、Invokeを呼び出すときに、JSON形式の文字列としてこのディレクティブを渡します。
  • context: 呼び出し、関数、実行環境に関する情報が含まれています。
  • callback: 3番目の引数のcallbackは、レスポンスを送信するために非同期関数で呼び出すことができる関数です。

Lambda関数とハンドラーの詳細については、Node.jsのAWS Lambda関数ハンドラーを参照してください。

2020-03-18T04:28:49 b9e15474 INFO Context: …

{
    "callbackWaitsForEmptyEventLoop": true,
    "functionVersion": "$LATEST",
    "functionName": "zombie_streamz_lambda",
    "memoryLimitInMB": "128",
    "logGroupName": "/aws/lambda/zombie_streamz_lambda",
    "logStreamName": "2020/03/10/[$LATEST]34de0361da2c4450a97f2ba2c674fcb0",
    "invokedFunctionArn": "arn:aws:lambda:us-east-1:458179560631:function:zombie_streamz_lambda",
    "awsRequestId": "980c8eef"
}

contextを確認できるように、サンプルのLambda関数では、この情報をログに記録しています。

console.log("Context: " + JSON.stringify(context, null, 2));

contextには、Lambda関数を呼び出したイベントに関する情報が含まれています。コンテキスト情報は、呼び出されたLambda ARNとそれを呼び出したトリガー(ビデオスキル、つまりinvokedFunctionArn)を示すこと以外は、あまり役に立ちません。不要と考えられがちですが、サンプルアプリではコンテキストをログに記録する処理がコメントアウトされていないため、アプリに渡される情報を確認できます。ビデオスキルは、このLambda関数に接続したスマートホーム用トリガーとして機能します。

2020-03-18T04:28:49 b9e15474 INFO Directive received from Alexa: SearchAndDisplayResults

{
    "directive": {
        "payload": {
            "entities": [
                {
                    "type": "Video",
                    "uri": "entity://provider/program/amzn1.p11cat.merged-video.ae637959-7f3c-5c5e-8baf-ce730ed9beb6",
                    "value": "アイアンマン2",
                    "externalIds": {
                        "ENTITY_ID": "amzn1.p11cat.merged-video.ae637959-7f3c-5c5e-8baf-ce730ed9beb6",
                        "tms": "MV002527780000"
                    }
                },
                {
                    "type": "Video",
                    "uri": "entity://provider/program/amzn1.p11cat.merged-video.b4e77dca-954a-555d-ba2b-da5e0ca9e903",
                    "value": "アイアンマン2",
                    "externalIds": {
                        "ENTITY_ID": "amzn1.p11cat.merged-video.b4e77dca-954a-555d-ba2b-da5e0ca9e903"
                    }
                },
                {
                    "type": "Video",
                    "uri": "entity://provider/program/amzn1.p11cat.merged-video.0e3a1fb7-b973-52c0-98ef-cc4765fb9238",
                    "value": "アイアンマン2",
                    "externalIds": {
                        "ENTITY_ID": "amzn1.p11cat.merged-video.0e3a1fb7-b973-52c0-98ef-cc4765fb9238"
                    }
                },
                {
                    "type": "Video",
                    "uri": "entity://provider/program/amzn1.p11cat.merged-video.6beb3014-436d-5138-b0c0-bcf819cfadef",
                    "value": "アイアンマン2",
                    "externalIds": {
                        "ENTITY_ID": "amzn1.p11cat.merged-video.6beb3014-436d-5138-b0c0-bcf819cfadef"
                    }
                },
                {
                    "type": "Video",
                    "uri": "entity://provider/program/amzn1.p11cat.merged-video.80a7eda2-3e16-55de-b56b-3136f5dc0e74",
                    "value": "アイアンマン2",
                    "externalIds": {
                        "ENTITY_ID": "amzn1.p11cat.merged-video.80a7eda2-3e16-55de-b56b-3136f5dc0e74",
                        "tms": "MV006154660000"
                    }
                }
            ],
            "searchText": {
                "transcribed": "アイアンマンツー"
            }
        },
        "header": {
            "payloadVersion": "3",
            "messageId": "f5cbe32c-72fc-4816-892d-92f695c125a4",
            "namespace": "Alexa.RemoteVideoPlayer",
            "name": "SearchAndDisplayResults",
            "correlationToken": "7e8bdcef-1aff-452e-b405-5a877d712755"
        },
        "endpoint": {
            "endpointId": "432521e3d404c9e3##amzn1-ask-skill-c7dc9021-ba50-4aa9-bbad-7b9bcd2e94c3##development##com-tomjoht-vskfiretv",
            "cookie": {
                "deviceType": "AKPGW064GI9HE",
                "VSKClientVersion": "1.4.6",
                "appPackageName": "com.tomjoht.vskfiretv",
                "deviceId": "G070VM1694861D9N",
                "appName": "VSKFireTV",
                "applicationInstanceId": "amzn1.adm-registration.v3.Y29tLmFtYXpvbi5EZXZpY2VNZXNzYWdpbmcuUmVnaXN0cmF0aW9uSWRFbmNyeXB0aW9uS2V5ITEheFgvdzhUODl1SGc0WU96N0svSlZCVWFXVnBqUkNhdnJuWHlXWFBucUtvMHVLd0g2M1lkVEhZcjNRZ3J2K3A5QVAzdzhYbEkzZGYrY0t2Q0NQSWVQSEtEMDEvbGRhYUgrZVY4b1JRTXFMQndpYUZXajRXZlN3MEwzQ2ZaWkh2d0tUd3c1cFlYKys5WFdxZkVtRkg4RTQzcXhDMmxJYkZmd3dJeVVObStnWU00VU9GVmVwQTBVYityTFRRZHlYanNGUy9RTEhDbDhMUm1URmovZldsazF6WGRnZUV1V1pGWjkrcXYzR0lRcFV1MlpSYS9yZFA0aDEwbU5EcG1ndDE4eVJPRUg1bUtIQWVpUkZ3UlNwSlljcG9XSE96R3ZtMWJvZys2U1QyWUxvM3l0Nk4xNWI0QmFvMjBLcjNuZEl5UHpvNFBSUUs5aHZ1VUdTZ1psdUlFeVVsdzBZajFnb1ZZKzBLL3NGN2pJQnJjPSFmdk1qcTFuM0tOcVBOVzdyOGhybkp3PT0"
            },
            "scope": {
                "token": null,
                "type": "BearerToken"
            }
        }
    }
}

このシナリオでは、コマンド 「アイアンマン2を検索して」を発話しました。その結果、SearchAndDisplayResultsが返されます。この発話はクラウドでAlexaによって解析され、SearchAndPlayディレクティブが生成されました。payloadには検索用語がvalueとして含まれています。ビデオスキルAPIを通じて、AlexaからLambda関数にディレクティブが送信されます。

Alexaによって生成されたSearchAndDisplayResultsディレクティブは、アプリで再生するコンテンツを指定します。ここでは「アイアンマン2」を指定しています。 Alexaは、ユーザーのリクエストに一致するタイトルをカタログ内で検索し、一致するエントリをexternalIds配列にリストします。Alexaは、一致したすべてのタイトルをLambda関数に送信し、アプリで再生するタイトルの決定をユーザーに要求します。

この「アイアンマン2」の例では、映画がシリーズ物であるため、ペイロードには"type": "Franchise"が含まれています。詳細については、シリーズで再生を参照してください。ユーザーが「シリーズで再生」のリクエストを行うと、ユーザーがリクエストしたシリーズ物(この例の場合は「アイアンマン」)をディレクティブで受け取りますが、このディレクティブにカタログIDは含まれません。このようなディレクティブの場合、カタログ内でこのコンテンツの検索を実行し、検索最上位のタイトルを再生することが望ましい動作です。

2020-03-18T04:28:49 b9e15474 INFO Search started

サンプルLambda関数には、受信したリクエストの値を解析し、一致するものをデータベースで探す検索ロジックが含まれています。この実装は、ここではディレクトリのペイロードを使用して別のデータソースへの検索を作成する方法の例としてのみ示されています。サンプルアプリでは、再生するメディアの量が限られているため、実際にはこのコンテンツを見つけて再生することはできません。

この場合、Lambda関数に接続された「データベース」はvideo_catalog.jsonファイルにあり、次のようなエントリが含まれています。

[
    {
        "id": 1,
        "movie_id": "FG1",
        "title": "Forrest Gump",
        "genre": "drama",
        "description": "トムハンクス主演のドラマ",
        "video_file": "https://test-videos.co.uk/vids/bigbuckbunny/mp4/h264/1080/Big_Buck_Bunny_1080_10s_10MB.mp4"
    },
    {
        "id": 2,
        "movie_id": "IM1",
        "title": "アイアンマン1",
        "genre": "action",
        "description": "トニースタークとパワードスーツ",
        "video_file": "https://test-videos.co.uk/vids/bigbuckbunny/mp4/h264/1080/Big_Buck_Bunny_1080_10s_10MB.mp4"
    },
    {
        "id": 3,
        "movie_id": "IM2",
        "title": "アイアンマン2",
        "genre": "action",
        "description": "トニースタークとパワードスーツがよりクールに",
        "video_file": "https://test-videos.co.uk/vids/bigbuckbunny/mp4/h264/1080/Big_Buck_Bunny_1080_10s_10MB.mp4"
    }
]

サンプルLambda関数では、「軽量なあいまい検索ライブラリ」であるJSライブラリのFuse.jsを利用して、検索用語に一致する可能性のあるリストを取得します。fuseは、データソースといくつかの検索オプションの2つの引数を受け取ります。Lambda関数では、次のように表示されます。

var fuse = new Fuse(VIDEOS, options)

VIDEOSはデータソースであり、const VIDEOS = require("video_catalog.json");として定義されます。optionsは、マッチングを実行するためのさまざまなフィールドの重み付けを定義します。

Lambda関数はAlexaディレクティブからキーワードを取得し(let searchTerm = directive.payload.entities[0].value;)、その結果をfuseに渡して検索を実行します。

fuse.search(searchTerm)

これらの検索用語は、適切なメディアを再生するためにアプリに渡すことをお勧めします。

アプリで検索を実行するためにFuseを実装する必要はありません。独自の(より堅牢な)ロジックを実装して、コンテンツの検索を実行できます。

2020-03-18T04:28:49 b9e15474 INFO Search Term: アイアンマン2

検索用語は、受信したAlexaディレクティブからdirective.payload.entities[0].value;を介して取得されます。

2020-03-18T04:28:49 b9e15474 INFO Search Results:

{
    "item": {
        "id": 3,
        "movie_id": "IM2",
        "title": "アイアンマン2",
        "genre": "action",
        "description": "トニースタークとパワードスーツがよりクールに",
        "video_file": "https://test-videos.co.uk/vids/bigbuckbunny/mp4/h264/1080/Big_Buck_Bunny_1080_10s_10MB.mp4"
    },
    "score": 0.0005
}
,
{
    "item": {
        "id": 2,
        "movie_id": "IM1",
        "title": "アイアンマン1",
        "genre": "action",
        "description": "トニースタークとパワードスーツ",
        "video_file": "https://test-videos.co.uk/vids/bigbuckbunny/mp4/h264/1080/Big_Buck_Bunny_1080_10s_10MB.mp4"
    },
    "score": 0.05
}
,
{
    "item": {
        "id": 201,
        "movie_id": "BA1_1",
        "episode": 1,
        "genre": "comedy",
        "title": "Black Adder Season 1 Ep 1",
        "description": "ローワン・アトキンソンは、Black Adderシリーズでデビューした面白い人です",
        "video_file": "https://test-videos.co.uk/vids/bigbuckbunny/mp4/h264/1080/Big_Buck_Bunny_1080_10s_10MB.mp4"
    },
    "score": 0.45
}
,
{
    "item": {
        "id": 202,
        "movie_id": "BA1_2",
        "episode": 2,
        "genre": "comedy",
        "title": "Black Adder Season 1 Ep 2",
        "description": "ローワン・アトキンソンは、引き続き、高く評価された不運なBlack Adderの役を演じています",
        "video_file": "https://test-videos.co.uk/vids/bigbuckbunny/mp4/h264/1080/Big_Buck_Bunny_1080_10s_10MB.mp4"
    },
    "score": 0.45
}
]

searchTermfuseに渡され、searchResultsに保存されます。これは、以下を介してコンソールに記録されます。

var searchResults = fuse.search(searchTerm)

2020-03-18T04:28:49 b9e15474 INFO ADM Registration ID

ADM登録IDがコンソールに記録されます。

2020-03-18T04:28:49 b9e15474 INFO Got Access Token Type:bearer Expires In:3600…

ADMは、認証情報を使用してアクセストークンを取得します。認証に成功すると、Lambda関数はsendMessageToDevice()関数を呼び出します。認証に失敗した場合、アクセストークンは付与されません。

2020-03-18T04:28:49 b9e15474 INFO Calling device…

ADMはsendMessageToDevice()関数を呼び出して、APIキーで識別したアプリにディレクティブを送信します。ディレクティブの送信には、リクエストモジュールを使用します。ADMドキュメントヘッダーの詳細については、アクセストークンのリクエスト方法を参照してください。

2020-03-18T04:28:49 b9e15474 INFO Sending message to app:

Lambda関数はADM経由でFire TV対応アプリにメッセージを送信します。JSONはエスケープされます。

 "{\"directive\":{\"payload\":{\"entities\":[{\"type\":\"Video\",\"uri\":\"entity://provider/program/amzn1.p11cat.merged-video.ae637959-7f3c-5c5e-8baf-ce730ed9beb6\",\"value\":\"アイアンマン2\",\"externalIds\":{\"ENTITY_ID\":\"amzn1.p11cat.merged-video.ae637959-7f3c-5c5e-8baf-ce730ed9beb6\",\"tms\":\"MV002527780000\"}},{\"type\":\"Video\",\"uri\":\"entity://provider/program/amzn1.p11cat.merged-video.b4e77dca-954a-555d-ba2b-da5e0ca9e903\",\"value\":\"アイアンマン2\",\"externalIds\":{\"ENTITY_ID\":\"amzn1.p11cat.merged-video.b4e77dca-954a-555d-ba2b-da5e0ca9e903\"}},{\"type\":\"Video\",\"uri\":\"entity://provider/program/amzn1.p11cat.merged-video.0e3a1fb7-b973-52c0-98ef-cc4765fb9238\",\"value\":\"アイアンマン2\",\"externalIds\":{\"ENTITY_ID\":\"amzn1.p11cat.merged-video.0e3a1fb7-b973-52c0-98ef-cc4765fb9238\"}},{\"type\":\"Video\",\"uri\":\"entity://provider/program/amzn1.p11cat.merged-video.6beb3014-436d-5138-b0c0-bcf819cfadef\",\"value\":\"アイアンマン2\",\"externalIds\":{\"ENTITY_ID\":\"amzn1.p11cat.merged-video.6beb3014-436d-5138-b0c0-bcf819cfadef\"}},{\"type\":\"Video\",\"uri\":\"entity://provider/program/amzn1.p11cat.merged-video.80a7eda2-3e16-55de-b56b-3136f5dc0e74\",\"value\":\"アイアンマン2\",\"externalIds\":{\"ENTITY_ID\":\"amzn1.p11cat.merged-video.80a7eda2-3e16-55de-b56b-3136f5dc0e74\",\"tms\":\"MV006154660000\"}}],\"searchText\":{\"transcribed\":\"アイアンマンツー\"},\"searchResults\":[{\"item\":{\"id\":3,\"movie_id\":\"IM2\",\"title\":\"アイアンマン2\",\"genre\":\"action\",\"description\":\"トニースタークとパワードスーツがよりクールに\",\"video_file\":\"https://test-videos.co.uk/vids/bigbuckbunny/mp4/h264/1080/Big_Buck_Bunny_1080_10s_10MB.mp4\"},\"score\":0.0005},{\"item\":{\"id\":2,\"movie_id\":\"IM1\",\"title\":\"アイアンマン1\",\"genre\":\"action\",\"description\":\"トニースタークとパワードスーツ\",\"video_file\":\"https://test-videos.co.uk/vids/bigbuckbunny/mp4/h264/1080/Big_Buck_Bunny_1080_10s_10MB.mp4\"},\"score\":0.05},{\"item\":{\"id\":201,\"movie_id\":\"BA1_1\",\"episode\":1,\"genre\":\"comedy\",\"title\":\"Black Adderシーズン1エピソード1\",\"description\":\"ローワン・アトキンソンは、Black Adderシリーズでデビューした面白い人です\",\"video_file\":\"https://test-videos.co.uk/vids/bigbuckbunny/mp4/h264/1080/Big_Buck_Bunny_1080_10s_10MB.mp4\"},\"score\":0.45},{\"item\":{\"id\":202,\"movie_id\":\"BA1_2\",\"episode\":2,\"genre\":\"comedy\",\"title\":\"Black Adderシーズン1エピソード2\",\"description\":\"ローワン・アトキンソンは、引き続き、高く評価された不運なBlack Adderの役を演じています\",\"video_file\":\"https://test-videos.co.uk/vids/bigbuckbunny/mp4/h264/1080/Big_Buck_Bunny_1080_10s_10MB.mp4\"},\"score\":0.45}]},\"header\":{\"payloadVersion\":\"3\",\"messageId\":\"f5cbe32c-72fc-4816-892d-92f695c125a4\",\"namespace\":\"Alexa.RemoteVideoPlayer\",\"name\":\"SearchAndDisplayResults\",\"correlationToken\":\"7e8bdcef-1aff-452e-b405-5a877d712755\"},\"endpoint\":{\"endpointId\":\"432521e3d404c9e3##amzn1-ask-skill-c7dc9021-ba50-4aa9-bbad-7b9bcd2e94c3##development##com-tomjoht-vskfiretv\",\"cookie\":{\"deviceType\":\"AKPGW064GI9HE\",\"VSKClientVersion\":\"1.4.6\",\"appPackageName\":\"com.tomjoht.vskfiretv\",\"deviceId\":\"G070VM1694861D9N\",\"appName\":\"VSKFireTV\",\"applicationInstanceId\":\"amzn1.adm-registration.v3.Y29tLmFtYXpvbi5EZXZpY2VNZXNzYWdpbmcuUmVnaXN0cmF0aW9uSWRFbmNyeXB0aW9uS2V5ITEheFgvdzhUODl1SGc0WU96N0svSlZCVWFXVnBqUkNhdnJuWHlXWFBucUtvMHVLd0g2M1lkVEhZcjNRZ3J2K3A5QVAzdzhYbEkzZGYrY0t2Q0NQSWVQSEtEMDEvbGRhYUgrZVY4b1JRTXFMQndpYUZXajRXZlN3MEwzQ2ZaWkh2d0tUd3c1cFlYKys5WFdxZkVtRkg4RTQzcXhDMmxJYkZmd3dJeVVObStnWU00VU9GVmVwQTBVYityTFRRZHlYanNGUy9RTEhDbDhMUm1URmovZldsazF6WGRnZUV1V1pGWjkrcXYzR0lRcFV1MlpSYS9yZFA0aDEwbU5EcG1ndDE4eVJPRUg1bUtIQWVpUkZ3UlNwSlljcG9XSE96R3ZtMWJvZys2U1QyWUxvM3l0Nk4xNWI0QmFvMjBLcjNuZEl5UHpvNFBSUUs5aHZ1VUdTZ1psdUlFeVVsdzBZajFnb1ZZKzBLL3NGN2pJQnJjPSFmdk1qcTFuM0tOcVBOVzdyOGhybkp3PT0\"},\"scope\":{\"token\":null,\"type\":\"BearerToken\"}}}}"

このJSONをアンエスケープして整形すると、次のようになります。

{
  "directive": {
    "payload": {
      "entities": [
        {
          "type": "Video",
          "uri": "entity://provider/program/amzn1.p11cat.merged-video.ae637959-7f3c-5c5e-8baf-ce730ed9beb6",
          "value": "アイアンマン2",
          "externalIds": {
            "ENTITY_ID": "amzn1.p11cat.merged-video.ae637959-7f3c-5c5e-8baf-ce730ed9beb6",
            "tms": "MV002527780000"
          }
        },
        {
          "type": "Video",
          "uri": "entity://provider/program/amzn1.p11cat.merged-video.b4e77dca-954a-555d-ba2b-da5e0ca9e903",
          "value": "アイアンマン2",
          "externalIds": {
            "ENTITY_ID": "amzn1.p11cat.merged-video.b4e77dca-954a-555d-ba2b-da5e0ca9e903"
          }
        },
        {
          "type": "Video",
          "uri": "entity://provider/program/amzn1.p11cat.merged-video.0e3a1fb7-b973-52c0-98ef-cc4765fb9238",
          "value": "アイアンマン2",
          "externalIds": {
            "ENTITY_ID": "amzn1.p11cat.merged-video.0e3a1fb7-b973-52c0-98ef-cc4765fb9238"
          }
        },
        {
          "type": "Video",
          "uri": "entity://provider/program/amzn1.p11cat.merged-video.6beb3014-436d-5138-b0c0-bcf819cfadef",
          "value": "アイアンマン2",
          "externalIds": {
            "ENTITY_ID": "amzn1.p11cat.merged-video.6beb3014-436d-5138-b0c0-bcf819cfadef"
          }
        },
        {
          "type": "Video",
          "uri": "entity://provider/program/amzn1.p11cat.merged-video.80a7eda2-3e16-55de-b56b-3136f5dc0e74",
          "value": "アイアンマン2",
          "externalIds": {
            "ENTITY_ID": "amzn1.p11cat.merged-video.80a7eda2-3e16-55de-b56b-3136f5dc0e74",
            "tms": "MV006154660000"
          }
        }
      ],
      "searchText": {
        "transcribed": "アイアンマンツー"
      },
      "searchResults": [
        {
          "item": {
            "id": 3,
            "movie_id": "IM2",
            "title": "アイアンマン2",
            "genre": "action",
            "description": "トニースタークとパワードスーツがよりクールに",
            "video_file": "https://test-videos.co.uk/vids/bigbuckbunny/mp4/h264/1080/Big_Buck_Bunny_1080_10s_10MB.mp4"
          },
          "score": 0.0005
        },
        {
          "item": {
            "id": 2,
            "movie_id": "IM1",
            "title": "アイアンマン1",
            "genre": "action",
            "description": "トニースタークとパワードスーツ",
            "video_file": "https://test-videos.co.uk/vids/bigbuckbunny/mp4/h264/1080/Big_Buck_Bunny_1080_10s_10MB.mp4"
          },
          "score": 0.05
        },
        {
          "item": {
            "id": 201,
            "movie_id": "BA1_1",
            "episode": 1,
            "genre": "comedy",
            "title": "Black Adder Season 1 Ep 1",
            "description": "ローワン・アトキンソンは、Black Adderシリーズでデビューした面白い人です",
            "video_file": "https://test-videos.co.uk/vids/bigbuckbunny/mp4/h264/1080/Big_Buck_Bunny_1080_10s_10MB.mp4"
          },
          "score": 0.45
        },
        {
          "item": {
            "id": 202,
            "movie_id": "BA1_2",
            "episode": 2,
            "genre": "comedy",
            "title": "Black Adder Season 1 Ep 2",
            "description": "ローワン・アトキンソンは、引き続き、高く評価された不運なBlack Adderの役を演じています",
            "video_file": "https://test-videos.co.uk/vids/bigbuckbunny/mp4/h264/1080/Big_Buck_Bunny_1080_10s_10MB.mp4"
          },
          "score": 0.45
        }
      ]
    },
    "header": {
      "payloadVersion": "3",
      "messageId": "f5cbe32c-72fc-4816-892d-92f695c125a4",
      "namespace": "Alexa.RemoteVideoPlayer",
      "name": "SearchAndDisplayResults",
      "correlationToken": "7e8bdcef-1aff-452e-b405-5a877d712755"
    },
    "endpoint": {
      "endpointId": "432521e3d404c9e3##amzn1-ask-skill-c7dc9021-ba50-4aa9-bbad-7b9bcd2e94c3##development##com-tomjoht-vskfiretv",
      "cookie": {
        "deviceType": "AKPGW064GI9HE",
        "VSKClientVersion": "1.4.6",
        "appPackageName": "com.tomjoht.vskfiretv",
        "deviceId": "G070VM1694861D9N",
        "appName": "VSKFireTV",
        "applicationInstanceId": "amzn1.adm-registration.v3.Y29tLmFtYXpvbi5EZXZpY2VNZXNzYWdpbmcuUmVnaXN0cmF0aW9uSWRFbmNyeXB0aW9uS2V5ITEheFgvdzhUODl1SGc0WU96N0svSlZCVWFXVnBqUkNhdnJuWHlXWFBucUtvMHVLd0g2M1lkVEhZcjNRZ3J2K3A5QVAzdzhYbEkzZGYrY0t2Q0NQSWVQSEtEMDEvbGRhYUgrZVY4b1JRTXFMQndpYUZXajRXZlN3MEwzQ2ZaWkh2d0tUd3c1cFlYKys5WFdxZkVtRkg4RTQzcXhDMmxJYkZmd3dJeVVObStnWU00VU9GVmVwQTBVYityTFRRZHlYanNGUy9RTEhDbDhMUm1URmovZldsazF6WGRnZUV1V1pGWjkrcXYzR0lRcFV1MlpSYS9yZFA0aDEwbU5EcG1ndDE4eVJPRUg1bUtIQWVpUkZ3UlNwSlljcG9XSE96R3ZtMWJvZys2U1QyWUxvM3l0Nk4xNWI0QmFvMjBLcjNuZEl5UHpvNFBSUUs5aHZ1VUdTZ1psdUlFeVVsdzBZajFnb1ZZKzBLL3NGN2pJQnJjPSFmdk1qcTFuM0tOcVBOVzdyOGhybkp3PT0"
      },
      "scope": {
        "token": null,
        "type": "BearerToken"
      }
    }
  }
}

Android StudioでLogcatを開き、検索リクエストで絞り込みます(「アイアン」などの部分的に一致する文字列を入力するか、キーワード全体を入力します)。このディレクティブがアプリ側で受信されたことを確認できるはずです。

アプリのADMメッセージハンドラーでディレクティブの受信を確認する
アプリのADMメッセージハンドラーでディレクティブの受信を確認する(検索したメディアタイトルで絞り込んだ状態)

アプリによる受信ディレクティブの処理方法の詳細については、VSKFireTVMessageHandler.javaクラスを参照してください。たとえば、113~131行目には、ビデオを再生するためのロジックが以下のように含まれています。

if (directive != null) {
            String directiveName = directive.getDirective().getHeader().getName();
            if ("SearchAndPlay".equals(directiveName)) {
                Movie firstMovie = MovieList.getList().get(0);
                String movieName = firstMovie.getTitle();
                Log.d(TAG, "Playing MOVIE " + movieName);

                //デモンストレーションのために、映画リストの最初のアイテムを取得します。movie IDには対応していません
                Movie someMovie = MovieList.getList().get(0);


                Intent playIntent = new Intent();
                String packageName = AlexaClientManager.getSharedInstance().getApplicationContext().getPackageName();
                playIntent.setClassName(packageName, packageName + ".PlaybackActivity");
                playIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);

                //PlaybackActivityは映画が現在選択されていることを想定しています。選択されていない場合に備えてここで設定します
                playIntent.putExtra(DetailsActivity.MOVIE, someMovie);
                AlexaClientManager.getSharedInstance().getApplicationContext().startActivity(playIntent);

              } else if ("SearchAndDisplayResults".equals(directiveName)) {
                  String searchTerm = directive.getDirective().getPayload().getEntities().get(0).getValue().toLowerCase();
                  Log.d(TAG, "Searching for: " + searchTerm);

                  String searchPayload = "";
                  ...
            }
      ... }

このサンプルアプリでは、Movie firstMovie = MovieList.getList().get(0)に示されているように、レスポンスとしてMovieListの最初の映画タイトルのみを取得します。ロジックの詳細については、アプリのMovieListクラスを参照してください。サンプルアプリのデータ量は限られているため、ここではごく少数のレスポンスをコーディングしています。実装では、通常、包括的なクラウドデータベースを使用して検索を行います。

2020-03-18T04:28:49 b9e15474 INFO Response from the app:

{
    "registrationID": "amzn1.adm-registration.v3.Y29tLmFtYXpvbi...WVFnbXppczc0K0dWb2pnRklBQjNvdEhJV0F1cjNZTHZrakkvS09JPSE2czc5UVFhYklEN01BT1R5dVJNZDlnPT0"
}

アプリは、通信を確認するために登録IDを返します(読みやすくするために、ここでは長い文字列を切り捨てています)。 ここで登録IDの送信は必須ではありません。必要に応じて何でも送信できます。

2020-03-18T04:28:49 b9e15474 INFO Sending SearchAndDisplayResults response back to Alexa:

{
    "event": {
        "header": {
            "messageId": "not-required",
            "correlationToken": "not-required",
            "name": "Response",
            "namespace": "Alexa",
            "payloadVersion": "3"
        },
        "endpoint": {
            "scope": {
                "type": "DirectedUserId",
                "directedUserId": "not-required"
            },
            "endpointId": "not-required"
        },
        "payload": {}
    }
}

Lambda関数がAlexaに「成功」レスポンスを返し、ディレクティブを受信したことを知らせます。この想定されるレスポンスについては、SearchAndDisplayResults APIのレスポンスの例を参照してください。

END RequestId:b9e15474

ディレクティブのリクエストIDです。これでやり取りが終了しました。

REPORT RequestId: b9e15474 Duration: 882.12 ms Billed Duration: 900 ms Memory Size: 128 MB Max Memory Used: 105 MB

ディレクティブの処理に要した時間とメモリです。この処理で請求対象となる実行時間です。ビデオスキルを使用するFire TV対応アプリで、 「1か月に100万件のリクエストおよび400,000GB/秒のコンピューティング時間」の無料利用枠を超えるメモリを使用することはほとんどありません。 詳細については、AWS Lambda料金を参照してください。

SearchAndPlayディレクティブのワークフロー

上記の詳細なCloudwatchチュートリアルでは、SearchAndDisplayResultsディレクティブを使用しました。「アイアンマン2を再生して」という発話では、代わりにSearchAndPlayディレクティブが送信されます。

ワークフローは同じですが、「Directive received from Alexa: SearchAndPlay」セクションが異なります。

{
    "directive": {
        "payload": {
            "entities": [
                {
                    "type": "Video",
                    "uri": "entity://provider/program/amzn1.p11cat.merged-video.ae637959-7f3c-5c5e-8baf-ce730ed9beb6",
                    "value": "アイアンマン2",
                    "externalIds": {
                        "ENTITY_ID": "amzn1.p11cat.merged-video.ae637959-7f3c-5c5e-8baf-ce730ed9beb6",
                        "tms": "MV002527780000"
                    }
                },
                {
                    "type": "Video",
                    "uri": "entity://provider/program/amzn1.p11cat.merged-video.b4e77dca-954a-555d-ba2b-da5e0ca9e903",
                    "value": "アイアンマン2",
                    "externalIds": {
                        "ENTITY_ID": "amzn1.p11cat.merged-video.b4e77dca-954a-555d-ba2b-da5e0ca9e903"
                    }
                },
                {
                    "type": "Video",
                    "uri": "entity://provider/program/amzn1.p11cat.merged-video.0e3a1fb7-b973-52c0-98ef-cc4765fb9238",
                    "value": "アイアンマン2",
                    "externalIds": {
                        "ENTITY_ID": "amzn1.p11cat.merged-video.0e3a1fb7-b973-52c0-98ef-cc4765fb9238"
                    }
                },
                {
                    "type": "Video",
                    "uri": "entity://provider/program/amzn1.p11cat.merged-video.a13bd1b9-b2f6-5c15-b5a5-387f24836ba3",
                    "value": "アイアンマン2",
                    "externalIds": {
                        "ENTITY_ID": "amzn1.p11cat.merged-video.a13bd1b9-b2f6-5c15-b5a5-387f24836ba3"
                    }
                },
                {
                    "type": "Video",
                    "uri": "entity://provider/program/amzn1.p11cat.merged-video.6beb3014-436d-5138-b0c0-bcf819cfadef",
                    "value": "アイアンマン2",
                    "externalIds": {
                        "ENTITY_ID": "amzn1.p11cat.merged-video.6beb3014-436d-5138-b0c0-bcf819cfadef"
                    }
                }
            ],
            "searchText": {
                "transcribed": "アイアンマンツー"
            }
        },
        "header": {
            "payloadVersion": "3",
            "messageId": "53b4f99d-b8e4-44aa-b32f-095589f13987",
            "namespace": "Alexa.RemoteVideoPlayer",
            "name": "SearchAndPlay",
            "correlationToken": "e114fc4d-098a-47ad-8c54-94fbd693871e"
        },
        "endpoint": {
            "endpointId": "432521e3d404c9e3##amzn1-ask-skill-c7dc9021-ba50-4aa9-bbad-7b9bcd2e94c3##development##com-tomjoht-vskfiretv",
            "cookie": {
                "deviceType": "AKPGW064GI9HE",
                "VSKClientVersion": "1.4.6",
                "appPackageName": "com.tomjoht.vskfiretv",
                "deviceId": "G070VM1694861D9N",
                "appName": "VSKFireTV",
                "applicationInstanceId": "amzn1.adm-registration.v3.Y29tLmFtYXpvbi5EZXZpY2VNZXNzYWdpbmcuUmVnaXN0cmF0aW9uSWRFbmNyeXB0aW9uS2V5ITEheFgvdzhUODl1SGc0WU96N0svSlZCVWFXVnBqUkNhdnJuWHlXWFBucUtvMHVLd0g2M1lkVEhZcjNRZ3J2K3A5QVAzdzhYbEkzZGYrY0t2Q0NQSWVQSEtEMDEvbGRhYUgrZVY4b1JRTXFMQndpYUZXajRXZlN3MEwzQ2ZaWkh2d0tUd3c1cFlYKys5WFdxZkVtRkg4RTQzcXhDMmxJYkZmd3dJeVVObStnWU00VU9GVmVwQTBVYityTFRRZHlYanNGUy9RTEhDbDhMUm1URmovZldsazF6WGRnZUV1V1pGWjkrcXYzR0lRcFV1MlpSYS9yZFA0aDEwbU5EcG1ndDE4eVJPRUg1bUtIQWVpUkZ3UlNwSlljcG9XSE96R3ZtMWJvZys2U1QyWUxvM3l0Nk4xNWI0QmFvMjBLcjNuZEl5UHpvNFBSUUs5aHZ1VUdTZ1psdUlFeVVsdzBZajFnb1ZZKzBLL3NGN2pJQnJjPSFmdk1qcTFuM0tOcVBOVzdyOGhybkp3PT0"
            },
            "scope": {
                "token": null,
                "type": "BearerToken"
            }
        }
    }
}

サンプルアプリのディレクティブ

CloudWatchログ(サンプルLambda関数によってログに記録される情報)の読み取り方法がわかったので、次はテストを実行します。このセクションでは、Alexaへの4つの異なる発話を保存し、アプリの動作とCloudWatchに記録されるログを確認します。アプリの明示的なターゲット指定はまだ利用できないため、サンプルアプリを起動し、フォアグラウンドに移動(アプリがFire TVにアクティブに表示されていることを確認)してから各発話を行う必要があります。

サンプルアプリは4つのディレクティブを処理できます。このLambda関数のサンプルコードでは、Lambda関数はAlexaから受け取ったのと同じディレクティブを渡すだけです。その後、アプリはペイロードを照会し、カタログでメディアを検索します。

Lambda関数には、アプリにとって有用なプロセスをコーディングすることができます。たとえば、別のワークフローとして、Lambda関数で独自の処理を実行して正しいメディア識別子を見つけ、必要に応じて、その結果をアプリに送信できます。

SearchAndDisplayResultsディレクティブ

サンプル発話: サンプルアプリを起動して、「アレクサ、アイアンマン2を検索して」と話しかけます。

レスポンス: 「<ビデオスキル名>からアイアンマン2を取得しています」。アプリが検索画面に切り替わり、「アイアンマン2」というテキストが検索されます。

AlexaがLambda関数に送信するディレクティブSearchAndDisplayResults

SearchAndPlayディレクティブ

サンプル発話: サンプルアプリを起動して、「アレクサ、アイアンマン2を見せて」と話しかけます。

レスポンス: 「<ビデオスキル名>からアイアンマン2を取得しています」。 その後、アプリのテストビデオが再生されます。

AlexaがLambda関数に送信するディレクティブSearchAndPlay

PlaybackController - Pauseディレクティブ

サンプル発話: サンプルアプリを起動し(まだフォアグラウンドで実行していない場合)、ビデオのいずれかを再生します。その後、「アレクサ、一時停止して」と話しかけます。

レスポンス: Alexaは何も応答せず、ビデオが一時停止されます。

AlexaがLambda関数に送信するディレクティブPlaybackController: Pause

PlaybackController - Playディレクティブ

サンプル発話: 前のシナリオでサンプルビデオを一時停止した状態で、「アレクサ、再生して」と話しかけます。

レスポンス: Alexaは何も応答せず、ビデオの再生が再開されます。

AlexaがLambda関数に送信するディレクティブPlaybackController: Play

トラブルシューティング

問題が発生した場合は、下記のトラブルシューティングのヒントを参照してください。

「うまくいきませんでした」と返答される
Lambda構文が無効である可能性があります。
Cloudwatchにログが表示されない
適切なAWSリージョンが選択されていることを確認してください。
「すみません、ちょっとわかりません」と返答される
Alexaがコマンドを理解していません。
Cloudwatchのログに[object Object]と表示される
JSONのレンダリングにJSON.Stringify関数を使用していません。

次のステップ

AlexaディレクティブとLambda関数の動作を理解できたので、次は独自のコンテンツおよびサービス用にLambda関数をカスタマイズします。手順9: Alexaディレクティブを解釈して応答するに進みます。

(問題が発生して続行できない場合は、トラブルシューティングを参照してください)