gogoend / blog

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

[译] 构建WebAssembly版本的FFmpeg——ffmpeg.wasm:第四部分:ffmpeg.wasm v0.2 —— Web Worker 与 Libx264 #48

Closed gogoend closed 3 years ago

gogoend commented 4 years ago

原文:https://itnext.io/build-ffmpeg-webassembly-version-ffmpeg-js-part-4-ffmpeg-js-v0-2-web-worker-and-libx264-d0596f1beb4e 作者:Jerome Wu 翻译:gogoend

前一篇文章:[译] 构建WebAssembly版本的FFmpeg——ffmpeg.wasm:第三部分:ffmpeg.wasm v0.1.0 —— 将avi转码为mp4

在这一部分你将了解到:

  1. 在Web Worker中运行ffmpeg-core.js
  2. 添加Libx264到ffmpeg-core.js
  3. 浏览器中的 ffmpeg.wasm Demo

在Web Worker中运行ffmpeg-core.js

在前一篇文章里,我们在Node.js环境下将ffmpeg.wasm跑了起来,以确保我们的ffmpeg-core.js能够工作。如果你足够仔细的话,你会发现ffmpeg.wasm是同步运行的,运行时会阻塞主线程。在Node.js中,这或许是可以接受的;然而在浏览器下会十分不理想 —— 你的应用程序可能会没有响应。甚至完全卡死。为了解决这个问题,我们使用Web Worker(Node.js中使用child_process),以在后台运行我们的任务。若要了解Web Worker的更多细节,MDN上的资料可以给你很好的参考:

使用Web Workers

第一步,来将我们的ffmpeg-core.js移到Web Worker,并提供两个函数:load(加载)和transcode(转码):

/**
 * This script is loaded by ffmpegwasm-v0.2.js
 */

let Module = null;
let ffmpeg = null;

const load = () => {
  global.importScripts('./ffmpeg-core.js');
  global.Module()
    .then((_Module) => {
      Module = _Module;
      ffmpeg = Module.cwrap('ffmpeg', 'number', ['number', 'number']);
      postMessage({ action: 'load', payload: { loaded: true } });
    });
};

const transcode = ({ data: _data, outputExt }) => {
  /* When an array received from postMessage,
   * its format will change to an "ArrayLike" object,
   * we need to transform it back to array
   */
  const data = Uint8Array.from({ ..._data, length: Object.keys(_data).length });
  const args = ['./ffmpeg', '-i', 'file:media', `media.${outputExt}`];
  Module.FS.writeFile('media', data);
  ffmpeg(args.length, strList2ptr(args));
  postMessage({
    action: 'transcode',
    payload: {
      data: Module.FS.readFile(`media.${outputExt}`),
    },
  });
};

global.addEventListener('message', ({ action, payload }) => {
  if (action === 'load') {
    load(payload);
  } else if (action === 'transcode') {
    transcode(payload);
  }
});

这一代码与我们在上一篇文章中展示的十分相似。主要差别是:addEventListener() 用于从前台获取消息;postMessage() 用于发送消息到前台。

现在我们将ffmpeg.wasm创建为一个前台脚本,用以运行我们的Worker:

const resolves = {};
const rejects = {};

const worker = new Worker('./worker.js');

worker.onmessage = ({ action, payload }) => {
  resolves[action](payload);
};

exports.load = () => new Promise((resolve) => {
  worker.postMessage({ acton: 'load' });
  resolves['load'] = resolve;
});

exports.transcode = (data, outputExt) => new Promise((resolve) => {
  worker.postMessage({ action: 'transcode', payload: { data, outputExt } });
  resolves['transcode'] = resolve;
});

ffmpeg.wasm导出了加载与转码函数,它们都是异步的,因为我们需要使用postMessage()来发送消息到后台Web Worker,并用onmessage()来等待消息返回。若要将这一过程Promise化,我们需要将resolve函数全局保存,并在onmessage()函数内调用。在使用Web Worker时,这是一个小技巧。

最终我们可以使用类似如下的方式来使用我们的ffmpeg.wasm库。

const { load, transcode } = require('./ffmpegwasm-v0.2');

(async () => {
  await load();
  const { data } = await transcode(mediaData, 'mp4');
  // do something with data
})();

上面这些便是创建由Web Worker所驱动的ffmpeg.wasm需要进行的所有工作。对于Node.js中的child_process,该过程十分相似,仅有一些关键的函数是不相同的(new Worker VS fork、postMessage VS send、onmessage VS on、global VS process),你可以尝试着来进行实践。

以上的示例代码仅包含了ffmpeg.wasm的重要部分,但这足以让你理解使用Web Worker来运行代码的基本概念。完整版本请参阅:https://github.com/ffmpegwasm/ffmpeg.wasm

添加Libx264到ffmpeg-core.js

下一步中,我们想要转码一个avi格式的视频,并在浏览器中播放它。ffmpeg.wasm默认可以将avi转码到mp4,但转码后的mp4文件不能在浏览器中播放,因为该mp4文件的编码不受支持。因此首先我们需要将libx264添加到我们的ffmpeg-core.js。

VideoLAN / x264

在添加libx264之前,我们需要确保我们能够使用Emscripten来编译它。

emconfigure ./configure --disable-asm --disable-pthread
emmake make

在端序测试阶段,编译失败了,这是因为Emscripten使用的是小端序。我们可以注释掉端序测试以继续接下来的操作。十分幸运,这次编译没有报任何错误。

然后我们将libx264的构建加入我们的构建脚本中。

#!/bin/bash -x

set -e -o pipefail

BUILD_DIR=$PWD/build

build_x264() {
  cd third_party/x264
  emconfigure ./configure \
    --disable-asm \
    --disable-thread \
    --prefix=$BUILD_DIR                        # install headers and libraries in build folder
  emmake make install-lib-static
  cd -
}

configure_ffmpeg() {
  emconfigure ./configure \
    --enable-gpl \
    --enable-libx264 \                         # add this flag to enable building with libx264
    --disable-pthreads \
    --disable-x86asm \
    --disable-inline-asm \
    --disable-doc \
    --disable-stripping \
    --disable-ffprobe \
    --disable-ffplay \
    --disable-ffmpeg \
    --prefix=$BUILD_DIR \
    --extra-cflags="-I$BUILD_DIR/include" \    # look for headers in build folder
    --extra-cxxflags="-I$BUILD_DIR/include" \
    --extra-ldflags="-L$BUILD_DIR/lib" \       # look for libraries in build folder
    --nm="llvm-nm -g" \
    --ar=emar \
    --cc=emcc \
    --cxx=em++ \
    --objcc=emcc \
    --dep-cc=emcc
}

make_ffmpeg() {
  NPROC=$(grep -c ^processor /proc/cpuinfo)
  emmake make -j${NPROC}
}

build_ffmpegjs() {
  emcc \
    -I. -I./fftools -I$BUILD_DIR/include \
    -Llibavcodec -Llibavdevice -Llibavfilter -Llibavformat -Llibavresample -Llibavutil -Llibpostproc -Llibswscale -Llibswresample -Llibpostproc -L${BUILD_DIR}/lib \
    -Qunused-arguments -Oz \
    -o 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 \   # add lx264 flag
    --closure 1 \
    -s USE_SDL=2 \
    -s MODULARIZE=1 \
    -s SINGLE_FILE=1 \
    -s EXPORTED_FUNCTIONS="[_ffmpeg]" \
    -s EXTRA_EXPORTED_RUNTIME_METHODS="[cwrap, FS, getValue, setValue]" \
    -s TOTAL_MEMORY=33554432 \
    -s ALLOW_MEMORY_GROWTH=1
}

main() {
  build_x264
  configure_ffmpeg
  make_ffmpeg
  build_ffmpegjs
}

main "$@"

其它详情请参阅仓库:https://github.com/ffmpegjs/FFmpeg

./configure中添加--prefix可以帮助我们在工作目录中组织构建输出,当你位于Dock容器中,且不希望每次都重新构建所有内容时,这将会很有用。

若一切都顺利的话,一个ffmpeg-core.js将会被构建,你可以不经任何更改,将其与上述的ffmpeg.wasm一起使用。

libx264已包含在@ffmpeg/core@^0.3.0

浏览器中的 ffmpeg.wasm Demo

本文的最后部分是一个ffmpeg.wasm v0.2.2的Demo,场景是创建一个允许用户上传视频文件(例如:avi)并在浏览器中进行播放的网页。由于直接播放avi文件是不可行的,因此首先我们使用ffmpeg.wasm来对其进行转码。

这是CodePen的playground(在此下载示例视频

<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);

该过程可能会需要很长时间才能完成,你可以打开开发者工具来查看日志。

在第四部分中,我们创建了使用Web Worker的ffmpeg.wasm的浏览器版本,并添加了libx264到ffmpeg-core.js,使得ffmpeg.wasm更加实用。

若要安装ffmpeg.wasm,你可以使用如下命令:

$ npm install @ffmpeg/ffmpeg@0.2.2
# or
$ npm install @ffmpeg/ffmpeg@latest

期待在第五部分:构建WebAssembly版本的FFmpeg——ffmpeg.wasm:第五部分:ffmpeg.wasm v0.2 —— pre-js 和 流媒体直播与你相见!😃

仓库:

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