Web 直播流的解析

原文鏈接: https://www.villianhr.com/2017/04/16/Web 直播流的解析

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

老鐵,沒毛病

那什麼情況會用到呢?

  • canvas
  • websocket
  • file
  • fetch
  • webgl

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

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

整體架構

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

記住:只有一個對象我們可以搞到 bit 流 --> ArrayBuffer

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

let socket = new WebSocket(ws://127.0.0.1:8080);nsocket.binaryType = arraybuffer;nnsocket.addEventListener(message, function (event) {n let arrayBuffer = event.data;n ···n});n

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

你可以通過 TypeArray 和 DataView 進行相關查看:

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

TypedArray

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

二進位

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

00000000000000000000000100111010n

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

  • signed: 從左到右第一位開始,如果為 0 則表示為正,為 1 則表示為負。例如:-127~+127
  • unsigned: 從左到右第一位不作為符號的表示。例如:0~255

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

  • 八進位(octet)
  • 十進位(Decimal)
  • 十六進位(Hexadecimal)

特別提醒的是:

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

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

位運算

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

與 (&)

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

// 在 Web 中二進位不能直接表示n001 & 101 = 001n

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

// 和 1000 進行相與nbuf & 8 n

或 (|)

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

// FROM MDNn 9 (base 10) = 00000000000000000000000000001001 (base 2)n 14 (base 10) = 00000000000000000000000000001110 (base 2)n --------------------------------n14 ^ 9 (base 10) = 00000000000000000000000000000111 (base 2) = 7 (base 10)n

非 (~)

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

// FROM MDNn 9 (base 10) = 00000000000000000000000000001001 (base 2)n --------------------------------n~9 (base 10) = 11111111111111111111111111110110 (base 2) = -10 (base 10)n

異或 (^)

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

// FROM MDNn 9 (base 10) = 00000000000000000000000000001001 (base 2)n 14 (base 10) = 00000000000000000000000000001110 (base 2)n --------------------------------n14 ^ 9 (base 10) = 00000000000000000000000000000111 (base 2) = 7 (base 10)n

左移 (<<)

基本格式為:x << y

將 x 向左移動 y 位數。空出來的補 0

// FROM MDNn9 (base 10): 00000000000000000000000000001001 (base 2)n --------------------------------n9 << 2 (base 10): 00000000000000000000000000100100 (base 2) = 36 (base 10)n

帶位右移 (>>)

什麼叫帶位呢?

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

格式為: x >> y

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

1001 >> 2 = 1110n

直接右移 (>>>)

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

格式為: x >> y

1001 >> 2 = 0010n

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

優先順序

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

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

~ >> << >>> & ^ |

位運算具體運用

狀態改變

後台在保存數據的時候,常常會遇到某一個欄位有多種狀態。例如,填表狀態:填完,未填,少填,填錯等。一般情況下直接用數字來進行代替就行,只要文檔寫清楚就沒事。例如:

  • 0: 填完
  • 1: 未填
  • 2:少填
  • 3:填錯

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

  • 0001: 填完
  • 0010: 未填
  • 0100:少填
  • 1000:填錯

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

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

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

var mask = 0001 | 1000;n

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

// 是否填錯nif(mask & 1000) doSth;n

或者,是否即填完又填錯

if(mask & (1000 | 0001)) doSth;n

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

// 取填完的反狀態nvar done = ~0001; // 1110nmask &= done;nn// 添加少填狀態;nmask |= 0100n

進位轉換

在 JS 中進位轉換有兩種方式:toString 和 parseInt。

  • toString(radix): 該可以將任意進位轉換為 2-36 的進位。radix 默認為 10。
  • parseInt(string,radix): 將指定 string 根據 radix 的標識轉換成為 10 進位。radix 默認為 10。另外它主要用作於字元串的提取。
  • Number(string): 字面上轉換字元串為十進位。

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

parseInt(15px, 10); // return 15n

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

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

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

parseInt( 0xF, 16); // return 15n

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

Number(0x11) // 17nNumber(0b11) // 3nNumber(0o11) // 9n

toString

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

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

例如:

0b1101101.toString(8); // 155n0b1101101.toString(10); // 109n0b1101101.toString(8); // 6dn

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

例如:16 進位轉換

function hexConvert(str){n return "0x" + str.toString(16);n}n

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

整體架構

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

  • Int8Array();
  • Uint8Array();
  • Uint8ClampedArray();
  • Int16Array();
  • Uint16Array();
  • Int32Array();
  • Uint32Array();
  • Float32Array();
  • Float64Array();

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

Uint8Array > Uint16Array > Int8Array > …

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

|數據類型|t位元組長度|t含義|t對應的C語言類型| |:—|:—|:—| |Int8|t1|t8位帶符號整數|tsigned char| |Uint8|t1|t8位不帶符號整數|tunsigned char| |Uint8C|t1|t8位不帶符號整數(自動過濾溢出)| unsigned char| |Int16|t2|t16位帶符號整數|tshort| |Uint16|t2|t16位不帶符號整數|tunsigned short| |Int32|t4|t32位帶符號整數|tint| |Uint32|t4|t32位不帶符號的整數|tunsigned int| |Float32|t4| 32位浮點數|tfloat| |Float64|t8|t64位浮點數|tdouble|

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

TypeArray 的實例化有 4 種:

new TypedArray(length); // 創建指定長度的 typeArraynnew TypedArray(typedArray); // 複製新的 typeArraynnew TypedArray(object); // 不常用nnew TypedArray(buffer [, byteOffset [, length]]); // 參數為 arrayBuffer。n

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

當創建實例 TypeArray 的構造函數時,內部會同時創建一個 arrayBuffer 用來作為數據的存儲。如果是通過 TypedArray(buffer); 方式創建,那麼 TypeArray 會直接使用該 buffer 的內存地址。

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

該例直接來源於 MDN

// From a lengthnvar uint8 = new Uint8Array(2);nuint8[0] = 42;nconsole.log(uint8[0]); // 42nconsole.log(uint8.length); // 2nconsole.log(uint8.BYTES_PER_ELEMENT); // 1nn// From an arraynvar arr = new Uint8Array([21,31]);nconsole.log(arr[1]); // 31nn// From another TypedArraynvar x = new Uint8Array([21, 31]);nvar y = new Uint8Array(x);nconsole.log(y[0]); // 21nn// From an ArrayBuffernvar buffer = new ArrayBuffer(8); // 創建 8個位元組長度的 arrayBuffernvar z = new Uint8Array(buffer, 1, 4);n

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

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

var arrBuffer = Uint8Array.from(123); // [1,2,3]nn// 或者nnvar arrBuffer = Uint8Array.of(1,2,3); // [1,2,3]n

多位元組圖

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

buf; // 10B 的 bufnnvar firstB = new Uint8Array(buf,0,1); // buf 中第一個位元組內容nnvar theRestB = new Uint8Array(buf,1,9); // buf 中 2~10 的位元組內容n

位元組概念

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

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

溢出

var arrBuffer = Uint8Array.from(61545);narrBuffer; // [6, 1, 5, 4, 5]n

然後我們做一下加法:

arrBuffer[0] += 1; // 7nnarrBuffer[0] += 0xfe; // 6。因為 7 + 254 溢出 6 n

然後是位元組序。

位元組序

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

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

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

(function () {n let buf = new ArrayBuffer(2);n (new DataView(buf)).setInt16(0, 256, true); // little-endian writen return (new Int16Array(buf))[0] === 256; // platform-spec read, if equal then LEn})();n

關於 TypeArray 的內容差不多就是上面將的。接下來, 我們再來看另外一個重要的對象 DataView。

DataView

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

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

new DataView(buffer [, byteOffset [, byteLength]])n

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

  • buffer: 需要接入的底層 ArrayBuffer
  • byteOffset: 偏移量,單位為位元組
  • byteLength: 獲取長度,單位為位元組

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

先以 16 位的作為例子:

dataview.getInt16(byteOffset [, littleEndian]);n// 根據位元組序,獲得偏移位元組後的兩個位元組。n

  • byteOffset: 單位為 位元組。
  • littleEndian[boolean]: 位元組序。默認為 false。表示大位元組序。

var buffer = new ArrayBuffer(8);nvar dataview = new DataView(buffer);ndataview.getInt16(1,true); // 0n

Buffer 場景

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

  • canvas
  • websocket
  • file
  • fetch
  • webgl

file

直接看代碼吧:

let fileInput = document.getElementById(fileInput);nlet file = fileInput.files[0];nlet reader = new FileReader();nreader.readAsArrayBuffer(file);nreader.onload = function () {n let arrayBuffer = reader.result;n ···n};n

AJAX

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

let xhr = new XMLHttpRequest();nxhr.open(GET, someUrl);nxhr.responseType = arraybuffer;nnxhr.onload = function () {n let arrayBuffer = xhr.response;n ···n};nnxhr.send();n

fetch

fetch(url)n.then(request => request.arrayBuffer())n.then(arrayBuffer => ···);n

canvas

let canvas = document.getElementById(my_canvas);nlet context = canvas.getContext(2d);nlet imageData = context.getImageData(0, 0, canvas.width, canvas.height);nlet uint8ClampedArray = imageData.data;n

websocket

let socket = new WebSocket(ws://127.0.0.1:8080);nsocket.binaryType = arraybuffer;nnsocket.addEventListener(message, function (event) {n let arrayBuffer = event.data;n ···n});n

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

能提供的 arrayBuffer 的都可以進行底層交流。

推薦閱讀:

互聯網已進入下半場,為什麼說直播還處在風口?
領跑遊戲直播後,戰旗眼中的下半場要怎麼玩?
直播燒錢陷入困境:下半場出路在B端?
鬥魚秀場發展超越LOL,陰陽師CS直播也吸金

TAG:Web开发 | 直播 | 前端开发 |