Tropo is part of CiscoLearn More

Power Conferences in PHP Part 2

Posted on March 17, 2017 by Ralf Schiffert

Thanks for coming back and Happy St. Patrick’s Day. Last week, we figured out a way to play audio to all attendees in a conference. If you want to stick around, I am going to build on that today to finally get to the self-terminating conferences, that we are pursuing in order to support workplace efficiency. We will make minor changes to our previous 3 Tropo scripting applications and augment them with some server side PHP script as the business backend, to start a timer when the conference starts and then send reminders 5min and 1min before the 20min limit. After 20min we are going to kill the conference and let everyone do their real jobs. Your boss will love you for all the efficiency you brought to your company.


We will stick with the basics we learned earlier, the ability to play audio to a conference. Now, of course, we want to do this in timed intervals. We also want to terminate the conference at a set time limit. I was thinking that Javascript does Callback timers very nicely and found the equivalent in PHP, evTimer. Unfortunately it’s a PECL extension so you have to install it (Installation instructions can be found below or on the web.). We will also modify our code to announce how many minutes are left in a variable instead of my hardcoded 5min from the earlier example, by transmitting data from one call to another. Finally you will see how to kill a conference, which essentially means sending each call leg a signal. There are other details, which I will explain as we go along.

As a reminder you can try out the final experience under +1 470 238 9092.

Self-terminating conferences

We do stick to our 3 part voice frontend ( Tropo scripting ) and add a server side PHP page to it, to take control of when to invoke the apps.

1*_ app1.php :  First we want to modify our conference bridge to inform our backend when the conference starts, which will set off the max-time timer and the announcement timers. You can read about how to make http requests from Tropo scripting here, but I decided to shamelessly use a hack for this:

The Tropo conference verb allows you to play a prompt to callers when they join, either a beep or a file. I decided for the file, which will do an announcement about the conference max time to each joining caller. As I am loading this file from the conf.php backend, which we will build later, it will not only returning the file but also do a couple additional things, among it start the timers. I am also introducing a way to signal the conference to terminate via an external REST API request. Users can still use “#” to terminate the call, or just hang up if they are done early.

// it seems in the callback currentCall is not accessible
// we copy it over here therefore
$sess = $currentCall->sessionId;
function FuncOnBadChoice($event) {
    say("Sorry I didn't get you. Just try typing your four digit meeting i d");
function FuncOnChoice($event) {
function FuncHangupWithAnnouncement($event) {
    say("You are now being disconnected from the conference");

// we ask Tropo's amazing speech recognition feature to
// ask for the conference ID
$result = ask("Please say or type your four digit meeting eye d.", array(
    "choices" => "[4 DIGITS]",
    "timeout" => 7.0,
    "attempts" => 3,
    "onChoice" => "FuncOnChoice",
    "onBadChoice" => "FuncOnBadChoice"

$confId = $result->value;
log("confId::" + $confId);

// the ngrok URL points to the php backend
    "joinPrompt" => "" . $confId . "&state=start&audio=limit.wav&session=" . sess,
    "terminator" => "#",
    "allowSignals" => "exit",
    "onSignal" => "FuncHangupWithAnnouncement"

Our code should look mostly familiar to you from last week. After declaring some functions used in callbacks we first use Tropo’s ask to ask the caller for the meeting ID.

Things get a little more interesting in line 31. The joinPrompt in the conference expects an audio file in response to the http request, which it will play to each caller joining. Limit.wav is my audio file, which announces the maximum time for the conference. You will see that the backend does way more than just return the audio file. We already talked about it setting timers, but it also tracks all ongoing conferences and associated sessions (=Calls), which is why I am passing the conference and sessions ID’s along in the URL as parameters.  Communicating the apps state as start-state is not strictly necessary, but may come in handy in the future when we extend the app.

The onSignal handler in line 34 is new too.  You can read up about the signaling here or just wait patiently for my explanation. In line 33 we register a signal that, when issued to the Tropo conference verb, will execute the signalHandler in line 34, which is our previously declared function FuncHangupWithAnnouncement. To do this signaling our PHP backend will do a POST request to a REST session instance resource where the conference is running, with a named signal “exit”. The handler in the app1.php will then stop executing the conference verb and jump into the function with the say(), to play a final announcement about the conference’s end, and then will hang up on the caller. We have to do this signaling for every callleg individually, which is why I was passing the sessionID to the backend earlier.

Note: In my version, I am using Amazon lightsail, which was a breeze J to set up. Alternatively, you can host a local LAMP stack and expose it with ngrok. ngrok is a way to expose my XAMPP server behind a firewall. It’s free and serves my purposes. Just Google it or use one of the many competing offers.


2*_ app2.php :  Our second app2 has changed too, but by not much. You still can see how it announces how many minutes are left, but now the app gets passed this info dynamically by the backend.

$minleft = " a couple ";
if ( null!==$currentCall->getHeader("x-minleft") ) {
    $minleft = $currentCall->getHeader("x-minleft");
say("You have " . $minleft . " minutes left in the conference");

Remember that this app is not launched, but called.  So we cannot pass launch parameters into the app like we did previously. Instead for this to work, I am relying on some other awesome Tropo feature; the ability to pass and retrieve SIP header information. Our modified app3.php will pass a proprietary SIP header x-minleft and here we extract it. Easy.


3*_ app3.php

    "headers"=>array("x-minleft" => $minleft),

Our app3.js hasn’t changed much either. Here we do set the SIP header with the info about how many minutes are left which passes it to app2.php with the SIP phone call. If you haven’t studied launch tokens yet, what may not be completely obvious is the $minleft is a variable that was passed via the launch token, which is controlled by the backend app. Essentially if I change the timers in the backend website I can change the announcements to reflect it, all from a single place. The same is true for the $conferenceid, which we are also passing from the backend. That way we can control to which of the running conferences the audio is played. 


4*_ conf.php 

This is the brain of the app. Our backend that maintains the timers and signals the app3.php ultimately resulting in the announcements to the conference.

// launch URL

// this is a hack to achieve a gloabl view of all Tropo sessions
$conf_id = "8459263464";

if ( isset($_REQUEST['conference'])) {
 $conf_id = $_REQUEST['conference'];

// the following is used to prevent session cookies from being sent
// our app really doesn't need it - we use it as a convenience method servers side
// we also would get a warning not doing it since we are returng a response with the audio file already and sessions cookies have to go out first
ini_set('session.use_cookies', 0); # disable session cookies

// let's first return the audio file
// we wrap this into some ob_ routines so we send the response and will continue processing with the subsequent code
if ( isset($_REQUEST['audio']) && file_exists($_REQUEST['audio'])) {


 $file = $_REQUEST['audio']; 

 header('Content-Description: File Transfer');
 header('Content-Type: audio/x-wav');
 header('Expires: 0');
 header('Cache-Control: must-revalidate');
 header('Pragma: public');
 header('Content-Length: ' . filesize($file));
 header('Connection: close');


// if the ev timer extension isn't loaded we pretty much cannot do anything here
if ( extension_loaded('ev') && isset($_REQUEST['state']) && "start" == $_REQUEST['state'] ) {
 // let's put the Tropo SessionID into a global shared storage here
 ini_set('session.use_cookies', 0); # disable session cookies

 if (empty($_SESSION['tropoid'])) {
 // if I am the first one who joins the conference, I will be the one that is responsible
 // for the timers and cutting down the conference
 // start the timer here - we use 60s as max conference time

 // we store the session ID's since we need to signal all of them to stop
 $_SESSION['tropoid'] = array($_REQUEST['session']);

 // let's register a timer that fired in 15min and sends the 5min reminder to the conference
 $w1 = new EvTimer(10/*15*60*/, 0, function () use ($conf_id) {
 file_get_contents("" . $conf_id);
 // we need another one that fires in 19min
 $w1 = new EvTimer(20 /*19*60*/, 0, function () use ($conf_id) {
 file_get_contents("" . $conf_id);

 // and one that kills the session - if a session is gone already there is no harm done
 // it usually just returns a NOT FOUND which we don't care about
 $w1 = new EvTimer(30 /*19*60*/, 0, function () use ($conf_id) {
 error_log("killing all sessions");
 ini_set('session.use_cookies', 0); # disable session cookies
 error_log("sessions" . print_r($_SESSION));
 foreach ( $_SESSION['tropoid'] as $sessionId) {
 error_log("killing session" . $sessionId );
 file_get_contents("" . $sessionId . "/signals?action=signal&value=exit", true);
 } else {
 // this is not the first joiner - we just extract the session ID and return the audio file
 if ( isset($_REQUEST['session'])) {
 $_SESSION['tropoid'][] = $_REQUEST['session'];

OK, there is quite a bit going on here, but I will make this fairly quick. Earlier I said: to terminate a conference we will have to signal the signal handler for each session (here a call). I am shamelessly abusing PHPs session as my storage to store all the Tropo session IDs for a conference. The conference ID will set the sessionID for the PHP session and store all the associated sessions. Just reading through it using PHP sessions as a container for Tropo sessions may have been a didactical error on my part, but I think you get the point. Now you know why I passed conference ID and session ID in the joinPrompt URL earlier.  It should also explain why the backend is able to work with several simultaneous different conference ID’s, i.e. several ongoing conferences at the same time, we just store each in a different PHP session. Essentially my conference ID is the bucket and in this bucket, I am storing all the associated Tropo sessions. This backend will do the right things all the time!

Side Note: Once I set my head on abusing sessions (instead of file or DB storage) there were a couple of things to work through.  PHP is pretty awesome about session protection. I had no idea. As long as a particular session is open only one client can write to it. So my first caller, which sets the timers, did block all other client connections. Instead now as you can see throughout the code I am opening and closing the sessions rapidly and several times which allows several clients (=Tropo sessions loading the joinPrompt) access to this server side storage.

Lines 8-10: Here we extract the conferenceID that we get passed by the joinPrompt URL and store it away for later use.

Lines16-17: I am disabling sending session cookies back to Tropo. There is really not need to keep sessions between requests from the same Tropo client.  I am keeping the PHP session server side only since it’s just my way of storing and sharing data among conference participants. Before I disabled the cookies and cache limiter, there had been some warnings when I opened sessions later in the code. PHP sessions are meant to be opened once at the beginning of the file, which we already said I am totally not doing in this code. Yep, call me stubborn.  The reason for the warnings around cookies and cache-limiter ultimately is rooted in the following section Lines 22-42 though, which sends the wav file as a response to Tropo. After that, no session info must be sent at all. As I said session cookies must be sent WITH the http response so calling session_start after the audio file was sent, would result in annoying warnings in the php_error file.

Lines 22-42

Not much magic here. I am loading the audio file requested in the joinPrompt URL via the audio parameter and send it back to Tropo immediately, while I continue to process the PHP page after the file was sent.  You can read up about the ob_ functions. They just allow me to do an asynchronous response.

Lines 46-87

Finally, we are getting to the meat. The first client connecting to the backend, i.e. typically the first person to join the conference will kick off the timers. The first two EvTimers will invoke the launch URL when they expire, which in turn will kick off our app3 to dial app2 and play the prompt to the conference. I told you earlier I have made it so that the remaining time announcement can be changed within the backend, which is why you see the minleft parameter. I am also sending the conferenceid along so the announcement plays to the right conference. There are two timers to play the announcements. Typically these would be 5min and 1min before the end of the conference. The final timer does iterate over all sessions that can be found in the PHP sessions store (i.e. all calls that belong to the conference) and signals them an exit, which effectively terminates all calls and finally the conference.

I also delete my PHP session storage so I can reuse the same conference ID in the future.


If the caller is not the first one that joined, I won’t set any timers. I am just returning the wav file and add the sessionID for this caller to the session store (conference)


That’s it. Works like a charm. If you have a bitter aftertaste about me abusing PHP sessions in an irresponsible way, I feel the same.  If you want to modify the code to use files, DB’s or memcache instead, I think this would be a good first step to dig a little deeper. Or maybe better, if you’d modify the code so only the second person that joins will start the timers. After all a conference with only one participant can handle be considered a conference.

By building this example, I started to not only learn a lot more about Tropo conferences but some of the advanced features like app to app calling, signaling, token launches, and SIP header manipulation. I am adding all these skills to my toolbox, as I know they will come in handy for more complex future apps which I hope I can share here with you.


Until next time.











Installing the EvTimer extension for PHP

  1. First, you need to install the PEAR package manager. You can find instructions here. I am using XAMPP so it’s already in my …..xamppfiles/bin directory.
  2. Next, we need to install the EvTimer extension. I found the package here but if you follow my instructions and happen to use XAMPP we are going to have it easy. To configure your system just follow the instructions here. Forgo the mrccrypt install but follow the two problem-solving suggestion to configure your php path and download and install autoconf. After you installed autoconf you can do a sudo pecl install ev
  3. Following the on screen instructions we add our to the php.ini ( mine is under ./etc/php.ini
  4. I did restart XAMPP and then we can check in the XAMPP website if the extension is indeed loaded or you just do a php –m. Looks good. Note: I had an issue in how the permissions for the module were set (no execution) which caused connections resets. I saw it immediately thanks to my trusted zsh and it’s awesome coloring of files.
  5. This one was tough. Apple introduced some issues in their El Capitan OS. The symptom is that you will see an exception in your error_log file like this one


It seems a lot of libraries broke. Despite hours of trying I could not find a different way than the suggested upgrade to Apple’s Sierra OS.


Leave a Reply