编译WebAssembly版本的FFmpeg(ffmpeg.wasm):(2)使用Emscripten编译
作者:Jerome Wu 原文链接:Build FFmpeg WebAssembly version (= ffmpeg.wasm): Part.2 Compile with Emscripten 译者:Yodoxu 2020/9更新:修改教程,使其能在MacOS中运行。 2020/2/11更新:从这个文章的回复来看,目前下面的说明在Mac环境下无法工作。我无法解决这个问题,因为我并不拥有一台。在Mac中最简单的方法是在虚拟机中安装Linux发行版,如Ubuntu。
上一篇文章:编译WebAssembly版本的FFmpeg(ffmpeg.wasm):(1)准备
从这里开始,事情会变得更加复杂和难以理解,如果你不知道发生了什么,你可能需要谷歌背景知识(或者你可以留下回复来问我)。 另外,为了使这个教程更实用,我尽量写下我是如何解决每个问题的细节,希望它能帮助你建立你选择的库。
在这一部分中,你将学习:
- 如何使用Docker设置Emscripten的环境
emconfigure
和emmake
的用法- 如何解决用Emscripten编译FFmpeg时的问题
如何使用Docker设置Emscripten的环境
在编译WebAssembly版本的FFmpeg(ffmpeg.wasm):(1)准备中,我们已经用GCC构建了原始版本的FFmpeg,现在我们转而使用Emscripten。
我们要使用的Emscripten版本是1.39.18(trzeci/emscripten:1.39.18-upstream),你可以通过官方教程安装Emscripten(在本教程中,我们在MacOS中使用setup-emsdk Github Actions)或者从docker hub拉取Emscripten镜像。
代码语言:javascript复制$ docker pull trzeci/emscripten:1.39.18-upstream
这可能需要几分钟的时间,因为其大小约为1GB。
然后我们需要更新build-with-docker.sh
,如下所示:
#!/bin/bash -x
EM_VERSION=1.39.18-upstream
docker pull trzeci/emscripten:$EM_VERSION
docker run
-v $PWD:/src
-v $PWD/cache-wasm:/emsdk_portable/.data/cache/wasm
trzeci/emscripten:$EM_VERSION
sh -c 'bash ./build.sh'
第8行’-v $PWD/cache-wasm:/emsdk_portable/.data/cache/wasm’不是必须的,但它可以帮助你在后续的构建中加快速度。
接下来我们要做的是找到用emscripten编译FFmpeg的配置,这是一个尝试和错误的过程,需要搜索文档和保持耐心。
emconfigure
和emmake &
的用法以及如何解决用Emscripten编译FFmpeg的问题
让我们开始寻找正确配置的旅程。在上一篇文章中,它以./configure --disable-x86asm
开始,要用emscripten做,你需要把它改为emconfigure ./configure --disable-x86asm
。(关于emconfigure的细节,请查看这里),由于我们要进行交叉编译,我们需要添加交叉编译的标志来明确告诉FFmpeg。
让我们更新build.sh
,如下所示。
#!/bin/bash -x
# verify Emscripten version
emcc -v
# configure FFMpeg with Emscripten
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
)
emconfigure ./configure "${ARGS[@]}"
神奇的是,没有任何错误或任何不妥之处,那么我们是否只需要输入emmake make -j
就可以得到FFmpeg.wasm?不幸的是,答案是否定的。emconfigure
最重要的任务之一是将编译器从gcc替换成emcc(或g 替换成em ),但在./configure
的输出中,我们仍然得到gcc作为我们的编译器。
emscripten sdl2-config called with /emsdk_portable/emscripten/tag-1.38.45/system/bin/sdl2-config --cflags
emscripten sdl2-config called with /emsdk_portable/emscripten/tag-1.38.45/system/bin/sdl2-config --libs
install prefix /usr/local
source path .
C compiler gcc # Should be emcc
C library glibc
ARCH x86 (generic)
big-endian no
runtime cpu detection yes
standalone assembly no
x86 assembler nasm
每个自动化工具都有其局限性,在这种情况下我们需要手动操作。让我们检查一下是否有任何参数可以拯救我们。
代码语言:javascript复制$ ./configure --help
在Toolchain
选项下,有一些参数用来指定要使用的编译器。
root@57ab95def750:/src# ./configure --help
Usage: configure [options]
Options: [defaults in brackets after descriptions]
Help options:
...
Toolchain options:
...
--nm=NM use nm tool NM [nm -g]
--ar=AR use archive tool AR [ar]
--as=AS use assembler AS []
--ln_s=LN_S use symbolic link tool LN_S [ln -s -f]
--strip=STRIP use strip tool STRIP [strip]
--windres=WINDRES use windows resource compiler WINDRES [windres]
--x86asmexe=EXE use nasm-compatible assembler EXE [nasm]
--cc=CC use C compiler CC [gcc]
--cxx=CXX use C compiler CXX [g ]
--objcc=OCC use ObjC compiler OCC [gcc]
--dep-cc=DEPCC use dependency generator DEPCC [gcc]
--nvcc=NVCC use Nvidia CUDA compiler NVCC [nvcc]
--ld=LD use linker LD []
...
让我们在build.sh
中传递这些参数来用emscripten进行编译。
#!/bin/bash -x
# verify Emscripten version
emcc -v
# configure FFMpeg with Emscripten
FLAGS=(
--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
--nm="llvm-nm"
--ar=emar
--ranlib=emranlib
--cc=emcc
--cxx=em
--objcc=emcc
--dep-cc=emcc
)
emconfigure ./configure "${FLAGS[@]}"
对于本地构建,请确保
llvm-ranlib
、llvm-as
和llvm-nm
存在。如果没有,你可以在$EMSDK_ROOT/upstream/bin
中找到它们。
有了这些参数,./configure
将需要更多的时间来运行,但你最终会得到想要的输出。
emscripten sdl2-config called with /emsdk_portable/emscripten/tag-1.39.18/system/bin/sdl2-config --cflags
emscripten sdl2-config called with /emsdk_portable/emscripten/tag-1.39.18/system/bin/sdl2-config --libs
install prefix /usr/local
source path .
C compiler emcc # emcc as expected
C library
ARCH x86 (generic)
big-endian no
runtime cpu detection yes
standalone assembly no
在build.sh
的结尾处添加emmake make -j4
(你可以将并行性提高到像-j8
那样,或者干脆使用-j
来使用所有内核):
#!/bin/bash -x
# verify Emscripten version
emcc -v
# configure FFMpeg with Emscripten
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
--nm="llvm-nm"
--ar=emar
--ranlib=emranlib
--cc=emcc
--cxx=em
--objcc=emcc
--dep-cc=emcc
)
emconfigure ./configure "${ARGS[@]}"
# build ffmpeg.wasm
emmake make -j4
然后它运行完立马报错:
代码语言:javascript复制...
./libavutil/x86/timer.h:39:24: error: invalid output constraint '=a' in asm
: "=a" (a), "=d" (d));
^
从输出信息中,我们可以确定这个错误与asm有关。打开./libavutil/x86/timer.h
,我们可以确认这个问题是由x86内联程序引起的,它与WebAssembly不兼容,所以解决办法是在build.sh
中禁用它。
#!/bin/bash -x
# verify Emscripten version
emcc -v
# configure FFMpeg with Emscripten
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
--nm="llvm-nm"
--ar=emar
--ranlib=emranlib
--cc=emcc
--cxx=em
--objcc=emcc
--dep-cc=emcc
)
emconfigure ./configure "${ARGS[@]}"
# build ffmpeg.wasm
emmake make -j4
它可以工作,并且一直在编译,直到我们遇到另一个错误。
代码语言:javascript复制...
CC libavfilter/dnn/dnn_backend_native_layers.o
In file included from libavfilter/aeval.c:26:
In file included from ./libavutil/avassert.h:31:
In file included from ./libavutil/avutil.h:296:
In file included from ./libavutil/common.h:533:
In file included from ./libavutil/internal.h:176:
./libavutil/libm.h:54:32: error: static declaration of 'cbrt' follows non-static declaration
static av_always_inline double cbrt(double x)
^
/emsdk_portable/upstream/emscripten/system/include/libc/math.h:151:13: note: previous declaration is here
double cbrt(double);
^
In file included from libavfilter/aeval.c:26:
这一次,根本原因不是那么明显,所以我们需要在./configure
过程中深入挖掘出错的原因。一个非常有用的文件是ffbuild/config.log
,它包含了./configure
期间的日志,大多数时候你可以在那里找到根本原因。
通过在config.log
中搜索cbrt
,我们发现了以下错误信息:
...
check_mathfunc cbrt 1 test_ld cc test_cc BEGIN /tmp/ffconf.syfN4Irw/test.c 1 #include <math.h> 2 float foo(float f, float g) { return cbrt(f); }
3 int main(void){ return (int) foo; }
END /tmp/ffconf.syfN4Irw/test.c
emcc -D_ISOC99_SOURCE -D_FILE_OFFSET_BITS=64 -D_LARGEFILE_SOURCE -D_POSIX_C_SOURCE=200112 -D_XOPEN_SOURCE=600 -std=c11 -fomit-frame-pointer -pthread -c -o /tmp/ffconf.syfN4Irw/test.o /tmp/ffconf.syfN4Irw/test.c emcc -Wl,-z,noexecstack -o /tmp/ffconf.syfN4Irw/test /tmp/ffconf.syfN4Irw/test.o wasm-ld: error: 'atomics' feature is used by /tmp/ffconf.syfN4Irw/test.o, so --shared-memory must be used
...
这个测试试图检查cbrt是否在工作,但由于atomics
功能出错而失败。atomics
是在你使用pthread
时被询问的,所以让我们添加pthread
标志。(关于pthread
标志的更多细节请看这里)
更新build.sh:
代码语言:javascript复制#!/bin/bash -x
# verify Emscripten version
emcc -v
# configure FFMpeg with Emscripten
CFLAGS="-s USE_PTHREADS"
LDFLAGS="$CFLAGS"
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
--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 "${ARGS[@]}"
# build ffmpeg.wasm
emmake make -j4
它可以工作,并且一直在编译,直到我们遇到另一个错误:
代码语言:javascript复制...
LD ffplay_g emcc: warning: ignoring unsupported linker flag: `-rpath-link=:libpostproc:libswresample:libswscale:libavfilter:libavdevice:libavformat:libavcodec:libavutil:libavresample` [-Wlinkflags]
7 warnings generated.
wasm-ld: error: initial memory too small, 19491744 bytes needed
...
make: *** [Makefile:114: ffplay_g] Error 1
make: *** Waiting for unfinished jobs....
emcc: warning: ignoring unsupported linker flag: `-rpath-link=:libpostproc:libswresample:libswscale:libavfilter:libavdevice:libavformat:libavcodec
:libavutil:libavresample` [-Wlinkflags]
...
这次的问题是由于初始内存太小(Emscripten默认只有16MB,而这里最小的是19 MB),所以我们需要通过-s INITIAL_MEMORY=33554432
(32MB)来提高初始内存的值。
#!/bin/bash -x
# verify Emscripten version
emcc -v
# configure FFMpeg with Emscripten
CFLAGS="-s USE_PTHREADS"
LDFLAGS="$CFLAGS -s INITIAL_MEMORY=33554432" # 33554432 bytes = 32 MB
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
--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 "${ARGS[@]}"
# build ffmpeg.wasm
emmake make -j4
在这次修复后,仍然有一个错误:
代码语言:javascript复制LD ffplay_g
emcc: warning: ignoring unsupported linker flag: `-rpath-link=:libpostproc:libswresample:libswscale:libavfilter:libavdevice:libavformat:libavcodec:libavutil:libavresample` [-Wlinkflags]
6 warnings generated.
LD ffmpeg_g
emcc: warning: ignoring unsupported linker flag: `-rpath-link=:libpostproc:libswresample:libswscale:libavfilter:libavdevice:libavformat:libavcodec:libavutil:libavresample` [-Wlinkflags]
9 warnings generated.
LD ffprobe_g
emcc: warning: ignoring unsupported linker flag: `-rpath-link=:libpostproc:libswresample:libswscale:libavfilter:libavdevice:libavformat:libavcodec:libavutil:libavresample` [-Wlinkflags]
STRIP ffmpeg
strip:ffmpeg_g: file format not recognized
make: *** [Makefile:107: ffmpeg] Error 1
make: *** Waiting for unfinished jobs....
由于我们无法剥离(因为它不是有效的二进制格式),让我们简单地用--disable-stripping
禁止剥离,然后重新制作。
#!/bin/bash -x
# verify Emscripten version
emcc -v
# configure FFMpeg with Emscripten
CFLAGS="-s USE_PTHREADS"
LDFLAGS="$CFLAGS -s INITIAL_MEMORY=33554432" # 33554432 bytes = 32 MB
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 stripping
--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 "${ARGS[@]}"
# build ffmpeg.wasm
emmake make -j4
最后我们成功地完成了emmake make -j
部分,你可以看到ffplay / ffplay_g、ffprobe / ffprobe_g和ffmpeg / ffmpeg_g在根文件夹中生成。它看起来很完美,但有一个奇怪的_g
后缀使输出文件像这样。
- ffmpeg
- ffmpeg_g
- ffmpeg_g.wasm
- ffmpeg_g.worker.js
这里的ffmpeg和ffmpeg_g都是实际的js文件,理想的命名方式如下:
- ffmpeg / ffmpeg_g => ffmpeg.js
- ffmpeg_g.wasm => ffmpeg.wasm
- ffmpeg_g.worker.js => ffmpeg.worker.js
为了解决这个问题,我们需要自己来构建它。建立ffmpeg的命令可以通过运行emmake make -n
来提取。
...
printf "LDt%sn" ffmpeg_g; emcc -Llibavcodec -Llibavdevice -Llibavfilter -Llibavformat -Llibavresample -Llibavutil -Llibpostproc -Llibswscale -Llibswresample -Wl,--as-needed -Wl,-z,noexecstack -Wl,--warn-common -Wl,-rpath-link=libpostproc:libswresample:libswscale:libavfilter:libavdevice:libavformat:libavcodec:libavutil:libavresample -Qunused-arguments -o ffmpeg_g fftools/ffmpeg_opt.o fftools/ffmpeg_filter.o fftools/ffmpeg_hw.o fftools/cmdutils.o fftools/ffmpeg.o -lavdevice -lavfilter -lavformat -lavcodec -lswresample -lswscale -lavutil -lm -pthread -lm -lm -pthread -lm -lm -lm -pthread -lm
printf "CPt%sn" ffmpeg; cp -p ffmpeg_g ffmpeg
...
再加上下面的命令:
代码语言:javascript复制emcc
-I. -I./fftools
-Llibavcodec -Llibavdevice -Llibavfilter -Llibavformat -Llibavresample -Llibavutil -Llibpostproc -Llibswscale -Llibswresample
-Qunused-arguments
-o ffmpeg_g fftools/ffmpeg_opt.o fftools/ffmpeg_filter.o fftools/ffmpeg_hw.o fftools/cmdutils.o fftools/ffmpeg.o
-lavdevice -lavfilter -lavformat -lavcodec -lswresample -lswscale -lavutil -lm
由于我们正在构建自己的版本,让我们在./configure
步骤中添加--disable-programs
和--disable-doc
,以加快构建速度,同时在构建ffmpeg时添加一些必要的标志。
#!/bin/bash -x
# verify Emscripten version
emcc -v
# configure FFMpeg with Emscripten
CFLAGS="-s USE_PTHREADS"
LDFLAGS="$CFLAGS -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 stripping
--disable-programs # disable programs build (incl. ffplay, ffprobe & ffmpeg)
--disable-doc # disable doc
--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 dependencies
emmake make -j4
# build ffmpeg.wasm
mkdir -p wasm/dist
ARGS=(
-I. -I./fftools
-Llibavcodec -Llibavdevice -Llibavfilter -Llibavformat -Llibavresample -Llibavutil -Llibpostproc -Llibswscale -Llibswresample
-Qunused-arguments
-o wasm/dist/ffmpeg.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
-s USE_SDL=2 # use SDL2
-s USE_PTHREADS=1 # enable pthreads support
-s INITIAL_MEMORY=33554432 # 33554432 bytes = 32 MB
)
emcc "${ARGS[@]}"
让我们创建一个basic.html来测试ffmpeg.wasm是否在工作。
代码语言:javascript复制<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title></title>
<script src="./dist/ffmpeg.js"></script>
</head>
<body>
</body>
</html>
运行一个轻量级的网络服务器(例如python3 -m http.server 3000
)并访问网页(例如http://localhost:3000/basic.html),然后打开Chrome DevTools。
你可以看到它的输出与原来的FFmpeg相似,它给了我们一个很好的起点来打磨我们的ffmpeg.wasm库。
你可以在这里访问资源库,看看它是如何工作的:https://github.com/ffmpegwasm/FFmpeg/tree/n4.3.1-p2
也可以在这里下载构建的工件:https://github.com/ffmpegwasm/FFmpeg/releases/tag/n4.3.1-p2
关于如何打磨和创建一个 “真正的 “ffmpeg.wasm库,请关注编译WebAssembly版本的FFmpeg( ffmpeg.wasm):(3) ffmpeg.wasm v0.1 - 将avi转为mp4的新一篇文章。