gogoend / blog

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

[译] 构建WebAssembly版本的FFmpeg——ffmpeg.wasm:第六部分:深入研究文件系统 #51

Closed gogoend closed 3 years ago

gogoend commented 4 years ago

原文:https://itnext.io/build-ffmpeg-webassembly-version-ffmpeg-js-part-6-a-deep-dive-into-file-system-56eba10067ca 作者:Jerome Wu 翻译:gogoend

2020/1/14:自 ffmpeg.wasm v0.6.0起,我已经抛弃了 IDBFS 和 NODEFS,并使用postMessage中的 Transferable以解决这个问题。IDBFS的主要问题是文件限制过小(大约200 MB),但你仍然可以阅读本文,将本文当作一个使用这两种文件系统的示例。

前一篇文章:[译] 构建WebAssembly版本的FFmpeg——ffmpeg.wasm:第五部分:ffmpeg.wasm v0.3 —— pre-js 和 流媒体直播

在这一部分你将了解到:

  1. MEMFS、IDBFS 和 NODEFS 的区别
  2. 如何挂载 IDBFS 和 NODEFS
  3. 解决一个现实的问题:ffmepg.js的文件大小限制

MEMFS、IDBFS 和 NODEFS 的区别

默认情况下,当你使用 Emscripten 来转换任何具有文件系统操作的库时, Emscripten 会使用一个名为MEMFS的模拟文件系统,以确保这个库可在浏览器与node.js环境下工作。

使用MEMFS很快捷,但它具有一些缺点:

  1. 由于Emscripten仅可以使用最多2 GB的内存,因此MEMFS很容易导致内存耗尽。
  2. 会在你的主线程与Emscripten产生数据“pass through”的行为(详见“解决一个现实的问题:ffmepg.js的文件大小限制”)

若要解决这些问题,一个方法是使用 IDBFS 和 NODEFS 来充当应用程序的真实文件系统。

在浏览器(以及Web Worker)环境中使用的IDBFS是使用 IndexedDB 作为文件系统来存储你的文件

由于IndexedDB具有一些同步问题,你需要在文件写入后使用FS.syncfs()

在Node.js环境中使用的 NODEFS 是使用Node.js 中的 fs API 来模拟一个文件系统。

详情可参阅:https://emscripten.org/docs/api_reference/Filesystem-API.html

如何挂载IDBFS 和 NODEFS

要挂载 IDBFS 和 NODEFS,你需要使用在第五部分中介绍的--pre-js。这次我们需要覆写一个函数,名为preRun(详见此处)。

下面是一个用法样例:


var logger = function(){}

Module['setLogger'] = function(_logger) { logger = _logger; };
Module['print'] = function(message) { logger(message, 'stdout'); };
Module['printErr'] = function(message) { logger(message, 'stderr'); };
Module['preRun'] = [
  function() {
    FS.mkdir('/data');
    /* Node.js Environment */
    if (typeof process === 'object' && typeof require === 'function') {
      try {
        require('fs').mkdirSync('./data');
      } catch(e) {}
      FS.mount(NODEFS, { root: '.' }, '/data');
    /* Web Worker Environment */
    } else if (typeof importScripts === 'function') {
      FS.mount(IDBFS, {}, '/data');
    }
  },
];

使用其它函数(比如preInit)也应该可以进行这个任务,但在这里我们使用的是preRun。具体取决于你的应用程序。

解决一个现实的问题:ffmepg.js的文件大小限制

有一天,有一个issueffmpeg.wasm不能够处理大文件。要解决这个问题,我们重新审视一下我们的设计:

image

当文件不是很大的时候,这个流程看起来还不错;但当媒体文件达到100 MB后,通过 postMessage() 或 send() 来传递如此巨大的文件看起来很不合理,因此会导致 ffmpeg.wasm 崩溃。

此处的瓶颈是由于我们使用 Web Worker / 子进程来充当传递组件来发送和接收媒体文件而造成的。要解决该问题,我们需要使用一个同时可被 Worker 和 Web Worker / 子进程访问的文件系统,因此我们需要重新来设计一下。这是优化后的设计:

image

这里的想法是,使得 Worker 和 Web Worker / 子进程能够在 IDBFS / NODEFS 中进行读写。这就解决了我们在初始设计中遇到的瓶颈。

虽然看起来更加复杂了,但这解决了在 ffmpeg.wasm 中处理大文件的问题。现在你可以在下方的CodePen中进行尝试。(你可以下载一个 90 MB 的视频文件)

<h3>Upload a video to transcode to mp4 (x264) and play!</h3>
<video id="output-video" controls></video><br/>
<input type="file" id="uploader">
<p id="message" />
html, body {
  margin: 0;
  width: 100%;
  height: 100%
}
body {
  display: flex;
  flex-direction: column;
  align-items: center;
}
const message = document.getElementById('message');
const { createFFmpeg } = FFmpeg;
const ffmpeg = createFFmpeg({
  log: true,
  progress: ({ ratio }) => {
    message.innerHTML = `Complete: ${(ratio * 100.0).toFixed(2)}%`;
  },
});

const transcode = async ({ target: { files }  }) => {
  const { name } = files[0];
  message.innerHTML = 'Loading ffmpeg-core.js';
  await ffmpeg.load();
  message.innerHTML = 'Start transcoding';
  await ffmpeg.write(name, files[0]);
  await ffmpeg.transcode(name,  'output.mp4');
  message.innerHTML = 'Complete transcoding';
  const data = ffmpeg.read('output.mp4');

  const video = document.getElementById('output-video');
  video.src = URL.createObjectURL(new Blob([data.buffer], { type: 'video/mp4' }));
}
document.getElementById('uploader').addEventListener('change', transcode);

这种方式十分显著的副作用是,它在用户的IndexedDB(浏览器)和文件系统(Node.js)中存储了大量的数据。记住要尽可能地进行清理。


以上就是第六部分的全部内容,感谢阅读。期待在下一部分中继续与你相见。

仓库:

ffmpeg-core.js: https://github.com/ffmpegwasm/FFmpeg ffmpeg.wasm: https://github.com/ffmpegwasm/ffmpeg.wasm