???????????????????? Android 語音通話模塊介紹(一)
???? PJSIP是一個開放源代碼的SIP協議棧;官網地址(http://www.pjsip.org/),它支持多種SIP的擴展功能 。PJLIB, PJLIB-UTIL, PJMEDIA, and PJSIP are released under dualopen source GPL?oralternative?license.
PJSIP包括的內容
PJSIP - Open Source SIP Stack[開源的SIP協議棧]
PJMEDIA - Open Source Media Stack[開源的媒體棧]
PJNATH - Open Source NAT Traversal Helper Library[開源的NAT-T輔助庫]
PJLIB-UTIL - Auxiliary Library[輔助工具庫]
PJLIB - Ultra Portable Base Framework Library[基礎框架庫]
PJSIP的優點
a、高度的可移殖性?
只需簡單的編譯一次,它能夠在多種平臺上運行(所有Windows系統列, Windows Mobile, Linux,所有Unix系列,MacOS?X, RTEMS,Symbian?OS,等等)。
b、極小的內存需求?
官方宣稱編譯后的庫,完全實現SIP的功能只需要150K的內存空間,這使得PJISPi不僅僅是嵌入開發的理想平臺,并且實用于那些內存運行于極小內存平臺的應用,這也意味著極小的用戶下載時間。
c、高效的性能?
這意味著極小的CPU運算需求下能同時實現更多的通話。
d、支持多種SIP功能及擴展功能多種SIP功能和擴展功能,例如多人會話,事件驅動框架,會話控制(presence),即時信息,電話傳輸,等等在庫文件里得以實現。
e、豐富的文檔資料
?PJSIP開發人員提供了大量的極有價值的文檔資料供大家使用。
PJMEDIA簡介
?PJMEDIA是一個為PJSIP建立一個完整特性SIP用戶代理應用提供的補充庫,這些應用包括:softphones/hardphones,gateways or B2BUA.?使用PJSIP與PJMEDIA一起開發的應用,具備如下的特性:
a、高度的可移殖性?
與PJSIP/PJLIB一起,PJMEDIA可運行在許多平臺上,包括服務器、桌面、PDA系統,定制的硬件、PDA或移動電話。
b、多種功能?
會議橋接、多種編解碼器、丟包隱蔽/ PLC,音頻發生器,靜音探測器,聲學回聲消除/ AEC,RFC2833,RTP / RTCP協議棧,speex/iLBC/GSM/G.711編解碼器等。
c、高質量?
PJMEDIA支持頻率為16KHz、32Khz的編碼和解碼,事實上能支持任何音頻采樣率,可提供高質量的采樣轉換。PJMEDIA也可以容忍一定量的網絡或聲音設備的不穩定和一些數據包丟失。
d、很好的支持嵌入式/DSP占用內存小,靈活性好。該媒體組件被設計成可替換成相應功能的硬件。
e、較好的文檔資料
???PJMEDIA配備了相當不錯的文檔。
PJNATH簡介
PJNATH是一個新的庫,幫助應用程序進行NAT穿越。它實現了NAT穿越的最新規范:STUN、TURN和ICE。
PJNATH可以作為一個獨立庫,在您的軟件中使用,也可以使用PJSUA- LIB庫,該庫很好的與PJSIP,PJMEDIA和PJNATH整合在一起,使用起來比較簡單。
PJNATH的特點
a、STUNbis實現,
實現符合
RFC5389??標準。既提供需要使用的STUN網絡接口,又提供基于STUN但更高層次的框架,既TURN和ICE。
b、NAT類型檢測,
根據
RFC3489(STUN) ,在前端可以執行NAT類型檢測。該檢測方法不能對所有NAT類型進行穿越,但該信息可能仍然是有用,以便進行故障排除,已經被ICE整合,因此提供了該檢測方式。
c、TURN實現,
?TURN是一個中繼通信協議,通過使用中繼,并結合ICE,提供了高效的最低代價的通信路徑。PJNATH中TURN的實現,符合draft-ietf-behave-turn-14草案。
d、ICE實現,
?ICE是一個發現兩個端點之間的通信路徑協議。PJNATH中ICE的實現符合draft-ietf-mmusic-ice-19.txt草案
e、在未來,將實現更多的協議(如UPnP IGD和SOCKS5)。
PJLIB-UTIL簡介
PJLIB-UTIL是一個輔助庫,為PJMEDIA和PJSIP提供支持。這個庫中的一些功能/組件:占用內存小的XML解析,STUN客戶端庫,異步/緩存DNS解析,哈希/加密功能等 。
PJLIB簡介
q占用內存小,高性能,高可移植性的抽象庫和框架,被PJSIP和PJMEDIA使用。
PJLIB是PJLIB-UTIL、PJMEDIA和PJSIP唯一依賴的庫,因為它提供了完整的抽象,不僅僅是操作系統依賴的屬性,還包括LIBC的抽象,并提供了一些有用的數據結構。
PJLIB基礎框架庫提供的功能
?內存的處理、數據的存儲?
?.數據結構的(hash表、link表、二叉樹、等)
.caching和pool;緩沖池和內存池
?OS抽象?
?.線程、互斥、臨界區、鎖對象、事件對象
.定時器?
.pj_str_t字符串
?操作系統級別的函數抽象?
?.socket的抽象(tcp/udp)
.
文件的讀寫
?使用前的初始化,使用后的清理
pjsip的整體框架圖(圖1.1)
????? 如圖1.1展示了PJSIP框架的各模塊,可以看出從上到下,Application(pjsua)模塊可調用下層所有的模塊,也即是PJSUA處于最高層,其整合了下層模塊的全部功能以
這也就是為什么我們基本的操作都在PJSUA這里進行。是因為通過PJSUA,我們就能很方便的深入到其他模塊中。接著Application模塊往下就是PJSUA_lib層,要讓應用層(
PJSUA)能更好的調用,當然得有個封裝好的庫,這個庫就是PJSUA_LIB庫,稱為高層用戶代理庫,集合SIP,Media以及NAT穿越,所以也就有了往下的PJMEDIA-CODEC和PJMEDIA(負責SDP協商媒體編碼和媒體傳輸),PJNATH(解決NAT穿越),PJSUA-UA(提供SIP用戶代理庫),PJSIP-SIMPLE(實現presence和及時消息),PJSIP(核協議棧,SIP協議),PJLIB-UTIL(提供有用的工具函數)以及PJLIB(每個功能根據其所在的層次以及負責的功能提供豐富的接口)等模塊。
???????從實現上來看,最上層為應用層,該層將在Android SDK的框架內,采用Java語言來實現;第二層為JNI層,SIP協議棧有很多種實現,其中,采用C語言的SIP協議棧在效
、速度、系統占用方面有著超越其他庫(如Java協議棧)的優勢,因此,該方案將在第三層采用純C語言實現的PJSIP協議棧。為了讓Java應用層能調用協議棧層,在兩層之
間需要一個銜接的橋梁,這就是JNI層。最后一層是驅動層,這部分一般是由手機廠商來實現的,此處將不做重點介紹。
?SIP協議棧及UA
??? SIP協議棧直接關系到整個系統的質量與效率,許多開源項目基本上都是采用純C語言開發的PJSIP庫。該庫采用C語言開發,且源碼開放,在兼容性與效率上有明顯優勢,不僅體積?。ㄍ暾腟IP封裝也不過150 KB),同時還實現了一個內存池,使得安全系數與運行效率大為提高
PJSIP協議棧
????? ?PJSIP協議棧遵循標準的SIP協議,采用分層架構:SIP/SDP消息編碼解析層、傳輸管理層、SIP終端、事務層、會話層以及應用層等。由于SIP協議采用文本消息發送請求和響應,所以首先需要將SIP消息按照巴斯克范式(ABNF)編碼和解析,這就是SIP/SDP消息編碼解析層所完成的功能。傳輸管理層用來管理用戶代理與服務器之間的請求和相應;SIP終端是PJSIP中轉機制的實現,它主要負責管理各個SIP組建,例如像SIP終端實例注冊組件,分發消息到事務層、會話層及應用層,回傳處理結果,管理定時器、I/O隊列等;事務層通過狀態機機制管理SIP信令,每一次狀態機狀態的改變都將觸發回調函數;會話層負責會話的發起與響應,一般與應用層結合在一起,用于用戶交互,不同的平臺有不同的實現,這里主要使用Andriod的GUI來實現。
?? PJSIP是一個高度封裝的庫,實際上它是通過PJSUA子庫來實現應用的。一個完整的PJSUA生命周期,首先需要初始化,通過函數init()來實現。在這個函數中,將創建代理、初始化變量和堆棧,以及創建一個UDP傳輸并在最后啟動代理;第二步將為UA添加用戶,如果需要的話,還要向服務器注冊用戶;當用戶添加成功后,此時可以建立一個呼叫連接,發起會話;當會話連接成功后,就可以使用SRTP協議實時傳輸加密后的數據,進行通話。最后的過程是掛起或銷毀呼叫。
UA原理
UA(User Agency)是協議棧的具體實現,PJSIP通過封裝了的PJSUA來實現,在這一點上,大部分的SIP庫都大同小異,在此將介紹UA的工作原理。
一個典型的UA包含UAC(User Agency Client)和UAS(User Agency Server)兩部分。會話由UAC發起。當呼叫發起時,UAC將首先發送“IN-VITE”消息給SIP代理服務器,服務器收到“INVITE”消息后將返回一個應答“200 OK”,并回答“ACK”進行確認,同時通知主叫用戶(即會話發起用戶)上線通話。如果主叫端(用戶端)主動結束會話,UAC將返回“BYE”消息,同時通知服務器;如果用戶端收到服務器傳來的“BY-E”消息,回答“200”,并結束會話。
服務器端,
UAS收到UAC(用戶端)發來的“INVITE”消息,首先從消息中提取出主、被叫對象,然后檢查當前是否有空閑信道,若沒有則返回“486 BUSY HERE”(即系統忙)消息;接著將檢查被叫用戶是否在服務區,如果被叫對象不在服務范圍,則返回“404 NOT FOUND”(即用戶不在服務區);若被叫用戶成功上線,則返回“200 OK”,同時準備開始會話。
SIP協議棧一般使用SIP統一資源定位符(URL)來標識,它根據URL來尋址,如集群用戶“200”,“300”分別對應SIP用戶為“200@192.168. 1.100”,“300@192.168.1.100”。本文中也使用這種方式來測試通信。
Pjsip實例分析
/**
* simple_pjsua.c
*
* This is a very simple but fully featured SIP user agent, with the
* following capabilities:
*? - SIP registration
*? - Making and receiving call
*? - Audio/media to sound device.
*
* Usage:
*? - To make outgoing call, start simple_pjsua with the URL of remote
*? ? destination to contact.
*? ? E.g.:
* simpleua sip:user@remote
*
*? - Incoming calls will automatically be answered with 200.
*
* This program will quit once it has completed a single call.
*/
#include
#define THIS_FILE"APP"
#define SIP_DOMAIN"example.com"
#define SIP_USER"alice"
#define SIP_PASSWD"secret"
/* Callback called by the library upon receiving incoming call */
staticvoidon_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 */
staticvoidon_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 */
staticvoidon_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 */
staticvoiderror_exit(constchar*title, pj_status_t status)
{
? ? pjsua_perror(THIS_FILE, title, status);
? ? pjsua_destroy();
exit(1);
}
/*
* main()
*
* argv[1] may contain URL to call.
*/
intmain(intargc,char*argv[])
{
? ? pjsua_acc_id acc_id;
? ? pj_status_t status;
//? 創建PJSIP
/* Create pjsua first! */
? ? status = pjsua_create();
if(status != PJ_SUCCESS) error_exit("Error in pjsua_create()", status);
//? 校驗被叫SIP地址是否正確
/* If argument is specified, it's got to be a valid SIP URL */
if(argc >1) {
status = pjsua_verify_url(argv[1]);
if(status != PJ_SUCCESS) error_exit("Invalid URL in argv", status);
? ? }
//? ? 初始化PJSUA,設置回調函數
/* Init pjsua */
? ? {
pjsua_config cfg;
pjsua_logging_config log_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;
status = pjsua_init(&cfg, &log_cfg, NULL);
if(status != PJ_SUCCESS) error_exit("Error in pjsua_init()", status);
? ? }
//? ? 創建PJSIP的傳輸端口
/* Add UDP transport. */
? ? {
pjsua_transport_config cfg;
pjsua_transport_config_default(&cfg);
cfg.port =5060;
status = pjsua_transport_create(PJSIP_TRANSPORT_UDP, &cfg, NULL);
if(status != PJ_SUCCESS) error_exit("Error creating transport", status);
? ? }
//? ? 啟動PJSIP
/* Initialization is done, now start pjsua */
? ? status = pjsua_start();
if(status != PJ_SUCCESS) error_exit("Error starting pjsua", status);
//? ? 設置SIP用戶帳號
/* 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(SIP_DOMAIN);
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);
? ? }
//? ? 發起一個呼叫
/* If URL is specified, make call to the URL. */
if(argc >1) {
pj_str_t uri = pj_str(argv[1]);
status = pjsua_call_make_call(acc_id, &uri,0, NULL, NULL, NULL);
if(status != PJ_SUCCESS) error_exit("Error making call", status);
? ? }
//? ? 循環等待
/* Wait until user press "q" to quit. */
for(;;) {
charoption[10];
puts("Press 'h' to hangup all calls, 'q' to quit");
if(fgets(option, sizeof(option), stdin) == NULL) {
puts("EOF while reading stdin, will quit now..");
break;
}
if(option[0] =='q')
break;
if(option[0] =='h')
? ? pjsua_call_hangup_all();
? ? }
/* Destroy pjsua */
? ? pjsua_destroy();
return0;
}
simple_pjsua.c的main函數主要流程:
這里可以分析一下它的代碼及流程圖:
1、一開始是回調使用的函數,例如on_incoming_call當來電話的時候,pjsip會自動去調用你寫的這個函數,前提是你在初始化pjsua的時候設置了on_incoming_call?= &on_incoming_call,
2、error_exit退出應用所需要的操作
3、main函數:
?(1)pjsua_create()創建pjsua的第一步,如果是要打電話要確認URL是否是正確的pjsua_verify_url
?(2)初始化pjsua,pjsua_config_default(&cfg)來初始化配置,然后設置一些回調函數,設置日志,最后初始化pjsua_init(&cfg, &log_cfg, NULL);
?(3)創建UDP的傳輸,設置端口號
?(4)接下來就是啟動pjsua,通過pjsua_start();
?(5)創建賬戶,這個是重點所在,pjsua_acc_config_default初始化配置,然后設置相關的內容,id對應這url,realm是服務器的域名,還有密碼和用戶名,最后調用?pjsua_acc_add(&cfg, PJ_TRUE, &acc_id);來實現帳號的注冊。
4、打電話,上面也提到過,你打電話的話需要驗證URL是否正確的?pjsua_verify_url,然后調用pjsua_call_make_call來打電話。
5、掛電話,調用?pjsua_call_hangup_all();
6、最后銷毀,pjsua_destroy();