ULPFEC在WebRTC中的實現

ULPFECWebRTC中的實現

1. 前言

基於IP網絡的多媒體通信系統(比如WebRTC)中,網絡丟包對多媒體通信質量有非常嚴重的影響:例如造成視頻的馬賽克、圖像模糊、幀率下降等問題,造成音頻的聲音失真、噪聲干擾、音頻中斷等問題。這都會嚴重影響系統的通信質量,造成非常差的用戶體驗。

WebRTC主要採取兩種手段對抗網絡丟包:丟包重傳(NACK)和前向糾錯(FEC)。丟包重傳之前文章中有專門論述[1],本文集中注意力於FECFEC是一種前向糾錯技術,發送端將負載數據加上一定的冗餘糾錯碼一起發送,接收端根據接收到的糾錯碼對數據進行差錯檢測,如果發現差錯,則利用糾錯碼進行糾錯。而ULPFEC(Uneven Level Protection FEC,直譯爲非均等保護前向糾錯)則是WebRTC實現的FEC方案之一,本文深入學習ULPFEC的理論基礎和實現細節。

2. ULPFEC理論學習

ULPFEC由RFC5109[2]定義,WebRTC中以RED格式進一步封裝RTP中傳輸。該標準使用XOR操作基於多個多媒體數據包生成FEC數據包,然後接收端根據FEC數據包和已接收數據包恢復丟失的數據包。ULPFEC能夠針對不同的數據包提供不同的保護級別,從而對重要的數據包提供更多的保護[3]。

2.1 ULPFEC基本概念

ULPFEC數據包中包含發送端需要告知接收端的一些重要信息,包括本FEC數據包所保護的媒體數據、保護級別和每個級別的保護長度。特別地,FEC數據包針對每個保護級別k設置一個偏移量掩碼m(k),如果m(k)的第i位被設置爲1,則序列號爲N+i的媒體數據包FEC包的第k級別被保護。其中N爲基準序列號,FEC包中設置。第k級別保護的媒體數據大小由L(k)指示,該值也FEC包中設置。以上保護長度、偏移量掩碼、負載類型和基準序列號能夠完全確定生成FEC數據包中的奇偶校驗碼[2]。

一般來說,FEC是帶寬和保護力度的權衡,針對同樣的媒體數據,更多的FEC數據包意味着更有力的抗丟包保護,但同時也會消耗更多的帶寬。通常情況下,對於媒體數據包,不同部分的重要程度不一樣。因此,我們可以針對數據包的不同部分實施不同程度保護(即非均等保護前向糾錯),以充分利用帶寬資源。更多帶寬花費更重要的數據部分,相反,較少帶寬花費不那麼重要的數據部分。媒體數據包根據重要程度劃分爲若干部分,每個部分就是我們所說的保護級別,每個部分的長度即爲保護長度,每個FEC包可攜帶多個保護級別的奇偶校驗碼。根據數據包不同部分重要程度進行保護的算法,就是所謂的ULPFEC非均等保護前向糾錯。

圖1 ULP非均等保護前向糾錯.png

圖1很好說明了ULP的概念:FEC包1L0級保護數據包A和B,FEC包2L0級保護數據包C和D,同時L1級保護數據包A、B、C和D。注意FEC包1和FEC包2的保護數據包集合不一樣大,同時保護長度也不一樣長。

2.2 ULPFEC報文格式

ULPFEC報文由一個頭部和多個保護級別組成,每個保護級別包含級別頭部和負載,如圖2、3、4所示:

圖2 FEC報文格式.png

RTP頭部只有FEC報文通過單獨數據流發送時纔用到,這裏的RTP頭部格式遵循RFC3550的定義[4]。

圖3 FEC頭部格式.png

FEC頭部爲10字節,包含內容如下:
E flag:擴展位,供將來使用,當前設置爲0。
L flag:指示長偏移掩碼是否使用,0表示偏移掩碼爲16位,1表示爲48位。
P/X/CC/M/PT recovery field:由本FEC包所保護的所有媒體數據包的RTP頭部的P/X/CC/M/PT flag位經XOR操作後得到。
SN base:本FEC包所保護的媒體數據包的RTP報文的序列號最小值。
TS recovery field: 由本FEC包所保護的所有媒體數據包的RTP頭部中的Timestamp字段經XOR操作後得到。
Length recovery field: 由本FEC包所保護的所有媒體數據包的負載長度(包括CSRC、RTP頭部擴展、負載和padding的長度之和,以16位無符號網絡序表示)經XOR操作後得到。

圖4 ULP級別頭部格式.png

根據FEC頭部中E flag是否設置,FEC級別頭部長度爲4字節或8字節。Protection length爲2字節表示本級別所保護的媒體數據的長度;mask爲2字節(如E flag設置則爲6字節)表示偏移掩碼,指示本級別所保護的媒體數據包的分佈情況。如果偏移掩碼的第i位置爲1,則表示第N+i個媒體數據包本級別中受保護,其中N爲FEC頭部中的媒體數據基準序列號。

偏移掩碼的設置遵循以下規則:

a)媒體數據包高於0級別的等級中只能被保護一次,但是可以0級別中被多個FEC包保護,只要這些FEC0級別的保護長度相等。
b)如果媒體數據包p級別被保護,那麼它也必須p-1級別被保護。注意保護p級別的FEC包和保護p-1級別的FEC包可能不是同一個。
c)如果FEC包包含p級別保護,那麼它也必須包含p-1級別保護。注意p級別保護的數據包可能和p-1級別保護的數據包不是同一個。

規則a)把多重保護限定0級別,高於0級別的多重保護會減小保護效果並且增大接收端恢復數據的複雜度。規則b)限定媒體數據包受保護的連續性,即不存中間某段數據不受保護的媒體數據包。規則c)限定FEC數據包保護級別的連續性,即不存中間某個級別不保護數據的FEC數據包。

下面以圖5來簡單說明,我們可以看到,FEC包10級別保護了數據包A、B,而FEC包20級別保護了數據包C、D,同時1級別保護了數據包A、B、C和D。這都符合上述三條規則。

圖5 ULP非均等保護組合舉例.png

2.3 ULPFEC報文構造

通過對比RFC3550中RTP頭部的定義我們可以發現,FEC頭部和RTP頭部非常類似:除E/L flag之外,FEC頭部前8字節基本上和RTP頭部前8字節定義相同,而且其數據也來源於媒體數據包的RTP頭部(經XOR運算後得到)。而後兩字節length recovery也是對媒體數據RTP負載長度計算得到的。因此,FEC頭部就是它所保護的所有RTP報文的頭部經XOR計算後得到的。

據此,我們很容易得到FEC頭部的構造辦法:對於本FEC包保護的所有媒體數據包,針對其RTP頭部的前8字節進行XOR運算,最終結果根據格式定義填入到FEC頭部中。具體細節此不展開,詳情可參考RFC5109[2]。需要注意的是FEC頭部只保護RTP頭部的前12字節,對於CSRC和RTP Extentions部分,FEC將其視爲RTP負載部分進行保護。

對於保護級別的頭部,根據預先確定的本級別保護長度和保護媒體數據集合,分別填入保護長度字段和設置偏移掩碼字段。對於保護級別的負載部分,則是由本級別保護的媒體數據包的對應部分進行XOR運算後得到,然後填入負載位置。

2.4 ULPFEC報文發送

ULPFEC報文可採取兩種方式發送:1)使用獨立的RTP流發送;2)封裝RED報文中隨源媒體數據一起發送。WebRTC採用第二種方式。RED(Redundant Coding)是針對RTP負載數據的二次封裝,所以叫冗餘編碼,其定義RFC2198[5]中。RED有兩種數據封裝格式:Primary Data Block和Redundant Data Block,分別如圖6、圖7所示:

圖6 Redundant Data Block.png

F flag:指示本Block後續是否還有其他Block跟隨,1表示有,0表示無。
block PT:本Block 的Payload Type,也即原始RTP負載數據的PT。
Timestamp offset:本Block相對於原始RTP頭部中timestamp的偏移量。
Block length:本Block的長度。

圖7 Primary Data Block.png

Primary Data Block表示本Block之後再無Block。

FEC報文構造之後,會封裝爲RED格式,然後再進一步封裝爲RTP報文,最後隨其他RTP報文(也已經封裝爲RED格式)一起發送到網絡。接收端,RTP報文首先根據負載判斷爲RED報文後,進行解包操作,得到原始RTP/FEC報文,然後繼續接下來的流程。

2.5 ULPFEC報文恢復

報文恢復即是報文構造的逆過程,接收端RTP數據包經過RED解包操作後,得到原始RTP包或者FEC包,前者進一步發送到VCM模塊並存儲FEC處理模塊,後者則進行丟包檢測和數據包恢復工作。FEC包能夠媒體數據包丟失的情況下恢復,丟失的媒體數據包可以部分或全部恢復,這取決於實際的數據包丟失情況。丟失媒體數據包恢復需要兩步:1)確定恢復丟失媒體數據包所需要的FEC數據包和未丟失媒體數據包的集合,有多種算法可確定這個數據包集合。2)重建丟失的媒體數據包,這又包括重建RTP頭部和RTP負載。下面分別描述之。

重建RTP頭部。設S是0級別恢復數據包xi RTP頭部所需要的FEC數據包和媒體數據包的集合,則重建xi的RTP頭部的過程如下:1)對於S中所有的媒體數據包,針對其前10字節執行XOR運算得到媒體比特流M(最後2字節基於數據包長度進行XOR運算);FEC比特流F則爲S中FEC數據包的前10字節。2)針對媒體比特流M和FEC比特流F執行XOR運算,得到恢復比特流R。3)新建一個標準RTP數據包,其頭部長度爲12字節。4)根據RTP頭部和FEC頭部的關係,用恢復比特流R填充RTP頭部 ,其中RTP頭部最後4字節爲SSRC已經預先知道,直接填充即可。

重建RTP負載。設S是n級別恢復數據包xi負載所需要的FEC數據包和媒體數據包的集合,則重建xi的RTP負載的過程如下:1)從FEC包中獲取n級別的負載數據保護長度Ln。2)定義FEC比特流Fn爲FECn級別的FEC負載。3)根據n級別FEC包中的位置,可計算得到S中的媒體數據包受本FEC包保護的負載數據段的起始位置,綜合所有媒體數據包的負載數據段執行XOR運算,得到媒體比特流Mn。4)計算恢復比特流Rn爲媒體比特流Fn和FEC比特流Mn的XOR運算結果。5)根據當前保護級別n和保護長度Ln,把恢復比特流Rn填充到恢復媒體數據包的相應位置。至此,丟失數據包當前n保護級別的負載數據得到恢復;針對每個保護級別都執行上述操作,最終即可恢復整個丟失數據包。

更多ULPFEC報文構造和恢復的例子,可參考RFC5109[2]的第10節。

3. ULPFECWebRTC中實現

本節深度分析WebRTC 60 Codebase中ULPFEC的相關代碼,總結出其實現算法和細節。下面以Video爲例,從FEC報文構建、FEC掩碼構造和丟失數據包恢復三個方面分析ULPFECWebRTC中的實現

3.1 ULPFEC報文構建

WebRTC中ULPFEC報文構建的流程如圖8所示:

圖8 FEC報文構建和發送流程.png

FEC報文構建開始於編碼線程編碼完一幀數據的後處理,控制流程經PayloadRouter到達RtpSenderVideo::SendVideo()函數。如果當前會話配置了red和ulpfec,則調用SendVideoPacketAsRedMaybeWithUlpfec(),該函數主要做四件事:1)把本rtp數據包送入UlpfecGenerator,並嘗試構造FEC包;2)獲取1)構造的FEC包(已經封裝爲RED包)列表;3)把本RTP數據包封裝爲RED包併發送到網絡;4)把FEC包列表發送到網絡。其中2)構造RED包的過程很簡單,按照RFC2198的定義填充字段即可,3)和4)發送RED包到網絡也很簡單,此不再過多論述。接下來重點分析2)的FEC包的構造過程。

步驟2)會觸發ForwardErrorCorrection::EncodeFec()函數,該函數流程僞代碼如下所示:

ForwardErrorCorrection::EncodeFec(media_packets,protection_factor, 
  num_important_packets, use_unequal_protection, fec_mask_type, fec_packets) {
  // step 1, 根據媒體數據包個數和保護因子,確定需要生成的fec數據包個數並初始化。
  int num_fec_packets = NumFecPackets(num_media_packets, protection_factor);
  for (int i = 0; i < num_fec_packets; ++i) {
    memset(generated_fec_packets_[i].data, 0, IP_PACKET_SIZE);
    fec_packets->push_back(&generated_fec_packets_[i]); 
  }

  // step2, 構建fec掩碼錶,並從中獲取構造fec包需要的掩碼,存儲packet_mask_中。
  // 這一步是最關鍵的,packet_masks_決定媒體數據包FEC包中受保護的分佈情況。
  const internal::PacketMaskTable mask_table(fec_mask_type, num_media_packets);
  internal::GeneratePacketMasks(num_media_packets, num_fec_packets,
     num_important_packets, use_unequal_protection,  mask_table, packet_masks_);

  // 步驟3,以packet_masks_和packet_mask_size_,media_packets和num_fec_packets
  // 爲輸入,生成FEC包集合generated_fec_packets_,包括半成品的頭部和成型的負載。
  GenerateFecPayloads(media_packets, num_fec_packets);

  // 步驟4,填充並修正生成生成FEC數據包的頭部,這很簡單,按照RFC填充即可。 
  FinalizeFecHeaders(num_fec_packets, media_ssrc, seq_num_base);
}

FEC構造過程首先會根據輸入媒體數據包的個數m和保護因子factor,確定需要生成的FEC包的個數num_fec = m * factor / 256,然後初始化這num_fecFEC包。接下來是關鍵的一步,創建掩碼錶並獲取num_fecFEC包所需要的掩碼數據,存儲packet_masks_。掩碼錶是預先定義好的一張三維表格,用以模擬不同情況下媒體數據包FEC包中的保護分佈情況,3.2節會針對該問題進一步分析。

接下來,函數以掩碼信息packet_masks_,媒體數據包media_packets和fec包個數num_fec爲輸入調用GenerateFecPayloads()函數生成FEC數據包,此時FEC包中的負載部分已經成型,但是FEC頭部需要進一步修正,這需要最後一步調用FinalizeFecHeaders()解決。

至此,FEC包的構造過程分析完畢。需要注意的是,WebRTC僅僅使用ULPFEC的Level 0對媒體數據包進行保護,也即FEC包中只有一個FEC Level。另外,WebRTC內部,保護RTP頭部的FEC頭部稱之爲Level 0,而保護RTP負載的FEC Level部分稱之爲Level 1。這裏和FEC5109文檔中所定義的稍有不同,需要注意一下。

3.2 ULPFEC掩碼錶和掩碼

根據RFC5109中相關定義可知,一個媒體數據包可以由多個FEC包保護,而一個FEC包也可以多保護多個媒體數據包。假設m個媒體數據包需要n個FEC數據包保護,則可以定義一個二維的m * n零一矩陣來描述媒體數據包fec包中的保護分佈情況:矩陣中元素m[i, j]置1表示第i個媒體數據包需要第j個FEC包保護。從行角度來看,第i行元素表示第i個FEC包保護的媒體數據包的集合;從列角度講,第j列元素表示保護第j個媒體數據包的FEC包的集合。由於該矩陣是零一矩陣,因此存儲上可以採用掩碼來存儲。這個掩碼也就是FEC Level Header中所定義的掩碼。

那麼掩碼中的1該如何分佈。我們知道,現實世界中網絡丟包分爲隨機丟包和突發丟包兩種情況,FEC包需要能夠針對這兩種情況對媒體數據包進行保護。WebRTC預先構造兩個掩碼錶kPacketMaskRandomTbl和kPacketMaskBurstyTbl,以模擬隨機情況和突發情況下媒體數據包FEC包中的保護分配情況。假設隨機丟包場景下,對於m * n的情況,我們只需要從kPacketMaskRandomTbl[m][n]就可以獲取FEC包所需要的全部掩碼,然後該掩碼爲基礎,構造FEC數據包。

根據ULP思想,FEC包可以對媒體數據包集合中的不同數據包實施不同的保護力度。這源於一幀視頻數據編碼後生成的一系列RTP數據包中,其重要性是不一樣的,比如開始幾個RTP包的重要性更大一些。因此,WebRTC構造FEC包的掩碼時,有均勻保護和非均勻保護兩種策略。

對於均勻保護,所有RTP包的重要性一樣,FEC包對他們進行平等的均勻的保護。對於m * n,FEC包使用的掩碼即爲kPacketMaskRandomTbl[m][n]。對於非均勻保護,RTP包集合被非爲重要數據包集合S1和普通數據包集合S2,分配較多個FEC包來保護S1,較少個FEC包保護S2。WebRTC定義了三種模式針對實現非均勻保護:

1)kModeNoOverlay:非疊加保護,保護S1的掩碼和S2的掩碼相互分離。
2)kModeOverlay:疊加保護,保護S1的掩碼和S2的掩碼疊加一起。
3)kModeBiasFirstPacket:均勻保護的基礎上,所有FEC包都保護第一個包。

假設保護場景爲(m, n),其中重要數據包爲前k個,分配給重要數據包的FEC包個數爲t,掩碼錶爲mask_table。則三種場景下最終掩碼的確定如下:

1)kModeNoOverlay:mask_table[k][t]和mask_table[m-k][n-t]的移位組合。
2)kModeOverlay: mask_table[k][t]和mask_table[m][n-t]的拼接。
3)kModeBiasFirstPacket:mask_table[m][n],再第一列全部置1。

至此,關於ULPFEC的掩碼錶和掩碼分析完畢。

3.3 ULPFEC報文接收和數據包恢復

ULPFEC報文接收和丟失數據包恢復是ULPFEC報文構造和發送的逆過程,該過程分爲三個子過程:1)把接收到的RED包進行解包得到RTP包或FEC包。2)把RTP/FEC包插入到FEC處理模塊的合適列表中。3)發起數據包恢復嘗試。圖9描述這個過程。

圖 9 ULPFEC接收數據包和丟失數據包恢復.png

RTP數據包首先到達RtpStreamReceiver::ReceivePacket(),判斷出該RTP包爲RED包後,調用ParseAndHandleEncapsulatingHeader()。該函數主要做了兩件事:RED解包和處理包。前者很簡單,就是按照RFC2198去掉RED頭部,得到純RTP報文或者FEC報文。後者深入到UlpfecReceiver中進一步處理RTP/FEC報文。

ProcessReceivedFec()函數中,若接收到的是RTP包,則首先調用callback的OnRecoveredPacket()把RTP包發送到VCM模塊,以不耽誤解碼過程。接下來調用DecodeFec()執行FEC恢復數據包過程。最後,把新恢復的數據包送到VCM模塊進行解碼。

DecodeFec()函數做兩件事情:把數據包插入到合適列表,併發起丟失包恢復過程。FEC包插入到Fec列表,RTP包插入到Media列表。對於FEC包,還要根據其掩碼遍歷Media列表,把所有本FEC包保護的RTP包掛自己名下的列表中。對於RTP包,還要根據自己的序列號遍歷Fec列表,把本RTP包掛所有保護自己的FEC包名下的列表中。

接下來是嘗試恢復丟失數據包過程AttemptRecovery:對於Fec列表中的每一個FEC包,判斷其目前的丟包數:如果未丟包則無需恢復操作,刪除本FEC包繼續下一個;如果丟包數大於1,則條件還不成熟,跳過本FEC包繼續下一個;如果丟包數等於1,則調用RecoveryPacket()恢復一個RTP數據包。然後把該恢復包插入到Media列表中,並根據該RTP包的序列號遍歷Fec列表,把該RTP包掛所有保護自己的FEC包名下的列表中。

最後是真正執行恢復數據包過程的RecoveryPacket()。流程走到這裏,所有條件都已經成熟,按照RFC5109的定義按部就班恢復數據包即可:1)準備階段:把本FEC包中的頭部數據和payload數據拷貝到recover_packet中作爲基準數據。2)恢復階段:針對本FEC包保護的每個已收到RTP包,recover_packet包與之執行XOR操作,結果仍然存儲recover_packet中。全部RTP包執行完XOR操作以後,recover_packet中的負載部分即爲待恢復RTP包的負載數據。3)收尾階段:修正recover_packet的RTP頭部,得到最終恢復的RTP包。

至此,一個完整的接收端恢復丟失數據包的過程分析完畢。

4. 總結

本文首先從理論上學習RFC5109關於ULPFEC的定義,然後深入分析WebRTC源碼,學習其ULPFEC的實現細節。通過本次學習,對ULPFEC有深入徹底的理解,爲後續學習音視頻技術和WebRTC其他模塊奠定基礎。

參考文獻

[1] WebRTC中丟包重傳NACK實現分析
       http://www.jianshu.com/p/a7f6ec0c9273
[2] RTP Payload Format for Generic Forward Error Correction
       https://tools.ietf.org/html/rfc5109
[3] ULPFEC (Uneven Level Protection Forward Error Correction)
       https://webrtcglossary.com/ulpfec/
[4] RTP: A Transport Protocol for Real-Time Applications
       https://tools.ietf.org/html/rfc3550
[5] RTP Payload for Redundant Audio Data
       https://tools.ietf.org/html/rfc2198

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章