在Vovida的基础上实现自己的SIP协议栈(六) 卢政 2003/08/08 3.3 等待对方的呼叫:上面花了那么长的时间叙述了如何发起一个呼叫,我们再来介绍一下如何接收一个呼叫: 当用户进入Idle状态以后,如果系统接收到一个INVITE消息,系统将进入Ring状态,并且进入Opring操作中,这个时候硬件设备将播放振铃声,这个时候如果用户决定摘机通话,那么offhook事件就会产生,同时OpAnswerCall将使状态机进入InCall状态,向主叫发送200响应消息,同样RTP/RTCP通道打开,开始通话,如果通话完毕,双方挂机,那么互相发送SIP Bye消息,OpEndCall将使系统重新回到Idle状态。 下图表示了从接收到INVITE消息通话完毕的各个状态之间程序各种类之间的迁移过程,和主动呼叫的情况一样,如果协议栈软件应用于Marshal或者是Redirection Server的话,那么采用的协议流程和下面又有一些不一样了,在后续章节会对这些做详细介绍。 在本图中粗体的部分表示加入MyEntryOperator队列中的操作符 正体的部分表示加入MyOperator队列中的操作符 斜体的部分表示加入MyExitOperator队列中的操作符 下面我们来详细地介绍每个操作: 3.3.1 OpRing等待对方的振铃消息 OpRing:获取对端向本地发送的INVITE消息 const Sptr < State > OpRing::process( const Sptr < SipProxyEvent > event ) { Sptr < SipEvent > sipEvent; sipEvent.dynamicCast( event ); if ( sipEvent == 0 ) { return 0; } Sptr < SipMsg > sipMsg = sipEvent->getSipMsg(); assert( sipMsg != 0 ); //接收INVITE消息; Sptr < InviteMsg > msg; msg.dynamicCast( sipMsg ); … … Sptr < UaCallInfo > call; call.dynamicCast( event->getCallInfo() ); assert( call != 0 ); //在UaCallInfo中存储当前的INVITE消息; call->setRingInvite( new InviteMsg( *msg ) ); call->setContactMsg(*msg); //保存当前的路由消息; call->setCalleeRoute1List( msg->getrecordrouteList() ); int numContact = msg->getNumContact(); if ( numContact ) {//保存连接 SipContact contact = msg->getContact( numContact - 1 ); Sptr < SipRoute > route = new SipRoute; route->setUrl( contact.getUrl() ); call->addRoute1( route ); } … … Sptr< BaseUrl > baseUrl = msg->getFrom().getUrl(); assert( baseUrl != 0 ); // Assume we have a SIP_URL Sptr< SipUrl > sipUrl; sipUrl.dynamicCast( baseUrl ); assert( sipUrl != 0 ); //获取主叫的Sip URL Data callingNum = sipUrl->getUserValue(); callingNum += "@"; callingNum += sipUrl->getHost(); signal->dataList.push_back( callingNum.getData(lo) ); //把主叫和被叫的地址(URL)都装入设备的信号队列中,为媒体流和铃声回放的RTP信道做准备 SipRequestLine reqLine = msg->getRequestLine(); baseUrl = reqLine.getUrl(); assert( baseUrl != 0 ); sipUrl.dynamicCast( baseUrl ); assert( sipUrl != 0 ); string calledNum = sipUrl->getUserValue().getData(lo); signal->dataList.push_back( calledNum ); UaDevice::getDeviceQueue()->add( signal ); //获取主叫的SDP Sptr remoteSdp.dynamicCast (msg->getContentData(0)); bool ringbackTone = false; //创建本地的SDP SipSdp localSdp; if ( remoteSdp != 0 ) { localSdp = *remoteSdp; Data host = theSystem.gethostAddress(); if(UaConfiguration::instance()->getNATAddress() != "") { host = UaConfiguration::instance()->getNATAddress(); } //设定本地的SDP setStandardSdp(localSdp, host,UaDevice::instance()->getRtpPort()); } //获取本地状态是否当前的硬件状态支持Call Waiting HardwareStatusType hdwStatus = UaDevice::instance()->getHardwareStatus(); //检验本地是否支持零声回放(回放的话要在零声消息里增加本地的SDP) StatusMsg statusMsg; if (UaConfiguration::instance()->getProvideRingback() && hdwStatus == HARDWARE_AVAILABLE && (remoteSdp != 0) ) {//提供零声回放回送183状态。 ringbackTone = true; StatusMsg status( *msg, 183 ); status.setContentData( &localSdp ); call->setLocalSdp( new SipSdp( localSdp ) ); statusMsg = status; } else { // 提供零声回放则回送180状态 StatusMsg status( *msg, 180 ); statusMsg = status; } if ( remoteSdp != 0 && UaConfiguration::instance()->getProvideRingback() ) { call->setRemoteSdp( new SipSdp( *remoteSdp ) ); call->setLocalSdp( new SipSdp( localSdp ) ); Sptr < SipSdp > localSdp = call->getLocalSdp(); Sptr < SipSdp > remoteSdp = call->getRemoteSdp(); //这里要建立RSVP会话,准备开始预留路径。 setupRsvp(*localSdp, *remoteSdp); } // TODO Call log Show caller information Sptr < SipCallId > callId = new SipCallId( sipEvent->getSipCallLeg()->getCallId() ); if ( hdwStatus == HARDWARE_AVAILABLE ) { // 处理当前的队列 UaDevice::instance()->setCallId( callId ); } else if ( hdwStatus == HARDWARE_CALLWAITING_ALLOWED ) { //把当前的呼叫放在等待队列中 UaDevice::instance()->addCallWaitingId( callId ); } else { return 0; } sipEvent->getSipStack()->sendReply( statusMsg ); if ( ringbackTone ) {//回放零声(如果需要的话)。 sendRemoteRingback(*remoteSdp); } Sptr < UaStateMachine > stateMachine; stateMachine.dynamicCast( event->getCallInfo()->getFeature() ); assert( stateMachine != 0 ); //进入StateRinging状态 return stateMachine->findState( "StateRinging" ); } 3.3.2 OpStartRinging开始响铃 OpStartRinging开始振铃,这个程序和简单,主要是在设备处理队列中加入振铃消息,并且由设备对振铃消息进行处理。 3.3.3 OpRingingInvite处理又一个INVITE消息(呼叫等待) OpRingingInvite是一个比较有趣的状态,如果在StateRinging期间有新的INVITE消息过来,那么就回送一个180消息给它,让它处于一个呼叫等待的状态。 3.3.4 OpAnswerCall被叫打开媒体通道开始通讯 OpAnswerCall const Sptr < State > 该操作的主要目的在于本地接收到主叫发送过来的Invite消息以后,根据主叫的SDP信息,创建本地的SDP并回送200消息,等待主叫发送ACK消息,正式打开媒体通道。 在SDP中最重要的项目莫过于"m="媒体流指示和"a="会话描述,在其之中定义了载荷类型,RTP/RTCP端口,编码方式这几个重要参数。 MediaList:代表的是媒体信息指示也就是所有"m="项目的描述列表 MediaAttrib:代表的是会话描述的内容也就是所有"a="项目的描述内容 我们以下列的SDP为例子: SDP Headers ----------------------------------------------------------------- Header: v=0 Header: o=CiscoSystemsSIP-IPPhone-UserAgent 13045 2886 IN IP4 192.168.6.20 Header: s=SIP Call Header: c=IN IP4 192.168.6.20 Header: t=0 0 Header: m=audio 30658 RTP/AVP 0 101 Header: a=rtpmap:0 pcmu/8000 Header: a=rtpmap:101 telephone-event/8000 Header: a=fmtp:101 0-11 Header: m=video 30700 RTP/AVP 102 Header: a=rtpmap:31 H261/9000 OpAnswerCall::process( const Sptr < SipProxyEvent > event ) { Sptr < UaDeviceEvent > deviceEvent; deviceEvent.dynamicCast( event ); if ( deviceEvent == 0 ) { return 0; } //检测是否摘机 if ( deviceEvent->type != DeviceEventHookUp && deviceEvent->type != DeviceEventFlash ) { return 0; } Sptr < UaCallInfo > call; call.dynamicCast( event->getCallInfo() ); assert( call != 0 ); //取得引发振铃的INVITE消息。 Sptr < InviteMsg > msg = call->getRingInvite(); assert( msg != 0 ); //取出当前的INVITE消息的CallID SipCallId callId = msg->getCallId(); //如果当前的CllID不是当前正在处理的CallID那么让当前的Call处于等待当中。 if ( UaDevice::instance()->isMyHardware( callId ) == false ) { Sptr < SipCallId > callWaitingId = //检验INVITE的CallID是否处于等待队列中 UaDevice::instance()->getCallWaitingId(); if ( callWaitingId == 0 ) { return 0; } if ( *callWaitingId != callId ) { return 0; } //如果两次发送Invite消息是相同的CallID那么第二个肯定是Re-Invite消息,取消当前 //的Call ID,因为有可能在前面介绍的呼叫等待当中,有可能在发送新的INVITE消息的时//候(OpRingingInvite接收到)让程序陷入当前OpRing-->OpAnswerCall的状态,把当前的//这样在当前的这个操作中把处于等待的Call ID消除。 UaDevice::instance()->setCallId( callWaitingId ); UaDevice::instance()->removeCallWaitingId( *callWaitingId ); } // 取得远端的SDP Sptr remoteSdp.dynamicCast ( msg->getContentData(0) ); call->setRemoteSdp( new SipSdp( *remoteSdp ) ); StatusMsg status( *msg, 200/*OK*/ ); // 根据Cfg文件配置本地的Url到SDP中。 Sptr< SipUrl > myUrl = new SipUrl; myUrl->setUserValue( UaConfiguration::instance()->getUserName() ); myUrl->setHost( Data( theSystem.gethostAddress() ) ); myUrl->setPort( atoi( UaConfiguration::instance()->getLocalSipPort().c_str() ) ); if(UaConfiguration::instance()->getSipTransport() == "TCP") { myUrl->setTransportParam( Data("tcp")); } SipContact me; me.setUrl( myUrl ); status.setNumContact( 0 ); // Clear status.setContact( me ); //根据远端回传的SDP创建本地的SDP Sptr localSdp.dynamicCast ( status.getContentData(0) ); //本地的SDP设置不为空的情况 if ( localSdp != 0 ) { //设定本地的RTP端口号 localSdp->setRtpPort( UaDevice::instance()->getRtpPort() ); // 设定RTP包的传输速率 int rtpPacketSize = UaConfiguration::instance()->getNetworkRtpRate(); //取得SDP描述符(会话符SdpSession) SdpSession sdpDesc = localSdp->getSdpDescriptor(); list < SdpMedia* > mediaList; //取得媒体描述列表例如:所有的以"m="打头的描述 mediaList = sdpDesc.getMediaList(); list < SdpMedia* > ::iterator mediaIterator = mediaList.begin(); //取得所有的媒体列表所有的以"m="打头的媒体描述 vector < Data > * formatList = (*mediaIterator)->getStringFormatList(); if ( formatList != 0 ) { formatList->clear(); } //采用缺省的方式来建立媒体名和传送地址,这里当主叫和被叫没有公共的媒体格式的时候,//被叫返回媒体流的"m"行,设置端口为0并且不返回载荷类型。 (*mediaIterator)->addFormat( 0 ); // MediaAttributes表示所有"a="和"a=rtpmap:…"的集合 MediaAttributes* mediaAttrib //取得媒体流的会话属性列表:(所有的"a="的列表) //a=rtpmap : <载荷类型> <算法名称> / <时钟采样频率> [/<带入参数>] mediaAttrib = (*mediaIterator)->getMediaAttributes(); if ( mediaAttrib != 0 ) { //取得一个单纯的a=<属性>:<值>(不包括"a=rtpmap")例如:a=recvonly vector < ValueAttribute* > * valueAttribList = mediaAttrib->getValueAttributes(); vector < ValueAttribute* > ::iterator attribIterator = valueAttribList->begin(); while ( attribIterator != valueAttribList->end() ) { char* attribName = (*attribIterator)->getAttribute(); //如果遇见a=ptime的情况,就表示要设置时长,那么也就是要设定RTP的帧长 if ( strcmp( attribName, "ptime" ) == 0 ) { rtpPacketSize = Data((*attribIterator)->getValue()).convertInt(); break; } attribIterator++; } mediaAttrib->flushValueAttributes(); mediaAttrib->flushrtpmap(); //以上是根据原段对段远端的SDP来获得相关的会话属性值,下面是如何将这些会话属性值//加入当前的本地的 SDP的会话属性列表"a="当中 //设定本地的简单的"a=<属性>:<值> " ValueAttribute* attrib = new ValueAttribute(); attrib->setAttribute( "ptime" ); LocalScopeAllocator lo; attrib->setValue( Data( rtpPacketSize ).getData(lo) ); //增加a=rtpmap : <载荷类型> <算法名称> / <时钟采样频率> [/<带入参数>] //在本地的a=rtpmap:当中 SdpRtpMapAttribute* rtpMapAttrib = new SdpRtpMapAttribute(); rtpMapAttrib->setPayloadType( 0 ); rtpMapAttrib->setEncodingName( "PCMU" ); rtpMapAttrib->setClockRate( 8000 ); mediaAttrib->addValueAttribute( attrib ); mediaAttrib->addmap( rtpMapAttrib ); } else//如果mediaAttrib为0的情况,也就是"a="项目为空的情况 { cpLog(LOG_DEBUG, "no mediaAttrib"); mediaAttrib = new MediaAttributes(); assert(mediaAttrib); (*mediaIterator)->setMediaAttributes(mediaAttrib); // create the new value attribute object ValueAttribute* attrib = new ValueAttribute(); // set the attribute and its value attrib->setAttribute("ptime"); LocalScopeAllocator lo; //通过Cfg文件获取RTP传输速率,创建一个a=ptime:<分组时间>的对话属性。 attrib->setValue( Data( UaConfiguration::instance()->getNetworkRtpRate() ).getData(lo) ); //add the rtpmap attribute for the default codec SdpRtpMapAttribute* rtpMapAttrib = new SdpRtpMapAttribute(); rtpMapAttrib->setPayloadType(0); rtpMapAttrib->setEncodingName("PCMU"); rtpMapAttrib->setClockRate(8000); // 增加新创建的会话属性到本地的SDP当中 mediaAttrib->addValueAttribute(attrib); mediaAttrib->addmap(rtpMapAttrib); } localSdp->setSdpDescriptor(sdpDesc); //回送OK给主叫端,并且通告本地的SDP call->setLocalSdp( new SipSdp( *localSdp ) ); deviceEvent->getSipStack()->sendReply( status ); } else // 根据远端创建的本地的SDP为0的情况。 { cpLog(LOG_DEBUG, "localSdp == 0"); // May not have SDP in original INVITE for 3rd party call control SipSdp sdp; Data hostAddr = theSystem.gethostAddress(); if(UaConfiguration::instance()->getNATAddress() != "") { hostAddr = UaConfiguration::instance()->getNATAddress(); } int rtpPort = UaDevice::instance()->getRtpPort(); //重新构造一个本地的SDP发送给主叫端,该媒体属性和主叫方的处于一致。 doAnswerStuff(sdp, remoteSdp, hostAddr, rtpPort); //在状态中回送本地的SDP status.setContentData( &sdp, 0 ); call->setLocalSdp( new SipSdp( sdp ) ); deviceEvent->getSipStack()->sendReply( status ); } //转移工作状态到StateInCall Sptr < UaStateMachine > stateMachine; stateMachine.dynamicCast( event->getCallInfo()->getFeature() ); assert( stateMachine != 0 ); return stateMachine->findState( "StateInCall" ); } 3.3.5 回到StateInCall状态 最后程序被叫和主叫所进入的状态都回到StateInCall状态,在这个状态里,被叫接收主叫发送的ACK消息,如果在其中含有新的SDP的话,就按照新的SDP进行处理,否则,不就按照UaCallInfo中所包含的SDP的进行处理。 4. 如何在改造现有的终端使之能传递视频流。 目前的Vocal平台是仅仅支持语音传输的,还没有考虑到视频部分,不过从整个协议栈的构造来说,我尝试过把当前的SIP修改成一个语音/视频一体的完整的视频电话系统(如果想修改成会议系统的话,我们稍后在Conference Server这节里面做介绍)只需要把当前的协议栈部分增加大概1500-2000行左右的代码(但不包括Codec部分)就可以完成,我们以增加一个H.261+能力作为我们对这个问题的讨论点: 4.1一个H.261+的Codec的基本构造: 有参加过视频压缩和解压缩算法开发工作的同志应该知道,一般来说一个视频通讯的基本类包括一个如下的流程: 以Openh323中实现的方式为例:(只叙述H.261+编码部分) 4.2 增加视频能力所需要做的工作 1.设备驱动/压缩/解压缩部分: a. 创建一个H.261的编码实例,构造一个P64的同步压缩器,您可以从加洲大学或者是在OpenH323组织的网站上下载该算法的原代码。 b. 在输入上绑定一个标准的摄相头部分,您可以按照一个标准输入设备来做,也可以使用PWLIB来作为这个设备的输入通道,进行绑定,不过,如果使用PWLIB的时候整体效率会降低很多。 c. 最后把编码类和摄相头联合构造一个标准的输出/输入类(类似于SoundCard的实例一样),如果这个标准类的调用接口方法可以构造成和标准的声音设备一样,也可以构造成在声音设备之内。 2.协议部分的改造: 对于SIP和H.323相比较而言,两者在视频通讯的概念上有很大的不同,SIP把所有的媒体讯息理解成相同的RTP流,区别只是带宽不相同;而H.323则有一种专门的快速方式把两者区分开,首先打开语音通道传送音频,然后打开视频通道传送视频信息,两者用不同的媒体通道传输。后者的最大好处在于,如果带宽不足的话,至少可以传递语音信息到对方。当然我们可以在SIP的协议前提下做适当的修改模仿H.323的运行方式。 a. 媒体流的描述: 一个媒体信息的具体内容包括: 一个H.261的视频流描述例子: m=video 513000 RTP/UDP 31 a=rtpmap:31 h261/90000 媒体类型:Video表示媒体类型,513000表示的是RTP端口号,这个端口号从管理上来说最好和音频不相同。 传送协议:采用RTP/UDP上传送,因为目前SIP Stack只支持UDP还不支持AVP的格式; 媒体格式:在RTP中定义的静态载荷类型文档号为31; 媒体地址和端口:目的地址和RTP的端口号。 这里最主要改造的地方是OpInviteUrl方法,和用户端回送OK消息的OpAnswerCall,这里是创建初始的视频描述的操作,在他们中间需要增加对视频流的描述,另外在被叫对主叫送来的INVITE消息中必须要检测对方的媒体类型是否能解析,如果不行的话还要在OK消息中做相应的拒绝返回。 例如: 主叫INVITE消息的SDP: Header: v=0 Header: o=- 1528076688 1528076688 IN IP4 192.168.66.1 Header: s=VOVIDA Session Header: c=IN IP4 192.168.66.1 Header: t=3177769010 0 Header: m=audio 56104 RTP/UDP 0 Header: a=rtpmap:0 PCMU/8000 Header: a=ptime:20 Header: m=video 56110 RTP/UDP 31 Header: a=rtpmap:0 H261/90000 如果被叫不愿意或者无能力接受视频流那么被叫回送的SDP如下: Header: v=0 Header: o=- 1528076688 1528076688 IN IP4 192.168.66.2 Header: s=VOVIDA Session Header: c=IN IP4 192.168.66.2 Header: t=3177769010 0 Header: m=audio 56114 RTP/AVP 0 Header: a=rtpmap:0 PCMU/8000 Header: a=ptime:20 Header: m=video 0 RTP/UDP 31 b. RTP/RTCP部分的改造: 首先在现有的音频RTP/RTCP会话基础上增加一个视频的RTP/RTCP会话,从前面介绍的我们知道视频和音频一般来说是不在一个RTP端口的,那么我们为了新开的视频RTP端口当然需要捆绑一个RTPSession(会话),在这里我们需要重写视频设备实例的ProcessRTP方法,换一句话来说,就是视频和音频设备有自己的ProcessRTP方法,分别从不同的端口进程读取相应的媒体数据流,另外相对视频信号而言,视频流的RTP帧肯定相对要大一些,不过为了保证声音/视频同步,一般来说两者的时长还是需要相等(一般是以20ms的数据帧,在这个时长内声音/视频的RTP的头和内容的比例还是比较均匀的,不过这样的话,要使用Jitter的方式,但是实际上,视频/音频的时间上并不能做到完全的相等,所以在声/象同步上并不能完全依赖SSRC,需要在设计时采用同步缓冲的方式,大家有兴趣的话可以参加一些RSVP工程组中有关于QoS改善的讨论)。 这样如果分别有自己的ProcessRTP方法,那么前面的说道的在视频流中如果发生需要保证音频带宽而需要放弃视频带宽的情况,这样的情况我们就很好处理了,在SIP协议中并不保证主叫可以单独打开一个音频通道,那么我们只能在RTP/UDP这一层来完成,可以利用RTCP中的APP分组来完成这个通告,比如主/被一端想终止视频或者音频通讯,在RTCP通道中发送一个自定义的APP分组就可以了。 但是这样在初始化阶段,如果RSVP在路径上没有预留足够的资源,那么在开始的视频和音频通讯就可能会造成阻塞,可能造成用户的媒体通讯超时,而不能向H.323那样可以先通过快速方式打开一个音频通道(0号通道),而后再开启视频通道,所以上述的方法对于会话初始阶段毫无用处。 c. 声象同步: 任何一个商业成功的商业运作的视频通讯软件中都需要比较好的去解决声象同步这 个重要的问题,我们一般建议采用的方式是建立FIFO队列缓冲,根据RTP包的SSRC重新排列这些分组,不过如果在操作系统中采用了Direct X或者是直接读写FrameBuffer技术的话,那么FIFO也就不能达到您想直接提高图象处理速度的能力. 3.QoS的提高: 最后说一下Qos的提高工作,视频通讯的处理不但消耗大量的系统资源,也要消耗大量的网络资源,当然按照前面所说的简单的对QoS进行处理当然是不行的,需要采取的策略有以下两种: a. RSVP上的改善:视频通讯预留的带宽要比原来音频的要大很多了,而且我们必须考虑到H.261/263协议中的帧间帧(IntraFrame)的情况,会产生突发性的带宽需求,所以预留带宽需要比平均带宽高30%左右,另外,对RESC/PATH消息对必须在通讯中周期性的传送,确保证实带宽(必须考虑消息对所占用的一定带宽)。 b. RTP信道上的改善:我们通过RTCP中的SR/RR分组了解到了丢失率,累计分组数,到达时延抖动等等QoS信息,我们可以通过这些信息对通讯终端的视频信号进行一定的控制,比如在发现QoS降低时,可以以降低量化度或单位时间帧数来降低带宽负载。 参考文献: RFC3261 RFC2548 RFC2205 RFC2212 RAPI -- An RSVP Application Programming Interface Version 5 Practical VOIP Using Vocal(O'REILLY) IP Phone Based on IP network and Multimedia communication(WOS) IP Phone in Internent(Oliver David) IP网络电话技术(人民邮电出版社) 视频压缩与视频编码技术(中国电力出版社) (完) 作者联系方法:[email protected] 作者供稿 CTI论坛编辑 |