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

villainthr發表於2019-02-12

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

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

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

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

直播協議

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

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

HLS

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

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

image.png-93.1kB
image.png-93.1kB

他的使用方式為:

<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 傳遞資料。兩者都可以起到流的劃分作用。流的內容也主要分為:視訊,音訊,相關協議包等。

詳細傳輸過程如圖:

image.png-29.3kB
image.png-29.3kB

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

HTTP-FLV

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

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

image.png-8.2kB
image.png-8.2kB

由於,每個 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 SourceSource 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 提供兩個最基本的操作 appendBufferremove。之後,我們就可以通過 appendBuffer 直接將 ArrayBuffer 放進去即可。

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

所以,整個流程圖為:

image.png-16kB
image.png-16kB

音視訊的 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 記憶體大小。此時,它裡面只是空的記憶體,這時候你需要借用其他兩個物件 TypedArrayDataView 來幫助你完成寫入和修改的操作。不過,AB 提供了一個非常重要的方法:slice()

slice() 和 Array 物件上的 slice 方法一樣也是將陣列中的一部分新建立一個副本返回。這個方法為啥有用呢?

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

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

接下來,我們就來看一下和 AB 實際對接最緊密的兩個物件 TypedArrayDataView

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

他可以直接通過 allocfrom 方法來直接建立指定的大小的 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 音訊大小大很多,這就是壓縮格式起的主要作用。具體流程圖如下:

image.png-37.1kB
image.png-37.1kB

首先,由原始數碼裝置提供相關的數字訊號流,然後經由視訊壓縮演算法,大幅度的減少流的大小,然後交給視訊盒子,打上相應的 dtspts 欄位,最終生成可用的視訊檔案。常用的視訊格式和壓縮格式如下:

image.png-136.4kB
image.png-136.4kB

視訊格式主要是參考 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 幀來說,還多了後一張影象的預測,所以它的壓縮率更高。

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

image.png-54kB
image.png-54kB

不過,這樣理解 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 直播的大致基本點差不多就介紹完了。後面如果還有機會,我們可以來進行一下 音視訊解碼的實戰操練。

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

相關文章