rgraciano / echo-sonos

Amazon Echo integration with Sonos
Other
712 stars 234 forks source link

audio player directives #57

Closed jratliff681 closed 7 years ago

jratliff681 commented 8 years ago

So I noticed today the new "Audio Player" option under skill information on developer.amazon.com.

Here's the link to the amazon information about it. https://developer.amazon.com/public/solutions/alexa/alexa-skills-kit/docs/custom-audioplayer-interface-reference

It seems it is looking to stream audio to the echo, but could we also use this to just control our Sonos commands? The part of it that looks useful is where it says if your skill was the last skill marked to be playing audio that you could then use any of the built in amazon commands next / previous / shuffle / stop / etc WITHOUT invoking the skill name! This would be nice to have if it's possible. I'm trying to read over it but wanted to share with those of you who may have a little better understanding than me.

mdcampbell commented 7 years ago

I've got this working, and it's really great. The main limitation of course, is that you need to use an "Alexa, ask sonos ..." command before you can use the short, native form - i.e. "Alexa next | previous | shuffle", etc. At some point, perhaps Amazon will let users specify a select skill as their default audio player...

It's a bit of a hack and I haven't cleaned it up much. But the basic idea is this:

You'll need to make edits to AlexaSkills.js and index.js. Let me know how it goes for you.

AlexaSkill.js

Response.prototype = (function () {
    var buildSpeechletResponse = function (options) {
        var alexaResponse = {
            outputSpeech: createSpeechObject(options.output),
            shouldEndSession: options.shouldEndSession
        };
        if (options.reprompt) {
            alexaResponse.reprompt = {
                outputSpeech: createSpeechObject(options.reprompt)
            };
        }
        if (options.cardTitle && options.cardContent) {
            alexaResponse.card = {
                type: "Simple",
                title: options.cardTitle,
                content: options.cardContent
            };
        }

        // START INSERT
        // Add directive to play a dummy audio file and thereby assume responsibility for native Amazon AudioPlay intents
        alexaResponse.directives = [ {
            type: "AudioPlayer.Play",
            playBehavior: "REPLACE_ALL",
            audioItem: {
                stream: {
                    token: "0123456789", // Dummy token
                    url: "<<<INSERT LINK TO MP3 FILE HERE>>>", // Five seconds of silence
                    offsetInMilliseconds: 0
                }
            }
        }];
        // END INSERT

        var returnResult = {
                version: '1.0',
                response: alexaResponse
        };
        if (options.session && options.session.attributes) {
            returnResult.sessionAttributes = options.session.attributes;
        }
        return returnResult;
    };

    return {
        tell: function (speechOutput) {
            this._context.succeed(buildSpeechletResponse({
                session: this._session,
                output: speechOutput,
                shouldEndSession: true
            }));
        },
        tellWithCard: function (speechOutput, cardTitle, cardContent) {
            this._context.succeed(buildSpeechletResponse({
                session: this._session,
                output: speechOutput,
                cardTitle: cardTitle,
                cardContent: cardContent,
                shouldEndSession: true
            }));
        },
        ask: function (speechOutput, repromptSpeech) {
            this._context.succeed(buildSpeechletResponse({
                session: this._session,
                output: speechOutput,
                reprompt: repromptSpeech,
                shouldEndSession: false
            }));
        },
        askWithCard: function (speechOutput, repromptSpeech, cardTitle, cardContent) {
            this._context.succeed(buildSpeechletResponse({
                session: this._session,
                output: speechOutput,
                reprompt: repromptSpeech,
                cardTitle: cardTitle,
                cardContent: cardContent,
                shouldEndSession: false
            }));
        }
    };
})();

index.js

    'AMAZON.PauseIntent': function (intent, session, response) {
        console.log("Amazon PauseIntent received");
        loadCurrentRoomAndService('DefaultEcho', null, function(room, service) {
            options.path = '/' + encodeURIComponent(room) + '/pause';
            httpreq(options, function(error) {
                genericResponse(error, response);
            });
        });
    },

    'AMAZON.StopIntent': function (intent, session, response) {
        console.log("Amazon PauseIntent received");
        loadCurrentRoomAndService('DefaultEcho', null, function(room, service) {
            options.path = '/' + encodeURIComponent(room) + '/pause';
            httpreq(options, function(error) {
                genericResponse(error, response);
            });
        });
    },

    'AMAZON.CancelIntent': function (intent, session, response) {
        console.log("Amazon PauseIntent received");
        loadCurrentRoomAndService('DefaultEcho', null, function(room, service) {
            options.path = '/' + encodeURIComponent(room) + '/pause';
            httpreq(options, function(error) {
                genericResponse(error, response);
            });
        });
    },

    'AMAZON.ResumeIntent': function (intent, session, response) {
        console.log("Amazon ResumeIntent received");
        loadCurrentRoomAndService('DefaultEcho', null, function(room, service) {
            options.path = '/' + encodeURIComponent(room) + '/play';
            httpreq(options, function(error) {
                genericResponse(error, response);
            });
        });
    },

    'AMAZON.NextIntent': function (intent, session, response) {
        console.log("NextTrackIntent received");
        loadCurrentRoomAndService('DefaultEcho', null, function(room, service) {
            actOnCoordinator(options, '/next', room,  function (error, responseBodyJson) {
                genericResponse(error, response);
            });
        });
    },

    'AMAZON.PreviousIntent': function (intent, session, response) {
        console.log("PreviousTrackIntent received");
        loadCurrentRoomAndService('DefaultEcho', null, function(room, service) {
            actOnCoordinator(options, '/previous', room,  function (error, responseBodyJson) {
                genericResponse(error, response);
            });
        });
    },

    'AMAZON.LoopOnIntent': function (intent, session, response) {
        console.log("RepeatIntent received");
        loadCurrentRoomAndService('DefaultEcho', null, function(room, service) {
            toggleHandler(room, 'on', "repeat", response);
        });
    },

    'AMAZON.LoopOffIntent': function (intent, session, response) {
        console.log("RepeatIntent received");
        loadCurrentRoomAndService('DefaultEcho', null, function(room, service) {
            toggleHandler(room, 'off', "repeat", response);
        });
    },

    'AMAZON.ShuffleOnIntent': function (intent, session, response) {
        console.log("ShuffleIntent received");
        loadCurrentRoomAndService('DefaultEcho', null, function(room, service) {
            toggleHandler(room, 'on', "shuffle", response);
        });
    },

    'AMAZON.ShuffleOffIntent': function (intent, session, response) {
        console.log("ShuffleIntent received");
        loadCurrentRoomAndService('DefaultEcho', null, function(room, service) {
            toggleHandler(room, 'off', "shuffle", response);
        });
    }
jratliff681 commented 7 years ago

Thank you so much! I modified my index.js with all the intents as soon as I posted this a while ago. I just couldn't figure out what to add to invoke the new Alexa Skill commands to get it to trigger the audio player part. I'll add those lines while I'm at work and test it when I get home this weekend, looking forward to trying it!

My idea I was going to try was to send the AudioPlayer.Stop command even if nothing was playing and see if that triggered the echo to mark the skill as the last audio directing skill. The silence is a good idea too. I usually host my audio files in an Amazon S3 bucket.

jratliff681 commented 7 years ago

It works great, thanks mdcampbell!

I have 5 separate skills with invocation names of each of my speaker room names. I noticed each echo is independent of each other when saving the last used audio player skill. So if I say "Alexa, tell the living room to play" on the living room echo, and "Alexa, tell the kitchen to play" on the kitchen dot, and so on in each room, then everytime I can just come back to any room and say "play/pause/next/previous" and it always controls the correct speaker! (As long as I didn't start a room from a different room).

I still keep a base "Sonos" skill that isn't designated as an audio player skill so that I can control music in another room without changing which skill is currently controlling the audio commands on that echo.

mdcampbell commented 7 years ago

Sounds like a sweet setup. Glad it's working for you!

rgraciano commented 7 years ago

This sounds like a great upgrade. @mdcampbell, any reason not to submit this as a PR and change the way invocation works for the skill?