├── NOTICE.txt ├── CODE_OF_CONDUCT.md ├── lambda ├── package.json ├── util.js ├── local-debugger.js └── index.js ├── ask-resources.json ├── skill-package ├── skill.json └── interactionModels │ └── custom │ └── en-US.json ├── README.md ├── CONTRIBUTING.md └── LICENSE.txt /NOTICE.txt: -------------------------------------------------------------------------------- 1 | Alexa Skill Kit Audio Player Sample skill. 2 | Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. 5 | -------------------------------------------------------------------------------- /lambda/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hello-world", 3 | "version": "1.2.0", 4 | "description": "alexa utility for quickly building skills", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "Amazon Alexa", 10 | "license": "Apache License", 11 | "dependencies": { 12 | "ask-sdk-core": "^2.7.0", 13 | "ask-sdk-model": "^1.19.0", 14 | "aws-sdk": ">=2.814.0", 15 | "ask-sdk-dynamodb-persistence-adapter": "^2.9.0" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /ask-resources.json: -------------------------------------------------------------------------------- 1 | { 2 | "askcliResourcesVersion": "2020-03-31", 3 | "profiles": { 4 | "default": { 5 | "skillMetadata": { 6 | "src": "./skill-package" 7 | }, 8 | "code": { 9 | "default": { 10 | "src": "./lambda" 11 | } 12 | }, 13 | "skillInfrastructure": { 14 | "type": "@ask-cli/lambda-deployer", 15 | "userConfig": { 16 | "runtime": "nodejs16.x", 17 | "handler": "index.handler" 18 | } 19 | } 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /lambda/util.js: -------------------------------------------------------------------------------- 1 | const AWS = require('aws-sdk'); 2 | 3 | const s3SigV4Client = new AWS.S3({ 4 | signatureVersion: 'v4', 5 | region: process.env.S3_PERSISTENCE_REGION 6 | }); 7 | 8 | module.exports.getS3PreSignedUrl = function getS3PreSignedUrl(s3ObjectKey) { 9 | 10 | const bucketName = process.env.S3_PERSISTENCE_BUCKET; 11 | const s3PreSignedUrl = s3SigV4Client.getSignedUrl('getObject', { 12 | Bucket: bucketName, 13 | Key: s3ObjectKey, 14 | Expires: 60*1 // the Expires is capped for 1 minute 15 | }); 16 | console.log(`Util.s3PreSignedUrl: ${s3ObjectKey} URL ${s3PreSignedUrl}`); 17 | return s3PreSignedUrl; 18 | 19 | } -------------------------------------------------------------------------------- /skill-package/skill.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest": { 3 | "apis": { 4 | "custom": { 5 | "endpoint": { 6 | "uri": "" 7 | }, 8 | "interfaces": [ 9 | { 10 | "type": "AUDIO_PLAYER" 11 | } 12 | ] 13 | } 14 | }, 15 | "manifestVersion": "1.0", 16 | "publishingInformation": { 17 | "category": "MUSIC_AND_AUDIO_ACCESSORIES", 18 | "distributionCountries": [], 19 | "isAvailableWorldwide": true, 20 | "locales": { 21 | "en-US": { 22 | "description": "Sample skill using the AudioPlayer interface", 23 | "examplePhrases": [ 24 | "Alexa open audio player sample" 25 | ], 26 | "keywords": [], 27 | "name": "Audioplayer sample", 28 | "summary": "Sample Short Description" 29 | } 30 | }, 31 | "testingInstructions": "Sample Testing Instructions." 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /skill-package/interactionModels/custom/en-US.json: -------------------------------------------------------------------------------- 1 | { 2 | "interactionModel": { 3 | "languageModel": { 4 | "invocationName": "audio player sample", 5 | "intents": [ 6 | { 7 | "name": "AMAZON.CancelIntent", 8 | "samples": [] 9 | }, 10 | { 11 | "name": "AMAZON.HelpIntent", 12 | "samples": [] 13 | }, 14 | { 15 | "name": "AMAZON.StopIntent", 16 | "samples": [] 17 | }, 18 | { 19 | "name": "PlayAudioIntent", 20 | "slots": [], 21 | "samples": [ 22 | "start my audio", 23 | "play my audio", 24 | "start audio", 25 | "play audio" 26 | ] 27 | }, 28 | { 29 | "name": "AMAZON.NavigateHomeIntent", 30 | "samples": [] 31 | }, 32 | { 33 | "name": "AMAZON.FallbackIntent", 34 | "samples": [] 35 | }, 36 | { 37 | "name": "AMAZON.PauseIntent", 38 | "samples": [] 39 | }, 40 | { 41 | "name": "AMAZON.ResumeIntent", 42 | "samples": [] 43 | } 44 | ], 45 | "types": [] 46 | } 47 | }, 48 | "version": "3" 49 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Build An Alexa Skill with the Audio Player Interface (Node.js) 2 | 3 | This Alexa sample skill is a template for using the AudioPlayer interface for Alexa-hosted skills. 4 | Note that as this code is set up so that you can directly import this skill into your hosted skill. 5 | Check out the Documentation below for links on how to import this skill directly from the Alexa 6 | developer console. 7 | 8 | ## Skill Architecture 9 | The skill consists of an inteface model and logic of the skill. This sample contains a sample skill that plays a single audio stream, 10 | along with handlers for all of the AudioPlayer events, touch controls and error handling. 11 | The skill also uses DynamoDB to keep track of current playback information. 12 | 13 | ## Additional Resources 14 | 15 | ### Documentation 16 | * [AudioPlayer Interface](https://developer.amazon.com/docs/alexa/custom-skills/audioplayer-interface-reference.html) 17 | * [Audio stream/file requirements](https://developer.amazon.com/docs/alexa/custom-skills/audioplayer-interface-reference.html#audio-stream-requirements) 18 | * [Import a skill from a Git repository](https://developer.amazon.com/docs/alexa/hosted-skills/alexa-hosted-skills-git-import.html) 19 | 20 | ### Other Samples 21 | * [Previous AudioPlayer samples (ASK CLI v1, ASK SDK v1)](https://github.com/alexa/skill-sample-nodejs-audio-player/releases) 22 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional 4 | documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary 7 | information to effectively respond to your bug report or contribution. 8 | 9 | 10 | ## Reporting Bugs/Feature Requests 11 | 12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 13 | 14 | When filing an issue, please check [existing open](https://github.com/alexa/skill-sample-nodejs-audio-player/issues), or [recently closed](https://github.com/alexa/skill-sample-nodejs-audio-player/issues?utf8=%E2%9C%93&q=is%3Aissue%20is%3Aclosed%20), issues to make sure somebody else hasn't already 15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 16 | 17 | * A reproducible test case or series of steps 18 | * The version of our code being used 19 | * Any modifications you've made relevant to the bug 20 | * Anything unusual about your environment or deployment 21 | 22 | 23 | ## Contributing via Pull Requests 24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 25 | 26 | 1. You are working against the latest source on the *master* branch. 27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 29 | 30 | To send us a pull request, please: 31 | 32 | 1. Fork the repository. 33 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 34 | 3. Ensure local tests pass. 35 | 4. Commit to your fork using clear commit messages. 36 | 5. Send us a pull request, answering any default questions in the pull request interface. 37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 38 | 39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 41 | 42 | 43 | ## Finding contributions to work on 44 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels ((enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any ['help wanted'](https://github.com/alexa/skill-sample-nodejs-audio-player/labels/help%20wanted) issues is a great place to start. 45 | 46 | 47 | ## Code of Conduct 48 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 49 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 50 | opensource-codeofconduct@amazon.com with any additional questions or comments. 51 | 52 | 53 | ## Security issue notifications 54 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. 55 | 56 | 57 | ## Licensing 58 | 59 | See the [LICENSE](https://github.com/alexa/skill-sample-nodejs-audio-player/blob/master/LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 60 | 61 | We may ask you to sign a [Contributor License Agreement (CLA)](http://en.wikipedia.org/wiki/Contributor_License_Agreement) for larger changes. 62 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | 2 | Amazon Software License 3 | 4 | This Amazon Software License (“License”) governs your use, reproduction, and distribution of the accompanying software as specified below. 5 | 1. Definitions 6 | 7 | “Licensor” means any person or entity that distributes its Work. 8 | 9 | “Software” means the original work of authorship made available under this License. 10 | 11 | “Work” means the Software and any additions to or derivative works of the Software that are made available under this License. 12 | 13 | The terms “reproduce,” “reproduction,” “derivative works,” and “distribution” have the meaning as provided under U.S. copyright law; provided, however, that for the purposes of this License, derivative works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work. 14 | 15 | Works, including the Software, are “made available” under this License by including in or with the Work either (a) a copyright notice referencing the applicability of this License to the Work, or (b) a copy of this License. 16 | 2. License Grants 17 | 18 | 2.1 Copyright Grant. Subject to the terms and conditions of this License, each Licensor grants to you a perpetual, worldwide, non-exclusive, royalty-free, copyright license to reproduce, prepare derivative works of, publicly display, publicly perform, sublicense and distribute its Work and any resulting derivative works in any form. 19 | 20 | 2.2 Patent Grant. Subject to the terms and conditions of this License, each Licensor grants to you a perpetual, worldwide, non-exclusive, royalty-free patent license to make, have made, use, sell, offer for sale, import, and otherwise transfer its Work, in whole or in part. The foregoing license applies only to the patent claims licensable by Licensor that would be infringed by Licensor’s Work (or portion thereof) individually and excluding any combinations with any other materials or technology. 21 | 3. Limitations 22 | 23 | 3.1 Redistribution. You may reproduce or distribute the Work only if (a) you do so under this License, (b) you include a complete copy of this License with your distribution, and (c) you retain without modification any copyright, patent, trademark, or attribution notices that are present in the Work. 24 | 25 | 3.2 Derivative Works. You may specify that additional or different terms apply to the use, reproduction, and distribution of your derivative works of the Work (“Your Terms”) only if (a) Your Terms provide that the use limitation in Section 3.3 applies to your derivative works, and (b) you identify the specific derivative works that are subject to Your Terms. Notwithstanding Your Terms, this License (including the redistribution requirements in Section 3.1) will continue to apply to the Work itself. 26 | 27 | 3.3 Use Limitation. The Work and any derivative works thereof only may be used or intended for use with the web services, computing platforms or applications provided by Amazon.com, Inc. or its affiliates, including Amazon Web Services, Inc. 28 | 29 | 3.4 Patent Claims. If you bring or threaten to bring a patent claim against any Licensor (including any claim, cross-claim or counterclaim in a lawsuit) to enforce any patents that you allege are infringed by any Work, then your rights under this License from such Licensor (including the grants in Sections 2.1 and 2.2) will terminate immediately. 30 | 31 | 3.5 Trademarks. This License does not grant any rights to use any Licensor’s or its affiliates’ names, logos, or trademarks, except as necessary to reproduce the notices described in this License. 32 | 33 | 3.6 Termination. If you violate any term of this License, then your rights under this License (including the grants in Sections 2.1 and 2.2) will terminate immediately. 34 | 4. Disclaimer of Warranty. 35 | 36 | THE WORK IS PROVIDED “AS IS” WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WARRANTIES OR CONDITIONS OF M ERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE OR NON-INFRINGEMENT. YOU BEAR THE RISK OF UNDERTAKING ANY ACTIVITIES UNDER THIS LICENSE. SOME STATES’ CONSUMER LAWS DO NOT ALLOW EXCLUSION OF AN IMPLIED WARRANTY, SO THIS DISCLAIMER MAY NOT APPLY TO YOU. 37 | 5. Limitation of Liability. 38 | 39 | EXCEPT AS PROHIBITED BY APPLICABLE LAW, IN NO EVENT AND UNDER NO LEGAL THEORY, WHETHER IN TORT (INCLUDING NEGLIGENCE), CONTRACT, OR OTHERWISE SHALL ANY LICENSOR BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING OUT OF OR RELATED TO THIS LICENSE, THE USE OR INABILITY TO USE THE WORK (INCLUDING BUT NOT LIMITED TO LOSS OF GOODWILL, BUSINESS INTERRUPTION, LOST PROFITS OR DATA, COMPUTER FAILURE OR MALFUNCTION, OR ANY OTHER COMM ERCIAL DAMAGES OR LOSSES), EVEN IF THE LICENSOR HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. -------------------------------------------------------------------------------- /lambda/local-debugger.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * Licensed under the Apache License, Version 2.0 (the "License"). 4 | * You may not use this file except in compliance with the License. 5 | * A copy of the License is located at 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * or in the "license" file accompanying this file. This file is distributed 9 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 10 | * express or implied. See the License for the specific language governing 11 | * permissions and limitations under the License. 12 | */ 13 | 14 | /* ## DEPRECATION NOTICE 15 | 16 | This script has been deprecated and is no longer supported. 17 | Please use the [ASK Toolkit for VS Code] 18 | (https://marketplace.visualstudio.com/items?itemName=ask-toolkit.alexa-skills-kit-toolkit), 19 | which provides a more end-to-end integration with Visual Studio Code. If you 20 | use another editor/IDE, please check out the [ASK SDK Local Debug package at npm] 21 | (https://www.npmjs.com/package/ask-sdk-local-debug). 22 | 23 | */ 24 | 25 | const net = require('net'); 26 | const fs = require('fs'); 27 | 28 | const localDebugger = net.createServer(); 29 | 30 | const httpHeaderDelimeter = '\r\n'; 31 | const httpBodyDelimeter = '\r\n\r\n'; 32 | const defaultHandlerName = 'handler'; 33 | const host = 'localhost'; 34 | const defaultPort = 0; 35 | 36 | /** 37 | * Resolves the skill invoker class dependency from the user provided 38 | * skill entry file. 39 | */ 40 | 41 | // eslint-disable-next-line import/no-dynamic-require 42 | const skillInvoker = require(getAndValidateSkillInvokerFile()); 43 | const portNumber = getAndValidatePortNumber(); 44 | const lambdaHandlerName = getLambdaHandlerName(); 45 | 46 | /** 47 | * Starts listening on the port for incoming skill requests. 48 | */ 49 | 50 | localDebugger.listen(portNumber, host, () => { 51 | console.log(`Starting server on port: ${localDebugger.address().port}.`); 52 | }); 53 | 54 | /** 55 | * For a new incoming skill request a new socket connection is established. 56 | * From the data received on the socket the request body is extracted, parsed into 57 | * JSON and passed to the skill invoker's lambda handler. 58 | * The response from the lambda handler is parsed as a HTTP 200 message format as specified 59 | * here - https://developer.amazon.com/docs/custom-skills/request-and-response-json-reference.html#http-header-1 60 | * The response is written onto the socket connection. 61 | */ 62 | 63 | localDebugger.on('connection', (socket) => { 64 | console.log(`Connection from: ${socket.remoteAddress}:${socket.remotePort}`); 65 | socket.on('data', (data) => { 66 | const body = JSON.parse(data.toString().split(httpBodyDelimeter).pop()); 67 | console.log(`Request envelope: ${JSON.stringify(body)}`); 68 | skillInvoker[lambdaHandlerName](body, null, (_invokeErr, response) => { 69 | response = JSON.stringify(response); 70 | console.log(`Response envelope: ${response}`); 71 | socket.write(`HTTP/1.1 200 OK${httpHeaderDelimeter}Content-Type: application/json;charset=UTF-8${httpHeaderDelimeter}Content-Length: ${response.length}${httpBodyDelimeter}${response}`); 72 | }); 73 | }); 74 | }); 75 | 76 | /** 77 | * Validates user specified port number is in legal range [0, 65535]. 78 | * Defaults to 0. 79 | */ 80 | 81 | function getAndValidatePortNumber() { 82 | const portNumberArgument = Number(getArgument('portNumber', defaultPort)); 83 | if (!Number.isInteger(portNumberArgument)) { 84 | throw new Error(`Port number has to be an integer - ${portNumberArgument}.`); 85 | } 86 | if (portNumberArgument < 0 || portNumberArgument > 65535) { 87 | throw new Error(`Port out of legal range: ${portNumberArgument}. The port number should be in the range [0, 65535]`); 88 | } 89 | if (portNumberArgument === 0) { 90 | console.log('The TCP server will listen on a port that is free.' 91 | + 'Check logs to find out what port number is being used'); 92 | } 93 | return portNumberArgument; 94 | } 95 | 96 | /** 97 | * Gets the lambda handler name. 98 | * Defaults to "handler". 99 | */ 100 | 101 | function getLambdaHandlerName() { 102 | return getArgument('lambdaHandler', defaultHandlerName); 103 | } 104 | 105 | /** 106 | * Validates that the skill entry file exists on the path specified. 107 | * This is a required field. 108 | */ 109 | 110 | // eslint-disable-next-line consistent-return 111 | function getAndValidateSkillInvokerFile() { 112 | const fileNameArgument = getArgument('skillEntryFile'); 113 | if (!fs.existsSync(fileNameArgument)) { 114 | throw new Error(`File not found: ${fileNameArgument}`); 115 | } 116 | return fileNameArgument; 117 | } 118 | 119 | /** 120 | * Helper function to fetch the value for a given argument 121 | * @param {argumentName} argumentName name of the argument for which the value needs to be fetched 122 | * @param {defaultValue} defaultValue default value of the argument that is returned if the value doesn't exist 123 | */ 124 | 125 | function getArgument(argumentName, defaultValue) { 126 | const index = process.argv.indexOf(`--${argumentName}`); 127 | if (index === -1 || typeof process.argv[index + 1] === 'undefined') { 128 | if (defaultValue === undefined) { 129 | throw new Error(`Required argument - ${argumentName} not provided.`); 130 | } else { 131 | return defaultValue; 132 | } 133 | } 134 | return process.argv[index + 1]; 135 | } 136 | -------------------------------------------------------------------------------- /lambda/index.js: -------------------------------------------------------------------------------- 1 | /* * 2 | * This sample demonstrates handling intents for an Alexa skill implementing the AudioPlayer interface using the Alexa Skills Kit SDK (v2). 3 | * This sample works using the default DynamoDB table associated with an Alexa-hosted skill - you will need to use this with a hosted skill, 4 | * or you use your own DynamoDB table in the request and response interceptors. 5 | * Please visit https://github.com/alexa-samples for additional examples on implementing slots, dialog management, 6 | * session persistence, api calls, and more. 7 | * */ 8 | const Alexa = require('ask-sdk-core'); 9 | const AWS = require('aws-sdk'); 10 | const ddbAdapter = require('ask-sdk-dynamodb-persistence-adapter'); 11 | const Util = require('./util.js'); 12 | 13 | const LaunchRequestHandler = { 14 | canHandle(handlerInput) { 15 | return Alexa.getRequestType(handlerInput.requestEnvelope) === 'LaunchRequest'; 16 | }, 17 | handle(handlerInput) { 18 | const speakOutput = 'Welcome, you can say "play audio" to start listening to music. What would you like to do?'; 19 | 20 | return handlerInput.responseBuilder 21 | .speak(speakOutput) 22 | .reprompt(speakOutput) 23 | .getResponse(); 24 | } 25 | }; 26 | /** 27 | * Intent handler to start playing an audio file. 28 | * By default, it will play a specific audio stream. 29 | * */ 30 | const PlayAudioIntentHandler = { 31 | canHandle(handlerInput) { 32 | return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest' 33 | && (Alexa.getIntentName(handlerInput.requestEnvelope) === 'PlayAudioIntent' 34 | || Alexa.getIntentName(handlerInput.requestEnvelope) === 'AMAZON.ResumeIntent'); 35 | }, 36 | async handle(handlerInput) { 37 | const playbackInfo = await getPlaybackInfo(handlerInput); 38 | 39 | const speakOutput = 'Playing the audio stream.'; 40 | const playBehavior = 'REPLACE_ALL'; 41 | const podcastUrl = 'https://audio1.maxi80.com'; 42 | 43 | /** 44 | * If your audio file is located on the S3 bucket in a hosted skill, you can use the line below to retrieve a presigned URL for the audio file. 45 | * https://developer.amazon.com/docs/alexa/hosted-skills/alexa-hosted-skills-media-files.html 46 | * 47 | * const podcastUrl = Util.getS3PreSignedUrl("Media/audio.mp3").replace(/&/g,'&'); 48 | * 49 | * If you cannot play your own audio in place of the sample URL, make sure your audio file adheres to the guidelines: 50 | * https://developer.amazon.com/docs/alexa/custom-skills/audioplayer-interface-reference.html#audio-stream-requirements 51 | */ 52 | 53 | return handlerInput.responseBuilder 54 | .speak(speakOutput) 55 | .addAudioPlayerPlayDirective( 56 | playBehavior, 57 | podcastUrl, 58 | playbackInfo.token, 59 | playbackInfo.offsetInMilliseconds 60 | ) 61 | .getResponse(); 62 | } 63 | }; 64 | 65 | /** 66 | * Intent handler to start playing an audio file. 67 | * By default, it will play a specific audio stream. 68 | * */ 69 | const PauseAudioIntentHandler = { 70 | canHandle(handlerInput) { 71 | return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest' 72 | && Alexa.getIntentName(handlerInput.requestEnvelope) === 'AMAZON.PauseIntent'; 73 | }, 74 | async handle(handlerInput) { 75 | return handlerInput.responseBuilder 76 | .addAudioPlayerStopDirective() 77 | .getResponse(); 78 | } 79 | }; 80 | /** 81 | * Intent handler for built-in intents that aren't supported in this sample skill. 82 | * As this is a sample skill for a single stream, these intents are irrelevant to this skill. 83 | * Regardless, the skill needs to handle this gracefully, which is why this handler exists. 84 | * */ 85 | const UnsupportedAudioIntentHandler = { 86 | canHandle(handlerInput) { 87 | return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest' 88 | && ( 89 | Alexa.getIntentName(handlerInput.requestEnvelope) === 'AMAZON.LoopOffIntent' 90 | || Alexa.getIntentName(handlerInput.requestEnvelope) === 'AMAZON.LoopOnIntent' 91 | || Alexa.getIntentName(handlerInput.requestEnvelope) === 'AMAZON.NextIntent' 92 | || Alexa.getIntentName(handlerInput.requestEnvelope) === 'AMAZON.PreviousIntent' 93 | || Alexa.getIntentName(handlerInput.requestEnvelope) === 'AMAZON.RepeatIntent' 94 | || Alexa.getIntentName(handlerInput.requestEnvelope) === 'AMAZON.ShuffleOffIntent' 95 | || Alexa.getIntentName(handlerInput.requestEnvelope) === 'AMAZON.ShuffleOnIntent' 96 | || Alexa.getIntentName(handlerInput.requestEnvelope) === 'AMAZON.StartOverIntent' 97 | ); 98 | }, 99 | async handle(handlerInput) { 100 | const speakOutput = 'Sorry, I can\'t support that yet.'; 101 | 102 | return handlerInput.responseBuilder 103 | .speak(speakOutput) 104 | .getResponse(); 105 | } 106 | }; 107 | 108 | const HelpIntentHandler = { 109 | canHandle(handlerInput) { 110 | return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest' 111 | && Alexa.getIntentName(handlerInput.requestEnvelope) === 'AMAZON.HelpIntent'; 112 | }, 113 | handle(handlerInput) { 114 | const speakOutput = 'You can say "play audio" to start playing music! How can I help?'; 115 | 116 | return handlerInput.responseBuilder 117 | .speak(speakOutput) 118 | .reprompt(speakOutput) 119 | .getResponse(); 120 | } 121 | }; 122 | 123 | const CancelAndStopIntentHandler = { 124 | canHandle(handlerInput) { 125 | return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest' 126 | && (Alexa.getIntentName(handlerInput.requestEnvelope) === 'AMAZON.CancelIntent' 127 | || Alexa.getIntentName(handlerInput.requestEnvelope) === 'AMAZON.StopIntent'); 128 | }, 129 | handle(handlerInput) { 130 | const speakOutput = 'Goodbye!'; 131 | 132 | return handlerInput.responseBuilder 133 | .speak(speakOutput) 134 | .getResponse(); 135 | } 136 | }; 137 | /* * 138 | * AudioPlayer events can be triggered when users interact with your audio playback, such as stopping and 139 | * starting the audio, as well as when playback is about to finish playing or playback fails. 140 | * This handler will save the appropriate details for each event and log the details of the exception, 141 | * which can help troubleshoot issues with audio playback. 142 | * */ 143 | const AudioPlayerEventHandler = { 144 | canHandle(handlerInput) { 145 | return handlerInput.requestEnvelope.request.type.startsWith('AudioPlayer.'); 146 | }, 147 | async handle(handlerInput) { 148 | const playbackInfo = await getPlaybackInfo(handlerInput); 149 | 150 | const audioPlayerEventName = handlerInput.requestEnvelope.request.type.split('.')[1]; 151 | console.log(`AudioPlayer event encountered: ${handlerInput.requestEnvelope.request.type}`); 152 | let returnResponseFlag = false; 153 | switch (audioPlayerEventName) { 154 | case 'PlaybackStarted': 155 | playbackInfo.token = handlerInput.requestEnvelope.request.token; 156 | playbackInfo.inPlaybackSession = true; 157 | playbackInfo.hasPreviousPlaybackSession = true; 158 | returnResponseFlag = true; 159 | break; 160 | case 'PlaybackFinished': 161 | playbackInfo.inPlaybackSession = false; 162 | playbackInfo.hasPreviousPlaybackSession = false; 163 | playbackInfo.nextStreamEnqueued = false; 164 | returnResponseFlag = true; 165 | break; 166 | case 'PlaybackStopped': 167 | playbackInfo.token = handlerInput.requestEnvelope.request.token; 168 | playbackInfo.inPlaybackSession = true; 169 | playbackInfo.offsetInMilliseconds = handlerInput.requestEnvelope.request.offsetInMilliseconds; 170 | break; 171 | case 'PlaybackNearlyFinished': 172 | break; 173 | case 'PlaybackFailed': 174 | playbackInfo.inPlaybackSession = false; 175 | console.log('Playback Failed : %j', handlerInput.requestEnvelope.request.error); 176 | break; 177 | default: 178 | break; 179 | } 180 | setPlaybackInfo(handlerInput, playbackInfo); 181 | return handlerInput.responseBuilder.getResponse(); 182 | }, 183 | }; 184 | 185 | 186 | /* * 187 | * PlaybackController events can be triggered when users interact with the audio controls on a device screen. 188 | * starting the audio, as well as when playback is about to finish playing or playback fails. 189 | * This handler will save the appropriate details for each event and log the details of the exception, 190 | * which can help troubleshoot issues with audio playback. 191 | * */ 192 | const PlaybackControllerHandler = { 193 | canHandle(handlerInput) { 194 | return handlerInput.requestEnvelope.request.type.startsWith('PlaybackController.'); 195 | }, 196 | async handle(handlerInput) { 197 | const playbackInfo = await getPlaybackInfo(handlerInput); 198 | const playBehavior = 'REPLACE_ALL'; 199 | const podcastUrl = 'https://audio1.maxi80.com'; 200 | const playbackControllerEventName = handlerInput.requestEnvelope.request.type.split('.')[1]; 201 | let response; 202 | switch (playbackControllerEventName) { 203 | case 'PlayCommandIssued': 204 | response = handlerInput.responseBuilder 205 | .addAudioPlayerPlayDirective( 206 | playBehavior, 207 | podcastUrl, 208 | playbackInfo.token, 209 | playbackInfo.offsetInMilliseconds 210 | ) 211 | .getResponse(); 212 | break; 213 | case 'PauseCommandIssued': 214 | response = handlerInput.responseBuilder 215 | .addAudioPlayerStopDirective() 216 | .getResponse(); 217 | break; 218 | default: 219 | break; 220 | } 221 | setPlaybackInfo(handlerInput, playbackInfo); 222 | 223 | console.log(`PlayCommandIssued event encountered: ${handlerInput.requestEnvelope.request.type}`); 224 | return response; 225 | }, 226 | }; 227 | /* * 228 | * SystemExceptions can be triggered if there is a problem with the audio that is trying to be played. 229 | * This handler will log the details of the exception and can help troubleshoot issues with audio playback. 230 | * */ 231 | const SystemExceptionHandler = { 232 | canHandle(handlerInput) { 233 | return handlerInput.requestEnvelope.request.type === 'System.ExceptionEncountered'; 234 | }, 235 | handle(handlerInput) { 236 | console.log(`System exception encountered: ${JSON.stringify(handlerInput.requestEnvelope.request)}`); 237 | }, 238 | }; 239 | 240 | /* * 241 | * FallbackIntent triggers when a customer says something that doesn’t map to any intents in your skill 242 | * It must also be defined in the language model (if the locale supports it) 243 | * This handler can be safely added but will be ingnored in locales that do not support it yet 244 | * */ 245 | const FallbackIntentHandler = { 246 | canHandle(handlerInput) { 247 | return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest' 248 | && Alexa.getIntentName(handlerInput.requestEnvelope) === 'AMAZON.FallbackIntent'; 249 | }, 250 | handle(handlerInput) { 251 | const speakOutput = 'Sorry, I don\'t know about that. Please try again.'; 252 | 253 | return handlerInput.responseBuilder 254 | .speak(speakOutput) 255 | .reprompt(speakOutput) 256 | .getResponse(); 257 | } 258 | }; 259 | /* * 260 | * SessionEndedRequest notifies that a session was ended. This handler will be triggered when a currently open 261 | * session is closed for one of the following reasons: 1) The user says "exit" or "quit". 2) The user does not 262 | * respond or says something that does not match an intent defined in your voice model. 3) An error occurs 263 | * */ 264 | const SessionEndedRequestHandler = { 265 | canHandle(handlerInput) { 266 | return Alexa.getRequestType(handlerInput.requestEnvelope) === 'SessionEndedRequest'; 267 | }, 268 | handle(handlerInput) { 269 | console.log(`~~~~ Session ended: ${JSON.stringify(handlerInput.requestEnvelope)}`); 270 | // Any cleanup logic goes here. 271 | return handlerInput.responseBuilder.getResponse(); // notice we send an empty response 272 | } 273 | }; 274 | /* * 275 | * The intent reflector is used for interaction model testing and debugging. 276 | * It will simply repeat the intent the user said. You can create custom handlers for your intents 277 | * by defining them above, then also adding them to the request handler chain below 278 | * */ 279 | const IntentReflectorHandler = { 280 | canHandle(handlerInput) { 281 | return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest'; 282 | }, 283 | handle(handlerInput) { 284 | const intentName = Alexa.getIntentName(handlerInput.requestEnvelope); 285 | const speakOutput = `You just triggered ${intentName}`; 286 | 287 | return handlerInput.responseBuilder 288 | .speak(speakOutput) 289 | //.reprompt('add a reprompt if you want to keep the session open for the user to respond') 290 | .getResponse(); 291 | } 292 | }; 293 | /** 294 | * Generic error handling to capture any syntax or routing errors. If you receive an error 295 | * stating the request handler chain is not found, you have not implemented a handler for 296 | * the intent being invoked or included it in the skill builder below 297 | * */ 298 | const ErrorHandler = { 299 | canHandle() { 300 | return true; 301 | }, 302 | handle(handlerInput, error) { 303 | const speakOutput = 'Sorry, I had trouble doing what you asked. Please try again.'; 304 | console.log(`~~~~ Error handled: ${JSON.stringify(error)}`); 305 | 306 | return handlerInput.responseBuilder 307 | .speak(speakOutput) 308 | .reprompt(speakOutput) 309 | .getResponse(); 310 | } 311 | }; 312 | 313 | /* HELPER FUNCTIONS */ 314 | 315 | async function getPlaybackInfo(handlerInput) { 316 | const attributes = await handlerInput.attributesManager.getPersistentAttributes(); 317 | return attributes.playbackInfo; 318 | } 319 | 320 | async function setPlaybackInfo(handlerInput, playbackInfoObject) { 321 | await handlerInput.attributesManager.setPersistentAttributes({ 322 | playbackInfo: playbackInfoObject 323 | }); 324 | } 325 | 326 | // Request and response interceptors using the DynamoDB table associated with Alexa-hosted skills 327 | 328 | const LoadPersistentAttributesRequestInterceptor = { 329 | async process(handlerInput) { 330 | const persistentAttributes = await handlerInput.attributesManager.getPersistentAttributes(); 331 | 332 | /** 333 | * Check if user is invoking the skill the first time and initialize preset values 334 | playbackInfo: { 335 | offsetInMilliseconds - this is used to set the offset of the audio file 336 | to save the position between sessions 337 | token - save an audio token for this play session 338 | inPlaybackSession - used to record the playback state of the session 339 | hasPreviousPlaybackSession - used to help confirm previous playback state 340 | } 341 | */ 342 | if (Object.keys(persistentAttributes).length === 0) { 343 | handlerInput.attributesManager.setPersistentAttributes({ 344 | playbackInfo: { 345 | offsetInMilliseconds: 0, 346 | token: 'sample-audio-token', 347 | inPlaybackSession: false, 348 | hasPreviousPlaybackSession: false, 349 | }, 350 | }); 351 | } 352 | }, 353 | }; 354 | 355 | const SavePersistentAttributesResponseInterceptor = { 356 | async process(handlerInput) { 357 | await handlerInput.attributesManager.savePersistentAttributes(); 358 | }, 359 | }; 360 | 361 | /** 362 | * This handler acts as the entry point for your skill, routing all request and response 363 | * payloads to the handlers above. Make sure any new handlers or interceptors you've 364 | * defined are included below. The order matters - they're processed top to bottom 365 | * */ 366 | exports.handler = Alexa.SkillBuilders.custom() 367 | .addRequestHandlers( 368 | LaunchRequestHandler, 369 | PlayAudioIntentHandler, 370 | PauseAudioIntentHandler, 371 | UnsupportedAudioIntentHandler, 372 | HelpIntentHandler, 373 | CancelAndStopIntentHandler, 374 | AudioPlayerEventHandler, 375 | PlaybackControllerHandler, 376 | SystemExceptionHandler, 377 | FallbackIntentHandler, 378 | SessionEndedRequestHandler, 379 | IntentReflectorHandler) 380 | .addErrorHandlers( 381 | ErrorHandler) 382 | .addRequestInterceptors(LoadPersistentAttributesRequestInterceptor) 383 | .addResponseInterceptors(SavePersistentAttributesResponseInterceptor) 384 | .withCustomUserAgent('sample/audioplayer-nodejs/v2.0') 385 | .withPersistenceAdapter( 386 | new ddbAdapter.DynamoDbPersistenceAdapter({ 387 | tableName: process.env.DYNAMODB_PERSISTENCE_TABLE_NAME, 388 | createTable: false, 389 | dynamoDBClient: new AWS.DynamoDB({apiVersion: 'latest', region: process.env.DYNAMODB_PERSISTENCE_REGION}) 390 | }) 391 | ) 392 | .lambda(); 393 | --------------------------------------------------------------------------------