現在,大多數已工作的前端工作者的學習方式,要麼直接到 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-Time Messaging Protocol
。它是基於 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 全稱就是 Media Source Extensions
。它是一套處理視訊流技術的簡稱,裡面包括了一系列 API:Media Source
,Source Buffer
等。在沒有 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。以前那種通過 new Buffer
的方法官方已經不推薦使用了,具體原因可以 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): 又叫做
Predicted picture
–前向預測幀。即,他會根據前面一張影像,來進行圖片間的動態壓縮,它的壓縮率和 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 直播的大致基本點差不多就介紹完了。後面如果還有機會,我們可以來進行一下 音視訊解碼的實戰操練。
更多內容,可以關注我的公眾號:前端小吉米