This blog is written by Mihail Comanescu, member of the engineering team
I have been working on implementing a JavaScript IVR in FreeSWITCH and, after checking the FreeSWITCH Wiki, I found that there wasn't much information on how to do that properly. Here are some of the challenges I encountered and solutions that I came up with when implementing a Javascript IVR for FreeSWITCH:
Looking around the Wiki, we find a handful of methods which allow us to relay information on the channel or to listen for DTMF tones.
Session.speak uses a text to speech engine for the playback and looked promising, but it let me down because Cepestral, one of the two TTS engines, didn't work out of the box, and flite, the other TTS engine, has voices that are too robotic for actual use.
Session.sayPhrase uses an XML language macro but relies on the say language module, and that is not what I wanted. I moved as much dialplan as possible from XML to javascript to get away from XML; thus, I expected to be able to write my IVR in javascript completely.
Session.streamFile can be used for playing audio files because it can auto detect the recommended encoding. The down side to using Session.streamFile it uses callback functions to process the DTMF tones it receives, which will execute out of order with the rest of the program; it thus lacks the ability to control the behavior of the IVR. According to the Wiki, streamFile(file,,,); can be set up with a function that will be called each and every time the user presses a key. Presumably, I could store the digits somewhere outside the function, but I would still encounter the following issues: How will I freeze and resume the current processing thread while the callback executes? If I have more than one file that needs streaming, then things can get complicated because DTMF operations might be caught by more than one callback.
As the callback eats the DTMF key pressed, I'd have to check to see if I have the key stored in some memory space and synchronize this somehow with the next stream in the list.
There is no way I can attach a callback function to the say application because they run in different execution contexts: javascript runs in the mod_spidermonkey, and say runs in the FreeSWITCH execution context.
Session.execute(''say'',''en name_spelled iterated'') will call the dialplan application for the Callie IVR which comes preinstalled with FreeSWITCH. Callie works by playing in sequence a list of files which matched on a rule the < said string>. For example the parameters en, will tell Callie to use the English recording set, name_spelled will tell Callie to try to spell the and iterated will tell Callie to go through one letter at a time. More information about the say application can be found here.
Basically, my IVR should at least be able to playback one or more recordings one after the other and, after the playback, give me the option of collecting the input from the user. In the case of an user pressing a key while the IVR was playing a file, the IVR will silence and collect user input until the desired timeout or the desired key is pressed. Neither streamFile nor execute(''say'') meet these requirements out of the box.
After much testing, I discovered that both say and streamFile respond to the channel variable playback_terminators, which, once set to a specific list of keys, will interrupt the current playback as soon as the key is hit. Furthermore, the playback terminator used is stored in the channel variable playback_terminator_used, which can be easily retrieved for further usage.
The last thing I needed was be a way to playback silence while we wait for user input, or until it times out. This turned out to be rather easy by using Session.execute(''sleep'',''''). Like say, sleep is a dialplan application that pauses the channel for the given timeout, or until one issues a DTMF signal (in which case, the execution thread wakes up and continues). At this point, this is almost perfect, unless we take into account that we might need the digits that were pressed while sleeping. Unfortunately for us, the digits are not caught as playback terminators but can be recovered with session.getDigits(), which, unfortunately is not capable of returning a playback terminator used. In other words, we would have to check both session.getDigits() and playback_terminator_used to get all the digits possibly pressed by the user.
So, how can we use the methods above to simply build a JavaScript IVR? Well, we can write some library code to deal with it:
View the code [Expand/Close]
var IVR=function(){// your path to the sounds folder. I used prerecorded sounds from callie this.localPath="path to your sounds folder";this.sleep=function(time){if(session.ready)session.execute("sleep",time);}this.play=function(filePath){if(!session.getVariable("playback_terminator_used"))try{session.streamFile(filePath);}catch(e){console_log("WARNING","Could not stream file "+this.localPath+filePath);console_log("WARNING","Error:"+e);}}this.say=function(stuff){if(!session.getVariable("playback_terminator_used"))session.execute("say","en name_spelled iterated "+stuff);}this.flushDigit=function(){var terms=session.getVariable("playback_terminator_used");if(!terms) {terms=session.getDigits(1,"");session.flushDigits();}session.execute("unset","playback_terminator_used");return terms;}this.clearDTMF=function(){session.execute("set","playback_terminators=none");session.flushDigit();}this.getDigits=function(numDigits,mTerminator,iTimeout,dTimeout){numDigits=Number(numDigits);iTimeout=Number(iTimeout);dTimeout=Number(dTimeout);if(!iTimeout) iTimeout=3000;if(!dTimeout) dTimeout=5000;if(!numDigits) return "";if(numDigits<0) return "";var keep=false,myDigits="",aDigit=this.flushDigit();var origTerminators=session.getVariable("playback_terminators");if(!mTerminator) mTerminator="";if(mTerminator.indexOf("+")>=0) keep=true;session.setVariable("playback_terminators","0123456789*#");if(!aDigit){this.sleep(iTimeout);aDigit=this.flushDigit();}numDigits--;if(aDigit){if(mTerminator.indexOf(aDigit)>=0){if(keep) myDigits+=aDigit;return myDigits;}}myDigits+=aDigit;var isDigit=true;if(!numDigits) return myDigits;while(numDigits&&isDigit){this.sleep(dTimeout);aDigit=this.flushDigit();if(aDigit){numDigits--;if(mTerminator.indexOf(aDigit)>=0) isDigit=false;if(isDigit||keep) myDigits+=aDigit;} else isDigit=false;}session.setVariable("playback_terminators",origTerminators);return myDigits;}}
That's a big chunk of code here, but what exactly does it do? Well, in short it tries to create an object which deals with the IVR issues described above. We now have a few functions which tackle different aspects of the problem.
The sleep, say and play functions are wrappers for the dialplan applications described above. Both play and say are not to be executed if a playback terminator is found, and doing so, if the user presses the magic key while the IVR is reciting something, then that sound and the rest of the menu is skipped. We choose this behavior because there is a high chance he already knows the menu and should be spared the wait, since everyone is in a hurry these days.
flushDigit will clear and return the pending DTMF digit from either the playback terminator or the session.getDigits method.
getDigits will return either the number of digits specified by numDigits or the number of digits until a terminator key is specified in the mTerminator variable. If the mTerminator has a “+” in the specifier, then getDigits will return the digits including the terminator. The next two fields are used for specifying timeouts. iTimeout represents the timout it should wait until the first digit is considered pressed and dTimeout represents the amount of time that is allowed between two consecutive digits.
Happy FreeSWITCH coding!