從Chrome源碼看audio/video流媒體實現一

從Chrome源碼看audio/video流媒體實現一

來自專欄極樂科技130 人贊了文章

現在絕大多數的網站已經從flash播放器轉向了瀏覽器原生的audio/video播放器,瀏覽器是如何載入和解析多媒體資源的,這對於web開發者來說是一個黑盒,所以很有必要看一下瀏覽器是怎麼實現的,Chromium文檔介紹了整體的過程是這樣的:

大體來說,由video標籤創建一個DOM對象,它會實例化一個WebMediaPlayer,這個player是控制中樞,player驅使Buffer去請求多媒體數據,然後交由FFmpeg進行多路解復用和音視頻解碼(FFmpeg是一個開源的第三方音視頻解碼庫),再把解碼後的數據傳給相應的渲染器對象進行渲染繪製,最後讓video標籤去顯示或者音效卡進行播放。

這裡面有兩個問題需要重點關註:

(1)編解碼的過程是怎麼樣的?

(2)流媒體數據的傳輸是怎麼控制的?

1. 編解碼基礎

攝像機或者相機通過鏡頭裡的感光元件,把收到的光轉換為電子像素,一個像素是由rgba 4通道組成,在視頻裡面通常只有rgb 3通道,共8b * 3 = 24b = 3B即3個位元組,一個1080p(1920 * 1080),幀率為30fps(一秒鐘有30張圖片)時長為1分鐘的視頻有多大呢?計算如下:

3B * 1920 * 1080 * 30 * 60 = 12441600000B = 10GB

約有10GB,一個100分鐘的電影就需要佔用1TB的硬碟空間,所以如果沒有壓縮的話這個體積是非常巨大的。但是我們看到一個1080p的mp4文件(h264,普通碼率)平均1分鐘的體積只有100MB不到,如下圖所示:

在這個例子裡面,h264編碼的mp4文件壓縮比達到了100:1。

因此編碼的目的就是壓縮,而解碼的目的就是解壓縮。壓縮又分為有損壓縮和無損壓縮,無損壓縮如gzip/flac,是可以還原成原本未壓縮的完整數據,而有損壓縮如h264/mp3等一旦壓完之後就無法再還原成更高清晰度的數據了。

視頻編碼通常都是有損壓縮,目標是降低一點清晰度的同時讓體積得到大大的減少,降低的清晰度能達到對人眼不可察覺或者幾乎無法分辨的水平。主要是方法是去除視頻裡面的冗餘信息,對於很多不是劇烈變化的場面,相鄰幀裡面有很多重複信息,通過幀間預測等方法分析和去除,而幀內預測可去掉同個幀里的重複信息,還有對畫面觀眾比較關注的前景部分高碼率編碼,而對背景部分做低碼率編碼,等等,這些取決於不同的壓縮演算法。

視頻的編碼方式從MPEG-1(VCD)到MPEG-2(DVD)再到MPEG-4(數碼相機),再到現在主流的網路視頻用的h264和比較新的h265,同等壓縮質量下,壓縮率不斷地提到提升,它們可能都是.mp4的文件後綴,mp4格式是一個容器, 可以支持流式傳輸,不用把整個文件下載下來才播放。另外還有谷歌主導的VP8、VP9和AV1編碼(主要在WebRTC裡面使用)。編碼的壓縮率越好,它的演算法通常會更複雜,所以一些低端設備可能會扛不住進而使用較老的編碼格式。

解碼的目的就是解壓縮,把數據還原成原始的像素。FFmpeg是一個很出名的開源的編解碼C庫,Chrome也使用了它做為它的解碼器之一,並且有人把它轉成了WASM,可以在網頁上跑。

視頻是用rga像素表示相對比較好理解,聲音又是怎麼表達呢?

屏幕圖像是用rgb即紅綠藍三原色混合組成各種各樣的顏色,而聲音是物體振動擠壓空氣形成的聲波,它主要有3個感觀屬性:音量(振幅)、音調(頻率)、音色,不同材料的音色不一樣,但是一個幾塊錢的耳機為什麼能夠發出各種各樣的音色的聲音呢?

要形成聲音需要振幅的周期性變化,如果只是振幅不斷單向變大是形成不了聲音的,下圖表示一個聲音的振幅隨時間變化的波形:

純音的波形是一個正弦曲線,但是在現實世界裡面一個物體主體部分在振動的同時,它上面的不同部位也會在做自己的振動,主體振動發出的聲音叫基音,子部位發出的聲音叫諧音,諧音往往有成百上千個,也就是說有上千個正弦曲線疊加:

y = 1/2 sin(x) + 3/7 sin(x) + ...

就形成了上面不規則的周期性曲線。這條曲線就決定了聲音的所有屬性包括音量(最大振幅和最小振幅的差距)、音調(周期性變化的快慢)以及音色,一個確定的波形就確定了音色。也就是說只要把喇叭按照上面的振幅移動就能夠還原記錄的聲音。這就是聲音播放的原理,沒錯就是這麼簡單。那麼這些幀幅是怎麼記錄的呢?

視頻是每秒25或者30幀圖像形成連續的動畫,用離散來表示連續,聲音也是同樣道理,它有一個採樣率的概念,麥克風把聲音轉化為不同強度的電流信號,就像開了一個水龍頭源源不斷地流水,每一點的水溫都是不一樣的,如果採樣率為1k Hz即1s 1000次,1s測1000次流過的水的溫度,CD唱片音質的採樣率44.1k Hz,所以為了高保真,1s需要測44k次。

溫度大小表示也是連續的,所以有一個位深的概念,位深為16位則表示有16位二進位數來表示水溫的水平,從低到高共65536個層次。

所以採樣率和位深就決定了音質的好壞,我們知道CD是超高音質的代表了,它的採樣率是44.1kHz和位深是16位,1分鐘的雙聲道(立體音)聲音的CD格式存儲需要佔用的硬碟空間大小為:

44100 * 16b * 60 / 8 * 2 = 10MB

所以一個沒有壓縮的聲音1分鐘需要10MB的空間,記錄為PCM格式,而FLAC/APE等編碼是無損壓縮,相對zip/gzip,它的特點是能夠實時播放。

而mp3格式是有損壓縮,它有一個碼率的概率,標準規定碼率最高為320kbps,常見的還有128kbps/192kbps等,如果碼率為192kbps,它是意思是每秒有192kb大小的內容:192kb = 192kB/ 8 = 24kB,每分鐘有24kB * 60 = 1.44MB,相對於PCM的10MB,壓縮比為7 : 1,所以這個壓縮率也是挺高的,對於192kbps的音質和無損音質,一般的音響或者普通人的耳朵來說已經無法分辨。

MP3壓縮的原理主要是人耳的聽覺掩蔽效應,對低頻聲音會比高頻的聲音感知度更高,壓縮時進行頻譜分析,把一些高頻的聲音降低分配的位數,從而達到極小失真但大大小減少體積的目的。

h264的音軌主要使用AAC編碼格式,相對於mp3同等音質AAC的壓縮率更高。

所以聲音解碼的目的就是把mp3/aac等格式還原成原始的PCM格式。

2. Chrome的buffer控制載入機制

在流媒體傳輸裡面有一個重要的概念就是buffer緩衝空間大小,如果buffer太大,那麼一次性下載的數據太多,用戶還沒播到那裡不划算,相反如果buffer太小不夠播放可能會經常卡住載入。在實時傳輸領域,實時流媒體通信的雙方如果buffer太大的話會導致延遲太大,如果buffer太小那麼能做的事情就比較少如擁塞控制、丟包重傳等。

我們可以來看一下Chrome播放音視頻的時候buffer是怎麼控制的,它的實現是在src/media/blink/multibuffer_data_source.cc這個文件的UpdateBufferSizes函數,簡單來說就是每次都往後預載入10s的播放長度,並且最大不超過50MB,最小不小於2MB,往前是保留2s播放長度。

詳細來說,首先要獲取碼率,即1s的音視頻需要佔用的空間,如下代碼所示:

// If bitrate is not known, use this.const int64_t kDefaultBitrate = 200 * 8 << 10; // 200 Kbps.// Maximum bitrate for buffer calculations.const int64_t kMaxBitrate = 20 * 8 << 20; // 20 Mbps.// Use a default bit rate if unknown and clamp to prevent overflow.int64_t bitrate = clamp<int64_t>(bitrate_, 0, kMaxBitrate);if (bitrate == 0) bitrate = kDefaultBitrate;

有一個默認碼率是200Kbps,最大碼率不超過20Mbps,如果還不知道碼率的情況下就使用默認碼率。在使用一個mp3文件做demo研究的時候發現preload了3次之後才拿到碼率,如下圖所示:

碼率為128kbps,知道碼率之後再獲取播放速率通常為默認的1倍速,進而知道10s應該是多少空間:

// 這裡的播放速率playback_rate為1 int64_t bytes_per_second = (bitrate / 8.0) * playback_rate; // 預載入10s的數據,不超過最大,不小於最小 int64_t preload = clamp(kTargetSecondsBufferedAhead * bytes_per_second, kMinBufferPreload, kMaxBufferPreload);

然後還有一個調整,再加上當前已下載數據的10%:

// Increase buffering slowly at a rate of 10% of data downloaded so // far, maxing out at the preload size. int64_t extra_buffer = std::min( preload, url_data_->BytesReadFromCache() * kSlowPreloadPercentage / 100); // Add extra buffer to preload. preload += extra_buffer;

然後把preload值傳給BufferReader,由它去觸發請求相應的數據。這個是使用http range功能請求相應範圍的位元組數,如下檢查Chrome的請求:

在某個請求裡面Chrome請求的範圍起點是從3604480開始,沒有終點,但是這裡並不是說Chrome只是發一個請求然後一次性把整個文件下載下來了,它應該是通過擁塞窗口之類的方法控制接收的速率,具體沒有深入去研究,把視頻暫停了你會發現載入停止了,再播放原先的http返回數據量又開始變大。

對於很多視頻網站我們發現他們的range都是指定明確範圍的:

相比之下原生播放器的策略只是使用一個http連接載入不同位元組範圍的視頻數據,而視頻網站是每需要一個range的數據的時候就發一個請求。這些請求的響應狀態碼都是206,表示返回部分內容。如果連接斷了或者服務端只返回部分數據就關閉連接,那麼chrome會重新發個請求。另外打開頁面的時候chrome不會提前預載入數據,只有點擊播放了才載入音視步內容。並且preload的buffer size是在載入過程中周期性更新的。

綜上,本篇主要介紹了編解碼的基礎,chrome的buffer載入機制,以及chrome載入和解析流媒體的基本模型,我們還沒有討論具體的解碼過程,下一篇將會繼續研究。

推薦閱讀:

今日流媒體
傳統主流媒體在全媒體時代的融合之道
Spotify:有權力不用王八蛋?!
網站運營探討(二):2008年主流媒體是誰?
主流媒體是個寶(外七篇)

TAG:流媒體 | 前端開發 | 源碼閱讀 |