Telnyx Media Forking Demo

60 minutes build time || Github Repo

Telnyx Media Forking demo built on Call Control and node.js.

In this tutorial, you’ll learn how to:

  1. Set up your development environment to use Telnyx Call Control using Node.
  2. Build a simple Telnyx Call Control IVR that makes use of Media Forking using Node.
  3. Receive forked UDP/RTP packets in a remote linux machine.


Prerequisites

Before you get started, you’ll need to set up a Mission Control Portal account, buy a number and connect that number to a Connection. You can learn how to do that in the [Quickstart guide] .

You’ll also need to have node installed to continue. You can check this by running the following:

Copy
Copied
$ node -v

Note: After pasting the above content, Kindly check and remove any new line added

If Node isn’t installed, follow the official installation instructions for your operating system to install it.

You’ll need to have the following Node dependencies installed for the Call Control API:

Copy
Copied
require('express');
require('superagent');

Note: After pasting the above content, Kindly check and remove any new line added

Finally, you'll need to have tcpdump installed on the remote Linux machine where you will receive the forked media. You can check this by running the following:

Copy
Copied
$ tcpdump --v

Note: After pasting the above content, Kindly check and remove any new line added

If tcpdump isn’t installed, follow the official installation instructions for your operating system to install it.

Telnyx Call Control Basics

For the Call Control application, we'll use a set of basic functions to perform Telnyx Call Control Commands. This tutorial will be using the following subset of Telnyx Call Control Commands:

  • [Call Control Forking Start]
  • [Call Control Transfer]
  • [Call Control Answer]
  • [Call Control Gather Using Speak]
  • [Call Control Hangup]

You can get the full set of available Telnyx Call Control Commands [here] .

For each Telnyx Call Control Command we will be creating a function that will execute an HTTP POST request back to Telnyx. To execute this API we are using superagent, so make sure you have it installed. If not you can install it with the following command:

Copy
Copied
$ npm install superagent --save

Note: After pasting the above content, Kindly check and remove any new line added

After that, you’ll be able to use superagent as part of your app code as follows:

Copy
Copied
var superagent = require('superagent');

Note: After pasting the above content, Kindly check and remove any new line added

To make use of the Telnyx Call Control Command API you’ll need to generate a Telnyx API Key and Secret.

To do this, go to the Mission Control Portal and under the Auth tab, select Auth V1.

Once you have them, you can include it as a ‘const’ variable in your code:

Copy
Copied
const g_telnyx_key = 'dfb642e0-558d-47b1-b9da-7b84cbhej8a';
const g_telnyx_secret = '6ow9hnixSPirLvgUgddnw-db';

Note: After pasting the above content, Kindly check and remove any new line added

Once all dependencies are set, we can create a function for each Telnyx Call Control Command. All Commands will follow the same syntax:

Copy
Copied
function call_control_COMMAND_NAME(f_call_control_id, f_INPUT1, ...){
  const cc_action =COMMAND_NAME;

  const request = superagent
    .auth(g_telnyx_key, g_telnyx_secret)
    .post(`https://api.telnyx.com/calls/${f_call_control_id}/actions/${cc_action}`)
    .send({ PARAM1: f_INPUT1 });

  request
    .then((response) => {
      const body = response.body;
    })
    .catch((error) => {
      console.log(error);
    });
}

Note: After pasting the above content, Kindly check and remove any new line added

Understanding the Command Syntax

There are several aspects of this function that deserve some attention:

  • Function Input Parameters : to execute every Telnyx Call Control Command you’ll need to feed your function with the following:
    • the Call Control ID
    • the input parameters, specific to the body of the Command you’re executing.

Having these set as function input parameters will make it generic enough to reuse for different use cases:

Copy
Copied
function call_control_COMMAND_NAME(f_call_control_id, f_INPUT)

Note: After pasting the above content, Kindly check and remove any new line added

All Telnyx Call Control Commands will be expecting the Call Control ID except Dial. There you’ll get a new one for the leg generated as response.

  • Name of the Call Control Command : as detailed [here] , the Command name is part of the API URL. In our code we call that the action name, and will feed the POST Request URL later:
Copy
Copied
  const cc_action =COMMAND_NAME;

Note: After pasting the above content, Kindly check and remove any new line added

  • Building the Telnyx Call Control Command : once you have the Command name defined, you should have all the necessary info to build the complete Telnyx Call Control Command:
Copy
Copied
  const request = superagent
    .auth(g_telnyx_key, g_telnyx_secret)
    .post(`https://api.telnyx.com/calls/${f_call_control_id}/actions/${cc_action}`)
    .send({ PARAM1: f_INPUT1 });
};

Note: After pasting the above content, Kindly check and remove any new line added

In this example, you can see that Call Control ID and the Action name will feed the URL of the API, both Telnyx Key and Telnyx Secret feed the Authentication headers, and the body will be formed with all of the different input parameters received for that specific Command.

  • Calling the Telnyx Call Control Command : Having the request headers and options / body set, the only thing left is to execute the POST Request to run the command. For that we are making use of node's request module:
Copy
Copied
  request
    .then((response) => {
      const body = response.body;
    })
    .catch((error) => {
      console.log(error);
    });
});

Note: After pasting the above content, Kindly check and remove any new line added

Telnyx Call Control Commands

This is what every Telnyx Call Control Command used in this application looks like:

Call Control Forking Start

Copy
Copied
function call_control_fork_start(
  f_call_control_id,
  f_fork_target,
  f_fork_rx,
  f_fork_tx
) {
  const cc_action = 'fork_start';
  const payload = {};

  if (!f_fork_target && f_fork_rx && f_fork_tx) {
    payload = {
      rx: f_fork_rx,
      tx: f_fork_tx,
    };
  } else if (f_fork_target && !f_fork_rx && !f_fork_tx) {
    payload = {
      target: f_fork_target,
    };
  }

  const request = superagent
    .auth(g_telnyx_key, g_telnyx_secret)
    .post(
      `https://api.telnyx.com/calls/${f_call_control_id}/actions/${cc_action}`
    )
    .send(payload);

  request
    .then((response) => {
      const body = response.body;
    })
    .catch((error) => {
      console.log(error);
    });
}

Note: After pasting the above content, Kindly check and remove any new line added

Call Control Transfer

Copy
Copied
function call_control_transfer(f_call_control_id, f_dest, f_orig) {
  const cc_action = 'transfer';

  const request = superagent
    .auth(g_telnyx_key, g_telnyx_secret)
    .post(
      `https://api.telnyx.com/calls/${f_call_control_id}/actions/${cc_action}`
    )
    .send({
      to: f_dest,
      from: f_orig,
    });

  request
    .then((response) => {
      const body = response.body;
    })
    .catch((error) => {
      console.log(error);
    });
}

Note: After pasting the above content, Kindly check and remove any new line added

Call Control Answer

Copy
Copied
function call_control_answer_call(f_call_control_id, f_client_state_s) {
  const cc_action = 'answer';
  const l_client_state_64 = null;

  if (f_client_state_s) {
    l_client_state_64 = Buffer.from(f_client_state_s).toString('base64');
  }

  const request = superagent
    .auth(g_telnyx_key, g_telnyx_secret)
    .post(
      `https://api.telnyx.com/calls/${f_call_control_id}/actions/${cc_action}`
    )
    .send({
      client_state: l_client_state_64, // if inbound call === null
    });

  request
    .then((response) => {
      const body = response.body;
    })
    .catch((error) => {
      console.log(error);
    });
}

Note: After pasting the above content, Kindly check and remove any new line added

Call Control Gather Using Speak

Copy
Copied
function call_control_gather_using_speak(
  f_call_control_id,
  f_tts_text,
  f_gather_digits,
  f_gather_max,
  f_client_state_s,
  f_term
) {
  const cc_action = 'gather_using_speak';
  const l_client_state_64 = null;

  if (f_client_state_s) {
    l_client_state_64 = Buffer.from(f_client_state_s).toString('base64');
  }

  const request = superagent
    .auth(g_telnyx_key, g_telnyx_secret)
    .post(
      `https://api.telnyx.com/calls/${f_call_control_id}/actions/${cc_action}`
    )
    .send({
      payload: f_tts_text,
      voice: g_ivr_voice,
      language: g_ivr_language,
      valid_digits: f_gather_digits,
      max: f_gather_max,
      terminating_digit: f_term,
      client_state: l_client_state_64,
    });

  request
    .then((response) => {
      const body = response.body;
    })
    .catch((error) => {
      console.log(error);
    });
}

Note: After pasting the above content, Kindly check and remove any new line added

Call Control Hangup

Copy
Copied
function call_control_hangup(f_call_control_id) {
  const cc_action = 'hangup';

  const request = superagent
    .auth(g_telnyx_key, g_telnyx_secret)
    .post(
      `https://api.telnyx.com/calls/${f_call_control_id}/actions/${cc_action}`
    )
    .send({});

  request
    .then((response) => {
      const body = response.body;
    })
    .catch((error) => {
      console.log(error);
    });
}

Note: After pasting the above content, Kindly check and remove any new line added

Client State: within some of the Telnyx Call Control Commands list we presented, you probably noticed we were including the Client State parameter. Client State is the key to ensure that we can have several levels on our IVR while consuming the same Call Control Events.

Because Call Control is stateless and async, your application will be receiving several events of the same type, e.g. user just included DTMF. With Client State you enforce a unique ID to be sent back to Telnyx which will be used within a particular Command flow, identifying it as being at Level 2 of a certain IVR for example.

Building an IVR that uses Media Forking

With all the basic Telnyx Call Control Commands set, we are ready to put them in the order that will create a simple IVR application. The main purpose of this tutorial is to demonstrate how to utilize Media Forking. For that, all we are going to do is:

  1. handle an incoming call
  2. greet and invite the calling party to press 1 to start the process
  3. redirect the call to a PSTN number and in parallel, start forking the media to a destination of your choice.

![IVR Arch]

To exemplify this process we created a simple API call that will be exposed as the webhook in Mission Portal. For that we would be using express:

Copy
Copied
$ npm install request --save

Note: After pasting the above content, Kindly check and remove any new line added

With express we can create an API wrapper that uses HTTP GET to call our Request Token method:

Copy
Copied
rest.post('/' + g_appName + '/mforking', function (req, res) {
  // IVR CODE GOES HERE
});

Note: After pasting the above content, Kindly check and remove any new line added

This would expose a webhook like the following:

Copy
Copied
https://MY_DOMAIN_URL/demo-mforking/mforking

You probably noticed g_appName in the previous point. That is part of a set of global variables we are defining with info we know we are going to use in this app: TTS parameters, like voice and language to be used, IVR redirecting contact points, and the Media Forking destination address.

You can set these at the beginning of your code:

Copy
Copied
// Application:
const g_appName = "demo-mforking";

// TTS Options
const g_ivr_voice     = 'female';
const g_ivr_language  = 'en-US';

// IVR Redirect Options
const g_pstn_phone     = '+10987654320;

// Forking Destination Options
const g_udp_target = 'udp:192.1.1.1:27000';
const g_udp_tx     = 'udp:192.1.1.1:27001';
const g_udp_rx     = 'udp:192.1.1.1:27002';

Note: After pasting the above content, Kindly check and remove any new line added

If you would like to run the application on your local machine you will have to expose the app to the public internet. To do this you can use ngrok. You can follow the setup guide for ngrok [here] .

With that set, we can fill in that space that we named as IVR CODE GOES HERE. When your webhook URL is ready you can add the webhook URL to your Mission Control Portal Connection associated with your number. Here's an example of what a Call Control setup looks like:

![Mission Control Portal Call Control setup]

So the first thing to be done is to identify the kind of event you just received and extract the Call Control Id and Client State:

Copy
Copied
if (req && req.body && req.body.event_type) {
  var l_hook_event_type = req.body.event_type;
  var l_call_control_id = req.body.payload.call_control_id;
  var l_client_state_64 = req.body.payload.client_state;
} else {
  res.end('0');
}

Note: After pasting the above content, Kindly check and remove any new line added

Once you identify the Event Type received, it’s just a matter of having your application react to that. It's the way you react to an Event that helps you in creating the IVR logic. We will execute Telnyx Call Control Commands as a reaction to Events.

Webhook Call Initiated >> Command Answer Call

Copy
Copied
if (l_hook_event_type == 'call_initiated') {
  if (req.body.payload.direction == 'incoming')
    call_control_answer_call(l_call_control_id, null);
  else call_control_answer_call(l_call_control_id, 'stage-outgoing');
  res.end();
}

Note: After pasting the above content, Kindly check and remove any new line added

Webhook Call Answered >> Command Gather Using Speak

Once your app is notified by Telnyx that the call was established you want to initiate your IVR. You do that using the Telnyx Call Control Command Gather Using Speak, with the IVR Lobby message.

As part of the Gather Using Speak Command, we indicate that valid digits for the DTMF collection are 1 and 2, and that only 1 digit input would be valid. At this point we will also set the client-state as stage-dial.

Copy
Copied
else if (l_hook_event_type=='call_answered'){
  if (!l_client_state_64) {
    // No State >> Incoming >> Gather Input
      call_control_gather_using_speak(
        l_call_control_id,
        'Welcome to this Telnyx Demo,' +
        'Please press 1 to transfer the call and start forking,',
        '1', '1', 'stage-dial', ''
      );
  }
  res.end();
}

Note: After pasting the above content, Kindly check and remove any new line added

Webhook Speak Ended >> Do Nothing

Your app will be informed that the Speak executed ended at some point. For the IVR we are doing nothing with that info, but we will need to reply to that command.

Copy
Copied
else if (l_hook_event_type =='speak_ended'){
  res.end();
}

Note: After pasting the above content, Kindly check and remove any new line added

For consistency, the Telnyx Call Control engine requires every single Webhook to be replied to by the Webhook end-point, otherwise we will keep trying to send it. For that reason, we have to be ready to consume every Webhook we expect to receive and reply with 200 OK.

Webhook Call Bridged >> Do Nothing

Your app will be informed that the call was bridged at some point. For the IVR we are doing nothing with that info, but we will need to reply to that command.

Copy
Copied
else if (l_hook_event_type == call_bridged){
  res.end();
}

Note: After pasting the above content, Kindly check and remove any new line added

Webhook Gather Ended >> IVR Logic

When you receive the Webhook informing your application that Call Control Gather Ended (DTMF input) we can create the redirection and forking logic:

Copy
Copied
else if (l_hook_event_type =='gather_ended'){
  // Receive DTMF Number
  var l_dtmf_number = req.body.payload.digits;

  // Set Client State
  var l_client_state_buff = new Buffer(l_client_state_64,'base64');
  var l_client_state_s = l_client_state_buff.toString('ascii');

  if (l_client_state_s == "stage-dial" && l_dtmf_number == 1) {

    // Transfer Call
    call_control_transfer(
      l_call_control_id,
      g_pstn_phone,
      req.body.payload.from
    );

  // Start Forking
    call_control_fork_start(
      l_call_control_id,
      g_udp_target,
      null,
      null
    );
  }
  res.end();
}

Note: After pasting the above content, Kindly check and remove any new line added

Here we do a very simple check for DTMF digits received and the client-state value. If it’s the same state that we set previously (stage-dial) and the tone received was ‘1’, then we proceed to the logic execution.

In the logic execution, we are using Call Control Transfer to transfer the call to the PSTN phone number hardcoded at the beginning of the code.

At the same time (because both the code and Call Control are async) we are starting the Media Forking using Call Control Fork Start. For Media Forking to start, we need to specify the destination for the media.

Consulting the Call Control Media Forking [documentation page] will give you more information about the three main parameters that can be used depending on the way we want to stream the media:

  • if the intention is to stream both inbound and outbound legs of the call - in this case the A and B legs of the call - to the same destination point, you will then use target in the body. To use target we are calling the global g_udp_target variable that includes both ip and port destination for receiving the data. If we use target we cannot specify the rx and tx attributes, hence specifying them null as the example above states.
  • if the intention is to stream inbound and outbound legs of the call to different IP:Port tuples, in case you would like to digest each leg separately or analyse one of the legs only, then you will not use target but the attributes rx and tx instead. For that case you will not formulate the call_control_fork_start function as above but as follows instead:
Copy
Copied
call_control_fork_start(l_call_control_id, null, g_udp_rx, g_udp_tx);

Note: After pasting the above content, Kindly check and remove any new line added

Copy
Copied
Both `g_udp_rx` and `g_udp_tx` are two global variables also specified at the beginning of this code, having different destination ports so we can differentiate both traffic’s legs.

If the destination for forked media is specified using the target attribute, the RTP will be encapsulated in an extra header, which adds a 24 byte header to each packet payload.

Lightning-Up the Application

Finally, the last piece of the puzzle is having your application listening for Telnyx Webhooks:

Copy
Copied
var server = rest.listen(8081, function () {
  var host = server.address().address;
  var port = server.address().port;
});

Note: After pasting the above content, Kindly check and remove any new line added

And start the application by executing the following command:

Copy
Copied
$ node demo-mforking.js

Note: After pasting the above content, Kindly check and remove any new line added

Listening for UDP

The easiest way to confirm that your media stream is being sent is to have a tcpdump capturing incoming UDP traffic for the destination IP and Ports designated for that.

There are several ways to do that, but in this tutorial we executed the following command:

Copy
Copied
$ tcpdump -i INTERFACE udp portrange 27000-27010 -w mforking.pcap

Note: After pasting the above content, Kindly check and remove any new line added

This command would listen for UDP packets within the port range 27000-27002 (the ones we use in this tutorial). Please be sure to replace INTERFACE with the interface that is exposed to the traffic. Once stopped, you will get all packets collected in a pcap file that you can analyse using a packet analyzer application such as Wireshark.

Get the Complete Application

If you enjoyed this tutorial and would like to grab all this code as a whole, be sure to visit our public GitHub page.