gogoend / blog

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

[译] 构建WebAssembly版本的FFmpeg——ffmpeg.wasm:第二部分:使用Emscripten编译 #46

Closed gogoend closed 3 years ago

gogoend commented 4 years ago

原文:https://itnext.io/build-ffmpeg-webassembly-version-ffmpeg-js-part-2-compile-with-emscripten-4c581e8c9a16 作者:Jerome Wu 翻译:gogoend

2020/2/11: 根据本文的一些回应来看,在Mac环境下无法按照下列说明进行操作。由于我没有Mac,因此无法解决这一问题。在Mac中最简单的解决问题的方法,便是在虚拟机里安装Linux发行版,例如Ubuntu。

前一篇文章:[译] 构建WebAssembly版本的FFmpeg——ffmpeg.wasm:第一部分:准备

从这里开始,很多东西将会变得更加复杂,变得难以理解。如果你不知道将会发生什么,你或许需要使用Google来查找一些背景知识(或者你也可以留下回应来询问我(作者))。 此外,为了让这篇教程更加实用,我将尝试写下我所遇到的每一个问题的解决过程,希望它可以帮助你构建你所选择的库。

在这一部分你将会了解到:

  1. 如何使用Docker来设置Emscripten环境
  2. emconfigureemmake命令的用法
  3. 如何修复在使用Emscripten编译FFmpeg过程中所遇到的问题

如何使用Docker来配置Emscripten的环境

构建WebAssembly版本的FFmpeg——ffmpeg.wasm:第一部分:准备一文中,我们使用gcc的Docker镜像,构建了原始版本的FFmpeg。现在,我们将移步使用Emscripten的Docker镜像。

在这里我使用的是trzeci/emscripten,标签为1.38.45。首先我们把它拉取下来。

$ docker pull trzeci/emscripten:1.38.45

这里将会花费一些时间,因为镜像大致有1G。

接下来要做的是,找到使用emscripten来构建FFmpeg的配置。这是一个不断尝试错误的过程,需要十分耐心地阅读很多文档。首先我们来运行一个emscripten容器,并挂载FFmpeg的源代码到/src目录。

# Make sure you are in the root of the FFmpeg repository
$ docker run -it \
  -v $PWD:/src \
  trzeci/emscripten:1.38.45 \
  /bin/bash

在Docker容器中,键入ls --color,然后你将会得到类似如下的输出: image

emconfigureemmake &的用法;如何修复在使用Emscripten编译FFmpeg时所发生的错误

我们来开始寻找正确配置的旅程。在第一部分中,这段旅程的第一步是./configure --disable-x86asm。若要使用emscripten,你需要将其更改为emconfigure ./configure --disable-x86asm。(对于emconfigure的其他详情,请参阅这里

$ emconfigure ./configure --disable-x86asm

令人惊讶的是,这里并没有报出任何错误,那我们是不是只需要执行emmake make -j4就可以得到FFmpeg.wasm了呢?不好意思,你想多了。emconfig其中一个最重要的任务,是将编译器从gcc替换为emcc(或是g++、em++),但是在输出的./configure文件中,我们依然将gcc作为了我们的编译器。

root@57ab95def750:/src# emconfigure ./configure --disable-x86asm                                                                 
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

每一个自动化工具都有其自身的局限性,因此我们需要手动来处理这个问题。来看看这里有没有什么参数能够拯救我们。

$ ./configure --help

Toolchain options章节下,有一些参数,用于指定要使用的编译器。

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 []
...

我们将这些参数传递给emscripten来进行编译

$ emconfigure ./configure \
  --disable-x86asm \
  --nm="llvm-nm -g" \
  --ar=emar \
  --cc=emcc \
  --cxx=em++ \
  --objcc=emcc \
  --dep-cc=emcc

有了这些参数以后,./configure将会花费更多时间来运行,但是在最后,你将会得到你想要的输出。

root@57ab95def750:/src# emconfigure ...                                                                                                                                                                        
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                emcc         # emcc as expected                                                                                    
C library                                                                                                                          
ARCH                      x86 (generic)                                                                                                     
big-endian                no                                                                                                              
runtime cpu detection     yes                                                                                                             
standalone assembly       no

我们来试着执行make,来看看将会发生什么

$ emmake make -j4

不出几秒,就失败了……

root@57ab95def750:/src# emmake make -j4
...
./libavutil/x86/timer.h:39:24: error: invalid output constraint '=a' in asm
                     : "=a" (a), "=d" (d));
                       ^

从输出信息中,我们可以看到这一错误与汇编有关。打开./libavutil/x86/timer.h,然后我们发现该问题是由x86的行内汇编导致的,这与WebAssembly不兼容,我们来将其禁用。

$ emconfigure ./configure \
  --disable-x86asm \
  --disable-inline-asm \    # Disable inline asm
  --nm="llvm-nm -g" \
  --ar=emar \
  --cc=emcc \
  --cxx=em++ \
  --objcc=emcc \
  --dep-cc=emcc

在configure之后,我们再次进行make。

$ emmake make -j4

此时编译器可以正常工作,并持续编译,直到我们又遇到了另一个错误。

root@57ab95def750:/src# emmake make -j4
...
AR      libavdevice/libavdevice.a
AR      libavfilter/libavfilter.a
AR      libavformat/libavformat.a
AR      libavcodec/libavcodec.a
AR      libswresample/libswresample.a
AR      libswscale/libswscale.a
AR      libavutil/libavutil.a
HOSTLD  doc/print_options
GENTEXI doc/avoptions_format.texi
/bin/sh: 1: doc/print_options: Exec format error
doc/Makefile:59: recipe for target 'doc/avoptions_format.texi' failed
make: *** [doc/avoptions_format.texi] Error 2
make: *** Waiting for unfinished jobs....

显然,该错误和文档生成相关,事实上我们也不关心这些文档,因此我们可以将其禁用。

$ emconfigure ./configure \
  --disable-x86asm \
  --disable-inline-asm \
  --disable-doc \    # Disable document generation
  --nm="llvm-nm -g" \
  --ar=emar \
  --cc=emcc \
  --cxx=em++ \
  --objcc=emcc \
  --dep-cc=emcc

然后我们再次进行make

$ emmake make -j4

现在我们已经通过了文档部分,却又败在了strip部分。

root@57ab95def750:/src# emmake make -j4
...
STRIP   ffmpeg
STRIP   ffprobe
strip:ffmpeg_g: File format not recognized
strip:ffprobe_g: File format not recognized
Makefile:101: recipe for target 'ffmpeg' failed
make: *** [ffmpeg] Error 1
make: *** Waiting for unfinished jobs....
Makefile:101: recipe for target 'ffprobe' failed
make: *** [ffprobe] Error 1

由于原始的strip和我们的WebAssembly不兼容,因此我们需要禁用它。

$ emconfigure ./configure \
  --disable-x86asm \
  --disable-inline-asm \
  --disable-doc \
  --disable-stripping \    # Disable stripping
  --nm="llvm-nm -g" \
  --ar=emar \
  --cc=emcc \
  --cxx=em++ \
  --objcc=emcc \
  --dep-cc=emcc

第四次执行make

$ emmake make -j4

最终make过程没有任何错误的结束了。🎉但是我们所得到的输出是一个ffmpeg,它不是一个js或wasm文件,无法运行。若要生成js文件,你需要在emcc命令中使用-o ffmpeg.js。这里有两种方式:

  1. 修改FFmpeg的Makefile文件
  2. 进行额外的编译、链接。

这里,使用的是第二个选项,笔者个人不太想修改FFmpeg的源代码,因为这可能会引发一些副作用。因此我们需要知道ffmpeg是如何使用make命令进行生成的。这里我们使用make的空运行功能。

$ emmake make -n

然后我们可以在输出中看到详尽的命令

root@57ab95def750:/src# emmake make -n
...
printf "LD\t%s\n" 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 "CP\t%s\n" ffmpeg; cp -p ffmpeg_g ffmpeg
...

看起来它有些乱,我们把未使用的参数移除(编译结束后你将看到),重新排列这些参数,并将ffmpeg_g重命名为ffmpeg.js

$ emcc \
  -Llibavcodec -Llibavdevice -Llibavfilter -Llibavformat -Llibavresample -Llibavutil -Llibpostproc -Llibswscale -Llibswresample \
  -Qunused-arguments \
  -o ffmpeg.js 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

该命令应当是可以工作的,但我们总会遇到总内存不足的问题。

root@57ab95def750:/src# emcc ...
shared:ERROR: Memory is not large enough for static data (11794000) plus the stack (5242880), please increase TOTAL_MEMORY (16777216) to at least 17037904

我们来加一个参数,以扩大TOTAL_MEMORY为原先两倍(33554432 Bytes := 32 MB)

$ emcc \
  -Llibavcodec -Llibavdevice -Llibavfilter -Llibavformat -Llibavresample -Llibavutil -Llibpostproc -Llibswscale -Llibswresample \
  -Qunused-arguments \
  -o ffmpeg.js 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 \
  -s TOTAL_MEMORY=33554432

最后,我们将得到js文件和wasm文件。

root@57ab95def750:/src# ls ffmpeg*   
ffmpeg  ffmpeg.js  ffmpeg.js.mem  ffmpeg.wasm  ffmpeg.worker.js  ffmpeg_g

我们来建立一个test.html文件,来测试FFmpeg.js是否能够工作。

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title></title>
  <script src="./ffmpeg.js"></script>
</head>
<body>
</body>
</html>

运行一个轻量服务器(例如python2 -m SimpleHTTPServer),浏览网页(例如http://localhost:8000/test.html),并打开Chrome开发者工具。

// 我是图片

我们可以看到它工作起来了,因为你可以看到一些类似于原始FFmpeg的输出。我们有了一个很好的起点来完善ffmpeg.wasm库。

对于如何创建并完善一个真正可用的ffmpeg.wasm库,请参阅本系列文章构建WebAssembly版本的FFmpeg——ffmpeg.wasm:第三部分:ffmpeg.wasm v0.1.0 —— 将avi转码为mp4

你可以在这一仓库找到构建脚本与输出:https://github.com/ffmpegwasm/FFmpeg (可参考build-with-docker.sh 和 build-js.sh)