gogoend / blog

blogs, ideas, etc.
MIT License
9 stars 2 forks source link

[译] 构建WebAssembly版本的FFmpeg——ffmpeg.wasm:第三部分:ffmpeg.wasm v0.1.0 —— 将avi转码为mp4 #47

Closed gogoend closed 3 years ago

gogoend commented 4 years ago

原文:https://itnext.io/build-ffmpeg-webassembly-version-ffmpeg-js-part-3-ffmpeg-js-v0-1-0-transcoding-avi-to-mp4-f729e503a397 作者:Jerome Wu 翻译:gogoend

前一篇文章:[译] 构建WebAssembly版本的FFmpeg——ffmpeg.wasm:第二部分:使用Emscripten编译

在这一部分你将了解到:

  1. 使用优化的参数构建一个库版本的FFmpeg。
  2. 管理Emscripten文件系统。
  3. 开发具有转码功能的ffmpeg.wasm v0.1.0。

使用优化的参数构建一个库版本的FFmpeg

在第三部分,我们的目标是创建一个基础的ffmpeg.wasm v0.1.0,以用于将avi转换为mp4。但在第二部分,我们仅仅创建了一个“裸金属”版本的FFmpeg,我们需要使用几个参数来对其更深一步地进行优化。

  1. -Oz:优化代码,减少代码大小(从30MB缩减到15MB);
  2. -o javascript/ffmpeg-core.js:将js文件与wasm文件输出到javascript文件夹。 (在这里,我们将其称为ffmpeg-core.js —— 我们将创建一个ffmpeg.wasm库来包装ffmpeg-core.js,并向用户提供友好的API。)
  3. -s MODULARIZE=1:创建一个库而非一个命令行工具(需要修改源代码,详见下方)
  4. -s EXPORTED_FUNCTIONS="[_ffmpeg]":将C语言中的函数“ffmpeg”输出到JavaScript的世界
  5. -s EXTRA_EXPORTED_RUNTIME_METHODS="[cwrap, FS, getValue, setValue]":用于操作函数、文件系统以及指针的一些额外的方法,参阅Interacting with code以了解详情。
  6. -s ALLOW_MEMORY_GROWTH=1:当内存用尽的时候,允许扩大内存。
  7. -lpthread:已删除,因为我们要创建我们自己版本的worker。(详见第四部分)

对于这些参数的详细信息,你可以参阅GitHub上 emscripten 仓库中的src/settings.js

当我们添加-s MODULARIZE=1参数时,为了适应模块化的相关要求,我们有必要对源代码进行一些更新(基本上就是移除main() 函数)。我们只需更改3行代码,即可使其工作。

  1. fftools/ffmpeg.c:重命名为ffmpeg

    - int main(int argc, char **argv)
    + int ffmpeg(int argc, char **argv)
  2. fftools/ffmpeg.h: 在该文件最后加入ffmpeg,以输出ffmpeg方法。

    + int ffmpeg(int argc, char** argv);
    #endif /* FFTOOLS_FFMPEG_H */
  3. fftools/cmdutils.c: 注释掉exit(ret),因为我们不希望我们的库为我们退出runtime。(在未来我们会增强这一部分)。

    void exit_program(int ret){
    if (program_exit)
        program_exit(ret);
    -   exit(ret);
    +    // exit(ret);
    }

我们新的构建脚本:

...

现在我们的ffmpeg-core.js就已经准备好了!

如果你熟悉ffmpeg,那你一定知道在命令行下ffmpeg的通用用法:

$ ffmpeg -i input.avi output.mp4

我们重用main函数,来作为我们的ffmpeg函数,我们将会这样调用我们的函数:

const args = ['./ffmpeg', '-i', 'input.avi', 'output.mp4'];
ffmpeg(args.length, args);

当然,要实现这一点并不容易。我们需要做一些预处理,来为JavaScript的世界与C的世界搭建桥梁。我们从emscripten的文件系统开始吧。

管理Emscripten的文件系统

Emscripten中包含一个虚拟的文件系统,用以支持C语言中标准文件的读写。我们重用了命令行版本的FFmpeg,该版本可将视频文件读写到文件系统,因此了解基本概念十分重要。

更多详情请参阅File System API

若要使用文件系统,首先你需要将FS API从Emscripten输出,这是通过上文中说到的一个参数实现的:

-s EXTRA_EXPORTED_RUNTIME_METHODS="[cwrap, FS, getValue, setValue]"

要将一个文件存入文件系统,你需要准备一个Uint8Array格式的数组。在Node.js环境下,你可以这样做:

const fs = require('fs');
const data = new Uint8Array(fs.readFileSync('./input.avi'));

而要将文件保存到emscripten文件系统,需要调用FS.writeFile()函数:

require('./ffmpeg-core.js)()
  .then(Module => {
    Module.FS.writeFile('input.avi', data);
  });

从emscripten文件系统加载文件也与此类似:

require('./ffmpeg-core.js)()
  .then(Module => {
    const data = Module.FS.readFile('output.mp4');
  });

在了解这些概念后,现在我们可以继续开发隐藏了复杂的概念,提供了用户友好API的ffmpeg.wasm。

开发具有转码功能的ffmpeg.wasm v0.1.0

ffmpeg.wasm的开发并不容易,因为你需要在JavaScript与C语言之间进行转换。但如果你熟悉指针,你将会更加容易理解我们现在大部分时间在做什么。

这一部分,我们的目标是开发一个类似这样的ffmpeg.wasm:

首先,我们需要加载ffmpeg-core.js。由emscripten构建的库,加载过程是异步的,因为我们不想让它阻塞我们的主线程。

一种典型的加载过程可能是: 你或许对我们使用Promise来包裹另一个Promise感到诧异,这是因为FFmpegCore()不是一个真正的promise,仅仅是一个具有类似API的函数。

Module是让我们进行所有操作的一个初始化库,这里我们使用 Singleton 工具来存储它,并在之后得到。

下一步我们将使用Module来得到我们的ffmpeg函数,这里我们需要使用cwrap

// int ffmpeg(int argc, char **argv)
const ffmpeg = Module.cwrap('ffmpeg', 'number', ['number', 'number']);

cwrap的第一个参数是函数名(必须在EXPORTED_FUNCTIONS中包含下划线),第二个参数是返回值类型,第三个参数是函数参数的类型( int argc 和 char **argv )。

很明显,argc是一个数值,但为何argv也是一个数值?这是因为,argv实际上是一个指针,指针中所存储的是内存地址(类似0xfffffff),所以在WebAssembly中,指针的类型是一个32位无符号整数。这就是argv的类型为数值的原因。

要调用ffmpeg(),第一个参数很容易,因为我们可以无需任何更改地在JavaScript中使用原生的数值,但第二个参数就不容易了,因为我们需要创建一个指向char类型的二维数组的指针(JavaScript中的Uint8)。

将这一问题拆分为两个小问题:

  1. 我们怎样创建一个指针,指向一个char数组?
  2. 我们怎样创建一个指针,指向一个指针数组?

要解决第一个问题,我们来创建一个工具函数,名为str2ptrModule._malloc()同C语言里的malloc()方法类似,它在堆中分配一部分的内存空间。Module.setValue()在给定的指针中设置真实值。

不要忘了在char数组最后加一个0,否则你可能会遇上意料之外的动作。

现在我们就解决了第一个问题,那我们来创建strList2ptr函数来解决后面这个问题。

这里的关键是理解指针在JavaScript中是一个Uint32值,因此listPtr是一个Uint32数组指针,它存储有指向Uint8数组的指针。

将这些东西放在一起,我们就有了ffmepg.transcode()的第一个版本。

没错,现在我们就有了将avi转码为mp4的ffmpeg.wasm v0.1.0。

如果你想直接尝试ffmpeg.wasm v0.1.0,现在你就可以通过这个命令来安装它:

$ npm install @ffmpeg/ffmpeg@0.1.0

其用法十分直接。

不过还是注意一下,目前仅仅是一个可在Node.js上运行的版本,但我们将会在构建WebAssembly版本的FFmpeg——ffmpeg.wasm:第四部分:ffmpeg.wasm v0.2 —— Web Worker 与 Libx264来开发浏览器版本。

期待在第四部分与你相见!😃

仓库: