pjsip學習筆記13 -- pjsua的啟動過程程式碼分析
阿新 • • 發佈:2019-01-25
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