webrtc代码走读五(JitterBuffer)
一、 什么是JitterBuffer
????????Jitter Buffer也叫做抖動緩沖區,它是實時音視頻里面的一個重要模塊,它對數據包丟失、亂序、延遲到達等情況進行處理,平滑的向解碼模塊輸出數據包/幀,抵抗各種弱網情況對播放/渲染造成的影響,降低卡頓,提高用戶的觀看體驗。
二、JitterBuffer在音視頻系統中的位置
????????JitterBuffer在實時音視頻系統中的位置如下所示:
?三、 視頻JitterBuffer的工作原理
1. JitterBuffer的核心思想
????????Jitter buffer的核心思想是用時間換空間,以增大端到端的延遲為代價來換取視頻通話的流暢性。當網絡不穩定時(抖動發生),增加buffer的長度,多緩存一些數據,以應對將來可能發生的抖動;當網絡穩定下來時,減小buffer的長度,少緩存一些數據,降低視頻端到端的延遲,提高實時性。因此jitter buffer的運行過程是一個根據抖動來動態調整buffer長度的過程。好的jitter buffer能夠在保證盡量不卡的前提下降低端到端的延遲,即它能夠在延遲和卡頓率之間取得較好的平衡。
2. 產生抖動的原因
????????1) 網絡傳輸路徑改變。例如,當前的傳輸路徑是A,但是下一刻路徑A上的某個路由器出現了故障,這時候數據包的路徑就會發生改變,導致端到端的傳輸時長發生變化。
????????2) 網絡自身的抖動。很多情況下網絡有噪聲,產生抖動是很正常的。
????????3) 網絡發生擁塞。擁塞發生的時候,數據包會在路由器上排隊,導致端到端延遲變大。
????????4) 抗丟包手段帶來的額外抖動。網絡出現丟包的時候,我們一般會使用nack/arq去重傳數據,重傳會帶來額外的延遲。
?3. 計算抖動的方法
????????數據包傳輸時長的變化就是抖動,假設相鄰的兩個數據包packet1和packet2,它們發送時間戳是send_timestamp1和send_ timestamp2,接收時間戳是recv_ timestamp1和recv_ timestamp2,那么它們之間的抖動可以按照下面的方法計算:
這是最簡單的計算方法,要想準確計算出網絡抖動還需要考慮很多因素,這里不再贅述。
4. JitterBuffer的工作原理
1) 接收側收到數據包,開始組幀,這一步是必須的,幀不完整會導致花屏。
2) 每個幀組好之后,放進buffer里,然后按照幀序號進行排序。
3) 檢查幀的參考關系。對于解碼器來說,如果一個幀的參考幀丟失了,那么這個幀將解碼失敗或者花屏,所以參考關系必須要滿足之后才能把數據送進解碼器里。
4) 根據每一幀的時間戳(采集時間戳或者發送時間戳)以及接收時間戳計算抖動。這里的難點在于如何精確計算抖動。
5) 根據抖動計算buffer的長度。
6) 根據抖動自適應的調整buffer長度。抖動越大,預留的buffer長度越大,這樣可以利用增加延遲的方式來降低卡頓;抖動越小,預留的buffer長度越小,這樣可以降低延遲。
四、淺析webrtc里的視頻JitterBuffer
?1.WebRTC里視頻JitterBuffer的運行機制
????????Jitterbuffer被兩個線程操作,寫線程負責組幀完成之后把數據寫入JitterBuffer里,讀線程負責從JitterBuffer里讀取數據然后解碼。
寫線程:
1) 判斷當前視頻幀是否有效,把幀插入buffer里,然后移除buffer里過期的、無效的幀;
2) 判斷幀之間的參考關系是否已經滿足;
3) 如果當前幀可以解碼,那么激活解碼線程(讀線程)。
讀線程:
1) 找到buffer中第一個可以解碼的幀(假設它是frame):如果這個幀的渲染時間戳是無效的,那么根據當前的抖動(開始的時候抖動值是0,它在步驟3中被更新)計算每個幀的渲染時間戳(render timestamp),并保存在幀信息中,然后根據這個幀的渲染時間戳和當前時間計算最大需要等待的時間(最大的等待時間不會超過200毫秒),然后休眠等待;
2) 如果在等待的時間內還有新的可以解碼的幀到來,那么重復步驟2,直到超時;
3) 根據frame的時間信息以及幀大小計算新的抖動值,并用這個抖動更新當前的抖動。
2. 計算抖動延遲
????????抖動延遲由網絡抖動延遲、解碼延遲、渲染延遲構成。其中,解碼延遲和渲染延遲比較穩定,網絡抖動延遲是動態變化的。計算網絡抖動是Jitterbuffer的核心之一。webrtc認為網絡抖動由兩個部分構成:?
1) 網絡噪聲帶來的抖動延遲,也叫做網絡排隊延遲。
2) 傳輸大的視頻幀(特別是關鍵幀)對網絡造成沖擊帶來的抖動延遲。
????????為了準確估算出抖動延遲,必須要估算出網絡排隊延遲和信道速率(通過信道速率可以計算大的視頻幀對網絡造成的沖擊所帶來的延遲) 。webrtc使用卡爾曼濾波估算網絡排隊延遲和信道速率。卡爾曼濾波是一種預測的算法,它以協方差為標準,根據上一時刻的系統狀態估算當前時刻系統的狀態,然后根據當前的測量值調整當前時刻系統的狀態,最后得到當前最優的系統狀態。它認為估算出來的值和測量出來值都是有偏差的,因此要根據一個偏好因子(卡爾曼濾波增益系數)來判斷我們最后需要的值更加偏向于估計值還是測量值。由于卡爾曼濾波比較復雜,這里并不打算深入探討,下面介紹一下使用卡爾曼濾波計算網絡抖動延遲的大致流程:
????????1) 抖動的計算與信道速率、網絡排隊延遲有關,因此要計算抖動,就必須先計算信道速度和網絡排隊延遲。
????????2) 把信道速率和網絡排隊延遲當作系統狀態,算法的目標就是估算出最優的信道速度和網絡排隊延遲。假設系統是一個線性系統,如果網絡非常好,那么很容易估算出當前系統的狀態等于上一個時刻的系統狀態,也就是說信道速度和網絡排隊延遲保持不變。
????????3) 但是實際上網絡是動態變化的,因此需要對估算出的這個系統狀態(即信道速度和網絡排隊延遲)進行調整。
? ? ? ? 4)調整的具體方式:
? ? ? ? 5)根據抖動延遲的觀測值(兩幀傳輸時長的變化值)和預測值(根據上一個系統狀態推導出來),計算它們的殘差;
? ? ? ? 6)利用殘差計算網絡噪聲;
? ? ? ? 7)?根據抖動延遲觀測值、前后兩幀大小差值、網絡噪聲、系統誤差協方差等計算卡爾曼增益系數。
? ? ? ? 8)利用卡爾曼增益系數更新系統狀態(即信道速率和網絡排隊延遲)。
? ? ? ? 9)根據更新后的系統狀態計算抖動延遲:
????????3. 根據抖動延遲計算視頻幀的渲染時間?
????????得到網絡抖動延遲之后,計算總的抖動延遲:jitter_delay = net_jitter_delay + decode_delay + render_delay。然后根據抖動延遲和當前的時間,計算什么時候渲染當前的視頻幀,然后根據渲染時間和當前時間確定當前幀在解碼之前需要等待的時間(wait_time),通過wait_time保證了各個視頻之間是平滑的,減少了卡頓。另外在等待的時間內也可以緩存更多的視頻幀,避免了下一次遇到弱網時再次卡頓。
- PacketBuffer:負責幀的完整性,保證組成幀的每個包序列號連續,并且有一個包標識幀的開始,有一個包標識幀的結束;
?????????
- RtpFrameReferenceFinder:負責給每個幀設置好參考幀,同時兼顧GOP內各幀的連續性;
- FrameBuffer:負責幀的連續性和可解碼性,這里幀的連續性是指某幀的所有參考幀都已經收到,幀的可解碼性是指某幀的所有參考幀都已經被解碼;
?
- VCMJitterEstimator:計算抖動(googJitterbufferMS),用于計算目標延遲(googTargetDelayMs),用于音視頻同步;
?????????
- VCMTiming:計算當前延遲(googCurrentDelayMs),用于計算渲染時間。
?
?五、?JitterBuffer結構和基本流程
?????????RtpVideoStreamReceiver類收到RTP包后,交給PacketBuffer類緩存、排序。
????????PacketBuffer收集滿1個完整的幀后,交還給RtpVideoStreamReceiver類。
????????RtpVideoStreamReceiver類將一個完整的幀交給RtpFrameReferenceFinder。
????????RtpFrameReferenceFinder類緩存最近的GOP,每個完整幀落在一個GOP中會填充好該幀的參考幀,交還給RtpVideoStreamReceiver。
????????RtpVideoStreamReceiver將填充好參考幀的完整幀交給FrameBuffer,FrameBuffer判斷某幀的所有參考幀都收到認為該幀連續,在某幀的所有參考幀都解碼后認為該幀可以解碼,從而可以交給解碼器。
????????可以認為JitterBuffer的這些模塊分三個層次分別做了RTP包的排序、GOP內幀的排序、GOP之間的排序:
- 包的排序:PacketBuffer;
- 幀的排序:RtpFrameReferenceFinder;
- GOP的排序:FrameBuffer。
六:幀完整性 - PacketBuffer
? ????????6.1?包緩存
????????PacketBuffer類有兩個類型的包緩存:
- std::vector data_buffer_,數據緩存,保存包原始數據,用于拼接整幀原始數據;
- std::vector sequence_buffer_,排序緩存,保存包連續性信息,用于緩存包序列號等信息并排序成完整的幀。?
?????????6.2? 幀的開始和結束?
?????????
????????在這里重點強調一幀第一個包的標識是因為該標識對判斷幀的完整性有重要作用,另外,一幀的最后一個包就是簡單根據RTP頭中的marker位來標識,只有在第一個包、最后一個包都取到并且中間的所有包都連續的情況下,才認為是一個完整的幀。
? ? ? ? 6.3? 插入RTP數據包 - PacketBuffer::InsertPacket?
??????????數據緩存、排序緩存這兩個包緩存都是初始長度為size_(512)的數組,一旦緩存滿會倍增容量,直到達到最大長度max_size_(2048)。
????????插入包的過程就是把數據填入這兩個緩存的過程,同時會判斷是否出現丟包,如果出現丟包則等待,在沒有出現丟包的情況下,會判斷是否已經獲得了完整的幀,如果已經組裝好了若干完整的幀,則通過OnAssembledFrame回調通知RtpVideoStreamReceiver。
????????
????????
? ? ? ? ?6.4??丟包檢測 - PacketBuffer::UpdateMissingPackets
????????PacketBuffer維護一個丟包緩存missing_packets_,主要用于在PacketBuffer::FindFrames中判斷某個已經完整的P幀前面是否有未完整的幀,如果有,該幀可能是I幀,也可能是P幀,這里并不會立刻把這個完整的P幀向后傳遞給RtpFrameReferenceFinder,而是暫時清除狀態,等待前面的所有幀完整后才重復檢測操作,所以這里實際上也發生了幀的排序,并產生了一定的幀間依賴。?????????
???????????6.5?連續包檢測 - PacketBuffer::PotentialNewFrame
PacketBuffer::PotentialNewFrame(uint16_t seq_num)函數用于檢測seq_num前的所有包是連續的,只有包連續,才進入完整幀的檢測,所以叫“潛在的新幀檢測”。?
?????????
????????6.6 幀完整性檢測 - PacketBuffer::FindFrames?
????????
?????????PacketBuffer::FindFrames函數會遍歷排序緩存中連續的包,檢查一幀的邊界,但是這里對VPX和H264的處理做了區分:
? ? ? ? 1)對VPX,這個函數認為包的frame_begin可信,這樣VPX的完整一幀就完全依賴于檢測到frame_begin和frame_end這兩個包
? ? ? ? 2)另外這里對H264的P幀做了一些特殊處理,雖然P幀可能已經完整,但是如果該P幀前面仍然有丟包空洞,不會立刻向后傳遞,會等待直到所有空洞被填滿,因為P幀前面可能有I幀,如果I幀還不完整,即使向后傳遞也無法解碼。
????????
????????七:總結
? ? ? ? 1)PacketBuffer::InsertPacket向包緩存插入RTP數據,并觸發幀完整性檢查;
? ? ? ? 2)PacketBuffer::PaddingReceived處理空包,并觸發幀完整性檢查;
? ? ? ? 3)PacketBuffer::UpdateMissingPackets,更新丟包信息,用于檢查P幀前面的空洞;
? ? ? ? 4)PacketBuffer::PotentialNewFrame,判斷包的連續性,只有連續的包才檢查幀完整性;
? ? ? ? 5)PacketBuffer::FindFrames,幀完整性檢查,如果得到完整幀,則通過OnAssembledFrame回調上報。
八? 查找參考幀 - RtpFrameReferenceFinder
????????
????????上圖描述了RtpFrameReferenceFinder的基本工作原理,顧名思義,RtpFrameReferenceFinder就是要找到每個幀的參考幀。I幀是GOP起始幀自參考,后續GOP內每個幀都要參考上一幀。?
?????????RtpFrameReferenceFinder維護最近的GOP表,收到P幀后,RtpFrameReferenceFinder找到P幀所屬的GOP,將P幀的參考幀設置為GOP內該幀的上一幀,之后傳遞給FrameBuffer。
????????RtpFrameReferenceFinder還保證GOP內幀的輸出連續,對H264來說,每收到一幀都判斷該幀的第一個包的序列號是否與之前GOP收到的最后一個包序列號連續,是則輸出連續幀,否則緩存等待直到連續。對VPX,只需要簡單判斷PID是否連續即可。這種連續傳遞的依賴關系會導致GOP內任一幀丟失則GOP內的剩余時間都處于卡頓狀態。
???????8.1 圖像ID - PID
?????????PID(Picture ID)是每幀圖像的唯一標識,VPX定義了PID,但是H264沒有這個概念,RtpFrameReferenceFinder使用每幀的最后一個包的序列號作為H264幀的PID。
????????在一個GOP內,除了I幀、P幀之外,可能還有WebRTC為補償發送碼率填充的空包,也會占用一個序列號。I幀是GOP的開始,沒有連續性問題,但是要判斷當前收到的P幀是否連續則需要判斷該P幀的第一個包序列號-1是否等于該GOP當前收到的最后一個包序列號,可能是上一幀的最后一個包,也可能是一個填充包。
????????RtpFrameReferenceFinder定義的的GOP表結構:
????????keyvaluelast_seq_num:I幀最后一個包序列號,PIDlast_picture_id_gop:GOP內最新的一個幀的最后一個包的序列號, 用于設置為下一個幀的參考幀。last_picture_id_with_padding_gop:GOP內最新一個包的序列號,有可能是last_picture_id_gop,也有可能是填充包,用于檢查幀的連續性。
????????8.2 設置參考幀 - RtpFrameReferenceFinder::ManageFramePidOrSeqNum
????????該函數用于檢查輸入幀的連續性,并且設置其參考幀。
?????????
????????8.4?處理填充包 - RtpFrameReferenceFinder::PaddingReceived?? ? ??
????????該函數緩存填充包,并更新填充包狀態,假如該填充包剛好填補了當前GOP的序列號空洞,則有可能有緩存的P幀進入連續狀態,所以嘗試處理一次緩存的P幀。
?????????
????????8.5 更新填充包狀態 - RtpFrameReferenceFinder::UpdateLastPictureIdWithPadding?
?????????8.6 處理緩存的包 - RtpFrameReferenceFinder::RetryStashedFrames????????????8.7 總結
????????RtpFrameReferenceFinder緩存GOP信息,每個幀(以及填充包)進入GOP排序,如果某個幀連續,則設置其參考幀為GOP內上一幀并輸出,I幀不需要參考幀,P幀需要參考幀?。
????????9 有序輸出 - FrameBuffer
????????上節的RtpFrameReferenceFinder為了設置P幀的參考幀為上一幀,保證了GOP內幀的有序,但是不保證GOP的有序,這個保證是由FrameBuffer來實現。? ?
?????????如上圖所示,FrameBuffer按照幀的先后順序向解碼器輸出幀。FrameBuffer按順序輸出“可解碼”的幀,這里的“可解碼”意思是某幀“連續”、并且其所有參考幀都已經被解碼,這里“連續”的意思是指某個幀的所有參考幀都已經收到。I幀是自參考的,所以直接是可解碼的,但是P幀則需要等待所有參考幀,也就是上一幀被收到。
????????這樣,因為PacketBuffer、RtpFrameReferenceFinder這兩個類只是保證幀的完整、GOP內幀的有序,一旦當前GOP的P幀還未完整,下個GOP的I幀提前進入FrameBuffer,則會直接丟棄當前GOP的所有后續P幀。
9.1 插入幀 - FrameBuffer::InsertFrame
????????該函數將當前幀插入幀緩存,如果該幀的所有參考幀都已經收到,那么認為該幀是連續的,那么通過同步事件通知解碼線程取待解碼幀,同時通知參考該幀的所有幀,檢查他們的未連續參考幀數量是否已經為0,是則連續。
? ???????
9.2 更新參考幀信息 - FrameBuffer::UpdateFrameInfoWithIncomingFrame?
????????該函數檢查某幀的參考幀是否已經連續,初始化未連續參考幀計數器num_missing_continuous、未解碼參考幀計數器num_missing_decodable,同時反向建立被參考幀與依賴幀之間的關系,方便狀態(連續、可解碼)傳播。?
9.3 FrameBuffer::NextFrame
????????該函數從幀緩存中獲取一個可以解碼的幀,該幀必須是連續的(所有參考幀都已經收到),并且其所有參考幀都已經被解碼。對I幀來說本身是連續的且自參考,可以直接被取走,P幀則需要依賴參考幀的連續、解碼狀態。?
????????????????
?????????
????????可解碼性傳播:?
?
????????9.4 總結
?????????FrameBuffer緩存即將進入解碼器的幀,按照順序向解碼器輸出連續的、所有參考幀都已經被解碼的幀。
? ? ?10?抖動與延遲
????????JitterBuffer包含Jitter與Buffer,上面幾節講了Buffer,主要用于緩存、排序、組幀、有序輸出,起到抗抖動的作用。但是網絡的具體抖動指標是多少,網絡的延遲是多少,需要其他的一些工具計算。
? ? ? ? ?10.1 抖動計算??????????? ?????
-
VCMInterFrameDelay:計算幀間延遲 = 兩幀的接收時間差 - 兩幀的發送時間差;
-
VCMJitterEstimator:通過VCMInterFrameDelay計算的幀間延遲計算出最優抖動值。
????????上圖描述了幀間延遲(抖動)觀測值的計算方法:jitter = tr_delta - ts_delta = (tr2 - tr1) - (ts2 - ts1),也就是兩幀的接收時間差 - 兩幀的發送時間差。
????????計算最優抖動的算法和GCC中使用到達時間濾波器(InterArrival)計算到達時間增量、使用過載估計器(OveruseEstimator)計算最優的到達間隔增量的算法基本一樣,都是利用卡爾曼濾波器,綜合幀間延遲的觀測值、預測值,獲得最優的幀間延遲(也就是網絡抖動),只是數據采樣的形式不太相同,GCC使用5ms的包簇(也可以稱為幀),這里直接使用視頻幀,這里不再詳述。?
? ? ? ? 10.2 延遲 - VCMTiming?
?????????VCMTiming可以輸出接收端的以下參數,這些參數可以在使用瀏覽器拉流時在chrome://webrtc-internals頁面中看到。
| googDecodeMs | 最近一次解碼耗時. |
| googMaxDecodeMs | 最大解碼耗時,實際上是第95百分位數,也就是大于采樣集合95%的解碼延遲. |
| googRenderDelayMs | 渲染耗時,固定為10ms. |
| googJitterBufferMs | 網絡抖動,見上節. |
| googMinPlayoutDelayMs | 最小播放時延,音視頻同步器輸出的視頻幀播放應該延遲的時長. |
| googTargetDelayMs | 目標時延,googCurrentDelayMs會逼近目標延遲. |
| googCurrentDelayMs | 當前時延,用于計算視頻幀渲染時間. |
?10.2.1 目標延遲 - googTargetDelayMs
????????很明顯,目標延遲基本上就是抖動+解碼時間+渲染時間,與播放延遲的最大者,也就是播放當前幀總體的期望延遲,作為當前延遲googCurrentDelayMs的參考值,并最終用于音視頻同步。
10.2.2 當前延遲 - googCurrentDelayMs?
? ? ? ? FrameBuffer每獲得一個可解碼幀會調用一次,更新當前延遲,最終用于計算渲染時間。
10.2.2 平滑渲染時間 - TimestampExtrapolator?
?????????FrameBuffer每獲得一個可解碼幀,都要更新其渲染時間,渲染時間通過TimestampExtrapolator類獲得。TimestampExtrapolator也是一個卡爾曼濾波器,其輸入為輸入幀的時間戳,TimestampExtrapolator會根據輸入幀的時間戳的間隔計算輸出渲染時間,目標是平滑輸出幀的時間間隔。
????????視頻幀的最終渲染時間 = 幀平滑時間 + 當前延遲。
?
11 總結?
????????RTP包進入JitterBuffer后,最終輸出了完整、連續、可解碼的視頻幀,并攜帶了可用于最終播放的渲染時間。
總結
以上是生活随笔為你收集整理的webrtc代码走读五(JitterBuffer)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: mysql查询3个表_mysql如何实现
- 下一篇: linux查询当前目录剩余空间,如何在l