Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
I am the chief technical officer at InVision App, Inc - a prototyping and collaboration platform for designers, built by designers. I also rock out in JavaScript and ColdFusion 24x7.
Meanwhile on Twitter
Loading latest tweet...
Ben Nadel at cf.Objective() 2013 (Bloomington, MN) with:

Playing An MP3 Over The Phone With ColdFusion And Twilio

By Ben Nadel on

After I posted yesterday about using Twilio to authenticate a user over the phone in realtime, Kate Maher asked me about calling a target number and playing an MP3 over the phone. As it turns out, thanks to Twilio's robust markup language, playing an MP3 is as easy as including the "Play" verb in your TwiML XML response. To demonstrate this, I've created a page in which you can select an MP3, provide a number, and watch the call status in realtime.

 
 
 
 
 
 
 
 
 
 

In the following demo, Twilio will be responsible for making the outgoing call and playing the MP3. But, in order to let the client-side user peer into the call processing, we'll need a way to send realtime messages from the ColdFusion server back to the browser. For this, we can use Pusher which allows messages to be pushed from the server to the client using HTML5 WebSockets (and other fallback strategies).

As far as workflow is concerned, the routing logic is actually pretty linear:

  1. User submits a call.
  2. Browser sends request to the ColdFusion server.
  3. ColdFusion server pushes "dialing" event back to browser.
  4. ColdFusion server sends outbound call request to Twilio.
  5. Twilio initiates call.
  6. Twilio asks ColdFusion server for call logic.
  7. ColdFusion server pushes "playing" event back to browser.
  8. ColdFusion server returns MP3 URL to Twilio.
  9. Twilio tells ColdFusion server that the call has ended.
  10. ColdFusion server pushes "ended" event back to browser.

Sounds straightforward right? Then let's dive into some code. First, we'll look at the Application.cfc file; this just initialized the application, providing both the Twilio and Pusher App account credentials.

Application.cfc

  • <cfcomponent
  • output="false"
  • hint="I define the application settings and event handlers.">
  •  
  • <!--- Define the application settings. --->
  • <cfset this.name = hash( getCurrentTemplatePath() ) />
  • <cfset this.applicationTimeout = createTimeSpan( 0, 0, 10, 0 ) />
  •  
  •  
  • <cffunction
  • name="onApplicationStart"
  • access="public"
  • returntype="boolean"
  • output="false"
  • hint="I initialize the application.">
  •  
  • <!---
  • Set up the Twilio information for Telephony integration.
  • We will need this information to initialize phone calls.
  • --->
  • <cfset application.twilio = {
  • accountSID = "*********************",
  • authToken = "*********************",
  • phoneNumber = "*********************"
  • } />
  •  
  • <!---
  • Set up the Pusher information for realtime push
  • notifications. We will need Pusher to let the
  • MP3 selection page know about the phone-based
  • interactions.
  • --->
  • <cfset application.pusher = {
  • appID = "*********************",
  • key = "*********************",
  • secret = "*********************"
  • } />
  •  
  • <!--- Return true so the page will continue loading. --->
  • <cfreturn true />
  • </cffunction>
  •  
  • </cfcomponent>

NOTE: Because all the Twilio interaction is being initiated within the ColdFusion code, we don't have to define any Voice or SMS end points for our Twilio number.

Next, let's look at the actual demo page where the user selects the MP3 and the target phone number. This is where most of the action is happening. In the following demo, notice that we are using both AJAX and Pusher to create a two-way stream of communication with the ColdFusion server. The AJAX sends the initial request; and, the Pusher service allows ColdFusion to send realtime status updates back to the browser.

Index.cfm (Demo Page)

  • <!---
  • Create a UUID for this page request. That way, our Pusher
  • (realtime notification service) knows to only connect to
  • this page.
  • --->
  • <cfset requestID = "request-#createUUID()#" />
  •  
  •  
  • <!--- Reset the output buffer and set the mime type. --->
  • <cfcontent type="text/html" />
  •  
  • <cfoutput>
  •  
  • <!DOCTYPE html>
  • <html>
  • <head>
  • <title>Playing An MP3 Over The Phone With Twilio</title>
  •  
  • <!--- jQuery library for DOM manipulation. --->
  • <script type="text/javascript" src="./jquery-1.5.1.js"></script>
  •  
  • <!--- Pusher library for realtime notification. --->
  • <script type="text/javascript" src="http://js.pusherapp.com/1.7/pusher.min.js"></script>
  • </head>
  • <body>
  •  
  • <h1>
  • Playing An MP3 Over The Phone With Twilio
  • </h1>
  •  
  •  
  • <form>
  •  
  • <!--- Store the request ID. --->
  • <input type="hidden" name="id" value="#requestID#" />
  •  
  • <p>
  • Please select an MP3 to play:
  •  
  • <select name="mp3">
  • <option value="1">Kenny Loggins</option>
  • <option value="2">Lana!</option>
  • <option value="3">Last Words</option>
  • <option value="4">Life Insurance</option>
  • </select>
  • </p>
  •  
  • <p>
  • Please enter a target phone number:
  •  
  • <input type="password" name="number" />
  • </p>
  •  
  • <p>
  • <input type="submit" value="Send MP3" />
  • </p>
  •  
  • </form>
  •  
  •  
  • <!---
  • Here is where we will display the call status for
  • the outgoing MP3-based phone call.
  • --->
  • <p style="font-size: 150% ; margin-top: 20px ;">
  • Call Status:
  • <strong data-status="na" class="status">N/A</strong>
  • </p>
  •  
  •  
  • <!--- --------------------------------------------- --->
  • <!--- --------------------------------------------- --->
  • <!--- --------------------------------------------- --->
  • <!--- --------------------------------------------- --->
  •  
  •  
  • <!---
  • Now that our DOM is in place, let's initialize the
  • user interaction scripts.
  • --->
  • <script type="text/javascript">
  •  
  • // Cache our jQuery DOM references.
  • var dom = {};
  • dom.form = $( "form" );
  • dom.id = dom.form.find( "input[ name = 'id' ]" );
  • dom.mp3 = dom.form.find( "select" );
  • dom.number = dom.form.find( "input[ name = 'number' ]" );
  • dom.status = $( "strong.status" );
  •  
  •  
  • // Override the form submission behavior so that we can
  • // implement our own AJAX-based interactions.
  • dom.form.submit(
  • function( event ){
  •  
  • // Prevent the default submission behavior.
  • event.preventDefault();
  •  
  • // Check to see that we are not currently in the
  • // middle of another phone-based interaction.
  • if (dom.status.data( "status" ) != "na"){
  •  
  • // There is another phone-based interaction
  • // happening. Exit out of this guard
  • // statement and function.
  • return;
  •  
  • }
  •  
  • // Get the cleaned up version of the phone number.
  • var number = dom.number.val().replace(
  • new RegExp( "^1|[^\\d]+", "g" ),
  • ""
  • );
  •  
  • // Check to make sure the phone number is 10
  • // digits - we are working with US numbers only
  • // for this demo.
  • if (number.length != 10){
  •  
  • // Let the user know their phone number is
  • // not valid.
  • alert( "Please enter a valid 10-digit phone number." );
  •  
  • // Exit out of this guard statement.
  • return;
  •  
  • }
  •  
  •  
  • // ASSERT: If we have made it this far then we
  • // know that we are dealing with valid data.
  •  
  •  
  • // Flag the call as being initialized.
  • dom.status
  • .data( "status", "initializing" )
  • .text( "Initializing" )
  • ;
  •  
  • // Make the call-request to the server.
  • var callRequest = $.ajax({
  • type: "post",
  • url: "./call.cfm",
  • data: {
  • id: dom.id.val(),
  • mp3: dom.mp3.val(),
  • number: number
  • },
  • dataType: "text"
  • });
  •  
  • // Listen for a failure event on the AJAX request
  • // so we can reset the call status.
  • callRequest.fail(
  • function(){
  •  
  • // Let the user know that something went
  • // wrong with the request.
  • alert( "Uh-oh! There was an unexpected error - you're in the Danger Zone!!!!" );
  •  
  • // Reset the call status.
  • dom.status
  • .data( "status", "na" )
  • .text( "N/A" )
  • ;
  •  
  • }
  • );
  •  
  • }
  • );
  •  
  •  
  • // ---------------------------------------------- //
  • // ---------------------------------------------- //
  •  
  •  
  • // Connect to Pusher so we can start to listen for
  • // realtime notifications during the outgoing mp3 call
  • // process.
  • var pusher = new Pusher( "#application.pusher.key#" );
  •  
  • // Listen to the Pusher channel for this unique request
  • // ID. This way, each request will only send notifications
  • // back to the page that initiated it.
  • var channel = pusher.subscribe( dom.id.val() );
  •  
  • // Bind to the dialing event. This is when Twilio has
  • // received the outgoing request.
  • channel.bind(
  • "dialing",
  • function( data ){
  •  
  • // Check to make sure that the status is
  • // not currently N/A, which would indicate an
  • // issue with the asynchronous requests.
  • if (dom.status.data( "status" ) == "na"){
  •  
  • // Exit out of guard statement.
  • return;
  •  
  • }
  •  
  • // Flag the status as being dialed.
  • dom.status
  • .data( "status", "dialing" )
  • .text( "Dialing" )
  • ;
  •  
  • }
  • );
  •  
  • // Bind to the playing event. This is when Twilio has
  • // connected to the target number and has begun to
  • // play the selected mp3.
  • channel.bind(
  • "playing",
  • function( data ){
  •  
  • // Check to make sure that the status is
  • // not currently N/A, which would indicate an
  • // issue with the asynchronous requests.
  • if (dom.status.data( "status" ) == "na"){
  •  
  • // Exit out of guard statement.
  • return;
  •  
  • }
  •  
  • // Flag the status as being played.
  • dom.status
  • .data( "status", "playing" )
  • .text( "Playing MP3" )
  • ;
  •  
  • }
  • );
  •  
  • // Bind to the call-ended event. This is when Twilio
  • // has finished playing the mp3 and has hung up on
  • // the target user.
  • channel.bind(
  • "ended",
  • function( data ){
  •  
  • // Check to make sure that the status is
  • // not currently N/A, which would indicate an
  • // issue with the asynchronous requests.
  • if (dom.status.data( "status" ) == "na"){
  •  
  • // Exit out of guard statement.
  • return;
  •  
  • }
  •  
  • // Flag the status as being ended.
  • dom.status
  • .data( "status", "na" )
  • .text( "Call ended" )
  • ;
  •  
  • }
  • );
  •  
  • </script>
  •  
  • </body>
  • </html>
  •  
  • </cfoutput>

Notice at the top of the page that we are creating a UUID of the current request. This unique ID will be used as the channel name for our Pusher interaction. When using Pusher, you need to subscribe to a channel within your Pusher application sandbox. In order to prevent messages from being pushed back to the wrong client, every single user will subscribe to events on their own unique channel.

When the user submits their call information, we make an AJAX request to the call page which has Twilio initiate the outbound phone call:

Call.cfm

  • <!--- Param the incoming form values. --->
  • <cfparam name="form.id" type="string" />
  • <cfparam name="form.mp3" type="numeric" />
  • <cfparam name="form.number" type="string" />
  •  
  •  
  • <!---
  • Even though we validated on the client-side, double check to make
  • sure that we are dealing with a valid 10-digit US phone number.
  • --->
  • <cfif (len( form.number) neq 10)>
  •  
  • <!---
  • The phone number is not valid. Return a 400 Bad Request
  • header so that the client can alert the user.
  • --->
  • <cfheader
  • statuscode="400"
  • statustext="Bad Request"
  • />
  •  
  • <!---
  • Abort the request since there is nothing more we can do as
  • far as processing this number is concerned.
  • --->
  • <cfabort />
  •  
  • </cfif>
  •  
  •  
  • <!---
  • Now that we are about to connect to Twilio in order to request
  • that the outgoing call be made, let the user (on the client-side)
  • know that we are dialing the number.
  • --->
  • <cfset createObject( "component", "Pusher" )
  • .init(
  • application.pusher.appID,
  • application.pusher.key,
  • application.pusher.secret
  • )
  • .pushMessage(
  • channel = form.id,
  • event = "dialing",
  • message = "true"
  • )
  • />
  •  
  •  
  • <!---
  • When we initiate the call to Twilio, we are going to need to
  • provide it with a URL for the processing logic and a URL for the
  • end-of-call logic. Let's set the root URL so that we can easily
  • define the URLs below.
  • --->
  • <cfset rootUrl = (
  • "http://" &
  • cgi.server_name &
  • getDirectoryFromPath( cgi.script_name )
  • ) />
  •  
  •  
  • <!--- Initiate the outgoing call with Twilio. --->
  • <cfhttp
  • result="outboundCall"
  • method="post"
  • url="https://api.twilio.com/2010-04-01/Accounts/#application.twilio.accountSID#/Calls"
  • username="#application.twilio.accountSID#"
  • password="#application.twilio.authToken#">
  •  
  •  
  • <!---
  • This is the Twilio phone number that will be placing the
  • outbound call to the given target user.
  •  
  • NOTE: It must start with "+" and include the country code.
  • This number happens to be a US number (+1).
  • --->
  • <cfhttpparam
  • type="formfield"
  • name="From"
  • value="+1#application.twilio.phoneNumber#"
  • />
  •  
  • <!---
  • This is the user-submitted number that we are calling
  • in order to present the MP3.
  •  
  • NOTE: We are starting the number with "+1" for the country
  • code; but, Twilio will also accept unformatted US phone
  • numbers as well.
  • --->
  • <cfhttpparam
  • type="formfield"
  • name="To"
  • value="+1#form.number#"
  • />
  •  
  • <!---
  • This is the URL that will define the TwiML (Twilio Markup)
  • that will provide the processing and routing logic of the
  • phone interaction (ie. to play the selected MP3).
  • --->
  • <cfhttpparam
  • type="formfield"
  • name="Url"
  • value="#rootUrl#play.cfm?id=#form.id#&mp3=#form.mp3#"
  • />
  •  
  • <!---
  • This is the URL that will be called when the outgoing call
  • has ended. We want to listen for this so we can notify the
  • user when the MP3 has stopped playing.
  • --->
  • <cfhttpparam
  • type="formfield"
  • name="StatusCallback"
  • value="#rootUrl#end.cfm?id=#form.id#"
  • />
  •  
  • </cfhttp>
  •  
  •  
  • <!---
  • Determine if the outbound call was successfully initiated. In
  • this case, we're just looking for any response code in the 200
  • block.
  • --->
  • <cfset success = !!reFind( "^20\d", outboundCall.statusCode ) />
  •  
  •  
  • <!---
  • Echo the status code of the outbound call. This will just give
  • us an easy way to handle the Javascript on the login page (using
  • the done/fail aspect of the jQuery deferred objects).
  •  
  • WARNING: jQuery deferred objects are sexier than they may at
  • first appear.
  • --->
  • <cfheader
  • statuscode="#listFirst( outboundCall.statusCode, ' ' )#"
  • statustext="#listRest( outboundCall.statusCode, ' ' )#"
  • />
  •  
  • <!--- Return the plain text response. --->
  • <cfcontent
  • type="text/plain"
  • variable="#toBinary( toBase64( toString( success ) ) )#"
  • />

When posting the outbound CFHTTP call request to Twilio, we are providing two URLs. The first URL is the page that provides the processing logic for the call. The second URL is a callback URL that Twilio will invoke when the call has ended. This second URL is needed in this demo only so that we can provide the user with feedback as to when the call has ended.

Once Twilio receives the call request, it makes a request to our call-processing page:

Play.cfm (Call Processing)

  • <!--- Param the incoming url values. --->
  • <cfparam name="url.id" type="string" />
  • <cfparam name="url.mp3" type="numeric" />
  •  
  •  
  • <!---
  • If Twilio has requested this file, it is because the call has
  • connected to the target user and Twilio has handed the processing
  • logic off to this ColdFusion file. As such, send a notification
  • to the client that the mp3 has started playing.
  • --->
  • <cfset createObject( "component", "Pusher" )
  • .init(
  • application.pusher.appID,
  • application.pusher.key,
  • application.pusher.secret
  • )
  • .pushMessage(
  • channel = url.id,
  • event = "playing",
  • message = "true"
  • )
  • />
  •  
  •  
  • <!--- Set up a list of file names for mp3s. --->
  • <cfset files = [
  • "./mp3/kenny_loggins.mp3",
  • "./mp3/lana.mp3",
  • "./mp3/last_words.mp3",
  • "./mp3/life_insurance.mp3"
  • ] />
  •  
  •  
  • <!---
  • Build the TwiML XML response that tells Twilio which MP3 file
  • to play over the current phone connection.
  • --->
  • <cfsavecontent variable="twiml">
  • <cfoutput>
  •  
  • <?xml version="1.0" encoding="UTF-8" ?>
  • <Response>
  •  
  • <!---
  • Provide a brief pause for the user to pickup the
  • phone and get adjusted.
  • --->
  • <Pause length="1" />
  •  
  • <!--- Tell Twilio to play the associated MP3. --->
  • <Play>#files[ url.mp3 ]#</Play>
  •  
  • </Response>
  •  
  • </cfoutput>
  • </cfsavecontent>
  •  
  •  
  • <!--- Set the Twilio response as XML. --->
  • <cfcontent
  • type="text/xml"
  • variable="#toBinary( toBase64( trim( twiml ) ) )#"
  • />

As you can see, this page alerts the user that the MP3 is now being played and then returns a relative path to the selected MP3 file (NOTE: You can also use an absolute URL).

According to the Twilio documentation, Twilio will do its best to locally cache your MP3 according to the expiration headers. And, from what I gather, it will also encode the MP3 into a format that makes sense for telephony. As such, there may or may not be some latency between when the call is connected and when the MP3 starts playing.

Once the MP3 has been played and Twilio hangs up the call, it invokes our "end of call" callback URL.

End.cfm (End-of-Call Callback)

  • <!--- Param the incoming url values. --->
  • <cfparam name="url.id" type="string" />
  •  
  • <!---
  • If Twilio has requested this file, it is because the MP3 has
  • finished playing and Twilio has hung up the call. As such,
  • let's let the user know that the call has ended.
  • --->
  • <cfset createObject( "component", "Pusher" )
  • .init(
  • application.pusher.appID,
  • application.pusher.key,
  • application.pusher.secret
  • )
  • .pushMessage(
  • channel = url.id,
  • event = "ended",
  • message = "true"
  • )
  • />

At this point, our Twilio interaction is over and the only requirement our end.cfm has left to do is alert the user that the call has ended.

I swear, Twilio is just a joy to work with! They really make programmatic telephone interactions as easy as any other piece of programming. This demo might seem complex, but most of that is the bidirectional communication that I am trying to create. If all you cared about was playing the MP3, this demo would have been half the size.




Reader Comments

Can Twilio be used to send files to other people through the phone? I know there are many smartphones that have the capacity of storing files such as .zip and .pdf files, which you upload from your computer to store on the phone.

Reply to this Comment

@Lola,

Hmm, very interesting question. Twilio can definitely send SMS Text messages to phones. I wonder if they can send vcards as part of that? I have never looked into that. I think attachments like ZIP files are out of the question. Probably no attachments at all; but, it's worth checking out.

Reply to this Comment

@Lola,

Twilio can't send files as attachments. MMS is coming soon which will be able to attach media files but I doubt PDFs.

But, you can create a system that posts the file to a server and then text messages a short link to the person. They click that link and it's downloaded to their phone.

Kind of a neat idea actually - like those old faxback systems they had in the 90's

Reply to this Comment

@Aaron,

I can't wait for MMS to come out!!

@All -- Aaron is the guy who turned me onto Twilio - woot!

Reply to this Comment

Hi Ben, great work!
One question. Why coldfusion server? only works in this server or could be work in php with the twilio client?

Regards

Reply to this Comment

Post A Comment

You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.