编译WebAssembly版本的FFmpeg(ffmpeg.wasm):(5)ffmpeg.wasm v0.3 - pre.js与实时音视频流

2022-04-16 02:10:14 浏览数 (2)

作者:Jerome Wu

原文链接:Build FFmpeg WebAssembly version (= ffmpeg.wasm): Part.5 ffmpeg.wasm v0.3 — pre-js and live streaming (OUTDATED)

译者:Yodonicc

上一篇文章:编译WebAssembly版本的FFmpeg(ffmpeg.wasm):(4)ffmpeg.wasm v0.2 - 添加Libx264

在这一部分中,你将学习:

  1. 使用--pre-js来重新定义模块中的函数
  2. 同时使用ffmpeg.js和网络摄像头

使用--pre-js来重新定义模块中的函数

FFmpeg有大量的输出,它包含重要的信息,如视频的元数据,编码器/解码器的输出和任务的进展。默认情况下,这些信息是由本地C语言库printf/sprintf打印的,虽然你可以在DevTools中看到,但你不能用JavaScript来操作。

幸运的是,在Emscripten中我们可以用--pre-js--post-js重新定义一些默认函数的行为。对于上面的情况,我们需要重新定义的函数是Module['printErr'](因为FFmpeg的输出使用stderr),并且用-pre-js添加到我们的ffmpeg.js中。

下面是我学到的一些经验供你参考。

  • 你只能在--pre-js中使用ES5语法(没有箭头函数、const、let)
  • 你需要添加额外的宏来防止你的代码被Closure编译器删除(这里我没有使用Closure编译器)

这里是目前ffmpeg.js中使用的prepend.js:

代码语言:javascript复制
var logger = function(){}

Module['setLogger'] = function(_logger) { logger = _logger; };
Module['print'] = function(message) { logger(message, 'stdout'); };
Module['printErr'] = function(message) { logger(message, 'stderr'); };

我们实际上定义了一个新的函数setLogger来注册我们的自定义记录器函数,并重新定义了两个现有的函数printprintErr。有了这个prepend.js,现在我们可以轻松地操作FFmpeg的输出信息,开发更多的功能(如进度条)。

在构建脚本中添加--pre-js很容易(第54行)

代码语言:shell复制
#!/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
  emmake make install-lib-static
  cd -
}

configure_ffmpeg() {
  emconfigure ./configure 
    --enable-gpl 
    --enable-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" 
    --extra-cxxflags="-I$BUILD_DIR/include" 
    --extra-ldflags="-L$BUILD_DIR/lib" 
    --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 
    --closure 1 
    --pre-js javascript/prepend.js     # add HERE
    -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 "$@"
view rawbuild_ffmpeg-core.js-v0.3.sh hosted with ❤ by GitHub

现在我们在模块对象中有了setLogger,我们可以在初始化后使用它(第13行)

代码语言:javascript复制
/**
 * 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;
      Module.setLogger(m => console.log(m));
      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);
  }
});
view rawworker-v0.3.js hosted with ❤ by GitHub

使用ffmpeg.js与网络摄像头

在这里,我想描述一下如何将ffmpeg用于流媒体直播,这里我们用网络摄像头作为例子,但大多数情况下应该有类似的工作流程。

基本的工作流程是:

  1. 使用MediaRecorder API将流媒体保存到Blob中
  2. 将Blob转换为Uint8Array数据
  3. 使用ffmpeg.js对Uint8Array数据进行转码

步骤1 使用getUserMedia访问网络摄像头(需要https协议)

代码语言:html复制
<video id="webcam" width="320px" height="180px"></video>
<script>
const webcam = document.getElementById('webcam');
(async () => {
  webcam.srcObject = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });
  await webcam.play();
})();
</script>

第2步:使用MediaRecorder来录制片段

代码语言:javascript复制
const startRecording = () => {
  const rec = new MediaRecorder(webcam.srcObject);
  const chunks = [];
  rec.ondataavailable = e => chunks.push(e.data);
  rec.onstop = async () => {
    transcode(new Uint8Array(await (new Blob(chunks)).arrayBuffer()));
  };
  rec.start();
};

步骤3 重复使用上一部分的trancode来做转码。

在第五篇文章中,我们学习了如何使用--pre-js来重新定义/扩展模块的能力,并介绍了一个如何在流媒体直播场景中使用ffmpeg的例子。

在第六篇文章中,我们将对文件系统进行深入研究:编译WebAssembly版本的FFmpeg(ffmpeg.wasm):(6) 深入研究文件系统

0 人点赞