SIP与RTP综合应用

SIP是一个会话协议,很多大企业都在用,通信行业的一个标准,其业务逻辑比较,简单地来说如下:

UserAgent                                                  Server

                ------------------REGISTER----------->

                <----------401(407) Unauthorized--

               ----------REG(带上用户口令)----------->

               ---------------200OK    1Bindings---

双方交互几次,注册成功。

因为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的工作流程。

下一步,就是自己动手实现SIPVOIP系统了。

如果商用的话,server采用Trixbox,也可以仔细研究一下 Asterisk。客户端就用x-lite好了。

做为程序员,第一反应就是怎么样自己动手写一个客户端,甚至服务器。好在开源产品众多,写一个并不难。

经过几天的调试,发现几个协议栈做的不错:

1 SIP协议栈:

    a osip+exosip (建立客户端及通信非常简单,质量也好。)

    b reSIProcate (全面,有server端例子,综合调试方便)

    c 其他的还用过一个pjsip,不过它与音视频结合成一个库之后, 音频质量不好。但是比较小巧, 听说台湾很多嵌入设备采用。

2 RTP协议栈:

    Linphone采用的是oRTP,音视频部分采用的是MediaStreamer2。

    JRtpLib,结合emiplib的音视频处理。

   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 如果自己想定制sipserver,干脆一步到位,下载reSIProcate,用vc2005编译,一次通过。运行时提示缺少几个dll,google一下很快都找到了,然后运行repro,做为server先临时用着,反正是学习。

客户端呢,网上流行一个很不错的,名字叫Youtoo,下载,简单编译后可以做为一个语音的客户端使用。

然后,PC上安装几个虚拟机,一个运行server,一个运行x-lite(做为一个参考的标准),主要上运行我们自己写的客户端进行测试。如果要调试server,就是主机上运行repro,虚拟上分别运行两个x-lite。

环境搭建立好了,下一步就开始调试。

根据这几天的实践,找出了一个最优的配置:

1 sipserver采用Trixbox,如果对Linux很熟建议直接用Asterisk。

2 客户端如果直接使用,建议ekiga。

顺便说一下几个客户端使用的感受:

1 linphone:好象名气不小,不过,最新版3.1.2安装后启动就崩溃。我安装的是普通的XP-SP3,电脑公司特别版。一般软件运行没问题。如果在这个平台上都崩溃,真不知道说什么好。

 后来再试3.1.1,这个可以启动,运行能看到视频图像。不过奇怪的是,与视频电话连接上视频窗口反而隐藏起来。结束通话,又显示出来。搞不懂它的视频功能是做什么用的。另外显示本地视频只支持QCIF。

2 eyeBeam:名气更大,使用起来也不错,这个不错不包含视频功能。如果启动了视频的话,只显示第一帧图,摄像头怎么转它也不动。另外,主动连视频电话时,不能启动视频功能。视频电话呼叫它才能启动。启动后,"StartVideo"点一下又灰住,完全不能用。

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等,稀里湖涂足足花了大半天时间把所有这些都编译好,然后编译ms2.lib时提示几个链接出错。因为我看到网上几个文章说明是用vc2005轻松编译出来的,我也用的vc2005。估计用mingw会简单一些。不过,已经耗了近一天的时间,感觉不爽,放弃。估计是linphone估计搞的复杂,好让antisip卖钱。

2 ekiga

这个需要ptlib,第一感觉这东西很麻烦,不过编译时出奇的顺利(关键是官方提供的资料详细,网友写的文章也详细)。然后编译opal,也很顺利。(只是占用机器比较厉害,P42.6的占CPU极严重。不过,用的机器是联想的超薄机箱那种,不排除官方弄个很烂的CPU冒充。因为换到另一个P43G,速度快上两三倍)。

其实,编译好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,ms2.lib(这个库不支持视频,否则就不用做上面那些苦力了)。音质相当不错。

4传输就用jrtplib,不过开始为了调试方便,自己写的UDP socket。

这些准备工作做好,下一步就开始参考RFC进行RTP的组包和解包了。

RTP接收部分比较简单(不用考虑jitterbuffer等),先从这里入手。

其实主要就3步:

1创建一个udp,监听一个端口,比如5200。

2 收到RTP包,送到解包程序,继续收第二个。

3收齐一帧后,或保存文件,或解码去播放。

下面详细说一下具体过程:

1创建UDP,非常非常地简单(这里只是简单地模拟RTP接收,虽然能正常工作,但是没有处理RTCP部分,会影响发送端):

lass CUDPSocket : publicCAsyncSocket
{
public:
   CUDPSocket();
    virtual~CUDPSocket();
    
    virtual voidOnReceive(int nErrorCode);

};

调用者:CUDPSocket m_udp;m_udp.Create(...);这样就可以了。注意端口,如果指定端口创建不成功,就端口+1或+2重试一下。

重写OnReceive:

void CUDPSocket::OnReceive(intnErrorCode)
{
    charszBuffer[1500];

   SOCKADDR_IN sockAddr;
   memset(&sockAddr, 0, sizeof(sockAddr));
    intnSockAddrLen = sizeof(sockAddr);

   int nResult = ReceiveFrom(szBuffer, 1500, (SOCKADDR*)&sockAddr,&nSockAddrLen, 0);
    if(nResult== SOCKET_ERROR)
    {
       return;
   }


//如果必要可以处理对方IP端口

   USHORT unPort =ntohs(sockAddr.sin_port);
    ULONG ulIP =sockAddr.sin_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等多种解包,解码的源码,稍加改动就可以拿来用。

首先看:video\common下的rtpframe.h这个文件,这是对RTP包头的数据和操作的封装:

#ifndef __RTPFRAME_H__
#define __RTPFRAME_H__ 1

#ifdef _MSC_VER
#pragma warning(disable:4800)  // disableperformance warning
#endif

class RTPFrame {
public:
  RTPFrame(const unsigned char * frame, intframeLen) {
    _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 {
    intsize;
    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]);
    return0;
  }

  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


原封不动,可以直接拿来使用。当然,自己写一个也不麻烦。很多人写不好估计是卡在位运算上了。

然后,进入video\MPEG4-ffmpeg目录下看mpeg4.cxx,这里包含了完整的RFC解包重组及MPEG4解码的源码。直接编译可能通不过,好在代码写的非常整齐,提取出来就是了。解包解码只要看这一个函数:

bool MPEG4DecoderContext::DecodeFrames(const BYTE * src, unsigned& srcLen,
                                      BYTE * dst, unsigned & dstLen,
                                      unsigned int & flags)

{
    if(!FFMPEGLibraryInstance.IsLoaded())
       return 0;

    // Createsour frames
    RTPFramesrcRTP(src, srcLen);
    RTPFramedstRTP(dst, dstLen, RTP_DYNAMIC_PAYLOAD);
    dstLen =0;
    flags =0;
    
    intsrcPayloadSize = srcRTP.GetPayloadSize();
   SetDynamicDecodingParams(true); // Adjust dynamic settings, restartallowed
    
    // Don'texceed buffer limits.  _encFrameLen set byResizeDecodingFrame
   if(_lastPktOffset + srcPayloadSize < _encFrameLen)
    {
       // Copy the payload data into the buffer and update theoffset
       memcpy(_encFrameBuffer + _lastPktOffset,srcRTP.GetPayloadPtr(),
              srcPayloadSize);
       _lastPktOffset += srcPayloadSize;
    }
    else {

       // Likely we dropped the marker packet, so at this point we havea
       // full buffer with some of the frame we wanted and some of thenext
       // 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 andstart 
       // again.


       // throw the data away and ask for an IFrame
       TRACE(1, "MPEG4\tDecoder\tWaiting for an I-Frame");
       _lastPktOffset = 0;
       flags = (_gotAGoodFrame ? PluginCodec_ReturnCoderRequestIFrame :0);
       _gotAGoodFrame = false;
       return 1;
    }

    // decodethe frame if we got the marker packet
    intgot_picture = 0;
    if(srcRTP.GetMarker()) {
       _frameNum++;
       int len = FFMPEGLibraryInstance.AvcodecDecodeVideo
                       (_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'vegot
               flags = (_gotAGoodFrame ? PluginCodec_ReturnCoderRequestIFrame :0);
               _gotAGoodFrame = false;
           }
#endif
           TRACE_UP(4, "MPEG4\tDecoder\tDecoded " << len << "bytes" << ", Resolution: " << _avcontext->width<< "x" << _avcontext->height);
           // If the decoding size changes on us, we can catch it andresize
           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*)dstRTP.GetPayloadPtr();
           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
           dstRTP.SetPayloadSize(sizeof(PluginCodec_Video_FrameHeader)
                                 + frameBytes);
           dstRTP.SetPayloadType(RTP_DYNAMIC_PAYLOAD);
           dstRTP.SetTimestamp(srcRTP.GetTimestamp());
           dstRTP.SetMarker(true);
           dstLen = dstRTP.GetFrameLen();
           flags = PluginCodec_ReturnCoderLastFrame;
           _gotAGoodFrame = true;
       }
       else {
           TRACE(1, "MPEG4\tDecoder\tDecoded "<< len << " byteswithout getting a Picture..."); 
           // decoding error, ask for an IFrame update
           flags = (_gotAGoodFrame ? PluginCodec_ReturnCoderRequestIFrame :0);
           _gotAGoodFrame = false;
       }
       _lastPktOffset = 0;
    }

return true;
}

写的非常非常的明白:if (srcRTP.GetMarker()),到了这里表示收满了一包,开始去解码。

mpeg4-es的RFC还原重组就这么简单,下一步的解码,就涉及到用libavcodec.dll了。

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值