Reputation: 359
I've followed the sample code from https://github.com/alexa/skill-sample-nodejs-audio-player and gotten the example podcast player to play via my Amazon Echo.
How would one modify this code in order to tell Alexa to "Ask MySkill to Play $trackname". Instead of just playing from the top?
I'm very new to creating skills but I've read all the documentation and understand that this involves Audio Directives. However, I can't figure out what goes where.
In the sample code, the audioAssets.js contains a list of titles and urls. So for example, if I wanted to say "Play Episode 138" (one of the titles) - which files would I need to modify in order to do this?
'use strict';
var audioData = [
{
'title' : 'Episode 140',
'url' : 'https://feeds.soundcloud.com/stream/275202399-amazon-web- services-306355661-amazon-web-services.mp3'
},
{
'title' : 'Episode 139',
'url' : 'https://feeds.soundcloud.com/stream/274166909-amazon-web-services-306355661-aws-podcast-episode-139.mp3'
},
{
'title' : 'Episode 138',
'url' : 'https://feeds.soundcloud.com/stream/273105224-amazon-web-services-306355661-aws-podcast-episode-138.mp3'
},
{
'title' : 'Episode 137',
'url' : 'https://feeds.soundcloud.com/stream/272089501-amazon-web-services-306355661-aws-podcast-episode-137.mp3'
}
];
module.exports = audioData;
I'm assuming the code would go into stateHandlers.js but honestly I'm not sure.
'use strict';
var Alexa = require('alexa-sdk');
var audioData = require('./audioAssets');
var constants = require('./constants');
var stateHandlers = {
startModeIntentHandlers : Alexa.CreateStateHandler(constants.states.START_MODE, {
/*
* All Intent Handlers for state : START_MODE
*/
'LaunchRequest' : function () {
// Initialize Attributes
this.attributes['playOrder'] = Array.apply(null, {length: audioData.length}).map(Number.call, Number);
this.attributes['index'] = 0;
this.attributes['offsetInMilliseconds'] = 0;
this.attributes['loop'] = true;
this.attributes['shuffle'] = false;
this.attributes['playbackIndexChanged'] = true;
// Change state to START_MODE
this.handler.state = constants.states.START_MODE;
var message = 'Welcome to the AWS Podcast. You can say, play the audio to begin the podcast.';
var reprompt = 'You can say, play the audio, to begin.';
this.response.speak(message).listen(reprompt);
this.emit(':responseReady');
},
'PlayAudio' : function () {
if (!this.attributes['playOrder']) {
// Initialize Attributes if undefined.
this.attributes['playOrder'] = Array.apply(null, {length: audioData.length}).map(Number.call, Number);
this.attributes['index'] = 0;
this.attributes['offsetInMilliseconds'] = 0;
this.attributes['loop'] = true;
this.attributes['shuffle'] = false;
this.attributes['playbackIndexChanged'] = true;
// Change state to START_MODE
this.handler.state = constants.states.START_MODE;
}
controller.play.call(this);
},
'AMAZON.HelpIntent' : function () {
var message = 'Welcome to the AWS Podcast. You can say, play the audio, to begin the podcast.';
this.response.speak(message).listen(message);
this.emit(':responseReady');
},
'AMAZON.StopIntent' : function () {
var message = 'Good bye.';
this.response.speak(message);
this.emit(':responseReady');
},
'AMAZON.CancelIntent' : function () {
var message = 'Good bye.';
this.response.speak(message);
this.emit(':responseReady');
},
'SessionEndedRequest' : function () {
// No session ended logic
},
'Unhandled' : function () {
var message = 'Sorry, I could not understand. Please say, play the audio, to begin the audio.';
this.response.speak(message).listen(message);
this.emit(':responseReady');
}
}),
playModeIntentHandlers : Alexa.CreateStateHandler(constants.states.PLAY_MODE, {
/*
* All Intent Handlers for state : PLAY_MODE
*/
'LaunchRequest' : function () {
/*
* Session resumed in PLAY_MODE STATE.
* If playback had finished during last session :
* Give welcome message.
* Change state to START_STATE to restrict user inputs.
* Else :
* Ask user if he/she wants to resume from last position.
* Change state to RESUME_DECISION_MODE
*/
var message;
var reprompt;
if (this.attributes['playbackFinished']) {
this.handler.state = constants.states.START_MODE;
message = 'Welcome to the AWS Podcast. You can say, play the audio to begin the podcast.';
reprompt = 'You can say, play the audio, to begin.';
} else {
this.handler.state = constants.states.RESUME_DECISION_MODE;
message = 'You were listening to ' + audioData[this.attributes['playOrder'][this.attributes['index']]].title +
' Would you like to resume?';
reprompt = 'You can say yes to resume or no to play from the top.';
}
this.response.speak(message).listen(reprompt);
this.emit(':responseReady');
},
'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');
}
}),
remoteControllerHandlers : Alexa.CreateStateHandler(constants.states.PLAY_MODE, {
/*
* All Requests are received using a Remote Control. Calling corresponding handlers for each of them.
*/
'PlayCommandIssued' : function () { controller.play.call(this) },
'PauseCommandIssued' : function () { controller.stop.call(this) },
'NextCommandIssued' : function () { controller.playNext.call(this) },
'PreviousCommandIssued' : function () { controller.playPrevious.call(this) }
}),
resumeDecisionModeIntentHandlers : Alexa.CreateStateHandler(constants.states.RESUME_DECISION_MODE, {
/*
* All Intent Handlers for state : RESUME_DECISION_MODE
*/
'LaunchRequest' : function () {
var message = 'You were listening to ' + audioData[this.attributes['playOrder'][this.attributes['index']]].title +
' Would you like to resume?';
var reprompt = 'You can say yes to resume or no to play from the top.';
this.response.speak(message).listen(reprompt);
this.emit(':responseReady');
},
'AMAZON.YesIntent' : function () { controller.play.call(this) },
'AMAZON.NoIntent' : function () { controller.reset.call(this) },
'AMAZON.HelpIntent' : function () {
var message = 'You were listening to ' + audioData[this.attributes['index']].title +
' Would you like to resume?';
var reprompt = 'You can say yes to resume or no to play from the top.';
this.response.speak(message).listen(reprompt);
this.emit(':responseReady');
},
'AMAZON.StopIntent' : function () {
var message = 'Good bye.';
this.response.speak(message);
this.emit(':responseReady');
},
'AMAZON.CancelIntent' : function () {
var message = 'Good bye.';
this.response.speak(message);
this.emit(':responseReady');
},
'SessionEndedRequest' : function () {
// No session ended logic
},
'Unhandled' : function () {
var message = 'Sorry, this is not a valid command. Please say help to hear what you can say.';
this.response.speak(message).listen(message);
this.emit(':responseReady');
}
})
};
module.exports = stateHandlers;
var controller = function () {
return {
play: function () {
/*
* Using the function to begin playing audio when:
* Play Audio intent invoked.
* Resuming audio when stopped/paused.
* Next/Previous commands issued.
*/
this.handler.state = constants.states.PLAY_MODE;
if (this.attributes['playbackFinished']) {
// Reset to top of the playlist when reached end.
this.attributes['index'] = 0;
this.attributes['offsetInMilliseconds'] = 0;
this.attributes['playbackIndexChanged'] = true;
this.attributes['playbackFinished'] = false;
}
var token = String(this.attributes['playOrder'][this.attributes['index']]);
var playBehavior = 'REPLACE_ALL';
var podcast = audioData[this.attributes['playOrder'][this.attributes['index']]];
var offsetInMilliseconds = this.attributes['offsetInMilliseconds'];
// Since play behavior is REPLACE_ALL, enqueuedToken attribute need to be set to null.
this.attributes['enqueuedToken'] = null;
if (canThrowCard.call(this)) {
var cardTitle = 'Playing ' + podcast.title;
var cardContent = 'Playing ' + podcast.title;
this.response.cardRenderer(cardTitle, cardContent, null);
}
this.response.audioPlayerPlay(playBehavior, podcast.url, token, null, offsetInMilliseconds);
this.emit(':responseReady');
},
stop: function () {
/*
* Issuing AudioPlayer.Stop directive to stop the audio.
* Attributes already stored when AudioPlayer.Stopped request received.
*/
this.response.audioPlayerStop();
this.emit(':responseReady');
},
playNext: function () {
/*
* Called when AMAZON.NextIntent or PlaybackController.NextCommandIssued is invoked.
* Index is computed using token stored when AudioPlayer.PlaybackStopped command is received.
* If reached at the end of the playlist, choose behavior based on "loop" flag.
*/
var index = this.attributes['index'];
index += 1;
// Check for last audio file.
if (index === audioData.length) {
if (this.attributes['loop']) {
index = 0;
} else {
// Reached at the end. Thus reset state to start mode and stop playing.
this.handler.state = constants.states.START_MODE;
var message = 'You have reached at the end of the playlist.';
this.response.speak(message).audioPlayerStop();
return this.emit(':responseReady');
}
}
// Set values to attributes.
this.attributes['index'] = index;
this.attributes['offsetInMilliseconds'] = 0;
this.attributes['playbackIndexChanged'] = true;
controller.play.call(this);
},
playPrevious: function () {
/*
* Called when AMAZON.PreviousIntent or PlaybackController.PreviousCommandIssued is invoked.
* Index is computed using token stored when AudioPlayer.PlaybackStopped command is received.
* If reached at the end of the playlist, choose behavior based on "loop" flag.
*/
var index = this.attributes['index'];
index -= 1;
// Check for last audio file.
if (index === -1) {
if (this.attributes['loop']) {
index = audioData.length - 1;
} else {
// Reached at the end. Thus reset state to start mode and stop playing.
this.handler.state = constants.states.START_MODE;
var message = 'You have reached at the start of the playlist.';
this.response.speak(message).audioPlayerStop();
return this.emit(':responseReady');
}
}
// Set values to attributes.
this.attributes['index'] = index;
this.attributes['offsetInMilliseconds'] = 0;
this.attributes['playbackIndexChanged'] = true;
controller.play.call(this);
},
loopOn: function () {
// Turn on loop play.
this.attributes['loop'] = true;
var message = 'Loop turned on.';
this.response.speak(message);
this.emit(':responseReady');
},
loopOff: function () {
// Turn off looping
this.attributes['loop'] = false;
var message = 'Loop turned off.';
this.response.speak(message);
this.emit(':responseReady');
},
shuffleOn: function () {
// Turn on shuffle play.
this.attributes['shuffle'] = true;
shuffleOrder((newOrder) => {
// Play order have been shuffled. Re-initializing indices and playing first song in shuffled order.
this.attributes['playOrder'] = newOrder;
this.attributes['index'] = 0;
this.attributes['offsetInMilliseconds'] = 0;
this.attributes['playbackIndexChanged'] = true;
controller.play.call(this);
});
},
shuffleOff: function () {
// Turn off shuffle play.
if (this.attributes['shuffle']) {
this.attributes['shuffle'] = false;
// Although changing index, no change in audio file being played as the change is to account for reordering playOrder
this.attributes['index'] = this.attributes['playOrder'][this.attributes['index']];
this.attributes['playOrder'] = Array.apply(null, {length: audioData.length}).map(Number.call, Number);
}
controller.play.call(this);
},
startOver: function () {
// Start over the current audio file.
this.attributes['offsetInMilliseconds'] = 0;
controller.play.call(this);
},
reset: function () {
// Reset to top of the playlist.
this.attributes['index'] = 0;
this.attributes['offsetInMilliseconds'] = 0;
this.attributes['playbackIndexChanged'] = true;
controller.play.call(this);
}
}
}();
function canThrowCard() {
/*
* To determine when can a card should be inserted in the response.
* In response to a PlaybackController Request (remote control events) we cannot issue a card,
* Thus adding restriction of request type being "IntentRequest".
*/
if (this.event.request.type === 'IntentRequest' && this.attributes['playbackIndexChanged']) {
this.attributes['playbackIndexChanged'] = false;
return true;
} else {
return false;
}
}
function shuffleOrder(callback) {
// Algorithm : Fisher-Yates shuffle
var array = Array.apply(null, {length: audioData.length}).map(Number.call, Number);
var currentIndex = array.length;
var temp, randomIndex;
while (currentIndex >= 1) {
randomIndex = Math.floor(Math.random() * currentIndex);
currentIndex -= 1;
temp = array[currentIndex];
array[currentIndex] = array[randomIndex];
array[randomIndex] = temp;
}
callback(array);
}
Upvotes: 1
Views: 4782
Reputation: 1
I was able to set specific indexes when I modified the intents within "playModeIntentHandlers." In the example, they were only setting this.attributes['index'] within "startModeIntentHandlers" - which in my case were never being called.
There are several intents that go in each handler, but I'm only showing 1 (PlaySongIntent) as an example.
var stateHandlers = {
startModeIntentHandlers : Alexa.CreateStateHandler(constants.states.START_MODE, {
'PlaySongIntent' : function () {
if (!this.attributes['playOrder']) {
// Initialize Attributes if undefined.
this.attributes['playOrder'] = Array.apply(null, {length: audioData.length}).map(Number.call, Number);
this.attributes['index'] = 1; //CHANGING THIS NUMBER NEVER WORKED FOR ME.
this.attributes['offsetInMilliseconds'] = 0;
this.attributes['loop'] = false;
this.attributes['shuffle'] = false;
this.attributes['playbackIndexChanged'] = true;
// Change state to START_MODE
this.handler.state = constants.states.START_MODE;
}
controller.play.call(this);
},...}),
playModeIntentHandlers : Alexa.CreateStateHandler(constants.states.PLAY_MODE, {
'PlaySongIntent' : function () {
this.attributes['index'] = 1; //HERE IS WHERE THE INDEX STICKS.
this.attributes['offsetInMilliseconds'] = 0;
this.attributes['playbackIndexChanged'] = true;
controller.play.call(this);
},...})
}
Upvotes: 0
Reputation: 4914
You need to create a custom intent and custom slot to your skill.
Go to your interaction model
add this
{
"intent": "PodcastIntent",
"slots": [
{
"name": "Podcast",
"type": "AMAZON.NUMBER"
}
]
}
In your sample Utterances add
PodcastIntent play episode {Podcast}
This will let user to able to say play episode 140
etc...
Then in your var stateHandlers = {
startModeIntentHandlers
create a new function for PodcastIntent
That part is up to you , the code I write wont work but should give you some idea, maybe something like
'PodcastIntent' : function () { var podname = this.handlerContext.event.request.intent.slots.Podcast.value;
//this should get the value from alexa if user say play episode 140 , podname sould be 140
//then in your audiodata dictionary you need to find episode 140
//again this part is your work
//when you find the url for episode 140
//you can set your state to _PLAY_MODE
//then pass the url to audio player
response().audioPlayerPlay('REPLACE_ALL', podcast.audioURL, token, previousToken, 0);
check the https://github.com/alexa for the AMAZON.NUMBER intents...
Upvotes: 1