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

第一篇主要介紹了Chrome加載音視頻的緩衝控制機制和編解碼基礎,本篇將比較深刻地介紹解碼播放的過程。以Chromium 69版本作研究。html

因爲Chromium默認不能播放Mp4,因此須要須要改一下源碼從新編譯一下。前端

1. 編譯一個能播放mp4的Chromium

自行編譯出來的Chromium是沒法播放mp4視頻,在官網下載的也不行,終端會提示這個錯誤:算法

[69542:775:0714/132557.522659:ERROR:render_media_log.cc(30)] MediaEvent: PIPELINE_ERROR DEMUXER_ERROR_NO_SUPPORTED_STREAMSchrome

說是在demux即多路解複用的時候發生了錯誤,不支持當前流格式,也就是Chromium不支持mp4格式的解析,這是爲何呢?通過一番搜索和摸索,發現只要把ffmpeg的編譯模式從Chromium改爲Chrome就能夠。編輯third_party/ffmpeg/ffmpeg_options.gni這個文件,把前面幾行代碼改一下——把_default_ffmpeg_branding強制設置成Chrome,再從新編譯一下就好了,以下代碼所示:小程序

# if (is_chrome_branded) {
  _default_ffmpeg_branding = "Chrome"
# } else {
# _default_ffmpeg_branding = "Chromium"
# }複製代碼

編譯出來的Chromium就能播放視頻了。Chromium工程的編譯目標branding能夠設置成Chrome(正式版)/Chromium/Chrome OS三種模式,ffmpeg的編譯設定會根據這個branding類型自動選擇它本身的branding,如上代碼的判斷,若是branding是Chrome,會額外多加一些解碼器,就可以播放mp4了。微信小程序

不過若是你想編譯成正式版Chrome,因爲缺乏相關的主題theme文件,是編譯不了的。bash

另外它還有一個proprietary_codecs的設置:微信

proprietary_codecs = is_chrome_branded || is_chromecast複製代碼

編譯正式版的Chrome會默認打開,它的做用是增長一些額外的解碼器,如對EME(Encrypted Media Extensions)加密媒體擴展的支持。數據結構

最後一開始不能打開和能打開播放的效果對好比下圖所示:編輯器

那麼爲何Chromium不直接打開mp4支持呢,它可能受到mp4或者ffmpeg的一些開源協議和專利限制。

2. mp4格式和解複用

一個視頻能夠有3個軌道(Track):視頻、音頻和文本,可是數據的存儲是一維的,從1個字節到第n個字節,那麼視頻應該放哪裏,音頻應該放哪裏,mp4/avi等格式此作了規定,把視軌音軌合成一個mp4文件的過程就叫多路複用(mux),而把mp4文件裏的音視頻軌分離出來就叫多路解複用(demux),這兩個詞是從通訊領域來的。

假設如今有個需求,須要取出用戶上傳視頻的第一幀作爲封面。這就要求咱們去解析mp4文件,並作解碼。

咱們以這個mountain.mp4時長爲1s大小487kB的mp4視頻作爲研究對象,用sublime等編輯器打開顯示其原始的二進制內容,以下圖所示:

上圖是用16進製表示的原始二進制內容,兩個16進制(0000)就表示1個字節,如上圖第4個字節是0x18。

mp4是使用box盒子表示它的數據存儲的,標準規定了若干種盒子類型,每種盒子存放的數據類型不同,box能夠嵌套box。每一個box的前4個字節表示它佔用的空間大小,如上面第一個box是0x18 = 24字節,也就是說在接下來的24字節都是這個box的內容,因此上圖到第2行的3431就是第一個box的內容。在前4個表示大小的字節以後緊接着的4個字節是盒子類型,值爲ASCII編碼,第一個盒子的類型爲:

6674 7970 => ftyp

ftyp盒子的做用是用來標誌當前文件類型,緊接着的4個字節表示它是一個微軟的MPEG-4格式,即日常說的mp4:

6d70 3432 => mp42

綜上,第1個盒子總體解析以下圖所示:

一樣對第二個盒子作分析,以下圖所示:

它是一個moov的盒子,moov存儲了盒子的metadata信息,包括有多少個音視頻軌道,視頻寬高是多少,有多少sample(幀),幀數據位於什麼位置等等關鍵信息。注意mp4格式多媒體數據存儲能夠是不連續的,日後播放的可能反而放在前面,可是不要緊。由於這些位置信息均可以從moov這個盒子裏面找到。若干個sample組成一個chunk,即一個chunk能夠包含1到多個sample,chunk的位置也是在moov盒子裏面。

最後面是一個mdat的盒子,這個就是放多媒體數據的盒子,大小爲492242B,它佔據了mp4文件的絕大部分空間。moov裏的chunk的位置偏移offset就是相對於mdat的。

上面咱們一個字節一個字節對照着解析比較累,能夠用一些現成的工具,如這個在線的MP4Box.js或者是這個MP4Parser,以下圖所示,moov裏面總共有兩個軌道的盒子:

展開視頻軌道的子盒子,找到stsz這個盒子,能夠看到總共有24幀,每一幀的大小也是能夠見到,以下圖所示:

這裏咱們發現最大的一幀有98KB,最小的一幀只有3KB,一幀就表示一張圖像,爲何不一樣幀差異會這麼大呢?

由於有些幀是關鍵幀(I幀,Intra frame),包含了該幀的完整圖象信息,因此比較大,I幀可作爲參考幀。另外一些幀只是記錄和參考幀的差別,叫幀間預測幀(Inter frame),因此比較小,預測幀有前向預測幀P幀和雙向預測幀B幀,P幀是參考前面解碼過的圖像,而B幀參考雙向的。因此只是拿到預測幀是沒有意義的,須要它前面的那個參考幀才能解碼。參考幀(h264)的壓縮比相似jpg,通常可達7比1,而預測幀的壓縮比可達幾十比1。

接着這些幀是怎麼存放的呢,它們分別是放在哪些chunk裏面的呢,每一個chunk的位置又在哪裏?以下圖stco的盒子所示:

能夠看到,總共有3個chunk,每一個chunk的位置offsset也都指明。而每一個chunk有多少個sample的信息是放在stsc這個盒子裏面,以下圖所示:

從[1, 3)即第1個到第2個chunk每一個chunk有10個sample,而從[3, end)即第3個chunk是4 sample。這樣若是我要找第13幀在mdat盒子裏的偏移,那麼能夠知道它是在第2個chunk裏的第3個sample,因此參考上面的數據計算起始位置:

而終止位置是236645 + 6274 = 242919,因此第13幀存放在mdat的[236645, 242919)區間。這裏有一篇文章介紹了怎麼取mp4幀數據的算法,和咱們上面分析的過程相似。

這個幀(13幀)這麼小,它極可能不是一個關鍵幀。具體怎麼判斷它是否是一個關鍵幀,主要經過幀頭部信息裏的nal類型,值爲5的則爲關鍵幀,這個要涉及到具體的解碼過程了。

還有一個問題,怎麼知道這個mp4是h264編碼,而不是h265之類的,這個經過avc1盒子能夠知道,以下圖所示:

avc1就是h.264的別名,這個盒子裏面放了不少解碼須要的參數,如level、SPS、PPS等,如最大參考幀的數目等,參考上圖註解。若是最大參考幀放得比較寬,可使用的參考幀比較多的時候,壓縮比能獲得提高,可是解碼的效率就會下降,而且在seek尋址的時候也不方便,須要日後讀不少幀,或者往前保留不少幀,特別是流式播放的時候可能須要提早下載不少內容。上面SPS分析獲得的最大參考幀數目是3(max_num_ref_frames).

接下來怎麼對圖像幀進行解碼還原成rgb圖像呢?

3. 視頻幀解碼

I幀的解碼不須要參考幀,解碼過程比較相似於JPG,P幀和B幀須要依賴先後幀才能還原完整內容,因此幀的解碼順序一般不是按照播放順序來的。咱們不妨研究一下上面的示例視頻的全部幀的類型,能夠藉助一個在線網站Online Video GOP Analyzer,分析結果以下圖所示:

x軸表示從0到23共24幀,y軸表示每一幀的大小,綠色的是關鍵幀I幀,紅色的是前向預測幀P幀,藍色的表示雙向預測幀B幀,能夠清楚地看到,在體積上I幀 > P幀 > B幀。這24個幀的排列順序:

I B B B P B B B P ... B P I

首尾兩幀都是I幀,恰好造成一個GOP圖像序列(group of pictures),在一個GOP序列裏面,I幀是起始幀,接下來是B幀和P幀(可能會沒有B幀)。

上圖的幀順序是按照每一個幀播放時間戳PTS(presentation timestamp)依次遞增,其中第12幀(中間紅色柱子)推導的播放時間點PTS是0.54s。

可是存儲順序和解碼順序並非按照播放的順序來的,可對比第2步裏的幀的大小圖:

其中,sample_sizes是存儲的順序,柱形圖的順序是按照PST,二者對比能夠看到每一幀的解碼時間戳DTS(decode timestamp)是按照如下順序:

I P B B B P B B B ...

在一個GOP序列裏面,I幀是起始幀,最早解析,而後就是P幀,最後纔是B幀,能夠猜想由於P幀依賴於I幀,因此要先P幀要先於B幀,而B幀可能要依賴於I幀和P幀,因此最後才能解析。那怎麼才能知道具體的依賴關係,也就是每一幀的參考幀列表呢?

首先每一幀的播放順序POC(Picture Order Count)能夠從每一幀的頭部信息計算獲得,藉助一些如JW Reference軟件,可以查到從存儲順序的第1幀到第5幀POC依次爲:

0 8 2 4 6 ...

這裏是按照2遞增的,換算成1的話就是:

0 4 1 2 3 ...

與上面的分析一致。

接着怎麼知道幀間預測幀B幀和P幀的參考幀是誰呢?在回答這個問題以前須要知道參考幀參考的是什麼,在jpg/h264裏面把圖片劃分爲一個個的宏塊(macroblock),一個宏塊是16 * 16px,以宏塊爲單位進行存儲,記錄的顏色信息是以YCbCr格式,Y是指亮度也就是灰度,Cb是指藍色份量,Cr是紅色的份量。以下圖所示:

幀間預測幀的宏塊只是記錄了差值,因此須要找到參考幀列表的類似宏塊。由類似宏塊和差值還原完整內容。

而參考幀列表是在解碼過程當中動態維護的,放到一個DPB(decoded picture buffer)的數據結構裏面,裏面有兩個list,list0放的是前向的,list1放的是後向的,依據最大參考幀數目DPB的空間有限,滿了以後會有必定的策略清空或者重置。

在參考幀裏面找到匹配的宏塊就叫運動估計,藉助匹配塊恢復完整宏塊就是運動補償。以下圖所示:

上圖第3幀B幀的一個塊找到的匹配塊有3個,分別是相鄰的I、P、B,這3幀就是它的參考幀。箭頭方向就是表示運動矢量,經過上圖示意,能夠知道物體是從上往下運動的(注意上面的順序是存儲順序IPBB,而播放順序是IBBP)。

運動估計和運動補償的算法有多種,h264有推薦的使用算法。

至此咱們知道了解碼的基本原理,具體怎麼把那一幀的圖像解碼爲rgb圖片,我在《wasm + ffmpeg實現前端截取視頻幀功能》把ffmpeg編譯成wasm,而後在前端頁實現了這個功能。主要利用ffmpeg的解碼,Chrome也是用的ffmpeg作爲它的解碼引擎。關鍵調用函數爲avcodec_decode_video2(這個已被deprecated,下文會繼續說起)。

藉助ffmpeg,咱們可以把全部的幀解析出來變成rgb圖片,這些圖片怎麼造成一個視頻呢?

4. 視頻播放

最直觀的作法就是根據幀率,如上面的示例視頻幀率爲25fps,1s有25幀,每一幀播放間隔時長爲1s / 25 = 0.04s,即每隔40ms就播放一幀,這樣就造成一個視頻了。利用ffmpeg的av_frame_get_best_effort_timestamp函數能夠獲得每一幀的PST播放時間,理論上以開始播放的時間爲起點,在相應的時間差播放對應PST的幀就能夠了。實現上可讓播放視頻的線程sleep相鄰兩個幀的pst時間差,時間到了線程喚醒後再display顯示新的幀。

實際上爲了更好地保證音視頻同步,須要以當前音頻播放的時間作一個修正。例如,若是解碼視頻的線程卡了跟不上了,和音頻的時間audioClock相差太多,超過一個閾值如0.1s那麼這一幀就丟掉了,不要展現了,相反若是是解碼視頻線程快了,那麼delay一下,讓播放視頻的線程休眠更長的時間,保持當前幀不動。更科學的方法是讓音頻和視頻同時以當前播放的時間作修正,即記錄一下開始播放的系統時間,用當前系統時間減掉開始時間就獲得播放時間。Chrome就是這麼作的,當咱們看Chrome源碼的時候會發現這個過程比上面描述得要複雜。

5. Chrome視頻播放過程

咱們從多路解複用開始提及,Chrome的多路解複用是在src/media/filters/ffmpeg_demuxer.cc裏面進行的,先借助buffer數據初始化一個format_context,記錄視頻格式信息,而後調avformat_find_stream_info獲得全部的streams,一個stream包含一個軌道,循環streams,根據codec_id區分audio、video、text三種軌道,記錄每種軌道的數量,設置播放時長duration,用fist_pts初始化播放開始時間start_time。並實例化一個DemuxerStream對象,這個對象會記錄視頻寬高、是否有旋轉角度等,初始化audio_config和video_config,給解碼的時候使用。這裏面的每一步幾乎都是經過PostTask進行的,即把函數看成一個任務拋給media線程處理,同時傳遞一個處理完成的回調函數。若是其中有一步掛了就不會進行下一步,例如遇到不支持的容器格式,在第一步初始化就會失敗,就不會調回調函數往下走了。

解碼是使用ffmpeg的avcodec_send_packet和avcodec_receive_frame進行的音視頻解碼,上文提到的avcodec_decode_video2已經被棄用,ffmpeg3起引入了新的解碼函數。

解碼和解複用都是在media線程處理的,以下圖所示:

音頻解碼完成會放到audio_buffer_renderer_algorithm的AudioBufferQueue裏面,等待AudioOutputDevice線程讀取。爲何起名叫algorithm,由於它還有一個做用就是實現WSOLA語音時長調整算法,即所謂的變速不變調,由於在JS裏面咱們是能夠設置audio/video的playback調整播放速度。

視頻解碼完成會放到video_buffer_renderer_algorithm.cc的buffer隊列裏面,這個類的做用也是爲了保證流暢的播放體驗,包括上面討論的時鐘同步的關係。

準備渲染的時候會先給video_frame_compositor.cc,這個在media裏的合成器充當media和Chrome Compositor(最終合成)的一箇中介,由它把處理好的frame給最終合成並渲染,以前的文章已經提過Chrome是使用skia作爲渲染庫,主要經過它提供的Cavans類操做繪圖。

Chrome使用的ffmpeg是有所刪減的,支持的格式有限,否則的話光是ffmpeg就要10多個MB了。

以上就是總體的過程,具體的細節如怎麼作音視頻同步等,本篇沒有深刻去研究。

7. 小結

本篇介紹了不少了視頻解碼的概念,包括mp4容器的格式特色,怎麼進行多路解複用取出音視頻數據,什麼是I幀、B幀和P幀。介紹了在解碼過程當中播放時間PST和解碼順序DST每每是不一致的(若是有B幀),B幀和P幀經過運動估計、運動補償進行解碼還原。最後介紹Chrome是怎麼利用ffmpeg進行解碼,分析了Chrome播放視頻的總體過程。

閱讀完本篇內容並不能成爲一個多媒體高手,可是能夠對多媒體的不少概念有一個基本瞭解,當你去參加一些多媒體技術會議的時候就不會聽得霧裏雲裏的。本篇把不少多媒體基礎串了起來,這也是我研究了好久才獲得的一些認知,個人感覺是多媒體領域的水很深,須要有耐心扎進去纔能有所成,但多媒體又是提升生活質量的一個很重要的媒介。

掘金微信小程序開發者大會

相關文章
相關標籤/搜索