播放端是整條管線裡唯一直接面對使用者的環節,也是唯一你通常控制不了執行環境的環節——別人的手機、別人的瀏覽器、別人的網路。前面所有優化的成果,最後都要通過這一關才算數,而這一關的評量標準不是「延遲多少」,是QoE(Quality of Experience)。


📖 學

一、播放器實際在做什麼

一個 ABR 播放器的迴圈只有四件事,不斷重複:

1. 下載清單 → 知道有哪些分片、有哪些碼率階
2. 估計當下能力 → 看歷史吞吐、看目前 buffer 水位
3. 決定下一個分片拉哪一階
4. 下載 → 塞進 buffer → 解碼 → 顯示

第 2 與第 3 步就是 ABR 演算法,它是播放器之間差異最大的地方。 同一條流、同樣的網路,不同播放器的畫質與卡頓表現可以差很多。

主要的播放器生態:

播放器適用備註
原生 <video>Safari/iOS 的 HLSApple 平台上原生支援 HLS,但可控性最低(ABR 策略、buffer 行為都由系統決定)
hls.js瀏覽器 HLS(非 Safari)用 MSE 自己實作,可控性高
dash.js瀏覽器 DASH參考實作,可控性高
Shaka PlayerHLS + DASH + DRMGoogle 維護,多 DRM 整合較完整
商用播放器跨平台付費換取跨裝置一致性與支援

iOS/Safari 的原生 HLS 是最大的變數。 在 iPhone 上,許多情況下你無法用 MSE 自己控制播放(受限於系統政策與版本),只能把清單交給系統播放器,然後接受它的 ABR 與 buffer 決策。這直接影響延遲(你調不動它的緩衝)與安全(自訂 HTTP 標頭通常送不出去,見 內容保護基礎)。

二、Buffer:唯一同時決定延遲與卡頓的東西

Buffer 是已下載但還沒播放的內容長度,單位是秒。

buffer 大 → 延遲高、抗抖動強、卡頓少
buffer 小 → 延遲低、抗抖動弱、卡頓多

「卡頓」在工程上的定義是 buffer 歸零。 播放器播到沒東西可播,只能停下來等。所以卡頓率本質上是「buffer 被抽乾的頻率」。

播放器通常有幾個 buffer 參數:

  • 起播門檻:buffer 累積到多少才開始播(影響首幀時間)。
  • 目標水位:穩定狀態下想維持多少。
  • 上限:最多預載多少(影響記憶體與頻寬浪費)。

低延遲直播的做法是把目標水位壓到接近 part 長度的 2–3 倍,但這同時意味著任何一次網路抖動都可能直接造成卡頓。這是為什麼低延遲系統必須同時提升抗抖動的其他手段(更保守的 ABR、更低的起始階、更快的降階反應)。

三、ABR 演算法的兩種流派與它們的失效模式

吞吐導向(throughput-based)——用最近幾個分片的下載速度估計頻寬,選一個略低於估計值的階。

  • 失效模式:估計落後現實。網路突然變差時,它要等下載完一個分片才知道,而那時 buffer 可能已經被抽乾。
  • 在低延遲(小 buffer)場景下特別危險。

緩衝導向(buffer-based)——看 buffer 水位:水位高就往上切,水位低就往下切。

  • 失效模式:啟動階段沒有訊號。剛開始播時 buffer 是空的,無法判斷。
  • 低延遲場景下 buffer 本來就很小,可用的判斷範圍被壓縮得幾乎沒有。

混合(hybrid)——實務上多數現代播放器的作法:啟動用吞吐、穩定後用緩衝、並加上遲滯避免震盪。

震盪(oscillation)是 ABR 最常見的體感問題:畫質每幾秒上下跳一次,比穩定在中等畫質更難看。對策是加大切換門檻的遲滯、限制單次切換的跨度(一次只能升一階)、並在升階前要求 buffer 達到一定水位。

四、QoE 指標:該量什麼

指標定義為什麼重要
首幀時間(TTFF / startup time)點擊到第一幀顯示直接決定跳出率,通常是影響最大的單一指標
卡頓率(rebuffering ratio)卡頓總時長 ÷ 觀看總時長對滿意度傷害最大
卡頓次數每次觀看平均卡幾次比總時長更貼近感受(短而頻繁最煩)
平均播放碼率實際播出的碼率加權平均畫質代理指標
碼率切換頻率每分鐘切換次數震盪的量化
播放失敗率完全播不出來的比例底線指標
端到端延遲glass-to-glass僅在互動場景重要

排序建議:首幀 > 卡頓率 > 播放失敗率 > 平均碼率 > 延遲。延遲放最後,除非你的場景有互動需求(下注、連麥、拍賣),否則它對滿意度的邊際影響遠低於前面幾項。

一個重要的量測紀律:客戶端上報的數據可能被偽造。 在有安全需求的場景(例如要據此做風控判斷),應該以伺服器端與邊緣節點的事實為主要依據,客戶端統計只當輔助訊號。這一點在使用者提供的方案文件裡被明確寫成一條原則,見 案例拆解。

五、首幀時間怎麼拆

首幀是最值得優化的指標,而它由幾段組成:

首幀 = 清單請求 + 清單解析 + 初始化分片下載 + 第一個媒體分片下載
       + 解碼器初始化 + 起播門檻等待

常見的優化點:

降低起播門檻。 不要等 buffer 滿才播,先播再補。代價是初期卡頓風險上升。

起始階選低一點。 用最低或次低階起播,播出來之後再往上升。這是首幀與初始畫質的直接取捨,多數場景應該偏向「先播出來」。

預連線與預載清單。 在使用者還沒點擊前就完成 DNS/TLS/清單下載(<link rel="preconnect">、提前 fetch 清單)。

減少重導與鑑權往返。 每一次 302 或鑑權 API 呼叫都加在首幀上。這是安全設計與體驗的直接衝突點——token 鑑權做得越複雜,首幀越慢。實務解法是讓鑑權與清單請求合併,或在頁面載入時就預先取得播放憑證。

六、瀏覽器端的現實限制

自動播放政策。 現代瀏覽器預設禁止有聲自動播放。直播頁面若要自動開始,必須靜音起播再提示使用者開聲音。這不是可以繞過的,也不該試圖繞過。

背景分頁節流。 分頁切到背景時,計時器被節流、解碼可能暫停。重新回到前景時播放器需要處理「追上直播邊緣」的邏輯,否則延遲會越累積越大。直播播放器必須有 catch-up 機制(通常是輕微加速播放,例如 1.05 倍速,直到追回目標延遲)。

記憶體限制。 行動裝置上 MSE 的 buffer 上限比桌面小得多,預載太多會被系統回收甚至讓分頁崩潰。

這些瀏覽器層面的限制與 響應式全螢幕遊戲 遇到的是同一組問題,那邊有更完整的展開。


🧠 記

卡頓的工程定義是 buffer 歸零,所以卡頓率本質上是 buffer 被抽乾的頻率。

ABR 的兩種流派各有失效模式,低延遲會同時削弱兩者的判斷依據,所以容易震盪。

QoE 的排序是首幀 > 卡頓 > 失敗率 > 碼率 > 延遲,延遲只在互動場景前移。

客戶端上報可被偽造,有安全需求時要以伺服器事實為準。

鑑權複雜度直接加在首幀上,這是安全與體驗最具體的衝突點。

直播播放器必須有 catch-up 機制,否則背景分頁回來後延遲會持續累積。


✍️ 實踐

用瀏覽器 console 做一次播放端歸因,20 分鐘:

// 在播放頁面的 console 執行
const v = document.querySelector('video')
 
// 1. 當前 buffer 秒數(延遲的最大來源之一)
const buffered = v.buffered.length
  ? v.buffered.end(v.buffered.length - 1) - v.currentTime
  : 0
console.log('buffer ahead:', buffered.toFixed(2), 's')
 
// 2. 卡頓監測:記錄 waiting 事件
let stalls = 0, stallStart = 0, stallTotal = 0
v.addEventListener('waiting', () => { stalls++; stallStart = performance.now() })
v.addEventListener('playing', () => {
  if (stallStart) { stallTotal += performance.now() - stallStart; stallStart = 0 }
})
setInterval(() => console.log(
  `stalls=${stalls} totalStall=${(stallTotal/1000).toFixed(1)}s ` +
  `ratio=${(stallTotal/1000/v.currentTime*100).toFixed(2)}%`
), 10000)
 
// 3. 解析度切換(ABR 震盪的直接證據)
setInterval(() => console.log('res:', v.videoWidth + 'x' + v.videoHeight), 2000)

讓它跑五分鐘,然後在開發者工具把網路調成 “Slow 3G” 或用 throttling 模擬抖動,觀察解析度切換的頻率。切換每分鐘超過三四次就是震盪,該調遲滯了。


🔗 延伸學習

  • hls.js — GitHub 與 API 文件——ABR 與 buffer 參數的可調項(abrEwmaDefaultEstimate、maxBufferLength、liveSyncDurationCount 等)都在這裡,是調低延遲最直接的旋鈕集合。
  • dash.js — GitHub——DASH 側的參考實作,ABR 演算法可替換,適合做對照實驗。

相關