Handling DTMF events¶
DTMF events are conveyed via the ChannelDtmfReceived
event. The event contains the channel that pressed the DTMF key, the digit that was pressed, and the duration of the digit.
While this concept is relatively straight forward, handling DTMF is quite common in applications, as it is the primary mechanism that phones have to inform a server to perform some action. This includes manipulating media, initiating call features, performing transfers, dialling, and just about every thing in between. As such, the examples on this page focus less on simply handling the event and more on using the DTMF in a relatively realistic fashion.
Example: A simple automated attendant¶
This example mimics the automated attendant/IVR dialplan example. It does the following:
- Plays a menu to the user which is cancelled when the user takes some action.
- If the user presses 1 or 2, the digit is repeated to the user and the menu restarted.
- If the user presses an invalid digit, a prompt informing the user that the digit was invalid is played to the user and the menu restarted.
- If the user fails to press anything within some period of time, a prompt asking the user if they are still present is played to the user and the menu restarted.
Tip
For this example, you will need the following:
- The extra sound package from Asterisk. You can install this using the
menuselect
tool. - If using the Python example,
ari-py
version 0.1.3 or later. - If using the JavaScript example, ari-client version 0.1.4 or later.
Dialplan¶
As usual, a very simple dialplan is sufficient for this example. The dialplan takes the channel and places it into the Stasis application channel-aa
.
extensions.conf:
Python¶
As this example is a bit larger, how the code is written and structured is broken up into two phases:
- Constructing the menu and handling its state as the user presses buttons.
- Actually handling the button presses from the user.
The full source code for this example immediately follows the walk through.
Playing the menu¶
Unlike Playback, which can chain multiple sounds together and play them back in one continuous operation, ARI treats all sound files being played as separate operations. It will queue each sound file up to be played on the channel, and hand back the caller an object to control the operation of that single sound file. The menu announcement for the attendant has the following requirements:
- Playback the options for the user
- If the user presses a DTMF key, cancel the playback of the options and handle the request
- If the user presses an invalid DTMF key, let them know and restart the menu
- If the user doesn't press anything, wait 10 seconds, ask them if they are still present, and restart the menu
The second requirement makes this a bit more challenging: when the user presses a DTMF key, we want to cancel whatever sound file is currently being played back and immediately handle their request. We thus have to maintain some state in our application about what sound file is currently being played so that we can cancel the correct playback. We also don't want to queue up all of the sounds immediately - we'd have to walk through all of the queued up sounds and cancel each one - that'd be annoying! Instead, we only want to start the next sound in our prompt when the previous has completed.
To start, we'll define in a list at the top of our script the sounds that make up the initial menu prompt:
Since we'll want to maintain some state, we'll create a small object to do that for us. In Python, tuples are immutable - and we'll want to mutate the state in callbacks when certain operations happen. As such, it makes sense to use a small class for this with two properties:
- The current sound being played
- Whether or not we should consider the menu complete
It's useful to have both pieces of data, as we may cancel the menu half-way through and want to take one set of actions, or we may complete the menu and all the sounds and start a different set of actions.
To start, we'll write a function, play_intro_menu
, that starts the menu on a channel. It will simply initialize the state of the menu, and get the ball rolling on the channel by calling queue_up_sound
.
queue_up_sound
will be responsible for starting the next sound file on the channel and handling the manipulation of that sound file. Since there's a fair amount of checking that goes into this, we'll put the actual act of starting the sound in play_next_sound
, which will return the Playback
object from ARI. We'll prep the menu_state
object for the next sound file playback, and pass it to the PlaybackFinished
handler for the current sound being played back to the channel.
play_next_sound
will do two things:
- If we shouldn't play another sound - either because we've run out of sounds to play or because the menu is now "complete", we bail and return None.
- If we should play back a sound, start it up on the channel and return the
Playback
object.
Our playback finished handler is very simple: since we've already incremented the state of the menu, we just call queue_up_sound
again:
To recap, our play_intro_menu
function has three nested functions:
queue_up_sound
- starts a sound on a channel, increments the state of the menu, and subscribes for thePlaybackFinished
event.play_next_sound
- if possible, actually starts the sound. Called fromqueue_up_sound
.on_playback_finished
- called whenPlaybackFinished
is received for the current playback, and callqueue_up_sound
to start the next sound in the menu.
This will play back the menu sounds, but it doesn't handle cancelling the menu, time-outs, or other conditions. To do that, we're going to need more information from Asterisk.
Cancelling the menu¶
When the user presses a DTMF key, we want to stop the current playback and end the menu. To do that, we'll need to subscribe for DTMF events from the channel. We'll define a new handler function, cancel_menu
, and tell ari-py
to call it when a DTMF key is received via the ChannelDtmfReceived
event. We don't really care about the digit here - we just want to cancel the menu. In the handler function, we'll set menu_state.complete
to True
, then tell the current_playback
to stop.
We should also stop the menu when the channel is hung up. Since the cancel_menu
, so we'll subscribe to the StasisEnd
event here and call cancel_menu
from it as well:
Timing out¶
Now we can cancel the menu, but we also need to restart it if the user doesn't do anything. We can use a Python timer to start a timer if we're finished playing sounds and we got to the end of the sound prompt list. We don't want to start the timer if the user pressed a DTMF key - in that case, we would have stopped the menu early and we should be off handling their DTMF key press. The timer will call menu_timeout
, which will play back a "are you still there?" prompt, then restart the menu.
Now that we've introduced timers, we know we're going to need to stop them if the user does something. We'll store the timers in a dictionary indexed by channel ID, so we can get them from various parts of the script:
Handling the DTMF options¶
While we now have code that plays back the menu to the user, we actually have to implement the attendant menu still. This is slightly easier than playing the menu. We can register for the ChannelDtmfReceived
event in the StasisStart
event handler. In that callback, we need to do the following:
- Cancel any timers associated with the channel. Note that we don't need to stop the playback of the menu, as the menu function
queue_up_sound
already registers a handler for that event and cancels the menu when it gets any digit. - Actually handle the digit, if the digit is a
1
or a2
. - If the digit isn't supported, play a prompt informing the user that their option was invalid, and re-play the menu.
The following implements these three items, deferring processing of the valid options to separate functions.
Cancelling the timer is done in a fashion similar to other examples. If the channel has a Python timer associated with it, we cancel the timer and remove it from the dictionary.
Finally, we need to actually do something when the user presses a 1 or a 2. We could do anything here - but in our case, we're merely going to play back the number that they pressed and restart the menu.
channel-aa.py¶
The full source for channel-aa.py
is shown below:
channel-aa.py | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 |
|
channel-aa.py in action¶
The following shows the output of channel-aa.py
when a PJSIP channel presses 1, 2, 8, then times out. Finally they hang up.
Channel PJSIP/alice-00000001 has entered the application
Channel PJSIP/alice-00000001 entered 1
Channel PJSIP/alice-00000001 entered 2
Channel PJSIP/alice-00000001 entered 8
Channel PJSIP/alice-00000001 entered an invalid option!
Channel PJSIP/alice-00000001 stopped paying attention...
PJSIP/alice-00000001 has left the application
JavaScript (Node.js)¶
As this example is a bit larger, how the code is written and structured is broken up into two phases:
- Constructing the menu and handling its state as the user presses buttons.
- Actually handling the button presses from the user.
The full source code for this example immediately follows the walk through.
Playing the menu¶
Unlike Playback, which can chain multiple sounds together and play them back in one continuous operation, ARI treats all sound files being played as separate operations. It will queue each sound file up to be played on the channel, and hand back the caller an object to control the operation of that single sound file. The menu announcement for the attendant has the following requirements:
- Playback the options for the user
- If the user presses a DTMF key, cancel the playback of the options and handle the request
- If the user presses an invalid DTMF key, let them know and restart the menu
- If the user doesn't press anything, wait 10 seconds, ask them if they are still present, and restart the menu
The second requirement makes this a bit more challenging: when the user presses a DTMF key, we want to cancel whatever sound file is currently being played back and immediately handle their request. We thus have to maintain some state in our application about what sound file is currently being played so that we can cancel the correct playback. We also don't want to queue up all of the sounds immediately - we'd have to walk through all of the queued up sounds and cancel each one - that'd be annoying! Instead, we only want to start the next sound in our prompt when the previous has completed.
To start, we'll define an object to represent the menu at the top of our script that defines sounds that make up the initial menu prompt as well as valid DTMF options for the menu:
To start with, well register a callback to handle a StasisStart and StasisEnd event on any channel that enters into our application:
Note that we register a callback to handle ChannelDtmfReceived events on a channel entering our application in StasisStart and then unregister that callback on StasisEnd. For long running, non-trivial applications, this allows the JavaScript garbage collector to clean up our callback. This is important since every channel entering into our application will register its own copy of the callback which is not be garbage collected until it is unregistered.
We'll cover the DTMF callback handler shortly, but first we'll cover writting functions to handle playing the menu prompt
First we'll write a function to initialize a new instance of our menu; playIntroMenu.
Since we'll want to maintain some state, we'll create a small object to do that for us. This object will keep track of the following:
- The current sound being played
- The current Playback object being played
- Whether or not this menu instance is done
It's useful to have this data, as we may cancel the menu half-way through and want to take one set of actions, or we may play all the sounds that make up the menu prompt and start a different set of actions.
playIntroMenu will
start the menu on a channel. It will simply initialize the state of the menu, and get the ball rolling on the channel by calling queueUpSound
which is a nested function within playIntroMenu.
We'll cover cancelMenu shortly, but first let's discuss queueUpSound. queueUpSound
will be responsible for starting the next sound file on the channel and handling the manipulation of that sound file. queueUpSound is also responsible for starting a timeout once all sounds for the menu prompt have completed to handle reminding the user that they must choose a menu option. We'll cover that part shortly but first, we'll cover handling progerssing through the sounds that make up the menu prompt. We first initiate playback on the current sound in the sequence. We then register a callback to handle that playback finishing, which will trigger queueUpSound to be called again, moving on to the next sound in the sequence. Finally, we update the state object to reflect the next sound to be played in the menu prompt sequence.
Notice that when registering our PlaybackFinished callback handler, we use the once method on the resource instance instead of on. This ensures that the callback will be invoked once and then automatically be unregistered. Since a PlaybackFinished event will only be invoked once for a given Playback instance, it makes sense to use this method which will also enable the callback to be garbage collected once it has been invoked.
queueUpSound will play back the menu sounds, but it doesn't handle cancelling the menu, time-outs, or other conditions. To do that, we're going to need more information from Asterisk.
Cancelling the menu¶
When the user presses a DTMF key, we want to stop the current playback and end the menu. To do that, we'll need to subscribe for DTMF events from the channel. We'll define a new handler function, cancelMenu
, and tell ari-client
to call it when a DTMF key is received via the ChannelDtmfReceived
event. We don't really care about the digit here - we just want to cancel the menu. In the handler function, we'll set state.done
to true
, then tell the currentPlayback
to stop.
We should also stop the menu when the channel is hung up. To do this we'll subscribe to the StasisEnd
event as well and register cancelMenu as its callback handler:
Note that once the cancelMenu callback is invoked, we unregister both the ChannelDtmfReceived and StasisEnd events. This is performed so that once this particular menu instance stops, we do not leave registered callbacks behind that will never be garbage collected.
Timing out¶
Now we can cancel the menu, but we also need to restart it if the user doesn't do anything. We can use a JavaScript timeout to start a timer if we're finished playing sounds and we got to the end of the sound prompt sequence. We don't want to start the timer if the user pressed a DTMF key - in that case, we would have stopped the menu early and we should be off handling their DTMF key press. The timer will call stillThere
, which will play back a "are you still there?" prompt, then restart the menu.
Now that we've introduced timers, we know we're going to need to stop them if the user does something. We'll store the timers in an object indexed by channel ID, so we can get them from various parts of the script:
Handling the DTMF options¶
While we now have code that plays back the menu to the user, we actually have to implement the attendant menu still. Earlier in our example we registered a callback handler for a ChannelDtmfReceived event on a channel that enters into our application. In that callback, we need to do the following:
- Cancel any timers associated with the channel. Note that we don't need to stop the playback of the menu, as the menu function
queueUpSound
already registers a handler for that event and cancels the menu when it gets any digit. - Actually handle the digit, if the digit is a
1
or a2
. - If the digit isn't supported, play a prompt informing the user that their option was invalid, and re-play the menu.
The following implements these three items, deferring processing of the valid options to a separate function.
Cancelling the timer is done in a fashion similar to other examples. If the channel has a JavaScript timeout associated with it, we cancel the timer and remove it from the object.
Finally, we need to actually do something when the user presses a 1 or a 2. We could do anything here - but in our case, we're merely going to play back the number that they pressed and restart the menu.
channel-aa.js¶
The full source for channel-aa.js
is shown below:
channel-aa.js | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 |
|
channel-aa.js in action¶
The following shows the output of channel-aa.js
when a PJSIP channel presses 1, 2, 8, then times out. Finally they hang up.
Channel PJSIP/alice-00000001 has entered the application
Channel PJSIP/alice-00000001 entered 1
Channel PJSIP/alice-00000001 entered 2
Channel PJSIP/alice-00000001 entered 8
Channel PJSIP/alice-00000001 entered an invalid option!
Channel PJSIP/alice-00000001 stopped paying attention...
PJSIP/alice-00000001 has left the application