Skip to content

Commit

Permalink
Initial release of audioplayer samples
Browse files Browse the repository at this point in the history
  • Loading branch information
nikhilym committed Oct 19, 2018
1 parent e1fac5f commit 25b5bd5
Show file tree
Hide file tree
Showing 35 changed files with 3,336 additions and 3 deletions.
32 changes: 32 additions & 0 deletions MultiStream/.ask/config
@@ -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"
}
}
}
}
}
}
}
}
115 changes: 115 additions & 0 deletions MultiStream/README.md
@@ -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.
35 changes: 35 additions & 0 deletions MultiStream/lambda/py/alexa/data.py
@@ -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",
}
]
154 changes: 154 additions & 0 deletions MultiStream/lambda/py/alexa/util.py
@@ -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)

0 comments on commit 25b5bd5

Please sign in to comment.