wasm + ffmpeg實現前端截取視頻幀功能

!function() {

let setFile = null;

// WASM下載並解析完畢

Module.onRuntimeInitialized = function () {

console.log('WASM initialized done!');

// 導出的核心處理函數

setFile = Module.cwrap('setFile', 'number',

['number', 'number', 'number']);

};

}();

需要在wasm下載並解析完成之後才能開始操作,它提供了一個onRuntimeInitialized的回調。

為了能夠使用C文件裡面導出的函數,可以使用Module.cwrap,第一個參數是函數名,第二個參數是返回類型,由於返回的是一個指針地址,這裡是一個32位的數字,所以用js的number類型,第三個參數是傳參類型。

接著讀取input的文件內容到放到一個buffer裡面:

let form = document.querySelector('form');

// 監聽onchange事件

form.file.onchange = function () {

if (!setFile) {

console.warn('WASM未加載解析完畢,請稍候');

return;

}

let fileReader = new FileReader();

fileReader.onload = function () {

// 得到文件的原始二進制數據ArrayBuffer

// 並放在buffer的Unit8Array裡面

let buffer = new Uint8Array(this.result);

// ...

};

// 讀取文件

fileReader.readAsArrayBuffer(form.file.files[0]);

};

讀取得到的buffer放在了一個Uint8Array,它是一個數組,數組裡面每個元素都是unit8類型的即無符號8位整型,就是一個字節的0101的數字大小。

接下來的關鍵問題是:怎麼把這個buffer傳給wasm的setFile函數?這個需要理解wasm的內存堆模型。

4. wasm的內存堆模型

上面在編譯的時候指定的wasm使用的總內存大小,內存裡面的內容可以通過Module.buffer和Module.HEAP8查看:

wasm + ffmpeg實現前端截取視頻幀功能

這個東西就是JS和WASM數據交互的關鍵,在JS裡面把數據放到這個HEAP8的數組裡面,然後告訴WASM數據的指針地址在哪裡和佔用的內存大小,即在這個HEAP8數組的index和佔用長度,反過來WASM想要返回數據給JS也是被放到這個HEA8裡面,然後返回指針地址和和長度。

但是我們不能隨便指定一個位置,需要用它提供的API進行分配和擴容。在JS裡面通過Module._molloc或者Module.dynamicMalloc申請內存,如下代碼所示:

// 得到文件的原始二進制數據,放在buffer裡面

let buffer = new Uint8Array(this.result);

// 在HEAP裡面申請一塊指定大小的內存空間

// 返回起始指針地址

let offset = Module._malloc(buffer.length);

// 填充數據

Module.HEAP8.set(buffer, offset);

// 最後調WASM的函數

let ptr = setFile(offset, buffer.length, +form.time.value * 1000);

調用malloc,傳需要的內存空間大小,然後會返回分配好的內存起始地址offset,這個offset其實就是HEAP8數組裡的index,然後調用Uint8Array的set方法填充數據。接著把這個offset的指針地址傳給setFile,並告知內存大小。這樣就實現了JS向WASM傳數據。

調用setFile之後返回值是一個指針地址,指向一個struct的數據結構:

typedef struct {

uint32_t width;

uint32_t height;

uint8_t *data;

} ImageData;

它的前4個字節,用來表示寬度,緊接著的4個字節是高度,後面的是圖片的rgb數據的指針,指針的大小也是4個字節,這個省略了數據長度,因為可以通過width * height * 3得到。

所以[ptr, ptr + 4)存的內容是寬度,[ptr + 4, ptr + 8)存的內容是長度,[ptr + 8, ptr + 12)存的內容是指向圖像數據的指針,如下代碼所示:

let ptr = setFile(offset, buffer.length, +form.time.value * 1000);

let width = Module.HEAPU32[ptr / 4]

height = Module.HEAPU32[ptr / 4 + 1],

imgBufferPtr = Module.HEAPU32[ptr / 4 + 2],

imageBuffer = Module.HEAPU8.subarray(imgBufferPtr,

imgBufferPtr + width * height * 3);

HEAPU32和上面的HEAP8是類似的,只不過它是每個32位就讀一個數,由於我們上面都是32位的數字,所以用這個剛剛好,它是4個字節一個單位,而ptr是一個字節一個單位,所以ptr / 4就得到index。這裡不用擔心不能夠被4整除,因為它是64位對齊的。

這樣我們就拿到圖片的rgb數據內容了,然後用canvas畫一下。

5. Canvas畫圖像

利用Canvas的ImageData類,如下代碼所示:

function drawImage(width, height, buffer) {

let imageData = ctx.createImageData(width, height);

let k = 0;

// 把buffer內存放到ImageData

for (let i = 0; i < buffer.length; i++) {

// 注意buffer數據是rgb的,而ImageData是rgba的

if (i && i % 3 === 0) {

imageData.data[k++] = 255;

}

imageData.data[k++] = buffer[i];

}

imageData.data[k] = 255;

memCanvas.width = width;

memCanvas.height = height;

canvas.height = canvas.width * height / width;

memContext.putImageData(imageData, 0, 0, 0, 0, width, height);

ctx.drawImage(memCanvas, 0, 0, width, height, 0, 0, canvas.width, canvas.height);

}

drawImage(width, height, imageBuffer);

這樣基本就完工了,但是還有一個很重要的事情要做,就是把申請的內存給釋放,不然反覆操作幾次之後,網頁的內存就飆到一兩個G,然後就拋內存不夠用異常了,所以在drawImage後之後把申請的內存釋放了:

drawImage(width, height, imageBuffer);

// 釋放內存

Module._free(offset);

Module._free(ptr);

Module._free(imgBufferPtr);

在C裡面寫的代碼也要釋放掉中間過程申請的內存,不然這個內存洩露還是挺厲害的。如果正確free之後,每次執行malloc的地址都是16358200,沒有free的話,每次都會重新擴容,返回遞增的offset地址。

但是這個東西整體消耗的內存還是比較大。

6. 存在的問題

初始化ffmpeg之後,網頁使用的內存就飆到500MB,如果選了一個300MB的文件處理,內存就會飆到1.3GB,因為在調setFile的時候需要malloc一個300MB大小的內存,然後在C代碼的setFile執行過程中又會malloc一個300MB大小的context變量,因為要處理mov/m4v格式的話為了獲取moov信息需要這麼大的,暫時沒優化,這幾個加起來就超過1GB了,並且WebAssembly.Memory只能grow,不能shrink,即只能往大擴,不能往小縮,擴充後的內存就一直在那裡了。而對於普通的mp4文件,context變量只需要1MB,這個可以把內存控制在1GB以內。

第二個問題是生成的wasm的文件比較大,原始有12.6MB,gzip之後還有5MB,如下圖所示:

wasm + ffmpeg實現前端截取視頻幀功能

因為ffmpeg本身比較大,如果能夠深入研究源碼,然後把一些沒用的功能disable掉或者不要include進來應該就可以給它瘦身,或者是隻提取有用的代碼,這個難度可能略高。

第三個問題是代碼的穩健性,除了想辦法把內存降下來,還需要考慮一些內存訪問越界的問題,因為有時候跑著跑著就拋了這個異常:

Uncaught RuntimeError: memory access out of bounds

雖然存在一些問題,但是起碼已經跑起來,可能暫時還不具備部署生產環境的價值,後面可以慢慢優化。

除了本文這個例子外,還可以利用ffmpeg實現其它一些功能,讓網頁也能夠直接處理多媒體。基本上只要ffmpeg能做的,在網頁也是能跑,並且wasm的性能要比直接跑JS的高。


分享到:


相關文章: