基於web assembly (WASM) 的H265 Web播放器

1 背景

目前這個時間點,原生支持H265(HEVC)播放的瀏覽器極少,可以說基本沒有,主要原因一個是H265的解碼有更高的性能要求,從而換取更高的壓縮率,目前大多數機器CPU軟解H265的超清視頻還是有點喫力,硬解兼容性又不好,另外一個原因主要是H265的專利費問題。因此H265有被各大瀏覽器廠商放棄的趨勢,轉而去支持更加開放的AV1編碼,但是AV1編碼的商用和普及估計還有段時間。

H265與H264相比主要的好處在於相同分辨率下降低了幾乎一倍的碼率,對帶寬壓力比較大的網站來說,使用H265可以極大削減帶寬消耗(儘管可能面臨專利費麻煩),但是由於瀏覽器的支持問題,目前H265的播放主要在APP端實現,藉助硬件解碼,可以獲得比較好的性能和體驗。

本文相關的代碼使用WASM、FFmpeg、WebGL、Web Audio等組件實現了一個簡易的支持H265的Web播放器,作爲探索、驗證,just for fun。

2 代碼

github地址: https://github.com/sonysuqin/WasmVideoPlayer.

3 依賴

3.1 WASM

WASM的介紹在這裏,可以在瀏覽器裏執行原生代碼(例如C、C++),要開發可以在瀏覽器運行的原生代碼,需要安裝他的工具鏈,我使用的是當時最新的版本(1.38.21)。編譯環境有Ubuntu、MacOS等,這裏有介紹。

3.2 FFmpeg

主要使用FFmpeg來做解封裝(demux)和解碼(decoder),由於使用了FFmpeg(3.3),理論上可以播放絕大多數格式的視頻,這裏只針對H265編碼、MP4封裝,在編譯時可以只按需編譯最少的模塊,從而得到比較小的庫。

使用Emscripten編譯FFmpeg主要參考下面這個網頁,做了一些修改: https://blog.csdn.net/Jacob_job/article/details/79434207

3.3 WebGL

H5使用Canvas來繪圖,但是默認的2d模式只能繪製RGB格式,使用FFmpeg解碼出來的視頻數據是YUV格式,想要渲染出來需要進行顏色空間轉換,可以使用FFmpeg的libswscale模塊進行轉換,爲了提升性能,這裏使用了WebGL來硬件加速,主要參考了這個項目,做了一些修改: https://github.com/p4prasoon/YUV-Webgl-Video-Player

3.4 Web Audio

FFmpeg解碼出來的音頻數據是PCM格式,可以使用H5的Web Audio Api來播放,主要參考了這個項目,做了一些修改: https://github.com/samirkumardas/pcm-player

4 播放器實現

這裏只是簡單實現了播放器的部分功能,包括下載、解封裝、解碼、渲染、音視頻同步等基本功能,每個環節還有很多細節可以優化。seek還沒有做,因爲涉及的東西比較多。

4.1 模塊結構

在這裏插入圖片描述

4.2 線程模型

理論上來說,播放器應該使用這樣的線程模型,各個模塊在各自線程各司其職: 

但是WASM目前對多線程(pthread)的支持不夠好,各個瀏覽器的WASM多線程支持還處於試驗階段,因此現在最好不要在原生代碼裏編寫pthread的代碼。這裏使用了Web Worker,把下載和對FFmpeg的調用放到單獨的線程中去。

主要有三個線程:

  • 主線程(Player):界面控制、播放控制、下載控制、音視頻渲染、音視頻同步;
  • 解碼線程(Decoder Worker):音視頻數據的解封裝、解碼;
  • 下載線程(Downloader Worker):下載某個chunk。 線程之間通過postMessage進行異步通信,在需要傳輸大量數據(例如視頻幀)的地方,需要使用Transferable接口來傳輸,避免大數據的拷貝損耗性能。

4.3 Player

4.3.1 接口

  • play:開始播放;
  • pause:暫停播放;
  • resume:恢復播放;
  • stop:停止播放;
  • fullscreen:全屏播放;
  • seek:seek播放未實現。

4.3.2 下載控制

爲防止播放器無限制地下載文件,在下載操作中佔用過多的CPU,浪費過多帶寬,這裏在獲取到文件碼率之後,以碼率一定倍數的速率下載文件。

4.3.3 緩衝控制

爲防止播放器無限制的解碼佔用過多的CPU,設置一個已解碼視頻幀隊列長度的閾值,超過閾值則停止解碼,隊列消耗到一定程度後重啓解碼。

4.3.4 音視頻同步

音頻數據直接餵給Web Audio,通過Web Audio的Api可以獲得當前播放的音頻的時間戳,以該時間戳爲時間基準來同步視頻幀,如果當前視頻幀的時間已經落後則立刻渲染,如果比較早,則需要delay。 在H5裏delay可以通過setTimeout實現(還未找到更好的方式),上面做緩衝控制的另外一個意義在於控制視頻的渲染頻率,如果調用setTimeout的視頻幀太多,內存會暴漲。

4.3.5 渲染

簡單地將PCM數據交給PCM Player,YUV數據交給WebGL Player。

4.4 Downloader

這個模塊很簡單,只是單純爲了不在主線程做太多事情而分離,功能主要有:

  • 通過Content-Length字段獲取文件的長度;
  • 通過Range字段下載一個chunk。

如上面提到的,Player會進行速率控制,因此需要把文件分成chunk,按照chunk方式進行下載。下載的數據先發給Player,由Player轉交給Decoder(理論上應該直接交給Decoder,但是Downloader無法直接與Decoder通信)。

4.5 Decoder

這個模塊需要加載原生代碼生成的膠水代碼(glue code),膠水代碼會加載wasm。

self.importScripts("libffmpeg.js");

4.5.1 接口

  • initDecoder:初始化解碼器,開闢文件緩存;
  • uninitDecoder:反初始化解碼器;
  • openDecoder:打開解碼器,獲取文件信息;
  • closeDecoder:關閉解碼器;
  • startDecoding:開始解碼;
  • pauseDecoding:暫停解碼。

這些方法都由Player模塊通過postMessage異步調用。

4.5.2 緩存

這裏簡單使用了WASM的MEMFS文件接口(WASM的文件系統參考),使用方式就是直接調用stdio的方法,然後在emcc的編譯命令中加入編譯選項:

-s FORCE_FILESYSTEM=1 

MEMFS會在內存中虛擬一個文件系統,Decoder收到Player發過來的文件數據直接寫入緩存,由解碼任務讀取緩存。

4.5.3 解碼

  • 播放開始後不能立刻打開解碼器,因爲FFmpeg探測數據格式需要一定的數據長度(例如MP4頭的長度);
  • 緩存的數據足夠後Player打開解碼器,會得到音頻的參數(通道數、採樣率、採樣大小、數據格式),視頻的參數(分辨率,duation、顏色空間),以這些參數來初始化渲染器、界面;
  • Player調用startDecoding會啓動一個定時器執行解碼任務,以一定的速率開始解碼;
  • Player緩存滿後會調用pauseDecoding暫停解碼器。

4.5.4 數據交互

解碼後的數據直接通過Transferable Objects postMessage給Player,這樣傳遞的是引用,不需要拷貝數據,提高了性能。

Javascript與C的數據交互:

發送:
……
this.cacheBuffer = Module._malloc(chunkSize);
……
Decoder.prototype.sendData = function (data) {
    var typedArray = new Uint8Array(data);
    Module.HEAPU8.set(typedArray, this.cacheBuffer); //拷貝
    Module._sendData(this.cacheBuffer, typedArray.length); //傳遞
};

接收:
this.videoCallback = Module.addFunction(function (buff, size, timestamp) {
    var outArray = Module.HEAPU8.subarray(buff, buff + size); //拷貝
    var data = new Uint8Array(outArray);
    var objData = {
        t: kVideoFrame,
        s: timestamp,
        d: data
    };
    self.postMessage(objData, [objData.d.buffer]); //發送給Player
});
需要把回調通過openDecoder方法傳入C層,在C層調用。

5 編譯

5.1 安裝Emscripten

參考其官方文檔

5.2 下載FFmpeg

git clone https://git.ffmpeg.org/ffmpeg.git

這裏切到了3.3分支。

5.3 下載本文的代碼

保證FFmpeg目錄和代碼目錄平級。

git clone https://github.com/sonysuqin/WasmVideoPlayer.git

5.4 編譯

進入代碼目錄,執行:

./build_decoder.sh

6 測試

可以使用任意的Http Server(Apache、Nginx等),例如: 如果安裝了node/npm/http-server,則在代碼目錄下執行:

http-server -p 8080 .

在瀏覽器輸入即可:

http://127.0.0.1:8080

7 瀏覽器支持

目前(20190207)沒有做太多嚴格的瀏覽器兼容性測試,主要在Chrome上開發,以下瀏覽器比較新的版本都可以運行:

  • Chrome(360瀏覽器、搜狗瀏覽器等webkit內核也支持);
  • Firefox;
  • Edge。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章