Alexa SkillのAudioPlayer機能【実装編】

概要編ではAudioPlayer機能の大枠を書かせていただきました。
実装編ではAudioPlayerを使用したSkillの実装を説明します。

実装サンプル

AmazonはAlexa Skills Kit SDK for Node.jsを使用したAudioPlayerのサンプルを用意してくれています。
サンプルは2種類あり、シングルストリーム(ラジオのライブストリーミングなど)用とマルチストリーム(ポッドキャスティングや音楽再生など)用になります。
ニーズが大きいと思われるのはマルチストリームの方だと思われるので、マルチストリームのサンプルを使用して解説していこうと思います。

構成とState

サンプルは以下のファイルで構成されています。

ファイル名 内容
index.js メインモジュールです。Skillのセットアップやハンドラーの登録を行っています。
constants.js アプリのIDなど、幾つかの定数を定義しています。
audioAssets.js 再生する音声ファイルのURLなどを格納しています。
stateHandlers.js カスタムIntentやBuilt-In Intentに対しての処理をStateごとに実装しています。
audioEventHandlers.js AudioPlayer Requestに対しての処理を実装しています。

また、State(Skillの状態)は以下の3種類で実装されています。

State 意味
START_MODE IntentなしでSkillを呼び出したときの状態です。
PLAY_MODE 音声ファイルを再生中です。
RESUME_DECISION_MODE 音声ファイルが途中まで再生された状態です。

音声ファイルの再生・停止

SDKではResponseBuilderというクラスに用意された下記のメソッドを使用することで音声ファイルを再生したり停止したりすることができます。
サンプルだとstateHandler.jsやaudioEventHandlers.jsで使用されており、このように呼び出されています。

this.response.audioPlayerPlay(playBehavior, podcast.url, token, null, offsetInMilliseconds);

audioPlayerPlay

音声ファイルを再生するメソッドです。シグネチャは以下のとおりです。

引数 意味
behavior 再生のタイミングを選びます。
REPLACE_ALL:対象の音声ファイルをすぐ再生します。現在再生されている音声ファイルはストップされます。
ENQUEUE:キューに対象の音声ファイルをプッシュします。現在再生されている音声ファイルには影響を与えません。
REPLACE_ENQUEUED:キューを対象の音声ファイルで置き換えますが、現在再生されている音声ファイルには影響を与えません。(再生完了後に対象の音声ファイルが再生されます。)
url 音声ファイルのロケーションを表すURLです。インターネットでアクセスできるHTTPSエンドポイントである必要があります。また、SSL証明書はオレオレ証明書でないものである必要があります。形式はMP3, AAC/MP4, HLS, PLS, M3Uが再生可能です。ビットレートは16〜384 kbpsが対応可能です。
token 対象の音声ファイルをあらわす識別子です。
expectedPreviousToken  behaviorがENQUEUEの場合のみ使用される引数で、現在再生されている音声ファイルのトークンを指定します。
 Alexaは現在再生されている音声ファイルのトークンとこの引数で指定されたトークンが一致した場合のみ、対象の音声ファイルをキューに追加します。
 この仕様はAlexaが自動でSkillに送ってくるPlaybackNearlyFinishedリクエストとユーザーの音声ファイルを変更する指示が同じタイミングで行われたときにユーザーの意図しない音声ファイルが再生されないための仕組みです。
offsetInMilliseconds 音声ファイルをどの地点から再生するかをミリ秒で指定します。0だと最初から再生されます。

audioPlayerStop

再生を停止する為のメソッドです。
引数はありません。

audioPlayerClearQueue

再生予定の音声ファイルをキューからクリアします。(behavior=ENQUEUEでaudioPlayerPlayを呼び出すとキューに入ります)

引数 意味
clearBehavior クリアをどのように行うかを選びます。
CLEAR_ENQUEUED:キューに入っている音声ファイルをすべてクリアしますが、現在再生中の音声ファイルはストップしません。
CLEAR_ALL:キューに入っている音声ファイルをすべてクリアし、現在再生中の音声ファイルもストップします。

Intentに対応する

ユーザーの発声によって、概要編で紹介したBuilt-In Intentや独自に定義したカスタムIntentが発生した場合に実行する処理を実装します。
サンプルのstateHandler.jsでは音声ファイルの再生や再生モードの設定、ヘルプ時のメッセージの返却などが実装されています。

playModeIntentHandlers : Alexa.CreateStateHandler(constants.states.PLAY_MODE, {
        〜中略〜
        'PlayAudio' : function () { controller.play.call(this) },
        'AMAZON.NextIntent' : function () { controller.playNext.call(this) },
        'AMAZON.PreviousIntent' : function () { controller.playPrevious.call(this) },
        'AMAZON.PauseIntent' : function () { controller.stop.call(this) },
        'AMAZON.StopIntent' : function () { controller.stop.call(this) },
        'AMAZON.CancelIntent' : function () { controller.stop.call(this) },
        'AMAZON.ResumeIntent' : function () { controller.play.call(this) },
        'AMAZON.LoopOnIntent' : function () { controller.loopOn.call(this) },
        'AMAZON.LoopOffIntent' : function () { controller.loopOff.call(this) },
        'AMAZON.ShuffleOnIntent' : function () { controller.shuffleOn.call(this) },
        'AMAZON.ShuffleOffIntent' : function () { controller.shuffleOff.call(this) },
        'AMAZON.StartOverIntent' : function () { controller.startOver.call(this) },
        'AMAZON.HelpIntent' : function () {
            // This will called while audio is playing and a user says "ask <invocation_name> for help"
            var message = 'You are listening to the AWS Podcast. You can say, Next or Previous to navigate through the playlist. ' +
                'At any time, you can say Pause to pause the audio and Resume to resume.';
            this.response.speak(message).listen(message);
            this.emit(':responseReady');
        },
        'SessionEndedRequest' : function () {
            // No session ended logic
        },
        'Unhandled' : function () {
            var message = 'Sorry, I could not understand. You can say, Next or Previous to navigate through the playlist.';
            this.response.speak(message).listen(message);
            this.emit(':responseReady');
        }
    }),

AudioPlayer Requestに対応する

AudioPlayer Requestは概要編でも紹介したAlexaが再生の状態が変わったタイミングで送ってくれるリクエスト(イベント)です。
これらのリクエストが来ることを想定して処理を実装することができます。
サンプルではaudioEventHandlers.jsに実装されており、PlaybackNearlyFinishedリクエストが来た際に次の音声ファイルを設定する、などの
処理を行っています。

var audioEventHandlers = Alexa.CreateStateHandler(constants.states.PLAY_MODE, {
    'PlaybackStarted' : function () {
        /*
         * AudioPlayer.PlaybackStarted Directive received.
         * Confirming that requested audio file began playing.
         * Storing details in dynamoDB using attributes.
         */
        this.attributes['token'] = getToken.call(this);
        this.attributes['index'] = getIndex.call(this);
        this.attributes['playbackFinished'] = false;
        this.emit(':saveState', true);
    },
    'PlaybackFinished' : function () {
        /*
         * AudioPlayer.PlaybackFinished Directive received.
         * Confirming that audio file completed playing.
         * Storing details in dynamoDB using attributes.
         */
        this.attributes['playbackFinished'] = true;
        this.attributes['enqueuedToken'] = false;
        this.emit(':saveState', true);
    },
    'PlaybackStopped' : function () {
        /*
         * AudioPlayer.PlaybackStopped Directive received.
         * Confirming that audio file stopped playing.
         * Storing details in dynamoDB using attributes.
         */
        this.attributes['token'] = getToken.call(this);
        this.attributes['index'] = getIndex.call(this);
        this.attributes['offsetInMilliseconds'] = getOffsetInMilliseconds.call(this);
        this.emit(':saveState', true);
    },
    'PlaybackNearlyFinished' : function () {
        /*
         * AudioPlayer.PlaybackNearlyFinished Directive received.
         * Using this opportunity to enqueue the next audio
         * Storing details in dynamoDB using attributes.
         * Enqueuing the next audio file.
         */
        if (this.attributes['enqueuedToken']) {
            /*
             * Since AudioPlayer.PlaybackNearlyFinished Directive are prone to be delivered multiple times during the
             * same audio being played.
             * If an audio file is already enqueued, exit without enqueuing again.
             */
            return this.context.succeed({});
        }

        var enqueueIndex = this.attributes['index'];
        enqueueIndex +=1;
        // Checking if  there are any items to be enqueued.
        if (enqueueIndex === audioData.length) {
            if (this.attributes['loop']) {
                // Enqueueing the first item since looping is enabled.
                enqueueIndex = 0;
            } else {
                // Nothing to enqueue since reached end of the list and looping is disabled.
                return this.context.succeed({});
            }
        }
        // Setting attributes to indicate item is enqueued.
        this.attributes['enqueuedToken'] = String(this.attributes['playOrder'][enqueueIndex]);

        var enqueueToken = this.attributes['enqueuedToken'];
        var playBehavior = 'ENQUEUE';
        var podcast = audioData[this.attributes['playOrder'][enqueueIndex]];
        var expectedPreviousToken = this.attributes['token'];
        var offsetInMilliseconds = 0;

        this.response.audioPlayerPlay(playBehavior, podcast.url, enqueueToken, expectedPreviousToken, offsetInMilliseconds);
        this.emit(':responseReady');
    },
    'PlaybackFailed' : function () {
        //  AudioPlayer.PlaybackNearlyFinished Directive received. Logging the error.
        console.log("Playback Failed : %j", this.event.request.error);
        this.context.succeed({});
    }
});

おわりに

AudioPlayerはSSMLではできない長い音声ファイルを再生できるので、非常に有用な機能だとおもいます。
Amazon Echoが日本で発売されたあかつきには、AudioPlayerを使ってSkillを作り、楽しんでみようと思います。