1. 程式人生 > >ExoPlayer Talk 01 緩存策略分析與優化

ExoPlayer Talk 01 緩存策略分析與優化

sca google mes efi allocator method policy 類型 let

操作系統:Windows8.1

顯卡:Nivida GTX965M

開發工具:Android studio 2.3.3 | ExoPlayer r2.5.1


使用 ExoPlayer 已經有一段時間了,對播放器的整體架構設計 到 具體實現 佩服至極,特別建議開發播放器的同學有機會一定要看看,相信會受益匪淺。這次分享的內容主要關於緩存策略優化。

Default Buffer Policy


Google ExoPlayer提供了默認的AV數據的緩存策略,並通過 DefaultLoadControl 組件實現。該加載器組件本身沒有問題,只不過在一些情景下,這種默認緩存策略,會減損"緩存"本身的效果。在 DefaultLoadControl

中有如下代碼片段:

  @Override  
public boolean shouldContinueLoading(long bufferedDurationUs) { ...
isBuffering = bufferTimeState == BELOW_LOW_WATERMARK || (bufferTimeState == BETWEEN_WATERMARKS && isBuffering && !targetBufferSizeReached); ...return isBuffering; }

該函數由於播放器調用,以確定是否應該繼續加載緩存AV數據。

/**
   * The default minimum duration of media that the player will attempt to ensure is buffered at all
   * times, in milliseconds.
   */
  public static final int DEFAULT_MIN_BUFFER_MS = 15000;

  /**
   * The default maximum duration of media that the player will attempt to buffer, in milliseconds.
   
*/ public static final int DEFAULT_MAX_BUFFER_MS = 30000; /** * The default duration of media that must be buffered for playback to start or resume following a * user action such as a seek, in milliseconds. */ public static final int DEFAULT_BUFFER_FOR_PLAYBACK_MS = 2500; /** * The default duration of media that must be buffered for playback to resume after a rebuffer, * in milliseconds. A rebuffer is defined to be caused by buffer depletion rather than a user * action. */ public static final int DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS = 5000;

DEFAULT_MIN_BUFFER_MS 常量定義了播放器觸發加載AV數據的時機,即當前緩沖區AV數據 duration time 小於15秒。

DEFAULT_MAX_BUFFER_MS 常量定義了播放器進行加載AV數據的上限,即當前緩沖區AV數據 duration time 小於30秒。

DEFAULT_BUFFER_FOR_PLAYBACK_MS 常量定義了播放器播放AV數據的條件,即緩沖區必須滿足AV數據 duration time 不小於2.5秒。

DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS 常量定義了播放器從 REBUFFER狀態(REBUFFER是由於運行時緩沖區耗盡觸發導致) 恢復為可播放狀態後可播放AV數據的條件,即緩沖區必須滿足AV數據 duration time 不小於5秒。

所以根據以上代碼了解,前兩個參數用於描述加載時序,後兩個參數用於描述是否有足夠的緩沖數據來播放。

我們可以概述默認加載器組件會按照如下方式工作:

  1. 加載組件持續加載,直到緩沖AV數據大於30秒,停止加載,進入等待狀態。
  2. 當緩沖AV數據小於15秒時,加載器重新執行加載邏輯。
  3. 播放器根據當前緩沖區AV數據 duration time 控制是否播放。

那麽問題來了,這樣設計的目的是什麽?很難想到一個情景應用該策略,換句話說是否可以自定義修改15秒30秒這樣的數值呢?

What about Buffering?


There are arguments that mobile carriers prefer this kind of traffic pattern over their networks (i.e. bursts rather than drip-feeding). Which is an important consideration given ExoPlayer is used by some very popular services. It may also be more battery efficient.

Whether these arguments are still valid is something we should probably take another look at fairly soon, since the information we used when making this decision is 3-4 years old now. We should also figure out whether we should adjust the policy dynamically based on network type (e.g. even if the arguments are still valid, they may only hold for mobile networks and not for WiFi).

關於此問題已經有人問詢過,其中一個ExoPlayer開發人員給出這樣的答復,大致意為,目前主流移動運營商將作為ExoPlayer的相關實現重要考慮對象,有證據表明運營商們更傾向於這種被稱為 bursts 的流量模式,而不是 drip-feeding 類的流量模式。除此之外也會提升電池使用效率。無論這些證據是否有效,很快會再次的了解一下,因為做出這個決定所參考的是3-4年前的信息了。還應該確定是否有必要根據網絡類型動態調整策略(即使這些參數仍然使用,它們只適用於移動網絡,而不是WiFi)。

盡現在使用的默認緩存策略比較通用,但不可能滿足所有的情況。為了更好的點播體驗,增加緩沖數據 duration time 為1分鐘或者更久,接下來我們進一步分析ExoPlayer默認緩存策略的實現原理,並在最後給出一般性的優化例子。

Water Marks


在默認的緩存策略實現中,有一個 water marks 的概念,類似水桶盛水過程中變化的"水位 ",具體代碼為:

  private static final int ABOVE_HIGH_WATERMARK = 0;
  private static final int BETWEEN_WATERMARKS = 1;
  private static final int BELOW_LOW_WATERMARK = 2;

三個級別的水位與前面提到的四個常量的關系如圖所示:

技術分享

參考完整的 getBufferTimeState()shouldContinueLoad() 函數,其中 getBufferTimeState() 根據當前的緩存AV數據的 duration time 來判斷處於哪個水位。

private int getBufferTimeState(long bufferedDurationUs) 
{
    return bufferedDurationUs > maxBufferUs ? ABOVE_HIGH_WATERMARK
        : (bufferedDurationUs < minBufferUs ? BELOW_LOW_WATERMARK : BETWEEN_WATERMARKS);
}
@Override
  public boolean shouldContinueLoading(long bufferedDurationUs)
  {
      int bufferTimeState = getBufferTimeState(bufferedDurationUs);
      boolean targetBufferSizeReached = allocator.getTotalBytesAllocated() >= targetBufferSize;
      boolean wasBuffering = isBuffering;

      isBuffering = bufferTimeState == BELOW_LOW_WATERMARK || (bufferTimeState == BETWEEN_WATERMARKS && isBuffering && !targetBufferSizeReached);

      if (priorityTaskManager != null && isBuffering != wasBuffering)
      {
          if (isBuffering)
          {
            priorityTaskManager.add(C.PRIORITY_PLAYBACK);
          }
          else
          {
            priorityTaskManager.remove(C.PRIORITY_PLAYBACK);
          }
      }

      return isBuffering;
  }

一個典型的加載行為經過 A、B、C 三個階段:

Pass A

起始階段,加載器開始連續的加載AV數據,一直達到或者超過 maxBufferUs 水位為止,需要註意的是這個時候時間線為停頓在 t1 ,如下圖所示:

技術分享

Pass B

t1 時間後,播放器消費緩沖區AV數據,但 isBuffering = false 並且 bufferTimeState == BETWEEN_WATERMARK,所以 shouldContinueLoading() 仍然返回 false,即不需要加載AV數據,時間線停頓在 t2 ,如下圖所示:

技術分享

Pass C

來到最後一個階段,當播放器持續消費緩沖區AV數據,直到水位低於 minBufferUs ,即 bufferTimeState == BELOW_LOW_WATERMARK 時候,我們恢復加載程序,時間線停頓在 t3 ,如下圖所示:

技術分享

作為一個小節,通過三個階段的圖示我們了解到,從 t1t3 之間區間內,加載器沒有做任何加載操作。因此會遇到這種情景,某時刻緩沖區中只有僅僅15秒的緩沖數據。

除此之外,對於緩沖區大小也是有限制的,一般來說當網絡狀況良好時,一般都可以緩存 15 到 30 秒的AV數據,換句話說,有可能根據需求擴展緩沖區大小。

How to customize the buffer?


應用前面提到的 drip - feeding 滴灌方式,移除緩沖區的上線限制,代碼如下:

isBuffering = bufferTimeState == BELOW_LOW_WATERMARK
                || (bufferTimeState == BETWEEN_WATERMARKS
            /*
             * commented below line to achieve drip-feeding method for better caching. once you are below maxBufferUs, do fetch immediately.
             */
            /* && isBuffering */
                && !targetBufferSizeReached);

同時擴大 maxBufferUs minBufferUs

/**
 * To increase buffer time and size.
 */
public static int BUFFER_SCALE_UP_FACTOR = 4;
....
minBufferUs = BUFFER_SCALE_UP_FACTOR * minBufferMs * 1000L;
maxBufferUs = BUFFER_SCALE_UP_FACTOR * maxBufferMs * 1000L;
...

可以在 shouldContinueLoading() 函數下面添加日誌,驗證修改前後的不同表現。

Log.d(CustomLoadControl.class.getSimpleName(), "current buffer durationUs: " + bufferedDurationUs + ",max bufferUs: " + maxBufferUs + ", min bufferUs: " + minBufferUs + " shouldContinueLoading: " + isBuffering);

修改之前的LOG如下,可以觀測到只有第一次水位達到 maxBufferUs ,之後的緩存策略一直維持在 minBufferUs

D/DefaultLoadControl:    current    buffer    durationUs:    0,max    bufferUs:    30000000,    min    bufferUs:    15000000    shouldContinueLoading:    TRUE
D/DefaultLoadControl:    current    buffer    durationUs:    981333,max    bufferUs:    30000000,    min    bufferUs:    15000000    shouldContinueLoading:    TRUE
D/DefaultLoadControl:    current    buffer    durationUs:    2154666,max    bufferUs:    30000000,    min    bufferUs:    15000000    shouldContinueLoading:    TRUE
D/DefaultLoadControl:    current    buffer    durationUs:    3136000,max    bufferUs:    30000000,    min    bufferUs:    15000000    shouldContinueLoading:    TRUE
...
D/DefaultLoadControl:    current    buffer    durationUs:    15160125,max    bufferUs:    30000000,    min    bufferUs:    15000000    shouldContinueLoading:    TRUE
D/DefaultLoadControl:    current    buffer    durationUs:    15973479,max    bufferUs:    30000000,    min    bufferUs:    15000000    shouldContinueLoading:    FALSE
D/DefaultLoadControl:    current    buffer    durationUs:    15973479,max    bufferUs:    30000000,    min    bufferUs:    15000000    shouldContinueLoading:    FALSE
D/DefaultLoadControl:    current    buffer    durationUs:    15963667,max    bufferUs:    30000000,    min    bufferUs:    15000000    shouldContinueLoading:    FALSE
...
D/DefaultLoadControl:    current    buffer    durationUs:    15003688,max    bufferUs:    30000000,    min    bufferUs:    15000000    shouldContinueLoading:    FALSE
D/DefaultLoadControl:    current    buffer    durationUs:    14993604,max    bufferUs:    30000000,    min    bufferUs:    15000000    shouldContinueLoading:    TRUE
D/DefaultLoadControl:    current    buffer    durationUs:    15975896,max    bufferUs:    30000000,    min    bufferUs:    15000000    shouldContinueLoading:    FALSE
D/DefaultLoadControl:    current    buffer    durationUs:    15975896,max    bufferUs:    30000000,    min    bufferUs:    15000000    shouldContinueLoading:    FALSE
D/DefaultLoadControl:    current    buffer    durationUs:    15964834,max    bufferUs:    30000000,    min    bufferUs:    15000000    shouldContinueLoading:    FALSE
...
D/DefaultLoadControl:    current    buffer    durationUs:    15005417,max    bufferUs:    30000000,    min    bufferUs:    15000000    shouldContinueLoading:    FALSE
D/DefaultLoadControl:    current    buffer    durationUs:    14994542,max    bufferUs:    30000000,    min    bufferUs:    15000000    shouldContinueLoading:    TRUE
D/DefaultLoadControl:    current    buffer    durationUs:    15891750,max    bufferUs:    30000000,    min    bufferUs:    15000000    shouldContinueLoading:    FALSE
D/DefaultLoadControl:    current    buffer    durationUs:    15891750,max    bufferUs:    30000000,    min    bufferUs:    15000000    shouldContinueLoading:    FALSE
D/DefaultLoadControl:    current    buffer    durationUs:    15880042,max    bufferUs:    30000000,    min    bufferUs:    15000000    shouldContinueLoading:    FALSE
...
D/DefaultLoadControl:    current    buffer    durationUs:    15003708,max    bufferUs:    30000000,    min    bufferUs:    15000000    shouldContinueLoading:    FALSE
D/DefaultLoadControl:    current    buffer    durationUs:    14992667,max    bufferUs:    30000000,    min    bufferUs:    15000000    shouldContinueLoading:    TRUE
D/DefaultLoadControl:    current    buffer    durationUs:    15601000,max    bufferUs:    30000000,    min    bufferUs:    15000000    shouldContinueLoading:    FALSE
D/DefaultLoadControl:    current    buffer    durationUs:    15601000,max    bufferUs:    30000000,    min    bufferUs:    15000000    shouldContinueLoading:    FALSE
D/DefaultLoadControl:    current    buffer    durationUs:    15588708,max    bufferUs:    30000000,    min    bufferUs:    15000000    shouldContinueLoading:    FALSE
...
D/DefaultLoadControl:    current    buffer    durationUs:    15004458,max    bufferUs:    30000000,    min    bufferUs:    15000000    shouldContinueLoading:    FALSE
D/DefaultLoadControl:    current    buffer    durationUs:    14993416,max    bufferUs:    30000000,    min    bufferUs:    15000000    shouldContinueLoading:    TRUE
D/DefaultLoadControl:    current    buffer    durationUs:    16081313,max    bufferUs:    30000000,    min    bufferUs:    15000000    shouldContinueLoading:    FALSE

修改之後,緩沖區擴大,持續加載,維持在 maxBufferUs

D/CustomControl:    current    buffer    durationUs:          0,max bufferUs:120000000,    min    bufferUs:60000000    shouldContinueLoading:    TRUE
D/CustomControl:    current    buffer    durationUs:     981333,max bufferUs:120000000,    min    bufferUs:60000000    shouldContinueLoading:    TRUE
D/CustomControl:    current    buffer    durationUs:    2154666,max bufferUs:120000000,    min    bufferUs:60000000    shouldContinueLoading:    TRUE
...
D/CustomControl:    current    buffer    durationUs:    53194146,max bufferUs:120000000,    min    bufferUs:60000000    shouldContinueLoading:    TRUE
D/CustomControl:    current    buffer    durationUs:    54319750,max bufferUs:120000000,    min    bufferUs:60000000    shouldContinueLoading:    TRUE
D/CustomControl:    current    buffer    durationUs:    55313834,max bufferUs:120000000,    min    bufferUs:60000000    shouldContinueLoading:    TRUE

Ok,本次分享的內容就到這裏了,以上內容如有不對之處,請多多指正。

ExoPlayer Talk 01 緩存策略分析與優化