近期项目中使用了sip协议进行音视频通话,百度的介绍是:
PJSIP同时支持语音、视频、状态呈现和即时通讯。PJSIP具有非常完善的文档,对开发者非常友好。
因特网电话(IP电话)正在向一种正式的商业电话模式演进,
SIP就是用来确保这种演进实现而需要的NGN(下一代网络)系列协议中重要的一员。支持H.264协议。
官方例程还是非常不错的,但是网上讲到使用官方例程的资料相对来讲还是比较少,并且对新手不太友好;
因为我是从头开始使用,就记录一下在Linux下从头开始编译到使用官方例程的过程;
#官方下载
http://www.pjsip.org/download.htm
我是使用了最新的pjproject-2.6版本进行编译;
如果需要早期的版本,可以通过svn直接check出来
http://svn.pjsip.org/repos/pjproject
#编译工程
这个版本是自带有Makefile文件并且使用configure不会自定生成Makefile
所以在下载后使用的时候还是有点惯性的删掉了Makefile想configure重新生成的时候失败了,其实通过configure脚本可以发现,其实2.6版本的configure是通过生成build.mak和config.status来进行相关的编译属性以及交叉编译配置的;
首先是根目录下:$ ./configure & make 并没能成功;
出现错误:
../../yuv/source/row_common.cc: In function ‘void libyuv::YuvPixel(uint8, uint8, uint8, uint8*, uint8*, uint8*,
const libyuv::YuvConstants*)’:
../../yuv/source/row_common.cc:1256: error: invalid types ‘const signed char __vector__[int]’ for array subscript
../../yuv/source/row_common.cc:1257: error: invalid types ‘const signed char __vector__[int]’ for array subscript
../../yuv/source/row_common.cc:1258: error: invalid types ‘const signed char __vector__[int]’ for array subscript
../../yuv/source/row_common.cc:1259: error: invalid types ‘const signed char __vector__[int]’ for array subscript
../../yuv/source/row_common.cc:1260: error: invalid types ‘const short int __vector__[int]’ for array subscript
../../yuv/source/row_common.cc:1261: error: invalid types ‘const short int __vector__[int]’ for array subscript
../../yuv/source/row_common.cc:1262: error: invalid types ‘const short int __vector__[int]’ for array subscript
../../yuv/source/row_common.cc:1263: error: invalid types ‘const short int __vector__[int]’ for array subscript
make[3]: *** [output/libyuv-x86_64-unknown-linux-gnu/row_common.o] Error 1
make[3]: Leaving directory `/home/zzy/pjproject-2.6/pjproject-2.6/third_party/build/yuv'
make[2]: *** [libyuv-x86_64-unknown-linux-gnu.a] Error 2
make[2]: Leaving directory `/home/zzy/pjproject-2.6/pjproject-2.6/third_party/build/yuv'
make[1]: *** [all] Error 1
make[1]: Leaving directory `/home/zzy/pjproject-2.6/pjproject-2.6/third_party/build'
make: *** [all] Error 1
/pjproject-2.6/third_party/lib/libwebrtc-x86_64-unknown-linux-gnu.a(cpu_features.o):(.eh_frame+0x12):
undefined reference to `__gxx_personality_v0'
collect2: ld returned 1 exit status
make[2]: *** [../bin/pjmedia-test-x86_64-unknown-linux-gnu] Error 1
这个报的是libyuv和libwebrtc库的错误,所以直接去掉相关的库的配置
$ ./configure --disable-libyuv --disable-libwebrtc & make
编译通过;
编译库的过程也是充满艰辛,网上搜索出来的结果基本上是清一色的:
# ./configure
# make dep
还在得自己慢慢看 ./configure -help
找到
–disable-libyuv
–disable-libwebrtc
libyuv是Google开源的实现各种YUV与RGB之间相互转换、旋转、缩放的库;
YUV主要用于优化彩色视频信号的传输,使其向后相容老式黑白电视。
与RGB视频信号传输相比,它最大的优点在于只需占用极少的频宽(RGB要求三个独立的视频信号同时传输)
WebRTC(Web Real-Time Communication)项目的最终目的主要是
让Web开发者能够基于浏览器(Chrome\FireFox…)轻易快捷开发出丰富的实时多媒体应用
接着就把生成的库以及头文件进行安装方便拷贝出来使用:
$ ./configure --disable-libyuv --disable-libwebrtc --prefix=$PWD/_install
$ make & make install
~/_install$ ls
include lib
~/_install/lib$ ls
libg7221codec-x86_64-unknown-linux-gnu.a
libpjmedia-codec-x86_64-unknown-linux-gnu.a
libpjsip-ua-x86_64-unknown-linux-gnu.a
libresample-x86_64-unknown-linux-gnu.a
libgsmcodec-x86_64-unknown-linux-gnu.a
libpjmedia-videodev-x86_64-unknown-linux-gnu.a
libpjsip-x86_64-unknown-linux-gnu.a
libspeex-x86_64-unknown-linux-gnu.a
libilbccodec-x86_64-unknown-linux-gnu.a
libpjmedia-x86_64-unknown-linux-gnu.a
libpjsua2-x86_64-unknown-linux-gnu.a
libsrtp-x86_64-unknown-linux-gnu.a
libpjlib-util-x86_64-unknown-linux-gnu.a
libpjnath-x86_64-unknown-linux-gnu.a
ibpjsua-x86_64-unknown-linux-gnu.a
pkgconfig
libpjmedia-audiodev-x86_64-unknown-linux-gnu.a
libpjsip-simple-x86_64-unknown-linux-gnu.a
libpj-x86_64-unknown-linux-gnu.a
include下面就比较多文件了,有13个文件夹…
交叉编译:
./configure --disable-libyuv --disable-libwebrtc --prefix=$PWD/_install --host=arm-none-linux-gnueabi
#工程目录介绍
pjproject-2.6的工程目录:
主要介绍:
pjlib,pjlib-util,pjmedia,pjnath,pjsip,pjsip-apps 这几个目录结构一致整齐如下
|----bin
|----build
|----docs
|----include
|----lib
|----src
build目录下的Makefile文件,可以把在src下面的所有的c/cpp文件编译成可执行的二进制文件,输出到bin目录下;
开发者可以直接在bin目录下运行各个可执行文件查看到相关接口的使用;
#测试官方Linux例程
这里使用的是pjsip-apps目录的官方例程了解一下2个设备之间的点对点通信:
进入build目录下$ make
则会把 pjsip-apps/pjsip-apps/src 的源代码编译出来,这里主要是pjsip-apps/pjsip-apps/src/samples/
这里下面的所有c/cpp文件将会被编译到pjsip-apps/bin/samples/x86_64-unknown-linux-gnu下面
调试simple_pjsua(源代码simple_pjsua.c)
这里需要有两台Ubuntu设备,开发板或者两台虚拟机 分别运行程序 ;
Ubuntu1(192.168.1.2) $ ./simple_pjsua
Ubuntu2(192.168.1.3) $ ./simple_pjsua sip:alice@192.168.1.2:5060
这里的Ubuntu2(192.168.1.3)运行程序带有参数,表示呼叫处于Ubuntu1(192.168.1.2)进程中的用户alice,当然这里同一个程序所以两个系统上都是使用了同一个用户名alice;
可以看到Ubuntu1(192.168.1.2)启动后,等待Ubuntu2(192.168.1.3)的呼入;
在Ubuntu1(192.168.1.2)可以看到来自Ubuntu2(192.168.1.3)的呼叫以及回复200
17:51:58.950 APP ..Incoming call from <sip:alice@example.com>!!
17:51:58.958 APP .........Call 0 state=CONNECTING
17:51:58.968 APP ...Call 0 state=CONFIRMED
17:51:58.968 pjsua_core.c .RX 373 bytes Request msg BYE/cseq=13927 (rdata0x7f6790002858) from UDP 192.168.1.3:5060:
在Ubuntu2(192.168.1.3)可以看到来自Ubuntu1(192.168.1.2)的回复
17:51:29.411 APP .......Call 0 state=CALLING
17:51:29.429 APP ......Call 0 state=CONNECTING
17:51:29.431 APP ......Call 0 state=CONFIRMED
17:51:29.432 pjsua_core.c .........TX 373 bytes Request msg BYE/cseq=13927 (tdta0x27ace80) to UDP 192.168.1.2:5060:
在Ubuntu2(192.168.1.3)上输入q主动挂断
Press 'h' to hangup all calls, 'q' to quit
q
18:22:17.824 pjsua_core.c !Shutting down, flags=0...
18:22:17.825 pjsua_core.c PJSUA state changed: RUNNING --> CLOSING
18:22:17.835 pjsua_call.c !.Hangup all calls..
18:22:17.836 pjsua_call.c ..Call 0 hanging up: code=0..
18:22:17.839 pjsua_core.c ......TX 372 bytes Request msg BYE/cseq=9320 (tdta0x8fcc10) to UDP 192.168.1.2:5060:
BYE sip:alice@192.168.1.2:5060;ob SIP/2.0
Via: SIP/2.0/UDP 192.168.1.3:5060;rport;branch=z9hG4bKPjD.uTS.HSdsoXMVbObmK052zwAVp4k-ti
Max-Forwards: 70
From: sip:alice@example.com;tag=vWTPJAwlV8.fvlOQXE8wvGVqiTyu9PWO
To: sip:alice@192.168.1.2;tag=ZpoAP-tusbemk5oGnF-bsHEQc1pGtw3A
Call-ID: ZgBtHs1h0wqWRCADyBlkIRupOlA-6spK
CSeq: 9320 BYE
Content-Length: 0
在Ubuntu1(192.168.1.2)看到通话被Ubuntu2(192.168.1.3)结束
--end msg--
18:22:47.046 pjsua_core.c .RX 372 bytes Request msg BYE/cseq=9320 (rdata0x7f0d40002858) from UDP 192.168.1.3:5060:
BYE sip:alice@192.168.1.2:5060;ob SIP/2.0
Via: SIP/2.0/UDP 192.168.1.3:5060;rport;branch=z9hG4bKPjD.uTS.HSdsoXMVbObmK052zwAVp4k-ti
Max-Forwards: 70
From: sip:alice@example.com;tag=vWTPJAwlV8.fvlOQXE8wvGVqiTyu9PWO
To: sip:alice@192.168.1.2;tag=ZpoAP-tusbemk5oGnF-bsHEQc1pGtw3A
Call-ID: ZgBtHs1h0wqWRCADyBlkIRupOlA-6spK
CSeq: 9320 BYE
Content-Length: 0
例程中使用的主要的两个接口
pjsua_acc_add : 传入本机URL(sip:alice@example.com)进行注册
pjsua_call_make_call : 传入呼叫方的URL(sip:alice@192.168.1.2:5060)进行呼叫
附官方例程说明:
/**
* simple_pjsua.c
*
* 这是一个非常简单但功能齐全的SIP用户代理,
* 具有以下功能
* - SIP 注册
* - 发起一个呼叫和接听一个呼叫
* - 音频媒体输出到声音设备上.
*
* 使用:
* - 发起一个新呼叫,启动应用程序simple_pjsua,传入参数 对方的URL进行连接
* 例如:
* ./simple_pjsua sip:user@remote
*
* - 接收到呼入将自动回复200表示收到呼叫
*
* 本程序在完成一次简单的呼叫后,就马上退出
*/
#include <pjsua-lib/pjsua.h>
#define THIS_FILE "APP"
#define SIP_DOMAIN "example.com"
#define SIP_USER "alice"
#define SIP_PASSWD "secret"
/* 接收到新的呼入时,由底层库回调到上层 */
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));
/* 自动回复呼入(200/OK) */
pjsua_call_answer(call_id, 200, NULL, NULL);
}
/* 由底层库回调到上层:底层已经改变了呼叫状态(接听,挂断,...) */
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));
}
/* 由底层库回调到上层: 媒体(音视频)状态改变 */
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) {
// 媒体连接启动时,连接到声音设备.
pjsua_conf_connect(ci.conf_slot, 0);
pjsua_conf_connect(0, ci.conf_slot);
}
}
/* 输出错误信息, 并退出应用程序 */
static void error_exit(const char *title, pj_status_t status)
{
pjsua_perror(THIS_FILE, title, status);
pjsua_destroy();
exit(1);
}
/*
* main()
*
* argv[1] 作为呼出的参数URL
*/
int main(int argc, char *argv[])
{
pjsua_acc_id acc_id;
pj_status_t status;
/* 首先创建一个PJSUA ! */
status = pjsua_create();
if (status != PJ_SUCCESS) error_exit("Error in pjsua_create()", status);
/* 如果带有参数(URL)用于呼出的,检测呼入的URL是否有效 */
if (argc > 1) {
status = pjsua_verify_url(argv[1]);
if (status != PJ_SUCCESS) error_exit("Invalid URL in argv", status);
}
/* 初始化 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);
}
/* 加入 UDP 传输 */
{
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);
}
/* 初始化工作完成后,启动 PJSUA */
status = pjsua_start();
if (status != PJ_SUCCESS) error_exit("Error starting pjsua", status);
/* 创建SIP账号,并注册到SIP服务器上 */
{
pjsua_acc_config cfg;
pjsua_acc_config_default(&cfg);
cfg.id = pj_str("sip:" SIP_USER "@" SIP_DOMAIN); // sip:alice@example.com
cfg.reg_uri = pj_str("sip:" SIP_DOMAIN); // sip:example.com
cfg.cred_count = 1;
cfg.cred_info[0].realm = pj_str(SIP_DOMAIN);//example.com
cfg.cred_info[0].scheme = pj_str("digest");
cfg.cred_info[0].username = pj_str(SIP_USER); //alice
cfg.cred_info[0].data_type = PJSIP_CRED_DATA_PLAIN_PASSWD;
cfg.cred_info[0].data = pj_str(SIP_PASSWD); //secret
status = pjsua_acc_add(&cfg, PJ_TRUE, &acc_id);
if (status != PJ_SUCCESS) {
error_exit("Error adding account", status);
PJ_LOG(3,(THIS_FILE, "Error adding account %s", cfg.reg_uri));
}
}
/* 如果有指定的要主动呼出URL,则使用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); // 呼出失败
} else {
PJ_LOG(3,(THIS_FILE, "Success making call %s", argv[1])); //呼出成功
}
}
/* 循环等待,直到用户按下 "q" 键,则退出程序 */
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;
}