外貿領航
首頁外貿學堂 > WebAssembly教程「webenglish」

WebAssembly教程「webenglish」

來源:互聯網 2023-05-17 22:32:30

WebAssembly 入門:如何與 C/C 項目結合

什么是 WebAssembly?一種新型的代碼,可以運行在 Web 瀏覽器,提供一些新特性并主要專注于高性能主要不是用于寫,而是 C/C 、C#、Rust 等語言編譯的目標,所以你即使不知道如何編寫 WebAssembly 代碼也能利用它的優勢其他語言編寫的代碼也能以近似于原生速度運行,客戶端 App 也能在 Web 上運行在瀏覽器或 Node.js 中可以導入 WebAssembly 模塊,JS 框架能夠使用 WebAssembly 來獲得巨大的性能優勢和新的特性的同時在功能上易于使用WebAssembly 的目標快、高效、便利 -- 通過利用一些通用的硬件能力,能夠跨平臺以近乎于原生的速度執行可讀、可調試 -- WebAssembly 是一種低層次的匯編語言,但是它也有一種人類可讀的文本格式,使得人們可編寫代碼、查看代碼、可調試代碼。確保安全 -- WebAssembly 明確運行在安全、沙箱的執行環境,類似其他 Web 的代碼,它會強制開啟同源和一些權限策略。不破壞現有的 Web -- WebAssembly 被設計與其他 Web 技術兼容運行,并且保持向后兼容性。WebAssembly 如何與 Web 兼容的?

Web 平臺可以看做有兩個部分:

一個虛擬機(VM)用于運行 Web 應用代碼,例如 JS 引擎運行 JS 代碼一系列 Web API,Web 應用可以調用這些 API 來控制 Web 瀏覽器/設備 的功能,來做某些事情(DOM、CSSOM、WebGL、IndexedDB、Web Audio API 等)

長期以來,VM 只能加載 JS 運行,JS 可能足夠滿足我們的需求,但如今我們卻遇到了各種性能問題,如 3D 游戲、VR/AR、計算機視覺、圖片/視頻編輯、以及其他需要原生性能的領域。

同時,下載、解析和編譯大體積的 JS 應用是很困難的,在一些資源更加受限的平臺上,如移動設備等,則會更加放到這種性能瓶頸。

WebAssembly 是一種與 JavaScript 不同的語言,它不是為了替代 JS 而生的,而是被設計為與 JS 互為補充并能協作,使得 Web 開發者能夠重復利用兩種語言的優點:

JS 是高層次的語言,靈活且極具表現力,動態類型、不需要編譯步驟,并且有強大的生態,非常易于編寫 Web 應用。WebAssembly 是一種低層次、類匯編的語言,使用一種緊湊的二級制格式,能夠以近乎原生的性能運行,并提供了低層次的內存模型,是 C 、Rust 等語言的編譯目標,使得這類語言編寫的代碼能夠在 Web 上運行(需要注意的是,WebAssembly 將在未來提供垃圾回收的內存模型等高層次的目標)

隨著 WebAssembly 的出現,上述提到的 VM 現在可以加載兩種類型的代碼執行:JavaScript 和 WebAssembly。

JavaScript 和 WebAssembly 可以互操作,實際上一份 WebAssembly 代碼被稱為一個模塊,而 WebAssembly 的模塊與 ES2015 的模塊在具有很多共同的特性。

WebAssembly 的關鍵概念

為了理解 WebAssembly 是如何在 Web 運行的,需要了解幾個關鍵概念:

Module:通過瀏覽器編譯成為可執行機器碼的 WebAssembly 二進制文件,Module 是無狀態的,類似 Blob,能夠在 Window 和 Worker 之間通過 postMessage 共享,一個 Module 聲明了類似 ES2015 模塊類似的 import 和 export。Memory:一個可調整大小的 ArrayBuffer,其中包含由 WebAssembly 的低層次內存訪問指令讀取和寫入的線性字節數組。Table:一個可調整大小的類型化引用數組(如函數),然而處于安全和可移植性的原因,不能作為原始字節存儲在內存中Instance:一個包含它在運行時用到的所有狀態,包含 Memory、Table、以及一系列導入值的 Module,一個 Instance 類似一個 ES2015 的模塊,它被加載到具有特定導入集的特定全局變量中

WebAssembly 的 JavaScript API 提供給開發者創建 Module、Memory、Table 和 Instance 的能力,給定一個 WebAssembly 的 Instance,JS 代碼可以同步的調用它的 exports -- 被作為普通的 JavaScript 函數導出。任意 JavaScript 函數可以被 WebAssembly 代碼同步的調用,通過將 JavaScript 函數作為 imports 傳給 WebAssembly Instance。

因為 JavaScript 能夠完全控制 WebAssembly 代碼的下載、編譯和運行,所以 JavaScript 開發者可以認為 WebAssembly 只是 JavaScript 的一個新特性 -- 可以高效的生成高性能的函數。

在未來, WebAssembly 模塊可以以 ES2015 的模塊加載形式加載,如 <script type="module">,意味著 JS 可以獲取、編譯、和導入一個 WebAssembly 模塊,就像導入 ES2015 模塊一樣簡單。

如何在應用里使用 WebAssembly?

WebAssembly 給 Web 平臺添加了兩塊內容:一種二進制格式代碼,以及一系列可用于加載和執行二進制代碼的 API。

WebAssembly 目前處于一個萌芽的節點,之后肯定會涌現出很多工具,而目前有四個主要的入口:

使用 EMScripten 來移植 C/C 應用在匯編層面直接編寫和生成 WebAssembly 代碼編寫 Rust 應用,然后將 WebAssembly 作為它的輸出使用 AssemblyScript,它是一門類似 TypeScript 的語言,能夠編譯成 WebAssembly 二進制移植 C/C 應用

雖然也有一些其他工具如:

WasmFiddle[1]WasmFiddle [2]WasmExplorer[3]

但是這些工具都缺乏 EMScripten 的工具鏈和優化操作,EMScripten 的具體運行過程如下:

EMScripten 將 C/C 代碼喂給 Clang 編譯器(一個基于 LLVM 編譯架構的 C/C 編譯器),編譯成 LLVM IREMScripten 將 LLVM IR 轉換成 .wasm 的二進制字節碼WebAssembly 無法直接獲取到 DOM,只能調用 JS,傳入整形或浮點型的等原始數據類型,因此 WebAssembly 需要調用 JS 來獲取 Web API 和調用,EMScripten 則通過創建了 HTML 文件和 JS 膠水代碼來達到上述效果

未來 WebAssembly 也可以直接調用 Web API[4]。

上述的 JS 膠水代碼并不像想象中那么簡單,一開始,EMScripten 實現了一些流行的 C/C 庫,如 SDL、OpenGL、OpenAL、以及一部分 POSIX 庫,這些庫都是根據 Web API 來實現的,所以需要 JS 膠水代碼來幫助 WebAssembly 和底層的 Web API 進行交互。

所以,有部分膠水代碼實現了 C/C 代碼需要用到的對應的庫的功能,膠水代碼還同時包含調用上述 WebAssembly JavaScript API 的以獲取、加載和運行 .wasm 文件的邏輯。

生成的 HTML 文檔加載 JS 膠水代碼,然后將輸出寫入到 <textarea> 中去,如果應用使用到了 OpenGL,HTML 也包含 <canvas> 元素來作為渲染目標,你可以很方便的改寫 EMScripten 的輸出,將其轉換成 Web 應用需要的形式。

直接編寫 WebAssembly 代碼

如果你想構建自己的編譯器、工具鏈,或者能夠在運行時生成 WebAssembly 代碼的 JS 庫,你可以選擇手寫 WebAssembly 代碼。和物理匯編語言類似,WebAssembly 的二進制格式也有一種文本表示,你可以手動編寫或生成這種文本格式,并通過 WebAssembly 的文本到二進制(text-to-binary)的工具將文本轉為二進制格式。

編寫 Rust 代碼,并編譯為 WebAssembly

多謝 Rust WebAssembly 工作組的不懈努力,我們現在可以將 Rust 代碼編譯為 WebAssembly 代碼。

可以參考這個鏈接:https://developer.mozilla.org/en-US/docs/WebAssembly/Rust_to_wasm

使用 AssemblyScript

對于 Web 開發者來說,可是使用類 TypeScript 的形式來嘗試 WebAssembly 的編寫,而不需要學習 C 或 Rust 的細節,那么 AssemblyScript 將會是最好的選擇。AssemblyScript 將 TypeScript 的變體編譯為 WebAssembly,使得 Web 開發者可以使用 TypeScript 兼容的工具鏈,例如 Prettier、VSCode Intellisense,你可以查看它的文檔[5]來了解如何使用。

如何編譯將新寫 C/C 代碼編譯到 WebAssembly?

通過 EMScripten 工具,可將新寫的 C/C 代碼編譯為 WebAssembly 使用。

準備條件

為了能夠使用 Emscripten 工具,我們需要安裝它。首先 Clone 相關代碼:

git clone https: // github . com / emscripten-core / emsdk . gitcd emsdk

然后執行如下腳本來配置 emsdk:

# 如果之前 clone 過,那么這里更新最新的代碼git pull# 下載和安裝最新的 SDK 工具./emsdk install latest# 為當前的 user 激活最新的 SDK 工具,在 .emscripten 文件中寫入當前用戶./emsdk activate latest# 將 SDK 相關的命令加入到 PATH,以及激活其他環境變量source ./emsdk_env.sh

通過上面的操作我們就可以在命令行使用 Emscripten 相關的命令了,一般我們使用 Emscripten 時,主要有兩種場景:

編譯成 WASM 然后創建 HTML 文檔來運行代碼,結合 JavaScript 膠水代碼來在 Web 環境運行 wasm 代碼編譯成 wasm 代碼,只創建 JavaScript 文件生成 HTML 和 JavaScript

首先在 emsdk 目錄同級創建一個文件夾:WebAssembly ,然后在文件夾下創建一份 C 代碼:hello.c 如下:

#include <stdio.h>int main() { printf("Hello Worldn");}

然后在命令行中導航到此 hello.c 目錄下, 運行如下命令來調用 Emscripten 進行編譯:

emcc hello.c -s WASM=1 -o hello.html

上述命令解釋如下:

emcc 為 Emscripten 的命令行命令-s WASM=1 則告訴 Emscripten 需要輸出 wasm 文件,如果不指定這個參數,那么默認會輸出 asm.js-o hello.html 則告訴編譯器生成一個名為 hello.html 的 HTML 文檔來運行代碼,以及 wasm 模塊和對應的用于編譯和實例化 wasm 的 JavaScript 膠水代碼,以便 wasm 可以在 Web 環境中使用

運行如上命令之后,你的 WebAssembly 目錄下應該多出了三個文件:

二進制的 wasm 模塊代碼:hello.wasm包含膠水代碼的 JavaScript 文件:hello.js ,通過它將原生 C 函數翻譯成 JavaScript/wasm 代碼一個 HTML 文件:hello.html ,用于加載、編譯和實例化 wasm 的代碼,并將 wasm 代碼的輸出展示在瀏覽器上。運行代碼

目前剩下的工作為在支持 WebAssembly 的瀏覽器中加載 hello.html 運行。

在 Firefox 52 、Chrome 57 和最小的 Opera 瀏覽器中默認支持,也可以通過在 Firefox 47 中的 about:config 開啟 javascript.options.wasm 以及 Chrome 51 、Opera 38 中的 chrome://flags 來允許實驗性的 WebAssembly 特效支持。

因為現代瀏覽器不支持 file:// 形式的 XHR 請求,所以在 HTML 中無法加載 .wasm 等相關的文件,所以為了能夠看到效果,需要額外的本地服務器支持,可以通過運行如下命令:

npx serve .

npx 為 npm 在 5.2.0 之后推出的一個便捷執行 npm 命令的工具,如上述的 serve,在運行時首先檢測本地是否存在,如果不存在則下載原創對應的包,并執行對應的命令,并且為一次性的操作,免除了先安裝再允許,且需要暫用本地內存的操作。

在 WebAssembly 文件夾下運行一個本地 Web 服務器,然后打開 http://localhost:5000/hello.html 查看效果:

可以看到 我們在 C 代碼里面編寫的打印 Hello World 的代碼,成功輸出到了瀏覽器里,你也可以打開控制臺看到對應的輸出:

恭喜你!你成功將一個 C 模塊編譯成了 WebAssembly,并將其運行在了瀏覽器中!

使用自定義的 HTML 模板

上述例子中是使用了 Emscripten 默認的 HTML 模板,但是很多場景下我們都需要用到自定義的 HTML 模板,如將 WebAssembly 整合到現有的項目中使用時,就需要自定義 HTML 模板,接下來我們了解一下如何使用自定義的 HTML 模板。

首先在 WebAssembly 目錄下新建 hello2.c 文件,寫入如下內容:

#include <stdio.h>int main() { printf("Hello Worldn");}

在之前 clone 到本地的 emsdk 倉庫代碼中找到 shell_minimal.html 文件,將其復制到 WebAssembly 目錄下的子文件夾 html_template 下(此文件夾需要新建),現在 WebAssembly 目錄下的文件結構如下:

.├── hello.c├── hello.html├── hello.js├── hello.wasm├── hello2.c└── html_template └── shell_minimal.html

在命令行導航到 WebAssembly 下,運行如下命令:

emcc -o hello2.html hello2.c -O3 -s WASM=1 --shell-file html_template/shell_minimal.html

可以看到,相比之前在參數傳遞上有幾點變化:

通過設置 -o hello2.html ,編譯器將會將輸出 hello2.js 的 JS 膠水代碼以及 hello2.html 的 HTML 文件同時設置了 --shell-file html_template/shell_minimal.html ,通過這個命令提供了你在生成 HTML 文件時使用的 HTML 模板地址。

現在讓我們運行這個 HTML,通過如下命令:

npx serve .

在瀏覽器中導航到:localhosthttp://localhost:5000/hello2.html[6] 來訪問運行結果,可以觀測到和之前類似的效果:

可以看到只是缺少了之前的 Emscripten 頭部,其他都和之前類似,查看 WebAssembly 文件目錄,會發現生成了類似的 JS、Wasm 代碼:

.├── hello.c├── hello.html├── hello.js├── hello.wasm├── hello2.c├── hello2.html├── hello2.js├── hello2.wasm└── html_template └── shell_minimal.html

注意:你可以指定只輸出 JavaScript 膠水代碼,而不是一份完整的 HTML 文檔,通過在 -o 標簽后面指定為 .js 文件,例如 emcc -o hello2.js hello2.c -O3 -s WASM=1 ,然后你可以自定義 HTML 文件,然后導入這份膠水代碼使用,然而這是一種更加高級的方法,常用的形式還是使用提供的 HTML 模板:

Emscripten 需要大量的 JavaScript 膠水代碼來處理內存分配,內存泄露以及一系列其他問題。調用在 C 中自定義的函數

如果你在 C 代碼里定義了一個函數,然后想在 JavaScript 中調用它,你可以使用 Emscripten 的 ccall 函數,以及 EMSCRIPTEN_KEEPALIVE 聲明(這個聲明將你的 C 函數加入到函數輸出列表,具體的工作過程如下:

首先在 WebAssembly 目錄下創建 hello3.c 文件,添加如下內容:

#include <stdio.h>#include <emscripten/emscripten.h>int main() { printf("Hello Worldn");}#ifdef __cplusplusextern "C" {#endifEMSCRIPTEN_KEEPALIVE void myFunction(int argc, char ** argv) { printf("MyFunction Calledn");}#ifdef __cplusplus}#endif

Emscripten 生成的代碼默認只調用 main 函數,其他函數會作為 “死代碼” 刪除掉。在函數名之前加入 EMSCRIPTEN_KEEPALIVE 聲明會阻止這種 “刪除” 發生,你需要導入 emscripten.h 頭文件來使用 EMSCRIPTEN_KEEPALIVE 聲明。

注意我們在代碼中添加了 #ifdef 塊,確保在 C 代碼中導入這個使用時也是可以正確工作的,因為 C 和 C 的命名可能存在一些混淆的規則,所以上述添加 EMSCRIPTEN_KEEPALIVE 聲明的函數可能會失效,所以在 C 環境下為函數加上 external ,將其當做 external 函數,這樣在 C 環境下也可以正確工作。

然后為了演示方便, HTML 文件照樣使用我們之前放到 html_template 目錄下的 shell_minimal.html 文件,然后使用如下命令編譯 C 代碼:

emcc -o hello3.html hello3.c -O3 -s WASM=1 --shell-file html_template/shell_minimal.html -s NO_EXIT_RUNTIME=1 -s "EXTRA_EXPORTED_RUNTIME_METHODS=['ccall']"

注意到在上述編譯中,我們加上了 NO_EXIT_RUNTIME 參數,因為當 main 函數運行完之后,程序就會退出,所以加上這個參數確保其他函數還是還能如期運行。

而額外添加的 EXTRA_EXPORTED_RUNTIME_METHODS 則用于為 WebAssembly 的 Module 導出 ccall 方法使用,使得可以在 JavaScript 調用導出的 C 函數。

當你通過 npx serve . 運行時,依然可以看到類似之前的結果:

現在我們可以嘗試在 JavaScript使用 myFunction 函數,首先在編輯器中打開 hello3.html 文件,然后添加一個 <button> 元素,并在 <button> 元素點擊時能夠調用 myFunction 函數:

<!-- 其他內容 ---><button class="mybutton">Run myFunction</button><script type='text/javascript'>// ... 其他生成的代碼// script 標簽底部document.querySelector('.mybutton') .addEventListener('click', function() { alert('check console'); var result = Module.ccall( 'myFunction', // name of C function null, // return type null, // argument types null // arguments ); });</script><!-- 其他內容 --->

保存上述內容,重新刷新瀏覽器可以看到如下結果:

當我們點擊上圖中的按鈕時,可以獲得如下結果:

首先會收到一個 alert 提示,然后在輸出里面打印了 MyFunction Called 內容,表示 myFunction 調用了,打開控制臺也可以看到如下打印結果:

上述例子展示了可以在 JavaScript 中通過 ccall 來調用 C 代碼中導出的函數。

如何編譯已經存在的 C 模塊到 WebAssembly?

一個 WebAssembly 的核心使用場景就是將重復利用已經存在的 C 生態系統中的庫,并將它們編譯到 Web 平臺上使用而不用重新實現一套代碼。

這些 C 庫通常依賴 C 的標準庫,操作系統,文件系統或者其他依賴,Emscripten 提供絕大部分上述依賴的特性,盡管還是存在一些限制。

讓我們將 C 庫的 WebP 編碼器編譯到 wasm 來了解如何編譯已經存在的 C 模塊,WebP codec 的源碼是用 C 實現的,能夠在 Github[7] 上找到它,同時可以了解到它的一些 API 文檔[8]。

首先 Clone WebP 編碼器的源碼到本地,和 emsdk 、WebAssembly 目錄同級:

git clone https://github.com/webmproject/libwebp

為了快速上手,我們可以先導出 encode.h 頭文件里面的 WebPGetEncoderVersion 函數給到 JavaScript 使用,首先在 WebAssembly 文件夾下創建 webp.c 文件并加入如下:

#include "emscripten.h"#include "src/webp/encode.h"EMSCRIPTEN_KEEPALIVEint version() { return WebPGetEncoderVersion();}

上述的例子可以很快速的檢驗是否正確編譯了 libwebp 的源碼并能成功使用其函數,因為上述函數無需各種復雜的傳參和數據結構即可成功執行。

為了編譯上述函數,我們首先得告訴編譯器如何找到 libwebp 庫的頭文件,通過在編譯時加上標志 I ,然后指定 libwep 頭文件的地址來告訴編譯器地址,并將編譯器所需要的所有 libwebp 里面的 C 文件都傳給它。但有時候一個個列舉 C 文件非常的繁瑣,所以一種有效的策略就是將所有的 C 文件都傳給編譯器,然后依賴編譯器自身去過濾掉那些不必要的文件,上述描述的操作可以通過在命令行編寫如下命令實現:

emcc -O3 -s WASM=1 -s EXTRA_EXPORTED_RUNTIME_METHODS='["cwrap"]' -I libwebp WebAssembly/webp.c libwebp/src/{dec,dsp,demux,enc,mux,utils}/*.c

注意:上述的傳參策略并不對在所有 C 項目都生效,有很多項目在編譯前依賴 autoconfig/automake 等庫來生成系統特定的代碼,而 Emscripten 提供了 emconfigure 和 emmake 來封裝這些命令,并注入合適的參數來抹平那些有前置依賴的項目。

運行上述命令之后,會產出一份 a.out.js 膠水代碼,和 a.out.wasm 文件,然后你需要在 a.out.js 文件輸出的目錄下創建一份 HTML 文件,并在其中添加如下代碼

<script src="./a.out.js"></script><script> Module.onRuntimeInitialized = async _ => { const api = { version: Module.cwrap('version', 'number', []), }; console.log(api.version()); };</script>

上述代碼中,我們首先導入編譯器編譯輸出的 a.out.js 膠水代碼,然后在 WebAssembly 的模塊初始化好了之后,通過 cwrap 函數導出 C 函數 version 使用,通過運行和之前類似的 npx serve . 命令,然后打開瀏覽器可以看到如下效果:

libwebp 通過十六進制的 0xabc 的 abc 來表示當前版本 a.b.c ,例如 v0.6.1,則會被編碼成十六進制 0x000601 ,對應的十進制為 1537。而這里為十進制 66049,轉成 16 進制則為 0x010201 ,表示當前版本為 v1.2.1。

在 JavaScript 中獲取圖片并放入 wasm 中運行

剛剛通過調用編碼器的 WebPGetEncoderVersion 方法來獲取版本號來證實了已經成功編譯了 libwebp 庫到 wasm,然后可以在 JavaScript 使用它,接下來我們將了解更加復雜的操作,如何使用 libwebp 的編碼 API 來轉換圖片格式。

libwebp 的 encoding API 需要接收一個關于 RGB、RGBA、BGR 或 BGRA 的字節數組,所以首先要回答的問題是,如何將圖片放入 wasm 運行?幸運的是,Canvas API 有一個 CanvasRenderingContext2D.getImageData 方法,能夠返回一個 Uint8ClampedArray ,這個數組包含 RGBA 格式的圖片數據。

首先我們需要在 JavaScript 中編寫加載圖片的函數,將其寫到上一步創建的 HTML 文件里:

<script src="./a.out.js"></script><script> Module.onRuntimeInitialized = async _ => { const api = { version: Module.cwrap('version', 'number', []), }; console.log(api.version()); }; async function loadImage(src) { // 加載圖片 const imgBlob = await fetch(src).then(resp => resp.blob()); const img = await createImageBitmap(imgBlob); // 設置 canvas 畫布的大小與圖片一致 const canvas = document.createElement('canvas'); canvas.width = img.width; canvas.height = img.height; // 將圖片繪制到 canvas 上 const ctx = canvas.getContext('2d'); ctx.drawImage(img, 0, 0); return ctx.getImageData(0, 0, img.width, img.height); }</script>

現在剩下的操作則是如何將圖片數據從 JavaScript 復制到 wasm,為了達成這個目的,需要在先前的 webp.c 函數里面暴露額外的方法:

一個為 wasm 里面的圖片分配內存的方法一個釋放內存的方法

修改 webp.c 如下:

#include <stdlib.h> // 此頭文件導入用于分配內存的 malloc 方法和釋放內存的 free 方法EMSCRIPTEN_KEEPALIVEuint8_t* create_buffer(int width, int height) { return malloc(width * height * 4 * sizeof(uint8_t));}EMSCRIPTEN_KEEPALIVEvoid destroy_buffer(uint8_t* p) { free(p);}

create_buffer 為 RGBA 的圖片分配內存,RGBA 圖片一個像素包含 4 個字節,所以代碼中需要添加 4 * sizeof(uint8_t) ,malloc 函數返回的指針指向所分配內存的第一塊內存單元地址,當這個指針返回給 JavaScript 使用時,會被當做一個簡單的數字處理。當通過 cwrap 函數獲取暴露給 JavaScript 的對應 C 函數時,可以使用這個指針數字找到復制圖片數據的內存開始位置。

我們在 HTML 文件中添加額外的代碼如下:

<script src="./a.out.js"></script><script> Module.onRuntimeInitialized = async _ => { const api = { version: Module.cwrap('version', 'number', []), create_buffer: Module.cwrap('create_buffer', 'number', ['number', 'number']), destroy_buffer: Module.cwrap('destroy_buffer', '', ['number']), encode: Module.cwrap("encode", "", ["number","number","number","number",]), free_result: Module.cwrap("free_result", "", ["number"]), get_result_pointer: Module.cwrap("get_result_pointer", "number", []), get_result_size: Module.cwrap("get_result_size", "number", []), }; const image = await loadImage('./image.jpg'); const p = api.create_buffer(image.width, image.height); Module.HEAP8.set(image.data, p); // ... call encoder ... api.destroy_buffer(p); }; async function loadImage(src) { // 加載圖片 const imgBlob = await fetch(src).then(resp => resp.blob()); const img = await createImageBitmap(imgBlob); // 設置 canvas 畫布的大小與圖片一致 const canvas = document.createElement('canvas'); canvas.width = img.width; canvas.height = img.height; // 將圖片繪制到 canvas 上 const ctx = canvas.getContext('2d'); ctx.drawImage(img, 0, 0); return ctx.getImageData(0, 0, img.width, img.height); }</script>

可以看到上述代碼除了導入之前添加的 create_buffer 和 destroy_buffer 外,還有很多用于編碼文件等方面的函數,我們將在后續講解,除此之外,代碼首先加載了一份 image.jpg 的圖片,然后調用 C 函數為此圖片數據分配內存,并相應的拿到返回的指針傳給 WebAssembly 的 Module.HEAP8 ,在內存開始位置 p,寫入圖片的數據,最后會釋放分配的內存。

編碼圖片

現在圖片數據已經加載進 wasm 的內存中,可以調用 libwebp 的 encoder 方法來完成編碼過程了,通過查閱 WebP 的文檔[9],發現可以使用 WebPEncodeRGBA 函數來完成工作。這個函數接收一個指向圖片數據的指針以及它的尺寸,以及一個區間在 0-100 的可選的質量參數。在編碼的過程中,WebPEncodeRGBA 會分配一塊用于輸出數據的內存,我們需要在編碼完成之后調用 WebPFree 來釋放這塊內存。

我們打開 webp.c 文件,添加如下處理編碼的代碼:

int result[2];EMSCRIPTEN_KEEPALIVEvoid encode(uint8_t* img_in, int width, int height, float quality) { uint8_t* img_out; size_t size; size = WebPEncodeRGBA(img_in, width, height, width * 4, quality, &img_out); result[0] = (int)img_out; result[1] = size;}EMSCRIPTEN_KEEPALIVEvoid free_result(uint8_t* result) { WebPFree(result);}EMSCRIPTEN_KEEPALIVEint get_result_pointer() { return result[0];}EMSCRIPTEN_KEEPALIVEint get_result_size() { return result[1];}

上述 WebPEncodeRGBA 函數執行的結果為分配一塊輸出數據的內存以及返回內存的大小。因為 C 函數無法使用數組作為返回值(除非我們需要進行動態內存分配),所以我們使用一個全局靜態數組來獲取返回的結果,這可能不是很規范的 C 代碼寫法,同時它要求 wasm 指針為 32 比特長,但是為了簡單起見我們可以暫時容忍這種做法。

現在 C 側的相關邏輯已經編寫完畢,可以在 JavaScript 側調用編碼函數,獲取圖片數據的指針和圖片所占用的內存大小,將這份數據保存到 JavaScript 自己的內存中,然后釋放 wasm 在處理圖片時所分配的內存,讓我們打開 HTML 文件完成上述描述的邏輯:

<script src="./a.out.js"></script><script> Module.onRuntimeInitialized = async _ => { const api = { version: Module.cwrap('version', 'number', []), create_buffer: Module.cwrap('create_buffer', 'number', ['number', 'number']), destroy_buffer: Module.cwrap('destroy_buffer', '', ['number']), encode: Module.cwrap("encode", "", ["number","number","number","number",]), free_result: Module.cwrap("free_result", "", ["number"]), get_result_pointer: Module.cwrap("get_result_pointer", "number", []), get_result_size: Module.cwrap("get_result_size", "number", []), }; const image = await loadImage('./image.jpg'); const p = api.create_buffer(image.width, image.height); Module.HEAP8.set(image.data, p); api.encode(p, image.width, image.height, 100); const resultPointer = api.get_result_pointer(); const resultSize = api.get_result_size(); const resultView = new Uint8Array(Module.HEAP8.buffer, resultPointer, resultSize); const result = new Uint8Array(resultView); api.free_result(resultPointer); api.destroy_buffer(p); }; async function loadImage(src) { // 加載圖片 const imgBlob = await fetch(src).then(resp => resp.blob()); const img = await createImageBitmap(imgBlob); // 設置 canvas 畫布的大小與圖片一致 const canvas = document.createElement('canvas'); canvas.width = img.width; canvas.height = img.height; // 將圖片繪制到 canvas 上 const ctx = canvas.getContext('2d'); ctx.drawImage(img, 0, 0); return ctx.getImageData(0, 0, img.width, img.height); }</script>

在上述代碼中我們通過 loadImage 函數加載了一張本地的 image.jpg 圖片,你需要事先準備一張圖片放置在 emcc 編譯器輸出的目錄下,也就是我們的 HTML 文件目錄下使用。

注意:new Uint8Array(someBuffer) 將會在同樣的內存塊上創建一個新視圖,而 new Uint8Array(someTypedArray) 只會復制 someTypedArray 的數據。

當你的圖片比較大時,因為 wasm 不能擴充可以容納 input 和 output 圖片數據的內存,你可能會遇到如下報錯:

但是我們例子中使用的圖片比較小,所以只需要單純的在編譯時加上一個過濾參數 -s ALLOW_MEMORY_GROWTH=1 忽略這個報錯信息即可:

emcc -O3 -s WASM=1 -s EXTRA_EXPORTED_RUNTIME_METHODS='["cwrap"]' -I libwebp test-dir/webp.c libwebp/src/{dec,dsp,demux,enc,mux,utils}/*.c -s ALLOW_MEMORY_GROWTH=1

再次運行上述命令,得到添加了編碼函數的 wasm 代碼和對應的 JavaScript 膠水代碼,這樣當我們打開 HTML 文件時,它已經能夠將一份 JPG 文件編碼成 WebP 的格式,為了近一步證實這個觀點,我們可以將圖片展示到 Web 界面上,通過修改 HTML 文件,添加如下代碼:

<script> // ... api.encode(p, image.width, image.height, 100); const resultPointer = api.get_result_pointer(); const resultSize = api.get_result_size(); const resultView = new Uint8Array(Module.HEAP8.buffer, resultPointer, resultSize); const result = new Uint8Array(resultView); // 添加到這里 const blob = new Blob([result], {type: 'image/webp'}); const blobURL = URL.createObjectURL(blob); const img = document.createElement('img'); img.src = blobURL; document.body.appendChild(img) api.free_result(resultPointer); api.destroy_buffer(p);</script>

然后刷新瀏覽器,應該可以看到WebP圖片展示到 Web 端,通過將這個文件下載到本地,可以看到其格式轉成了 WebP:

通過上述的流程我們成功編譯了現有的 libwebp C 庫到 wasm 使用,并將 JPG 圖片轉成了 WebP 格式并展示在 Web 界面上,通過 wasm 來處理計算密集型的轉碼操作可以大大提高網頁的性能,這也是 WebAssembly 帶來的主要優勢之一。

如何編譯 FFmpeg 到 WebAssembly?emconfigure is to replace the compiler from gcc to emcc (or g to em ):編譯 C 項目Make generates wasm object files:生成 wasm 對象 .o 文件

在第二個例子中我們成功編譯了已經存在的 C 模塊到 WebAssembly,但是有很多項目在編譯前依賴 autoconfig/automake 等庫來生成系統特定的代碼,而 Emscripten 提供了 emconfigure 和 emmake 來封裝這些命令,并注入合適的參數來抹平那些有前置依賴的項目,接下來我們通過實際編譯 ffmpeg 來講解如何處理這種依賴 autoconfig/automake 等庫來生成特定的代碼。

經過實踐發現 ffmpeg 的編譯依賴于特定的 ffmpeg 版本、Emscripten 版本、操作系統環境等,所以以下的 ffmpeg 的編譯都是限制在特定的條件下進行的,主要是為之后通用的 ffmpeg 的編譯提供一種思路和調試方法。

編譯步驟

使用 Emscripten 編譯大部分復雜的 C/C 庫時,主要需要三個步驟:

使用 emconfigure 運行項目的 configure 文件將 C/C 代碼編譯器從 gcc/g 換成 emcc/em 通過 emmake make 來構建 C/C 項目,生成 wasm 對象的 .o 文件為了生成特定形式的輸出,手動調用 emcc 來編譯特定的文件安裝特定依賴

為了驗證 ffmpeg 的驗證,我們需要依賴特定的版本,下面詳細講解依賴的各種文件版本。

首先安裝 1.39.18 版本的 Emscripten 編譯器,進入之前我們 Clone 到本地的 emsdk 項目運行如下命令:

./emsdk install 1.39.18./emsdk activate 1.39.18source ./emsdk_env.sh

通過在命令行中輸入如下命令驗證是否切換成功:

emcc -v # 輸出 1.39.18

在 emsdk 同級下載分支為 n4.3.1 的 ffmpeg 代碼:

git clone --depth 1 --branch n4.3.1 https://github.com/FFmpeg/FFmpeg使用 emconfigure 處理 configure 文件

通過如下腳本來處理 configure 文件:

export CFLAGS="-s USE_PTHREADS -O3"export LDFLAGS="$CFLAGS -s INITIAL_MEMORY=33554432"emconfigure ./configure --target-os=none # 設置為 none 來去除特定操作系統的一些依賴 --arch=x86_32 # 選中架構為 x86_32 --enable-cross-compile # 處理跨平臺操作 --disable-x86asm # 關閉 x86asm --disable-inline-asm # 關閉內聯的 asm --disable-stripping # 關閉處理 strip 的功能,避免誤刪一些內容 --disable-programs # 加速編譯 --disable-doc # 添加一些 flag 輸出 --extra-cflags="$CFLAGS" --extra-cxxflags="$CFLAGS" --extra-ldflags="$LDFLAGS" --nm="llvm-nm" # 使用 llvm 的編譯器 --ar=emar --ranlib=emranlib --cc=emcc # 將 gcc 替換為 emcc --cxx=em # 將 g 替換為 em --objcc=emcc --dep-cc=emcc

上述腳本主要做了如下幾件事:

USE_PTHREADS 開啟 pthreads 支持-O3 表示在編譯時優化代碼體積,一般可以從 30MB 壓縮到 15MBINITIAL_MEMORY 設置為 33554432 (32MB),主要是 Emscripten 可能占用 19MB,所以設置更大的內存容量來避免在編譯過程中可分配的內存不足的問題實際使用 emconfigure 來配置 configure 文件,替換 gcc 編譯器為 emcc ,以及設置一些必要的操作來處理可能遇到的編譯 BUG,最終生成用于編譯構建的配置文件使用 emmake make 來構建依賴

通過上述步驟,就處理好了配置文件,接下來需要通過 emmake 來構建實際的依賴,通過在命令行中運行如下命令:

# 構建最終的 ffmpeg.wasm 文件emmake make -j4

通過上述的編譯,會生成如下四個文件:

ffmpegffmpeg_gffmpeg_g.wasmffmpeg_g.worker.js

前兩個都是 JS 文件,第三個為 wasm 模塊,第四個是處理 worker 中運行相關邏輯的函數,上述生成的文件的理想形式應該為三個,為了達成這種自定義的編譯,有必要自定義使用 emcc 命令來進行處理。

使用 emcc 個性化編譯

在 FFmpeg 目錄下創建 wasm 文件夾,用于放置構建之后的文件,然后自定義編譯文件輸出如下:

mkdir -p wasm/distemcc -I. -I./fftools -Llibavcodec -Llibavdevice -Llibavfilter -Llibavformat -Llibavresample -Llibavutil -Llibpostproc -Llibswscale -Llibswresample -Qunused-arguments -o wasm/dist/ffmpeg-core.js fftools/ffmpeg_opt.c fftools/ffmpeg_filter.c fftools/ffmpeg_hw.c fftools/cmdutils.c fftools/ffmpeg.c -lavdevice -lavfilter -lavformat -lavcodec -lswresample -lswscale -lavutil -lm -O3 -s USE_SDL=2 # 使用 SDL2 -s USE_PTHREADS=1 -s PROXY_TO_PTHREAD=1 # 將 main 函數與瀏覽器/UI主線程分離 -s INVOKE_RUN=0 # 執行 C 函數時不首先執行 main 函數 -s EXPORTED_FUNCTIONS="[_main, _proxy_main]" -s EXTRA_EXPORTED_RUNTIME_METHODS="[FS, cwrap, setValue, writeAsciiToMemory]" -s INITIAL_MEMORY=33554432

上述的腳本主要有如下幾點改進:

-s PROXY_TO_PTHREAD=1 在編譯時設置了 pthread 時,使得程序具備響應式特效-o wasm/dist/ffmpeg-core.js 則將原 ffmpeg js 文件的輸出重命名為 ffmpeg-core.js ,對應的輸出 ffmpeg-core.wasm 和 ffmpeg-core.worker.js-s EXPORTED_FUNCTIONS="[_main, _proxy_main]" 導出 ffmpeg 對應的 C 文件里的 main 函數,proxy_main 則是通過設置 PROXY_TO_PTHREAD代理 main 函數用于外部使用-s EXTRA_EXPORTED_RUNTIME_METHODS="[FS, cwrap, setValue, writeAsciiToMemory]" 則是導出一些幫助函數,用于導出 C 函數、處理文件系統、指針的操作

通過上述編譯命令最終輸出下面三個文件:

ffmpeg-core.jsffmpeg-core.wasmffmpeg-core.worker.js使用編譯完成的 ffmpeg wasm 模塊

在 wasm 目錄下創建 ffmpeg.js 文件,在其中寫入如下代碼:

const Module = require('./dist/ffmpeg-core.js');Module.onRuntimeInitialized = () => { const ffmpeg = Module.cwrap('proxy_main', 'number', ['number', 'number']);};

然后通過如下命令運行上述代碼:

node --experimental-wasm-threads --experimental-wasm-bulk-memory ffmpeg.js

上述代碼解釋如下:

onRuntimeInitialized 是加載 WebAssembly 模塊完成之后執行的邏輯,我們所有相關邏輯需要在這個函數中編寫cwrap 則用于導出 C 文件中(fftools/ffmpeg.c )的 proxy_main 使用,函數的簽名為 int main(int argc, char **argv) ,其中 int 對應到 JavaScript 就是 number ,而 char **argv 是 C 中的指針,也可以映射到 number接著處理 ffmpeg 的傳參兼容邏輯,對于命令行中運行 ffmpeg -hide_banner ,在我們代碼里通過函數調用需要 main(2, ["./ffmpeg", "-hide_banner"]) ,第一個參數很好解決,那么我們如何傳遞一個字符串數組呢?這個問題可以分解為兩個部分: 我們需要將 JavaScript 的字符串轉換成 C 中的字符數組我們需要將 JavaScript 中的數字數組轉換為 C 中的指針數組

第一部分很簡單,因為 Emscripten 提供了一個輔助函數 writeAsciiToMemory 來完成這一工作:

const str = "FFmpeg.wasm";const buf = Module._malloc(str.length 1); // 額外分配一個字節的空間來存放 0 表示字符串的結束Module.writeAsciiToMemory(str, buf);

第二部分有一點困難,我們需要創建 C 中的 32 位整數的指針數組,可以借助 setValue 來幫助我們創建這個數組:

const ptrs = [123, 3455];const buf = Module._malloc(ptrs.length * Uint32Array.BYTES_PER_ELEMENT);ptrs.forEach((p, idx) => { Module.setValue(buf (Uint32Array.BYTES_PER_ELEMENT * idx), p, 'i32');});

將上述的代碼合并起來,我們就可以獲取一個能與 ffmpeg 交互的程序:

const Module = require('./dist/ffmpeg-core');Module.onRuntimeInitialized = () => { const ffmpeg = Module.cwrap('proxy_main', 'number', ['number', 'number']); const args = ['ffmpeg', '-hide_banner']; const argsPtr = Module._malloc(args.length * Uint32Array.BYTES_PER_ELEMENT); args.forEach((s, idx) => { const buf = Module._malloc(s.length 1); Module.writeAsciiToMemory(s, buf); Module.setValue(argsPtr (Uint32Array.BYTES_PER_ELEMENT * idx), buf, 'i32'); }) ffmpeg(args.length, argsPtr);};

然后通過同樣的命令運行程序:

node --experimental-wasm-threads --experimental-wasm-bulk-memory ffmpeg.js

上述運行的結果如下:

可以看到我們成功編譯并運行了 ffmpeg 。

處理 Emscripten 文件系統

Emscripten 內建了一個虛擬的文件系統來支持 C 中標準的文件讀取和寫入,所以我們需要將音頻文件傳給 ffmpeg.wasm 時先寫入到文件系統中。

可以戳此查看更多關于文件系統 API[10] 。

為了完成上述的任務,只需要使用到 FS 模塊的兩個函數 FS.writeFile() 和 FS.readFile() ,對于從文件系統中讀取和寫入的所有數據都要求是 JavaScript 中的 Uint8Array 類型,所以在消費數據之前有必要約定數據類型。

我們將通過 fs.readFileSync() 方法讀取名為 flame.avi 的視頻文件,然后使用 FS.writeFile() 將其寫入到 Emscripten 文件系統。

const fs = require('fs');const Module = require('./dist/ffmpeg-core');Module.onRuntimeInitialized = () => { const data = Uint8Array.from(fs.readFileSync('./flame.avi')); Module.FS.writeFile('flame.avi', data); const ffmpeg = Module.cwrap('proxy_main', 'number', ['number', 'number']); const args = ['ffmpeg', '-hide_banner']; const argsPtr = Module._malloc(args.length * Uint32Array.BYTES_PER_ELEMENT); args.forEach((s, idx) => { const buf = Module._malloc(s.length 1); Module.writeAsciiToMemory(s, buf); Module.setValue(argsPtr (Uint32Array.BYTES_PER_ELEMENT * idx), buf, 'i32'); }) ffmpeg(args.length, argsPtr);};使用 ffmpeg.wasm 編譯視頻

現在我們已經可以將視頻文件保存到 Emscripten 文件系統了,接下來就是實際使用編譯好的 ffmepg 來進行視頻的轉碼了。

我們修改代碼如下:

const fs = require('fs');const Module = require('./dist/ffmpeg-core');Module.onRuntimeInitialized = () => { const data = Uint8Array.from(fs.readFileSync('./flame.avi')); Module.FS.writeFile('flame.avi', data); const ffmpeg = Module.cwrap('proxy_main', 'number', ['number', 'number']); const args = ['ffmpeg', '-hide_banner', '-report', '-i', 'flame.avi', 'flame.mp4']; const argsPtr = Module._malloc(args.length * Uint32Array.BYTES_PER_ELEMENT); args.forEach((s, idx) => { const buf = Module._malloc(s.length 1); Module.writeAsciiToMemory(s, buf); Module.setValue(argsPtr (Uint32Array.BYTES_PER_ELEMENT * idx), buf, 'i32'); }); ffmpeg(args.length, argsPtr); const timer = setInterval(() => { const logFileName = Module.FS.readdir('.').find(name => name.endsWith('.log')); if (typeof logFileName !== 'undefined') { const log = String.fromCharCode.apply(null, Module.FS.readFile(logFileName)); if (log.includes("frames successfully decoded")) { clearInterval(timer); const output = Module.FS.readFile('flame.mp4'); fs.writeFileSync('flame.mp4', output); } } }, 500);};

在上述代碼中,我們添加了一個定時器,因為 ffmpeg 轉碼視頻的過程是異步的,所以我們需要不斷的去讀取 Emscripten 文件系統中是否有轉碼好的文件標志,當拿到文件標志且不為 undefined,我們就使用 Module.FS.readFile() 方法從 Emscripten 文件系統中讀取轉碼好的視頻文件,然后通過 fs.writeFileSync() 將視頻寫入到本地文件系統。最終我們會收到如下結果:

在瀏覽器中使用 ffmpeg 轉碼視頻并播放

在上一步中,我們成功在 Node 端使用了編譯好的 ffmpeg 完成從了 avi 格式到 mp4 格式的轉碼,接下來我們將在瀏覽器中使用 ffmpeg 轉碼視頻,并在瀏覽器中播放。

之前我們編譯的 ffmpeg 雖然可以將 avi 格式轉碼到 mp4 ,但是 mp4 的文件無法直接在瀏覽器中播放,因為不支持這種編碼,所以我們需要使用 libx264 編碼器來將 mp4 文件編碼成瀏覽器可播放的編碼格式。

首先在 WebAssembly 目錄下下載 x264 的編碼器源碼:

curl -OL https://download.videolan.org/pub/videolan/x264/snapshots/x264-snapshot-20170226-2245-stable.tar.bz2ttar xvfj x264-snapshot-20170226-2245-stable.tar.bz2

然后進入 x264 的文件夾,可以創建一個 build.sh 文件,并加入如下內容:

#!/bin/bash -xROOT=$PWDBUILD_DIR=$ROOT/buildcd $ROOT/x264-snapshot-20170226-2245-stableARGS=( --prefix=$BUILD_DIR --host=i686-gnu # use i686 gnu --enable-static # enable building static library --disable-cli # disable cli tools --disable-asm # disable asm optimization --extra-cflags="-s USE_PTHREADS=1" # pass this flags for using pthreads)emconfigure ./configure "${ARGS[@]}"emmake make install-lib-static -j4cd -

注意需要在 WebAssembly 目錄下運行如下命令來構建 x264:

bash x264-snapshot-20170226-2245-stable/build-x264.sh

安裝了 x264 編碼器之后,就可以在 ffmpeg 的編譯腳本中加入打開 x264 的開關,這一次我們在 ffmpeg 文件夾下創建 Bash 腳本用于構建,創建 configure.sh 如下:

#!/bin/bash -xemcc -vROOT=$PWDBUILD_DIR=$ROOT/buildcd $ROOT/ffmpeg-4.3.2-3CFLAGS="-s USE_PTHREADS -I$BUILD_DIR/include"LDFLAGS="$CFLAGS -L$BUILD_DIR/lib -s INITIAL_MEMORY=33554432" # 33554432 bytes = 32 MBCONFIG_ARGS=( --target-os=none # use none to prevent any os specific configurations --arch=x86_32 # use x86_32 to achieve minimal architectural optimization --enable-cross-compile # enable cross compile --disable-x86asm # disable x86 asm --disable-inline-asm # disable inline asm --disable-stripping --disable-programs # disable programs build (incl. ffplay, ffprobe & ffmpeg) --disable-doc # disable doc --enable-gpl ## required by x264 --enable-libx264 ## enable x264 --extra-cflags="$CFLAGS" --extra-cxxflags="$CFLAGS" --extra-ldflags="$LDFLAGS" --nm="llvm-nm" --ar=emar --ranlib=emranlib --cc=emcc --cxx=em --objcc=emcc --dep-cc=emcc )emconfigure ./configure "${CONFIG_ARGS[@]}" # build ffmpeg.wasmemmake make -j4cd -

然后創建用于自定義輸出構建文件的腳本文件 build-ffmpeg.sh :

ROOT=$PWDBUILD_DIR=$ROOT/buildcd ffmpeg-4.3.2-3ARGS=( -I. -I./fftools -I$BUILD_DIR/include -Llibavcodec -Llibavdevice -Llibavfilter -Llibavformat -Llibavresample -Llibavutil -Llibpostproc -Llibswscale -Llibswresample -L$BUILD_DIR/lib -Qunused-arguments # 這一行加入 -lpostproc 和 -lx264,添加加入 x264 的編譯 -o wasm/dist/ffmpeg-core.js fftools/ffmpeg_opt.c fftools/ffmpeg_filter.c fftools/ffmpeg_hw.c fftools/cmdutils.c fftools/ffmpeg.c -lavdevice -lavfilter -lavformat -lavcodec -lswresample -lswscale -lavutil -lpostproc -lm -lx264 -pthread -O3 # Optimize code with performance first -s USE_SDL=2 # use SDL2 -s USE_PTHREADS=1 # enable pthreads support -s PROXY_TO_PTHREAD=1 # detach main() from browser/UI main thread -s INVOKE_RUN=0 # not to run the main() in the beginning -s EXPORTED_FUNCTIONS="[_main, _proxy_main]" # export main and proxy_main funcs -s EXTRA_EXPORTED_RUNTIME_METHODS="[FS, cwrap, setValue, writeAsciiToMemory]" # export preamble funcs -s INITIAL_MEMORY=268435456 # 268435456 bytes = 268435456 MB)emcc "${ARGS[@]}"cd -實際使用 ffmpeg 轉碼

我們將創建一個 Web 網頁,然后提供一個上傳視頻文件的按鈕,以及播放上傳的視頻文件。盡管無法直接在 Web 端播放 avi 格式的視頻文件,但是我們可以通過 ffmpeg 轉碼之后播放。

在 ffmpeg 目錄下的 wasm 文件夾下創建 index.html 文件,然后添加如下內容:

<html> <head> <style> html, body { margin: 0; width: 100%; height: 100% } body { display: flex; flex-direction: column; align-items: center; } </style> </head> <body> <h3>上傳視頻文件,然后轉碼到 mp4 (x264) 進行播放!</h3> <video id="output-video" controls></video><br/> <input type="file" id="uploader"> <p id="message">ffmpeg 腳本需要等待 5S 左右加載完成</p> <script type="text/javascript"> const readFromBlobOrFile = (blob) => ( new Promise((resolve, reject) => { const fileReader = new FileReader(); fileReader.onload = () => { resolve(fileReader.result); }; fileReader.onerror = ({ target: { error: { code } } }) => { reject(Error(`File could not be read! Code=${code}`)); }; fileReader.readAsArrayBuffer(blob); }) ); const message = document.getElementById('message'); const transcode = async ({ target: { files } }) => { const { name } = files[0]; message.innerHTML = '將文件寫入到 Emscripten 文件系統'; const data = await readFromBlobOrFile(files[0]); Module.FS.writeFile(name, new Uint8Array(data)); const ffmpeg = Module.cwrap('proxy_main', 'number', ['number', 'number']); const args = ['ffmpeg', '-hide_banner', '-nostdin', '-report', '-i', name, 'out.mp4']; const argsPtr = Module._malloc(args.length * Uint32Array.BYTES_PER_ELEMENT); args.forEach((s, idx) => { const buf = Module._malloc(s.length 1); Module.writeAsciiToMemory(s, buf); Module.setValue(argsPtr (Uint32Array.BYTES_PER_ELEMENT * idx), buf, 'i32'); }); message.innerHTML = '開始轉碼'; ffmpeg(args.length, argsPtr); const timer = setInterval(() => { const logFileName = Module.FS.readdir('.').find(name => name.endsWith('.log')); if (typeof logFileName !== 'undefined') { const log = String.fromCharCode.apply(null, Module.FS.readFile(logFileName)); if (log.includes("frames successfully decoded")) { clearInterval(timer); message.innerHTML = '完成轉碼'; const out = Module.FS.readFile('out.mp4'); const video = document.getElementById('output-video'); video.src = URL.createObjectURL(new Blob([out.buffer], { type: 'video/mp4' })); } } }, 500); }; document.getElementById('uploader').addEventListener('change', transcode); </script> <script type="text/javascript" src="./dist/ffmpeg-core.js"></script> </body> </html>

打開上述網頁運行,我們可以看到如下效果:

恭喜你!成功編譯 ffmpeg 并在 Web 端使用。

參考https://www.ruanyifeng.com/blog/2017/09/asmjs_emscripten.htmlhttps://pspdfkit.com/blog/2017/webassembly-a-new-hope/https://hacks.mozilla.org/2017/02/what-makes-webassembly-fast/https://www.sitepoint.com/understanding-asm-js/http://www.cmake.org/download/https://developer.mozilla.org/en-US/docs/WebAssembly/existing_C_to_wasmhttps://research.mozilla.org/webassembly/https://itnext.io/build-ffmpeg-webassembly-version-ffmpeg-js-part-2-compile-with-emscripten-4c581e8c9a16?gi=e525b34f2c21https://dev.to/alfg/ffmpeg-webassembly-2cblhttps://gist.github.com/rinthel/f4df3023245dd3e5a27218e8b3d79926https://github.com/Kagami/ffmpeg.js/https://qdmana.com/2021/04/20210401214625324n.htmlhttps://github.com/leandromoreira/ffmpeg-libav-tutorialhttp://ffmpeg.org/doxygen/4.1/examples.htmlhttps://github.com/alfg/ffmpeg-webassembly-examplehttps://github.com/alfg/ffprobe-wasmhttps://gist.github.com/rinthel/f4df3023245dd3e5a27218e8b3d79926#file-ffmpeg-emscripten-build-shhttps://emscripten.org/docs/compiling/Building-Projects.html#integrating-with-a-build-systemhttps://itnext.io/build-ffmpeg-webassembly-version-ffmpeg-js-part-2-compile-with-emscripten-4c581e8c9a16https://github.com/mymindstorm/setup-emsdkhttps://github.com/emscripten-core/emsdkhttps://github.com/FFmpeg/FFmpeg/blob/n4.3.1/INSTALL.mdhttps://yeasy.gitbook.io/docker_practice/container/run參考資料

[1]

WasmFiddle: https://wasdk.github.io/WasmFiddle/

[2]

WasmFiddle : https://anonyco.github.io/WasmFiddlePlusPlus/

[3]

WasmExplorer: https://mbebenita.github.io/WasmExplorer/

[4]

直接調用 Web API: https://github.com/WebAssembly/gc/blob/master/README.md

[5]

它的文檔: https://www.assemblyscript.org/

[6]

localhosthttp://localhost:5000/hello2.html: http://localhost:5000/hello2.html

[7]

Github: https://github.com/webmproject/libwebp

[8]

API 文檔: https://developers.google.com/speed/webp/docs/api

[9]

WebP 的文檔: https://developers.google.com/speed/webp/docs/api#simple_encoding_api

[10]

文件系統 API: https://emscripten.org/docs/api_reference/Filesystem-API.html

??/ 感謝支持 /

以上便是本次分享的全部內容,希望對你有所幫助^_^

喜歡的話別忘了 分享、點贊、收藏 三連哦~

歡迎關注公眾號 程序員巴士,來自字節、蝦皮、招銀的三端兄弟,分享編程經驗、技術干貨與職業規劃,助你少走彎路進大廠。

鄭重聲明:本文版權歸原作者所有,轉載文章僅為傳播更多信息之目的,如有侵權行為,請第一時間聯系我們修改或刪除,多謝。

CopyRight ? 外貿領航 2023 All Rights Reserved.