WebRTC視頻數據統計之延時、抖動與丟包

一、前言

這篇文章主要想說明的是WebRTC內部對視頻上下行延時、抖動、丟包如何更新,上層又怎麼獲取到這些統計信息的。對應的WebRTC版本:63

二、背景

最近在內網情況下測試視頻會議,視頻下行延時很大,很多時候超過100ms。另外,視頻的上下行抖動總是穩定在30~40ms這個區間。這些統計在內網環境下是不正常的,於是決定看看是哪裏導致這些問題的。

在解決這些問題的過程中,也對WebRTC內部視頻統計數據做了一次梳理。

閱讀這篇文章之前,最好對RTP、RTCP、SR、RR有一些瞭解。這裏就不過多展開,可以參考以下文章:

[RTP Data Transfer Protocol][https://tools.ietf.org/html/rfc3550#section-5]
[RTP Control Protocol – RTCP][https://tools.ietf.org/html/rfc3550#section-6]
[RTP/RTSP/RTCP有什麼區別][https://www.zhihu.com/question/20278635/answer/14590945]

三、綜述

下圖是WebRTC內部獲取視頻統計信息和統計信息如何被更新的流程圖:(其中的箭頭代表函數調用)
在這裏插入圖片描述
上圖中的類A~G屬於公司內部代碼,不便公開,還請見諒。總體共有兩個大的模塊,如何取如何更新
ps: 下文關於流程圖細節部分都取自於該圖,讀者可根據相關文字在總圖中找到對應的位置 在這裏下載上圖的電子版: webrtc-statistics.gliffy

1. 如何取

上面部分“客戶端視頻數據統計入口”中,左下角的WebRtcVideoChannel::GetStats是WebRTC對外暴露的獲取統計信息的入口,視頻的上下行統計數據最終分別使用右上角SendStatisticsProxy::stats_ReceiveStatisticsProxy::stats_CallStats::avg_rtt_ms_來填充返回。

2. 如何更新

下面部分“延時、抖動、丟包更新流程”部分,從網絡接收到RTP/RTCP之後,使用三個不同顏色代表三種統計信息的更新流程,比如紅色代表下行抖動/丟包更新流程、藍色代表RTT的更新流程等。

統計信息大多不是由一條調用流程完成的(這就是下文會說到的“階段”),會有幾次類似緩衝區的“中轉”,然後由另外的線程或函數繼續做統計信息的整理,最終達到上一步的SendStatisticsProxy::stats_ReceiveStatisticsProxy::stats_CallStats::avg_rtt_ms_,等待上層獲取。

四、幾個統計信息詳細介紹

1、延時

這裏統計的延時指的是往返延時 rtt。WebRTC使用SR/RR來計算rtt

(1) 延時的計算

1) SR和RR報文格式
Sender Report RTCP Packet Receiver Report RTCP Packet
在這裏插入圖片描述 在這裏插入圖片描述
2) 計算rtt

以下流程通過結合SR/RR包報文格式,瀏覽RTCPReceiver::HandleReceiverReportRTCPReceiver::HandleReportBlockModuleRtpRtcpImpl::SendCompoundRTCPRTCPSender::BuildSRRTCPSender::BuildRR函數。前面2個函數是接收端計算rtt,後面3個函數是對端在構造RR時LSR/DLSR如何設置的。

  • 首先,發送端構造SR時,sender info部分的NTP字段被設置爲當前ntp時間戳;
  • 接收端收到最新的SR之後,使用last_received_sr_ntp_字段記錄當前ntp時間戳;
  • 接收端構造RR時,設置RR的DLSR字段爲當前ntp時間戳 - last_received_sr_ntp_,之後發出RR包;
  • 發送端在接收到RR包之後,記錄RR包到達時間A
  • 使用公式 A - LSR - DLSR 計算rtt。
3) 用一個圖描述上述RTT計算流程

在這裏插入圖片描述

SR與RR的個數並不完全相同,因爲RR並不是對SR的迴應,它們的發送各自獨立;另外丟包也會導致一部分SR/RR沒有被對方接收。因此上圖中,SR和RR傳輸中,實線代表發了一次SR/RR,並且被被對方接收了。這裏想說明的是:即便SR或RR丟失一部分,只要發送端收到了RR,它總能計算出rtt,因爲RR中使用的LSR和DLSR字段都是從最近一次收到的SR中取到的。

(2) 延時的更新流程

下文所說的第一階段、第二階段等,都是指 數據從一個位置轉移到另一個位置的過程,或者說是一次模式。比如:F1函數把數據從A點轉移到B點就返回了,F2函數把數據從B點轉移到C點就返回了,那A->B就是第一階段,B->C就是第二階段。如下:

在這裏插入圖片描述

1) rtt統計第一階段

由上文可知:從RR可以計算出往返延時rtt,這個rtt最終保存在RTCPReceiver::received_report_blocks_
在這裏插入圖片描述

2) rtt統計第二階段

ModuleRtpRtcpImpl::Process會定時把rtt從RTCPReceiver::received_report_blocks_更新到CallStats::reports_,這個更新過程,CallStats::reports_中每個rtt都會與一個更新時間戳綁定。參考CallStats::OnRttUpdate 函數。
在這裏插入圖片描述

3) rtt統計第三階段

CallStats繼承ModuleCallStats::Process函數會定時做以下三個步驟:

  • 根據第二階段綁定的時間戳,清理掉 reports_ 中距當前時間1.5s以前的rtt;
  • 計算1.5s內的平均rtt;
  • 使用平均rtt,更新 avg_rtt_ms_ 成員;

在這裏插入圖片描述

(3) 獲取延時

調用CallStats::avg_rtt_ms函數獲取rtt時,直接返回avg_rtt_ms_ ;

2、下行抖動和丟包

下行抖動和丟包,通過在接收端根據收到的RTP包來計算和更新。

(1) 抖動和丟包的計算

1) 抖動定義

抖動被定義爲:一對數據包在接收端與發送端的數據包時間間距之差。如下:
在這裏插入圖片描述
如果Si代表第i個包的發送時間戳,Ri代表第i個包的接收時間戳。Sj、Rj同理。
抖動(i, j) = |(Rj - Ri) - (Sj - Si)| = |(Rj - Sj) - (Ri - Si)|

WebRTC爲了統一抖動,並且爲了很好的降噪、降低突發抖動的影響,把上面的抖動(i, j)定義爲D(i, j)抖動J(i)定義爲:
J(i) = J(i-1) + (|D(i-1, i)| - J(i - 1)) / 16

我雖然看不出J(i)和D(i)的關係,但是D(i-1, j)是唯一引起J(i)變化的因素,是需要重點關注的。

2) 抖動計算存在的問題:

RTP報文頭部,有timestamp字段,該字段用來表示該RTP包所屬幀的capture time。接收RTP包時如果記錄接收時間戳,再根據頭部的timestamp字段,D(i, j)就可以計算出來,J也就有了。(事實上webrtc原本也是這樣乾的,而且這種方式計算的抖動還對外暴露,可以參考StreamStatisticianImpl::UpdateJitter函數)

但是這樣計算抖動是存在問題的:每一幀的視頻數據放進多個RTP包之後,這些RTP包的頭部timestamp字段都是一樣的(都是幀的capture time),但是實際發送時間不一樣,到達時間也不同。

3) 如何正確計算抖動:

計算D(i, j)時,Si不能只使用RTP timestamp,而是應該使用該RTP實際發送到網絡的時間戳。這種抖動被命名爲jitter_q4_transmission_time_offset,意爲考慮了transmission_time_offset的jitter。

  • a. transmission_time_offset是什麼?

transmission_time_offset是一段時間間隔,該時間間隔代表屬於同一幀的RTP的實際發送時間距離幀的capture time偏移量 。下圖是對transmission_offset_time的解釋:

在這裏插入圖片描述

其中,箭頭代表一個RTP,發送端的豎線代表時間軸,虛線代表幀的capture time。

最開始三個RTP包在距離capture time offset1時間之後發送到網絡,因此這三個RTP包的transmission_time_offset應該是offset1。同理第四個RTP包的transmission_time_offset應該是offset2,第五個RTP包的transmission_time_offset應該是offset3。

  • b. transmission_time_offset在RTP包的哪裏放着?

transmission_time_offset存在於RTP的擴展頭部,設置該擴展頭可以參考RTPSender::SendToNetwork函數,但使用之前該擴展頭之前需要註冊,否則在設置transmission_time_offset擴展頭會失敗。

下面的代碼段是WebRTC中D(i, j)的計算:

  // Extended jitter report, RFC 5450.
  // Actual network jitter, excluding the source-introduced jitter.
  int32_t time_diff_samples_ext =
    (receive_time_rtp - last_receive_time_rtp) -
    ((header.timestamp +
      header.extension.transmissionTimeOffset) -
     (last_received_timestamp_ +
      last_received_transmission_time_offset_));

其中:

  • receive_time_rtp 代表當前RTP的到達時間戳;
  • last_receive_time_rtp 是上一個RTP到達時記錄的時間戳;
  • header.timestamp + header.extension.transmissionTimeOffset 前者是capture time,後者是對應的transmission time offset,兩者相加代表該RTP實際發送到網絡的時間戳;
  • last_received_timestamp_ + last_received_transmission_time_offset_ 含義同上,但是代表的是上一個RTP的實際發送到網絡的時間戳;

(2) 下行抖動的更新流程

1) 抖動統計第一階段

接收端收到的RTP包,會經過StreamStatisticianImpl::UpdateJitter函數,該函數內部會計算經過這個RTP包之後的抖動值,並更新到成員jitter_q4_transmission_time_offset_成員中。
在這裏插入圖片描述

2) 抖動統計第二階段

ModuleRtpRtcpImpl::Process會定時發送RR,在構建RR的Report Block時,會蒐集本地接收報告並把第一階段保存的jitter_q4_transmission_time_offset_信息更新到ReceiveStatisticsProxy::stats_
在這裏插入圖片描述

(3) 下行丟包的更新流程

1) 丟包統計第一階段

接收端收到的RTP包,會經過StreamStatisticianImpl::UpdateCounters 函數,在該函數內部,會累加接收到的RTP包的個數和重傳包的個數,以及當前收到的最大的sequence。

2) 丟包統計第二階段

下圖是WebRTC內部計算下行丟包
在這裏插入圖片描述
丟包率更新的週期是發送一次RR,在發送RR時,會根據第一階段記錄的數據統計丟包,丟包根據下面的公式:

fraction_lost = RTP包丟失個數 / 期望接收的RTP包個數

其中:

包丟失個數 = 期望接收的RTP包個數 - 實際收到的RTP包個數

期望接收的RTP包個數 = 當前最大sequence - 上次最大sequence

實際收到的RTP包個數 = 正常有序RTP包 + 重傳包

計算出來的丟包,連同抖動一起被更新到ReceiveStatisticsProxy::stats_
在這裏插入圖片描述

(3) 獲取下行抖動和丟包

下行抖動和丟包最終會從ReceiveStatisticsProxy::stats_ 獲取。

3、上行抖動和丟包

下行抖動和丟包,從對方發來的RR包中獲取。RR包格式參考上文鏈接。

(1) 上行抖動和丟包的更新流程

本地上行抖動和丟包,就是對端下行抖動和丟包,對端按照上面介紹的方式計算下行抖動和丟包,然後通過RR返回。

從RR獲取抖動和丟包,沒有太多階段,只有一次過程。接收端在收到RR之後,就把內部的抖動和丟包更新到SendStatisticsProxy::stats_中,這裏就是客戶端主動獲取上行抖動和丟包時最終的數據源。

(2) 獲取上行抖動和丟包

上行抖動和丟包最終會從SendStatisticsProxy::stats_ 獲取。

五、最後

以上是最近對WebRTC視頻統計數據的瞭解,希望能對有需要的人有所幫助。如有不對的地方,歡迎指正!!

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