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.
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:
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.
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.
Want to use code from this post? Check out the license.
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.
@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.
@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
@Aaron,
I can't wait for MMS to come out!!
@All -- Aaron is the guy who turned me onto Twilio - woot!
The RIAA will be contacting you shortly for Kenny Loggins' royalties. :-)
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