JOVANA
Explore Library Glossary Getting Started Three Levels Fields How it works Mission
Join the mission
All guides

抖動與播放緩衝區

在即時媒體的三個敵人裡,抖動是最陰險的那一個:網路不只是替每個封包加上延遲,而是替每個封包加上不一樣的延遲。本文要講清楚這件事為什麼會毀掉一通電話,以及一個小小的緩衝區——一段刻意而巧妙的等待——是如何把斷斷續續的一團亂,變回平順的語音。

每次延遲都一樣,其實沒問題

上一篇點名了即時媒體的三個敵人——延遲、遺失,還有最陰險的那一個:抖動——並且承諾這一篇會把解藥拆開來看。我們先釐清一件大多數人會意外的事:一通電話其實能好好地撐過很大的延遲,只要那個延遲對每個封包都一樣。想像網路總是替每個語音封包正好加上 200 毫秒。對對話式媒體來說這很惱人,因為你會開始跟對方搶話,但音訊本身會被平順地播出來——每一小塊都晚到 200 毫秒,依序到達、間隔均勻,就跟它被講出來的時候一模一樣。

麻煩在於,網際網路並不會每次都給你一樣的延遲。一個語音來源把聲音切成一串穩定的小封包——比方說每 20 毫秒一個——並且像節拍器那樣間隔均勻地把它們交給網路。但接下來每個封包都各走各的路,穿過那些佇列正在一刻一刻地填滿又排空的路由器。某個封包剛好穿過幾乎空著的佇列;下一個卻卡在別人一陣突發流量的後頭。它們是像鐘錶一樣準時送出的,到達時卻成團又有空隙。這種到達時間上的變動,正是我們所說的抖動

抖動究竟從哪裡來

把一個封包的總延遲拆成幾塊會很有幫助。有固定的部分——傳播延遲,也就是光或電在實體上跨越線路所需的時間,它只取決於距離,對某一條既定路徑而言永遠不變。也有可變的部分——佇列延遲,也就是封包在路由器緩衝區裡排在別的封包後面所花的等待時間。傳播是定值;佇列不是,因為排在你前面的封包有多少,取決於在那個確切瞬間其他所有使用者在做什麼。抖動本質上就是這個佇列成分逐封包之間的變動。

抖動為什麼會毀掉播放

假設你的音訊播放器做了那件天真的事:每個封包一到就立刻播。來源在時間 0 送出第 1 塊、20 毫秒時送第 2 塊、40 毫秒時送第 3 塊——完美均勻。但假設第 1 塊延遲 30 毫秒到達、第 2 塊又是 30 毫秒,而第 3 塊卡住了、延遲 75 毫秒才到。現在出現了一個空隙:第 2 塊播完之後,第 3 塊還沒到,於是說話聲沉默了一拍,接著第 3 塊遲遲地脫口而出,後面幾塊又趕著追上來。結果就是大家都認得的那種斷續、機械、咕嚕咕嚕的聲音——壞掉的通話的聲音。音訊本身是好的;是傳遞的時序把它毀了。

關鍵的洞見在這裡,而且近乎弔詭:對付可變延遲的解藥,是刻意加上一點固定的延遲。如果接收端拒絕一收到任何一塊就立刻播,而是在播出之前先把每一塊都拿著、做一段短暫而刻意的等待,那麼慢到的封包就有時間追上快到的封包。我們用一小段固定的延遲,去換來一個平順、節拍均勻的輸出。那個刻意設下的等候室,就是播放緩衝區,有時也叫抖動緩衝區。

播放緩衝區是怎麼運作的

這個把戲靠的是一條資訊:每個封包都帶著一個時間戳記(timestamp),標明它在來源端是何時產生的。(下一篇會看到,這正是 RTP 標頭所提供的東西。)利用時間戳記,接收端為整條串流挑一個單一的播放點(playout point):一個固定的偏移量,叫它 d,在某封包的產生時間之後。規則於是很簡單:在時間 t 產生的封包,就在時間 t + d 播出,不早也不晚。在自己的播放時刻之前就到達的封包,會在緩衝區裡等候;而播放點 d 挑得夠大,大到幾乎所有封包都來得及。

  1. 一個封包到達。接收端讀出它的來源時間戳記 t,並記下它實際出現的時刻。
  2. 如果本地時鐘還沒到 t + d,這個封包就是早到——把它放進緩衝區依時間戳記的順序等候。
  3. 到了時間 t + d,就把那個封包從緩衝區取出,交給音訊解碼器播放,正好踩在它排定的節拍上。
  4. 如果某個封包到了它的 t + d 時刻還沒到,對播放而言它就等同遺失了——跳過它,並用遺失隱藏來填補那個空隙,因為再等下去會讓整條串流卡住。

因為輸出現在是以來源時間戳記、而不是以到達時間來計時的,播放器又會像節拍器一樣每 20 毫秒吐出一塊,不管到達有多麼參差不齊。緩衝區已經把那些變動吸收掉了。把先前的例子放進去跑一遍:挑 d = 產生後 80 毫秒。第 3 塊晚了 75 毫秒到,但它的播放時刻是在 80 毫秒處,所以它早就在緩衝區裡等著,還多出 5 毫秒——沒有空隙,沒有卡頓。

躲不掉的取捨,以及一個沒到的封包

挑選播放偏移量 d 是一場貨真價實的拉鋸,沒有哪個設定能兩頭通吃。把 d 設大,緩衝區連嚴重延遲的封包都能吸收,於是幾乎沒有東西被丟掉、音訊保持平順——但對方說的每個字現在都得晚那麼一點才傳到你耳裡,而在即時對話裡,延遲太大會讓人們搶起話來。把 d 設小,對話感覺起來俐落而即時——但任何一個佇列延遲暴衝超過 d 的封包,都會徹底錯過它的時段、被當成遺失。平順對上即時:你只能犧牲其中一邊,去偏向另一邊。

好的軟體電話不會把 d 挑定一次就凍結;它們跑的是自適應(adaptive)的播放緩衝區。它們持續量測近期的抖動——也就是到達延遲最近散得有多開——在網路變得顛簸時把 d 往上推、在它平靜下來時往下調。它們把這些調整偷偷塞進字詞之間自然的停頓裡,於是你永遠不會聽到緩衝區在伸長或縮短。這就是為什麼一條不穩的連線上的 VoIP,有時感覺像是又往後漂了一拍:那是緩衝區剛剛長大,好騎過一段崎嶇的路。

那麼那個真的永遠來不及到的封包呢?你不會讓串流卡住去等它——那等於用一次更糟的凍結,去換掉一次卡頓。你會跳過它,並倚靠遺失隱藏:解碼器把那個洞遮起來,例如重複前一小段聲音、或者平滑地在空隙兩端之間內插。單獨一塊不見的 20 毫秒語音,用這種方式糊過去,通常是察覺不到的。這也是為什麼即時媒體這麼常跑在 UDP 而不是 TCP 上:TCP 會盡責地重送那個遺失的封包,但等重送到達時,它的播放時刻早就過去了——遲到的音訊就是沒用的音訊,所以與其等到卡住,不如跳過再隱藏。