app_dial re-implemented using phpari

app_dial is one of the most (if not THE MOST) useful Asterisk applications. ARI extends the ability to control and interact with existing channels and bridges, in ways that were beyond horrific in previous versions. However, due to the stateful and synchronous nature of app_dial, re-implementing it with ARI is somewhat of a challenge. Further than that, as PHP is a single threaded environment, supporting multiple calls using a single PHPARI application isn’t a straight forward task. This question had been bugging me for a while now, and following a few hours of realizing what dial should actually do – I managed to cook up a simple wireframe for implementing app_dial using ARI.

Please refresh the page to continue...

 How does it work?

Let’s understand one thing first, the original asterisk application (app_dial) was totally synchronous and as it was working inside a thread of its own, it was a fairly self contained environment. Sure, it was able to access variables and global variables, however, it was still confined to its own work space. A PHPARI script is a self contained environment, however, if you don’t pay attention to what you’re doing, it’s very easy to have 2 (or more) calls influence each other – specifically if you are using global variables.

The simplest way to solve this is using a form of in-memory data store – or in our case, an array that is used as a hash map. Our array is called:

.
private $channelStorage = array();
.
Our storage hash map

The trick is to understand the relations between the channels and how they interact.

The application can be activated in two manners – with arguments or without arguments. It currently accepts two arguments – an endpoint and a timeout (seems familiar, no?). When the application is triggered from the dialplan, it will call the StasisStart event, executing the following code:

           $this->stasisEvents->on('StasisStart', function ($event) {

                $this->stasisLogger->notice("Event received: StasisStart");

                $args = $event->args;
                if (isset($args[0])) {
                    $this->stasisChannelID = $event->channel->id;
                    $this->stasisLogger->notice("Creating new instance in channelStorage");
                    $this->stasisLogger->notice("channelStorage: " . print_r($this->channelStorage, TRUE));
                    $this->stasisLogger->notice("About to originate a call to another party, and bridge to us");
                    $response = $this->phpariObject->channels()->channel_originate(
                        $args[0],
                        NULL,
                        array(
                            "app"     => "askme-dial",
                            "appArgs" => '',
                            "timeout" => $args[1]
                        )
                    );

                    /* Creating a Bridge resource */
                    $this->phpariObject->bridges()->create('mixing', 'bridge_' . $response['id']);

                    /* Populate the Storage */
                    $this->channelStorage[$response['id']]['epoch'] = time();
                    $this->channelStorage[$response['id']]['bridge'] = "bridge_" . $response['id'];
                    $this->channelStorage[$response['id']]['A'] = $event->channel->id;
                    $this->channelStorage[$response['id']]['B'] = $response['id'];
                    $this->channelStorage[$event->channel->id]['bridge'] = "bridge_" . $response['id'];
                    $this->channelStorage[$event->channel->id]['B'] = $event->channel->id;
                    $this->channelStorage[$event->channel->id]['A'] = $response['id'];

                    /* Join the bridge */
                    $this->phpariObject->channels()->channel_ringing_start($event->channel->id);

                } else {
                    $this->stasisLogger->notice("First channel is joinging the bridge: " . $event->channel->id . " -> bridge_" . $response['id']);
                    $this->phpariObject->channels()->channel_ringing_stop($this->channelStorage[$event->channel->id]['A']);
                    $this->phpariObject->channels()->channel_answer($this->channelStorage[$event->channel->id]['A']);
                    $this->phpariObject->bridges()->addchannel($this->channelStorage[$event->channel->id]['bridge'], $this->channelStorage[$event->channel->id]['A']);
                    $this->stasisLogger->notice("Second channel is joining the bridge: " . $event->channel->id . " -> bridge_" . $event->channel->id);
                    $this->phpariObject->channels()->channel_answer($event->channel->id);
                    $this->phpariObject->bridges()->addchannel($this->channelStorage[$event->channel->id]['bridge'], $event->channel->id);
                }
            });
StasisStart Event Handling

When the application is activated from the dialplan, it will automatically generate a ringing tone to the calling party and originate a call to the second party.

                    $this->stasisChannelID = $event->channel->id;
                    $this->stasisLogger->notice("Creating new instance in channelStorage");
                    $this->stasisLogger->notice("channelStorage: " . print_r($this->channelStorage, TRUE));
                    $this->stasisLogger->notice("About to originate a call to another party, and bridge to us");
                    $response = $this->phpariObject->channels()->channel_originate(
                        $args[0],
                        NULL,
                        array(
                            "app"     => "askme-dial",
                            "appArgs" => '',
                            "timeout" => $args[1]
                        )
                    );

                    /* Creating a Bridge resource */
                    $this->phpariObject->bridges()->create('mixing', 'bridge_' . $response['id']);

                    /* Populate the Storage */
                    $this->channelStorage[$response['id']]['epoch'] = time();
                    $this->channelStorage[$response['id']]['bridge'] = "bridge_" . $response['id'];
                    $this->channelStorage[$response['id']]['A'] = $event->channel->id;
                    $this->channelStorage[$response['id']]['B'] = $response['id'];
                    $this->channelStorage[$event->channel->id]['bridge'] = "bridge_" . $response['id'];
                    $this->channelStorage[$event->channel->id]['B'] = $event->channel->id;
                    $this->channelStorage[$event->channel->id]['A'] = $response['id'];

                    /* Join the bridge */
                    $this->phpariObject->channels()->channel_ringing_start($event->channel->id);
Indicate ringing to the first channel

Pay attention to the activation of the application from within originate, there are no arguments now. For this application, activation without arguments indicate a connection from the second channel, and should be handled by this code:

                    $this->stasisLogger->notice("First channel is joinging the bridge: " . $event->channel->id . " -> bridge_" . $response['id']);
                    $this->phpariObject->channels()->channel_ringing_stop($this->channelStorage[$event->channel->id]['A']);
                    $this->phpariObject->channels()->channel_answer($this->channelStorage[$event->channel->id]['A']);
                    $this->phpariObject->bridges()->addchannel($this->channelStorage[$event->channel->id]['bridge'], $this->channelStorage[$event->channel->id]['A']);
                    $this->stasisLogger->notice("Second channel is joining the bridge: " . $event->channel->id . " -> bridge_" . $event->channel->id);
                    $this->phpariObject->channels()->channel_answer($event->channel->id);
                    $this->phpariObject->bridges()->addchannel($this->channelStorage[$event->channel->id]['bridge'], $event->channel->id);
Handling of second StatisStart event

Pay attention to how StasisEnd is handled, to verify that our memory, channels and bridges are cleared accordingly:

                $this->stasisLogger->notice("Event received: StasisEnd");
                if (isset($this->channelStorage[$event->channel->id])) {
                    $this->stasisLogger->notice("channelStorage: " . print_r($this->channelStorage, TRUE));

                    $this->stasisLogger->notice("Terminating: " . $event->channel->id);
                    $this->phpariObject->channels()->channel_delete($event->channel->id);

                    $this->stasisLogger->notice("Terminating: " . $this->channelStorage[$event->channel->id]['A']);
                    $this->phpariObject->channels()->channel_delete($this->channelStorage[$event->channel->id]['A']);

                    $this->stasisLogger->notice("Terminating: " . $this->channelStorage[$event->channel->id]['bridge']);
                    $this->phpariObject->bridges()->terminate($this->channelStorage[$event->channel->id]['bridge']);

                    unset($this->channelStorage[$this->channelStorage[$event->channel->id]['A']]);
                    unset($this->channelStorage[$event->channel->id]);
                    $this->stasisLogger->notice("channelStorage: " . print_r($this->channelStorage, TRUE));
                }
Bridge teardown and termination

And there we have it – app_dial re-implemented using PHPARI.

3 comments for “app_dial re-implemented using phpari

  1. oscar
    June 12, 2015 at 7:42 am

    hi, exist a way in ARI for detect if a channel (calling) is a fax?
    that is, exist a StasisEvent?

    I would to call a number, when I receive stasisStart event if it’s a voice then I create a bridge, create a new channel (I call a my agent) and add the 2 channel in the bridge, so the 2 channels talks. If it’s a fax then I would store this info in my DB and hungup the channel, without create bridge and new channel.

    How could I do?

  2. Jayrue
    August 31, 2016 at 10:02 pm

    Hi wont this create a FAS call? you make the inbound leg ring as soon as the call comes in.. e.g If the destination is a mobile and out of service.. we would hear ring ring then a message saying the phone is out of service.. .this is bad telecom.!

    • admin
      February 23, 2017 at 7:29 am

      Affectively, you are correct – remember, this is just a minor example of how to use the library and create something. If you would like to create something a bit more robust, what I would do would be the following:

      1. Perform the actual dialing using the Dialplan Dial command
      2. Once the call is answered by the remote party, transfer both calls into a stasis application for additional processing.

      Remember, the examples here are just examples, not full blown implementations.

Leave a Reply

Your email address will not be published. Required fields are marked *

Please Do the Math