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,提供了高效的最低代价的通信路径。PJNATHTURN的实现,符合draft-ietf-behave-turn-14草案。

d、ICE 实现,

ICE是一个发现两个端点之间的通信路径协议。PJNATHICE的实现符合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)

Android 语音通话模块介绍(一) 开源的SIP协议栈_第1张图片

如图1.1展示了PJSIP框架的各模块,可以看出从上到下,Applicationpjsua)模块可调用下层所有的模块,也即是PJSUA处于最高层,其整合了下层模块的全部功能以

这也就是为什么我们基本的操作都在PJSUA这里进行。是因为通过PJSUA,我们就能很方便的深入到其他模块中。接着Application模块往下就是PJSUA_lib层,要让应用层(

PJSUA)能更好的调用,当然得有个封装好的库,这个库就是PJSUA_LIB库,称为高层用户代理库,集合SIPMedia以及NAT穿越,所以也就有了往下的PJMEDIA-CODECPJMEDIA(负责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协议,采用分层架构:SIPSDP消息编码解析层、传输管理层、SIP终端、事务层、会话层以及应用层等。由于SIP协议采用文本消息发送请求和响应,所以首先需要将SIP消息按照巴斯克范式(ABNF)编码和解析,这就是SIPSDP消息编码解析层所完成的功能。传输管理层用来管理用户代理与服务器之间的请求和相应;SIP终端是PJSIP中转机制的实现,它主要负责管理各个SIP组建,例如像SIP终端实例注册组件,分发消息到事务层、会话层及应用层,回传处理结果,管理定时器、IO队列等;事务层通过状态机机制管理SIP信令,每一次状态机状态的改变都将触发回调函数;会话层负责会话的发起与响应,一般与应用层结合在一起,用于用户交互,不同的平台有不同的实现,这里主要使用AndriodGUI来实现。


PJSIP是一个高度封装的库,实际上它是通过PJSUA子库来实现应用的。一个完整的PJSUA生命周期,首先需要初始化,通过函数init()来实现。在这个函数中,将创建代理、初始化变量和堆栈,以及创建一个UDP传输并在最后启动代理;第二步将为UA添加用户,如果需要的话,还要向服务器注册用户;当用户添加成功后,此时可以建立一个呼叫连接,发起会话;当会话连接成功后,就可以使用SRTP协议实时传输加密后的数据,进行通话。最后的过程是挂起或销毁呼叫。

UA原理

UAUser Agency)是协议栈的具体实现,PJSIP通过封装了的PJSUA来实现,在这一点上,大部分的SIP库都大同小异,在此将介绍UA的工作原理。

一个典型的UA包含UACUser Agency Client)和UASUser 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@1921681100”。本文中也使用这种方式来测试通信。

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.cmain函数主要流程:

Android 语音通话模块介绍(一) 开源的SIP协议栈_第2张图片

这里可以分析一下它的代码及流程图:

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 语音通话模块介绍(一) 开源的SIP协议栈)