不再碎片化學習,快速掌握 H5 直播技術
現在,大多數已工作的前端工作者的學習方式,要麼直接到 Stackoverflow 上搜代碼,要麼直接看看相關博文。這樣是快,但是零零碎碎只是一個一個孤立的知識點而已。有可能一下午都忘記了,唯一可能記住的收藏一下那個文章,然後就徹底躺屍了。那有沒有啥更好的辦法能解決呢?
當然有,第一,有時間,第二,有人指導,第三,找對資料。
這其實和看書是一樣的,一本書,最有價值的地方不在它的內容或者作者,而在於它的 目錄,是否真正的打動你。如果只是出現一些模糊而沒有落地技術的目錄的書籍,還是別再上面浪費時間了。
所以,本文主要給大家介紹一下當下 HTML5 直播所涵蓋的技術範圍,如果要深度學習每一個技術,我們後續可以繼續討論。
直播協議
直播是 16 年搭著短視頻熱火起來的。它的業務場景有很多,有遊戲主播,才藝主播,網上教學,群體實驗(前段時間,有人直播讓觀眾來炒股)等等。不過,根據技術需求的劃分,還可以分為低延遲和高延遲的直播,這裡就主要是協議選擇的問題。
現在,常用的直播協議有很多種,比如 RTMP,HLS,HTTP-FLV。不過,最常用的還是 HLS 協議,因為支持度高,技術簡單,但是延遲非常嚴重。這對一些對實時性比較高的場景,比如運動賽事直播來說非常蛋疼。這裡,我們來細分的看一下每個協議。
HLS
HLS 全稱是 HTTP Live Streaming。這是 Apple 提出的直播流協議。(其實,Adobe 公司 FLV 播放器的沒落,蘋果也是幕後黑手之一。)
HLS 由兩部分構成,一個是 .m3u8 文件,一個是 .ts 視頻文件(TS 是視頻文件格式的一種)。整個過程是,瀏覽器會首先去請求 .m3u8 的索引文件,然後解析 m3u8,找出對應的 .ts 文件鏈接,並開始下載。更加詳細的說明可以參考這幅圖:
他的使用方式為:
- <video controls autoplay>
- <source src="http://devimages.apple.com/iphone/samples/bipbop/masterplaylist.m3u8" type="application/vnd.apple.mpegurl" />
- <p class="warning">Your browser does not support HTML5 video.</p>
- </video>
直接可以將 m3u8 寫進 src 中,然後交由瀏覽器自己去解析。當然,我們也可以採取 fetch 來手動解析並獲取相關文件。HLS 詳細版的內容比上面的簡版多了一個 playlist,也可以叫做 master。在 master 中,會根據網路段實現設置好不同的 m3u8 文件,比如,3G/4G/wifi 網速等。比如,一個 master 文件中為:
- #EXTM3U
- #EXT-X-VERSION:6
- #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=2855600,CODECS="avc1.4d001f,mp4a.40.2",RESOLUTION=960x540
- live/medium.m3u8
- #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=5605600,CODECS="avc1.640028,mp4a.40.2",RESOLUTION=1280x720
- live/high.m3u8
- #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=1755600,CODECS="avc1.42001f,mp4a.40.2",RESOLUTION=640x360
- live/low.m3u8
- #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=545600,CODECS="avc1.42001e,mp4a.40.2",RESOLUTION=416x234
- live/cellular.m3u8
大家只要關注 BANDWIDTH(帶寬)欄位,其他的看一下欄位內容大致就清楚了。假如這裡選擇 high.m3u8文件,那麼,裡面內容為:
- #EXTM3U
- #EXT-X-VERSION:6
- #EXT-X-TARGETDURATION:10
- #EXT-X-MEDIA-SEQUENCE:26
- #EXTINF:9.901,
- http://media.example.com/wifi/segment26.ts
- #EXTINF:9.901,
- http://media.example.com/wifi/segment27.ts
- #EXTINF:9.501,
- http://media.example.com/wifi/segment28.ts
注意,其中以 ts 結尾的鏈接就是我們在直播中真正需要播放的視頻文件。該第二級的 m3u8 文件也可以叫做 media 文件。該文件,其實有三種類型:
- live playlist: 動態列表。顧名思義,該列表是動態變化的,裡面的 ts 文件會實時更新,並且過期的 ts 索引會被刪除。默認,情況下都是使用動態列表。
- #EXTM3U
- #EXT-X-VERSION:6
- #EXT-X-TARGETDURATION:10
- #EXT-X-MEDIA-SEQUENCE:26
- #EXTINF:9.901,
- http://media.example.com/wifi/segment26.ts
- #EXTINF:9.901,
- http://media.example.com/wifi/segment27.ts
- #EXTINF:9.501,
- http://media.example.com/wifi/segment28.ts
- event playlist: 靜態列表。它和動態列表主要區別就是,原來的 ts 文件索引不會被刪除,該列表是不斷更新,而且文件大小會逐漸增大。它會在文件中,直接添加 #EXT-X-PLAYLIST-TYPE:EVENT 作為標識。
- #EXTM3U
- #EXT-X-VERSION:6
- #EXT-X-TARGETDURATION:10
- #EXT-X-MEDIA-SEQUENCE:0
- #EXT-X-PLAYLIST-TYPE:EVENT
- #EXTINF:9.9001,
- http://media.example.com/wifi/segment0.ts
- #EXTINF:9.9001,
- http://media.example.com/wifi/segment1.ts
- #EXTINF:9.9001,
- http://media.example.com/wifi/segment2.ts
- VOD playlist: 全量列表。它就是將所有的 ts 文件都列在 list 當中。如果,使用該列表,就和播放一整個視頻沒有啥區別了。它是使用 #EXT-X-ENDLIST 表示文件結尾。
- #EXTM3U
- #EXT-X-VERSION:6
- #EXT-X-TARGETDURATION:10
- #EXT-X-MEDIA-SEQUENCE:0
- #EXT-X-PLAYLIST-TYPE:VOD
- #EXTINF:9.9001,
- http://media.example.com/wifi/segment0.ts
- #EXTINF:9.9001,
- http://media.example.com/wifi/segment1.ts
- #EXTINF:9.9001,
- http://media.example.com/wifi/segment2.ts
- #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 編碼。
- Content-Type:video/x-flv
- Expires:Fri, 10 Feb 2017 05:24:03 GMT
- Pragma:no-cache
- 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 是如何完成基本流的處理的。
- var vidElement = document.querySelector(video);
- if (window.MediaSource) {
- var mediaSource = new MediaSource();
- vidElement.src = URL.createObjectURL(mediaSource);
- mediaSource.addEventListener(sourceopen, sourceOpen);
- } else {
- console.log("The Media Source Extensions API is not supported.")
- }
- function sourceOpen(e) {
- URL.revokeObjectURL(vidElement.src);
- var mime = video/webm; codecs="opus, vp9";
- var mediaSource = e.target;
- var sourceBuffer = mediaSource.addSourceBuffer(mime);
- var videoUrl = droid.webm;
- fetch(videoUrl)
- .then(function(response) {
- return response.arrayBuffer();
- })
- .then(function(arrayBuffer) {
- sourceBuffer.addEventListener(updateend, function(e) {
- if (!sourceBuffer.updating && mediaSource.readyState === open) {
- mediaSource.endOfStream();
- }
- });
- sourceBuffer.appendBuffer(arrayBuffer);
- });
- }
上面的代碼完成了相關的獲取流和處理流的兩個部分。其中,主要利用的是 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。
- 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 位運算。
整個優先順序為:
- ~ >> << >>> & ^ |
位元組序
位元組序說白了就是 bit 放置的順序,因為歷史遺漏原因,位元組的放置有兩種順序:
- 大位元組序(BigEndian): 將數據從大到小放置,認為第一個位元組是最高位(正常思維)。
- 小位元組序(LittleEndian):將數據從小到達防止,認為第一個位元組是最低位。
這個概念在我們後面寫入過程中,經常用到。當然,我們如何了解到某台電腦使用的是大位元組還是小位元組呢?(其實大部分都是小位元組)。可以使用 IIFE 進行簡單的判斷:
- const LE = (function () {
- let buf = new ArrayBuffer(2);
- (new DataView(buf)).setInt16(0, 256, true); // little-endian write
- return (new Int16Array(buf))[0] === 256; // platform-spec read, if equal then LE
- })();
而在前端,我們歸根結底的就是操作 ArrayBuffer。它也是我們直接和 Buffer 交流的通道。
ArrayBuffer
AB(ArrayBuffer) 不是像 NodeJS 的 Buffer 對象一樣是一個純粹的集合流處理的工具。它只是一個流的容器,這也是底層 V8 實現的內容。基本用法就是給實例化一個固定的內存區:
- 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 處理的集合。怎麼說呢?它裡面還可以細分為
- Int8Array();
- Uint8Array();
- Uint8ClampedArray();
- Int16Array();
- Uint16Array();
- Int32Array();
- Uint32Array();
- Float32Array();
- Float64Array();
為什麼會有這麼多呢?
因為 TA 是將 Buffer 根據指定長度分隔為指定大小的數組。比如:
- var buf = new Uint8Array(arrayBuffer);
- buf[0]
- buf[1]
- ...
像這樣具體通過 index 來獲取指定的 Buffer 中的比特值。比如像上面 Uint8Array 每一位就是 1B。
出來分隔的長度不同,剩下的內容,基本上就可以用 TA 來進行整體概括。實際上,大家也可以把它理解為 Array 即可。為啥呢?你可以看一下它有哪些方法後,就徹底明白了:
- reverse()
- set()
- slice()
- some()
- sort()
- subarray()
- ...
不過,由於兼容性的原因,對於某些方法來說,我們需要加上相關的 polyfill 才行。不過,這也不影響我們的研究性學習,並且,因為 MSE 是針對現代手機瀏覽器開發的,所以,我們在做 Web 播放器的時候,也並不需要過度關注瀏覽器的兼容。
TypedArray 最常用的操作方式,是直接根據 index 進行相關的寫入操作:
- buf[0] = fmt << 6 | 1;
- buf[1] = chunkID % 256 - 64;
- buf[2] = Math.floor(chunkID / 256);
需要注意在 TypedArray 中的位元組序,是根據平台默認的位元組序來讀取 Buffer 的,比如 UintArray32()。不過,大部分平台默認都是 little-endian 來進行讀取。
DataView
DV(DataView) 和 TypedArray 很類似,也是用來修改底層的 Buffer 的。說白了,它倆就是 NodeJS Buffer 的兩部分,可能由於某些原因,將兩者給分開創建。DataView 提供的 API 很簡單,就是一些 get/set 之類的方法。基本用法為:
- new DataView(Arraybuffer [, byteOffset [, byteLength]])
注意,前面那個參數只能是 ArrayBuffer ,你不能把 TypedArray 也給我算進去,不然的話...你可以試試。
同樣需要提醒的是 DataView 的修改是和對象一樣的,是進行引用類型的修改,即,如果對一個 Buffer 創建多個 DataView 那麼,多次修改只會在一個 Buffer 顯現出來。
DV 最大的用處就可可以很方便的寫入不同位元組序的值,這相比於使用 TypedArray 來做 swap()(交換) 是很方便的事。當然,位元組序相關也只能是大於 8 bit 以上的操作才有效。
這裡以 setUInt32 為例子,其基本格式為:
- 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 才行!
比如:
- let view = new DataView(buffer);
- view.setUint32(0, arr[0] || 1, BE);
- // 使用 TypedArray 手動構造 swap
- buf = new Uint8Array(11);
- buf[3] = byteLength >>> 16 & 0xFF;
- buf[4] = byteLength >>> 8 & 0xFF;
- buf[5] = byteLength & 0xFF;
當然,如果你覺得不放心,可以直接使用,一個 IIFE 進行相關判斷:
- const LE = (function () {
- let buf = new ArrayBuffer(2);
- (new DataView(buf)).setInt16(0, 256, true); // little-endian write
- return (new Int16Array(buf))[0] === 256; // platform-spec read, if equal then LE
- })();
上面是前端 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。
格式為:
- Buffer.from(arrayBuffer[, byteOffset[, length]])
參考 NodeJS 提供的 demo:
- const arr = new Uint16Array(2);
- arr[0] = 5000;
- arr[1] = 4000;
- // 共享 arr 的緩存
- const buf = Buffer.from(arr.buffer);
- // 列印結果: <Buffer 88 13 a0 0f>
- console.log(buf);
- // 直接改變原始的 Buffer 值
- arr[1] = 6000;
- // 列印: <Buffer 88 13 70 17>
- console.log(buf);
在 Node Buffer 對象上,還掛載了比如:
- buf.readInt16BE(offset[, noAssert])
- buf.readInt16LE(offset[, noAssert])
- buf.readInt32BE(offset[, noAssert])
- buf.readInt32LE(offset[, noAssert])
有點不同的是,它是直接根據名字的不同而決定使用哪種位元組序。
- BE 代表 BigEndian
- LE 代表 LittleEndian
之後,我們就可以使用指定的方法進行寫入和讀取操作。
- const buf = Buffer.from([0, 5]);
- // Prints: 5
- console.log(buf.readInt16BE());
- // Prints: 1280
- 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 在整個流中的順序
所以視頻幀的順序簡單的來表示一下就是:
- PTS: 1 4 2 3
- DTS: 1 2 3 4
- Stream: I P B B
可以看到,我們使用 DTS 來解碼,PTS 來進行播放。OK,關於 Web 直播的大致基本點差不多就介紹完了。後面如果還有機會,我們可以來進行一下 音視頻解碼的實戰操練。
更多內容可以關注我的公眾號--前端小吉米:
http://weixin.qq.com/r/Ij9hebLEpO-brfYa92ow (二維碼自動識別)
推薦閱讀:
※es6用於web前端(非node後端)是不是還太早了?
※12個前端初學者必學技能
※是什麼原因使你最終確定以前端開發工程師為職業的?