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

Welcome to the 5th part of this tutorial series!

In the previous tutorial, we have created an iOS project, and made pjsip functions ready to be called. In today’s tutorial, we are going to actually call those pjsip functions from our iOS app.

In case you’re new to this series, here is a quick recap of previous tutorials:

  • Part 1: How to compile pjsip and run the embeded demo on your iOS device
  • Part 2: How to set up your own VoIP server via Kamailio, and actually make VoIP call between an iOS device and a Mac.
  • Part 3: A brief introduction of pjsip API, by digging into the source code of a very simple Mac VoIP program.
  • Part 4: How to add the pjsip libraries and header files into your own iOS project, so that you can call pjsip API from your app.

You could download the source code of this project (starting from the 4th tutorial) on Github here. Each commit of the project, is representing one step of the tutorial.

(This article was written on 2014 and posted on my old blog. Most of the content should still work on latest Mac and iOS, i.e, macOS Sierra and Xcode 8.3. Please let me know if you find anything not working, thanks.)

Adding XCPjsua.c and XCPjsua.h

As we mentioned before, the interfaces of pjsip functions are in C. So in order to call pjsip methods, normally, we would create a C file to encapsulate invocation of pjsip methods, and then expose the methods of this C file via a header file.

Create a new Header File, and name it XCPjsua.h

Then, create a new C File, and name it XCPjsua.c

Your project will look like this afterwards:

Starting pjsua

In the XCPjsua.h, add the following declaration of startPjsua() method:

/**
 * Initialize and start pjsua.
 *
 * @param sipUser the sip username to be used for register
 * @param sipDomain the domain of the sip register server
 *
 * @return When successful, returns 0.
 */
int startPjsip(char *sipUser, char* sipDomain);

In the XCPjsua.c, add the following code to implement this startPjsua() method:

#include <pjsua-lib/pjsua.h>

#define THIS_FILE "XCPjsua.c"
static pjsua_acc_id acc_id;

const size_t MAX_SIP_ID_LENGTH = 50;
const size_t MAX_SIP_REG_URI_LENGTH = 50;

static void on_incoming_call(pjsua_acc_id acc_id, pjsua_call_id call_id, pjsip_rx_data *rdata);
static void on_call_state(pjsua_call_id call_id, pjsip_event *e);
static void on_call_media_state(pjsua_call_id call_id);
static void error_exit(const char *title, pj_status_t status);

int startPjsip(char *sipUser, char* sipDomain)
{
    pj_status_t status;

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

    // Init pjsua
    {
        // 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;

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

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

    // Add UDP 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_UDP, &cfg, NULL);
        if (status != PJ_SUCCESS) error_exit("Error creating transport", status);
    }

    // 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);
    }

    // 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_config cfg;

        pjsua_acc_config_default(&cfg);

        char sipId[MAX_SIP_ID_LENGTH];
        sprintf(sipId, "sip:%s@%s", sipUser, sipDomain);
        cfg.id = pj_str(sipId);

        char regUri[MAX_SIP_REG_URI_LENGTH];
        sprintf(regUri, "sip:%s", sipDomain);
        cfg.reg_uri = pj_str(regUri);

        status = pjsua_acc_add(&cfg, PJ_TRUE, &acc_id);
        if (status != PJ_SUCCESS) error_exit("Error adding account", status);
    }

    return 0;
}

/* 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)
{
    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));

    /* Automatically answer incoming calls with 200/OK */
    pjsua_call_answer(call_id, 200, NULL, NULL);
}

/* 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));
}

/* 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);
    }
}

/* Display error and exit application */
static void error_exit(const char *title, pj_status_t status)
{
    pjsua_perror(THIS_FILE, title, status);
    pjsua_destroy();
    exit(1);
}

This code is almost the same as what we’ve discussed in the 3rd post. If you don’t understand any part of it, you could probably find your answer in that post.

We have 2 main differences here:

  1. We made the account id – “pjsua_acc_id acc_id” – a static variable. Because we’ll need this acc_id to make VoIP call later.
  2. We parameterized the sipUser and the sipDomain, so that we could easily connect to different VoIP servers in differnt userNames.

Make VoIP call and end VoIP call

Declare the makeCall() and endCall() in the XCPjsua.h

/**
 * Make VoIP call.
 *
 * @param destUri the uri of the receiver, something like "sip:192.168.43.106:5080"
 */
void makeCall(char* destUri);

/**
 * End ongoing VoIP calls
 */
void endCall();

And add the following code in XCPjsua.c to implement them:

void makeCall(char* destUri)
{
    pj_status_t status;
    pj_str_t uri = pj_str(destUri);

    status = pjsua_call_make_call(acc_id, &uri, 0, NULL, NULL, NULL);
    if (status != PJ_SUCCESS) error_exit("Error making call", status);
}

void endCall()
{
   pjsua_call_hangup_all();
}

The code itself should be quite straight forward. We simply make use of the pjsua_call_make_call() and the pjsua_call_hangup_all() to make call and end call.

The acc_id being used in pjsua_call_make_call() is the global variable we declared before.

Define PJ_AUTOCONF

You should be able to compile and run this app in the simulator now. (But 64-bit simulator is not supported yet, becasue we didn’t include the x86-64 architecture in our fat pjsip library. We only included the i386 architecture, which is for 32-bit simulator).

However, if you try to build and run it on a real device, you’ll get compile error like the following:

User Defined Issues: "Please specify target machine."

Basically, it’s asking you to specify a target architecture. But we could let pjsip do it automatically for you:

Go to the “Build Settings”, and add PJ_AUTOCONF macro in the Preprocessor Macros field.

Try again, and you’ll be able to run this project on your own iPhone!

Initialize pjsip and actually make VoIP call

After all these hard prepartion work, it’s super easy now to initialize pjsip and make our VoIP call.

Start the local Kamailio server on your Mac, and run the mac receiver for receiving the incoming call (If you forgot how to do this, you could get more info in the second tutorial). And then, check the IP of your Mac, which would be the IP of your SIP server, and also the IP of your receiver.

Next, add this import statement at the beginning of your XCAppDelegate.m:

#import "XCPjsua.h"

And add the following code in the didFinishLaunchingWithOptions method of XCAppDelegate.m. In my case, the IP of my Mac is 192.168.43.106, you should change it according to your own IP.

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    // You should change 192.168.43.106 to the IP of your own Mac
    startPjsip("iPhone", "192.168.43.106");
    makeCall("sip:192.168.43.106:5080");
    return YES;
}

The port being used here – 5080 – is the port the receiver program listening to.

Now, go ahead to compile and run your app on your iPhone. When the app launches, it will register itself on your SIP server, and make call to the receiver program. You will then be able to hear your voice from your iPhone on your Mac and vice versa!

8 thoughts on “How to Make an iOS VoIP App With Pjsip: Part 5

  1. I got this error trying to work on my iPhoneSE

    ld: library not found for -lpj-arm64-apple-darwin_ios
    clang: error: linker command failed with exit code 1 (use -v to see invocation)

    Like

  2. Thanks a lot for great tutorial.
    I have a problem with XCPjsua.c class
    I’m getting error for this part:

    static void on_incoming_call(pjsua_acc_id acc_id, pjsua_call_id call_id, pjsip_rx_data *rdata);
    static void on_call_state(pjsua_call_id call_id, pjsip_event *e);
    static void on_call_media_state(pjsua_call_id call_id);
    static void error_exit(const char *title, pj_status_t status);

    Use of undefined identifier ‘pj_status_t’
    Use of undefined identifier ‘status’ ect.

    I have imported framework, but not sure why I’m getting there errors.

    Regards,
    Aleksandar

    Like

  3. Hi, Thanks a lot for great tutorial.

    After implementation all the steps , when I integrate it with my app I got below response from my SIP server

    12:31:15.507 pjsua_acc.c ….IP address change detected for account 0 (192.168.2.34:5080 –> 112.196.29.3:5080). Updating registration (using method 4)
    12:31:15.512 sip_auth_clien …Unable to set auth for tdta0x10117d400: can not find credential for d0c20d13-e5b4-4649-821e-9ab8ec94b141/Digest
    12:31:15.512 pjsua_acc.c ….SIP registration error: No suitable credential (PJSIP_ENOCREDENTIAL) [status=171101]

    After verify about it there is something which I need to do is

    first you attempt register
    FS will ask for digest credential (40x)
    then you register with credientials
    FS will decide success or fail

    Please let me know where I am missing.
    Can you please guide me , How can I make a User agent , End point etc.

    Thanks!

    Like

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s