文章的大綱:html
探索意味着前方都是未知的事物,但願這篇文章可以帶着讀者,一塊兒回顧我探索的過程,同時學習遇到的種種未知的事物。如今開始吧。前端
在一些視頻點播網站的視頻播放界面,用戶將鼠標移動到進度條上時,會彈出一個浮窗並展現了一張圖片,意在告訴用戶這張圖片是鼠標所在位置的時間點對應的視頻畫面。並且目前的實現,用戶體驗是足夠好的,預覽圖出現的速度很是快,並且不一樣時間範圍展現的也是不一樣的畫面,達到模擬實時預覽的效果,如圖: git
經過翻看各大視頻網站,發現彈窗中的畫面通常是一張背景圖片,打開背景圖片的連接看到的是一張視頻縮略拼圖。打開 Chrome 瀏覽器 DevTools 的 Elements 面板,能夠看到: github
FFmpeg 是一個很是強大的音視頻處理工具,它的官網是這麼介紹的: web
我寫了一個 C 應用程序,實現瞭如何使用 FFmpeg 生成視頻縮略拼圖。它接收一個視頻文件路徑做爲參數,獲取到參數後,使用 FFmpeg 的方法讀取視頻文件並通過一系列步驟(解複用 -> 幀解碼 -> 幀轉碼… )處理以後,會在當前目錄生成一張拼圖。 總結了一下程序邏輯執行的步驟:算法
以上是生成視頻縮略拼圖程序邏輯執行的步驟。由於這部分與這篇文章的主題無關,因此就不貼代碼了。感興趣的同窗能夠前往 GitHub - VVangChen/video-thumbnail-sprite 查看完整源碼,也能夠下載可執行文件在本地運行。api
上面講到了如何使用 FFmpeg 生成視頻縮略拼圖,接下來向探索的目標再進一步。在常見的實現方式中,最重要的一環就是生成視頻縮略拼圖,那麼能不能將這最重要的一環在瀏覽器中實現呢?答案是確定的,而且應該能聯想到最近比較火熱的 WebAssembly,由於它就是爲此而生。瀏覽器
WebAssembly,是一門被設計成能夠運行在瀏覽器中的編譯目標語言,意在經過移植將原生應用的能力帶到瀏覽器中。若是想要了解更多,能夠瀏覽它的官網WebAssembly 或者前往 WebAssembly | MDN 進行學習。接下來要講的,是如何將上面實現的,生成視頻縮略拼圖的 C 程序移植到 Chrome 中。緩存
簡單來講,「單純的移植」只需如下兩步:bash
其中 emconfigure、emmake 和 emcc 都是 Emscripten 的 emsdk 提供的工具,經過 emsdk 能夠很是簡單地將 C/C++ 程序移植到瀏覽器中。使用 emcc 可以將 C 程序編譯成 wasm 模塊,同時還會生成一個 JS 文件,它暴露了一系列工具方法,使得 JS 可以訪問 C 模塊導出的方法,訪問 C 模塊的內存。emsdk 的安裝方法參考 Download and install。 安裝好以後咱們開始移植咱們的 C 程序:
emconfigure ./configure --prefix=/usr/local/ffmpeg-web --cc="emcc" --enable-cross-compile --target-os=none --arch=x86_64 --cpu=generic \
--disable-ffplay --disable-ffprobe --disable-asm --disable-doc --disable-devices --disable-pthreads --disable-w32threads --disable-network \
--disable-hwaccels --disable-parsers --disable-bsfs --disable-debug --disable-protocols --disable-indevs --disable-outdevs --enable-protocol=file
複製代碼
emmake make && sudo make install
emcc -o web_api.html web_api.c preview.c \
-s ASSERTIONS=1 -s VERBOSE=1 \
-s ALLOW_MEMORY_GROWTH=1 -s TOTAL_MEMORY=67108864 \
-s WASM=1 \
-s EXTRA_EXPORTED_RUNTIME_METHODS='["ccall", "cwrap"]' \
`pkg-config --libs --cflags libavutil libavformat libavcodec libswscale`
複製代碼
咱們能夠看到運行命令生成了 wasm 和 js 文件,這樣咱們就算完成了移植。
可是 「單純的移植」 後的應用是不能直接運行的,由於在瀏覽器中程序不能直接操做用戶的本地文件。因此須要稍微改造一下以前的 C 程序,以及增長 Web 端的代碼,移植後的應用邏輯大概是這樣的:
接下來簡單分析一下移植後的 Web 應用。由於這一節不是今天的主題,就不貼完整的代碼了,感興趣的同窗能夠前往GitHub - VVangChen/video-thumbnail-sprite查看完整源碼及示例。
function generateSprite(data, cols = 5, interval = 10) {
// 獲取 c 模塊暴露的 getSpriteImage 方法
const getSpriteImage = Module.cwrap('getSpriteImage', 'number',
['number', 'number', 'number', 'number']);
const uint8Data = new Uint8Array(data.buffer)
// 分配內存
const offset = Module._malloc(uint8Data.length)
// 將數據寫入內存
Module.HEAPU8.set(uint8Data, offset)
// 調用 getSpriteImage,獲得生成的拼圖地址
const ptr = getSpriteImage(offset, uint8Data.length, cols, interval)
// 從內存中取出拼圖的內存地址
const spriteData = Module.HEAPU32[ptr / 4]
...
...
,,,
// 獲取拼圖數據
const spriteRawData = Module.HEAPU8.slice(spriteData, spriteData + size)
// 釋放內存
Module._free(offset)
Module._free(ptr)
Module._free(spriteData)
return ...
}
複製代碼
另外,若是 Web 端想要調用 C 模塊的方法,須要在 C 代碼中使用宏標記想要暴露給 Web 端的方法,以下所示:
EMSCRIPTEN_KEEPALIVE // 用來標記想要暴露給 Web 端的方法
SpriteImage *getSpriteImage(uint8_t *buffer, const int buff_size, int cols, int interval);
複製代碼
這樣就能夠在 JS 中直接調用 C 模塊的 getSpriteImage 方法,等待 C 模塊生成視頻縮略拼圖後返回給 Web 端,而後在 Canvas 畫布中將其繪製並展現。能夠前往 GitHub - VVangChen/video-thumbnail-sprite查看完整源碼及示例。
在上一節中,完成了在瀏覽器中獨立地實現完整的視頻幀預覽功能。那麼離探索的目標只差一步,就是真正實時地生成視頻預覽圖。 開頭講過,真正實時有兩個條件,一是不預先準備好圖片,而是在鼠標移到進度條上時再去生成;二是每一個時間點的預覽圖都是不一樣的,就是說展現的圖片必定是那一秒的視頻畫面。 其中第一個條件只是時間上的延遲,因此只要在鼠標移到進度條上時再觸發生成拼圖的動做就行;而第二個條件,只要縮短拼圖中縮略圖的採樣頻率到1秒1次就行。 現有的方案都是基於拼圖來實現的,可是事實上,如今的需求並不須要預先生成全部畫面的縮略圖,只須要生成那一秒的就行。考慮到已經可以生成全部畫面的縮略圖,那麼只生成一張確定是能夠實現的。 另外,既然如今只須要生成一張縮略圖,而不是全部視頻畫面的拼圖,那麼是否是隻須要獲取這一張縮略圖的數據就行?答案也是確定的。因此如何獲取某個時間點的視頻縮略圖數據,是此次探索成敗的關鍵。 先來看下最終實現的程序,執行邏輯是怎樣的:
其中 2 - 5 與上一節實現的方法相同,就再也不贅述,查看完整源碼請前往 :github.com/VVangChen/v… 。 剩下的內容中,主要講下如何實現第 1 步,獲取鼠標指針所選時間點的幀數據。它能夠被拆解爲兩個步驟:
在這裏,只考慮目前比較流行的 mp4 格式的視頻文件。因此能夠將第一步轉換成:如何在 mp4 格式的視頻文件中,計算出某個時間點對應的幀數據的偏移量及其大小? 這涉及到對 mp4 文件結構的解析。mp4 文件是由一個個連續的被稱爲 ‘box’ 的結構單元構成的,每個 ‘box’ 由 header 和 data 組成,header 至少包含大小和類型,data 能夠是 ‘box’ 自身的數據,也能夠是一個或多個 ‘box’。不一樣的 ‘box’ 有不一樣的做用,對於計算幀數據的偏移量,主要須要用到如下幾個 ‘box’:
經過這些 ‘box’,按照必定的算法就能夠獲得幀數據的偏移量和大小:
實現瞭如何計算幀數據在 mp4 文件中的偏移量,以及幀數據的長度。接下來進行第二步,獲取視頻文件 [偏移量, 偏移量 + 幀數據長度] 範圍內的數據。它能夠被轉換成下面這個問題:如何獲取 URL 資源的某部分數據? 它能夠經過 HTTP 的範圍請求來實現。若是資源服務器支持,只須要在 HTTP 請求中指定一個 Range 請求頭,它的值是想要獲取的資源數據的範圍,看下示例:
function fetchRangeData(url, offset, size) {
return new Promise((resolve) => {
const xhr = new XMLHttpRequest()
xhr.onload = (e) => {
resolve(xhr.response)
}
xhr.open('GET', url)
// 設置 Range 請求頭
xhr.setRequestHeader('Range', `bytes=${offset}-${offset + size - 1}`)
xhr.responseType = 'arraybuffer'
xhr.send()
})
}
複製代碼
經過調用 fetchRangeData 函數,傳入資源的 URL,想要請求的字節偏移量和字節大小,就能夠得到你想要的字節序列。
至此,已經實現了獲取某個時間點的視頻幀數據,但這並不意味着必定可以生成用戶想要的預覽圖。即便從獲取到的部分幀數據大小也能夠發現,它們很是小,有的才幾十字節,顯然不夠描述一幅圖片。若是把這些幀數據直接傳給 FFmpeg,也沒法成功被解碼。這又是爲何呢? 這是由於在 H.264 編碼中,幀主要分爲三種類型: 1. I 幀:獨立解碼幀,又稱關鍵幀(Intra frame),表示解碼它不依賴其餘幀 2. P 幀:前向預測幀,表示解碼它須要參照幀序列中的上一幀 3. B 幀:雙向預測幀,表示解碼它須要參照幀序列中的上一幀和後一幀 顯而易見,P 幀和 B 幀相對於 I 幀,會小不少。這也是爲何一些幀只須要幾十個字節。 從上面幀類型的描述能夠得知,在解碼時幀與幀之間的依賴(參照)關係,若是不是 I 幀,就沒法被獨立解碼。要解碼非 I 幀,就須要獲取到它參照的全部幀。在 H.264 編碼的碼流中,幀序列中的幀是以參照關係排列的,參照關係也決定了幀解碼的順序,由於被參照幀的解碼順序必定在參照幀的前面。 由於只有 I 幀可以獨立解碼,因此它在一組參照關係中必定是被排在最前面。若是想要解碼非 I 幀,只須要獲取到所選幀到它所在參照組中最前面的 I 幀之間的全部幀。通常將能夠獨立解碼的參照組序列稱爲一組幀(GOP),它通常是兩個 I 幀之間的一段幀序列。如示例圖所示:
查看獲取幀序列的代碼請前往: github.com/VVangChen/v… 獲取到鼠標指針所選時間點的幀數據後,將其傳給 C 模塊,生成 RGB 數據後返回給 Web 端,而後在 Canvas 畫布上繪製並展現,用戶就能夠看到所選時間點的視頻畫面了。 至此,就實現了使用純前端技術實現實時的視頻幀預覽。
感謝可以耐心看完的讀者。確定有人會問了,作這件事的意義在哪裏?我能回答是既然是探索,前方確定也應該是未知的,路的盡頭在走到以前誰也不清楚是什麼,況且探索的腳步並未中止。 目前實現的程序還存在不少問題,好比:
接下來會着手解決這些問題,並繼續探索如何將其應用於生產環境,使其更具實際使用的價值。因此探索的腳步並未中止,敬請期待,共勉。
若是文章有錯誤或待商榷的地方,歡迎指出或討論,感謝!