在Vovida的基礎上實現自己的SIP協議棧(六)

 在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;
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;
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;
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]

發佈了14 篇原創文章 · 獲贊 1 · 訪問量 6萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章