不再碎片化學習,快速掌握 H5 直播技術

現在,大多數已工作的前端工作者的學習方式,要麼直接到 Stackoverflow 上搜代碼,要麼直接看看相關博文。這樣是快,但是零零碎碎只是一個一個孤立的知識點而已。有可能一下午都忘記了,唯一可能記住的收藏一下那個文章,然後就徹底躺屍了。那有沒有啥更好的辦法能解決呢?

當然有,第一,有時間,第二,有人指導,第三,找對資料。

這其實和看書是一樣的,一本書,最有價值的地方不在它的內容或者作者,而在於它的 目錄,是否真正的打動你。如果只是出現一些模糊而沒有落地技術的目錄的書籍,還是別再上面浪費時間了。

所以,本文主要給大家介紹一下當下 HTML5 直播所涵蓋的技術範圍,如果要深度學習每一個技術,我們後續可以繼續討論。

直播協議

直播是 16 年搭著短視頻熱火起來的。它的業務場景有很多,有遊戲主播,才藝主播,網上教學,群體實驗(前段時間,有人直播讓觀眾來炒股)等等。不過,根據技術需求的劃分,還可以分為低延遲和高延遲的直播,這裡就主要是協議選擇的問題。

現在,常用的直播協議有很多種,比如 RTMP,HLS,HTTP-FLV。不過,最常用的還是 HLS 協議,因為支持度高,技術簡單,但是延遲非常嚴重。這對一些對實時性比較高的場景,比如運動賽事直播來說非常蛋疼。這裡,我們來細分的看一下每個協議。

HLS

HLS 全稱是 HTTP Live Streaming。這是 Apple 提出的直播流協議。(其實,Adobe 公司 FLV 播放器的沒落,蘋果也是幕後黑手之一。)

HLS 由兩部分構成,一個是 .m3u8 文件,一個是 .ts 視頻文件(TS 是視頻文件格式的一種)。整個過程是,瀏覽器會首先去請求 .m3u8 的索引文件,然後解析 m3u8,找出對應的 .ts 文件鏈接,並開始下載。更加詳細的說明可以參考這幅圖:

他的使用方式為:

  1. <video controls autoplay>
  2. <source src="devimages.apple.com/iph" type="application/vnd.apple.mpegurl" />
  3. <p class="warning">Your browser does not support HTML5 video.</p>
  4. </video>

直接可以將 m3u8 寫進 src 中,然後交由瀏覽器自己去解析。當然,我們也可以採取 fetch 來手動解析並獲取相關文件。HLS 詳細版的內容比上面的簡版多了一個 playlist,也可以叫做 master。在 master 中,會根據網路段實現設置好不同的 m3u8 文件,比如,3G/4G/wifi 網速等。比如,一個 master 文件中為:

  1. #EXTM3U
  2. #EXT-X-VERSION:6
  3. #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=2855600,CODECS="avc1.4d001f,mp4a.40.2",RESOLUTION=960x540
  4. live/medium.m3u8
  5. #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=5605600,CODECS="avc1.640028,mp4a.40.2",RESOLUTION=1280x720
  6. live/high.m3u8
  7. #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=1755600,CODECS="avc1.42001f,mp4a.40.2",RESOLUTION=640x360
  8. live/low.m3u8
  9. #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=545600,CODECS="avc1.42001e,mp4a.40.2",RESOLUTION=416x234
  10. live/cellular.m3u8

大家只要關注 BANDWIDTH(帶寬)欄位,其他的看一下欄位內容大致就清楚了。假如這裡選擇 high.m3u8文件,那麼,裡面內容為:

  1. #EXTM3U
  2. #EXT-X-VERSION:6
  3. #EXT-X-TARGETDURATION:10
  4. #EXT-X-MEDIA-SEQUENCE:26
  5. #EXTINF:9.901,
  6. http://media.example.com/wifi/segment26.ts
  7. #EXTINF:9.901,
  8. http://media.example.com/wifi/segment27.ts
  9. #EXTINF:9.501,
  10. http://media.example.com/wifi/segment28.ts

注意,其中以 ts 結尾的鏈接就是我們在直播中真正需要播放的視頻文件。該第二級的 m3u8 文件也可以叫做 media 文件。該文件,其實有三種類型:

  • live playlist: 動態列表。顧名思義,該列表是動態變化的,裡面的 ts 文件會實時更新,並且過期的 ts 索引會被刪除。默認,情況下都是使用動態列表。
  1. #EXTM3U
  2. #EXT-X-VERSION:6
  3. #EXT-X-TARGETDURATION:10
  4. #EXT-X-MEDIA-SEQUENCE:26
  5. #EXTINF:9.901,
  6. http://media.example.com/wifi/segment26.ts
  7. #EXTINF:9.901,
  8. http://media.example.com/wifi/segment27.ts
  9. #EXTINF:9.501,
  10. http://media.example.com/wifi/segment28.ts
  • event playlist: 靜態列表。它和動態列表主要區別就是,原來的 ts 文件索引不會被刪除,該列表是不斷更新,而且文件大小會逐漸增大。它會在文件中,直接添加 #EXT-X-PLAYLIST-TYPE:EVENT 作為標識。
  1. #EXTM3U
  2. #EXT-X-VERSION:6
  3. #EXT-X-TARGETDURATION:10
  4. #EXT-X-MEDIA-SEQUENCE:0
  5. #EXT-X-PLAYLIST-TYPE:EVENT
  6. #EXTINF:9.9001,
  7. http://media.example.com/wifi/segment0.ts
  8. #EXTINF:9.9001,
  9. http://media.example.com/wifi/segment1.ts
  10. #EXTINF:9.9001,
  11. http://media.example.com/wifi/segment2.ts
  • VOD playlist: 全量列表。它就是將所有的 ts 文件都列在 list 當中。如果,使用該列表,就和播放一整個視頻沒有啥區別了。它是使用 #EXT-X-ENDLIST 表示文件結尾。
  1. #EXTM3U
  2. #EXT-X-VERSION:6
  3. #EXT-X-TARGETDURATION:10
  4. #EXT-X-MEDIA-SEQUENCE:0
  5. #EXT-X-PLAYLIST-TYPE:VOD
  6. #EXTINF:9.9001,
  7. http://media.example.com/wifi/segment0.ts
  8. #EXTINF:9.9001,
  9. http://media.example.com/wifi/segment1.ts
  10. #EXTINF:9.9001,
  11. http://media.example.com/wifi/segment2.ts
  12. #EXT-X-ENDLIST

裡面相關欄位解釋可以參考: Apple HLS

HLS 缺陷

HLS 啥都好,就是延遲性太大了,估計蘋果一開始設計的時候,並不在乎它的延時性。HLS 中的延時包括:

  • TCP 握手
  • m3u8 文件下載
  • m3u8 文件下所有 ts 文件下載

這裡,我們先假設每個 ts 文件播放時長為 5s,每個 m3u8 最多可攜帶的 ts 文件數為 3~8。那麼最大的延遲則為 40s。注意,只有當一個 m3u8 文件下所有的 ts 文件下載完後,才能開始播放。這裡還不包括 TCP 握手,DNS 解析,m3u8 文件下載。所以,HLS 總的延時是非常令人絕望的。那解決辦法有嗎? 有,很簡單,要麼減少每個 ts 文件播放時長,要麼減少 m3u8 的中包含 ts 的數量。如果超過平衡點,那麼每次請求新的 m3u8 文件時,都會加上一定的延時,所以,這裡需要根據業務指定合適的策略。當然,現在由於 mediaSource 的普及,自定義一個播放器也沒有多大的難度,這樣就可以保證直播延遲性的同時,完成直播的順利進行。

RTMP

RTMP 全稱為: Real-TimeMessagingProtocol。它是基於 FLV 格式進行開發的,所以,第一反應就是,卧槽,又不能用了!!!

是的,在現在設備中,由於 FLV 的不支持,基本上 RTMP 協議在 Web 中,根本用不到。不過,由於 MSE(MediaSource Extensions)的出現,在 Web 上直接接入 RTMP 也不是不可能的。基本思路是根據 WebSocket 直接建立長連接進行數據的交流和監聽。這裡,我們就先不細說了。我們主要目的是講概念,講框架。RTMP 協議根據不同的套層,也可以分為:

  • 純 RTMP: 直接通過 TCP 連接,埠為 1935
  • RTMPS: RTMP + TLS/SSL,用於安全性的交流。
  • RTMPE: RTMP + encryption。在 RTMP 原始協議上使用,Adobe 自身的加密方法
  • RTMPT: RTMP + HTTP。使用 HTTP 的方式來包裹 RTMP 流,這樣能直接通過防火牆。不過,延遲性比較大。
  • RTMFP: RMPT + UDP。該協議常常用於 P2P 的場景中,針對延時有變態的要求。

RTMP 內部是藉由 TCP 長連接協議傳輸相關數據,所以,它的延時性非常低。並且,該協議靈活性非常好(所以,也很複雜),它可以根據 message stream ID 傳輸數據,也可以根據 chunk stream ID 傳遞數據。兩者都可以起到流的劃分作用。流的內容也主要分為:視頻,音頻,相關協議包等。

詳細傳輸過程如圖:

如果後期要使用到 RTMP 協議,可以直接參考

HTTP-FLV

該協議和 RTMP 比起來其實差別不大,只是落地部分有些不同:

RTMP 是直接將流的傳輸架在 RTMP 協議之上,而 HTTP-FLV 是在 RTMP 和客戶端之間套了一層轉碼的過程,即:

由於,每個 FLV 文件是通過 HTTP 的方式獲取的,所以,它通過抓包得出的協議頭需要使用 chunked 編碼。

  1. Content-Type:video/x-flv
  2. Expires:Fri, 10 Feb 2017 05:24:03 GMT
  3. Pragma:no-cache
  4. Transfer-Encoding:chunked

它用起來比較方便,不過後端實現的難度和直接使用 RTMP 來說還是比較大的。

上面簡單介紹了一下三種協議,具體選擇哪種協議,還是需要和具體的業務進行強相關,否則的話吃虧的還是自己(自己挖坑)。。。

這裡,簡單的做個對比

協議對比

協議優勢缺陷延遲性HLS支持性廣延時巨高10s 以上RTMP延時性好,靈活量大的話,負載較高1s 以上HTTP-FLV延時性好,遊戲直播常用只能在手機 APP 播放2s 以上

前端音視頻流

由於各大瀏覽器的對 FLV 的圍追堵截,導致 FLV 在瀏覽器的生存狀況堪憂,但是,FLV 憑藉其格式簡單,處理效率高的特點,使各大視頻後台的開發者都捨不得啟用,如果一旦更改的話,就需要對現有視頻進行轉碼,比如變為 MP4,這樣不僅在播放,而且在流處理來說都有點重的讓人無法接受。而 MSE 的出現,徹底解決了這個尷尬點,能夠讓前端能夠自定義來實現一個 Web 播放器,確實完美。(不過,蘋果老大爺覺得沒這必要,所以,在 IOS 上無法實現。)

MSE

MSE 全稱就是 MediaSourceExtensions。它是一套處理視頻流技術的簡稱,裡面包括了一系列 API: MediaSource, SourceBuffer 等。在沒有 MSE 出現之前,前端對 video 的操作,僅僅局限在對視頻文件的操作,而並不能對視頻流做任何相關的操作。現在 MSE 提供了一系列的介面,使開發者可以直接提供 media stream。

我們來看一下 MSE 是如何完成基本流的處理的。

  1. var vidElement = document.querySelector(video);
  2. if (window.MediaSource) {
  3. var mediaSource = new MediaSource();
  4. vidElement.src = URL.createObjectURL(mediaSource);
  5. mediaSource.addEventListener(sourceopen, sourceOpen);
  6. } else {
  7. console.log("The Media Source Extensions API is not supported.")
  8. }
  9. function sourceOpen(e) {
  10. URL.revokeObjectURL(vidElement.src);
  11. var mime = video/webm; codecs="opus, vp9";
  12. var mediaSource = e.target;
  13. var sourceBuffer = mediaSource.addSourceBuffer(mime);
  14. var videoUrl = droid.webm;
  15. fetch(videoUrl)
  16. .then(function(response) {
  17. return response.arrayBuffer();
  18. })
  19. .then(function(arrayBuffer) {
  20. sourceBuffer.addEventListener(updateend, function(e) {
  21. if (!sourceBuffer.updating && mediaSource.readyState === open) {
  22. mediaSource.endOfStream();
  23. }
  24. });
  25. sourceBuffer.appendBuffer(arrayBuffer);
  26. });
  27. }

上面的代碼完成了相關的獲取流和處理流的兩個部分。其中,主要利用的是 MS 和 Source Buffer 來完成的。接下來,我們來具體涉及一下詳細內容:

MediaSource

MS(MediaSource) 只是一系列視頻流的管理工具,它可以將音視頻流完整的暴露給 Web 開發者來進行相關的操作和處理。所以,它本身不會造成過度的複雜性。

MS 整個只掛載了 4 個屬性,3 個方法和 1 個靜態測試方法。有:

4 個屬性:

  • sourceBuffers: 獲得當前創建出來的 SourceBuffer
  • activeSourceBuffers: 獲得當前正處於激活狀態的 SourceBuffer
  • readyState: 返回當前 MS 的狀態,比如: closed, open, ended.
  • duration: 設置當前 MS 的播放時長。

3 個方法:

  • addSourceBuffer(): 根據給定的 MIME 創建指定類型的 SourceBuffer
  • removeSourceBuffer(): 將 MS 上指定的 SourceBuffer 移除。
  • endOfStream(): 直接終止該流

1 個靜態測試方法:

  • isTypeSupported(): 主要用來判斷指定的音頻的 MIME 是否支持。

最基本的就是使用 addSourceBuffer 該方法來獲得指定的 SourceBuffer。

  1. var sourceBuffer = mediaSource.addSourceBuffer(video/mp4; codecs="avc1.42E01E, mp4a.40.2");

Source Buffer

一旦利用 MS 創建好 SourceBuffer 之後,後續的工作就是將額外獲得的流放進 Buffer 裡面進行播放即可。所以,SourceBuffer 提供兩個最基本的操作 appendBuffer, remove。之後,我們就可以通過 appendBuffer直接將 ArrayBuffer 放進去即可。

其中,SourceBuffer 還提供了一個應急的方法 abort() 如果該流發生問題的話可以直接將指定的流給廢棄掉。

所以,整個流程圖為:

音視頻的 ArrayBuffer 通過 MediaSource 和 SourceBuffer 的處理直接將 <audio> && <video> 接入。然後,就可以實現正常播放的效果。

當然,上面介紹的僅僅只是一些概念,如果要實際進行編碼的話,還得繼續深入下去學習。有興趣的同學,可以繼續深入了解,我的另外一篇博客:全面進階 H5 直播。

當然,如果後期有機會,可以繼續來實現以下如何進行實際的編碼。本文,主要是給大家介紹直播所需的必要技術和知識點,只有完備之後,我們才能沒有障礙的完成實際編碼的介紹。

流的處理

上面我們已經講解了在直播中,我們怎樣通過 MSE 接觸到實際播放的流,那麼,接下來,我們就要開始腳踏實地的了解具體的流的操作處理。因為,視頻流格式解協議中,最常涉及的就是拼包,修改欄位,切包等操作。

在正式介紹之前,我們需要先了解一下關於流的一些具體概念:

二進位

二進位沒啥說的就是 比特流。但是,在 Web 中,有幾個簡寫的進位方式:二進位,八進位,十六進位。

  • 二進位(binary):使用 0b 字面上表示二進位。每一位代表 1bit(2^1)
  • 八進位(octet): 使用 0o 字面上表示八進位。每一位代表 3bit(2^3)
  • 十六進位(hexadecimal): 使用 0x 字面上表示十六進位。每一位代表 4bit(2^4)

上面說的每一位代表著,實際簡寫的位數,比如 0xff31; 這個就代表 2B 的長度。

位運算

位運算在處理流操作中,是非常重要的,不過由於前端 Buffer 提供的操作集合不多,所以,有些輪子我們還得需要自己構造一下。

這裡,我就不細緻介紹,在 Web 中常用的位運算符有:

  • &
  • |
  • ~
  • ^
  • <<
  • >>
  • >>>

詳細介紹可以參考 Web 位運算。

整個優先順序為:

  1. ~ >> << >>> & ^ |

位元組序

位元組序說白了就是 bit 放置的順序,因為歷史遺漏原因,位元組的放置有兩種順序:

  • 大位元組序(BigEndian): 將數據從大到小放置,認為第一個位元組是最高位(正常思維)。
  • 小位元組序(LittleEndian):將數據從小到達防止,認為第一個位元組是最低位。

這個概念在我們後面寫入過程中,經常用到。當然,我們如何了解到某台電腦使用的是大位元組還是小位元組呢?(其實大部分都是小位元組)。可以使用 IIFE 進行簡單的判斷:

  1. const LE = (function () {
  2. let buf = new ArrayBuffer(2);
  3. (new DataView(buf)).setInt16(0, 256, true); // little-endian write
  4. return (new Int16Array(buf))[0] === 256; // platform-spec read, if equal then LE
  5. })();

而在前端,我們歸根結底的就是操作 ArrayBuffer。它也是我們直接和 Buffer 交流的通道。

ArrayBuffer

AB(ArrayBuffer) 不是像 NodeJS 的 Buffer 對象一樣是一個純粹的集合流處理的工具。它只是一個流的容器,這也是底層 V8 實現的內容。基本用法就是給實例化一個固定的內存區:

  1. new ArrayBuffer(length)

創建指定的 length Byte 內存大小。此時,它裡面只是空的內存,這時候你需要借用其他兩個對象 TypedArray 和 DataView 來幫助你完成寫入和修改的操作。不過,AB 提供了一個非常重要的方法: slice()

slice() 和 Array 對象上的 slice 方法一樣也是將數組中的一部分新創建一個副本返回。這個方法為啥有用呢?

因為,通過 TypedArray 和 DataView 創建的對象底層的 AB 都是不能改變的,所以,如果你想對一個 Buffer 進行不同的操作,比如,對 AB 的 4-8B 全部置 0,並且後面又置為 1。如果你想保留兩者的話,就需要手動創建一個副本才行,這就需要用到 slice 方法了。

AB 具體的屬性和方法我這裡就不多說了,有興趣的同學可以參考 MDN ArrayBuffer

接下來,我們就來看一下和 AB 實際對接最緊密的兩個對象 TypedArray 和 DataView。

TypedArray

TA(TypedArray) 是一套 ArrayBuffer 處理的集合。怎麼說呢?它裡面還可以細分為

  1. Int8Array();
  2. Uint8Array();
  3. Uint8ClampedArray();
  4. Int16Array();
  5. Uint16Array();
  6. Int32Array();
  7. Uint32Array();
  8. Float32Array();
  9. Float64Array();

為什麼會有這麼多呢?

因為 TA 是將 Buffer 根據指定長度分隔為指定大小的數組。比如:

  1. var buf = new Uint8Array(arrayBuffer);
  2. buf[0]
  3. buf[1]
  4. ...

像這樣具體通過 index 來獲取指定的 Buffer 中的比特值。比如像上面 Uint8Array 每一位就是 1B。

出來分隔的長度不同,剩下的內容,基本上就可以用 TA 來進行整體概括。實際上,大家也可以把它理解為 Array 即可。為啥呢?你可以看一下它有哪些方法後,就徹底明白了:

  1. reverse()
  2. set()
  3. slice()
  4. some()
  5. sort()
  6. subarray()
  7. ...

不過,由於兼容性的原因,對於某些方法來說,我們需要加上相關的 polyfill 才行。不過,這也不影響我們的研究性學習,並且,因為 MSE 是針對現代手機瀏覽器開發的,所以,我們在做 Web 播放器的時候,也並不需要過度關注瀏覽器的兼容。

TypedArray 最常用的操作方式,是直接根據 index 進行相關的寫入操作:

  1. buf[0] = fmt << 6 | 1;
  2. buf[1] = chunkID % 256 - 64;
  3. buf[2] = Math.floor(chunkID / 256);

需要注意在 TypedArray 中的位元組序,是根據平台默認的位元組序來讀取 Buffer 的,比如 UintArray32()。不過,大部分平台默認都是 little-endian 來進行讀取。

DataView

DV(DataView) 和 TypedArray 很類似,也是用來修改底層的 Buffer 的。說白了,它倆就是 NodeJS Buffer 的兩部分,可能由於某些原因,將兩者給分開創建。DataView 提供的 API 很簡單,就是一些 get/set 之類的方法。基本用法為:

  1. new DataView(Arraybuffer [, byteOffset [, byteLength]])

注意,前面那個參數只能是 ArrayBuffer ,你不能把 TypedArray 也給我算進去,不然的話...你可以試試。

同樣需要提醒的是 DataView 的修改是和對象一樣的,是進行引用類型的修改,即,如果對一個 Buffer 創建多個 DataView 那麼,多次修改只會在一個 Buffer 顯現出來。

DV 最大的用處就可可以很方便的寫入不同位元組序的值,這相比於使用 TypedArray 來做 swap()(交換) 是很方便的事。當然,位元組序相關也只能是大於 8 bit 以上的操作才有效。

這裡以 setUInt32 為例子,其基本格式為:

  1. setInt32(byteOffset, value [, littleEndian])

其中,littleEndian 是 boolean 值,用來表示寫入位元組序的方式,默認是使用大位元組序。參考:

It is big-endian by default and can be set to little-endian in the getter/setter methods.

所以,如果你想使用小位元組序的話,則需要手動傳入 true 才行!

比如:

  1. let view = new DataView(buffer);
  2. view.setUint32(0, arr[0] || 1, BE);
  3. // 使用 TypedArray 手動構造 swap
  4. buf = new Uint8Array(11);
  5. buf[3] = byteLength >>> 16 & 0xFF;
  6. buf[4] = byteLength >>> 8 & 0xFF;
  7. buf[5] = byteLength & 0xFF;

當然,如果你覺得不放心,可以直接使用,一個 IIFE 進行相關判斷:

  1. const LE = (function () {
  2. let buf = new ArrayBuffer(2);
  3. (new DataView(buf)).setInt16(0, 256, true); // little-endian write
  4. return (new Int16Array(buf))[0] === 256; // platform-spec read, if equal then LE
  5. })();

上面是前端 Buffer 的部分,為了讓大家更好的了解到 JS 開發工作者從前端到後端操作 Buffer 的區別,這裡一併提一下在 NodeJS 中如何處理 Buffer。

Node Buffer

Node Buffer 實際上才是前端最好用的 Buffer 操作,因為它是整合的 ArrayBuffer , TypedArray ,Dataview 一起的一個集合,該對象上掛載了所有處理的方式。詳情可以參考一下:Node Buffer。

他可以直接通過 alloc 和 from 方法來直接創建指定的大小的 Buffer。以前那種通過 newBuffer 的方法官方已經不推薦使用了,具體原因可以 stackoverflow 搜一搜,這裡我就不多說了。這裡想特別提醒的是,NodeJS 已經可以和前端的 ArrayBuffer 直接轉換了。通過 from 方法,可以直接將 ArrayBuffer 轉換為 NodeJS 的 Buffer。

格式為:

  1. Buffer.from(arrayBuffer[, byteOffset[, length]])

參考 NodeJS 提供的 demo:

  1. const arr = new Uint16Array(2);
  2. arr[0] = 5000;
  3. arr[1] = 4000;
  4. // 共享 arr 的緩存
  5. const buf = Buffer.from(arr.buffer);
  6. // 列印結果: <Buffer 88 13 a0 0f>
  7. console.log(buf);
  8. // 直接改變原始的 Buffer 值
  9. arr[1] = 6000;
  10. // 列印: <Buffer 88 13 70 17>
  11. console.log(buf);

在 Node Buffer 對象上,還掛載了比如:

  1. buf.readInt16BE(offset[, noAssert])
  2. buf.readInt16LE(offset[, noAssert])
  3. buf.readInt32BE(offset[, noAssert])
  4. buf.readInt32LE(offset[, noAssert])

有點不同的是,它是直接根據名字的不同而決定使用哪種位元組序。

  • BE 代表 BigEndian
  • LE 代表 LittleEndian

之後,我們就可以使用指定的方法進行寫入和讀取操作。

  1. const buf = Buffer.from([0, 5]);
  2. // Prints: 5
  3. console.log(buf.readInt16BE());
  4. // Prints: 1280
  5. console.log(buf.readInt16LE());

在實際使用中,我們一般對照著 Node 官方文檔使用即可,裡面文檔很詳盡。

音視頻基本概念

為了大家能夠在學習中減少一定的不適感,這裡先給大家介紹一下音視頻的基本概念,以防止以後別人在吹逼,你可以在旁邊微微一笑。首先,基本的就是視頻格式和視頻壓縮格式。

視頻格式應該不用多說,就是我們通常所說的 .mp4, .flv, .ogv, .webm 等。簡單來說,它其實就是一個盒子,用來將實際的視頻流以一定的順序放入,確保播放的有序和完整性。

視頻壓縮格式和視頻格式具體的區別就是,它是將原始的視頻碼流變為可用的數字編碼。因為,原始的視頻流非常大,打個比方就是,你直接使用手機錄音,你會發現你幾分鐘的音頻會比市面上出現的 MP3 音頻大小大很多,這就是壓縮格式起的主要作用。具體流程圖如下:

首先,由原始數碼設備提供相關的數字信號流,然後經由視頻壓縮演算法,大幅度的減少流的大小,然後交給視頻盒子,打上相應的 dts, pts 欄位,最終生成可用的視頻文件。常用的視頻格式和壓縮格式如下:

視頻格式主要是參考 ISO 提供的格式文件進行學習,然後參照進行編解碼即可。

這裡,主要想介紹一下壓縮演算法,因為這個在你實際解碼用理解相關概念很重要。

首先來看一下,什麼叫做視頻編碼。

視頻編碼

視頻實際上就是一幀一幀的圖片,拼接起來進行播放而已。而圖片本身也可以進行相關的壓縮,比如去除重複像素,合併像素塊等等。不過,還有另外一種壓縮方法就是,運動估計和運動補償壓縮,因為相鄰圖片一定會有一大塊是相似的,所以,為了解決這個問題,可以在不同圖片之間進行去重。

所以,總的來說,常用的編碼方式分為三種:

  • 變換編碼:消除圖像的幀內冗餘
  • 運動估計和運動補償:消除幀間冗餘
  • 熵編碼:提高壓縮效率

變換編碼

這裡就涉及到圖像學裡面的兩個概念:空域和頻域。空域就是我們物理的圖片,頻域就是將物理圖片根據其顏色值等映射為數字大小。而變換編碼的目的是利用頻域實現去相關和能量集中。常用的正交變換有離散傅里葉變換,離散餘弦變換等等。

熵編碼

熵編碼主要是針對碼節長度優化實現的。原理是針對信源中出現概率大的符號賦予短碼,對於概率小的符號賦予長碼,然後總的來說實現平均碼長的最小值。編碼方式(可變字長編碼)有:霍夫曼編碼、算術編碼、遊程編碼等。

運動估計和運動補償

上面那兩種辦法主要是為了解決圖像內的關聯性。另外,視頻壓縮還存在時間上的關聯性。例如,針對一些視頻變化,背景圖不變而只是圖片中部分物體的移動,針對這種方式,可以只對相鄰視頻幀中變化的部分進行編碼。

接下來,再來進行說明一下,運動估計和運動補償壓縮相關的,I,B,P 幀。

I,B,P 幀

I,B,P 實際上是從運動補償中引出來的,這裡為了後面的方便先介紹一下。

  • I 幀(I-frame): 學名叫做: Intra-coded picture。也可以叫做獨立幀。該幀是編碼器隨機挑選的參考圖像,換句話說,一個 I 幀本身就是一個靜態圖像。它是作為 B,P 幀的參考點。對於它的壓縮,只能使用 熵 和 變化編碼 這兩種方式進行幀內壓縮。所以,它的運動學補償基本沒有。
  • P 幀(P?frame): 又叫做 Predictedpicture--前向預測幀。即,他會根據前面一張圖像,來進行圖片間的動態壓縮,它的壓縮率和 I 幀比起來要高一些。
  • B 幀(B?frame): 又叫做 Bi-predictive picture-- 雙向預測。它比 P 幀來說,還多了後一張圖像的預測,所以它的壓縮率更高。

可以參考一下雷博士的圖:

不過,這樣理解 OK,如果一旦涉及實際編碼的話,那麼就不是那麼一回事了。設想一下,視頻中的 IBP 三幀,很有可能會遇到 I B B P 的情形。這樣其實也還好,不過在對數據解碼的時候,就會遇到一個問題,B 幀是相對於前後兩幀的,但是只有前一幀是固定幀,後面又相對於前面,即,B 幀只能相對於 I/P。但是,這時候 P 幀又還沒有被解析。所以,為了解決這個問題,在解碼的時候,就需要將他們換一個位置,即 I P B B。這樣就可以保證解碼的正確性。

那怎麼進行保證呢?這就需要 DTS 和 PTS 來完成。這兩個是我們在進行視頻幀編解碼中最終要的兩個屬性(當然還有一個 CTS)。

解釋一下:

  • pts(presentation time stamps):顯示時間戳,顯示器從接受到解碼到顯示的時間。
  • dts(decoder timestamps): 解碼時間戳。也表示該 sample 在整個流中的順序

所以視頻幀的順序簡單的來表示一下就是:

  1. PTS: 1 4 2 3
  2. DTS: 1 2 3 4
  3. Stream: I P B B

可以看到,我們使用 DTS 來解碼,PTS 來進行播放。OK,關於 Web 直播的大致基本點差不多就介紹完了。後面如果還有機會,我們可以來進行一下 音視頻解碼的實戰操練。

更多內容可以關注我的公眾號--前端小吉米:

weixin.qq.com/r/Ij9hebL (二維碼自動識別)


推薦閱讀:

es6用於web前端(非node後端)是不是還太早了?
12個前端初學者必學技能
是什麼原因使你最終確定以前端開發工程師為職業的?

TAG:直播 | 前端开发 | 前端工程师 |