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.

Overview

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.

<?php
// 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) {
    return($event->value);
}
function FuncHangupWithAnnouncement($event) {
    say("You are now being disconnected from the conference");
    hangup();
}

// 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
conference($confId,array(
    "joinPrompt" => "http://54.86.144.41/conf.php?conference=" . $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.


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

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


<?php
call("9990679485@sip.tropo.com",array(
    "headers"=>array("x-minleft" => $minleft),
    ));
conference($conferenceid);
hangup();
?>

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.

<?php
// launch URL
// https://api.tropo.com/1.0/sessions?action=create&token=6853444b48634b58566c6e6b4643715a6f5657646a78596547416b6f6777474e596263626962614f4b71514e&minutes_left=five

// 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
ini_set('session.cache_limiter','');

// 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'])) {
 ignore_user_abort(true);
 set_time_limit(0);

 ob_start();

 $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');
 readfile($file);

 ob_end_flush();
 ob_flush();
 flush();
}

// 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
 session_id($conf_id);
 session_start();

 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']);
 session_write_close();

 // 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) {
 error_log($conf_id);
 file_get_contents("https://api.tropo.com/1.0/sessions?action=create&token=7a4252747a567a624c686a4d65516f58706b715a654b746b597a59714153624474454f4d6370596f53416877&minleft=5&conferenceid=" . $conf_id);
 });
 Ev::run();
 // we need another one that fires in 19min
 $w1 = new EvTimer(20 /*19*60*/, 0, function () use ($conf_id) {
 error_log($conf_id);
 file_get_contents("https://api.tropo.com/1.0/sessions?action=create&token=7a4252747a567a624c686a4d65516f58706b715a654b746b597a59714153624474454f4d6370596f53416877&minleft=1&conferenceid=" . $conf_id);

 });
 Ev::run();
 // 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
 session_id($conf_id);
 session_start();
 error_log("sessions" . print_r($_SESSION));
 foreach ( $_SESSION['tropoid'] as $sessionId) {
 error_log("killing session" . $sessionId );
 file_get_contents("https://api.tropo.com/1.0/sessions/" . $sessionId . "/signals?action=signal&value=exit", true);
 }
 session_unset();
 session_destroy();
 });
 Ev::run();
 } 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'];
 }
 session_write_close();
 }
}
?>

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.

L90-L96

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.

 

 

 

 

 

 

 

 

 

Note:

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 ev.so 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

Picture1

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.

Picture2

Spring Cleaning Voice Engines

Posted on March 13, 2017 by Ralf Schiffert

Spring is upon us and we are excited about doing some house cleaning. As mentioned previously we will be getting rid of our aging and inferior Text-To-Speech engine and moving to a more natural-sounding offer for US English/MX Spanish voices.

We are giving you a little more time to make changes and the final cutover date is now March 21, 2017.

If you don’t make any changes to your app you will be automatically moved to the new voice a.k.a. Ava. Absolutely no apps will break. In fact, they will all sound better.

If you don’t like our new default voice for US English you can try some of the alternatives listed in the previous blog post by using the voice attribute of say.

Power Conferences in PHP

Posted on March 8, 2017 by Ralf Schiffert

You have heard it all: Avoid meetings at all costs!” “If you must have meetings do them all on the same weekday and keep your other days free!”, “Always send an agenda and your presentation before the meeting!” “Keep your meetings to 20min or less!”

canstockphoto5516389

I won’t go into a reality check of these recommendations, but here is what we are going build today, a conference with built-in time management so you don’t have to. Yep that’s right, it’s like speed dating. If your users know how much time they have upfront and will be reminded of it via regular announcements they are going to get things done, quickly.

Our power conference will send initial gentle reminders to all participants before ultimately and forcefully ending the conference when the time is up.

If you want to try it out you can call +1 470 238 9092.

This example is the Skippy version of a conference though. Start to end, 45s, done.

We are going to break this effort into 2 parts. In the 1st part, I am going to start with the basic solution illustrating how to play an announcement into a conference and in the 2nd post I will build upon it with some business logic in the backend to maintain the timers and ultimately end the conference for anyone lingering around.

You are in for a bit of a ride. Not only will we learn a lot about Tropo’s advanced features like automatic speech recognition, instant call setup, call-2-call data transfers and application signaling, but will also have to delve a little into the deep end of PHP particularly session management and thread safety. But no worries, it took me a little to figure things out which allows me to safely guide you around the rough edges.

Part 1 Playing audio to a conference

So, let’s get started.

First we need to solve how to play audio into a running conference. There are essentially 3 pieces to this puzzle,

  • An app that that puts people into a conference room
  • An app that plays our announcement
  • And app that calls the app that plays the prompt

Let’s go over these 3. I made them as simple as possible and hardcoded some stuff. No worries, we will make this more flexible as we go along.

1_ app1_basic.php : A Tropo app that places callers into a conference. For simplicity we use an inbound app, but of course outbound dialing to participants would be just as easy with Tropo.

The basic scripting API code looks like this:

<?php
say("Thank you for joining our conference call");
conference("1233");
?>

When someone dials their conference dial-in, the phone number you assigned to this scripting app in either the account portal or via REST API, they will be dropped into the conference 1233 which we hardcoded for simplicity here. Tropo conferences will accommodate up to 100 people in our standard configuration, which would be enough to get a lot of people annoyed about time-wasting meetings. But hold your horses, in the advanced section we will put an end to this.

2_ app2_basic.php : Next we need an app that plays an announcement. Let’s use this

<?php
say("You have five minutes left in the conference");
hangup();
?>

This is pretty self-explanatory. The say() is Tropo’s magical prompter that renders text to audible words or when provided with an audio file URL, will play the same greeting done by your voice talent, albeit likely for a much higher price.

I’ll let you look up all the options you can use for the say but also wanted to remind you that we have two new, great sounding TTS US voices, Zoe and Evelyn. We think they sound pretty darn good and you should give them a spin.

3_ app3_basic.php Lastly we need some magic sauce to connect the conference bridge to the audio playing application. In short we’ll launch an app, which then dials the application in 2_ and joins it to the conference in 1_. The code would look something like this:

<?php
call("sip:9991988936@sip.tropo.com");
conference("1233");
hangup();
?>

When we do this we use Tropo’s instant call setup feature. The launch URL to start this app can be found in your account portal or via the REST API. Mine looks like this

https://api.tropo.com/1.0/sessions?action=create&token=6853444b48634b58566c6e6b4643715a 6f5657646a78596547416b6f6777474e59626362696261decafbad

Executing this launch URL will kickoff app3_basic.php which starts a VoIP call to 9991988936@sip.tropo.com (we’ll get to that in a second) and places that app into the conference, resulting in the announcement being played to the conference bridge. The hangup() after the conference is not strictly necessary as we do a hangup already on the callee (app2) side after we are done playing the announcement.

So really this call only lasts for as long as it takes to play the announcement to the conference bridge.

The end result here will be that people in the conference will hear the announcement (“You have five minutes left….”) whenever the launch URL is executed. You can post the URL into your browser to try it out or use cURL if you want to play with it. Later we’ll call that URL programmatically from our business backend.

Let’s quickly chew on the SIP Uri sip:9991988936@sip.tropo.com. Tropo automatically assigns every app a SIP URI, which enables you to call your new app from any SIP capable equipment like a CISCO phone or a SIP Softphone like Blink. Calling a SIP number is also the fastest and easiest way to call from one Tropo app to another, a feature we will be making good use here.

Tropo assigned to my announcement app the following SIP URI. Recognize that number?

sip-php-conf

We could also assign a real PSTN number to that app and while Tropo’s phone numbers are available in more than 50 countries and generally inexpensive, there really would be little use. Call setup times in PSTN networks are much slower than VoIP. Hence the term I used earlier: Tropo’s instant-call-setup. Come to think of it, it should really be Instant-Call-Setup™ ©Tropo.

We are done with the basic solution. That was a little whirlwind. To try it out create 3 applications in the Tropo.com portal with the scripting code I showed you and then use your very own launch URL to play the announcement to your conference.

Before we stop, let’s introduce a little flexibility to the app. This will help us later. We are going to get rid of the hardcoded conference ID and instead we will ask the caller for the meeting ID when they dial in. That’s just how Webex and other commercial systems behave. Since Tropo provides advanced speech recognition in over 60 languages we leave it to the caller if they want to speak their meeting ID or rather press touchtones.

Back in 2011, we published a tutorial featuring the conference app code that we used as our company conference line at the time. Let’s use the code from that tutorial here.

An enhanced app1_enhanced.php looks like this:

<?php
answer();
$voice = 'Eva';
while ($currentCall->isActive()) {
  $response = ask('Enter your conference eye d. Press the pound key when finished.', array(
      'choices' => '[4 DIGITS]',
      'terminator' => '#',
      'mode' => 'keypad',
      'timeout' => 8
    ));
  switch($response->name) {
    case 'choice':
      say('<speak>Conference i d <say-as interpret-as="vxml:digits">' . $response->value . '</say-as> accepted. You will now be placed into the conference. Press pound to exit without disconnecting. Please announce yourself.</speak>');
      conference($response->value, array('terminator' => '#'));
      // Pause a moment before asking for another conference.
      sleep(1);
      break;
    case 'badChoice':
      say('Sorry, that is not a valid conference eye dee.', array('voice' => $voice));
    break;
      case 'silenceTimeout':
      case 'timeout':
      say('Sorry, I didn\'t hear anything.', array('voice' => $voice));
    break;
  }
}
?>

We are using Tropo’s ask verb to solicit input from the caller. Here they get prompted for a 4 digit number. The input result can be found in result->value and will be used to inform the caller about the conference they are joining, as well as kick them right into it. If you want to talk to your friends, of course they have to specify the same meeting ID as you did.

You do see me playing with the Text To Speech engine here when I used eye d, i d, and previously eye id. It didn’t make any difference for the default Tropo voice but some TTS engines and voices are reacting to these chances with minor changes in pronunciations. Of course Tropo is pretty unique in that you can fine tune pronunciations with the standards based SSML markup.

You can always read up the linked docs to get more background info or better signup for a free Tropo account and give it a spin.

This is it for part 1. Next week we will introduce a couple more Tropo features like call-2-call data transfers and enhanced signaling to Tropo scripts.