关于ffmpeg:基于FFmpeg和Wasm的Web端视频截帧方案

4次阅读

共计 10218 个字符,预计需要花费 26 分钟才能阅读完成。

作者 | 小萱

导读 

基于理论业务需要,介绍了自定义 Wasm 截帧计划的实现原理和实现计划。解决传统的基于 canvas 的截帧计划所存在的问题,更高效灵便的实现截帧能力。

全文 10103 字,预计浏览工夫 26 分钟。

01 我的项目背景

在视频编辑器里常见这样的性能,在用户上传完视频后抽取关键帧,提供给用户以便快捷选取封面,如下图:

在本文中,咱们将探讨一种应用 FFmpeg 和 WebAssembly(Wasm)的 Web 端视频截帧计划,以解决传统的基于 canvas 的截帧计划所存在的问题。通过采纳这种新办法,咱们能够克服 video 标签的限度,实现更高效、更灵便的视频截帧性能。

首先,咱们须要理解一下传统的 Web 截帧计划的局限性。尽管该计划在解决一些常见的视频格式(如 MP4、WebM 和 OGG)时体现良好,但其存在以下缺点:

  • 类型无限:video 标签反对的视频格式非常无限,无奈解决一些其余常见的视频格式,如 FLV、MKV 和 AVI 等。
  • DOM 依赖:该计划依赖于 DOM,只能在主线程中实现。这意味着在解决大量截帧工作时,可能会对页面性能产生负面影响。
  • 抽帧策略局限:传统计划无奈准确管制抽帧策只能传递工夫交给浏览器,设置 currentTime 时会解码寻找最靠近的帧,而非关键帧。

为解决上述问题,选取 FFmpeg+Wasm 的计划,通过自定义编译 FFmpeg,在 web-worker 里执行 rgb24 格局数据到 ImageData 的运算,再传递后果给主线程,实现。

02 Wasm 外围原理

2.1 Wasm 是什么

用官网的话说,WebAssembly(缩写为 Wasm)是一种用于基于堆栈的虚拟机的二进制指令格局。

WebAssembly (abbreviated Wasm) is a binary instruction format for a stack-based virtual machine. Wasm is designed as a portable compilation target for programming languages, enabling deployment on the web for client and server applications.  

— https://webassembly.org/

Wasm 能够看作一种容器技术,它定义了一种独立的、可移植的虚拟机,能够在各种平台上执行,类比于 docker,但更为轻量。WebAssembly 于 2017 年弹冠相庆,2019 年 12 月正式认证为 Web 规范之一并被举荐,领有高性能、跨平台、安全性、多语言高可移植等劣势。

业界有很多 Wasm 虚拟机的实现,蕴含解释器,单层 / 多层 AOT、JIT 模式。

2.2 chrome 如何运行 Wasm

浏览器内置 JIT 引擎,V8 应用了分层编译模式(Tiered)来编译和优化 WASM 代码。分层编译模式包含两个次要的编译器:

  1. 基线编译器(Baseline compiler)Liftoff 编译器
  2. 优化编译器(Optimizing compiler)TurboFun 编译器

2.2.1 Liftoff 编译器

当 WASM 代码首次加载时,V8 应用 Liftoff 编译器进行疾速编译。Liftoff 是一个线性工夫编译器,它能够在极短的工夫内为每个 WASM 指令生成机器代码。这意味着,它能够尽快地生成可执行代码,从而缩短代码加载工夫。

然而,Liftoff 编译器的优化空间无限。它采纳一种简略的一对一映射策略,将 WASM 指令独立地转换为机器代码,而不进行任何高级优化。这使得生成的代码性能较低。

2.2.2 TurboFan 编译器

对于那些被频繁调用的热函数(Hot Functions),V8 会应用 TurboFan 编译器进行优化编译。TurboFan 是一个更高级的编译器,可能执行各种简单的优化技术,如内联缓存(Inline Caching)、死代码打消(Dead Code Elimination)、循环展开(Loop Unrolling)和常量折叠(Constant Folding)等,从而显著进步代码的运行效率。

V8 会监控 WASM 函数的调用频率。一旦一个函数达到特定的阈值,它就会被认为是 Hot,并在后盾线程中触发从新编译。在优化编译实现后,新生成的 TurboFan 代码会替换原有的 Liftoff 代码。之后对该函数的任何新调用都将应用 TurboFan 生成的新的优化代码,而不是 Liftoff 代码。

2.2.3 流式编译与代码缓存

V8 引擎反对流式编译(Streaming Compilation),这意味着 WASM 代码能够在下载的同时进行编译。这大大缩短了从加载到可执行的总工夫。流式编译在基线编译阶段(Liftoff 编译器)尤为重要,因为它能够确保 WASM 代码在最短的工夫内变得可运行。

为了进一步提高性能和加载速度,V8 引擎反对代码缓存(Code Caching)机制。代码缓存能够将编译后的 WASM 代码存储在缓存中,以便在未来须要时间接从缓存中加载,而无需从新编译。这大大缩短了页面加载工夫,进步了用户体验。目前 WebAssembly 缓存仅针对流式 API 调用,compileStreaming 和 instantiateStreaming 这两个 API,应用流式 API 领有更好的性能。对于缓存的工作原理:

  1. 当 TurboFan 实现编译后,如果.wasm 资源足够大(128 kb),Chrome 会将编译后的代码写入 WebAssembly 代码缓存。
  2. 当.wasm 第二次申请资源时(hot run),Chrome.wasm 从资源缓存中加载资源,同时查问代码缓存。如果缓存命中,编译后的 module bytes 将发送到渲染器过程并传递给 V8,V8 将其进行反序列化,与编译相比,反序列化速度更快,占用的 CPU 更少。
  3. 如果.wasm 资源产生了变动或是 V8 产生了变动,缓存会生效,缓存的本地代码会从缓存中革除,编译会像步骤 1 一样持续进行。
2.2.6 编译管道(Compilation Pipeline)

△频成果 V8 编译 Wasm 的流程图

V8 编译 WASM 代码的整个过程能够概括为以下几个步骤:

  1. 解码(Decoding):首先,将 WASM 模块解码为二进制可执行代码,并验证其是否合乎 WASM 规范。
  2. 基线编译(Baseline Compilation):接下来,应用 Liftoff 编译器进行疾速编译。这一阶段生成的代码性能较低,但编译速度快。流式编译在这个阶段发挥作用,使得代码在下载过程中就能进行编译。
  3. 热点剖析(Hotspot Analysis):V8 引擎会继续监控 WASM 函数的调用频率,以辨认 Hot Function。
  4. 优化编译(Optimizing Compilation):对于被标记为热门函数的代码,应用 TurboFan 编译器进行优化编译。编译实现后,优化后的代码会替换原有的 Liftoff 代码。这一过程称为分层降级(Tier-up)。
  5. 执行(Execution):在优化编译实现后,代码将在 V8 引擎中运行。

比照 V8 执行 js 的流程,省去了Parser生成 ast,Ignition生成字节码的的过程,因而有更高的性能和执行效率。

03 FFmpeg 的介绍

FFmpeg 作为一个开源的弱小的音视频解决工具,实现视频和音频的录制、转换、编辑等多种性能。FFmpeg 蕴含了泛滥的编码库和工具,能够解决各种格局的音视频文件,例如 MPEG、AVI、FLV、WMV、MP4 等等。

FFmpeg 最后是由 Fabrice Bellard 于 2000 年创建的,当初它是由一个宏大的社区保护的开源软件我的项目。FFmpeg 反对各种操作系统,包含 Windows、macOS、Linux 等,也反对各种硬件平台,例如 x86、ARM 等。

FFmpeg 的性能十分弱小,能够进行很多简单的音视频解决操作,例如视频转码、视频合并、音频剪辑、音频混合等等。FFmpeg 反对泛滥编码格局和协定,包含 H.264、HEVC、VP9、AAC、MP3 等等。同时,它还能够进行流媒体的解决,例如将视频流推送到 RTMP 服务器、从 RTSP 服务器拉取视频流等等。

04 截帧策略的制订

4.1 I、B、P 帧是什么

这个概念来源于视频编码,为形容视频压缩编码中的帧类型。

I 帧(Intra-coded frame),也叫关键帧(keyframe),它是视频序列中的一种独立帧,也就是说,它不须要参考其它帧进行解码。I 帧通常用来作为视频序列的参考点,后续的 B 帧和 P 帧都会参考它进行编码。I 帧通常具备较高的压缩比和较大的文件大小,然而它也提供了最高的图像品质。

P 帧(Predictive-coded frame) 是通过对后面的 I 帧或 P 帧进行静止预测失去的帧,也就是说,P 帧须要参考后面的一个或多个帧进行解码。P 帧通常比 I 帧小一些,然而它的压缩比比 I 帧高。

B 帧(Bidirectionally-predictive-coded frame) 是通过对后面和前面的帧进行静止预测失去的帧,也就是说,B 帧须要参考后面和前面的帧进行解码。B 帧通常比 P 帧更小,因为它能够更充沛地利用前后两个参考帧之间的冗余信息进行编码。

因而,视频编码中通常会应用一种叫做“三合一”编码的形式,行将一个 I 帧和它后面的若干个 P 帧以及前面的若干个 B 帧组成一个 GOP(Group of Pictures)。这样的编码方式既能够进步编码的效率,也能够提供高质量的图像。

△I、B、P 帧关系示例图

4.2 关键帧生成策略

视频编辑器抽帧的目标是为用户提供无效的封面图选取,因而咱们心愿抽出来蕴含较大信息量品质较高的图作为抽帧产物,从下面的介绍可知,个别状况下关键帧是蕴含信息量较大的帧,因而现实状态是只产出关键帧。

依照需要场景,咱们须要对每个视频提取 12 张图片。若应用 canvas 抽帧计划,就意味着这 12 张图片只能依据工夫距离进行抽取,无奈应用视频自身的关键帧信息,图片可能是关键帧,也可能是 BP 帧。非关键帧的图片往往品质较差不适宜作为封面图。且浏览器也须要基于 I 帧进行逐帧的解码,这会消耗较长的工夫。因而咱们决定借助 FFmpeg 库的能力,生成关键帧。

为什么不间接应用 FFmpeg 的命令生成关键帧呢,一个视频具体有多少张关键帧这是不肯定的,可能多于 12 张也可能少于 12 张,因而只用 FFmpeg 的命令生成关键帧一把梭生成全副关键帧这是不够的。

对于少于 12 张关键帧的视频,采取补齐的策略,在两关键帧之间,以 2s 为工夫距离进行补齐。如果两帧间隔时间有余 2s 距离调配,那就依照两关键帧间隔时间 / 在此距离须要补的帧数,计算出须要补齐的帧的所在工夫。

FFmpeg 在获取关键帧是很快的,因为关键帧的工夫信息是能够间接从视频里获取到的,能够间接调用av\_seek\_frame 跳到关键帧地位,而后解一帧即可,对于指定工夫的非关键帧的寻找,须要跳到最近的关键帧,再一帧帧的解包寻找,晓得寻找的指定的工夫,进行输入。

对于超出 12 帧关键帧的视频,依照相等的距离进行选取,比方有 24 张,那么选取 0、2、…23 索引的帧为输入帧。

其余的优化点,第一帧肯定是 I 帧,因而在第一工夫读取第一帧并返回,让用户霎时看到一帧,缩小视觉等待时间,其余帧每确定一帧是合乎输入帧就立刻输入,用户看到的是一帧帧输入的,而不是等到全副抽帧工作实现再输入。

△百家号 wasm 抽帧效果图

05 定义编译 FFmpeg

5.1 环境筹备

Emscripten、LLVM、Clang 都能够将 c、cpp 代码编译成 Wasm,咱们应用 Emscripten 编译。Emscripten 会帮你生成胶水代码 (.js 文件) 和 Wasm 文件。

首先下载 emsdk,执行以下命令配置并激活已装置的 Emscripten。

git clone https://github.com/emscripten-core/emsdk.git
cd emsdk
  git pull
  ./emsdk install latest
  ./emsdk activate latest
   source ./emsdk_env.sh

最初 source 环境变量,配置 Emscripten 各个组件的 PATH 等环境变量。

5.2 编译 FFmpeg

为了产出能在以在浏览器中运行的 WebAssembly 版本的 FFmpeg,咱们禁用了大部分针对特定平台或体系结构的优化,以便生成尽可能兼容的 WebAssembly 代码。

应用 Emscripten 的emconfigure命令运行 FFmpeg 的configure脚本,传入自定义参数以便实现兼容。上面是自定义参数:

CFLAGS="-s USE_PTHREADS"
LDFLAGS="$CFLAGS -s INITIAL_MEMORY=33554432" # 33554432 bytes = 32 MB
CONFIG_ARGS=(
  --prefix=$WEB_CAPTURE_PATH/lib2/ffmpeg-emcc \
  --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-12"
  --ar=emar
  --ranlib=emranlib
  --cc=emcc
  --cxx=em++
  --objcc=emcc
  --dep-cc=emcc
)
cd $FFMPEG_PATH
emconfigure ./configure "${CONFIG_ARGS[@]}"

PS:下面咱们容许了 C ++ 应用 pthread,但因为在浏览器应用 pthread 多线程须要SharedArrayBuffer 容许多个 Web Workers 或 WebAssembly 线程拜访和操作雷同的内存区域,而SharedArrayBuffer的兼容性较差,并且要求 https,因而咱们在接下来产出 wasm 时禁用 pthread。

FFmpeg 蕴含了很多库,若间接应用 @ffmpeg/ffmpeg @ffmpeg/core 便是全量的库的 wasm 版本。

  1. libavformat:负责多媒体文件和流的格局解决。这个库能够帮忙你读取和写入多种音频和视频文件格式,以及网络流。
  2. libavcodec:负责音视频编解码。这个库蕴含了泛滥的音频和视频编解码器,能够解决多种格局的音频和视频。
  3. libavutil:提供一些实用功能,例如内存治理、数学运算、工夫解决等。这个库被 libavformat 和 libavcodec 等其余库所应用,用于辅助解决各种工作。
  4. libswscale:负责图像的缩放和色彩空间转换。这个库能够帮忙你将视频帧从一种像素格局转换为另一种,或者对图像进行缩放。
  5. libswresample:负责音频重采样、混合和格局转换。这个库用于解决音频数据,例如扭转采样率、扭转声道数等。
  6. libavfilter:负责音视频滤镜解决。这个库提供了一系列音视频滤镜,用于解决音频和视频,例如调整色调、裁剪、增加水印等。
  7. libavdevice:负责获取和输出设备相干的操作。这个库提供了对各种设施的反对,例如摄像头、麦克风、屏幕捕获等。

而咱们抽帧只须要读取视频文件或流、解码、对产生的像素格局转换以及通用工具函数,也就是 libavformat、libavcodec、libswscale 和 libavutil 这几个库,在接下来产出 wasm 咱们便选取这几个库作为编译的输出文件,能够大幅缩小产出的 wasm 资源体积。

5.3 编译产出.wasm、.js

Emscripten 反对产出多种格式文件,咱们这里应用他为咱们筹备的胶水代码,故生成.wasm 和.js 文件,

应用 emcc 命令编译 cpp 代码,首先通过Clang编译为LLVM 字节码,而后依据不同的指标编译为asm.js或Wasm。因为外部调用Clang,因而emcc反对绝大多数的Clang编译选项,比方-s OPTIONS=VALUE、-O、-g等。除此之外,为了适应 Web 环境,emcc减少了一些特有的选项,如–pre-js <file>、–post-js <file>等。

emcc $WEB_CAPTURE_PATH/src/capture.c $FFMPEG_PATH/lib/libavformat.a $FFMPEG_PATH/lib/libavcodec.a $FFMPEG_PATH/lib/libswscale.a $FFMPEG_PATH/lib/libavutil.a \
    -O0 \
    # 应用 workerfs 文件系统
    -lworkerfs.js \
    # 讲这个文件内连到胶水 js 外面 共享上下文
    --pre-js $WEB_CAPTURE_PATH/dist/capture.worker.js \
    # 指定编译入口门路
    -I "$FFMPEG_PATH/include" \
    # 申明编译指标是 wasm
    -s WASM=1 \
    -s TOTAL_MEMORY=$TOTAL_MEMORY \
    # 通知编译器咱们心愿从编译后的代码中拜访哪些内容(如果不应用,内容可能会被删除)
    -s EXPORTED_RUNTIME_METHODS='["ccall","cwrap"]' \
    # 通知编译器须要塞到 Module 里的办法
    -s EXPORTED_FUNCTIONS='["_main","_free","_captureByMs","_captureByCount"]' \
    -s ASSERTIONS=0 \
    # 容许 wasm 的内存增长
    -s ALLOW_MEMORY_GROWTH=1 \
    # 产出门路
    -o $WEB_CAPTURE_PATH/dist/capture.worker.js

Emscripten 提供了四种文件系统,默认是MEMFS(memory fs),其余都须要在编译时候增加进来,-lnodefs.js (NODEFS), -lidbfs.js (IDBFS), -lworkerfs.js (WORKERFS), or -lproxyfs.js (PROXYFS)。咱们在 worker 中运行 wasm,选取workerfs文件系统,它提供了在 worker 中的 file 和 Blob 对象的只读拜访,而不须要将整个数据复制到内存中,可能用于微小的文件,避免了文件过大导致的浏览器 crash。

生成的 js 外面,Module 是全局 JavaScript 对象,Module 里固有的办法,能够参考文档 Module object documentation,同时,你也能够通过 –pre-js 往 Module 里增加办法,没有塞入 Module 的办法能够通过EXPORTED\_FUNCTIONS增加。

△Module 内办法的定义

5.4 Js 和 C 的通信

5.4.1 Js 调用 C

JavaScript 调用 C 只能应用Number作为参数,因而如果参数是数组、对象等非Number类型,就麻烦了,应用Module.\_malloc()分配内存,拿到栈指针地址,将数组拷贝到栈空间,将指针作为参数调用 c 的办法。Emscripten 的cwrap办法能够轻松解决。

crap(函数名,返回值,传入 c 的参数类型数组)

// example ts:captureByMs(info: 'string', path:'string', id:'number'):number
this.cCaptureByMs = Module.cwrap('captureByMs', 'number', ['string', 'string', 'number']);

5.4.2 C 调用 Js

能够通过emscripten\_run\_scriptapi 在 c 里调用 js,承受参数是拼接成字符串的要执行的 js 内容,用起来很像 eval。

emscripten_run_script("console.log('hi')");

如果传参是指针,js 的办法里承受到的是 c 的指针地址,在以后版本的 Emscripten 中,指针地址类型为 int32,Wasm 中 js 的内存空间均为ArrayBuffer,Emscripten 提供的拜访对象是Module.buffer, 然而 js 中的ArrayBuffer无奈间接拜访,Emscripten 提供TypedArray对象进行拜访。

比方须要传递给 js 是构造体指针,是这样定义的。

typedef struct
{
    uint32_t width;
    uint32_t height;
    uint32_t duration;
    uint8_t *data;
} ImageData;

构造体的内存对齐,所以选取最长的就是uint32\_t,uint32\_t对应的TypedArray数组是Module.HEAPU32,因为是 4 字节无符号整数,因而 js 拿到的 ptr 需除以 4(既右移 2 位)取得正确的索引。按此类比,8 字节无符号整数就须要右移 3 位。

尽管看起来 c 调用 js 很简略,但你不应该做频繁的调用,这会导致较大的开销对消掉 Wasm 自身的物理劣势。这也是为什么 dom 操作相干的框架不会选用 Wasm 进行优化,Wasm 还无奈间接操作 dom,频繁的 js 和 Wasm 的上下文的开销也带来不可漠视的性能缺失,他的目标从不是代替 js,类比 react,reconciler 局部是能够用 rust/go 重写,社区也有人做过此尝试,然而并没有带来显著性能劣势,社区也有用 go/rust 编写 web 利用的框架,比方(yew),他们为跨端带来更多的可能。

5.5 FFmpeg api 介绍

对整体抽帧流程应用到的要害 api 做简略的介绍,蕴含对视频的解码、编码以及解决等操作。

  • av\_register\_all 注册全副解码器,在应用 FFmpeg 的其余函数之前调用,以确保 Ffmpeg 能够正确地加载和初始化。

  • avformat\_open\_input 依据门路读取文件,并将其解析为一个AVFormatContext构造体,其中蕴含了文件的格局信息和媒体流的信息。

  • avformat\_find\_stream\_info 获取视频的媒体信息 类比 ffplay file 获取的信息,蕴含编码格局、视频长度、fps、分辨率等。

  • avcodec\_find\_decoder 寻找视频对应的解码器。

  • av\_read\_frame 大量耗时在解码环节,在解码前,能够通过读取压缩的帧信息,获取关键帧队列,AVPacket 构造体里的 flag 等于 1,标记该帧是关键帧。

  • av\_seek\_frame 疾速定位到某个工夫戳的视频帧,在这里应用它定位到关键帧。

  • 基于关键帧进行解包,先调用av\_read\_frame读取压缩帧,avcodec\_send\_packet发送压缩包到 FFmpeg 的解码队列(如果胜利,则返回 0),avcodec\_receive\_frame从解码队列里胜利取出,判断 pts(位于的工夫),符合条件的 frame 信息被存储。

△抽帧的要害代码及解释

5.6 编译后产物体积比照

自定义编译

应用 npm 包 @ffmpeg/ffmpeg @ffmpeg/core

比照全量引入 24.5M,咱们只须要 4M,体积上的收益还是非常明显的。

06 总结

应用 FFmepg+Wasm 计划进行视频抽帧,通过自定义编译 FFmpeg 缩小编译产物的体积;定义关键帧优先策略,第一工夫给到用户抽帧后果,尽可能减少用户等待时间。在 Emscripten 工具链的加持下,能够不便地将 C /C++ 代码编译成 Wasm,并配合产出残缺的与 web 的交互 js。在速度和体验以及视频兼容性方面都获得了较为显著的收益,请大胆拥抱 WebAssembly 为 web 赋能吧!

目前这套计划已在百家号视频场景落地数月,收益显著。

我的项目地址:https://github.com/wanwu/cheetah-capture,欢送 star。

封装好 api 反对依照帧数目和秒数抽取。你也抉择自定义编译,通过更改 FFmpeg 的编译参数让他反对更多的视频类型,通过更改 capture.c 文件减少更多 api 能力,期待你来丰盛更多场景。

——END——

举荐浏览

百度研发效力从度量到数字化变质之路

百度内容了解推理服务 FaaS 实战——Punica 零碎

精准水位在流批一体数据仓库的摸索和实际

视频编辑场景下的文字模版技术计划

浅谈流动场景下的图算法在反作弊利用

Serverless:基于个性化服务画像的弹性伸缩实际

正文完
 0