Android 语音通话模块介绍(一)
PJSIP简介
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语言开发,且源码开放,在兼容性与效率上有明显优势,不仅体积小(完整的SIP封装也不过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用户为“[email protected]. 1.100”,“300@192.168.1.100”。本文中也使用这种方式来测试通信。
Pjsip实例分析
代码:simple_pjsua.c
/**
* 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();