Playing An MP3 Over The Phone With ColdFusion And Twilio

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.

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

User submits a call.

Browser sends request to the ColdFusion server.

ColdFusion server pushes "dialing" event back to browser.

ColdFusion server sends outbound call request to Twilio.

Twilio initiates call.

Twilio asks ColdFusion server for call logic.

ColdFusion server pushes "playing" event back to browser.

ColdFusion server returns MP3 URL to Twilio.

Twilio tells ColdFusion server that the call has ended.

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.

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:

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.

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.

I am the co-founder and lead engineer at InVision App, Inc — the world's leading prototyping,
collaboration & workflow platform. I also rock out in JavaScript and ColdFusion 24x7 and I dream about
promise resolving asynchronously.