<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0" />
<base href="https://wiki.asterisk.org/wiki" />
<title>Message Title</title>
<style type="text/css">@media only screen and (max-device-width: 480px) {.mobile-only {
width: auto !important;
height: auto !important;
overflow: visible !important;
line-height: normal !important;
font-size: inherit !important;
mso-hide: all;
}
.desktop-only {
display: none !important;
}
/* iPhone 3GS fix for unwanted 20px right margin */
body { min-width: 100% !important; padding: 0; margin: 0; }
#center-content-table { max-width: none; !important; }
#header-pattern-container { padding: 10px 10px 10px 10px !important; line-height: 20px !important; }
#header-avatar-image-container { padding-right: 8px !important; }
#email-content-container { padding: 0 !important; }
.mobile-expand { border-radius: 0 !important; border-left: 0 !important; border-right: 0 !important; padding-left: 26px !important;}
.mobile-resize-text { font-size: 16px !important; line-height: 22px !important; }
#page-title-pattern-header { font-size: 20px !important; line-height: 28px !important; }
#page-title-pattern-icon-image-container-cell { padding-top: 7px !important; }
#inline-user-pattern { display: block !important; }
#inline-user-pattern-avatar { padding-top: 3px !important; }
.contextual-area-pattern { border-bottom: 1px solid #ccc !important; padding: 15px 10px 0 10px !important;}
.users-involved-pattern-column-table { width: 100% !important; }
.users-involved-pattern-avatar-table-cell { padding: 3px 5px 5px 0 !important; }
.users-involved-pattern-column-container { padding-right: 0 !important; }
.contextual-excerpt-pattern, #users-involved-pattern { border: 0 !important; }
/** Aui Typography upsized for mobile **/
#content-excerpt-pattern-container, #contextual-excerpt-pattern-text-container { font-size: 16px !important; line-height: 22px !important; }
#content-excerpt-pattern-container h1, #contextual-excerpt-pattern-text-container h1 { font-size: 24px !important; line-height: 28px !important; }
#content-excerpt-pattern-container h2, #contextual-excerpt-pattern-text-container h2 { font-size: 20px !important; line-height: 28px !important; }
#content-excerpt-pattern-container h3, #contextual-excerpt-pattern-text-container h3 { font-size: 18px !important; line-height: 24px !important; }
#content-excerpt-pattern-container h4, #contextual-excerpt-pattern-text-container h4 { font-size: 16px !important; line-height: 22px !important; }
#content-excerpt-pattern-container h5, #contextual-excerpt-pattern-text-container h5 { font-size: 14px !important; line-height: 20px !important; }
#content-excerpt-pattern-container h6, #contextual-excerpt-pattern-text-container h6 { font-size: 14px !important; line-height: 20px !important; }
.user-mention { line-height: 18px !important; }
/** Aui Typography end **/
/* Show appropriate footer logo on mobile, display links vertically */
#footer-pattern { padding: 15px 10px !important; }
#footer-pattern-logo-desktop-container { padding: 0 !important; }
#footer-pattern-logo-desktop { width: 0 !important; height: 0 !important; }
#footer-pattern-logo-mobile {
padding-top: 10px !important;
width: 30px !important;
height: 27px !important;
display: inline !important;
}
#footer-pattern-text {
display: block !important;
}
#footer-pattern-links-container { line-height: 0 !important;}
.footer-pattern-links.mobile-resize-text,
.footer-pattern-links.mobile-resize-text,
#footer-pattern-text.mobile-resize-text,
#footer-pattern-links-container.no-footer-links {
font-size: 14px !important;
line-height: 20px !important;
}
.footer-link { display: block !important; }
#footer-pattern-links-container table { display: inline-block !important; float: none !important; }
#footer-pattern-links-container, #footer-pattern-text { text-align: center !important; }
#footer-pattern-links { padding-bottom: 5px !important; }
/** Team Calendar overrides, these should be removed when notifications are updated in Team Calendars. For now CSS
overrides are being used because the structure of the content can't change without rereleasing the plugin */
.mail-calendar-container .day-header + table tr td:first-child {
vertical-align: top !important;
padding-top: 5px !important;
}}
@media (min-width: 900px) {#center-content-table { width: 900px; }}
@media all {#outlook a {padding:0;} /* Force Outlook to provide a "view in browser" menu link. */
/* Prevent Webkit and Windows Mobile platforms from changing default font sizes.*/
body{-webkit-text-size-adjust:100%; -ms-text-size-adjust:100%;}
.ExternalClass {width:100%;} /* Force Hotmail to display emails at full width */
#background-table {margin:0; padding:0; width:100% !important; }
/* Needed to override highlighting on date and time links in iOS */
.grey a {color: #707070; text-decoration: none; }/* These styles are appended to the head element of a notification in order to prevent Apple Mail and similar
clients from underlining the due dates with a blue hyperlink */
/* a lozenge outside an inline task should always be #333, lozenges inside an inline task should be
colored according to their upcoming due dates, a completed task date lozenge or deleted task date
lozenge should always be #707070 */
.date-time-lozenge a {color: #333333; text-decoration: none; }
.inline-task-text-container .date-time-lozenge.date-upcoming a {color: #DF6F00; text-decoration: none; }
.inline-task-text-container .date-time-lozenge.date-past a {color: #D04437; text-decoration: none; }
.inline-task-text-container.content-deleted-color .date-time-lozenge a,
.inline-task-text-container.checked .date-time-lozenge a {
color: #707070; text-decoration: none;
}}
</style>
</head>
<body>
<table id="background-table" cellpadding="0" cellspacing="0" width="100%" style="border-collapse: collapse; mso-table-lspace: 0pt; mso-table-rspace: 0pt; color: #333; background-color: #f5f5f5">
<tbody>
<tr>
<td id="header-pattern-container" style="padding: 0px; border-collapse: collapse; padding: 10px 20px">
<table id="header-pattern" cellspacing="0" cellpadding="0" border="0" style="border-collapse: collapse; mso-table-lspace: 0pt; mso-table-rspace: 0pt; color: #333">
<tbody>
<tr>
<td id="header-avatar-image-container" valign="top" style="padding: 0px; border-collapse: collapse; vertical-align: top; width: 32px; padding-right: 9px"><a href="https://wiki.asterisk.org/wiki/display/~mmichelson?src=email" style="color: #3b73af; text-decoration: none"><img id="header-avatar-image" class="image_fix" src="cid:avatar_99ed0aa29d6f204db4785296f8170422" height="32" width="32" border="0" style="border-radius: 3px; vertical-align: top" /></a></td>
<td id="header-text-container" valign="middle" style="padding: 0px; border-collapse: collapse; vertical-align: middle; font-family: Arial, sans-serif; font-size: 14px; line-height: 20px; mso-line-height-rule: exactly; mso-text-raise: 1px">Mark Michelson <strong>created</strong> a page</td>
</tr>
</tbody>
</table> </td>
</tr>
<!-- End Header pattern -->
<tr>
<td id="email-content-container" style="padding: 0px; border-collapse: collapse; padding: 0 20px">
<table id="email-content-table" cellspacing="0" cellpadding="0" border="0" width="100%" style="border-collapse: collapse; mso-table-lspace: 0pt; mso-table-rspace: 0pt; color: #333; border-spacing: 0; border-collapse: separate">
<tbody>
<tr>
<td class="email-content-rounded-top mobile-expand" style="padding: 0px; border-collapse: collapse; color: #fff; padding: 0 15px 0 16px; height: 15px; background-color: #fff; border-left: 1px solid #ccc; border-top: 1px solid #ccc; border-right: 1px solid #ccc; border-bottom: 0; border-top-right-radius: 5px; border-top-left-radius: 5px"> </td>
</tr>
<tr>
<td class="email-content-main mobile-expand" style="padding: 0px; border-collapse: collapse; border-left: 1px solid #ccc; border-right: 1px solid #ccc; border-top: 0; border-bottom: 0; padding: 0 15px 15px 16px; background-color: #fff">
<table id="page-title-pattern" cellspacing="0" cellpadding="0" border="0" width="100%" style="border-collapse: collapse; mso-table-lspace: 0pt; mso-table-rspace: 0pt; color: #333">
<tbody>
<tr>
<td id="page-title-pattern-icon-image-container" valign="top" style="padding: 0px; border-collapse: collapse; width: 16px; vertical-align: top">
<table cellspacing="0" cellpadding="0" border="0" style="border-collapse: collapse; mso-table-lspace: 0pt; mso-table-rspace: 0pt; color: #333">
<tbody>
<tr>
<td id="page-title-pattern-icon-image-container-cell" style="padding: 0px; border-collapse: collapse; width: 16px; padding: 9px 8px 0px 0px; mso-text-raise: 5px; mso-line-height-rule: exactly"><a href="https://wiki.asterisk.org/wiki/display/AST/Playbacks?src=email" title="page icon" style="vertical-align: top;; color: #3b73af; text-decoration: none"><img style="vertical-align: top; display: block;" src="cid:page-icon" alt="page icon" title="page icon" height="16" width="16" border="0" /></a></td>
</tr>
</tbody>
</table> </td>
<td style="vertical-align: top;; padding: 0px; border-collapse: collapse; padding-right: 5px; font-size: 20px; line-height: 30px; mso-line-height-rule: exactly" id="page-title-pattern-header-container"><span id="page-title-pattern-header" style="font-family: Arial, sans-serif; padding: 0; font-size: 20px; line-height: 30px; mso-text-raise: 2px; mso-line-height-rule: exactly; vertical-align: middle"><a href="https://wiki.asterisk.org/wiki/display/AST/Playbacks?src=email" title="Playbacks" style="color: #3b73af; text-decoration: none">Playbacks</a></span></td>
</tr>
</tbody>
</table> </td>
</tr>
<tr>
<td class="email-content-main mobile-expand" style="padding: 0px; border-collapse: collapse; border-left: 1px solid #ccc; border-right: 1px solid #ccc; border-top: 0; border-bottom: 0; padding: 0 15px 15px 16px; background-color: #fff">
<table class="content-excerpt-pattern" cellspacing="0" cellpadding="0" border="0" width="100%" style="border-collapse: collapse; mso-table-lspace: 0pt; mso-table-rspace: 0pt; color: #333; font-family: Arial, sans-serif; font-size: 14px; line-height: 20px; mso-line-height-rule: exactly; mso-text-raise: 1px">
<tbody>
<tr>
<td class="content-excerpt-pattern-container mobile-resize-text " style="padding: 0px; border-collapse: collapse; padding: 0 0 0 24px">
<div class="sectionColumnWrapper">
<div class="sectionMacro">
<div class="sectionMacroRow">
<div class="columnMacro">
<h1 id="Playbacks-Queryingforsounds" style="margin: 10px 0 0 0; margin-top: 0; font-size: 24px; font-weight: normal; line-height: 30px; margin: 40px 0 0 0; margin-top: 0">Querying for sounds</h1>
<p style="margin: 10px 0 0 0">In our voice mail application we have been creating, we have learned the ins and outs of creating and manipulating live and stored recordings. Let's make the voice mail application more user-friendly now by adding some playbacks of installed sounds. The voice mail application has some nice capabilities, but it is not very user-friendly yet. Let's modify the current application to play a greeting to the user when they call into the application. This is the updated state machine:</p>
<p style="text-align: center;; margin: 10px 0 0 0"> <span id="gliffy-container-30474302-8933" class="gliffy-container " data-fullwidth="900" data-ceoid="30277828" data-filename="vm-full"> <span class="gliffy-chrome-container"> <span class="gliffy-chrome "> <span class="gliffy-item gliffy-first"> <img class="gliffy-logo" src="/wiki/download/resources/com.gliffy.integration.confluence/icons/logo_16x16.png" title="Gliffy" alt="Gliffy" /> Gliffy </span> </span> </span> <map id="gliffy-map-30474302-8550" name="gliffy-map-30474302-8550"></map> <img id="gliffy-image-30474302-8933" class="gliffy-image " width="900" height="525" data-full-width="900" data-full-height="525" src="/wiki/download/attachments/30277828/vm-full.png?version=1&modificationDate=1412020623021&api=v2" alt="vm-full" usemap="#gliffy-map-30474302-8550" /> <map id="gliffy-dynamic-map-30474302-8933" class="gliffy-dynamic" name="gliffy-dynamic-map-30474302-8933"></map> </span> </p>
</div>
<div class="columnMacro">
<div class="panel" style="border-width: 1px;">
<div class="panelHeader" style="border-bottom-width: 1px;">
<b>On this Page</b>
</div>
<div class="panelContent">
<p style="margin: 10px 0 0 0; margin-top: 0"> <style type="text/css">/**/
div.rbtoc1412020627553 {padding: 0px;}
div.rbtoc1412020627553 ul {list-style: disc;margin-left: 0px;}
div.rbtoc1412020627553 li {margin-left: 0px;padding-left: 0px;}
/**/</style> </p>
<div class="toc-macro rbtoc1412020627553" style="padding: 0px; padding: 0px">
<ul class="toc-indentation" style="margin: 10px 0 0 0; margin-top: 0; list-style: disc; margin-left: 0px; list-style: disc; margin-left: 0px">
<li style="margin-left: 0px; padding-left: 0px; margin-left: 0px; padding-left: 0px"> <a href="#Playbacks-Queryingforsounds" style="color: #3b73af; text-decoration: none">Querying for sounds</a> </li>
<li style="margin-left: 0px; padding-left: 0px; margin-left: 0px; padding-left: 0px"> <a href="#Playbacks-Controllingplaybacks" style="color: #3b73af; text-decoration: none">Controlling playbacks</a> </li>
<li style="margin-left: 0px; padding-left: 0px; margin-left: 0px; padding-left: 0px"> <a href="#Playbacks-Playbacksonbridges" style="color: #3b73af; text-decoration: none">Playbacks on bridges</a> </li>
</ul>
</div>
<p style="margin: 10px 0 0 0"></p>
</div>
</div>
</div>
</div>
</div>
</div> <p style="margin: 10px 0 0 0">Information on playing a sound file on a channel can be found here. To make this more interesting, we are going to add some safety to this state by ensuring that the sound we want to play is installed on the system. The <code style="font-family: monospace">/playbacks</code> resource in ARI provides methods to list the sounds installed on the system, as well as the ability to get specific sound files.</p> <p style="margin: 10px 0 0 0">Asterisk searches for sounds in the <code style="font-family: monospace">/sounds/</code> subdirectory of the configured <code style="font-family: monospace">astdatadir</code> option in <code style="font-family: monospace">asterisk.conf</code>. By default, Asterisk will search for sounds in <code style="font-family: monospace">/var/lib/asterisk/sounds</code>. When Asterisk starts up, it indexes the installed sounds and keeps an in-data representation of the installed sounds. When an ARI application asks Asterisk for details about a specific sound or for a list of sounds on the system, Asterisk consults its in-memory index instead of searching the file system directly. This means that if you add sound files to the sounds directory after Asterisk has started, then your ARI application will not be able to retrieve details about the newly-added sound. This can be remedied by running the Asterisk CLI command "module reload sounds".</p> <p style="margin: 10px 0 0 0">For our greeting, we will play the built-in sound "vm-intro". Here is the code for our new state:</p>
<div class="code panel pdl" style="border-width: 1px;">
<div class="codeHeader panelHeader pdl hide-border-bottom" style="border-bottom-width: 1px;">
<b class=" code-title">greeting_state.py</b>
<span class="collapse-source expand-control"><span class="expand-control-icon icon"> </span><span class="expand-control-text">Expand source</span></span>
</div>
<div class="codeContent panelContent pdl hide-toolbar">
<pre class="theme: Confluence; brush: py; collapse: true; gutter: false" style="font-size:12px;; margin: 10px 0 0 0; margin-top: 0">from event import Event
def sounds_installed(client):
try:
client.sounds.get(soundId='vm-intro')
except:
print "Required sound 'vm-intro' not installed. Aborting"
raise
class GreetingState(object):
state_name = "greeting"
def __init__(self, call):
self.call = call
self.hangup_event = None
self.playback_finished = None
self.dtmf_event = None
self.playback = None
sounds_installed(call.client)
def enter(self):
print "Entering greeting state"
self.hangup_event = self.call.channel.on_event('ChannelHangupRequest',
self.on_hangup)
self.playback_finished = self.call.client.on_event(
'PlaybackFinished', self.on_playback_finished)
self.dtmf_event = self.call.channel.on_event('ChannelDtmfReceived',
self.on_dtmf)
self.playback = self.call.channel.play(media="sound:vm-intro")
def cleanup(self):
self.playback_finished.close()
self.dtmf_event.close()
self.hangup_event.close()
def on_hangup(self, channel, event):
print "Abandoning voicemail recording on hangup"
self.cleanup()
self.call.state_machine.change_state(Event.HANGUP)
def on_playback_finished(self, playback):
self.cleanup()
self.call.state_machine.change_state(Event.PLAYBACK_COMPLETE)
def on_dtmf(self, channel, event):
digit = event.get('digit')
if digit == '#':
print "Cutting off greeting on DTMF #"
# Let on_playback_finished take care of state change
self.playback.stop()</pre>
</div>
</div>
<div class="code panel pdl" style="border-width: 1px;">
<div class="codeHeader panelHeader pdl hide-border-bottom" style="border-bottom-width: 1px;">
<b class=" code-title">greeting_state.js</b>
<span class="collapse-source expand-control"><span class="expand-control-icon icon"> </span><span class="expand-control-text">Expand source</span></span>
</div>
<div class="codeContent panelContent pdl hide-toolbar">
<pre class="theme: Confluence; brush: js; collapse: true; gutter: false" style="font-size:12px;; margin: 10px 0 0 0; margin-top: 0">var Event = require('./event');
function sounds_installed(client) {
client.sounds.get({soundId: 'vm-intro'}, function(err) {
if (err) {
console.log("Required sound 'vm-intro' not installed. Aborting");
throw err;
}
});
}
var GreetingState = function(call) {
this.state_name = "greeting";
this.call = call;
sounds_installed(call.client);
}
GreetingState.prototype.enter = function() {
var self = this;
var playback = self.call.client.Playback();
console.log("Entering greeting state");
self.call.channel.on("ChannelHangupRequest", on_hangup);
self.call.channel.on("ChannelDtmfReceived", on_dtmf);
self.call.client.on("PlaybackFinished", on_playback_finished);
self.call.channel.play({media: 'sound:vm-intro'}, playback);
function cleanup() {
self.call.channel.removeListener('ChannelHangupRequest', on_hangup);
self.call.channel.removeListener('ChannelDtmfReceived', on_dtmf);
self.call.client.removeListener('PlaybackFinished', on_playback_finished);
}
function on_hangup(event, channel) {
console.log("Abandoning voicemail recording on hangup");
cleanup();
self.call.state_machine.change_state(Event.HANGUP);
}
function on_playback_finished(event) {
if (playback && playback.id === event.playback.id) {
cleanup();
self.call.state_machine.change_state(Event.PLAYBACK_COMPLETE);
}
}
function on_dtmf(event, channel) {
switch (event.digit) {
case '#':
console.log("Skipping greeting");
playback.stop();
//Let on_playback_finished take care of state change
}
}
}
module.exports = GreetingState;</pre>
</div>
</div> <p style="margin: 10px 0 0 0">By checking for the sound's existence in the initialization of <code style="font-family: monospace">GreetingState</code>, we can abort the call early if the sound is not installed.</p> <p style="margin: 10px 0 0 0">And here is our updated state machine:</p>
<div class="code panel pdl" style="border-width: 1px;">
<div class="codeHeader panelHeader pdl hide-border-bottom" style="border-bottom-width: 1px;">
<b class=" code-title">vm-call.py</b>
<span class="collapse-source expand-control"><span class="expand-control-icon icon"> </span><span class="expand-control-text">Expand source</span></span>
</div>
<div class="codeContent panelContent pdl hide-toolbar">
<pre class="theme: Confluence; brush: py; collapse: true; gutter: false" style="font-size:12px;; margin: 10px 0 0 0; margin-top: 0">#At the top of the file
from greeting_state import GreetingState
#In VoiceMailCall::setup_state_machine()
def setup_state_machine(self):
hungup_state = HungUpState(self)
recording_state = RecordingState(self)
ending_state = EndingState(self)
reviewing_state = ReviewingState(self)
greeting_state = GreetingState(self)
self.state_machine = StateMachine()
self.state_machine.add_transition(recording_state, Event.DTMF_OCTOTHORPE,
reviewing_state)
self.state_machine.add_transition(recording_state, Event.HANGUP,
hungup_state)
self.state_machine.add_transition(recording_state, Event.DTMF_STAR,
recording_state)
self.state_machine.add_transition(reviewing_state, Event.HANGUP,
hungup_state)
self.state_machine.add_transition(reviewing_state, Event.DTMF_OCTOTHORPE,
ending_state)
self.state_machine.add_transition(reviewing_state, Event.DTMF_STAR,
recording_state)
self.state_machine.add_transition(greeting_state, Event.HANGUP,
hungup_state)
self.state_machine.add_transition(greeting_state,
Event.PLAYBACK_COMPLETE,
recording_state)
self.state_machine.start(greeting_state)</pre>
</div>
</div>
<div class="code panel pdl" style="border-width: 1px;">
<div class="codeHeader panelHeader pdl hide-border-bottom" style="border-bottom-width: 1px;">
<b class=" code-title">vm-call.js</b>
<span class="collapse-source expand-control"><span class="expand-control-icon icon"> </span><span class="expand-control-text">Expand source</span></span>
</div>
<div class="codeContent panelContent pdl hide-toolbar">
<pre class="theme: Confluence; brush: js; collapse: true; gutter: false" style="font-size:12px;; margin: 10px 0 0 0; margin-top: 0">//At the top of the file
var GreetingState = require('./greeting_state');
//In VoicemailCall::setup_state_machine()
VoiceMailCall.prototype.setup_state_machine = function() {
var hungup_state = new HungUpState(this);
var recording_state = new RecordingState(this);
var ending_state = new EndingState(this);
var reviewing_state = new ReviewingState(this);
var greeting_state = new GreetingState(this);
this.state_machine = new StateMachine();
this.state_machine.add_transition(recording_state, Event.DTMF_OCTOTHORPE, reviewing_state);
this.state_machine.add_transition(recording_state, Event.HANGUP, hungup_state);
this.state_machine.add_transition(recording_state, Event.DTMF_STAR, recording_state);
this.state_machine.add_transition(reviewing_state, Event.DTMF_OCTOTHORPE, ending_state);
this.state_machine.add_transition(reviewing_state, Event.HANGUP, hungup_state);
this.state_machine.add_transition(reviewing_state, Event.DTMF_STAR, recording_state);
this.state_machine.add_transition(greeting_state, Event.HANGUP, hungup_state);
this.state_machine.add_transition(greeting_state, Event.PLAYBACK_COMPLETE, recording_state);
this.state_machine.start(greeting_state);
}</pre>
</div>
</div> <p style="margin: 10px 0 0 0">Here is a sample run where the user cuts off the greeting by pressing the '#' key, records a greeting and presses the '#' key, and after listening to the recording presses the '#' key once more.</p>
<div class="preformatted panel" style="border-width: 1px;">
<div class="preformattedContent panelContent">
<pre style="margin: 10px 0 0 0; margin-top: 0">Channel PJSIP/200-0000000b recording voicemail for 305
Entering greeting state
Cutting off greeting on DTMF #
Entering recording state
Recording voicemail at voicemail/305/1411503204.75
Accepted recording voicemail/305/1411503204.75 on DTMF #
Cleaning up event handlers
Entering reviewing state
Accepted recording voicemail/305/1411503204.75 on DTMF #
Ending voice mail call from PJSIP/200-0000000b</pre>
</div>
</div>
<div class="panel" style="background-color: silver;border-style: solid;border-width: 1px;">
<div class="panelHeader" style="border-bottom-width: 1px;border-bottom-style: solid;background-color: seagreen;color: black;">
<b>Reader Exercise 5</b>
</div>
<div class="panelContent" style="background-color: silver;">
<p style="margin: 10px 0 0 0; margin-top: 0">Our current implementation of <code style="font-family: monospace">GreetingState</code> does not take language into consideration. The <code style="font-family: monospace">sounds_installed</code> method checks for the existence of the sound file, but it does not ensure that we have the sound file in the language of the channel that is in our application.</p>
<p style="margin: 10px 0 0 0">For this exercise, modify <code style="font-family: monospace">sounds_installed()</code> to also check if the retrieved sound exists in the language of the calling channel. The channel's language can be retrieved using the <code style="font-family: monospace">getChannelVar()</code> method on a channel to retrieve the value of variable "CHANNEL(language)". The sound returned by <code style="font-family: monospace">sounds.get()</code> contains an array of <code style="font-family: monospace">FormatLang</code> objects that are a pair of format and language strings. If the sound exists, but not in the channel's language, then throw an exception.</p>
</div>
</div> <h1 id="Playbacks-Controllingplaybacks" style="margin: 10px 0 0 0; font-size: 24px; font-weight: normal; line-height: 30px; margin: 40px 0 0 0">Controlling playbacks</h1> <p style="margin: 10px 0 0 0">We've seen playbacks get stopped, but there are a lot more interesting operations that can be done on playbacks, such as reversing and fast-forwarding them. Within the context of recording a voicemail, these operations are pretty useless, so we will shift our focus now to the other side of voicemail: listening to recorded voicemails.</p> <p style="margin: 10px 0 0 0">For this, we will write a new application. This new application will allow a caller to listen to the voicemails that are stored in a specific mailbox. When the caller calls in, a prompt is played to the caller saying which message number the caller is hearing. When the message number finishes playing (or if the caller interrupts the playback with '#'), then the caller hears the specified message in the voicemail box. While listening to the voicemail, the caller can do several things:</p>
<ul style="margin: 10px 0 0 0">
<li>Press the '1' key to go back 3 seconds in the current message playback.</li>
<li>Press the '2' key to pause or unpause the current message playback.</li>
<li>Press the '3' key to go forward 3 seconds in the current message playback.</li>
<li>Press the '4' key to play to the previous message.</li>
<li>Press the '5' key to restart the current message playback.</li>
<li>Press the '6' key to play to the next message.</li>
<li>Press the '*' key to delete the current message and play the next message.</li>
<li>Press the '#' key to end the call.</li>
</ul> <p style="margin: 10px 0 0 0">If all messages in a mailbox are deleted or if the mailbox contained no messages to begin with, then "no more messages" is played back to the user, and the call is completed.</p> <p style="margin: 10px 0 0 0">This means defining a brand new state machine. To start with, we'll define a three new states. The "preamble" state is a state where the current message number is played back to the listener. The "listening" state is where the voice mail message is played back to the listener. The "empty" state is where no more messages remain in the mailbox. Here is the state machine we will be using:</p> <p style="text-align: center;; margin: 10px 0 0 0"> <span id="gliffy-container-30474304-2321" class="gliffy-container " data-fullwidth="1020" data-ceoid="30277828" data-filename="vm-listen"> <span class="gliffy-chrome-container"> <span class="gliffy-chrome "> <span class="gliffy-item gliffy-first"> <img class="gliffy-logo" src="/wiki/download/resources/com.gliffy.integration.confluence/icons/logo_16x16.png" title="Gliffy" alt="Gliffy" /> Gliffy </span> </span> </span> <map id="gliffy-map-30474304-9314" name="gliffy-map-30474304-9314"></map> <img id="gliffy-image-30474304-2321" class="gliffy-image " width="1020" height="652" data-full-width="1020" data-full-height="652" src="/wiki/download/attachments/30277828/vm-listen.png?version=1&modificationDate=1412020623069&api=v2" alt="vm-listen" usemap="#gliffy-map-30474304-9314" /> <map id="gliffy-dynamic-map-30474304-2321" class="gliffy-dynamic" name="gliffy-dynamic-map-30474304-2321"></map> </span> </p> <p style="margin: 10px 0 0 0">Notice that DMTF '4', '6', and '*' all change the state to the preamble state. This is so that the new message number can be played back to the caller before the next message is heard. Also notice that the preamble state is responsible for determining if the state should change to empty. This keeps the logic in the listening state more straight-forward since it is already having to deal with a lot of DTMF events. It also gracefully handles the case where a caller calls into the application when the caller has no voicemail messages.</p> <p style="margin: 10px 0 0 0">Here is the implementation of the application.</p>
<div class="code panel pdl" style="border-width: 1px;">
<div class="codeHeader panelHeader pdl hide-border-bottom" style="border-bottom-width: 1px;">
<b class=" code-title">vm-playback.py</b>
<span class="collapse-source expand-control"><span class="expand-control-icon icon"> </span><span class="expand-control-text">Expand source</span></span>
</div>
<div class="codeContent panelContent pdl hide-toolbar">
<pre class="theme: Confluence; brush: py; collapse: true; gutter: false" style="font-size:12px;; margin: 10px 0 0 0; margin-top: 0">#!/usr/bin/env python
import ari
import logging
import sys
from state_machine import StateMachine
from ending_state import EndingState
from hungup_state import HungUpState
from listening_state import ListeningState
from preamble_state import PreambleState
from empty_state import EmptyState
from event import Event
logging.basicConfig(level=logging.ERROR)
LOGGER = logging.getLogger(__name__)
client = ari.connect('http://10.24.20.249:8088', 'asterisk', 'asterisk')
class VoiceMailCall(object):
def __init__(self, ari_client, channel, mailbox):
self.client = ari_client
self.channel = channel
self.voicemails = []
recordings = ari_client.recordings.listStored()
vm_number = 1
for rec in recordings:
if rec.json['name'].startswith('voicemail/{0}'.format(mailbox)):
self.voicemails.append((vm_number, rec.json['name']))
vm_number += 1
self.current_voicemail = 0
self.setup_state_machine()
def setup_state_machine(self):
hungup_state = HungUpState(self)
ending_state = EndingState(self)
listening_state = ListeningState(self)
preamble_state = PreambleState(self)
empty_state = EmptyState(self)
self.state_machine = StateMachine()
self.state_machine.add_transition(listening_state, Event.DTMF_4,
preamble_state)
self.state_machine.add_transition(listening_state, Event.DTMF_6,
preamble_state)
self.state_machine.add_transition(listening_state, Event.HANGUP,
hungup_state)
self.state_machine.add_transition(listening_state, Event.DTMF_OCTOTHORPE,
ending_state)
self.state_machine.add_transition(listening_state, Event.DTMF_STAR,
preamble_state)
self.state_machine.add_transition(preamble_state, Event.DTMF_OCTOTHORPE,
listening_state)
self.state_machine.add_transition(preamble_state,
Event.PLAYBACK_COMPLETE,
listening_state)
self.state_machine.add_transition(preamble_state, Event.MAILBOX_EMPTY,
empty_state)
self.state_machine.add_transition(preamble_state, Event.HANGUP,
hungup_state)
self.state_machine.add_transition(empty_state, Event.HANGUP,
hungup_state)
self.state_machine.add_transition(empty_state,
Event.PLAYBACK_COMPLETE,
ending_state)
self.state_machine.start(preamble_state)
def next_message(self):
self.current_voicemail += 1
if self.current_voicemail == len(self.voicemails):
self.current_voicemail = 0
def previous_message(self):
self.current_voicemail -= 1
if self.current_voicemail < 0:
self.current_voicemail = len(self.voicemails) - 1
def delete_message(self):
del self.voicemails[self.current_voicemail]
if self.current_voicemail == len(self.voicemails):
self.current_voicemail = 0
def get_current_voicemail_number(self):
return self.voicemails[self.current_voicemail][0]
def get_current_voicemail_file(self):
return self.voicemails[self.current_voicemail][1]
def mailbox_empty(self):
return len(self.voicemails) == 0
def stasis_start_cb(channel_obj, event):
channel = channel_obj['channel']
channel_name = channel.json.get('name')
mailbox = event.get('args')[0]
print("Channel {0} recording voicemail for {1}".format(
channel_name, mailbox))
channel.answer()
VoiceMailCall(client, channel, mailbox)
client.on_channel_event('StasisStart', stasis_start_cb)
client.run(apps=sys.argv[1])</pre>
</div>
</div>
<div class="code panel pdl" style="border-width: 1px;">
<div class="codeHeader panelHeader pdl hide-border-bottom" style="border-bottom-width: 1px;">
<b class=" code-title">vm-playback.js</b>
<span class="collapse-source expand-control"><span class="expand-control-icon icon"> </span><span class="expand-control-text">Expand source</span></span>
</div>
<div class="codeContent panelContent pdl hide-toolbar">
<pre class="theme: Confluence; brush: js; collapse: true; gutter: false" style="font-size:12px;; margin: 10px 0 0 0; margin-top: 0">/*jshint node:true*/
'use strict';
var ari = require('ari-client');
var util = require('util');
var path = require('path');
var Event = require('./event');
var StateMachine = require('./state_machine');
var EndingState = require('./ending_state');
var HungUpState = require('./hungup_state');
var PreambleState = require('./preamble_state');
var EmptyState = require('./empty_state');
var ListeningState = require('./listening_state');
ari.connect('http://10.24.20.249:8088', 'asterisk', 'asterisk', clientLoaded);
var VoiceMailCall = function(ari_client, channel, mailbox) {
var self = this;
self.client = ari_client;
self.channel = channel;
self.current_voicemail = 0;
self.voicemails = [];
ari_client.recordings.listStored(function (err, recordings) {
var vm_number = 1;
var regex = new RegExp('^voicemail/' + mailbox);
for (var i = 0; i < recordings.length; i++) {
var rec_name = recordings[i].name;
if (rec_name.search(regex) != -1) {
console.log("Found voicemail", rec_name);
self.voicemails.push({'number': vm_number, 'file': rec_name});
vm_number++;
}
}
self.setup_state_machine();
});
}
VoiceMailCall.prototype.setup_state_machine = function() {
var hungup_state = new HungUpState(this);
var ending_state = new EndingState(this);
var listening_state = new ListeningState(this);
var preamble_state = new PreambleState(this);
var empty_state = new EmptyState(this);
this.state_machine = new StateMachine(this);
this.state_machine.add_transition(listening_state, Event.DTMF_4, preamble_state);
this.state_machine.add_transition(listening_state, Event.DTMF_6, preamble_state);
this.state_machine.add_transition(listening_state, Event.HANGUP, hungup_state);
this.state_machine.add_transition(listening_state, Event.DTMF_OCTOTHORPE, ending_state);
this.state_machine.add_transition(listening_state, Event.DTMF_STAR, preamble_state);
this.state_machine.add_transition(preamble_state, Event.DTMF_OCTOTHORPE, listening_state);
this.state_machine.add_transition(preamble_state, Event.PLAYBACK_COMPLETE, listening_state);
this.state_machine.add_transition(preamble_state, Event.MAILBOX_EMPTY, empty_state);
this.state_machine.add_transition(preamble_state, Event.HANGUP, hungup_state);
this.state_machine.add_transition(empty_state, Event.HANGUP, hungup_state);
this.state_machine.add_transition(empty_state, Event.PLAYBACK_COMPLETE, ending_state);
this.state_machine.start(preamble_state);
}
VoiceMailCall.prototype.next_message = function() {
this.current_voicemail++;
if (this.current_voicemail === this.voicemails.length) {
this.current_voicemail = 0;
}
}
VoiceMailCall.prototype.previous_message = function() {
this.current_voicemail--;
if (this.current_voicemail < 0) {
this.current_voicemail = this.voicemails.length - 1;
}
}
VoiceMailCall.prototype.delete_message = function() {
this.voicemails.splice(this.current_voicemail, 1);
if (this.current_voicemail === this.voicemails.length) {
this.current_voicemail = 0;
}
}
VoiceMailCall.prototype.get_current_voicemail_number = function() {
return this.voicemails[this.current_voicemail]['number'];
}
VoiceMailCall.prototype.get_current_voicemail_file = function() {
return this.voicemails[this.current_voicemail]['file'];
}
VoiceMailCall.prototype.mailbox_empty = function() {
return this.voicemails.length === 0;
}
function clientLoaded(err, client) {
if (err) {
throw err;
}
client.on('StasisStart', stasisStart);
function stasisStart(event, channel) {
var mailbox = event.args[0]
channel.answer(function(err) {
if (err) {
throw err;
}
new VoiceMailCall(client, channel, mailbox);
});
}
client.start(process.argv[2]);
}</pre>
</div>
</div> <p style="margin: 10px 0 0 0"> </p> <p style="margin: 10px 0 0 0">Quite a bit of this is similar to what we were using for <code style="font-family: monospace">vm-call.py</code> previously. Aside from the new states, which will be introduced later, the real difference here is the new set of methods on the <code style="font-family: monospace">VoiceMailCall</code>.</p> <p style="margin: 10px 0 0 0">First of the new states is the "preamble" state.</p>
<div class="code panel pdl" style="border-width: 1px;">
<div class="codeHeader panelHeader pdl hide-border-bottom" style="border-bottom-width: 1px;">
<b class=" code-title">preamble_state.py</b>
<span class="collapse-source expand-control"><span class="expand-control-icon icon"> </span><span class="expand-control-text">Expand source</span></span>
</div>
<div class="codeContent panelContent pdl hide-toolbar">
<pre class="theme: Confluence; brush: py; collapse: true; gutter: false" style="font-size:12px;; margin: 10px 0 0 0; margin-top: 0">from event import Event
import uuid
def sounds_installed(client):
try:
client.sounds.get(soundId='vm-message')
except:
print "Required sound 'vm-message' not installed. Aborting"
raise
class PreambleState(object):
state_name = "preamble"
def __init__(self, call):
self.call = call
self.hangup_event = None
self.playback_finished = None
self.dtmf_event = None
self.playback = None
sounds_installed(call.client)
def enter(self):
print "Entering preamble state"
if self.call.mailbox_empty():
self.call.state_machine.change_state(Event.MAILBOX_EMPTY)
return
self.hangup_event = self.call.channel.on_event("ChannelHangupRequest",
self.on_hangup)
self.playback_finished = self.call.client.on_event(
'PlaybackFinished', self.on_playback_finished)
self.dtmf_event = self.call.channel.on_event('ChannelDtmfReceived',
self.on_dtmf)
self.initialize_playbacks()
def initialize_playbacks(self):
self.current_playback = 0
current_voicemail = self.call.get_current_voicemail_number()
self.sounds_to_play = [
{
'id': str(uuid.uuid4()),
'media': 'sound:vm-message'
},
{
'id': str(uuid.uuid4()),
'media': 'number:{0}'.format(current_voicemail)
}
]
self.start_playback()
def start_playback(self):
current_sound = self.sounds_to_play[self.current_playback]
self.playback = self.call.channel.playWithId(
playbackId=current_sound['id'],
media=current_sound['media']
)
def cleanup(self):
self.playback_finished.close()
if self.playback:
self.playback.stop()
self.dtmf_event.close()
self.hangup_event.close()
def on_hangup(self, channel, event):
self.playback = None
self.cleanup()
self.call.state_machine.change_state(Event.HANGUP)
def on_playback_finished(self, event):
current_sound = self.sounds_to_play[self.current_playback]
if current_sound['id'] == event.get('playback').get('id'):
self.playback = None
self.current_playback += 1
if self.current_playback == len(self.sounds_to_play):
self.cleanup()
self.call.state_machine.change_state(Event.PLAYBACK_COMPLETE)
else:
self.start_playback()
def on_dtmf(self, channel, event):
digit = event.get('digit')
if digit == '#':
self.cleanup()
self.call.state_machine.change_state(Event.DTMF_OCTOTHORPE)</pre>
</div>
</div>
<div class="code panel pdl" style="border-width: 1px;">
<div class="codeHeader panelHeader pdl hide-border-bottom" style="border-bottom-width: 1px;">
<b class=" code-title">preamble_state.js</b>
<span class="collapse-source expand-control"><span class="expand-control-icon icon"> </span><span class="expand-control-text">Expand source</span></span>
</div>
<div class="codeContent panelContent pdl hide-toolbar">
<pre class="theme: Confluence; brush: js; collapse: true; gutter: false" style="font-size:12px;; margin: 10px 0 0 0; margin-top: 0">var Event = require('./event');
function sounds_installed(client) {
client.sounds.get({soundId: 'vm-message'}, function(err) {
if (err) {
console.log("Required sound 'vm-message' not installed. Aborting");
throw err;
}
});
}
var PreambleState = function(call) {
this.state_name = "preamble";
this.call = call;
}
PreambleState.prototype.enter = function() {
var self = this;
var current_playback;
var sounds_to_play;
var playback;
console.log("Entering preamble state");
if (self.call.mailbox_empty()) {
self.call.state_machine.change_state(Event.MAILBOX_EMPTY);
return;
}
console.log("Mailbox not empty?");
self.call.channel.on("ChannelHangupRequest", on_hangup);
self.call.client.on("PlaybackFinished", on_playback_finished);
self.call.channel.on("ChannelDtmfReceived", on_dtmf);
initialize_playbacks();
function initialize_playbacks() {
console.log("Initializing playbacks");
current_playback = 0;
sounds_to_play = [
{
'playback': self.call.client.Playback(),
'media': 'sound:vm-message'
},
{
'playback': self.call.client.Playback(),
'media': 'number:' + self.call.get_current_voicemail_number()
}
];
start_playback();
}
function start_playback() {
current_sound = sounds_to_play[current_playback];
playback = current_sound['playback'];
console.log("Starting a playback of", current_sound['media']);
self.call.channel.play({media: current_sound['media']}, playback);
}
function cleanup() {
self.call.channel.removeListener('ChannelHangupRequest', on_hangup);
self.call.channel.removeListener('ChannelDtmfReceived', on_dtmf);
self.call.client.removeListener('PlaybackFinished', on_playback_finished);
if (playback) {
playback.stop();
}
}
function on_hangup(event, channel) {
playback = null;
cleanup();
self.call.state_machine.change_state(Event.HANGUP);
}
function on_playback_finished(event) {
var current_sound = sounds_to_play[current_playback];
console.log("Got told playback finished");
if (playback && (playback.id === event.playback.id)) {
playback = null;
current_playback++;
if (current_playback === sounds_to_play.length) {
cleanup();
self.call.state_machine.change_state(Event.PLAYBACK_COMPLETE);
} else {
start_playback();
}
}
}
function on_dtmf(event, channel) {
switch(event.digit) {
case '#':
cleanup();
self.call.state_machine.change_state(Event.DTMF_OCTOTHORPE);
}
}
}
module.exports = PreambleState;</pre>
</div>
</div> <p style="margin: 10px 0 0 0"> <code style="font-family: monospace">PreambleState</code> should look similar to the <code style="font-family: monospace">GreetingState</code> introduced previously. The biggest difference is that the code is structured to play multiple sound files instead of just a single one. Note that it is acceptable to call <code style="font-family: monospace">channel.play()</code> multiple times consecutively to queue playbacks on a channel. However, we have elected to play the second sound only after the first has completed. This is to make it easier to clean up after ourself when it is time to change states.</p> <p style="margin: 10px 0 0 0">Next, here is the "empty" state code:</p>
<div class="code panel pdl" style="border-width: 1px;">
<div class="codeHeader panelHeader pdl hide-border-bottom" style="border-bottom-width: 1px;">
<b class=" code-title">empty_state.py</b>
<span class="collapse-source expand-control"><span class="expand-control-icon icon"> </span><span class="expand-control-text">Expand source</span></span>
</div>
<div class="codeContent panelContent pdl hide-toolbar">
<pre class="theme: Confluence; brush: py; collapse: true; gutter: false" style="font-size:12px;; margin: 10px 0 0 0; margin-top: 0">from event import Event
import uuid
def sounds_installed(client):
try:
client.sounds.get(soundId='vm-nomore')
except:
print "Required sound 'vm-nomore' not installed. Aborting"
raise
class EmptyState(object):
state_name = "empty"
def __init__(self, call):
self.call = call
self.playback_id = None
self.hangup_event = None
self.playback_finished = None
self.playback = None
sounds_installed(call.client)
def enter(self):
self.playback_id = str(uuid.uuid4())
print "Entering empty state"
self.hangup_event = self.call.channel.on_event("ChannelHangupRequest",
self.on_hangup)
self.playback_finished = self.call.client.on_event(
'PlaybackFinished', self.on_playback_finished)
self.playback = self.call.channel.playWithId(
playbackId=self.playback_id, media="sound:vm-nomore")
def cleanup(self):
self.playback_finished.close()
if self.playback:
self.playback.stop()
self.hangup_event.close()
def on_hangup(self, channel, event):
# Setting playback to None stops cleanup() from trying to stop the
# playback.
self.playback = None
self.cleanup()
self.call.state_machine.change_state(Event.HANGUP)
def on_playback_finished(self, event):
if self.playback_id == event.get('playback').get('id'):
self.playback = None
self.cleanup()
self.call.state_machine.change_state(Event.PLAYBACK_COMPLETE)</pre>
</div>
</div>
<div class="code panel pdl" style="border-width: 1px;">
<div class="codeHeader panelHeader pdl hide-border-bottom" style="border-bottom-width: 1px;">
<b class=" code-title">empty_state.js</b>
<span class="collapse-source expand-control"><span class="expand-control-icon icon"> </span><span class="expand-control-text">Expand source</span></span>
</div>
<div class="codeContent panelContent pdl hide-toolbar">
<pre class="theme: Confluence; brush: js; collapse: true; gutter: false" style="font-size:12px;; margin: 10px 0 0 0; margin-top: 0">var Event = require('./event');
function sounds_installed(client) {
client.sounds.get({soundId: 'vm-nomore'}, function(err) {
if (err) {
console.log("Required sound 'vm-nomore' not installed. Aborting");
throw err;
}
});
}
var EmptyState = function(call) {
this.state_name = "empty";
this.call = call;
}
EmptyState.prototype.enter = function() {
var self = this;
console.log("Entering empty state");
playback = self.call.client.Playback();
self.call.channel.on("ChannelHangup", on_hangup);
self.call.client.on("PlaybackFinished", on_playback_finished);
self.call.channel.play({media: 'sound:vm-nomore'}, playback);
function cleanup() {
self.call.channel.removeListener('ChannelHangupRequest', on_hangup);
self.call.channel.removeListener('PlaybackFinished', on_playback_finished);
if (playback) {
playback.stop();
}
}
function on_hangup(event, channel) {
playback = null;
cleanup();
self.call.state_machine.change_state(Event.HANGUP);
}
function on_playback_finished(event) {
if (playback && playback.id === event.playback.id) {
playback = null;
cleanup();
self.call.state_machine.change_state(Event.PLAYBACK_COMPLETE);
}
}
}
module.exports = EmptyState;</pre>
</div>
</div> <p style="margin: 10px 0 0 0">And finally, here is the "listening" state code:</p>
<div class="code panel pdl" style="border-width: 1px;">
<div class="codeHeader panelHeader pdl hide-border-bottom" style="border-bottom-width: 1px;">
<b class=" code-title">listening_state.py</b>
<span class="collapse-source expand-control"><span class="expand-control-icon icon"> </span><span class="expand-control-text">Expand source</span></span>
</div>
<div class="codeContent panelContent pdl hide-toolbar">
<pre class="theme: Confluence; brush: py; collapse: true; gutter: false" style="font-size:12px;; margin: 10px 0 0 0; margin-top: 0">from event import Event
import uuid
class ListeningState(object):
state_name = "listening"
def __init__(self, call):
self.call = call
self.playback_id = None
self.hangup_event = None
self.playback_finished = None
self.dtmf_event = None
self.playback = None
def enter(self):
self.paused = False
self.playback_id = str(uuid.uuid4())
print "Entering listening state"
self.hangup_event = self.call.channel.on_event("ChannelHangupRequest",
self.on_hangup)
self.playback_finished = self.call.client.on_event(
'PlaybackFinished', self.on_playback_finished)
self.dtmf_event = self.call.channel.on_event('ChannelDtmfReceived',
self.on_dtmf)
self.playback = self.call.channel.playWithId(
playbackId=self.playback_id, media="recording:{0}".format(
self.call.get_current_voicemail_file()))
def cleanup(self):
self.playback_finished.close()
if self.playback:
self.playback.stop()
self.dtmf_event.close()
self.hangup_event.close()
def on_hangup(self, channel, event):
self.cleanup()
self.call.state_machine.change_state(Event.HANGUP)
def on_playback_finished(self, event):
if self.playback_id == event.get('playback').get('id'):
self.playback = None
def on_dtmf(self, channel, event):
digit = event.get('digit')
if digit == '1':
if self.playback:
self.playback.control(operation='reverse')
elif digit == '2':
if not self.playback:
return
if self.paused:
self.playback.control(operation='unpause')
self.paused = False
else:
self.playback.control(operation='pause')
self.paused = True
elif digit == '3':
if self.playback:
self.playback.control(operation='forward')
elif digit == '4':
self.cleanup()
self.call.previous_message()
self.call.state_machine.change_state(Event.DTMF_4)
elif digit == '5':
if self.playback:
self.playback.control(operation='restart')
elif digit == '6':
self.cleanup()
self.call.next_message()
self.call.state_machine.change_state(Event.DTMF_6)
elif digit == '#':
self.cleanup()
self.call.state_machine.change_state(Event.DTMF_OCTOTHORPE)
elif digit == '*':
print ("Deleting stored recording {0}".format(
self.call.get_current_voicemail_file()))
self.cleanup()
self.call.client.recordings.deleteStored(
recordingName=self.call.get_current_voicemail_file())
self.call.delete_message()
self.call.state_machine.change_state(Event.DTMF_STAR)</pre>
</div>
</div>
<div class="code panel pdl" style="border-width: 1px;">
<div class="codeHeader panelHeader pdl hide-border-bottom" style="border-bottom-width: 1px;">
<b class=" code-title">listening_state.js</b>
<span class="collapse-source expand-control"><span class="expand-control-icon icon"> </span><span class="expand-control-text">Expand source</span></span>
</div>
<div class="codeContent panelContent pdl hide-toolbar">
<pre class="theme: Confluence; brush: js; collapse: true; gutter: false" style="font-size:12px;; margin: 10px 0 0 0; margin-top: 0">var Event = require('./event');
var ListeningState = function(call) {
this.state_name = "listening";
this.call = call;
}
ListeningState.prototype.enter = function() {
var self = this;
var playback = self.call.client.Playback();
var url = "recording:" + self.call.get_current_voicemail_file();
console.log("Entering Listening state");
self.call.channel.on("ChannelHangupRequest", on_hangup);
self.call.channel.on("ChannelDtmfReceived", on_dtmf);
self.call.client.on("PlaybackFinished", on_playback_finished);
console.log("Playing file %s", url);
self.call.channel.play({media: url}, playback, function(err) {
if (err) {
console.error(err);
}
});
function cleanup() {
self.call.channel.removeListener('ChannelHangupRequest', on_hangup);
self.call.channel.removeListener('ChannelDtmfReceived', on_dtmf);
self.call.client.removeListener('PlaybackFinished', on_playback_finished);
if (playback) {
playback.stop();
}
}
function on_hangup(event, channel) {
playback = null;
cleanup();
self.call.state_machine.change_state(Event.HANGUP);
}
function on_playback_finished(event) {
if (playback && (playback.id === event.playback.id)) {
playback = null;
}
}
function on_dtmf(event, channel) {
switch (event.digit) {
case '1':
if (playback) {
playback.control({operation: 'reverse'});
}
break;
case '2':
if (!playback) {
break;
}
if (paused) {
playback.control({operation: 'unpause'});
paused = false;
} else {
playback.control({operation: 'pause'});
paused = true;
}
break;
case '3':
if (playback) {
playback.control({operation: 'forward'});
}
break;
case '4':
cleanup();
self.call.previous_message();
self.call.state_machine.change_state(Event.DTMF_4);
break;
case '5':
if (playback) {
playback.control({operation: 'restart'});
}
break;
case '6':
cleanup();
self.call.next_message();
self.call.state_machine.change_state(Event.DTMF_6);
break;
case '#':
cleanup();
self.call.state_machine.change_state(Event.DTMF_OCTOTHORPE);
break;
case '*':
console.log("Deleting stored recording", self.call.get_current_voicemail_file());
cleanup();
self.call.client.recordings.deleteStored({recordingName:self.call.get_current_voicemail_file()});
self.call.delete_message();
self.call.state_machine.change_state(Event.DTMF_STAR);
}
}
}
module.exports = ListeningState;</pre>
</div>
</div> <p style="margin: 10px 0 0 0"> <code style="font-family: monospace">ListeningState</code> is where we introduce new playback control concepts. The <code style="font-family: monospace">control()</code> method on playbacks allows for the current playback to be manipulated in various ways. All operations (reverse, pause, unpause, forward, and restart) are demonstrated here.</p> <h1 id="Playbacks-Playbacksonbridges" style="margin: 10px 0 0 0; font-size: 24px; font-weight: normal; line-height: 30px; margin: 40px 0 0 0">Playbacks on bridges</h1> <p style="margin: 10px 0 0 0">Just as channels allow for playbacks to be performed on them, bridges also have the capacity to have sounds, recordings, tones, numbers, etc. played back on them. The difference is is that all participants in the bridge will hear the playback instead of just the single channel. In bridging situations, it can be useful to play certain sounds to an entire bridge (e.g. Telling participants the call is being recorded), but it can also be useful to play sounds to specific participants (e.g. Telling a caller he has joined a conference bridge). A playback on a bridge can be stopped or controlled exactly the same as a playback on a channel.</p>
<div class="panel" style="background-color: silver;border-style: solid;border-width: 1px;">
<div class="panelHeader" style="border-bottom-width: 1px;border-bottom-style: solid;background-color: seagreen;color: black;">
<b>Reader Exercise 6</b>
</div>
<div class="panelContent" style="background-color: silver;">
<p style="margin: 10px 0 0 0; margin-top: 0">If you've read through the Recording and Playbacks pages, then you should have a good grasp on the operations available, as well as a decent state machine implementation. Instead of adding onto the existing voice mail applications, create a new application that uses the recording and playback operations that you have learned about.</p>
<p style="margin: 10px 0 0 0">You will be creating a rudimentary call queue application. The queue application will have two types of participants: agents and callers. Agents and callers call into the same Stasis application and are distinguished based on arguments to the Stasis application (e.g. A caller might call Stasis(queue,caller) and an agent might call Stasis(queue,agent) from the dialplan).</p>
<ul style="margin: 10px 0 0 0">
<li>When an agent calls into the Stasis application, the agent is placed into an agent queue.
<ul style="margin: 10px 0 0 0; margin-top: 0">
<li>While in the agent queue, the agent should hear music on hold. Information about how to play music on hold to a channel can be found here.</li>
</ul> </li>
<li>When a caller calls into the Stasis application, the caller is placed into a caller queue.
<ul style="margin: 10px 0 0 0; margin-top: 0">
<li>While in the caller queue, the caller should hear music on hold.</li>
<li>Every 30 seconds, the caller should hear an announcement. If the caller is at the front of the queue, the "queue-youarenext" sound should play. If the caller is not at the front of the queue, then the caller should hear the "queue-thereare" sound, then the number of callers ahead in the queue, then the "queue-callswaiting" sound.</li>
</ul> </li>
<li>If there is a caller in the caller queue and an agent in the agent queue, then the caller and agent at the front of their respective queues are removed and placed into a bridge with each other. If this happens while a caller has a sound file playing, then this should cause the sound file to immediately stop playing in order to bridge the call.</li>
<li>Once bridged, the agent can perform the following:
<ul style="margin: 10px 0 0 0; margin-top: 0">
<li>Pressing '0' should start recording the bridge. Upon recording the bridge, the caller should hear a beep sound.</li>
<li>Pressing '5' while the call is being recorded should pause the recording. Pressing '5' a second time should unpause the recording.</li>
<li>Pressing '7' while the call is being recorded should stop the recording.</li>
<li>Pressing '9' while the call is being recorded should discard the recording.</li>
<li>Pressing '#' at any time (whether the call is being recorded or not) should end the call, causing the agent to be placed into the back of the agent queue and the caller to be hung up.</li>
<li>If the agent hangs up, then the caller should also be hung up.</li>
</ul> </li>
<li>Once bridged, if the caller hangs up, then the agent should be placed into the back of the agent queue.</li>
</ul>
<p style="margin: 10px 0 0 0">Best of luck!</p>
</div>
</div> </td>
</tr>
</tbody>
</table> </td>
</tr>
<tr>
<td class="email-content-main mobile-expand action-padding last-row-padding" style="padding: 0px; border-collapse: collapse; border-left: 1px solid #ccc; border-right: 1px solid #ccc; border-top: 0; border-bottom: 0; padding: 0 15px 15px 16px; background-color: #fff; padding-bottom: 10px; padding-bottom: 10px">
<table id="actions-pattern" cellspacing="0" cellpadding="0" border="0" width="100%" style="border-collapse: collapse; mso-table-lspace: 0pt; mso-table-rspace: 0pt; color: #333; font-family: Arial, sans-serif; font-size: 14px; line-height: 20px; mso-line-height-rule: exactly; mso-text-raise: 1px">
<tbody>
<tr>
<td id="actions-pattern-container" valign="middle" style="padding: 0px; border-collapse: collapse; padding: 15px 0 0 24px; vertical-align: middle">
<table align="left" style="border-collapse: collapse; mso-table-lspace: 0pt; mso-table-rspace: 0pt; color: #333">
<tbody>
<tr>
<td class="actions-pattern-action-icon-container" style="padding: 0px; border-collapse: collapse; font-family: Arial, sans-serif; font-size: 14px; line-height: 20px; mso-line-height-rule: exactly; mso-text-raise: 0px; vertical-align: middle"><a href="https://wiki.asterisk.org/wiki/display/AST/Playbacks?src=email" title="View page Icon" style="color: #3b73af; text-decoration: none"><img class="actions-pattern-action-icon-image" height="16" width="16" border="0" title="View page Icon" src="cid:com.atlassian.confluence.plugins.confluence-email-resources%3Aview-page-email-adg-footer-item%3Aicon" alt="View page Icon" style="vertical-align: middle" /></a></td>
<td class="actions-pattern-action-text-container" style="padding: 0px; border-collapse: collapse; font-family: Arial, sans-serif; font-size: 14px; line-height: 20px; mso-line-height-rule: exactly; mso-text-raise: 4px; padding-left: 5px; white-space: nowrap"><a href="https://wiki.asterisk.org/wiki/display/AST/Playbacks?src=email" title="View page" style="color: #3b73af; text-decoration: none">View page</a></td>
<td class="actions-pattern-action-bull" style="padding: 0px; border-collapse: collapse; font-family: Arial, sans-serif; font-size: 14px; line-height: 20px; mso-line-height-rule: exactly; mso-text-raise: 4px; color: #999; padding: 0 5px">•</td>
</tr>
</tbody>
</table>
<table align="left" style="border-collapse: collapse; mso-table-lspace: 0pt; mso-table-rspace: 0pt; color: #333">
<tbody>
<tr>
<td class="actions-pattern-action-icon-container" style="padding: 0px; border-collapse: collapse; font-family: Arial, sans-serif; font-size: 14px; line-height: 20px; mso-line-height-rule: exactly; mso-text-raise: 0px; vertical-align: middle"><a href="https://wiki.asterisk.org/wiki/display/AST/Playbacks?showComments=true&showCommentArea=true&src=email#addcomment" title="Add comment Icon" style="color: #3b73af; text-decoration: none"><img class="actions-pattern-action-icon-image" height="16" width="16" border="0" title="Add comment Icon" src="cid:com.atlassian.confluence.plugins.confluence-email-resources%3Aadd-comment-to-content-email-adg-footer-item%3Aicon" alt="Add comment Icon" style="vertical-align: middle" /></a></td>
<td class="actions-pattern-action-text-container" style="padding: 0px; border-collapse: collapse; font-family: Arial, sans-serif; font-size: 14px; line-height: 20px; mso-line-height-rule: exactly; mso-text-raise: 4px; padding-left: 5px; white-space: nowrap"><a href="https://wiki.asterisk.org/wiki/display/AST/Playbacks?showComments=true&showCommentArea=true&src=email#addcomment" title="Add comment" style="color: #3b73af; text-decoration: none">Add comment</a></td>
<td class="actions-pattern-action-bull" style="padding: 0px; border-collapse: collapse; font-family: Arial, sans-serif; font-size: 14px; line-height: 20px; mso-line-height-rule: exactly; mso-text-raise: 4px; color: #999; padding: 0 5px">•</td>
</tr>
</tbody>
</table>
<table style="border-collapse: collapse; mso-table-lspace: 0pt; mso-table-rspace: 0pt; color: #333">
<tbody>
<tr>
<td class="actions-pattern-action-icon-container" style="padding: 0px; border-collapse: collapse; font-family: Arial, sans-serif; font-size: 14px; line-height: 20px; mso-line-height-rule: exactly; mso-text-raise: 0px; vertical-align: middle"><a href="https://wiki.asterisk.org/wiki/plugins/likes/like.action?contentId=30277828&src=email" title="Like Icon" style="color: #3b73af; text-decoration: none"><img class="actions-pattern-action-icon-image" height="16" width="16" border="0" title="Like Icon" src="cid:com.atlassian.confluence.plugins.confluence-like%3Aview-email-adg-content-item%3Aicon" alt="Like Icon" style="vertical-align: middle" /></a></td>
<td class="actions-pattern-action-text-container" style="padding: 0px; border-collapse: collapse; font-family: Arial, sans-serif; font-size: 14px; line-height: 20px; mso-line-height-rule: exactly; mso-text-raise: 4px; padding-left: 5px; white-space: nowrap"><a href="https://wiki.asterisk.org/wiki/plugins/likes/like.action?contentId=30277828&src=email" title="Like" style="color: #3b73af; text-decoration: none">Like</a></td>
</tr>
</tbody>
</table> </td>
</tr>
</tbody>
</table> </td>
</tr>
<tr>
<td class="email-content-rounded-bottom mobile-expand" style="padding: 0px; border-collapse: collapse; color: #fff; height: 5px; line-height: 5px; padding: 0 15px 0 16px; background-color: #fff; border-bottom-right-radius: 5px; border-bottom-left-radius: 5px; border-top: 0; border-left: 1px solid #ccc; border-bottom: 1px solid #ccc; border-right: 1px solid #ccc; mso-line-height-rule: exactly"> </td>
</tr>
</tbody>
</table> </td>
</tr>
<tr>
<td id="footer-pattern" style="padding: 0px; border-collapse: collapse; padding: 12px 20px">
<table id="footer-pattern-container" cellspacing="0" cellpadding="0" border="0" width="100%" style="border-collapse: collapse; mso-table-lspace: 0pt; mso-table-rspace: 0pt; color: #333">
<tbody>
<tr>
<td id="footer-pattern-links-container" width="100%" style="padding: 0px; border-collapse: collapse; color: #999; font-size: 12px; line-height: 18px; font-family: Arial, sans-serif; mso-line-height-rule: exactly; mso-text-raise: 2px">
<table align="left" style="border-collapse: collapse; mso-table-lspace: 0pt; mso-table-rspace: 0pt; color: #333; font-size: 12px; line-height: 18px; font-family: Arial, sans-serif; mso-line-height-rule: exactly; mso-text-raise: 2px">
<tbody>
<tr>
<td class="footer-pattern-links mobile-resize-text" style="padding: 0px; border-collapse: collapse"><a href="https://wiki.asterisk.org/wiki/users/removespacenotification.action?spaceKey=AST&src=email" title="" style="color: #3b73af; text-decoration: none">Stop watching space</a></td>
<td class="footer-pattern-links-bull" style="padding: 0px; border-collapse: collapse; padding: 0 5px; color: #999">•</td>
</tr>
</tbody>
</table>
<table style="border-collapse: collapse; mso-table-lspace: 0pt; mso-table-rspace: 0pt; color: #333; font-size: 12px; line-height: 18px; font-family: Arial, sans-serif; mso-line-height-rule: exactly; mso-text-raise: 2px">
<tbody>
<tr>
<td class="footer-pattern-links mobile-resize-text" style="padding: 0px; border-collapse: collapse"><a href="https://wiki.asterisk.org/wiki/users/editmyemailsettings.action?src=email" title="" style="color: #3b73af; text-decoration: none">Manage notifications</a></td>
</tr>
</tbody>
</table> </td>
</tr>
<tr>
<td id="footer-pattern-text" class="mobile-resize-text" width="100%" style="padding: 0px; border-collapse: collapse; color: #999; font-size: 12px; line-height: 18px; font-family: Arial, sans-serif; mso-line-height-rule: exactly; mso-text-raise: 2px; display: none">This message was sent by Atlassian Confluence 5.6.1</td>
</tr>
</tbody>
</table> </td>
</tr>
</tbody>
</table>
<table id="sealed-section" border="0" cellpadding="0" cellspacing="0" width="0" style="border-collapse: collapse; mso-table-lspace: 0pt; mso-table-rspace: 0pt; color: #333; display: none">
<tbody>
<tr>
<td style="padding: 0px; border-collapse: collapse; border: 0; font-size: 0px; line-height: 0; mso-line-height-rule: exactly"></td>
</tr>
</tbody>
</table>
</body>
</html>