Xianwen's blog

Life is either a daring adverventure, or nothing.

How to Make an iOS VoIP App With Pjsip: Part 3

Welcome to the third part of this tutorial series!

In the 1st post, we talked about how to compile pjsip, and run the built in demo on a real device. And in the 2nd post, we discussed setting up your own VoIP server, and actually made our 1st VoIP call from the iPhone to the Mac.

Today, we’re going to show you how to handle basic VoIP operations using pjsip. We’ll go into the details of the mac receiver voip app. Yes, this tutorial is supposed to talk about iOS app. However, since all the APIs provided by pjsip are in C, it’s easier to discuss them in a pure C program.

In fact, when we’re developing an iOS VoIP app, all the VoIP releated API calls are written in C. We then expose those C functions to our Objective-C world to be called.

The initialization and tear down of Pjsip

You could download the source code of the receiver voip app here first. Our tutorial will go through the source code.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/**
 * Display error and exit application
 *
 * @param title The error message.
 * @param status The error status code.
 */
static void error_exit(const char *title, pj_status_t status);

int main()
{
    pj_status_t status;

    // 1. Create pjsua first
    status = pjsua_create();
    if (status != PJ_SUCCESS) error_exit("Error in pjsua_create()", status);

  ...

    // 2. Destroy pjsua
    pjsua_destroy();

    return 0;
}

static void error_exit(const char *title, pj_status_t status)
{
    pjsua_perror(THIS_FILE, title, status);
    pjsua_destroy();
    exit(1);
}
  1. Before you can call any other pjsua API, you must call pjsua_create() to create the user agent first.
  2. And in the tear down of the app, you should destroy that user agent object
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
int main()
{
  ...
  
    status = pjsua_create();
    if (status != PJ_SUCCESS) error_exit("Error in pjsua_create()", status);

    // Init pjsua 
    {
        // 1. Init the config structure
        pjsua_config cfg;
        pjsua_config_default (&cfg);

        cfg.cb.on_incoming_call = &on_incoming_call;
        cfg.cb.on_call_media_state = &on_call_media_state;
        cfg.cb.on_call_state = &on_call_state;

        // 2. Init the logging config structure
        pjsua_logging_config log_cfg;
        pjsua_logging_config_default(&log_cfg);
        log_cfg.console_level = 4;

        // 3. Init the pjsua
        status = pjsua_init(&cfg, &log_cfg, NULL);
        if (status != PJ_SUCCESS) error_exit("Error in pjsua_init()", status);
    }

  ...
}
  1. Pjsip init takes a data structure pjsua_config to pass in the configuration. By calling pjsua_config_default, we pop the cfg with the default configuartion first. And then, we setup the 3 call back to be used, so that we could handle VoIP events.
  2. Similarly, the pjsua_logging_config structure is used to pass logging related configuration
  3. Now both configuration objects are ready, we could use them to actually initialize the pjsua.

Starting Pjsip

Pjsip supports both UDP transport and TCP transport. In this program, we enabled both UDP and TCP.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
int main()
{
  ...

    // Init pjsua 
    {
        ...
    }

    // Add UDP transport.
    {
        // 1. Init transport config structure
        pjsua_transport_config cfg;
        pjsua_transport_config_default(&cfg);
        cfg.port = 5080;

        // 2. Add UDP transport.
        status = pjsua_transport_create(PJSIP_TRANSPORT_UDP, &cfg, NULL);
        if (status != PJ_SUCCESS) error_exit("Error creating transport", status);
    }

    // 3. Add TCP transport.
    {
        // Init transport config structure
        pjsua_transport_config cfg;
        pjsua_transport_config_default(&cfg);
        cfg.port = 5080;

        // Add TCP transport.
        status = pjsua_transport_create(PJSIP_TRANSPORT_TCP, &cfg, NULL);
        if (status != PJ_SUCCESS) error_exit("Error creating transport", status);
    }

    // 4. Initialization is done, now start pjsua
    status = pjsua_start();
    if (status != PJ_SUCCESS) error_exit("Error starting pjsua", status);
  ...
}
  1. You should be quite familiar with this trick now: create a pjsua_transport_config, fill it with the default, and then do some configurations as we like. Just note that the 5080 is the default port for VoIP.
  2. By passing PJSIP_TRANSPORT_UDP to pjsua_transport_create, we create a UDP transport for the pjsua.
  3. We create TCP transport as well via similiar steps.
  4. Finally, we can now start the pjsua.

Theoretically speaking, this program is ready to take incoming call and make outgoing call at this point. However, in a real environment, we’ll need a mechanism to let the users to be aware of each other, so that they could actually make/receive calls.

To achieve this, we need to register ourself in a server, so that users could look up the server to find each others.

Register the account on pjsip server

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
int main()
{
  ...

    // Initialization is done, now start pjsua
    status = pjsua_start();
    if (status != PJ_SUCCESS) error_exit("Error starting pjsua", status);

    // Register the account on local sip server
    pjsua_acc_id acc_id;
    {
#define SIP_DOMAIN "localhost"
#define SIP_USER "receiver"

      // 1. Create the account configuration object
        pjsua_acc_config cfg;
        pjsua_acc_config_default(&cfg);
        cfg.id = pj_str("sip:" SIP_USER "@" SIP_DOMAIN);
        cfg.reg_uri = pj_str("sip:" SIP_DOMAIN);

      // 2. Register the account
        status = pjsua_acc_add(&cfg, PJ_TRUE, &acc_id);
        if (status != PJ_SUCCESS) error_exit("Error adding account", status);
    }
  ...
}
  1. Aha, another configuartion object. This time, we specify the user id via the cfg.id, and the uri of the sip server via the cfg.reg_uir. In this case, it’s “sip:receiver@localhost” and “sip:localhost”.
  2. Then, we call pjsua_acc_add to register our account on the local sip server.

Handling the events

Pjsip uses callback mechanism to notify events. These callbacks are set in the pjsua_config object when you call the pjsua_init function.

As the name suggests, the cb.on_incoming_call is called when there is an incoming call, the cb.on_call_media_state is for media state change, and the cb.on_call_state is for any call status change.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/* Callback called by the library upon receiving incoming call */
static void on_incoming_call(pjsua_acc_id acc_id, pjsua_call_id call_id,
        pjsip_rx_data *rdata)
{
  // 1. Get caller info
    pjsua_call_info ci;

    PJ_UNUSED_ARG(acc_id);
    PJ_UNUSED_ARG(rdata);

    pjsua_call_get_info(call_id, &ci);

    PJ_LOG(3,(THIS_FILE, "Incoming call from %.*s!!",
                (int)ci.remote_info.slen,
                ci.remote_info.ptr));

  // 2. Answer the call
    /* Automatically answer incoming calls with 200/OK */
    pjsua_call_answer(call_id, 200, NULL, NULL);
}
  1. We get the info of the caller via pjsua_call_get_info, and log it to the console
  2. To make things simple, we simply answers the call automatically.

In pjsip, every call is assigned an incremental number as the “call_id”. For example, when you received 2 incoming calls simultaneously, their call_id might be 0 and 1. We’ll use this call_id for quite a lot of pjsip APIs, to let pjsip know on which call we want to execute the action.

In this example, we utilized this call_id in 2 funtions: pjsua_call_get_info to get the infomation of the call, and pjsua_call_answer to answer that call.

1
2
3
4
5
6
7
8
9
10
11
12
/* Callback called by the library when call's state has changed */
static void on_call_state(pjsua_call_id call_id, pjsip_event *e)
{
    pjsua_call_info ci;

    PJ_UNUSED_ARG(e);

    pjsua_call_get_info(call_id, &ci);
    PJ_LOG(3,(THIS_FILE, "Call %d state=%.*s", call_id,
                (int)ci.state_text.slen,
                ci.state_text.ptr));
}

In this callback, we simply log the new state of the call.

1
2
3
4
5
6
7
8
9
10
11
12
13
/* Callback called by the library when call's media state has changed */
static void on_call_media_state(pjsua_call_id call_id)
{
    pjsua_call_info ci;

    pjsua_call_get_info(call_id, &ci);

    if (ci.media_status == PJSUA_CALL_MEDIA_ACTIVE) {
        // When media is active, connect call to sound device.
        pjsua_conf_connect(ci.conf_slot, 0);
        pjsua_conf_connect(0, ci.conf_slot);
    }
}

Sometimes, the media could become unavailable. For example, when the user is on a native call, the media would be occupied by the native call. So, when we found that the media is ready to use, we call pjsua_conf_connect to actually use the media.

To be continued…

Comments