Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Initial release of audioplayer samples
- Loading branch information
Showing
35 changed files
with
3,336 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
{ | ||
"deploy_settings": { | ||
"default": { | ||
"skill_id": "amzn1.ask.skill.e5c21f3d-32a7-49b1-b341-d09b8f650fbe", | ||
"was_cloned": false, | ||
"resources": { | ||
"manifest": { | ||
"eTag": "1a9a9cec03b0583af5828d1db0d11ff9" | ||
}, | ||
"interactionModel": { | ||
"en-US": { | ||
"eTag": "8a62193c7f6432661809bc883985beb2" | ||
}, | ||
"en-GB": { | ||
"eTag": "28bc1baebe415480fb571e1ac3d1c1c1" | ||
} | ||
} | ||
}, | ||
"merge": { | ||
"manifest": { | ||
"apis": { | ||
"custom": { | ||
"endpoint": { | ||
"uri": "ask-custom-multistream-audioplayer-default" | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,115 @@ | ||
# Multi Stream Audio Player Sample Skill (using ASK Python SDK) | ||
|
||
The Alexa Skills Kit now allows developers to build skills that play long-form audio content on Alexa devices. This sample project demonstrates how to use the new interfaces for triggering playback of audio and handling audio player input events. | ||
|
||
## How to Run the Sample | ||
|
||
You will need to comply to the prerequisites below and to change a few configuration files before creating the skill and upload the lambda code. | ||
|
||
### Pre-requisites | ||
|
||
0. This sample uses the [ASK Python SDK](https://alexa-skills-kit-python-sdk.readthedocs.io/en/latest/) packages for developing the Alexa skill. | ||
|
||
- If you already have the ASK Python SDK installed, then install the dependencies in the ``lambda/py/requirements.txt`` | ||
using ``pip``. | ||
- If you are starting fresh, follow the [Setting up the ASK SDK](https://alexa-skills-kit-python-sdk.readthedocs.io/en/latest/GETTING_STARTED.html) | ||
documentation, to get the ASK Python SDK installed on your machine. We recommend you to use the | ||
[virtualenv approach](https://alexa-skills-kit-python-sdk.readthedocs.io/en/latest/GETTING_STARTED.html#option-1-set-up-the-sdk-in-a-virtual-environment). | ||
Please run the following command in your virtualenv, to install the dependencies, before working on the skill code. | ||
|
||
`` | ||
pip install -r lambda/py/requirements.txt | ||
`` | ||
|
||
1. You need an [AWS account](https://aws.amazon.com) and an [Amazon developer account](https://developer.amazon.com) to create an Alexa Skill. | ||
|
||
|
||
### Code changes before deploying | ||
|
||
1. ```./skill.json``` | ||
|
||
Change the skill name, example phrase, icons, testing instructions etc ... | ||
|
||
Remember than most information is locale-specific and must be changed for each locale (en-GB and en-US) | ||
|
||
Please refer to https://developer.amazon.com/docs/smapi/skill-manifest.html for details about manifest values. | ||
|
||
2. ```./lambda/py/alexa/data.py``` | ||
|
||
Modify each value in the ```data.py``` file to provide your skill with the correct runtime values for ``AUDIO_DATA``, different responses by Alexa, DynamoDB table name. | ||
|
||
To learn more about Alexa App cards, see https://developer.amazon.com/docs/custom-skills/include-a-card-in-your-skills-response.html | ||
|
||
3. ```./models/*.json``` | ||
|
||
Change the model definition to replace the invocation name (it defaults to "audio player") and the sample phrases for each intent. | ||
|
||
Repeat the operation for each locale you are planning to support. | ||
|
||
4. DynamoDB table | ||
|
||
The dynamodb table is used to store the playback settings information of the user. If the table does not exist, the persistence code will create the table at the first invocation of the skill. | ||
|
||
You can manually create the DynamoDB table with the following command: | ||
|
||
```bash | ||
aws dynamodb create-table --table-name Audio-Player-Multi-Stream --attribute-definitions AttributeName=id,AttributeType=S --key-schema AttributeName=id,KeyType=HASH --provisioned-throughput ReadCapacityUnits=5,WriteCapacityUnits=5 | ||
``` | ||
|
||
To minimize latency, we recommend to create the DynamoDB table in the same region as the Lambda function. | ||
|
||
When using DynamoDB, you also must ensure your Lambda function [execution role](http://docs.aws.amazon.com/lambda/latest/dg/intro-permission-model.html) will have permissions to read and write to the DynamoDB table. Be sure [to add the following policy](http://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_manage_modify.html) to the Lambda function [execution role](http://docs.aws.amazon.com/lambda/latest/dg/intro-permission-model.html): | ||
|
||
```json | ||
{ | ||
"Version": "2012-10-17", | ||
"Statement": [ | ||
{ | ||
"Sid": "sid123", | ||
"Effect": "Allow", | ||
"Action": [ | ||
"dynamodb:PutItem", | ||
"dynamodb:GetItem", | ||
"dynamodb:UpdateItem" | ||
], | ||
"Resource": "arn:aws:dynamodb:us-east-1:YOUR_ACCOUNT_ID:table/Audio-Player-Multi-Stream" | ||
} | ||
] | ||
} | ||
``` | ||
|
||
### Deployment | ||
|
||
For AWS Lambda to correctly execute the skill code, we need to zip the skill code along | ||
with all dependencies and upload it. Follow the steps mentioned [here](https://alexa-skills-kit-python-sdk.readthedocs.io/en/latest/DEVELOPING_YOUR_FIRST_SKILL.html#preparing-your-code-for-aws-lambda) | ||
to get your skill code ready for uploading to AWS Lambda console. | ||
|
||
## On Device Tests | ||
|
||
To invoke the skill from your device, you need to login to the Alexa Developer Console, and enable the "Test" switch on your skill. | ||
|
||
See https://developer.amazon.com/docs/smapi/quick-start-alexa-skills-kit-command-line-interface.html#step-4-test-your-skill for more testing instructions. | ||
|
||
Then, just say : | ||
|
||
```text | ||
Alexa, open audio player. | ||
``` | ||
|
||
## How it Works | ||
|
||
Alexa Skills Kit now includes a set of output directives and input events that allow you to control the playback of audio files or streams. There are a few important concepts to get familiar with: | ||
|
||
* **AudioPlayer directives** are used by your skill to start and stop audio playback from content hosted at a publicly accessible secure URL. You send AudioPlayer directives in response to the intents you've configured for your skill, or new events you'll receive when a user controls their device with a dedicated controller (see PlaybackController events below). | ||
* **PlaybackController events** are sent to your skill when a user selects play/next/prev/pause on dedicated hardware controls on the Alexa device, such as on the Amazon Tap or the Voice Remote for Amazon Echo and Echo Dot. Your skill receives these events if your skill is currently controlling audio on the device (i.e., you were the last to send an AudioPlayer directive). | ||
* **AudioPlayer events** are sent to your skill at key changes in the status of audio playback, such as when audio has begun playing, been stopped or has finished. You can use them to track what's currently playing or queue up more content. Unlike intents, when you receive an AudioPlayer event, you may only respond with appropriate AudioPlayer directives to control playback. | ||
|
||
You can learn more about the new [AudioPlayer interface](https://developer.amazon.com/public/solutions/alexa/alexa-skills-kit/docs/custom-audioplayer-interface-reference) and [PlaybackController interface](https://developer.amazon.com/public/solutions/alexa/alexa-skills-kit/docs/custom-playbackcontroller-interface-reference). | ||
|
||
## Cleanup | ||
|
||
If you were deploying this skill just for learning purposes or for testing, do not forget to clean your AWS account to avoid recurring charges for your DynamoDB table. | ||
|
||
- delete the lambda function | ||
- delete the IAM execution role | ||
- delete the DynamoDB table (Audio-Player-Multi-Stream) |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
# -*- coding: utf-8 -*- | ||
|
||
WELCOME_MSG = "Welcome to the Alexa Dev Chat Podcast. You can say, play the audio to begin the podcast." | ||
WELCOME_REPROMPT_MSG = "You can say, play the audio, to begin." | ||
WELCOME_PLAYBACK_MSG = "You were listening to {}. Would you like to resume?" | ||
WELCOME_PLAYBACK_REPROMPT_MSG = "You can say yes to resume or no to play from the top" | ||
DEVICE_NOT_SUPPORTED = "Sorry, this skill is not supported on this device" | ||
LOOP_ON_MSG = "Loop turned on." | ||
LOOP_OFF_MSG = "Loop turned off." | ||
HELP_MSG = WELCOME_MSG | ||
HELP_PLAYBACK_MSG = WELCOME_PLAYBACK_MSG | ||
HELP_DURING_PLAY_MSG = "You are listening to the Alexa Dev Chat 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." | ||
STOP_MSG = "Goodbye." | ||
EXCEPTION_MSG = "Sorry, this is not a valid command. Please say help, to hear what you can say." | ||
PLAYBACK_PLAY = "This is {}" | ||
PLAYBACK_PLAY_CARD = "Playing {}" | ||
PLAYBACK_NEXT_END = "You have reached the end of the playlist" | ||
PLAYBACK_PREVIOUS_END = "You have reached the start of the playlist" | ||
|
||
DYNAMODB_TABLE_NAME = "Audio-Player-Multi-Stream" | ||
|
||
AUDIO_DATA = [ | ||
{ | ||
"title": "Episode 22", | ||
"url": "https://feeds.soundcloud.com/stream/459953355-user-652822799-episode-022-getting-started-with-alexa-for-business.mp3", | ||
}, | ||
{ | ||
"title": "Episode 23", | ||
"url": "https://feeds.soundcloud.com/stream/476469807-user-652822799-episode-023-voicefirst-in-2018-where-are-we-now.mp3", | ||
}, | ||
{ | ||
"title": "Episode 24", | ||
"url": "https://feeds.soundcloud.com/stream/496340574-user-652822799-episode-024-the-voice-generation-will-include-all-generations.mp3", | ||
} | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,154 @@ | ||
# -*- coding: utf-8 -*- | ||
|
||
import random | ||
from typing import List, Dict | ||
from ask_sdk_model import IntentRequest, Response | ||
from ask_sdk_model.ui import SimpleCard | ||
from ask_sdk_model.interfaces.audioplayer import ( | ||
PlayDirective, PlayBehavior, AudioItem, Stream, StopDirective) | ||
from ask_sdk_core.handler_input import HandlerInput | ||
from . import data | ||
|
||
|
||
def get_playback_info(handler_input): | ||
# type: (HandlerInput) -> Dict | ||
persistence_attr = handler_input.attributes_manager.persistent_attributes | ||
return persistence_attr.get('playback_info') | ||
|
||
|
||
def can_throw_card(handler_input): | ||
# type: (HandlerInput) -> bool | ||
playback_info = get_playback_info(handler_input) | ||
if (isinstance(handler_input.request_envelope.request, IntentRequest) | ||
and playback_info.get('playback_index_changed')): | ||
playback_info['playback_index_changed'] = False | ||
return True | ||
else: | ||
return False | ||
|
||
|
||
def get_token(handler_input): | ||
"""Extracting token received in the request.""" | ||
# type: (HandlerInput) -> str | ||
return handler_input.request_envelope.request.token | ||
|
||
|
||
def get_index(handler_input): | ||
"""Extracting index from the token received in the request.""" | ||
# type: (HandlerInput) -> int | ||
token = int(get_token(handler_input)) | ||
persistent_attr = handler_input.attributes_manager.persistent_attributes | ||
|
||
return persistent_attr.get("playback_info").get("play_order").index(token) | ||
|
||
|
||
def get_offset_in_ms(handler_input): | ||
"""Extracting offset in milliseconds received in the request""" | ||
# type: (HandlerInput) -> int | ||
return handler_input.request_envelope.request.offset_in_milliseconds | ||
|
||
|
||
def shuffle_order(): | ||
# type: () -> List | ||
podcast_indices = [l for l in range(0, len(data.AUDIO_DATA))] | ||
random.shuffle(podcast_indices) | ||
return podcast_indices | ||
|
||
|
||
class Controller: | ||
"""Audioplayer and Playback Controller.""" | ||
@staticmethod | ||
def play(handler_input, is_playback=False): | ||
# type: (HandlerInput) -> Response | ||
playback_info = get_playback_info(handler_input) | ||
response_builder = handler_input.response_builder | ||
|
||
play_order = playback_info.get("play_order") | ||
offset_in_ms = playback_info.get("offset_in_ms") | ||
index = playback_info.get("index") | ||
|
||
play_behavior = PlayBehavior.REPLACE_ALL | ||
podcast = data.AUDIO_DATA[play_order[index]] | ||
token = play_order[index] | ||
playback_info['next_stream_enqueued'] = False | ||
|
||
response_builder.add_directive( | ||
PlayDirective( | ||
play_behavior=play_behavior, | ||
audio_item=AudioItem( | ||
stream=Stream( | ||
token=token, | ||
url=podcast.get("url"), | ||
offset_in_milliseconds=offset_in_ms, | ||
expected_previous_token=None), | ||
metadata=None)) | ||
).set_should_end_session(True) | ||
|
||
if not is_playback: | ||
# Add card and response only for events not triggered by | ||
# Playback Controller | ||
handler_input.response_builder.speak( | ||
data.PLAYBACK_PLAY.format(podcast.get("title"))) | ||
|
||
if can_throw_card(handler_input): | ||
response_builder.set_card(SimpleCard( | ||
title=data.PLAYBACK_PLAY_CARD.format( | ||
podcast.get("title")), | ||
content=data.PLAYBACK_PLAY_CARD.format( | ||
podcast.get("title")))) | ||
|
||
return response_builder.response | ||
|
||
@staticmethod | ||
def stop(handler_input): | ||
# type: (HandlerInput) -> Response | ||
handler_input.response_builder.add_directive(StopDirective()) | ||
return handler_input.response_builder.response | ||
|
||
@staticmethod | ||
def play_next(handler_input, is_playback=False): | ||
# type: (HandlerInput) -> Response | ||
persistent_attr = handler_input.attributes_manager.persistent_attributes | ||
|
||
playback_info = persistent_attr.get("playback_info") | ||
playback_setting = persistent_attr.get("playback_setting") | ||
next_index = (playback_info.get("index") + 1) % len(data.AUDIO_DATA) | ||
|
||
if next_index == 0 and not playback_setting.get("loop"): | ||
if not is_playback: | ||
handler_input.response_builder.speak(data.PLAYBACK_NEXT_END) | ||
|
||
return handler_input.response_builder.add_directive( | ||
StopDirective()).response | ||
|
||
playback_info["index"] = next_index | ||
playback_info["offset_in_ms"] = 0 | ||
playback_info["playback_index_changed"] = True | ||
|
||
return Controller.play(handler_input, is_playback) | ||
|
||
@staticmethod | ||
def play_previous(handler_input, is_playback=False): | ||
# type: (HandlerInput) -> Response | ||
persistent_attr = handler_input.attributes_manager.persistent_attributes | ||
|
||
playback_info = persistent_attr.get("playback_info") | ||
playback_setting = persistent_attr.get("playback_setting") | ||
prev_index = playback_info.get("index") - 1 | ||
|
||
if prev_index == -1: | ||
if playback_setting.get("loop"): | ||
prev_index += len(data.AUDIO_DATA) | ||
else: | ||
if not is_playback: | ||
handler_input.response_builder.speak( | ||
data.PLAYBACK_PREVIOUS_END) | ||
|
||
return handler_input.response_builder.add_directive( | ||
StopDirective()).response | ||
|
||
playback_info["index"] = prev_index | ||
playback_info["offset_in_ms"] = 0 | ||
playback_info["playback_index_changed"] = True | ||
|
||
return Controller.play(handler_input, is_playback) |
Oops, something went wrong.