乐趣区

关于webassembly:高级前端进阶我是如何把-CC-代码跑在浏览器上的

最近组长交给我一个工作,让我尝试一下将出名视频转码库 ffmpeg(应用 C 编写)跑在浏览器外面,我过后就懵了,还能这么玩?调研了一番,发现有个叫 WebAssembly 的货色能够干这么件事件,于是就有了这篇文章。

什么是 WebAssembly?

  • 一种新型的代码,能够运行在 Web 浏览器,提供一些新个性并次要专一于高性能
  • 次要不是用于写,而是 C/C++、C#、Rust 等语言编译的指标,所以你即便不晓得如何编写 WebAssembly 代码也能利用它的劣势
  • 其余语言编写的代码也能以近似于原生速度运行,客户端 App 也能在 Web 上运行
  • 在浏览器或 Node.js 中能够导入 WebAssembly 模块,JS 框架可能应用 WebAssembly 来取得微小的性能劣势和新的个性的同时在性能上易于应用

WebAssembly 的指标

  1. 快、高效、便当 — 通过利用一些通用的硬件能力,可能跨平台以近乎于原生的速度执行
  2. 可读、可调试 — WebAssembly 是一种低层次的汇编语言,然而它也有一种人类可读的文本格式,使得人们可编写代码、查看代码、可调试代码。
  3. 确保安全 — WebAssembly 明确运行在平安、沙箱的执行环境,相似其余 Web 的代码,它会强制开启同源和一些权限策略。
  4. 不毁坏现有的 Web — WebAssembly 被设计与其余 Web 技术兼容运行,并且放弃向后兼容性。

WebAssembly 如何与 Web 兼容的?

Web 平台能够看做有两个局部:

  1. 一个虚拟机(VM)用于运行 Web 利用代码,例如 JS 引擎运行 JS 代码
  2. 一系列 Web API,Web 利用能够调用这些 API 来管制 Web 浏览器 / 设施 的性能,来做某些事件(DOM、CSSOM、WebGL、IndexedDB、Web Audio API 等)

长期以来,VM 只能加载 JS 运行,JS 可能足够满足咱们的需要,但现在咱们却遇到了各种性能问题,如 3D 游戏、VR/AR、计算机视觉、图片 / 视频编辑、以及其余须要原生性能的畛域。

同时,下载、解析和编译大体积的 JS 利用是很艰难的,在一些资源更加受限的平台上,如挪动设施等,则会更加放到这种性能瓶颈。

WebAssembly 是一种与 JavaScript 不同的语言,它不是为了代替 JS 而生的,而是被设计为与 JS 互为补充并能合作,使得 Web 开发者可能反复利用两种语言的长处:

  1. JS 是高层次的语言,灵便且极具表现力,动静类型、不须要编译步骤,并且有弱小的生态,十分易于编写 Web 利用。
  2. WebAssembly 是一种低层次、类汇编的语言,应用一种紧凑的二级制格局,可能以近乎原生的性能运行,并提供了低层次的内存模型,是 C++、Rust 等语言的编译指标,使得这类语言编写的代码可能在 Web 上运行(须要留神的是,WebAssembly 将在将来提供垃圾回收的内存模型等高层次的指标)

随着 WebAssembly 的呈现,上述提到的 VM 当初能够加载两种类型的代码执行:JavaScript 和 WebAssembly。

JavaScript 和 WebAssembly 能够互操作,实际上一份 WebAssembly 代码被称为一个模块,而 WebAssembly 的模块与 ES2015 的模块在具备很多独特的个性。

WebAssembly 的要害概念

为了了解 WebAssembly 是如何在 Web 运行的,须要理解几个要害概念:

  1. Module:通过浏览器编译成为可执行机器码的 WebAssembly 二进制文件,Module 是无状态的,相似 Blob,可能在 Window 和 Worker 之间通过 postMessage 共享,一个 Module 申明了相似 ES2015 模块相似的 import 和 export。
  2. Memory:一个可调整大小的 ArrayBuffer,其中蕴含由 WebAssembly 的低层次内存拜访指令读取和写入的线性字节数组。
  3. Table:一个可调整大小的类型化援用数组(如函数),然而处于平安和可移植性的起因,不能作为原始字节存储在内存中
  4. 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

<!—->

  • WasmFiddle++

<!—->

  • WasmExplorer

然而这些工具都不足 EMScripten 的工具链和优化操作,EMScripten 的具体运行过程如下:

  1. EMScripten 将 C/C++ 代码喂给 Clang 编译器(一个基于 LLVM 编译架构的 C/C++ 编译器),编译成 LLVM IR
  2. EMScripten 将 LLVM IR 转换成 .wasm 的二进制字节码
  3. WebAssembly 无奈间接获取到 DOM,只能调用 JS,传入整形或浮点型的等原始数据类型,因而 WebAssembly 须要调用 JS 来获取 Web API 和调用,EMScripten 则通过创立了 HTML 文件和 JS 胶水代码来达到上述成果

将来 WebAssembly 也能够间接调用 Web API。

上述的 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…

应用 AssemblyScript

对于 Web 开发者来说,可是应用类 TypeScript 的模式来尝试 WebAssembly 的编写,而不须要学习 C 或 Rust 的细节,那么 AssemblyScript 将会是最好的抉择。AssemblyScript 将 TypeScript 的变体编译为 WebAssembly,使得 Web 开发者能够应用 TypeScript 兼容的工具链,例如 Prettier、VSCode Intellisense,你能够查看它的文档来理解如何应用。

如何编译将新写 C/C++ 代码编译到 WebAssembly?

通过 EMScripten 工具,可将新写的 C/C++ 代码编译为 WebAssembly 应用。

筹备条件

为了可能应用 Emscripten 工具,咱们须要装置它。首先 Clone 相干代码:

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

cd 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 World\n");

}

而后在命令行中导航到此 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 World\n");

}

在之前 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 来拜访运行后果,能够观测到和之前相似的成果:

能够看到只是短少了之前的 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 World\n");

}



#ifdef __cplusplus

extern "C" {

#endif



EMSCRIPTEN_KEEPALIVE void myFunction(int argc, char ** argv) {printf("MyFunction Called\n");

}



#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 上找到它,同时能够理解到它的一些 API 文档。

首先 Clone WebP 编码器的源码到本地,和 emsdkWebAssembly 目录同级:

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

为了疾速上手,咱们能够先导出 encode.h 头文件外面的 WebPGetEncoderVersion 函数给到 JavaScript 应用,首先在 WebAssembly 文件夹下创立 webp.c 文件并退出如下:

#include "emscripten.h"

#include "src/webp/encode.h"



EMSCRIPTEN_KEEPALIVE

int 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 提供了 emconfigureemmake 来封装这些命令,并注入适合的参数来抹平那些有前置依赖的我的项目。

运行上述命令之后,会产出一份 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_KEEPALIVE

uint8_t* create_buffer(int width, int height) {return malloc(width * height * 4 * sizeof(uint8_t));

}



EMSCRIPTEN_KEEPALIVE

void 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_bufferdestroy_buffer 外,还有很多用于编码文件等方面的函数,咱们将在后续解说,除此之外,代码首先加载了一份 image.jpg 的图片,而后调用 C 函数为此图片数据分配内存,并相应的拿到返回的指针传给 WebAssembly 的 Module.HEAP8,在内存开始地位 p,写入图片的数据,最初会开释调配的内存。

编码图片

当初图片数据曾经加载进 wasm 的内存中,能够调用 libwebp 的 encoder 办法来实现编码过程了,通过查阅 WebP 的文档,发现能够应用 WebPEncodeRGBA 函数来实现工作。这个函数接管一个指向图片数据的指针以及它的尺寸,以及一个区间在 0-100 的可选的品质参数。在编码的过程中,WebPEncodeRGBA 会调配一块用于输入数据的内存,咱们须要在编码实现之后调用 WebPFree 来开释这块内存。

咱们关上 webp.c 文件,增加如下解决编码的代码:

int result[2];

EMSCRIPTEN_KEEPALIVE

void 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_KEEPALIVE

void free_result(uint8_t* result) {WebPFree(result);

}



EMSCRIPTEN_KEEPALIVE

int get_result_pointer() {return result[0];

}



EMSCRIPTEN_KEEPALIVE

int 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 不能裁减能够包容 inputoutput 图片数据的内存,你可能会遇到如下报错:

然而咱们例子中应用的图片比拟小,所以只须要单纯的在编译时加上一个过滤参数 -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 提供了 emconfigureemmake 来封装这些命令,并注入适合的参数来抹平那些有前置依赖的我的项目,接下来咱们通过理论编译 ffmpeg 来解说如何解决这种依赖 autoconfig/automake 等库来生成特定的代码。

通过实际发现 ffmpeg 的编译依赖于特定的 ffmpeg 版本、Emscripten 版本、操作系统环境等,所以以下的 ffmpeg 的编译都是限度在特定的条件下进行的,次要是为之后通用的 ffmpeg 的编译提供一种思路和调试办法。

编译步骤

应用 Emscripten 编译大部分简单的 C/C++ 库时,次要须要三个步骤:

  1. 应用 emconfigure 运行我的项目的 configure 文件将 C/C++ 代码编译器从 gcc/g++ 换成 emcc/em++
  2. 通过 emmake make 来构建 C/C++ 我的项目,生成 wasm 对象的 .o 文件
  3. 为了生成特定模式的输入,手动调用 emcc 来编译特定的文件

装置特定依赖

为了验证 ffmpeg 的验证,咱们须要依赖特定的版本,上面具体解说依赖的各种文件版本。

首先装置 1.39.18 版本的 Emscripten 编译器,进入之前咱们 Clone 到本地的 emsdk 我的项目运行如下命令:

./emsdk install 1.39.18

./emsdk activate 1.39.18

source ./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 压缩到 15MB
  • INITIAL_MEMORY 设置为 33554432(32MB),次要是 Emscripten 可能占用 19MB,所以设置更大的内存容量来防止在编译过程中可调配的内存不足的问题
  • 理论应用 emconfigure 来配置 configure 文件,替换 gcc 编译器为 emcc,以及设置一些必要的操作来解决可能遇到的编译 BUG,最终生成用于编译构建的配置文件

应用 emmake make 来构建依赖

通过上述步骤,就解决好了配置文件,接下来须要通过 emmake 来构建理论的依赖,通过在命令行中运行如下命令:

# 构建最终的 ffmpeg.wasm 文件

emmake make -j4

通过上述的编译,会生成如下四个文件:

  • ffmpeg

<!—->

  • ffmpeg_g

<!—->

  • ffmpeg_g.wasm

<!—->

  • ffmpeg_g.worker.js

前两个都是 JS 文件,第三个为 wasm 模块,第四个是解决 worker 中运行相干逻辑的函数,上述生成的文件的现实模式应该为三个,为了达成这种自定义的编译,有必要自定义应用 emcc 命令来进行解决。

应用 emcc 个性化编译

FFmpeg 目录下创立 wasm 文件夹,用于搁置构建之后的文件,而后自定义编译文件输入如下:

mkdir -p wasm/dist



emcc \                   

 -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

上述的脚本次要有如下几点改良:

  1. -s PROXY_TO_PTHREAD=1 在编译时设置了 pthread 时,使得程序具备响应式特效
  2. -o wasm/dist/ffmpeg-core.js 则将原 ffmpeg js 文件的输入重命名为 ffmpeg-core.js,对应的输入 ffmpeg-core.wasmffmpeg-core.worker.js
  3. -s EXPORTED_FUNCTIONS="[_main, _proxy_main]" 导出 ffmpeg 对应的 C 文件里的 main 函数,proxy_main 则是通过设置 PROXY_TO_PTHREAD 代理 main 函数用于内部应用
  4. -s EXTRA_EXPORTED_RUNTIME_METHODS="[FS, cwrap, setValue, writeAsciiToMemory]" 则是导出一些帮忙函数,用于导出 C 函数、解决文件系统、指针的操作

通过上述编译命令最终输入上面三个文件:

  • ffmpeg-core.js

<!—->

  • ffmpeg-core.wasm

<!—->

  • ffmpeg-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。

为了实现上述的工作,只须要应用到 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.bz2

ttar xvfj x264-snapshot-20170226-2245-stable.tar.bz2

而后进入 x264 的文件夹,能够创立一个 build.sh 文件,并退出如下内容:

 #!/bin/bash -x



ROOT=$PWD

BUILD_DIR=$ROOT/build



cd $ROOT/x264-snapshot-20170226-2245-stable

ARGS=(

  --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 -j4



cd -

留神须要在 WebAssembly 目录下运行如下命令来构建 x264:

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

装置了 x264 编码器之后,就能够在 ffmpeg 的编译脚本中退出关上 x264 的开关,这一次咱们在 ffmpeg 文件夹下创立 Bash 脚本用于构建,创立 configure.sh 如下:

 #!/bin/bash -x



emcc -v



ROOT=$PWD

BUILD_DIR=$ROOT/build



cd $ROOT/ffmpeg-4.3.2-3



CFLAGS="-s USE_PTHREADS -I$BUILD_DIR/include"

LDFLAGS="$CFLAGS -L$BUILD_DIR/lib -s INITIAL_MEMORY=33554432" # 33554432 bytes = 32 MB



CONFIG_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.wasm

emmake make -j4



cd -

而后创立用于自定义输入构建文件的脚本文件 build-ffmpeg.sh

ROOT=$PWD

BUILD_DIR=$ROOT/build



cd ffmpeg-4.3.2-3



ARGS=(

  -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/bl…
  • https://pspdfkit.com/blog/201…
  • https://hacks.mozilla.org/201…
  • https://www.sitepoint.com/und…
  • http://www.cmake.org/download/
  • https://developer.mozilla.org…
  • https://research.mozilla.org/…
  • https://itnext.io/build-ffmpe…
  • https://dev.to/alfg/ffmpeg-we…
  • https://gist.github.com/rinth…
  • https://github.com/Kagami/ffm…
  • https://qdmana.com/2021/04/20…
  • https://github.com/leandromor…
  • http://ffmpeg.org/doxygen/4.1…
  • https://github.com/alfg/ffmpe…
  • https://github.com/alfg/ffpro…
  • https://gist.github.com/rinth…
  • https://emscripten.org/docs/c…
  • https://itnext.io/build-ffmpe…
  • https://github.com/mymindstor…
  • https://github.com/emscripten…
  • https://github.com/FFmpeg/FFm…
  • https://yeasy.gitbook.io/dock…

❤️/ 感激反对 /

以上便是本次分享的全部内容,心愿对你有所帮忙 ^_^

喜爱的话别忘了 分享、点赞、珍藏 三连哦~

欢送关注公众号 程序员巴士,来自字节、虾皮、招银的三端兄弟,分享编程教训、技术干货与职业规划,助你少走弯路进大厂。

退出移动版