如果有问题,请加QQ群 891339868 进行交流
上次在NUC972上移植好了pjsip库后,实现了一个简单的VOIP网关的功能,由于前一段时间再忙别的事情,没有来得及整理,今天忙里偷闲,总结一下,废话少说,直接步入正题。
pjsip库里面提供了好多例程,实现简单的VOIP功能还是挺简单的,主要包含以下几个部分:
1、初始化,废话少说,直接上代码:
int my_pjsua_init(void)
{
pjsua_acc_id acc_id;
pj_status_t status;
/* Create pjsua first! */
status = pjsua_create();
if (status != PJ_SUCCESS)
error_exit("Error in pjsua_create()", status);
/* Init pjsua */
{
pjsua_config cfg;
pjsua_logging_config log_cfg;
pjsua_media_config media_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;
pjsua_logging_config_default(&log_cfg);
log_cfg.console_level = 4;
pjsua_media_config_default(&media_cfg);
media_cfg.clock_rate = 8000;
media_cfg.snd_clock_rate = 8000;
media_cfg.no_vad = PJ_TRUE;
media_cfg.ec_options = 2;
media_cfg.quality = 5;
media_cfg.channel_count = 2;
status = pjsua_init(&cfg, &log_cfg, &media_cfg);
if (status != PJ_SUCCESS)
error_exit("Error in pjsua_init()", status);
pj_str_t host = pj_str("speex");
status = pjsua_codec_set_priority(&host, 0 );
if (status != PJ_SUCCESS)
error_exit("pjsua_codec_set_priority()", status);
host = pj_str("G722");
status = pjsua_codec_set_priority(&host, 0 );
if (status != PJ_SUCCESS)
error_exit("pjsua_codec_set_priority()", status);
}
/* Add UDP transport. */
{
pjsua_transport_config cfg;
pjsua_transport_config_default(&cfg);
cfg.port = 8060;
status = pjsua_transport_create(PJSIP_TRANSPORT_UDP, &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 to SIP server by creating SIP account. */
{
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);
cfg.cred_count = 1;
cfg.cred_info[0].realm = pj_str("*");//域名最好设置成*,要不然很容易注册不上
cfg.cred_info[0].scheme = pj_str("Digest");
cfg.cred_info[0].username = pj_str(SIP_USER);
cfg.cred_info[0].data_type = PJSIP_CRED_DATA_PLAIN_PASSWD;
cfg.cred_info[0].data = pj_str(SIP_PASSWD);
status = pjsua_acc_add(&cfg, PJ_TRUE, &acc_id);
if (status != PJ_SUCCESS)
error_exit("Error adding account", status);
}
return 0;
}
这个是我的初始化函数,下面详细分析一下:
1、使用pjsua_create()这个函数创建一个sip用户代理;
2、对sip用户代理的基本配置、日志打印配置和媒体配置进行初始化,具体流程都是使用相对应的默认配置函数进行默认配置,分别为pjsua_config_default,pjsua_logging_config_default,pjsua_media_config_default。
基本配置的功能主要是配置在运行过程中,需要调用的回调函数,这些回调函数是库自动调度的,我这里使用了三个回调函数:
on_incoming_call:来电处理函数。来具体看一下代码:
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);
}
我这里使用了来电自动接听的功能,所以使用了pjsua_call_answer(call_id, 200, NULL, NULL)这个函数。
on_call_media_state:更改媒体状态回调函数。来具体看一下代码:
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);
}
}
这个函数主要是在建立连接时使用。
on_call_state:调用当前呼叫状态回调函数,主要是当前呼叫状态发生状态时,库会自动调用该函数,来具体看一下代码:
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));
}
从代码中可以看出来,主要是打印当前呼叫的状态。
由于网关没有考虑自己主动挂断当前呼叫,所以没考虑其他回调函数。
接着来看一下日志打印配置,我这里配置的打印级别是4,这个数越大,级别越低,来看一下代码:
pjsua_logging_config_default(&log_cfg);
log_cfg.console_level = 4;
打印级别太高的话,控制台输出信息太多,挺讨厌的。
接着来看一下媒体配置信息,来看一下代码:
pjsua_media_config_default(&media_cfg);
media_cfg.clock_rate = 8000;
media_cfg.snd_clock_rate = 8000;
media_cfg.no_vad = PJ_TRUE;
media_cfg.ec_options = 2;
media_cfg.quality = 5;
media_cfg.channel_count = 2;
从代码上可以看出来,时钟和声音时钟都为8Khz,VAD(有效语音侦测功能)关闭,AEC算法选择的是webtrc,语音质量是5,最高10,双声道。这里重点说一下AEC算法和语音质量,NUC972性能有限,AEC算法的选择和语音质量的选择要慎重,不然的话会出现声音卡的现象,如果是其他高性能处理器,这个就不用考虑了。
到此为止,这三个配置基本上完成,就可以调用函数pjsua_init初始化了,如下代码所示:
status = pjsua_init(&cfg, &log_cfg, &media_cfg);
if (status != PJ_SUCCESS)
error_exit("Error in pjsua_init()", status);
虽然现在基本的配置已经完成了,但是在NUC972上,还需要对编解码算法的支持做一下限制,因为NUC972是一个ARM9,工作频率只有300MHz,在运行speex算法和G722算法时,性能是不够的,会出现非常严重的卡顿现象,所以需要将这两种算法给禁止了,当然,如果使用其他平台可以根据自己的实际情况设置,具体的配置看一下下面的代码:
pj_str_t host = pj_str("speex");
status = pjsua_codec_set_priority(&host, 0 );
if (status != PJ_SUCCESS)
error_exit("pjsua_codec_set_priority()", status);
host = pj_str("G722");
status = pjsua_codec_set_priority(&host, 0 );
if (status != PJ_SUCCESS)
error_exit("pjsua_codec_set_priority()", status);
将speex和G722算法的优先级都设置为0,这两种算法就被禁止了。
接下来说一下关于数据传输的配置,如下面的代码所示:
pjsua_transport_config cfg;
pjsua_transport_config_default(&cfg);
cfg.port = 8060;
status = pjsua_transport_create(PJSIP_TRANSPORT_UDP, &cfg, NULL);
if (status != PJ_SUCCESS)
error_exit("Error creating transport", status);
从上面的代码中可以很容易的看出来,我设置的传输模式是UDP模式,使用的端口是8060。到这里,基本的配置就算完了,下面就可以启动sip的用户代理了,如下面的代码所示:
/* Initialization is done, now start pjsua */
status = pjsua_start();
if (status != PJ_SUCCESS)
error_exit("Error starting pjsua", status);
/* Register to SIP server by creating SIP account. */
{
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);
cfg.cred_count = 1;
cfg.cred_info[0].realm = pj_str("*");//域名最好设置成*,要不然很容易注册不上
cfg.cred_info[0].scheme = pj_str("Digest");
cfg.cred_info[0].username = pj_str(SIP_USER);
cfg.cred_info[0].data_type = PJSIP_CRED_DATA_PLAIN_PASSWD;
cfg.cred_info[0].data = pj_str(SIP_PASSWD);
status = pjsua_acc_add(&cfg, PJ_TRUE, &acc_id);
if (status != PJ_SUCCESS)
error_exit("Error adding account", status);
}
库函数pjsua_start()是启动服务的函数,启动服务以后,要向sip服务器注册,如果目标服务器有当前的账号,就可以注册成功。
最后说一下在主函数中怎么使用pthread_create创建线程并调用pjsip相关的子函数。
一般情况下,系统需要实现多线程,这就牵扯到了pjsip线程与原有线程兼容性的问题,要想解决这一问题,就需要将使用pthread_create创建的线程注册到pjsip线程中,具体的做法看如下代码:
pthread_t thread20;
pj_thread_desc desc;
pj_bzero(desc, sizeof(desc));
int iRet20;
iRet20 = pthread_create(&thread20, PTHREAD_CREATE_JOINABLE,
(void *)wait_for_hangup, NULL);
if(iRet20)
{
perror("pthread_create:thread20. \n");
return 0;
}
pj_thread_t *thread = &thread20;
pj_thread_register(NULL, desc, &thread);
我用pthread_create创建了一个线程以后,再使用pjsip库函数pj_thread_register想pjsip库注册一下就OK了!好了,说到这里,用NUC972实现的VOIP网关就可以工作了,下面看一下具体的工作效果:
1、开机运行,打印了一大堆东西,主要看最后几行:
出现这种提示就说明已经注册成功,本机的账户是104,sip服务器的URL是192.168.0.200:8060,设置的是来电自动接听。
2、来电自动接听,又是打印了一堆东西,乱七八糟的,具体的就先不贴了
3、对方挂断,依然是打印了一堆东西。
好了,nuc972上实现简单的自动接听的VOIP网关的功能已经完成。