WebRtc Video Receiver(八)-基于Kalman filter模型的JitterDelay原理分析

1)前言

  • 前一篇文章分析了FrameBuffer模塊對視頻幀的插入原理,以及出隊(送到解碼隊列)的機制。
  • 在出隊的過程中涉及到了很多和延遲相關的信息,沒有分析,諸如渲染時間的計算、幀延遲的計算、抖動的計算等都未進行相應的分析。
  • 其中FrameBuffer延遲JitterDleay對視頻流的單向延遲有重要的影響,很大程度上決定了應用的實時性,本文結合參考文獻對JitterDleay的計算進行深入原理分析。
  • 本文首先介紹gcc算法的Arrival-time model模型
  • 其次介紹Arrival-time filter(卡爾曼濾波)的公式推導
  • 最后分析結合代碼分析卡爾曼濾波在JitterDleay中的應用,以及了解jitter delay究竟描述的是個啥東西

2)Arrival-time model模型介紹

image-20210610215218805.png
  • 上圖為網絡傳輸過程中相鄰兩幀數據的到達時間模型
  • 已知T(i-1)為第(i-1)幀數據發送時間,T(i)為第(i)幀數據發送時間
  • t(i)為第i幀數據的接收時間,t(i-1)為第(i-1)幀數據的接收時間
  • 理論上若傳輸過程中無噪聲(網絡噪聲)和其他因素(傳輸大幀引起的延遲)的影響,第i幀傳輸曲線應該如上述紅色線條(這樣jitter delay應該等于0),但正由于網絡噪聲和傳輸大幀導致的影響使得第I幀傳輸的時延遲可能大于第i-1幀,也或者小于第i-1幀,所以就有了jitter delay
  • webrtc的FrameBuffer模塊中通過獲取當前的幀間延遲.來告知解碼隊列何時解碼該幀比較合適,從而來讓接收端所接收到的視頻幀能夠較為平滑的進入到解碼器,從而緩解卡頓現象
  • 通過上圖可以理論建模如下:
d(i) = t(i) - t(i-1) - (T(i) - T(i-1))                                  (2.1.1)
     = L(i)/C(i) - L(i-1)/C(i-1) + w(i)
        L(i)-L(i-1)
     = -------------- + w(i)
           C(i)
     = dL(i)/C(i) + w(i)
  • d(i) 就是最終的幀間延遲
  • 其中C(i)為信道傳輸速率、L(i)為每幀數據的數據量,dL(i)為兩幀數據的數據量的差值,以上假定傳輸速率恒定
  • w(i)為是隨機過程w的一個樣本,它是C(i)、當前發送數據量和當前發送速率的函數,并假定w為高斯白噪聲
  • 當傳輸信道過載發送數據時,w(i)的方差將變大,傳輸信道空載時w(i)將隨之變小,其他情況w(i)為0
  • t(i) - t(i-1) > T(i) - T(i-1)表示幀I相對幀i-1的延遲大了
  • 若將網絡排隊延遲m(i)w(i)中提取出來使得過程噪聲的均值為0,則會得出如下:
w(i) = m(i) + v(i)
d(i) = dL(i)/C(i) + m(i) + v(i)
  • m(i)表示網絡排隊延遲(如經歷路由器的時候的排隊延遲),v(i)表示測量噪聲(如最大幀數據量和平均每幀數據量的計算誤差,時間同步等)。
  • 以上就是幀間延遲的基本模型,在該模型中我們實際要求的是利用卡爾曼濾波來求得C(i)m(i)
  • 并通過迭代使得C(i)m(i)的誤差最小,從而得到最優的d(i)幀間延遲

3)Kalman filter 建模及理論推導

3.1 卡爾曼濾波-建立狀態空間方程

 theta(i) = A * theta(i-1) + u(i-1)                                     P(u) ~ (0,Q)         
          = theta(i-1) + u(i-1)                                          (3.1.1)
 Q(i) = E{u_bar(i) * u_bar(i)^T}
 diag(Q(i)) = [10^-13 10^-3]^T
  • theta(i)為狀態空間變量,其中A為狀態轉移矩陣,該案例取值為二維單位矩陣,u(i-1)為過程噪聲(無法測量),其概率分布服從正太分布,數學期望為0,協方差矩陣為Q,推薦為對角矩陣

  • theta(i)包含兩個變量如下

theta_bar(i) = [1/C(i)  m(i)]^T   
  • 1/C(i)為信道傳輸速率的倒數
  • m(i)為幀i的網絡排隊時延(路由器或者交換機處理數據包排隊所消耗的時間)
  • C(i)m(i)也是我們實際要求得的目標值

3.2 卡爾曼濾波-建立觀測方程

  • 觀測方程如下:
d(i) = H * theta(i) + v(i)                  P(V) ~ (0,R)                    (3.2.2)
  • 上述觀測方程測量的是時間,為一維方程
  • H為觀測系數矩陣,其定義如下:
h_bar(i) = [dL(i)  1]^T
H = h_bar(i)^T = [dL(i)  1]    
  • dL(i)為第i幀和第i-1幀的數據量之差(detal L(i))

  • 將其變形為google 官方公式如下:

d(i) = h_bar(i)^T * theta_bar(i) + v(i)                                     (3.2.3)
  • v(i)為觀測噪聲,同樣其概率分布服從正太分布,數學期望為0,協方差矩陣為R
variance var_v = sigma(v,i)^2
R(i) = E{v_bar(i) * v_bar(i)^T}  
     = var_v
  • h_bar向量代入到(3.2.3)式中
d(i) = h_bar(i)^T * theta_bar(i) + v(i)
     = [dL(i)  1] * [1/C(i)  m(i)]^T + v(i)                                 (3.2.4)
  • 有了狀態方程和測量方程則可以根據卡爾曼濾波的預測和校正模型進行估計

3.3 卡爾曼濾波-預測計算先驗估計

  • 綜合上述公式可求得先驗估計值如下:
  • 預測其實就是使用上一次的最優結果預測當前的值
  • 首先是計算先驗估計的誤差協方差
theta_hat^-(i) = theta_hat(i-1) + u(i-1)                                    (3.3.1)
  • theta_hat^-(i)第i幀的先驗估計值
  • 其次根據先驗估計值計算先驗估計的誤差協方差

3.4 卡爾曼濾波-預測計算先驗估計誤差協方差

e^-(i) = theta(i) - theta_hat^-(i)                      P(E(i)) ~ (0 , P)   (3.4.1)
P^-(i) = {e^-(i) * e^-(i)^T}
       = E{(theta(i) - theta_hat^-(i)) * (theta(i) - theta_hat^-(i))^T}     (3.4.2)
       = A * P(i-1) * A^T + Q                                               (3.4.3)
       = P(i-1) + Q 
       = E(i-1) + Q                                                         (3.4.4)
  • e^-(i)表示當前幀的實際值和估計值之間的誤差,也就是先驗估計的誤差
  • P^-(i)表示當前幀的先驗誤差協方差
  • P(i-1)為上一次的誤差協方差
  • 也就是說先驗估計的誤差協方差等于上一次的后驗估計的誤差協方差 + 過程噪聲的的誤差協方差
  • 有了先驗估計的誤差協方差之后就是計算當前幀的最優卡爾曼增益為迭代做準備

3.5 卡爾曼濾波-校正計算卡爾曼增益

  • 此處不做數學理論推導,直接給出如下公式
                       P^-(i) * H^T
     k_bar(i) = ------------------------------------------------------
                    H * P^-(i) * H^T + R                   
              
                       P^-(i) * h_bar(i)
              = ------------------------------------------------------              (3.5.1)
                    h_bar(i)^T * P^-(i) * h_bar(i) + R                   
                     
                     ( E(i-1) + Q(i) ) * h_bar(i)
              = ------------------------------------------------------              (3.5.2)
                var_v_hat(i) + h_bar(i)^T * (E(i-1) + Q(i)) * h_bar(i)
  • 其中R = var_v_hat(i)
  • 在webrtc gcc算法中R表示測量誤差協方差,它使用指數平均濾波器,通過如下方法計算R
The variance var_v(i) = sigma_v(i)^2 is estimated using an exponential averaging filter, modified for variable sampling rate
var_v_hat(i) = max(beta * var_v_hat(i-1) + (1-beta) * z(i)^2, 1)                    (3.5.3)
beta = (1-chi)^(30/(1000 * f_max))                                                  (3.5.4)
  • 測量誤差協方差最小取值為1
  • z(i)為網絡殘差(當前測量值 - 上一次估計值)

3.6 卡爾曼濾波-校正計算后驗估計值

theta_hat(i) = theta_hat^-(i) + k_bar(i) * (d(i) - H * theta_hat^-(i))
             = theta_hat(i-1) + k_bar(i) * (d(i) - H * theta_hat(i-1))              (3.6.1)
             = theta_hat(i-1) + k_bar(i) * d(i) - k_bar(i) * H * theta_hat(i-1)     
             = (1 - k_bar(i) * H) * theta_hat(i-1)  + k_bar(i) * d(i)               (3.6.2)
k_bar(i) ~ [0 ~ 1/H]
  • theta_hat(i)為第i幀的估計值,叫做后驗估計值
  • k_bar(i)為當前幀(第i幀)的卡爾曼增益
  • 卡爾曼濾波的目標就是去尋找適當的k_bar(i),使得theta_hat(i)的值更加趨向theta(i)(也就是實際值),如何去選擇值肯定是和誤差v(i)以及u(i-1)息息相關的
  • k_bar(i)趨近0的時候,更相信算出來的結果(估計值)
  • k_bar(i)趨近1/H的時候,更相信測量出來的結果
  • 將上述公式進行替換如下
z(i) = d(i) - h_bar(i)^T * theta_hat(i-1)                                           (3.6.3)
theta_hat(i) = theta_hat(i-1) + z(i) * k_bar(i)                                     (3.6.4)
  • z(i)也叫網絡殘差(當前測量值 - 上一次估計值)
  • 上述公式則為google給出的官方公式
  • 結合(3.6.1)(3.5.2)可知當var_v_hat(i)越大時,說明測量誤差較大,此時卡爾曼增益將越小,最終的估計值將更加趨近與一次的估計值

3.7 卡爾曼濾波-更新誤差協方差

  • 計算后驗估計誤差協方差
e(i) = theta(i) - theta_hat(i)                      P(E(i)) ~ (0 , P)               (3.7.1)
P(i) = E{e(i) * e(i)^T}                                                             (3.7.2)
     = E{(theta(i) - theta_hat(i)) * (theta(i) - theta_hat(i))^T}                   (3.7.3)
     = E(i)
  • e(i)表示當前幀的實際值和估計值之間的誤差

  • P(i)表示當前幀的誤差協方差,此處叫做后驗誤差協方差

  • 綜合(3.6.1)、(3.5.1)最后得出

 P(i) = (I - k_bar(i) * H) * P^-(i)                                                 (3.7.4)
      = (I - k_bar(i) * h_bar(i)^T) * (E(i-1) + Q(i))                               (3.7.5)
      = E(i)
  • 其中I為2*2的單位矩陣

3.8 卡爾曼濾波-系統模型圖

  • 結合公式3.6.2我們可以得出如下模型圖


    image-20210523222453018.png
m(i) = (1 ? K(i)) * m(i?1) + K(i) * (dm(i))

4)WebRTC中JitterDelay的計算和迭代過程

image-20210612222555068.png
  • 步驟1、在FrameBuffer模塊中通過Decode任務隊列,通過重復任務調用FindNextFrameGetNextFrame獲取待解碼的視頻幀
  • 步驟2、然后通過VCMInterFrameDelay模塊的CalculateDelay()函數依據當前找到的幀的rtp時間戳以及接收時間計算幀間延遲得到frameDelayMS
  • 步驟3、以frameDelayMS和當前幀的frameSizeBytes(當前幀的數據大小字節)為參數,調用VCMJitterEstimator::UpdateEstimate()函數對卡爾曼濾波器進行迭代,并計算出當前幀的最優jitterDelay
  • 步驟4、對卡爾曼濾波進行校準供下一次迭代使用
  • 最后通過VCMJitterEstimator::GetJitterEstimate()函數返回最優估計jitterdelay并將其作用到VCMTiming模塊供期望RenderDelay的計算
  • 其中步驟2中的frameDelayMS的計算對應了第二節的Arrival-time model示意圖,并將T(i)代表第i幀的發送的rtp時間戳,T(i-1)為第i-1幀的rtp時間戳,t(i)為第i幀的本地接收時間,t(i-1)為第i-1幀的本地接收時間
  • 步驟2的實現代碼如下:
bool VCMInterFrameDelay::CalculateDelay(uint32_t timestamp,
                                        int64_t* delay,
                                        int64_t currentWallClock) {
  .....
  // Compute the compensated timestamp difference and convert it to ms and round
  // it to closest integer.
  _dTS = static_cast<int64_t>(
      (timestamp + wrapAroundsSincePrev * (static_cast<int64_t>(1) << 32) -
       _prevTimestamp) /
          90.0 +
      0.5);\
  // frameDelay is the difference of dT and dTS -- i.e. the difference of the
  // wall clock time difference and the timestamp difference between two
  // following frames.
  *delay = static_cast<int64_t>(currentWallClock - _prevWallClock - _dTS);

  _prevTimestamp = timestamp;
  _prevWallClock = currentWallClock;
  return true;
}
  • _dTS表示相鄰兩幀的rtp時間戳
  • _prevWallClock為上一幀的本地接收時間
  • currentWallClock為當前幀的本地接收時間
  • 接下來首先介紹JitterDelay的計算流程,最后分析卡爾曼濾波預測和校正過程

4.1)計算JitterDelay

  • FrameBuffer模塊中使用如下函數獲取
int VCMJitterEstimator::GetJitterEstimate(
    double rttMultiplier,
    absl::optional<double> rttMultAddCapMs) {
  //調用CalculateEstimate()計算當前的jitterDelay,OPERATING_SYSTEM_JITTER默認為10ms  
  //這就意味著默認最小的jittterDelay至少是10ms?     
  double jitterMS = CalculateEstimate() + OPERATING_SYSTEM_JITTER;
  uint64_t now = clock_->TimeInMicroseconds();
  //kNackCountTimeoutMs = 60000
  // FrameNacked會更新_latestNackTimestamp單位為微秒  
  //1分鐘內若所有幀都未丟包則清除  
  if (now - _latestNackTimestamp > kNackCountTimeoutMs * 1000)
    _nackCount = 0;

  if (_filterJitterEstimate > jitterMS)
    jitterMS = _filterJitterEstimate;
  if (_nackCount >= _nackLimit) {//_nackLimit
    if (rttMultAddCapMs.has_value()) {
      jitterMS +=
          std::min(_rttFilter.RttMs() * rttMultiplier, rttMultAddCapMs.value());
    } else {
      jitterMS += _rttFilter.RttMs() * rttMultiplier;
    }
  }
  ....
  return rtc::checked_cast<int>(std::max(0.0, jitterMS) + 0.5);
}

  • FrameBuffer只針對未重傳過包的幀進行jitterDelay的迭代和計算,也就是說假設第i幀數據有丟包,那么該幀是不會計算JitterDelay的,該幀的期望渲染時間計算過程中所用到的jitterDelay值是上一幀的jitterDelay
  • webrtc jitter delay對丟過包的數據幀會在FrameBuffer::GetNextFrame()函數中通過jitter_estimator_.FrameNacked()函數告知VCMJitterEstimator模塊,使得_nackCount變量自增(前提通過WebRTC-AddRttToPlayoutDelay/Enable使能),超過3會清除
  • _nackCount大于等于3的時候在時候jitterDelay會在后續未丟包的幀中加上一個RTT乘以某個系數
  • 通過參閱網上的一些文摘表示,如果某幀有丟包如果不處理該RTT會很容易造成卡頓
  • 以上函數的核心主要是調用CalculateEstimate()函數計算jitterDelay
// Calculates the current jitter estimate from the filtered estimates.
double VCMJitterEstimator::CalculateEstimate() {
  double ret = _theta[0] * (_maxFrameSize - _avgFrameSize) + NoiseThreshold();
  .......
  _prevEstimate = ret;
  return ret;
}
  • _theta[0]記錄的是信道傳輸速率的倒數,也就是上述卡爾曼狀態方程中的1/c(i)
  • _maxFrameSize表示自會話開始以來所收到的最大幀大小
  • _avgFrameSize表示當前平均幀大小
  • NoiseThreshold()計算噪聲補償閥值
double VCMJitterEstimator::NoiseThreshold() const {
  double noiseThreshold = _noiseStdDevs * sqrt(_varNoise) - _noiseStdDevOffset;
  if (noiseThreshold < 1.0) {
    noiseThreshold = 1.0;
  }
  return noiseThreshold;
}
  • _noiseStdDevs表示噪聲標準差系數取值2.33
  • _varNoise表示測量噪聲方差對應公式(3.2.3)中的v(i),默認初始值為4.0
  • _noiseStdDevOffset噪聲標準差偏移(噪聲扣除常數)取值30.0ms

4.2)JitterDelay迭代更新機制

  • 通過第4大節中的截圖看出,卡爾曼的更新是通過Framebuffer模塊調用其UpdateEstimate函數來實現的
// Updates the estimates with the new measurements.
void VCMJitterEstimator::UpdateEstimate(int64_t frameDelayMS,
                                        uint32_t frameSizeBytes,
                                        bool incompleteFrame /* = false */) {
  //1)計算當前幀和上一幀的數據量之差
  int deltaFS = frameSizeBytes - _prevFrameSize;
  //2)計算_avgFrameSize平均每幀數據的大小 
  if (_fsCount < kFsAccuStartupSamples) {
    _fsSum += frameSizeBytes;
    _fsCount++;
  } else if (_fsCount == kFsAccuStartupSamples) {//kFsAccuStartupSamples取值為5超過5幀開始計算平均幀大小
    // Give the frame size filter.
    _avgFrameSize = static_cast<double>(_fsSum) / static_cast<double>(_fsCount);
    _fsCount++;
  }
  /*3)若當前輸入幀的大小比平均每幀數據的數據量要大,則對其進行滑動平均處理,比如說如果當前是一個I幀,數據量顯然會比較大,
    默認incompleteFrame為false,所以每幀都會計算平均值*/
  if (!incompleteFrame || frameSizeBytes > _avgFrameSize) {
    //滑動平均算法,_phi的取值為0.97,取接近前30幀數據大小的平均值,求得的avgFrameSize值為接近近30幀數據的平均大小  
    double avgFrameSize = _phi * _avgFrameSize + (1 - _phi) * frameSizeBytes;
    //如果I幀數據量會比較大,如下的判斷會不成立,偏移太大不計賦值_avgFrameSize   
    if (frameSizeBytes < _avgFrameSize + 2 * sqrt(_varFrameSize)) {
      // Only update the average frame size if this sample wasn't a key frame.
      _avgFrameSize = avgFrameSize;
    }
    // Update the variance anyway since we want to capture cases where we only
    // get key frames.
    //3.1)此處更新平均幀大下的方差,默認方差為100,取其最大值,根據_varFrameSize可以得出在傳輸過程中每幀數據大小的均勻性
    //    若方差較大則說明幀的大小偏離平均幀大小的程度越大,則均勻性也越差
    _varFrameSize = VCM_MAX(
        _phi * _varFrameSize + (1 - _phi) * (frameSizeBytes - avgFrameSize) *
                                   (frameSizeBytes - avgFrameSize),
        1.0);
  }

  // Update max frameSize estimate.
  //4)計算最大幀數據量  
  _maxFrameSize =
      VCM_MAX(_psi * _maxFrameSize, static_cast<double>(frameSizeBytes));

  if (_prevFrameSize == 0) {
    _prevFrameSize = frameSizeBytes;
    return;
  }
  //賦值上一幀數據大小  
  _prevFrameSize = frameSizeBytes;

  // Cap frameDelayMS based on the current time deviation noise.
  /*5) 根據當前時間偏移噪聲求frameDelayMS,_varNoise為噪聲方差,默認4.0很顯然該值在傳輸過程中會變化,
     time_deviation_upper_bound_為時間偏移上限值,默認為3.5,所以默認初始值計算出來max_time_deviation_ms
     為7,對于幀率越高,默認輸入的frameDelayMS會越小,這里和max_time_deviation_ms去最小值,當噪聲的方差越大
     max_time_deviation_ms的值月越大,其取值就會越接近取向傳入的frameDelayMS*/ 
  int64_t max_time_deviation_ms =
      static_cast<int64_t>(time_deviation_upper_bound_ * sqrt(_varNoise) + 0.5);
  frameDelayMS = std::max(std::min(frameDelayMS, max_time_deviation_ms),
                          -max_time_deviation_ms);

  /*6)根據得出的延遲時間計算樣本與卡爾曼濾波器估計的期望延遲之間的延遲差(反映網絡噪聲的大小),計算公式為
     frameDelayMS - (_theta[0] * deltaFSBytes + _theta[1])
     當前測量值 - 上一次卡爾曼濾波后的估計值 對應公式3.6.3和3.6.4*/ 
  double deviation = DeviationFromExpectedDelay(frameDelayMS, deltaFS);
  // Only update the Kalman filter if the sample is not considered an extreme
  // outlier. Even if it is an extreme outlier from a delay point of view, if
  // the frame size also is large the deviation is probably due to an incorrect
  // line slope.    
  //根據注釋的意思是只有當樣本不被認為是極端異常值時才更新卡爾曼濾波器,言外之意就是網絡殘差值不能超過
  //  _numStdDevDelayOutlier * sqrt(_varNoise) = 30ms 默認值,隨_varNoise的大小變化而變化 
  if (fabs(deviation) < _numStdDevDelayOutlier * sqrt(_varNoise) ||
      frameSizeBytes >
          _avgFrameSize + _numStdDevFrameSizeOutlier * sqrt(_varFrameSize)) {
    // Update the variance of the deviation from the line given by the Kalman
    // filter.
    EstimateRandomJitter(deviation, incompleteFrame);
    // Prevent updating with frames which have been congested by a large frame,
    // and therefore arrives almost at the same time as that frame.
    // This can occur when we receive a large frame (key frame) which has been
    // delayed. The next frame is of normal size (delta frame), and thus deltaFS
    // will be << 0. This removes all frame samples which arrives after a key
    // frame.
    if ((!incompleteFrame || deviation >= 0.0) &&
        static_cast<double>(deltaFS) > -0.25 * _maxFrameSize) {
      // Update the Kalman filter with the new data
      KalmanEstimateChannel(frameDelayMS, deltaFS);
    }
  } else {  // 如果網絡殘差太大,說明噪聲偏移太大,需要對測量噪聲進行校正,本次不進行卡爾曼預測和校正  
    int nStdDev =
        (deviation >= 0) ? _numStdDevDelayOutlier : -_numStdDevDelayOutlier;
    EstimateRandomJitter(nStdDev * sqrt(_varNoise), incompleteFrame);
  }
  // Post process the total estimated jitter
  //6) 求得當前幀的jitterDelay最優估計值 
  if (_startupCount >= kStartupDelaySamples) {
    PostProcessEstimate();
  } else {
    _startupCount++;
  }
}
  • _avgFrameSize表示平均幀大小,使用滑動平均算法
  • _varFrameSize表示傳輸過程中幀大小的方差,象征了傳輸過程中每幀數據量的均勻性,_varFrameSize越大表示均勻性越差,所以在傳輸過程中,在發送端應當盡量使得每幀數據的數據量盡可能的接近(如盡量減少I幀的發送,按需所?。?/li>
  • _maxFrameSize表示自會話以來最大幀的數據量大小
  • deltaFS表示當前幀和上一幀的數據量只差
  • deviation表示網絡殘差,使用當前測量值減去估計值求得,代碼如下:
double VCMJitterEstimator::DeviationFromExpectedDelay(
    int64_t frameDelayMS,
    int32_t deltaFSBytes) const {
  return frameDelayMS - (_theta[0] * deltaFSBytes + _theta[1]); 
}
  • 以上計算對應公式3.6.33.6.4
  • 上述第5步中根據當前網絡殘差是否在某范圍內來決定是只更新測量噪聲的方差還是即更新測量噪聲誤差也進行預測和校正工作
  • 接下來著重分析EstimateRandomJitter測量噪聲方差的計算和KalmanEstimateChannel函數的預測以及校正分析
  • 同時以上UpdateEstimate函數的過程中計算平均幀大小以及平均幀大小方差都使用了移動平均算法如下:
double avgFrameSize = _phi * _avgFrameSize + (1 - _phi) * frameSizeBytes;
_varFrameSize = VCM_MAX(
    _phi * _varFrameSize + (1 - _phi) * (frameSizeBytes - avgFrameSize) *
    (frameSizeBytes - avgFrameSize),1.0);
  • 使用移動平均算法的原因是讓計算出來的值和前面幀的值有關聯,確保離散數據的平滑性,當_phi取1的時候就不平滑了

4.3)更新誤差方差

void VCMJitterEstimator::EstimateRandomJitter(double d_dT,
                                              bool incompleteFrame) {
  uint64_t now = clock_->TimeInMicroseconds();
  //1)對幀率進行采樣統計  
  if (_lastUpdateT != -1) {
    fps_counter_.AddSample(now - _lastUpdateT);
  }
  _lastUpdateT = now;

  if (_alphaCount == 0) {
    assert(false);
    return;
  }
  //2) alpha = 399  
  double alpha =
      static_cast<double>(_alphaCount - 1) / static_cast<double>(_alphaCount);
  _alphaCount++;
  if (_alphaCount > _alphaCountMax)
    _alphaCount = _alphaCountMax;//_alphaCountMax = 400

  // In order to avoid a low frame rate stream to react slower to changes,
  // scale the alpha weight relative a 30 fps stream.
  double fps = GetFrameRate();
  if (fps > 0.0) {
    double rate_scale = 30.0 / fps;
    // At startup, there can be a lot of noise in the fps estimate.
    // Interpolate rate_scale linearly, from 1.0 at sample #1, to 30.0 / fps
    // at sample #kStartupDelaySamples.
    if (_alphaCount < kStartupDelaySamples) {
      rate_scale =
          (_alphaCount * rate_scale + (kStartupDelaySamples - _alphaCount)) /
          kStartupDelaySamples;//kStartupDelaySamples = 30
    }
    //alpha = pow(399/400,30.0 / fps)  
    alpha = pow(alpha, rate_scale);
  }

  double avgNoise = alpha * _avgNoise + (1 - alpha) * d_dT;
  double varNoise =
      alpha * _varNoise + (1 - alpha) * (d_dT - _avgNoise) * (d_dT - _avgNoise);
  if (!incompleteFrame || varNoise > _varNoise) {
    _avgNoise = avgNoise;
    _varNoise = varNoise;
  }
  if (_varNoise < 1.0) {
    // The variance should never be zero, since we might get stuck and consider
    // all samples as outliers.
    _varNoise = 1.0;
  }
}
  • 測量誤差的更新和幀率相關,alpha為指數移動平均算法的指數加權系數收幀率大小的影響較大,數學模型為遞減指數函數,當fps變低時,rate_scale增大,從而alpha會變小,最終avgNoise的值會增大,意味著此時噪聲增大,實時幀率越接近30fps,效果越理想
  • _avgNoise為自會話以來的平均測量噪聲,_varNoise會噪聲方差,變量d_dT表示網絡殘差
  • 噪聲方差以及平均噪聲的更新都采用指數型移動平均算法求取,其特性是指數式遞減加權的移動平均,各數值的加權影響力隨時間呈指數式遞減,時間越靠近當前時刻的數據加權影響力越大
double avgNoise = alpha * _avgNoise + (1 - alpha) * d_dT;
double varNoise =
      alpha * _varNoise + (1 - alpha) * (d_dT - _avgNoise) * (d_dT - _avgNoise);
  • 以上計算對應公式3.5.33.5.4
  • _varNoise較大時,說明此時網絡噪聲較大,此時后續的卡爾曼增益計算的結果會較小,說明測量誤差較大,使得當前幀的jitterDelay更趨近上一次的估計值,從而收當前網絡殘差的影響更小一些

4.4)Kalman Filter 預測及校正

  • 結合第3章界的理論推導,預測首先利用上一次的估計結果求得先驗估計誤差協方差
  • VCMJitterEstimator模塊中定義如下核心矩陣
  double _thetaCov[2][2];  // Estimate covariance 先驗估計誤差協方差
  double _Qcov[2][2];      // Process noise covariance 過程噪聲協方差對應Q向量
/**
@ frameDelayMS:為測量出來的當前幀和上一幀的幀間抖動延遲
@ deltaFSBytes:當前幀的數據量和上一幀的數據量之差
**/
void VCMJitterEstimator::KalmanEstimateChannel(int64_t frameDelayMS,
                                               int32_t deltaFSBytes) {
  
  double Mh[2];//P(i) = E[1/c(i) m(i)]
  double hMh_sigma;
  double kalmanGain[2];
  double measureRes;
  double t00, t01;
  // Kalman filtering
  /*1)計算先驗估計誤差協方差  
     結合公式3.4.1~3.4.4  
    e^-(i) = theta(i) - theta_hat^-(i)                          
    P^-(i) = {e^-(i) * e^-(i)^T}
           = E{(theta(i) - theta_hat^-(i)) * (theta(i) - theta_hat^-(i))^T}     
           = A * P(i-1) * A^T + Q                                               
           = P(i-1) + Q 
           = E(i-1) + Q   
   當前幀(i)的先驗估計誤差協防差 = 上一幀(i-1)的誤差協方差 + 過程噪聲協方差        
  */     
    
  //Prediction
  //M = M + Q = E(i-1) + Q     
  _thetaCov[0][0] += _Qcov[0][0];
  _thetaCov[0][1] += _Qcov[0][1];
  _thetaCov[1][0] += _Qcov[1][0];
  _thetaCov[1][1] += _Qcov[1][1];

  /*  
     2) 校正 根據公式3.5.1~3.5.2計算卡爾曼增益
    
                       P^-(i) * H^T
     k_bar(i) = ------------------------------------------------------
                    H * P^-(i) * H^T + R                   
              
                       P^-(i) * h_bar(i)
              = ------------------------------------------------------              (3.5.1)
                    h_bar(i)^T * P^-(i) * h_bar(i) + R                   
                     
                     ( E(i-1) + Q(i) ) * h_bar(i)
              = ------------------------------------------------------              (3.5.2)
                var_v_hat(i) + h_bar(i)^T * (E(i-1) + Q(i)) * h_bar(i)    
  
  */
  // Kalman gain
  // K = M*h'/(sigma2n + h*M*h') = M*h'/(1 + h*M*h') = M*h'/(var_v_hat(i) + h*M*h')
  // h = [dFS 1] 其中dFS對應的入參deltaFSBytes
  // Mh = M*h' = _thetaCov[2][2] * [dFS 1]^
  // hMh_sigma = h*M*h' + R = h_bar(i)^T * (E(i-1) + Q(i)) * h_bar(i) + R   
  Mh[0] = _thetaCov[0][0] * deltaFSBytes + _thetaCov[0][1];// 對應1/C(i) 信道傳輸速率的誤差協方差
  Mh[1] = _thetaCov[1][0] * deltaFSBytes + _thetaCov[1][1];// 對應網絡排隊延遲m(i)的誤差協方差
  // sigma weights measurements with a small deltaFS as noisy and
  // measurements with large deltaFS as good
  if (_maxFrameSize < 1.0) {
    return;
  }
  //sigma為測量噪聲標準差的指數平均濾波,對應的是測量噪聲的協方差R
  double sigma = (300.0 * exp(-fabs(static_cast<double>(deltaFSBytes)) /
                              (1e0 * _maxFrameSize)) +
                  1) *
                 sqrt(_varNoise);
  if (sigma < 1.0) {
    sigma = 1.0;
  }
  // hMh_sigma 對應H * P^-(i) * H^T = h_bar(i)^T * (E(i-1) + Q(i)) * h_bar(i) + R 
  // 對應公式(3.5.1)  
  //[dFS 1]^ * Mh  =  dFS * Mh[0] + Mh[1]  
  hMh_sigma = deltaFSBytes * Mh[0] + Mh[1] + sigma;
  if ((hMh_sigma < 1e-9 && hMh_sigma >= 0) ||
      (hMh_sigma > -1e-9 && hMh_sigma <= 0)) {
    assert(false);
    return;
  }
  //計算卡爾曼增益Mh / hMh_sigma  
  kalmanGain[0] = Mh[0] / hMh_sigma;
  kalmanGain[1] = Mh[1] / hMh_sigma;

  /*
  3)根據公式3.6.1~3.6.4校正計算后驗估計值
    theta_hat(i) = theta_hat^-(i) + k_bar(i) * (d(i) - H * theta_hat^-(i))
                 = theta_hat(i-1) + k_bar(i) * (d(i) - H * theta_hat(i-1))              (3.6.1)
                 = theta_hat(i-1) + k_bar(i) * d(i) - k_bar(i) * H * theta_hat(i-1)     
                 = (1 - k_bar(i) * H) * theta_hat(i-1)  + k_bar(i) * d(i)               (3.6.2)
    其中k_bar(i) ~ [0 ~ 1/H]  
    z(i) = d(i) - h_bar(i)^T * theta_hat(i-1)                                           (3.6.3)
    theta_hat(i) = theta_hat(i-1) + z(i) * k_bar(i)                                     (3.6.4)  
  */  
  // Correction
  // theta = theta + K*(dT - h*theta)
  // 計算網絡殘差,得到最優估計值  
  measureRes = frameDelayMS - (deltaFSBytes * _theta[0] + _theta[1]);
  _theta[0] += kalmanGain[0] * measureRes; //公式(3.6.4)  
  _theta[1] += kalmanGain[1] * measureRes; //公式(3.6.4)  

  if (_theta[0] < _thetaLow) {
    _theta[0] = _thetaLow;
  }
  /**
  4)根據公式3.7.1~3.7.4更新誤差協方差,為下一次預測提供最優濾波器系數
    e(i) = theta(i) - theta_hat(i)                      P(E(i)) ~ (0 , P)               (3.7.1)
    P(i) = E{e(i) * e(i)^T}                                                             (3.7.2)
         = E{(theta(i) - theta_hat(i)) * (theta(i) - theta_hat(i))^T}                   (3.7.3)
         = (I - k_bar(i) * H) * P^-(i)                                                  (3.7.4)
         = (I - k_bar(i) * h_bar(i)^T) * (E(i-1) + Q(i))                                (3.7.5)
         = E(i)  
  */
  // M = (I - K*h)*M
  t00 = _thetaCov[0][0];
  t01 = _thetaCov[0][1];
  _thetaCov[0][0] = (1 - kalmanGain[0] * deltaFSBytes) * t00 -
                    kalmanGain[0] * _thetaCov[1][0];
  _thetaCov[0][1] = (1 - kalmanGain[0] * deltaFSBytes) * t01 -
                    kalmanGain[0] * _thetaCov[1][1];
  _thetaCov[1][0] = _thetaCov[1][0] * (1 - kalmanGain[1]) -
                    kalmanGain[1] * deltaFSBytes * t00;
  _thetaCov[1][1] = _thetaCov[1][1] * (1 - kalmanGain[1]) -
                    kalmanGain[1] * deltaFSBytes * t01;

  // Covariance matrix, must be positive semi-definite.
  assert(_thetaCov[0][0] + _thetaCov[1][1] >= 0 &&
         _thetaCov[0][0] * _thetaCov[1][1] -
                 _thetaCov[0][1] * _thetaCov[1][0] >=
             0 &&
         _thetaCov[0][0] >= 0);
}
  • 以上分成4步完成預測以及校準過程,其中第4步涉及到舉證的減法以及乘法的運算

  • 下面給出更新誤差協方差的矩陣運算公式


    image-20210613181020401.png
  • 到此為止卡爾曼濾波的一次更新以及迭代完成,總結其過程,卡爾曼濾波主要是為了得到較為準確的信道傳輸速率的倒數1/c(i)以及網絡排隊延遲m(i)

  • 最后通過如下計算jitterDelay

double noiseThreshold = _noiseStdDevs * sqrt(_varNoise) - _noiseStdDevOffset;
jitterDelay = _theta[0] * (_maxFrameSize - _avgFrameSize) + NoiseThreshold();
  • 其中_varNoise為測量誤差,通過計算當前幀的測量幀間延遲和上一次的卡爾曼最優估計出來的傳輸速率的倒數1/c(i)以及網絡排隊延遲m(i)并計算出估計幀間延遲進行相減,得出網絡殘差

  • 依據網絡殘差更新測量誤差平均值以及平均方差

  • 最終將最優估計jitterDelay作用到vcmTiming模塊用于計算最優期望渲染時間

  • 至此代碼分析完畢

4.5)webrtc jitterdelay相關數據測試

  • 通過實際調試,1080P@60fps 線上測試抓得如下數據


    image-20210613173701391.png
  • 以上一共近2000幀數據,從上圖來看,在937幀數據左右,畫面從靜止編程動畫,數據量瞬間增大,此時,平均幀大小的方差也隨之增大,從圖像右邊的測量噪聲的方差曲線來看,也發現測量噪聲的方差隨之增大
  • 右下圖顯示的是實際測量的frameDelayMs、jitterMs(估計后)、以及加上10ms操作延遲的變化曲線,由該曲線可以看出kalman filter最終確實能使實際的幀間抖動變得更加平滑,但是從曲線來看,似乎準確性有待提升。
  • 綜合分析,VCMJitterEstimator模塊所得出的jitterDelay它的核心作用是給到vcmTiming模塊用于估計期望渲染時間,為什么要這么做?
  • 在實際的傳輸過程中若完全不做處理,那么實際的幀間延遲就如上圖右下方藍色曲線所示,如果來一幀數據就送到解碼器讓解碼器做解碼并渲染,那么在數據量變化劇烈或突然出現網絡波動等一系列導致幀間延遲瞬時變化較大的時候可能會出現卡頓的問題,并且視覺上也會有明顯的表象

總結:

  • 本文通過分析VCMJitterEstimator模塊的實現,學習卡爾曼濾波的應用場景,并深入分析其濾波步驟等
  • 通過分析VCMJitterEstimator模塊了解webrtc視頻接收流水線中jitterDelay的用途以及更新過程,為進一步優化卡頓做鋪墊

參考文獻

[1].A Google Congestion Control Algorithm for Real-Time Communication draft-alvestrand-rmcat-congestion-03
[2].Analysis and Design of the Google Congestion Control for Web Real-time Communication (WebRTC)
[3].DR_CAN 卡爾曼濾波視頻教程
[4].從放棄到精通!卡爾曼濾波從理論到實踐

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
禁止轉載,如需轉載請通過簡信或評論聯系作者。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,885評論 6 541
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,312評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 177,993評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,667評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,410評論 6 411
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,778評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,775評論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,955評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,521評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,266評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,468評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,998評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,696評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,095評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,385評論 1 294
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,193評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,431評論 2 378

推薦閱讀更多精彩內容