天天看點

web 直播流的解析

本文作者:ivweb villainthr

Web 進制操作是一個比較底層的話題,因為平常做業務的時候根本用不到太多,或者說,根本用不到。

<code>老鐵,沒毛病</code>

那什麼情況會用到呢?

canvas

websocket

file

fetch

webgl

...

上面隻是列了部分内容。現在比較流行的就是音視訊的處理,怎麼說呢?

如果,有涉及直播的話,那麼這應該就是一個非常!非常!非常!重要的一塊内容。我這裡就不廢話了,先主要看一下裡面的基礎内容。

首先,一開始我們是怎麼接觸到底層的 bit 流呢?

記住:隻有一個對象我們可以搞到 bit 流 --&gt; ArrayBuffer

這很似曾相識,例如在 fetch 使用中,我們可以通過 res.arrayBuffer(); 來直接擷取 ArrayBuffer 對象。websocket 中,監聽<code>message</code>,傳回來的<code>event.data</code> 也是 arraybuffer。

但是,ArrayBuffer 并不能直接提供底層流的擷取操作!!!

你可以通過 TypeArray 和 DataView 進行相關檢視:

web 直播流的解析

接下來,我們具體看一下 TypeArray 和 DataView 的具體細節吧。

首先聲明這并不是一個具體的 array 對象,而是一整個底層 Buffer 的概念集合。首先,我們了解一下底層的二進制:

在一般程式語言裡面,最底層的資料大概就可以用 0 和 1 來表示:

<code>00000000000000000000000100111010</code>

根據底層的比特的資料還可以劃分為兩類:

signed: 從左到右第一位開始,如果為 0 則表示為正,為 1 則表示為負。例如:-127~+127

unsigned: 從左到右第一位不作為符号的表示。例如:0~255

而我們程式表達時,為了易讀性和簡便性常常會結合其他進制一起使用。

八進制(octet)

十進制(Decimal)

十六進制(Hexadecimal)

特别提醒的是:

在 JS 中: 使用 <code>0x</code>字面上表示十六進制。每一位代表 4bit(2^4)。 使用 <code>0o</code>字面上表示八進制。每一位代表 3bit(2^3)。還有一種是直接使用<code>0</code> 為開頭,不過該種 bug 較多,不推薦。 使用 <code>0b</code> 字面上表示二進制。每一位代表 1bit(2^1)。

了解了二進制之後,接下來我們主要來了解一下 Web 比特位運算的基本内容。

Web 中的位運算和其它語言中類似,有基本的 7 個。

在相同位上,都為 1 時,結果才為 1:

并且,該運算常常會和叫做<code>bitmask</code>(屏蔽字)結合起來使用。比如,在音視訊的 Buffer 中,第 4 位 bit 表示該 media segments 裡面是否存在 video。那麼為了檢驗,則需要提取第 4 位,這時候就需要用到我們的 bitmask。

在相同位上,有一個為 1 時,結果為 1。

隻和自己做運算,如果為 0,結果為 1。如果為 1 結果為 0。反正就是相反的意思了:

當兩者中隻有一個 1 那麼結果才為 1。

基本格式為:x &lt;&lt; y

将 x 向左移動 y 位數。空出來的補 <code>0</code>

什麼叫<code>帶位</code>呢?

上面我們提到過<code>signed</code> 和 <code>unsigned</code>。那麼這裡針對的就是<code>signed</code> 的位移類型。

格式為: x &gt;&gt; y

将 x 向右移動 y 位數。左邊空出來的位置根據最左邊的第一位決定,如果為 1 則補 1,反之。

<code>1001 &gt;&gt; 2 = 1110</code>

該方式和上面具體差別就是,該運算針對的是 <code>unsigned</code> 的移動。不管你左邊是啥,都給我補上 0。

<code>1001 &gt;&gt; 2 = 0010</code>

上面這些運算符主要是針對 32bit 的。不過有時候為了簡便,可以省去前面多餘的 0。不過大家要清楚,這是針對 32 位的即可。

上面簡單介紹了位操作符,但是他們的優先級是怎麼樣的呢?詳情可以參考:precedence;

簡單來說:(按照下列順序,優先級降低)

~ &gt;&gt; &lt;&lt; &gt;&gt;&gt; &amp; ^ |

狀态改變

背景在儲存資料的時候,常常會遇到某一個字段有多種狀态。例如,填表狀态:填完,未填,少填,填錯等。一般情況下直接用數字來進行代替就行,隻要文檔寫清楚就沒事。例如:

0: 填完

1: 未填

2:少填

3:填錯

不過,我們還可以通過比特位來進行表示,每一位表示一個具體的狀态。

0001: 填完

0010: 未填

0100:少填

1000:填錯

這樣我們隻要找到每一位是否為 1 就可以知道裡面有哪些狀态存在。并且,還可以對狀态進行組合,例如,填完并且填錯,如果按照數字來說就沒啥說明這樣的情況。

那麼基本的狀态值有了,接下來就是怎麼進行指派和修改。

現在假設,某人的填寫狀态為 填完 + 填錯。那麼結果可以表示為:

<code>var mask = 0001 | 1000;</code>

後面如果涉及條件判斷,例如:該人是否填錯,則可以使用<code>&amp;</code> 來表示:

或者,是否即填完又填錯

<code>if(mask &amp; (1000 | 0001)) doSth;</code>

後面涉及到狀态改變的話,則需要用到<code>|</code>運算。假設,現在該人為填完,現在變為少填。那麼狀态改變應該為:

在 JS 中進制轉換有兩種方式:<code>toString</code> 和 <code>parseInt</code>。

toString(radix): 該可以将任意進制轉換為 2-36 的進制。radix 預設為 10。

parseInt(string,radix): 将指定 string 根據 radix 的辨別轉換成為 10 進制。radix 預設為 10。另外它主要用作于字元串的提取。

Number(string): 字面上轉換字元串為十進制。

parseInt 用于字元串過濾,例如:

<code>parseInt('15px', 10); // return 15</code>

裡面的字元不僅隻有數字,而且還包括字母。

不過需要注意的是,parseInt 是不認可,以 0 開頭的八進制,但認可 0o。是以,在使用的時候需要額外注意。

上面說過,parseInt 是将其它進制轉換為 10 進制,其第二個參數主要就是為了表示前面内容的進制,如果沒寫,引擎内部會進行相關識别,但不保證一定正确。是以,最好寫上。

<code>parseInt(' 0xF', 16); // return 15</code>

如果你隻是想簡單轉換一下字元串,那麼使用 Number() 無疑是最簡單的。

toString 裡面的坑就沒有 parseInt 這麼多了。它也是進制轉換非常好用的一個工具。因為是 <code>字元串</code>,是以,這裡就隻能針對字面量進制進行轉換了--2,8,(10),16。這四種進制的相關之間轉換。

提醒:如果你是直接使用字面量轉換的話,需要注意使用 10 進制轉換時,隐式轉換會失效。即,100.toString(2) 會報錯。

例如:

如上面所述,他們轉換後的結果一般沒有進制字首。這個時候,就需要手動加上相關的字首即可。

例如:16 進制轉換

到這裡,進制轉換基本就講完了。後面我們來看一下具體的 TypeArray

TypeArray 不是一個可以用程式寫出來的概念,它是許多 TypeArray 的總稱。參考: TypeArray。可以了解到,它的子類如下:

Int8Array();

Uint8Array();

Uint8ClampedArray();

Int16Array();

Uint16Array();

Int32Array();

Uint32Array();

Float32Array();

Float64Array();

看上去很多,不過在 JS 中,因為它天生都不是用來處理 <code>signed</code> 類型的。是以,<code>Uint</code>系列在 JS 中應該算是主流。大概排個序:

Uint8Array &gt; Uint16Array &gt; Int8Array &gt; ...

他們之間的具體不同,參照:

資料類型

位元組長度

含義

對應的C語言類型

Int8

1

8位帶符号整數

signed char

Uint8

8位不帶符号整數

unsigned char

8位不帶符号整數(自動過濾溢出)

Int16

2

16位帶符号整數

short

Uint16

16位不帶符号整數

unsigned short

Int32

4

32位帶符号整數

int

Uint32

32位不帶符号的整數

unsigned int

Float32

32位浮點數

float

Float64

8

64位浮點數

double

雖然口頭上說 TypeArray 沒有一個具體的執行個體,但是私下,上面那幾個 array 都是叫他爸爸。因為他定義了一些 uintArray 的基本功能。首先是執行個體化:

TypeArray 的執行個體化有 4 種:

上面 4 中最常用的應該為 1 和 4。接着,我們了解一下,具體才建立的時候,TypeArray 到底做了些什麼。

當建立執行個體 TypeArray 的構造函數時,内部會同時建立一個 arrayBuffer 用來作為資料的存儲。如果是通過 <code>TypedArray(buffer)</code>; 方式建立,那麼 TypeArray 會直接使用該<code>buffer</code>的記憶體位址。

接下來,我們就以 <code>Uint8Array</code> 為主要參照,來看一下基本的處理和操作。

該例直接來源于 MDN

它上面的方法大家直接參考 MDN 的上的就 OK。一句話總結就是,你可以想操作 Array 一樣,操作裡面的内容。

根據 ArrayBuffer 的描述,它本身的是從 files 和 base64 編碼來擷取的。如果隻是初始化,他裡面的每一位都是 0.不過,為了容易測試,我們可以直接自己指定:

假如一個 Buffer 很長,假設有 80 位,算下來就是 10B。一開始我們的想法就是直接建立一個 typeArray就 OK。不過,根據上面的構造函數上看,其實,可以将一整個 buffer 拆成不同的 typeArray 進行讀取。

在位元組中,還有幾個相關的概念需要了解一下。一個是溢出,一個是位元組序。同樣,還是根據 Uint8 來說明。

Uint8 每一個數組位,表示 8 位二進制,即範圍為 0~255。

溢出

然後我們做一下加法:

然後是位元組序。

位元組序

在 JS,Java,C 等進階語言中,位元組序一般都是大位元組序。而一些硬體則會以小位元組序作為标準。

大位元組序:假如 0xAABB 被 Uint16 存儲為 2 位。那麼按照大位元組序就是按順序來,即 0: 0xAA, 1:0xBB。

小位元組序:和上面相反,即,0:0xBB,1:0xAA。

當然如果隻是在 PC 上操作了的話,位元組序可以使用 IIFE 檢測一下:

關于 TypeArray 的内容差不多就是上面将的。接下來, 我們再來看另外一個重要的對象 <code>DataView</code>。

DataView 沒有 TypeArray 這麼複雜,衍生出這麼多個 Uint/IntArray。它就是一個構造函數。同樣,它的目的也是對底層的 arrayBuffer 進行讀取。那麼,為什麼它會被建立出來呢?

是因為有 <code>位元組序</code> 的存在。上面說過位元組序有兩種。通常,PC 和目前流行的電子裝置都是大位元組序,而如果是接收一些外部資源,就不能排除會接受一些小位元組序的檔案。為了解決這個問題,就出現了 DataView。它的執行個體格式為:

<code>new DataView(buffer [, byteOffset [, byteLength]])</code>

同樣,它的格式和 TypeArray 類似,也是用來作為 buffer 的讀寫對象。

buffer: 需要接入的底層 ArrayBuffer

byteOffset: 偏移量,機關為位元組

byteLength: 擷取長度,機關為位元組

它的具體操作不是直接通過<code>[]</code>擷取,而是使用相關的<code>get/set</code>方法來完成。而他針對<code>位元組序</code>的操作,主要是針對 &gt;=16 比特的流來差別,即,get/setInt8() 是沒有<code>位元組序</code> 的概念的。

先以 16 位的作為例子:

byteOffset: 機關為 位元組。

littleEndian[boolean]: 位元組序。預設為 false。表示大位元組序。<code>var buffer = new</code> <code>ArrayBuffer(8); var dataview = new</code> <code>DataView(buffer); dataview.getInt16(1,true); // 0</code>

如上面所述,Buffer 的場景有:

直接看代碼吧:

這裡和 fetch 區分一下,作為一種相容性比較好的選擇。

上面這些都是可以和 Buffer 進行交流的對象。那還有其他的嗎?有的,總的一句話:

能提供的 arrayBuffer 的都可以進行底層交流。 原文連結:http://www.ivweb.io/topic/58f2f7fab4d59277e3e2d03c