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:
To make the new "greeting" state 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 /sounds resource in ARI provides methods to list the sounds installed on the system, as well as the ability to get specific sound files.
Asterisk searches for sounds in the /sounds/ subdirectory of the configured astdatadir option in asterisk.conf. By default, Asterisk will search for sounds in /var/lib/asterisk/sounds. When Asterisk starts up, it indexes the installed sounds and keeps an in-data representation of those sound files. 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 has some trade-offs. When querying for sound information, this in-memory indexing makes the operations much faster. On the other hand, it also means that Asterisk has to be "poked" to re-index the sounds if new sounds are added to the file system after Asterisk is running. The Asterisk CLI command "module reload sounds" provides a means of having Asterisk re-index the sounds on the system so that they are available to ARI.
For our greeting, we will play the built-in sound "vm-intro". Here is the code for our new state:
fromeventimportEventdefsounds_installed(client):try:client.sounds.get(soundId='vm-intro')except:print("Required sound 'vm-intro' not installed. Aborting")raiseclassGreetingState(object):state_name="greeting"def__init__(self,call):self.call=callself.hangup_event=Noneself.playback_finished=Noneself.dtmf_event=Noneself.playback=Nonesounds_installed(call.client)defenter(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")defcleanup(self):self.playback_finished.close()self.dtmf_event.close()self.hangup_event.close()defon_hangup(self,channel,event):print("Abandoning voicemail recording on hangup")self.cleanup()self.call.state_machine.change_state(Event.HANGUP)defon_playback_finished(self,playback):self.cleanup()self.call.state_machine.change_state(Event.PLAYBACK_COMPLETE)defon_dtmf(self,channel,event):digit=event.get('digit')ifdigit=='#':print("Cutting off greeting on DTMF #")# Let on_playback_finished take care of state changeself.playback.stop()
varEvent=require('./event');functionsounds_installed(client){client.sounds.get({soundId:'vm-intro'},function(err){if(err){console.log("Required sound 'vm-intro' not installed. Aborting");throwerr;}});}functionGreetingState(call){this.state_name="greeting";sounds_installed(call.client);this.enter=function(){varplayback=call.client.Playback();console.log("Entering greeting state");call.channel.on("ChannelHangupRequest",on_hangup);call.channel.on("ChannelDtmfReceived",on_dtmf);call.client.on("PlaybackFinished",on_playback_finished);call.channel.play({media:'sound:vm-intro'},playback);functioncleanup(){call.channel.removeListener('ChannelHangupRequest',on_hangup);call.channel.removeListener('ChannelDtmfReceived',on_dtmf);call.client.removeListener('PlaybackFinished',on_playback_finished);if(playback){playback.stop();}}functionon_hangup(event,channel){console.log("Abandoning voicemail recording on hangup");cleanup();call.state_machine.change_state(Event.HANGUP);}functionon_playback_finished(event){if(playback&&playback.id===event.playback.id){cleanup();call.state_machine.change_state(Event.PLAYBACK_COMPLETE);}}functionon_dtmf(event,channel){switch(event.digit){case'#':console.log("Skipping greeting");cleanup();call.state_machine.change_state(Event.DTMF_OCTOTHORPE);}}}}module.exports=GreetingState;
The sounds.get() method employed here allows for a single sound to be retrieved based on input parameters. Here, we simply specify the name of the recording we want to ensure that it exists in some form on the system. By checking for the sound's existence in the initialization of GreetingState, we can abort the call early if the sound is not installed.
And here is our updated state machine:
```python title="vm-call.py" linenumbers="1" #At the top of the file from greeting_state import GreetingState
```javascript title="vm-call.js" linenums="1"
//At the top of the file
var GreetingState = require('./greeting_state');
//In VoicemailCall::setup_state_machine()
this.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);
}
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.
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
Our current implementation of GreetingState does not take language into consideration. The sounds_installed 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.
For this exercise, modify sounds_installed() to also check if the retrieved sound exists in the language of the calling channel. The channel's language can be retrieved using the getChannelVar() method on a channel to retrieve the value of variable "CHANNEL(language)". The sound returned by sounds.get() contains an array of FormatLang 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.
So far in our voice mail application, we have stopped playbacks, but there are a lot more interesting operations that can be done on them, 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.
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 enters the application, 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:
Press the 1' key to go back 3 seconds in the current message playback.
Press the 2 key to pause or unpause the current message playback.
Press the 3 key to go forward 3 seconds in the current message playback.
Press the 4 key to play to the previous message.
Press the 5 key to restart the current message playback.
Press the 6 key to play to the next message.
Press the * key to delete the current message and play the next message.
Press the # key to end the call.
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.
This means defining a brand new state machine. To start with, we'll define three new states. The "preamble" state is the initial state of the state machine, 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:
Notice that while in the listening state, 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.
Quite a bit of this is similar to what we were using for our voice mail recording application. The biggest difference here is that the call has many more methods defined since playing back voice mails is more complicated than recording a single one.
Now that we have the state machine defined and the application written, let's actually write the required new states. First of the new states is the "preamble" state.
varEvent=require('./event');functionsounds_installed(client){client.sounds.get({soundId:'vm-message'},function(err){if(err){console.log("Required sound 'vm-message' not installed. Aborting");throwerr;}});}functionPreambleState(call){this.state_name="preamble";this.enter=function(){varcurrent_playback;varsounds_to_play;varplayback;console.log("Entering preamble state");if(call.mailbox_empty()){call.state_machine.change_state(Event.MAILBOX_EMPTY);return;}call.channel.on("ChannelHangupRequest",on_hangup);call.client.on("PlaybackFinished",on_playback_finished);call.channel.on("ChannelDtmfReceived",on_dtmf);initialize_playbacks();functioninitialize_playbacks(){current_playback=0;sounds_to_play=[{'playback':call.client.Playback(),'media':'sound:vm-message'},{'playback':call.client.Playback(),'media':'number:'+call.get_current_voicemail_number()}];start_playback();}functionstart_playback(){current_sound=sounds_to_play[current_playback];playback=current_sound['playback'];call.channel.play({media:current_sound['media']},playback);}functioncleanup(){call.channel.removeListener('ChannelHangupRequest',on_hangup);call.channel.removeListener('ChannelDtmfReceived',on_dtmf);call.client.removeListener('PlaybackFinished',on_playback_finished);if(playback){playback.stop();}}functionon_hangup(event,channel){playback=null;cleanup();call.state_machine.change_state(Event.HANGUP);}functionon_playback_finished(event){varcurrent_sound=sounds_to_play[current_playback];if(playback&&(playback.id===event.playback.id)){playback=null;current_playback++;if(current_playback===sounds_to_play.length){cleanup();call.state_machine.change_state(Event.PLAYBACK_COMPLETE);}else{start_playback();}}}functionon_dtmf(event,channel){switch(event.digit){case'#':cleanup();call.state_machine.change_state(Event.DTMF_OCTOTHORPE);}}}}module.exports=PreambleState;
PreambleState should look similar to the GreetingState introduced previously on this page. 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 channel.play() while a playback is playing on a channel in order to queue a second playback. For our application though, we have elected to play the second sound only after the first has completed. The reason for this is that if there is only a single active playback at any given time, then it becomes easier to clean up the current state when an event occurs that causes a state change.
fromeventimportEventimportuuiddefsounds_installed(client):try:client.sounds.get(soundId='vm-nomore')except:print"Required sound 'vm-nomore' not installed. Aborting"raiseclassEmptyState(object):state_name="empty"def__init__(self,call):self.call=callself.playback_id=Noneself.hangup_event=Noneself.playback_finished=Noneself.playback=Nonesounds_installed(call.client)defenter(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")defcleanup(self):self.playback_finished.close()ifself.playback:self.playback.stop()self.hangup_event.close()defon_hangup(self,channel,event):# Setting playback to None stops cleanup() from trying to stop the# playback.self.playback=Noneself.cleanup()self.call.state_machine.change_state(Event.HANGUP)defon_playback_finished(self,event):ifself.playback_id==event.get('playback').get('id'):self.playback=Noneself.cleanup()self.call.state_machine.change_state(Event.PLAYBACK_COMPLETE)
ListeningState is where we introduce new playback control concepts. Playbacks have their controlling operations wrapped in a single method, control(), rather than having lots of separate operations. All control operations (reverse, pause, unpause, forward, and restart) are demonstrated by this state.
You may have noticed while exploring the playbacks API that the control() method takes no parameters other than the operation. This means that certain properties of operations are determined when the playback is started on the channel.
For this exercise, modify the ListeningState so that DTMF '1' and '3' reverse or forward the playback by 5000 milliseconds instead of the default 3000 milliseconds.
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 a 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.
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. For our final exercise, instead of adding onto the existing voice mail applications, create a new application that uses some of the recording and playback operations that you have learned about.
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).
When an agent calls into the Stasis application, the agent is placed into an agent queue.
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.
When a caller calls into the Stasis application, the caller is placed into a caller queue.
While in the caller queue, the caller should hear music on hold.
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.
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. For more information on bridging channels, see this page. 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.
Once bridged, the agent can perform the following:
Pressing '0' should start recording the bridge. Upon recording the bridge, the caller should hear a beep sound.
Pressing '5' while the call is being recorded should pause the recording. Pressing '5' a second time should unpause the recording.
Pressing '7' while the call is being recorded should stop the recording.
Pressing '9' while the call is being recorded should discard the recording.
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.
If the agent hangs up, then the caller should also be hung up.
Once bridged, if the caller hangs up, then the agent should be placed into the back of the agent queue.