Skip to main content
Ben Nadel at InVision In Real Life (IRL) 2019 (Phoenix, AZ) with: Joe Gores
Ben Nadel at InVision In Real Life (IRL) 2019 (Phoenix, AZ) with: Joe Gores

Playing An MP3 Over The Phone With ColdFusion And Twilio

By
Published in , Comments (6)

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.

Want to use code from this post? Check out the license.

Reader Comments

148 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.

15,811 Comments

@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.

29 Comments

@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

1 Comments

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

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel