Android(安卓)语音通话模块介绍(一) 开源的SIP协议栈
Android 语音通话模块介绍(一)
PJSIP简介
PJSIP 是一个开放源代码的 SIP 协议栈;官网地址( http://www.pjsip.org/ ),它支持多种 SIP 的扩展功能 。 PJLIB, PJLIB-UTIL, PJMEDIA, and PJSIP are released under dual open source GPL or alternative 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用户为“200@192.168. 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 */static 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);}/* Callback called by the library when call's state has changed */static 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));}/* Callback called by the library when call's media state has changed */static 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); }}/* Display error and exit application */static void error_exit(const char *title, pj_status_t status){ pjsua_perror(THIS_FILE, title, status); pjsua_destroy(); exit(1);}/* * main() * * argv[1] may contain URL to call. */int main(int argc, 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 (;;) {char option[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(); return 0;}
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 ();
更多相关文章
- Android(安卓)Launcher3 禁止用户拖动图标创建文件夹,控制拖动图
- Android(安卓)activity exported属性理解
- i.MX6 安装交叉编译器
- Android防止用户快速点击
- Android(安卓)6.0关于权限的问题
- android:nextFocus 属性介绍
- android telephony phone application
- android 自定义控件之AutoCompleteTextView邮箱后缀自动补全
- Android- Context理解