admin管理员组文章数量:1536095
2024年7月19日发(作者:)
基于SIP协议的音视频的软件开发
前言
在VoIP中,我们知道SIP协议有很大的用处。这里,我们就针对SIP协议,
来看看SIP软电话开发环境的建立都需要知道哪方面的知识。那么首先我们来看
看这个开发平台的条件。要在windows或者linux平台下开发基于SIP软电话,
需要以下软件。
服务器端软件: 注册多个客户端到服务器上,可以进行通话测试
SIP客户端软件:主要用于测试,可以对别人已经完成的客户端进行抓包,
以比对自己程序的发包数据
SIP协议栈:基于某个现成的SIP协议栈来开发会加快开发进度 RTP栈:传
输语言或者视频数据的协议栈
抓包测试工具:调试网络程序最有效的办法
以下介绍这些软件主要以开源软件为主
一 服务器端软件
1. Asterisk:Linux系统下开源的IPPBX,功能强大稳定,主要用c语言开
发。配置稍麻烦。
2. Vocal:Linux系统下开源的SIP服务器端。可以作为IPPBX也可以作为
运营系统。很多voip虚拟运营商都用这个作为自己的运营系统。提供BS结构的
管理界面。
3. YATE:跨平台(Linux,Windows)的开源SIP服务器端。在windows下安
装非常简单。其他方面没有进行过测试。
4. SER:Linux平台下重量级的SIP服务器断。功能比较丰富,也是很多voip
虚拟运营商的系统选择。不过据说配置比较麻烦,具体没有试过。
5. sipX:Linux平台下的SIP服务器。这个好像不能作为客户端再次注册
到其他SIP服务器上。提供BS结构的管理界面。
二 SIP客户端软件
1. Windows Messenger 5.1:微软出的SIP客户端,操作方便。
2. YATE Client:跨平台(Linux,Windows)的开源SIP客户端软件。安装方
便,功能简单。
3. xten:windows平台下的SIP软电话。功能齐全,使用方便。
三 SIP协议栈
1. osip:跨平台的开源SIP协议栈。用c语言实现,体积小。
2. exosip:对osip进行封装,使其方便SIP客户端软件开发。同样开源跨
平台。
3. sipXtackLib:由SIPfoundry提供的开源跨平台的SIP协议栈,功能齐
全。C++开发,已经被用于开发数个商业SIP终端。
四 RTP栈
1. JRTPLIB: 开源的跨平台rtp栈,用C++语言开发,使用方便。
五 抓包测试工具
1. Wireshark:非常有名的开源跨平台网络抓包工具,以前叫做Ethereal。
内容:
SIP是一个会话协议,很多大企业都在用,通信行业的一个标准(不过从个
人角度不喜欢这个协议格式,罗嗦).
其业务逻辑比较,简单地来说如下:
User Agent Server ------------------REGISTER----------->
<----------401(407) Unauthorized--
----------REG(带上用户口令)----------->
---------------200 OK 1 Bindings---
双方交互几次,注册成功。
因为Sip 通信一般采用UDP,所以有个保活的问题,一般每隔两三分钟再向
server注册一下。server也可能每隔一两分钟向客户发Unauthorized,让客户
再刷新一下登录。
登录成功后,某个客户端向另一个客户端发起呼叫,通过服务器中转命令。
简单来讲,这个和IM的原理是一样的。对方同意接收呼叫后,把媒体端口通知
给
server 及对方。到了这里,有IM开发经验的人,自然就知道下一步怎么做
了:如果想P2P直连的话,就先穿透NAT打洞,否则就通过Server中转。
很明显,SIP会话和现有的IM类似,但效率或效果上来讲差的很多,比如
登录保活, 还是同名用户同时登录等等,都处理的不够好。不过SIP是电信协
议,最初是用在VOIP和可视电话上,环境比IM简单地多,所以这个协议足够用
了,估计名字中的S也是因为这个原因。
sip呼叫成功,建立连接之后,媒体传输(音视频)是通过RTP协议进行的。
简单地说,采集到声音和视频,先按指定编码方面编码,比如音频编码成 g711,
视频编码成h263,然后根据RFC相关协议加上包头用UDP向指定发送出去。对方
收到后先解包,再解码,然后播放。
如果想了解SIP的详细工作流程,可以这样:
1 找一个外网的sip server (如果有经验,可以用yate2,或Trixbox等自
己搭建)
2 安装x-lite ( 很不错的sip软电话客户端,如果安装eyeBeam更好,带
视频) 3 安装ethereal和WinPcap (抓包工具)
然后,用x-lite拨打其他的客户端或SIP话机,用抓包工具抓出相关的数
据包,先看流程,然后再看包结构。
后面附上一个介绍SIP的PPT,写的非常好,可能是台湾方面出品,以前收
集的。是个.rar文件,因为这里只能上传图片,所以改名为.jpg再上传,下载
后把.jpg去掉解压就可以了。
PPT写的非常好,用心看,很快就能了解SIP的工作流程。
下一步,就是自己动手实现SIP VOIP系统了。
如果商用的话,server 采用Trixbox,也可以仔细研究一下 Asterisk。客
户端就用x-lite好了。
做为程序员,第一反应就是怎么样自己动手写一个客户端,甚至服务器。好
在开源产品众多,写一个并不难。
经过几天的调试,发现几个协议栈做的不错:
1 SIP协议栈:
a osip+exosip (建立客户端及通信非常简单,质量也好),
b reSIProcate (全面,有server端例子,综合调试方便)。
c 其他的还用过一个pjsip,不过它与音视频结合成一个库之后, 音频质
量不好。但是比较小巧, 听说台湾很 多嵌入设备采用。
2 RTP协议栈:
a Linphone采用的是oRTP,音视频部分采用的是 MediaStreamer2 b JRtpLib,
结合emiplib的音视频处理。
c ffmpeg,ffmpeg本来是专门处理音视频编解码的,不过也提供了
rtp,rtsp,最近好象也增加了rtmp协议的支持。顺便一提,MS2和 emiplib
底层也采用了ffmpeg。只要和音视频打交道,并且质量很不错的产品,都离不
开它,比如mplayer,ffdshow。顺便BS一下 kmplayer,上了ffmpeg黑名单。
这里面着重提到的是jrtplib,之前误解为它只是按RTP传输数据包,以前
写过的几个文章,都是在RTP包之后,自己再封装了一下,当然,做为自己用 的
音视频聊天程序,这样是没问题的。但用在SIP及其他VOIP产品上,要考虑互
通,就要严格搂RTP协议来执行了。
了解了几个开源的东西,下面自己动手建一个简单的SIP环境:
1 对Linux比较熟的人, 在CentOS上安装Asterisk,客户端采用Linphone,
自己研究吧。
2 象我这样只要在Linux下用点g++的,如果想针对VOIP快速学习的话,
服务器安装yate2,客户端随便拿哪个都行。
3 如果自己想定制sip server,干脆一步到位,下载reSIProcate,用vc2005
编译,一次通过。运行时提示缺少几个dll,google一下很快都找到了,然后运
行repro,做为server先临时用着,反正是学习。
客户端呢,网上流行一个很不错的,名字叫Youtoo,下载,简单编译后可以
做为一个语音的客户端使用。
然后,PC上安装几个虚拟机,一个运行server,一个运行x-lite(做为一
个参考的标准),主要上运行我们自己写的客户端进行测试。如果要调试server,
就是主机上运行repro,虚拟上分别运行两个x-lite。 环境搭建立好了,下一步
就开始调试。 根据这几天的实践,找出了一个最优的配置:
1 sip server采用Trixbox,如果对Linux很熟建议直接用Asterisk.
2 客户端如果直接使用,建议ekiga. 顺便说一下几个客户端使用的感受:
1 linphone:好象名气不小,不过,最新版3.1.2安装后启动就崩溃。我安
装的是普通的XP-SP3,电脑公司特别版。一般软件运行没问题。如果在这个平
台上都崩溃,真不知道说什么好。
后来再试3.1.1,这个可以启动,运行能看到视频图像。不过奇怪的是,与
视频电话连接上视频窗口反而隐藏起来。结束通话,又显示出来。搞不懂它的视
频功能是做什么用的。另外显示本地视频只支持QCIF。
2 eyeBeam :名气更大,使用起来也不错,这个不错不包含视频功能。如果
启动了视频的话,只显示第一帧图,摄像头怎么转它也不动。另外,主动连视频
电话时,不能启动视频功能。视频电话呼叫它才能启动。启动后,"Start Video"
点一下又灰住,完全不能用。
3 Wengo不错,现在改名叫Quate了,连接摄像头非常快,本地预览也正常。
不过,解码有问题,看 到的是一堆绿色图块,不知道是它解不了码,还是弄几
个颜色块在那边骗人玩。
4 yate好象不支持视频,不过声音倒是不错。
5 回头再说ekiga,边续试用了上面几个软件,以为我用的视频电话硬件有
问题,但用ekiga连接后,双方的视频都正常。 上面是从视频效果角度出发来
评测的,如果不使用视频的话都差不多。
搭建好环境,测试通过,熟悉协议之后, 就是自己做一个这样的平台了。 服
务器想都不用想,直接用Tixbox,重头写不现实。
至于客户端,一般的程序架构应该如下:
一 协议部分:
主要处理sip的注册,呼叫,接收,挂机等功能,所有的协议都差不多,随
便选一个就行。
二 媒体传输,这部分比较复杂 :
1 音视频采集
2 音视频编码
3 音视频编码后组RTP
4 RTP/RTCP发送
5 RTP/RTCP接收
6 从RTP解包还原成编码后的音视频
7 音视频解码
8 音视频播放
一般如果分配任务,快速做一个客户端,首先想到的就是找一个开源,编译
出来再修改。 不过,试了几个,极度痛苦,分别说一下。 1 Linphone:
这个产品只能算一般,不过用到的lib非常不错,exosip+osip为sip命令
服 务,ortp+mediastreamer2为流媒体服务。不过,编译真是麻烦,别的不说,
光
mediastreamer2就用到了 ffmpeg,gsm,ortp,srtp,openssl,speex, theora
等,稀里湖涂足足花了大半天时间把所有这些都编译好,然后编译时提
示几个链接出错。因为我看到网上几个文章说明是用 vc2005轻松编译出来的,
我也用的vc2005。估计用mingw会简单一些。不过,已经耗了近一天的时间,
感觉不爽,放弃。估计是linphone 估计搞的复杂,好让antisip卖钱。 2 ekiga
这个需要ptlib,第一感觉这东西很麻烦,不过编译时出奇的顺利(关键是官
方提供的资料详 细,网友写的文章也详细)。然后编译opal,也很顺利。(只是
占用机器比较厉害,P4 2.6的占CPU极严重。不过,用的机器是联想的超薄机
箱那种,不排除官方弄个很烂的CPU冒充。因为换到另一个P4 3G,速度快上两
三倍)。 其实,编译好opal,基本就可以了,它带了很不错的例子,拨打电话
接听都不错。
最后编译ekiga时,需要交叉编译,直接放弃掉,有那时间不如好好研究
opal了。
3 其他
编译了一下emiplib,这个库写的真不错。虽然封装的比较深,不过调用时,
可以选择比较靠上的类来调用,有点类似ACE。只是视频格式少了一点。
回头再看上面的一般结构,SIP部分不用操心,随便找个库就能达到目的,
关键是媒体传输这部分。仔细看1-8这些部分,很多我们自己动手就可以做,其
实我们并不需要一下完整的全功能的库。
比如,音频采集播放用DirectSound,视频采用播放用DirectShow.编解码用
ffmpeg编译出来的libavcodec,传输用jrtplib.这么一看,只有3 音视频编码
后组RTP和6 从RTP解包还原成编码后的音视频 这两部分相对陌生,其他的都
能找到成熟的代码。基于这个想法,就不用上述开源产品,直接自己写一个好了。
先做准备工作:
1 编译好ffmpeg及所带的libavcodec等几个lib和dll,音视频编解码时
需要。 2 利用DirectShow做视频采集。播放就直接用GDI画图好了,简洁。 3
声音部分,参考Youtoo这个程序,连SIP都有了,用现成的
exosip,osip,(这个库不支持视频,否则就不用做上面那些苦力了)。
音质相当不错。
4 传输就用jrtplib,不过开始为了调试方便,自己写的UDP socket。 这
些准备工作做好,下一步就开始参考RFC进行RTP的组包和解包了。 RTP接收
部分比较简单(不用考虑jitterbuffer等),先从这里入手。 其实主要就3步:
1 创建一个udp,监听一个端口,比如5200。
2 收到RTP包,送到解包程序,继续收第 二个。
3 收齐一帧后,或保存文件,或解码去播放。 下面详细说一下具体过程:
1 创建UDP,非常非常地简单(这里只是简单地模拟RTP接收,虽然能正常
工作,但是没有处理RTCP部分,会影响发送端): lass CUDPSocket : public
CAsyncSocket { public: CUDPSocket(); virtual ~CUDPSocket(); virtual void
OnReceive(int nErrorCode);
};
调用者:CUDPSocket m_udp; m_(...);这样就可以了。注意端口,
如果指定端口创建不成功,就端口+1或+2重试一下。
重写OnReceive: void CUDPSocket::OnReceive(int nErrorCode) { char
szBuffer[1500]; SOCKADDR_IN sockAddr; memset(&sockAddr, 0,
sizeof(sockAddr)); int nSockAddrLen = sizeof(sockAddr); int nResult =
ReceiveFrom(szBuffer, 1500, (SOCKADDR*)&sockAddr, &nSockAddrLen, 0);
if(nResult == SOCKET_ERROR) { return; }
//如果必要可以处理对方IP端口 USHORT unPort =
ntohs(_port); ULONG ulIP = _addr.s_addr;
//收到的数据送去解码 Decode((BYTE*)szBuffer, nResult); }
2 收到了数据,开始Decode,一般通过RTP传输的视频主要有h263
(old,1998,2000),h264,mpeg4-es。mpeg4-es格式最简单,就从它入手。 如
果了解RFC3160,直接分析格式写就是了。如果想偷懒,用现成的, 也找的到:
在opal项目下,有个plugins目录,视频中包含了h261,h263,h264,mpeg4等多
种解包,解码的源码,稍加改动就可以拿来用。
首先看:videocommon下的rtpframe.h这个文件,这是对RTP包头的数据
和操作的封装:
/********************************************************************
*********/
/* The contents of this file are subject to the Mozilla Public License
*/ /* Version 1.0 (the "License"); you may not use this file except in
*/ /* compliance with the License. You may obtain a copy of the License
at */ /*
/MPL/ */ /* */ /* Software distributed under
the License is distributed on an "AS IS" */ /* basis, WITHOUT WARRANTY
OF ANY KIND, either express or implied. See the */ /* License for the
specific language governing rights and limitations under */ /* the License.
*/ /* */ /* The Original Code is the Open H323 Library. */ /* */ /* The
Initial Developer of the Original Code is Matthias Schneider */ /*
Copyright (C) 2007 Matthias Schneider, All Rights Reserved. */ /* */ /*
Contributor(s): Matthias Schneider
(ma30002000@) */ /* */ /* Alternatively, the contents of this
file may be used under the terms of */ /* the GNU General Public License
Version 2 or later (the "GPL"), in which */ /* case the provisions of the
GPL are applicable instead of those above. If */ /* you wish to allow use
of your version of this file only under the terms */
/* of the GPL and not to allow others to use your version of this file
under */ /* the MPL, indicate your decision by deleting the provisions
above and */ /* replace them with the notice and other provisions required
by the GPL. */ /* If you do not delete the provisions above, a recipient
may use your */ /* version of this file under either the MPL or the GPL.
*/ /* */ /* The Original Code was written by Matthias Schneider
/********************************************************************
*********/ #ifndef __RTPFRAME_H__ #define __RTPFRAME_H__ 1 #ifdef
_MSC_VER #pragma warning(disable:4800) // disable performance warning
#endif class RTPFrame { public: RTPFrame(const unsigned char * frame, int
frameLen) { _frame = (unsigned char*) frame; _frameLen = frameLen; };
RTPFrame(unsigned char * frame, int frameLen, unsigned char
payloadType) { _frame = frame; _frameLen = frameLen; if (_frameLen > 0)
_frame [0] = 0x80; SetPayloadType(payloadType); } unsigned
GetPayloadSize() const { return (_frameLen - GetHeaderSize()); }
void SetPayloadSize(int size) { _frameLen = size + GetHeaderSize(); }
int GetFrameLen () const { return (_frameLen); } unsigned char *
GetPayloadPtr() const { return (_frame + GetHeaderSize()); } int
GetHeaderSize() const { int size; size = 12; if (_frameLen < 12) return
0; size += (_frame[0] & 0x0f) * 4; if (!(_frame[0] & 0x10)) return size;
if ((size + 4) < _frameLen) return (size + 4 + (_frame[size + 2] << 8)
+ _frame[size + 3]); return 0; } bool GetMarker() const { if (_frameLen
< 2) return false; return (_frame[1] & 0x80); } unsigned
GetSequenceNumber() const { if (_frameLen < 4) return 0; return (_frame[2]
<< 8) + _frame[3]; } void SetMarker(bool set) { if (_frameLen < 2) return;
_frame[1] = _frame[1] & 0x7f; if (set) _frame[1] = _frame[1] | 0x80; }
void SetPayloadType(unsigned char type) { if (_frameLen < 2) return;
_frame[1] = _frame [1] & 0x80; _frame[1] = _frame [1] | (type & 0x7f); }
unsigned char GetPayloadType() const { if (_frameLen < 1) return 0xff;
return _frame[1] & 0x7f; } unsigned long GetTimestamp() const { if
(_frameLen < 8) return 0; return ((_frame[4] << 24) + (_frame[5] << 16)
+ (_frame[6] << 8) + _frame[7]); } void SetTimestamp(unsigned long
timestamp) { if (_frameLen < 8) return; _frame[4] = (unsigned char)
((timestamp >> 24) & 0xff); _frame[5] = (unsigned char) ((timestamp >>
16) & 0xff); _frame[6] = (unsigned char) ((timestamp >> 8) & 0xff);
_frame[7] = (unsigned char) (timestamp & 0xff); }; protected: unsigned
char* _frame; int _frameLen; }; struct frameHeader { unsigned int x;
unsigned int y; unsigned int width; unsigned int height; }; #endif /*
__RTPFRAME_H__ */
原封不动,可以直接拿来使用。当然,自己写一个也不麻烦。很多人写不好
估计是卡在位运算上了。
然后,进入videoMPEG4-ffmpeg目录下看,这里包含了完整的
RFC解包重组及MPEG4解码的源码。直接编译可能通不过,好在代码写的非常整
齐,提取出来就是了。解包解码只要看这一个函数: bool
MPEG4DecoderContext::DecodeFrames(const BYTE * src, unsigned & srcLen,
BYTE * dst, unsigned & dstLen, unsigned int & flags) { if
(!ed()) return 0; // Creates our frames
RTPFrame srcRTP(src, srcLen); RTPFrame dstRTP(dst, dstLen,
RTP_DYNAMIC_PAYLOAD); dstLen = 0; flags = 0; int srcPayloadSize =
loadSize();
SetDynamicDecodingParams(true); // Adjust dynamic settings, restart
allowed // Don't exceed buffer limits. _encFrameLen set by
ResizeDecodingFrame if(_lastPktOffset + srcPayloadSize < _encFrameLen)
{ // Copy the payload data into the buffer and update the offset
memcpy(_encFrameBuffer + _lastPktOffset, loadPtr(),
srcPayloadSize); _lastPktOffset += srcPayloadSize; } else {
// Likely we dropped the marker packet, so at this point we have a
// full buffer with some of the frame we wanted and some of the next //
frame.
//I'm on the fence about whether to send the data to the
// decoder and hope for the best, or to throw it all away and start
// again.
// throw the data away and ask for an IFrame TRACE(1,
"MPEG4tDecodertWaiting for an I-Frame"); _lastPktOffset = 0;
flags = (_gotAGoodFrame ? PluginCodec_ReturnCoderRequestIFrame : 0);
_gotAGoodFrame = false; return 1; } // decode the frame if we got the marker
packet int got_picture = 0; if (ker()) { _frameNum++; int
len = cDecodeVideo (_avcontext, _avpicture,
&got_picture, _encFrameBuffer, _lastPktOffset); if (len >= 0 &&
got_picture) { #ifdef LIBAVCODEC_HAVE_SOURCE_DIR if
(DecoderError(_keyRefreshThresh)) { // ask for an IFrame update, but
still show what we've got flags = (_gotAGoodFrame ?
PluginCodec_ReturnCoderRequestIFrame : 0); _gotAGoodFrame = false; }
#endif
TRACE_UP(4, "MPEG4tDecodertDecoded " << len << " bytes" << ",
Resolution: " << _avcontext->width << "x" << _avcontext->height); // If
the decoding size changes on us, we can catch it and resize if
(!_disableResize && (_frameWidth != (unsigned)_avcontext->width ||
_frameHeight != (unsigned)_avcontext->height)) { // Set the decoding
width to what avcodec says it is _frameWidth = _avcontext->width;
_frameHeight = _avcontext->height; // Set dynamic settings (framesize),
restart as needed SetDynamicDecodingParams(true);
return true; } // it's stride time int frameBytes = (_frameWidth *
_frameHeight * 3) / 2; PluginCodec_Video_FrameHeader * header =
(PluginCodec_Video_FrameHeader *)loadPtr(); header->x =
header->y = 0; header->width = _frameWidth; header->height =
_frameHeight;
unsigned char *dstData = OPAL_VIDEO_FRAME_DATA_PTR(header); for (int
i=0; i<3; i ++) { unsigned char *srcData = _avpicture->data[i]; int
dst_stride = i ? _frameWidth >> 1 : _frameWidth; int src_stride =
_avpicture->linesize[i]; int h = i ? _frameHeight >> 1 : _frameHeight;
if (src_stride==dst_stride) { memcpy(dstData, srcData, dst_stride*h);
dstData += dst_stride*h; } else { while (h--) { memcpy(dstData, srcData,
dst_stride); dstData += dst_stride; srcData += src_stride; } } } //
Treating the screen as an RTP is weird
loadSize(sizeof(PluginCodec_Video_FrameHeader)
frameBytes);
+
loadType(RTP_DYNAMIC_PAYLOAD);
ker(true); estamp(estamp());
dstLen = meLen(); flags = PluginCodec_ReturnCoderLastFrame;
_gotAGoodFrame = true; } else {
TRACE(1, "MPEG4tDecodertDecoded "<< len << " bytes without getting
");
// decoding error, ask for an IFrame update flags = (_gotAGoodFrame ?
PluginCodec_ReturnCoderRequestIFrame : 0); _gotAGoodFrame = false; }
_lastPktOffset = 0; } return true; }
写的非常非常的明白:if (ker()),到了这里表示收满了一
包,开始去解码。
mpeg4-es的RFC还原重组就这么简单,下一步的解码,就涉及到用
了。
版权声明:本文标题:基于SIP协议的音视频的软件开发 内容由热心网友自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:https://m.elefans.com/xitong/1721389203a875210.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
发表评论