音頻傳輸之Jitter Buffer設計與實現

在語音通訊中Jitter Buffer(下面簡稱JB)是接收側一個很是重要的模塊,它是決定音質的重要因素之一。一方面它會把收到的亂序的語音包排好序放在buffer里正確的位置上,另外一方面它把接收到的語音包放在buffer中緩衝一些時間使播放的更平滑從而得到更好的語音質量。下圖是JB在接收側軟件框圖中的位置。緩存

 

從上圖能夠看出,從網絡上收到的語音包會放在JB裏(這個操做叫作PUT),在須要的時候便從JB裏取出來(這個操做叫作GET)解碼直到播放出來。JB有兩種模式:adaptive(自適應的)和fixed(固定的)。Adaptive是指buffer的大小能夠根據網絡環境的情況自適應的調整;fixed是指buffer的大小固定不變。自適應的模式實現難度大,要求高,fixed相對簡單,如今基本上都用adaptive的模式。JB在生命週期裏也有兩種狀態:prefetching(預存取)和processing(處理中),只有在processing時才能從JB中取到語音幀。初始化時把狀態置成prefetching,當在JB中的語音包個數達到指定的值時便把狀態切到processing。若是從JB裏取不到語音幀了,它將又回到prefetching。等buffer里語音包個數達到指定值時又從新回到processing狀態。網絡

 

首先看PUT操做。RTP包有包頭和負載(payload),爲了便於處理,將包頭和payload在buffer中分開保存,保存包頭中相關屬性的叫attribute buffer,保存payload的叫payload buffer。下圖是JB裏存RTP包的buffer關係圖:oop

  

要明確哪幾種類型的RTP包會被PUT進JB,我最初設計JB時類型有G711/G722/G729/SID(靜音包)/RFC2833(DTMF包)。G711/G722十毫秒payload是80個字節,G729十毫秒payload是10個字節,當VAD使能時十毫秒payload是2個字節(G729 VAD是內置的)或0個字節(DTX),一個SID包payload是1個或11個字節,一個RFC2833包payload是4個字節,明確這些是爲了肯定payload buffer中一個block的大小(取這些類型中最大的,80個字節),attribute buffer中一個block的大小是固定的,即要保存的屬性的個數(這些屬性主要用於控制payload的存放和讀取,有media type(G711/G722/G729/SID/RFC2833),sequence number,timestamp,ssrc,payload size,相對應的存放payload的buffer block指針等。每一個RTP的包頭佔一個attribute buffer block,但每一個RTP的payload有可能佔幾個payload buffer block,這跟media type 和packet time有關,例如一個packet time爲20ms的G711包,就須要兩個payload buffer block,attribute buffer block和payload buffer block之間有一個映射關係。將attribute buffer block和payload buffer block個數都定爲256(index從0到255,設定256是爲了早到的包毫不會把前面的包給覆蓋掉,若是block個數小了則有可能),這樣JB 裏最少能夠存2560ms的語音數據。 fetch

 

至於JB裏最多能放多少個包(即容量capacity),這取決於media type和packet time。若是media type是G711/G722, capacity = 256*10/packet time,例如當packet time爲20ms時,capacity是128,即最多放128個包。這樣attribute buffer和payload buffer的映射關係以下圖:ssr

       

若是media type是G729,考慮到packet time 一般不會超過160ms, 就設定一個G7299包的payload佔2個block(160個字節,通常是存不滿的),這樣capacity就是128(256/2)。至於SID和RFC2833包,payload只有幾個字節,爲了處理簡單,它們的payload佔幾個block是跟着語音包走的,好比一個20ms的G711語音包payload佔2個block,SID包和RFC2833包的payload也會佔2個block。設計

 

從網絡上來的RTP包有多是亂序的,PUT操做要把這些亂序的包(attribute & payload)放在buffer里正確的block裏,這主要依靠attribute裏的sequence number和timestamp作判斷。RTP協議裏sequence number數據類型是unsigned short,範圍是0~65535,就存在從65535到0的轉換,這增長了複雜度。對於收到的RTP包,首先要看它是否來的太遲(相對於上一個已經取出的包),太遲了就要把這個包主動丟棄掉。設上一個已經取出的包的sequence number爲 last_got_senq,timestamp 爲last_got_timestamp,當前收到的將要放的包的sequence number爲 cur_senq,timestamp 爲cur_timestamp,當前包的sequence number與上一個取走的sequence number的gap爲delta_senq,則delta_senq能夠根據下面的邏輯關係獲得。指針

 

若是delta_senq小於1,就能夠認爲這個包來的太遲,就要主動丟棄掉。因爲咱們的buffer足夠大(256個block),若是包早到了也會被放到對應的position上,不會把相應位置上的還沒取走的覆蓋掉。調試

 

接下來看怎麼把包放到正確的位置上。對於收到的第一個包,它的位置(position,範圍是0 ~ capacity-1)是sequence number % capacity。後面的包放的position依賴於它上一個已放好的包的position。設上一個已放好的包的sequence number爲 last_put_senq,timestamp 爲last_put_timestamp,position爲last_put_position,當前收到的將要放的包的sequence number爲 cur_senq,timestamp 爲cur_timestamp,position爲cur_position,當前的包的sequence number與上一個放好的sequence number的gap爲delta_senq,則cur_position能夠根據下面的邏輯關係獲得。code

 

獲得了當前包的position後就能夠把包頭裏的timestamp等放到相應的attribute buffer block裏了,payload根據算好的佔幾個block放到相應的那幾個block上(有可能填不滿block,不過不要緊,取payload時是根據index取的)。若是放進對應block時發現裏面已經有包了而且sequence number同樣,說明這個包是重複包,就要把這個包主動丟棄掉。blog

 

再來看GET操做。每次從JB裏不是取一個包,而是取1幀(能編解碼的最小單位,一般是10ms,也有例外,好比AMR-WB是20ms),這主要是由於播放loop是10ms一次(每次都是取一幀語音數據播放)。取時老是從head上取,開始時head爲第一個放進JB的包的position,每取完一個包(幾幀)後head就會向後移一個位置。若是到某個位置時它的block裏沒有包,就說明這個包丟了,這時取出的就是payload大小就是0,告訴後續的decoder要作PLC。不一樣類型的包取法不同,下面分別加以介紹。

 

對於G711/G722,每次從payload buffer裏取10ms數據(一個block, 80個字節),一個包取完後取下一個包。對於G729,每次從payload buffer裏取10ms數據(10個字節或2個字節(VAD使能後的靜音payload)或0個字節(DTX)),一個包取完後取下一個包。至於VAD使能後取10個字節仍是2個字節仍是0個字節,要取決於當前包以及上一包的payload size。這處理好能顯著提升G7229 VAD使能場景下的語音質量MOS值。以packet time爲20ms爲例,若是上一個包的payload size是20個字節,當前包的payload size是12個字節,在取時前10ms取10個字節,後10ms取2個字節。若是上一個包的payload size是12個字節,當前包的payload size是10個字節,在取時前10ms取0個字節(DTX),後10ms取10個字節。

 

對於SID包,每次都是從當前包中取相同的payload一直到發現JB裏這個SID包後面又有包而且timestamp又大於等於這個包的timestamp,下一次就會從這個新包裏取payload。對於RFC2833包,包裏有個duration attribute,當前RFC2833包和上一個RFC2833包的duration相減再除以80就是當前包的packet time,根據這算是從這個包裏取得次數,次數到後就從下一個包取。

 

上面說過如今JB通常都是用adaptive的mode,即buffer size(緩存包的個數)根據網絡環境自適應的調整大小。那怎麼來實現呢?JB初始化時會設定一個緩存包的個數值(叫prefetch),並處於prefetching狀態,這種狀態下是取不到語音幀的。JB裏緩存包的個數到達設定的值後就會變成processing狀態,同時能夠從JB裏取語音幀了。在通話過程當中因爲網絡環境變得惡劣,GET的次數比PUT的次數多,GET完最後一幀就進入prefetching狀態。當再有包PUT進JB時,先看前面共有多少次連續的GET,從而增大prefetch值,即增大buffer size的大小。若是網絡變得穩定了,GET和PUT就會交替出現,當交替出現的次數達到必定值時,就會減少prefetch值,即減少buffer size的大小,交替的次數更多時再繼續減少prefetch值。

 

再來看一下在哪些狀況下須要reset JB,讓JB在初始狀態下開始運行。

1)當收到的語音包的媒體類型(G711/G722/G729,不包括SID/RFC2833等)變了,就認爲來了新的stream,須要reset JB。

2)當收到的語音包的SSRC變了,就認爲來了新的stream,須要reset JB。

3)當收到的語音包的packet time變了,就認爲來了新的stream,須要reset JB。

 

前面說過JB是語音通訊接收側最重要的模塊之一,固然它也是容易出問題的模塊之一。出問題不怕,關鍵是怎麼快速定位問題。對於JB來講,須要知道當前的運行狀態以及一些統計信息等。若是這些信息正常,就說明問題很大可能不是由JB引發的,不正常則說明有很大的可能性。這些信息主要以下:

1)JB當前運行狀態:prefetching / processing

2)JB裏有多少個緩存的包

3)從JB中取幀的head的位置

4)緩衝區的capacity是多少

5)網絡丟包的個數

6)因爲來的太遲而被主動丟棄的包的個數

7)因爲JB裏已有這個包而被主動丟棄的包的個數

8)進prefetching狀態的次數(除了第一次)

 

上面就是JB設計的主要思想,在實現時還有不少細節須要注意,這裏就不一一詳細說了。我第一次設計實現JB是在2011年,當時從設計實現到調試完成(指標是:bulk call > 10000次,long call time > 60 小時,各類場景下的各類codec的語音質量要達標)總共花了近三個月,仍是在對JB有基礎的狀況下,要是沒基礎花的時間更多。從設計到能打電話時間不長,主要是後面要過bulk call/long call/voice quality。有好多狀況設計時沒考慮到,這也是一個迭代的過程,當調試完成了設計也更完整了。最初設計時只支持G711/G722/G729這三種codec,可是機制定了。後來系統要支持AMR-WB,JB這部分根據現有的機制再加上AMR-WB特有的很快就調好了。

相關文章
相關標籤/搜索