在Vovida的基礎(chǔ)上實(shí)現(xiàn)自己的SIP協(xié)議棧(五)
在Vovida的基礎(chǔ)上實(shí)現(xiàn)自己的SIP協(xié)議棧(六)
盧政 2003/08/08
3.3 等待對方的呼叫:
上面花了那么長的時(shí)間敘述了如何發(fā)起一個(gè)呼叫,我們再來介紹一下如何接收一個(gè)呼叫:
當(dāng)用戶進(jìn)入Idle狀態(tài)以后,如果系統(tǒng)接收到一個(gè)INVITE消息,系統(tǒng)將進(jìn)入Ring狀態(tài),并且進(jìn)入Opring操作中,這個(gè)時(shí)候硬件設(shè)備將播放振鈴聲,這個(gè)時(shí)候如果用戶決定摘機(jī)通話,那么offhook事件就會(huì)產(chǎn)生,同時(shí)OpAnswerCall將使?fàn)顟B(tài)機(jī)進(jìn)入InCall狀態(tài),向主叫發(fā)送200響應(yīng)消息,同樣RTP/RTCP通道打開,開始通話,如果通話完畢,雙方掛機(jī),那么互相發(fā)送SIP
Bye消息,OpEndCall將使系統(tǒng)重新回到Idle狀態(tài)。
下圖表示了從接收到INVITE消息通話完畢的各個(gè)狀態(tài)之間程序各種類之間的遷移過程,和主動(dòng)呼叫的情況一樣,如果協(xié)議棧軟件應(yīng)用于Marshal或者是Redirection
Server的話,那么采用的協(xié)議流程和下面又有一些不一樣了,在后續(xù)章節(jié)會(huì)對這些做詳細(xì)介紹。
在本圖中粗體的部分表示加入MyEntryOperator隊(duì)列中的操作符
正體的部分表示加入MyOperator隊(duì)列中的操作符
斜體的部分表示加入MyExitOperator隊(duì)列中的操作符

(點(diǎn)擊放大)
下面我們來詳細(xì)地介紹每個(gè)操作:
3.3.1 OpRing等待對方的振鈴消息
OpRing:獲取對端向本地發(fā)送的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中存儲(chǔ)當(dāng)前的INVITE消息;
call->setRingInvite( new InviteMsg( *msg ) );
call->setContactMsg(*msg);
//保存當(dāng)前的路由消息;
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)都裝入設(shè)備的信號(hào)隊(duì)列中,為媒體流和鈴聲回放的RTP信道做準(zhǔn)備
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;
remoteSdp.dynamicCast (msg->getContentData(0));
bool ringbackTone = false;
//創(chuàng)建本地的SDP
SipSdp localSdp;
if ( remoteSdp != 0 )
{
localSdp = *remoteSdp;
Data host = theSystem.gethostAddress();
if(UaConfiguration::instance()->getNATAddress() != "")
{
host = UaConfiguration::instance()->getNATAddress();
}
//設(shè)定本地的SDP
setStandardSdp(localSdp, host,UaDevice::instance()->getRtpPort());
}
//獲取本地狀態(tài)是否當(dāng)前的硬件狀態(tài)支持Call Waiting
HardwareStatusType hdwStatus = UaDevice::instance()->getHardwareStatus();
//檢驗(yàn)本地是否支持零聲回放(回放的話要在零聲消息里增加本地的SDP)
StatusMsg statusMsg;
if (UaConfiguration::instance()->getProvideRingback() &&
hdwStatus == HARDWARE_AVAILABLE &&
(remoteSdp != 0) )
{//提供零聲回放回送183狀態(tài)。
ringbackTone = true;
StatusMsg status( *msg, 183 );
status.setContentData( &localSdp );
call->setLocalSdp( new SipSdp( localSdp ) );
statusMsg = status;
}
else
{
// 提供零聲回放則回送180狀態(tài)
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會(huì)話,準(zhǔn)備開始預(yù)留路徑。
setupRsvp(*localSdp, *remoteSdp);
}
// TODO Call log Show caller information
Sptr < SipCallId > callId
= new SipCallId( sipEvent->getSipCallLeg()->getCallId() );
if ( hdwStatus == HARDWARE_AVAILABLE )
{
// 處理當(dāng)前的隊(duì)列
UaDevice::instance()->setCallId( callId );
}
else if ( hdwStatus == HARDWARE_CALLWAITING_ALLOWED )
{
//把當(dāng)前的呼叫放在等待隊(duì)列中
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 );
//進(jìn)入StateRinging狀態(tài)
return stateMachine->findState( "StateRinging" );
}
3.3.2 OpStartRinging開始響鈴
OpStartRinging開始振鈴,這個(gè)程序和簡單,主要是在設(shè)備處理隊(duì)列中加入振鈴消息,并且由設(shè)備對振鈴消息進(jìn)行處理。
3.3.3 OpRingingInvite處理又一個(gè)INVITE消息(呼叫等待)
OpRingingInvite是一個(gè)比較有趣的狀態(tài),如果在StateRinging期間有新的INVITE消息過來,那么就回送一個(gè)180消息給它,讓它處于一個(gè)呼叫等待的狀態(tài)。
3.3.4 OpAnswerCall被叫打開媒體通道開始通訊
OpAnswerCall const Sptr < State >
該操作的主要目的在于本地接收到主叫發(fā)送過來的Invite消息以后,根據(jù)主叫的SDP信息,創(chuàng)建本地的SDP并回送200消息,等待主叫發(fā)送ACK消息,正式打開媒體通道。
在SDP中最重要的項(xiàng)目莫過于"m="媒體流指示和"a="會(huì)話描述,在其之中定義了載荷類型,RTP/RTCP端口,編碼方式這幾個(gè)重要參數(shù)。
MediaList:代表的是媒體信息指示也就是所有"m="項(xiàng)目的描述列表
MediaAttrib:代表的是會(huì)話描述的內(nèi)容也就是所有"a="項(xiàng)目的描述內(nèi)容
我們以下列的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;
}
//檢測是否摘機(jī)
if ( deviceEvent->type != DeviceEventHookUp &&
deviceEvent->type != DeviceEventFlash )
{
return 0;
}
Sptr < UaCallInfo > call;
call.dynamicCast( event->getCallInfo() );
assert( call != 0 );
//取得引發(fā)振鈴的INVITE消息。
Sptr < InviteMsg > msg = call->getRingInvite();
assert( msg != 0 );
//取出當(dāng)前的INVITE消息的CallID
SipCallId callId = msg->getCallId();
//如果當(dāng)前的CllID不是當(dāng)前正在處理的CallID那么讓當(dāng)前的Call處于等待當(dāng)中。
if ( UaDevice::instance()->isMyHardware( callId ) == false )
{
Sptr < SipCallId > callWaitingId =
//檢驗(yàn)INVITE的CallID是否處于等待隊(duì)列中
UaDevice::instance()->getCallWaitingId();
if ( callWaitingId == 0 )
{
return 0;
}
if ( *callWaitingId != callId )
{
return 0;
}
//如果兩次發(fā)送Invite消息是相同的CallID那么第二個(gè)肯定是Re-Invite消息,取消當(dāng)前
//的Call ID,因?yàn)橛锌赡茉谇懊娼榻B的呼叫等待當(dāng)中,有可能在發(fā)送新的INVITE消息的時(shí)//候(OpRingingInvite接收到)讓程序陷入當(dāng)前OpRing-->OpAnswerCall的狀態(tài),把當(dāng)前的//這樣在當(dāng)前的這個(gè)操作中把處于等待的Call
ID消除。
UaDevice::instance()->setCallId( callWaitingId );
UaDevice::instance()->removeCallWaitingId( *callWaitingId );
}
// 取得遠(yuǎn)端的SDP
Sptr remoteSdp;
remoteSdp.dynamicCast ( msg->getContentData(0) );
call->setRemoteSdp( new SipSdp( *remoteSdp ) );
StatusMsg status( *msg, 200/*OK*/ );
// 根據(jù)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 );
//根據(jù)遠(yuǎn)端回傳的SDP創(chuàng)建本地的SDP
Sptr localSdp;
localSdp.dynamicCast ( status.getContentData(0) );
//本地的SDP設(shè)置不為空的情況
if ( localSdp != 0 )
{
//設(shè)定本地的RTP端口號(hào)
localSdp->setRtpPort( UaDevice::instance()->getRtpPort() );
// 設(shè)定RTP包的傳輸速率
int rtpPacketSize = UaConfiguration::instance()->getNetworkRtpRate();
//取得SDP描述符(會(huì)話符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();
}
//采用缺省的方式來建立媒體名和傳送地址,這里當(dāng)主叫和被叫沒有公共的媒體格式的時(shí)候,//被叫返回媒體流的"m"行,設(shè)置端口為0并且不返回載荷類型。
(*mediaIterator)->addFormat( 0 );
// MediaAttributes表示所有"a="和"a=rtpmap:…"的集合
MediaAttributes* mediaAttrib
//取得媒體流的會(huì)話屬性列表:(所有的"a="的列表)
//a=rtpmap : <載荷類型> <算法名稱> / <時(shí)鐘采樣頻率> [/<帶入?yún)?shù)>]
mediaAttrib = (*mediaIterator)->getMediaAttributes();
if ( mediaAttrib != 0 )
{ //取得一個(gè)單純的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的情況,就表示要設(shè)置時(shí)長,那么也就是要設(shè)定RTP的幀長
if ( strcmp( attribName, "ptime" ) == 0 )
{
rtpPacketSize = Data((*attribIterator)->getValue()).convertInt();
break;
}
attribIterator++;
}
mediaAttrib->flushValueAttributes();
mediaAttrib->flushrtpmap();
//以上是根據(jù)原段對段遠(yuǎn)端的SDP來獲得相關(guān)的會(huì)話屬性值,下面是如何將這些會(huì)話屬性值//加入當(dāng)前的本地的 SDP的會(huì)話屬性列表"a="當(dāng)中
//設(shè)定本地的簡單的"a=<屬性>:<值> "
ValueAttribute* attrib = new ValueAttribute();
attrib->setAttribute( "ptime" );
LocalScopeAllocator lo;
attrib->setValue( Data( rtpPacketSize ).getData(lo) );
//增加a=rtpmap : <載荷類型> <算法名稱> / <時(shí)鐘采樣頻率> [/<帶入?yún)?shù)>]
//在本地的a=rtpmap:當(dāng)中
SdpRtpMapAttribute* rtpMapAttrib = new SdpRtpMapAttribute();
rtpMapAttrib->setPayloadType( 0 );
rtpMapAttrib->setEncodingName( "PCMU" );
rtpMapAttrib->setClockRate( 8000 );
mediaAttrib->addValueAttribute( attrib );
mediaAttrib->addmap( rtpMapAttrib );
}
else//如果mediaAttrib為0的情況,也就是"a="項(xiàng)目為空的情況
{
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傳輸速率,創(chuàng)建一個(gè)a=ptime:<分組時(shí)間>的對話屬性。 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);
// 增加新創(chuàng)建的會(huì)話屬性到本地的SDP當(dāng)中
mediaAttrib->addValueAttribute(attrib);
mediaAttrib->addmap(rtpMapAttrib);
}
localSdp->setSdpDescriptor(sdpDesc);
//回送OK給主叫端,并且通告本地的SDP
call->setLocalSdp( new SipSdp( *localSdp ) );
deviceEvent->getSipStack()->sendReply( status );
}
else // 根據(jù)遠(yuǎn)端創(chuàng)建的本地的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();
//重新構(gòu)造一個(gè)本地的SDP發(fā)送給主叫端,該媒體屬性和主叫方的處于一致。
doAnswerStuff(sdp, remoteSdp, hostAddr, rtpPort);
//在狀態(tài)中回送本地的SDP
status.setContentData( &sdp, 0 );
call->setLocalSdp( new SipSdp( sdp ) );
deviceEvent->getSipStack()->sendReply( status );
}
//轉(zhuǎn)移工作狀態(tài)到StateInCall
Sptr < UaStateMachine > stateMachine;
stateMachine.dynamicCast( event->getCallInfo()->getFeature() );
assert( stateMachine != 0 );
return stateMachine->findState( "StateInCall" );
}
3.3.5 回到StateInCall狀態(tài)
最后程序被叫和主叫所進(jìn)入的狀態(tài)都回到StateInCall狀態(tài),在這個(gè)狀態(tài)里,被叫接收主叫發(fā)送的ACK消息,如果在其中含有新的SDP的話,就按照新的SDP進(jìn)行處理,否則,不就按照UaCallInfo中所包含的SDP的進(jìn)行處理。
4. 如何在改造現(xiàn)有的終端使之能傳遞視頻流。
目前的Vocal平臺(tái)是僅僅支持語音傳輸?shù),還沒有考慮到視頻部分,不過從整個(gè)協(xié)議棧的構(gòu)造來說,我嘗試過把當(dāng)前的SIP修改成一個(gè)語音/視頻一體的完整的視頻電話系統(tǒng)(如果想修改成會(huì)議系統(tǒng)的話,我們稍后在Conference
Server這節(jié)里面做介紹)只需要把當(dāng)前的協(xié)議棧部分增加大概1500-2000行左右的代碼(但不包括Codec部分)就可以完成,我們以增加一個(gè)H.261+能力作為我們對這個(gè)問題的討論點(diǎn):
4.1一個(gè)H.261+的Codec的基本構(gòu)造:
有參加過視頻壓縮和解壓縮算法開發(fā)工作的同志應(yīng)該知道,一般來說一個(gè)視頻通訊的基本類包括一個(gè)如下的流程:
以O(shè)penh323中實(shí)現(xiàn)的方式為例:(只敘述H.261+編碼部分)

(點(diǎn)擊放大)
4.2 增加視頻能力所需要做的工作
1.設(shè)備驅(qū)動(dòng)/壓縮/解壓縮部分:
a. 創(chuàng)建一個(gè)H.261的編碼實(shí)例,構(gòu)造一個(gè)P64的同步壓縮器,您可以從加洲大學(xué)或者是在OpenH323組織的網(wǎng)站上下載該算法的原代碼。
b. 在輸入上綁定一個(gè)標(biāo)準(zhǔn)的攝相頭部分,您可以按照一個(gè)標(biāo)準(zhǔn)輸入設(shè)備來做,也可以使用PWLIB來作為這個(gè)設(shè)備的輸入通道,進(jìn)行綁定,不過,如果使用PWLIB的時(shí)候整體效率會(huì)降低很多。
c. 最后把編碼類和攝相頭聯(lián)合構(gòu)造一個(gè)標(biāo)準(zhǔn)的輸出/輸入類(類似于SoundCard的實(shí)例一樣),如果這個(gè)標(biāo)準(zhǔn)類的調(diào)用接口方法可以構(gòu)造成和標(biāo)準(zhǔn)的聲音設(shè)備一樣,也可以構(gòu)造成在聲音設(shè)備之內(nèi)。
2.協(xié)議部分的改造:
對于SIP和H.323相比較而言,兩者在視頻通訊的概念上有很大的不同,SIP把所有的媒體訊息理解成相同的RTP流,區(qū)別只是帶寬不相同;而H.323則有一種專門的快速方式把兩者區(qū)分開,首先打開語音通道傳送音頻,然后打開視頻通道傳送視頻信息,兩者用不同的媒體通道傳輸。后者的最大好處在于,如果帶寬不足的話,至少可以傳遞語音信息到對方。當(dāng)然我們可以在SIP的協(xié)議前提下做適當(dāng)?shù)男薷哪7翲.323的運(yùn)行方式。
a. 媒體流的描述:
一個(gè)媒體信息的具體內(nèi)容包括:
一個(gè)H.261的視頻流描述例子:
m=video 513000 RTP/UDP 31
a=rtpmap:31 h261/90000
媒體類型:Video表示媒體類型,513000表示的是RTP端口號(hào),這個(gè)端口號(hào)從管理上來說最好和音頻不相同。
傳送協(xié)議:采用RTP/UDP上傳送,因?yàn)槟壳癝IP Stack只支持UDP還不支持AVP的格式;
媒體格式:在RTP中定義的靜態(tài)載荷類型文檔號(hào)為31;
媒體地址和端口:目的地址和RTP的端口號(hào)。
這里最主要改造的地方是OpInviteUrl方法,和用戶端回送OK消息的OpAnswerCall,這里是創(chuàng)建初始的視頻描述的操作,在他們中間需要增加對視頻流的描述,另外在被叫對主叫送來的INVITE消息中必須要檢測對方的媒體類型是否能解析,如果不行的話還要在OK消息中做相應(yīng)的拒絕返回。
例如:
主叫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部分的改造:
首先在現(xiàn)有的音頻RTP/RTCP會(huì)話基礎(chǔ)上增加一個(gè)視頻的RTP/RTCP會(huì)話,從前面介紹的我們知道視頻和音頻一般來說是不在一個(gè)RTP端口的,那么我們?yōu)榱诵麻_的視頻RTP端口當(dāng)然需要捆綁一個(gè)RTPSession(會(huì)話),在這里我們需要重寫視頻設(shè)備實(shí)例的ProcessRTP方法,換一句話來說,就是視頻和音頻設(shè)備有自己的ProcessRTP方法,分別從不同的端口進(jìn)程讀取相應(yīng)的媒體數(shù)據(jù)流,另外相對視頻信號(hào)而言,視頻流的RTP幀肯定相對要大一些,不過為了保證聲音/視頻同步,一般來說兩者的時(shí)長還是需要相等(一般是以20ms的數(shù)據(jù)幀,在這個(gè)時(shí)長內(nèi)聲音/視頻的RTP的頭和內(nèi)容的比例還是比較均勻的,不過這樣的話,要使用Jitter的方式,但是實(shí)際上,視頻/音頻的時(shí)間上并不能做到完全的相等,所以在聲/象同步上并不能完全依賴SSRC,需要在設(shè)計(jì)時(shí)采用同步緩沖的方式,大家有興趣的話可以參加一些RSVP工程組中有關(guān)于QoS改善的討論)。
這樣如果分別有自己的ProcessRTP方法,那么前面的說道的在視頻流中如果發(fā)生需要保證音頻帶寬而需要放棄視頻帶寬的情況,這樣的情況我們就很好處理了,在SIP協(xié)議中并不保證主叫可以單獨(dú)打開一個(gè)音頻通道,那么我們只能在RTP/UDP這一層來完成,可以利用RTCP中的APP分組來完成這個(gè)通告,比如主/被一端想終止視頻或者音頻通訊,在RTCP通道中發(fā)送一個(gè)自定義的APP分組就可以了。
但是這樣在初始化階段,如果RSVP在路徑上沒有預(yù)留足夠的資源,那么在開始的視頻和音頻通訊就可能會(huì)造成阻塞,可能造成用戶的媒體通訊超時(shí),而不能向H.323那樣可以先通過快速方式打開一個(gè)音頻通道(0號(hào)通道),而后再開啟視頻通道,所以上述的方法對于會(huì)話初始階段毫無用處。
c. 聲象同步:
任何一個(gè)商業(yè)成功的商業(yè)運(yùn)作的視頻通訊軟件中都需要比較好的去解決聲象同步這
個(gè)重要的問題,我們一般建議采用的方式是建立FIFO隊(duì)列緩沖,根據(jù)RTP包的SSRC重新排列這些分組,不過如果在操作系統(tǒng)中采用了Direct X或者是直接讀寫FrameBuffer技術(shù)的話,那么FIFO也就不能達(dá)到您想直接提高圖象處理速度的能力.
3.QoS的提高:
最后說一下Qos的提高工作,視頻通訊的處理不但消耗大量的系統(tǒng)資源,也要消耗大量的網(wǎng)絡(luò)資源,當(dāng)然按照前面所說的簡單的對QoS進(jìn)行處理當(dāng)然是不行的,需要采取的策略有以下兩種:
a. RSVP上的改善:視頻通訊預(yù)留的帶寬要比原來音頻的要大很多了,而且我們必須考慮到H.261/263協(xié)議中的幀間幀(IntraFrame)的情況,會(huì)產(chǎn)生突發(fā)性的帶寬需求,所以預(yù)留帶寬需要比平均帶寬高30%左右,另外,對RESC/PATH消息對必須在通訊中周期性的傳送,確保證實(shí)帶寬(必須考慮消息對所占用的一定帶寬)。
b. RTP信道上的改善:我們通過RTCP中的SR/RR分組了解到了丟失率,累計(jì)分組數(shù),到達(dá)時(shí)延抖動(dòng)等等QoS信息,我們可以通過這些信息對通訊終端的視頻信號(hào)進(jìn)行一定的控制,比如在發(fā)現(xiàn)QoS降低時(shí),可以以降低量化度或單位時(shí)間幀數(shù)來降低帶寬負(fù)載。
參考文獻(xiàn):
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網(wǎng)絡(luò)電話技術(shù)(人民郵電出版社)
視頻壓縮與視頻編碼技術(shù)(中國電力出版社)
(完)
作者聯(lián)系方法:lu_zheng@21cn.com
作者供稿 CTI論壇編輯