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。