PJSUA是一个开源的命令行SIP用户代理(软电话),用PJSIP协议,PJNATH,和PJMEDIA实现
PJSUA虽然只有很简单的命令行界面,但是功能齐全。
如何在PJSUA基础上改建自己的USER agent? 首先要理清PJSUA的程序框架。
源码阅读提示,实现调用栈的跟踪,貌似线程安全的(使用线程TLS机制:https://blog.csdn.net/waruqi/article/details/53201531)
pj_log_push_indent()
pj_log_pop_indent()
一) PJSUA的程序入口函数
int main(int argc, char *argv[])
{
//pj_run_app封装了操作系统引起的差异,通过对最后一个参数的扩展,可为特定OS系统提供更多的启动信息
return pj_run_app(&main_func, argc, argv, 0);
}
二) PJSUA的App程序框架
int main_func(int argc, char *argv[])
{
pj_status_t status = PJ_TRUE;
//从面向对象的角度来理解:
// app实例只是对pjsua-core的封装层实例
// cfg参数提供封装层对象的(创建和销毁过程中)回调接口
// app真正的配置参数(来自命令行参数或配置文件)的声明位于pjsua_app_common.c: pjsua_app_config app_config
// pjsua-core的配置pjsua_config相当于pjsua_app_config的“父类”---------------参见pjsua_app_config声明中的成员cfg;
// 操作系统相关的消息会里回调使用setup_signal_handler()/setup_socket_signal()进行设置
//封装层对象使用pjsua_app_init初始化一个app实例
//封装层对象使用pjsua_app_run运行一个app实例
//封装层对象使用pjsua_app_destroy销毁一个app实例
//如果封装层对象被其他对象所使用例如有其他线程中的对象使用了app中的资源),则pjsua_app_destroy应该等待该线程结束后在进行销毁
pj_bzero(&cfg, sizeof(cfg));
cfg.on_started = &on_app_started;
cfg.on_stopped = &on_app_stopped;
cfg.on_config_init = &on_app_config_init;
cfg.argc = argc;
cfg.argv = argv;
setup_signal_handler();
setup_socket_signal();
while (running) {
status = pjsua_app_init(&cfg);
if (status == PJ_SUCCESS) {
status = pjsua_app_run(PJ_TRUE);
} else {
running = PJ_FALSE;
}
if (!receive_end_sig) {
pjsua_app_destroy();
/* This is on purpose */
pjsua_app_destroy();
} else {
pj_thread_join(sig_thread);
}
}
return 0;
}
三) app实例初始化函数pjsua_app_init()
pj_status_t pjsua_app_init(const pjsua_app_cfg_t *cfg)
{
pj_status_t status;
pj_memcpy(&app_cfg, cfg, sizeof(app_cfg));
/* 初始化函数app_init()主要用来进行pjsip内核初始化
* 包括默认参数的设置
* 命令行解析,配置文件加载
* 回调函数设置
* PJSIP协议栈内核初始化
* 会议桥初始化----------------为什么用到会议桥, 拨号铃声、震铃音、DTMF音频需要用到会议桥
* SIP账户加载
* */
status = app_init();
if (status != PJ_SUCCESS)
return status;
/* 在设计模式中,cli_init()事项了典型的基于工厂模式的接口初始化案例
* 根据不同的命令接口类型, 使用不同的工厂函数,创建不同的命令接口对象
* 我只需要再设计一个新的接口, 就可以实现自己的命令接口
* 例如,自己的电话键盘扫描接口
*
* 在 pj_cli_create函数中,会初始化一个命令入口映射表: 字符-命令处理函数--------------cli_setup_command函数建立这个映射表
* 用来设置命令所对应的处理函数入口, 可以是相同的入口函数,也可以是不同的入口函数
* 通过扩展自己的命令处理入口,可以用来匹配自己的cli接口对象
* */
if (app_config.use_cli) {
status = cli_init(); //命令行接口初始化
}
return status;
}
四) app实例运行函数pjsua_app_run()
1) 启动pjua内核: pjsua_start()
2) 如果命令行参数含有一个指向被叫uri_to_call, 发起呼叫:pjsua_call_make_call(current_acc, &uri_arg, &call_opt, NULL, NULL, NULL);
3) 命令行参数处理:
legacy_main() 实现了老式的控制台命令行接口
legacy_main使用打印菜单信息方式提示用户输入所需要的命令行命令以及相应的参数
控制台命令参考:https://blog.csdn.net/zoutian007/article/details/7970160?locationNum=4&fps=1
cli_main() 则采用抽象接口方式实现一个能够兼容各种命令输入接口的统一接口
目前,提供了cli_telnet和cli_console两种方式,具体功能到底实现没有,暂时还没研究
legacy_main()/cli_main()都是无限循环的(除非输入退出命令)
所以可以认为pjsua_app_run()是阻塞的, 输入退出命令之前, pjsua程序实例不会退出
五) pjsip 内核初始化app_init()
这里有一篇参考文档: https://www.2cto.com/kf/201802/719769.html
官方的参考文档: http://www.pjsip.org/pjsip/docs/html/group__PJSUA__LIB.htm
1) 创建pjsua对象实例: pjsua_create(), 在pjsua_create内
初始化pjua用到的所有组建库(嗲用相应的init函数, 例如pj_init())
设置默认的音频/视频设备ID
建立pjusa-core实例操作锁,定时器操作锁
创建sip_end_point,建立SIP协议栈处理框架
后续的pjsua_init()会启动如下的工作线程, pjsua_handle_events调用pjsip_endpt_handle_events2()驱动对endpt->ioqueue的轮询
2) 初始化命令命令接口设备的默认参数: 例如: 命令接口的类型信息,控制台命令提示字符串, telnet接口的网络接口参数等
3) 命令行参数解析: load_config(app_cfg.argc, app_cfg.argv, &uri_arg);
如果命令行参数提供了配置文件, 还需要从配置文件中进一步加载配置信息
命令行参数可以参考这里: https://blog.csdn.net/zoutian007/article/details/7970160?locationNum=4&fps=1
4) 设置pjsua-core的回调函数:
代码中的app_config.cfg是pjsua-core中所定义的保存配置信息的结构体对象
app_config.cfg.cb.on_call_state = &on_call_state;
app_config.cfg.cb.on_call_media_state = &on_call_media_state;
app_config.cfg.cb.on_incoming_call = &on_incoming_call;
app_config.cfg.cb.on_call_tsx_state = &on_call_tsx_state;
app_config.cfg.cb.on_dtmf_digit = &call_on_dtmf_callback;
app_config.cfg.cb.on_call_redirected = &call_on_redirected;
app_config.cfg.cb.on_reg_state = &on_reg_state;
app_config.cfg.cb.on_incoming_subscribe = &on_incoming_subscribe;
app_config.cfg.cb.on_buddy_state = &on_buddy_state;
app_config.cfg.cb.on_buddy_evsub_state = &on_buddy_evsub_state;
app_config.cfg.cb.on_pager = &on_pager;
app_config.cfg.cb.on_typing = &on_typing;
app_config.cfg.cb.on_call_transfer_status = &on_call_transfer_status;
app_config.cfg.cb.on_call_replaced = &on_call_replaced;
app_config.cfg.cb.on_nat_detect = &on_nat_detect;
app_config.cfg.cb.on_mwi_info = &on_mwi_info;
app_config.cfg.cb.on_transport_state = &on_transport_state;
app_config.cfg.cb.on_ice_transport_error = &on_ice_transport_error;
app_config.cfg.cb.on_snd_dev_operation = &on_snd_dev_operation;
app_config.cfg.cb.on_call_media_event = &on_call_media_event;
5) 设置声卡延迟参数(不设置的话pjmedia会使用默认值):
app_config.media_cfg.snd_rec_latency = app_config.capture_lat;
app_config.media_cfg.snd_play_latency = app_config.playback_lat;
-------- 看了看源码源码中alsa声卡, 好像与alsa声卡的缓冲区总帧数(总的缓冲时间)有关, 如下:
tmp_buf_size = (rate / 1000) * param->input_latency_ms;
snd_pcm_hw_params_set_buffer_size_near (stream->ca_pcm, params, &tmp_buf_size);
6) 用户层的配置信息加载: (*app_cfg.on_config_init)(&app_config);
还记得在 “二) PJSUA的App程序框架” 中配置的on_config_init回调函数吗?
openwrt下可以在此回调函数中, 使用uci接口来修改应用程序的配置, 这样的好处是不需要修改pjsua程序原来的配置文件加载代码
7) 初始化pjsua-core: pjsua_init(&app_config.cfg, &app_config.log_cfg,&app_config.media_cfg);
入口参数携带了pjsua-core, log系统和media系统的初始化参数
8) 向sip_end_point注册app层模块: pjsip_endpt_register_module(pjsua_get_pjsip_endpt(), &mod_default_handler);
这样,我们的app层模块才能融入sip_end_point的modules模块链表中
当pjsua_handle_events(TIMEOUT)驱动sip_end_point工作时, 我们的模块才会起到作用
app层模块只实现了on_rx_request接口, 应用层在次对incoming 请求消息作出响应, 关键代码:
pjsip_endpt_send_response2(pjsua_get_pjsip_endpt(), rdata, tdata, NULL, NULL);
10) 初始化calls数组, 特别关注超时回调:
app_config.call_data[i].timer.id = PJSUA_INVALID_ID;
app_config.call_data[i].timer.cb = &call_timeout_callback;
11) 创建wav文件播放对象,并attach到会议桥的wav_port: ---你可以把它作为个性化铃声
pjsua_player_create(&app_config.wav_files[i], play_options, &wav_id); -------每个wave文件对应一个, 用于播放震铃、回铃音
app_config.call_data[i].timer.cb = &call_timeout_callback; 并为playfile对象安装on_playfile_done回调函数
12) 创建用于DTMF音频合成的波形发生器,并加入会议桥:
pjmedia_tonegen_create2(app_config.pool, &label,8000, 1, 160, 16,PJMEDIA_TONEGEN_LOOP, &tport);
pjsua_conf_add_port(app_config.pool, tport, &app_config.tone_slots[i]);
13) 创建电话录音pjmedia_port并连接到会议桥
pjsua_recorder_create(&app_config.rec_file, 0, NULL, 0, 0, &app_config.rec_id);
14) 呼出等待回铃音合成器创建并attach到会议桥
pjmedia_tonegen_create2(app_config.pool, &name, app_config.media_cfg.clock_rate, app_config.media_cfg.channel_count,
samples_per_frame, 16, PJMEDIA_TONEGEN_LOOP, &app_config.ringback_port);
pjsua_conf_add_port(app_config.pool, app_config.ringback_port, &app_config.ringback_slot)
15) 呼入震铃音合成器创建并attach到会议桥
pjmedia_tonegen_create2(app_config.pool, &name, app_config.media_cfg.clock_rate, app_config.media_cfg.channel_count,
samples_per_frame, 16, PJMEDIA_TONEGEN_LOOP, &app_config.ring_port);
pjsua_conf_add_port(app_config.pool, app_config.ringback_port, &app_config.ring_slot)
16) 信令消息传输层初始化
pjsua_transport_create(type,&app_config.udp_cfg,&transport_id);
17) 账户信息添加
pjsua_acc_add(&app_config.acc_cfg[i], PJ_TRUE, NULL);
18) 电话簿信息添加
pjsua_buddy_add(&app_config.buddy_cfg[i], NULL);
19) 编码器优先级设置: pjsua_codec_set_priority
20) 声卡设备指定(不采用默认声卡设备时): pjsua_set_snd_dev
PJSUA虽然只有很简单的命令行界面,但是功能齐全。
如何在PJSUA基础上改建自己的USER agent? 首先要理清PJSUA的程序框架。
源码阅读提示,实现调用栈的跟踪,貌似线程安全的(使用线程TLS机制:https://blog.csdn.net/waruqi/article/details/53201531)
pj_log_push_indent()
pj_log_pop_indent()
一) PJSUA的程序入口函数
int main(int argc, char *argv[])
{
//pj_run_app封装了操作系统引起的差异,通过对最后一个参数的扩展,可为特定OS系统提供更多的启动信息
return pj_run_app(&main_func, argc, argv, 0);
}
二) PJSUA的App程序框架
int main_func(int argc, char *argv[])
{
pj_status_t status = PJ_TRUE;
//从面向对象的角度来理解:
// app实例只是对pjsua-core的封装层实例
// cfg参数提供封装层对象的(创建和销毁过程中)回调接口
// app真正的配置参数(来自命令行参数或配置文件)的声明位于pjsua_app_common.c: pjsua_app_config app_config
// pjsua-core的配置pjsua_config相当于pjsua_app_config的“父类”---------------参见pjsua_app_config声明中的成员cfg;
// 操作系统相关的消息会里回调使用setup_signal_handler()/setup_socket_signal()进行设置
//封装层对象使用pjsua_app_init初始化一个app实例
//封装层对象使用pjsua_app_run运行一个app实例
//封装层对象使用pjsua_app_destroy销毁一个app实例
//如果封装层对象被其他对象所使用例如有其他线程中的对象使用了app中的资源),则pjsua_app_destroy应该等待该线程结束后在进行销毁
pj_bzero(&cfg, sizeof(cfg));
cfg.on_started = &on_app_started;
cfg.on_stopped = &on_app_stopped;
cfg.on_config_init = &on_app_config_init;
cfg.argc = argc;
cfg.argv = argv;
setup_signal_handler();
setup_socket_signal();
while (running) {
status = pjsua_app_init(&cfg);
if (status == PJ_SUCCESS) {
status = pjsua_app_run(PJ_TRUE);
} else {
running = PJ_FALSE;
}
if (!receive_end_sig) {
pjsua_app_destroy();
/* This is on purpose */
pjsua_app_destroy();
} else {
pj_thread_join(sig_thread);
}
}
return 0;
}
三) app实例初始化函数pjsua_app_init()
pj_status_t pjsua_app_init(const pjsua_app_cfg_t *cfg)
{
pj_status_t status;
pj_memcpy(&app_cfg, cfg, sizeof(app_cfg));
/* 初始化函数app_init()主要用来进行pjsip内核初始化
* 包括默认参数的设置
* 命令行解析,配置文件加载
* 回调函数设置
* PJSIP协议栈内核初始化
* 会议桥初始化----------------为什么用到会议桥, 拨号铃声、震铃音、DTMF音频需要用到会议桥
* SIP账户加载
* */
status = app_init();
if (status != PJ_SUCCESS)
return status;
/* 在设计模式中,cli_init()事项了典型的基于工厂模式的接口初始化案例
* 根据不同的命令接口类型, 使用不同的工厂函数,创建不同的命令接口对象
* 我只需要再设计一个新的接口, 就可以实现自己的命令接口
* 例如,自己的电话键盘扫描接口
*
* 在 pj_cli_create函数中,会初始化一个命令入口映射表: 字符-命令处理函数--------------cli_setup_command函数建立这个映射表
* 用来设置命令所对应的处理函数入口, 可以是相同的入口函数,也可以是不同的入口函数
* 通过扩展自己的命令处理入口,可以用来匹配自己的cli接口对象
* */
if (app_config.use_cli) {
status = cli_init(); //命令行接口初始化
}
return status;
}
四) app实例运行函数pjsua_app_run()
1) 启动pjua内核: pjsua_start()
2) 如果命令行参数含有一个指向被叫uri_to_call, 发起呼叫:pjsua_call_make_call(current_acc, &uri_arg, &call_opt, NULL, NULL, NULL);
3) 命令行参数处理:
legacy_main() 实现了老式的控制台命令行接口
legacy_main使用打印菜单信息方式提示用户输入所需要的命令行命令以及相应的参数
控制台命令参考:https://blog.csdn.net/zoutian007/article/details/7970160?locationNum=4&fps=1
cli_main() 则采用抽象接口方式实现一个能够兼容各种命令输入接口的统一接口
目前,提供了cli_telnet和cli_console两种方式,具体功能到底实现没有,暂时还没研究
legacy_main()/cli_main()都是无限循环的(除非输入退出命令)
所以可以认为pjsua_app_run()是阻塞的, 输入退出命令之前, pjsua程序实例不会退出
五) pjsip 内核初始化app_init()
这里有一篇参考文档: https://www.2cto.com/kf/201802/719769.html
官方的参考文档: http://www.pjsip.org/pjsip/docs/html/group__PJSUA__LIB.htm
1) 创建pjsua对象实例: pjsua_create(), 在pjsua_create内
初始化pjua用到的所有组建库(嗲用相应的init函数, 例如pj_init())
设置默认的音频/视频设备ID
建立pjusa-core实例操作锁,定时器操作锁
创建sip_end_point,建立SIP协议栈处理框架
后续的pjsua_init()会启动如下的工作线程, pjsua_handle_events调用pjsip_endpt_handle_events2()驱动对endpt->ioqueue的轮询
2) 初始化命令命令接口设备的默认参数: 例如: 命令接口的类型信息,控制台命令提示字符串, telnet接口的网络接口参数等
3) 命令行参数解析: load_config(app_cfg.argc, app_cfg.argv, &uri_arg);
如果命令行参数提供了配置文件, 还需要从配置文件中进一步加载配置信息
命令行参数可以参考这里: https://blog.csdn.net/zoutian007/article/details/7970160?locationNum=4&fps=1
4) 设置pjsua-core的回调函数:
代码中的app_config.cfg是pjsua-core中所定义的保存配置信息的结构体对象
app_config.cfg.cb.on_call_state = &on_call_state;
app_config.cfg.cb.on_call_media_state = &on_call_media_state;
app_config.cfg.cb.on_incoming_call = &on_incoming_call;
app_config.cfg.cb.on_call_tsx_state = &on_call_tsx_state;
app_config.cfg.cb.on_dtmf_digit = &call_on_dtmf_callback;
app_config.cfg.cb.on_call_redirected = &call_on_redirected;
app_config.cfg.cb.on_reg_state = &on_reg_state;
app_config.cfg.cb.on_incoming_subscribe = &on_incoming_subscribe;
app_config.cfg.cb.on_buddy_state = &on_buddy_state;
app_config.cfg.cb.on_buddy_evsub_state = &on_buddy_evsub_state;
app_config.cfg.cb.on_pager = &on_pager;
app_config.cfg.cb.on_typing = &on_typing;
app_config.cfg.cb.on_call_transfer_status = &on_call_transfer_status;
app_config.cfg.cb.on_call_replaced = &on_call_replaced;
app_config.cfg.cb.on_nat_detect = &on_nat_detect;
app_config.cfg.cb.on_mwi_info = &on_mwi_info;
app_config.cfg.cb.on_transport_state = &on_transport_state;
app_config.cfg.cb.on_ice_transport_error = &on_ice_transport_error;
app_config.cfg.cb.on_snd_dev_operation = &on_snd_dev_operation;
app_config.cfg.cb.on_call_media_event = &on_call_media_event;
5) 设置声卡延迟参数(不设置的话pjmedia会使用默认值):
app_config.media_cfg.snd_rec_latency = app_config.capture_lat;
app_config.media_cfg.snd_play_latency = app_config.playback_lat;
-------- 看了看源码源码中alsa声卡, 好像与alsa声卡的缓冲区总帧数(总的缓冲时间)有关, 如下:
tmp_buf_size = (rate / 1000) * param->input_latency_ms;
snd_pcm_hw_params_set_buffer_size_near (stream->ca_pcm, params, &tmp_buf_size);
6) 用户层的配置信息加载: (*app_cfg.on_config_init)(&app_config);
还记得在 “二) PJSUA的App程序框架” 中配置的on_config_init回调函数吗?
openwrt下可以在此回调函数中, 使用uci接口来修改应用程序的配置, 这样的好处是不需要修改pjsua程序原来的配置文件加载代码
7) 初始化pjsua-core: pjsua_init(&app_config.cfg, &app_config.log_cfg,&app_config.media_cfg);
入口参数携带了pjsua-core, log系统和media系统的初始化参数
8) 向sip_end_point注册app层模块: pjsip_endpt_register_module(pjsua_get_pjsip_endpt(), &mod_default_handler);
这样,我们的app层模块才能融入sip_end_point的modules模块链表中
当pjsua_handle_events(TIMEOUT)驱动sip_end_point工作时, 我们的模块才会起到作用
app层模块只实现了on_rx_request接口, 应用层在次对incoming 请求消息作出响应, 关键代码:
pjsip_endpt_send_response2(pjsua_get_pjsip_endpt(), rdata, tdata, NULL, NULL);
10) 初始化calls数组, 特别关注超时回调:
app_config.call_data[i].timer.id = PJSUA_INVALID_ID;
app_config.call_data[i].timer.cb = &call_timeout_callback;
11) 创建wav文件播放对象,并attach到会议桥的wav_port: ---你可以把它作为个性化铃声
pjsua_player_create(&app_config.wav_files[i], play_options, &wav_id); -------每个wave文件对应一个, 用于播放震铃、回铃音
app_config.call_data[i].timer.cb = &call_timeout_callback; 并为playfile对象安装on_playfile_done回调函数
12) 创建用于DTMF音频合成的波形发生器,并加入会议桥:
pjmedia_tonegen_create2(app_config.pool, &label,8000, 1, 160, 16,PJMEDIA_TONEGEN_LOOP, &tport);
pjsua_conf_add_port(app_config.pool, tport, &app_config.tone_slots[i]);
13) 创建电话录音pjmedia_port并连接到会议桥
pjsua_recorder_create(&app_config.rec_file, 0, NULL, 0, 0, &app_config.rec_id);
14) 呼出等待回铃音合成器创建并attach到会议桥
pjmedia_tonegen_create2(app_config.pool, &name, app_config.media_cfg.clock_rate, app_config.media_cfg.channel_count,
samples_per_frame, 16, PJMEDIA_TONEGEN_LOOP, &app_config.ringback_port);
pjsua_conf_add_port(app_config.pool, app_config.ringback_port, &app_config.ringback_slot)
15) 呼入震铃音合成器创建并attach到会议桥
pjmedia_tonegen_create2(app_config.pool, &name, app_config.media_cfg.clock_rate, app_config.media_cfg.channel_count,
samples_per_frame, 16, PJMEDIA_TONEGEN_LOOP, &app_config.ring_port);
pjsua_conf_add_port(app_config.pool, app_config.ringback_port, &app_config.ring_slot)
16) 信令消息传输层初始化
pjsua_transport_create(type,&app_config.udp_cfg,&transport_id);
17) 账户信息添加
pjsua_acc_add(&app_config.acc_cfg[i], PJ_TRUE, NULL);
18) 电话簿信息添加
pjsua_buddy_add(&app_config.buddy_cfg[i], NULL);
19) 编码器优先级设置: pjsua_codec_set_priority
20) 声卡设备指定(不采用默认声卡设备时): pjsua_set_snd_dev