AudioPlayerの再生キューの活用

2018.02.14

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

渡辺です。

AudioPlayerでハローワールドでは、AudioPlayerを利用するスキルの基本を解説しました。 AudioPlayerスキルのセッションとイベントでは、AudioPlayerスキルでハンドリングしなければならないイベントと、実装時の注意事項を解説しました。 簡単な音楽再生スキルは充分に作成できると思います。

今回はAudioPlayerスキルの仕上げとして、複数の音楽ファイルを再生するスキルを作成してみました。

再生キューとストリーミング

AudioPlayerを有効にしたスキルを起動した場合、クライアント(Echo端末など)は再生キューを持ちます。

スキルからPlayディレクティブを送信すると、ストリーミング再生がはじまります。 次の曲を続けて再生したい場合、再生が終了する前に、次のオーディオを追加できます。 この時、オーディオはクライアントの再生キューにキューイングされます。 クライアントでは再生中のオーディオの再生が終わり次第、キューイングされたオーディオを順番に再生します。 キューイングされることにより、曲と曲の間のタイムラグを最小にできます。

再生キューは、PlayディレクティブClearQueueディレクティブで制御できます。

Playディレクティブ

Playディレクティブでは、playBehaviorパラメーターで、どのようにオーディオを再生するか指定します。 playBehaviorパラメーターに設定できる値は以下の3種類です。

  • REPLACE_ALL
  • ENQUEUE
  • REPLACE_ENQUEUED

REPLACE_ALLを指定した場合、そのオーディオを即時に再生します。 キューイングされたオーディオが再生されていても上書き(REPLACE)するので、単一のオーディオを再生する場合は、REPLACE_ALLが適します。

ENQUEUEを指定した場合、キューの最後にオーディオを追加します。 再生中のオーディオに影響を与えないため、次の曲を指定するようなケースで有効です。 クライアントでは、現在の曲が終わり次第、連続的に再生されます。 連続して曲を再生するスキルでは、PlaybackNearlyFinishedイベントの処理で、ENQUEUEを指定して次の曲をクライアントに指定すると良いでしょう。

REPLACE_ENQUEUEDを指定した場合、キューをクリアした上で、次の曲としてオーディオを追加します。 再生中のオーディオには影響ありません。

ClearQueueディレクティブ

キューのクリアだけを行いたい場合、ClearQueueディレクティブを使用します。 Playディレクティブと同様に、clearBehaviorをヒントとして与えます。

  • CLEAR_ENQUEUED
  • CLEAR_ALL

CLEAR_ENQUEUEDは、キューイングされたオーディオをクリアしますが、再生中のオーディオの再生は継続します。 CLEAR_ALLでは、キューイングされたオーディオをクリアし、再生中のオーディオも停止します。

ジュークボックス系スキルを作ってみる

スキルで再生リストを管理し、各イベントをハンドリングすることで、ジュークボックス系スキルが作れます。 この時、キューイングを上手く活用し、クライアントから曲がスムーズに流れるようにすると良いでしょう。

再生リストの利用と再生開始

はじめに初期化処理を行います。

オーディオリスト(audioList)は、タイトルと曲のURLで構成されます。 今回はLambdaにハードコーディングしましたが、ここはDyanamoDBなどから取得しても良いでしょう。

playList関数は、曲のインデックスを配列で返します。 ランダム再生に対応するため、シャッフルオプションを用意しておきました。

const audioList = [
  {
    title: "001",
    url: "https://s3-ap-northeast-1.amazonaws.com/[BucketName]/001.mp3"
  },
  {
    title: "002",
    url: "https://s3-ap-northeast-1.amazonaws.com/[BucketName]/002.mp3"
  },
  {
    title: "003",
    url: "https://s3-ap-northeast-1.amazonaws.com/[BucketName]/003.mp3"
  }
];
const playList = function(length, shuffled) {
  var array = Array.apply(null, {length: length}).map(Number.call, Number);
  if (shuffled) {
    for (var i = 0; i < length; i++) {
      var j = Math.floor(Math.random() * length);
      var temp = array[i];
      array[i] = array[j];
      array[j] = temp;      
    }
  }
  return array;
};
const findIndexInPlayList = function(title) {
  for (var index = 0 ; index < audioList.length; index++) {
    if (audioList[index]['title'] === title) return index;
  }
  return -1;
};
  'Start': function () {
  	// ex [0, 1, 2, 3]
    this.attributes['playOrder'] = playList(audioList.length, false); 
    this.attributes['index'] = 0;
    this.attributes['token'] = null;
    this.attributes['loop'] = false;
    const playOrder = this.attributes['playOrder'];
    const index = this.attributes['index'];
    const target = playlist[playOrder[index]];
    const url = target['url'];
    const token = target['title'];
    const expectedPreviousToken = null;
    const offset = 0;
    this.response.audioPlayerPlay('REPLACE_ALL', url, token, expectedPreviousToken, offset);
    this.emit(':responseReady');
  },

PlaybackStartedイベントの処理

クライアントで曲が再生されるとPlaybackStartedイベントが発生します。 この時、再生された曲の情報をセッション(DyanmoDB)に保存しておきましょう。

  'PlaybackStarted':  function () {
    const token = this.event.request.token;
    this.attributes['index'] = findIndexInPlayList(token);
    this.attributes['token'] = token;
    this.emit(':responseReady');
  },

イベントのrequest.tokenから再生した曲のTokenが取れるため、そこから曲を逆引きしています。

PlaybackStartedイベントは、停止後の再生などでも発生するので注意してください。

PlaybackNearlyFinishedイベントの処理

PlaybackNearlyFinishedは、曲の再生が終わる前に呼ばれるイベントです。 ここで、次の曲をキューに追加し、クライアントでスムーズな再生をさせるとよいでしょう。

この時、セッションから再生中の曲のインデックスを取り、インクリメントすることで次曲を装填しています。 なお、キューに追加する場合は、前曲のTokenexpectedPreviousTokenに指定しなければなりません。

  'PlaybackNearlyFinished':  function () {
    var index = this.attributes['index'];
    if (this.attributes['loop']) {
      // loop
    } else {
      index++;
      if (index === audioList.length) index = 0;
    }
    const playOrder = this.attributes['playOrder'];
    const target = playlist[playOrder[index]];
    const url = target['url'];
    const token = target['title'];
    const expectedPreviousToken = this.attributes['token'];
    const offset = 0;
    this.response.audioPlayerPlay('ENQUEUE', url, token, expectedPreviousToken, offset);
    this.emit(':responseReady');
  },

NextIntent/PreviousIntentの処理

「Alexa, 次の曲」などAudioPlayerの標準機能を実装します。 曲を指定する流れは変わりませんが、即時に再生をはじめるためREPLACE_ALLを設定します。

  'AMAZON.NextIntent': function () {
    var index = this.attributes['index'] + 1;
    if (index === audioList.length) index = 0;
    const playOrder = this.attributes['playOrder'];
    const target = audioList[playOrder[index]];
    const url = target['url'];
    const token = target['title'];
    const expectedPreviousToken = null;
    const offset = 0;
    this.response.audioPlayerPlay('REPLACE_ALL', url, token, expectedPreviousToken, offset);
    this.emit(':responseReady');
  },
  'AMAZON.PreviousIntent': function () {
    var index = this.attributes['index'] - 1;
    if (index === -1) index = (audioList.length - 1);
    const playOrder = this.attributes['playOrder'];
    const target = audioList[playOrder[index]];
    const url = target['url'];
    const token = target['title'];
    const expectedPreviousToken = null;
    const offset = 0;
    this.response.audioPlayerPlay('REPLACE_ALL', url, token, expectedPreviousToken, offset);
    this.emit(':responseReady');
  },

ShuffleOnIntent/ShuffleOffIntentの処理

シャッフル再生も対応しましょう。 シャッフルが有効になった場合の処理と無効になった場合の処理を実装します。

今回は、シャッフルしても現在の曲は流れたままという仕様にしました。

  'AMAZON.ShuffleOnIntent': function () {
    this.attributes['playOrder'] = playList(audioList.length, true);
    const output = this.t('SHUFFLE_ON');
    this.response.speak(output);
    this.emit(':responseReady');
  },
  'AMAZON.ShuffleOffIntent': function () {
    this.attributes['playOrder'] = playList(audioList.length, false);
    const output = this.t('SHUFFLE_OFF');
    this.response.speak(output);
    this.emit(':responseReady');
  },

LoopOffIntent/LoopOnIntentの処理

ループ再生機能のオン/オフを処理します。 ループ再生は再生リスト全体をループするかどうかという仕様にしました。 ループ再生がオフの場合、PlaybackNearlyFinishedイベントで最後の曲の場合、キューに曲を追加しません。

  'AMAZON.LoopOffIntent': function () {
    this.attributes['loop'] = false;
    const output = this.t('LOOP_OFF');
    this.response.speak(output);
    this.emit(':responseReady');
  },
  'AMAZON.LoopOnIntent': function () {
    this.attributes['loop'] = true;
    const output = this.t('LOOP_ON');
    this.response.speak(output);
    this.emit(':responseReady');
  },

1曲のみのループ再生は別のインテントで実装可能です。

RepeatIntentの処理

RepeatIntentは「Alexa, もう1回」といった発話に対応します。 ここでは曲を最初から再生しなおす実装としました。

  'AMAZON.RepeatIntent': function () {
    const token = this.event.context.AudioPlayer.token;
    const index = findIndexInPlayList(token);
    const playOrder = this.attributes['playOrder'];
    const target = audioList[playOrder[index]];
    const url = target['url'];
    const expectedPreviousToken = null;
    const offset = 0;
    this.response.audioPlayerPlay('REPLACE_ALL', url, token, expectedPreviousToken, offset);
    this.emit(':responseReady');
  },

まとめ

こんな感じに実装すれば、ジュークボックス系スキルが実装できます。 キューを活用してクライアントに曲を先行で送ることがポイントでしょう。

なお、デフォルトで実装すべきインテントが多くあります。 どんな発話に対応してインテントが起動するかに注意してください。 意図した発話でない場合、期待しない挙動をとってしまいます。 必要に応じてカスタムインテントを追加しましょう。