作者 | 小萱

导读

依据实践业务需求,介绍了自界说Wasm截帧计划的完结原理和完结计划。处理传统的依据canvas的截帧计划所存在的问题,更高效灵敏的完结截帧才能。

全文10103字,估计阅读时刻26分钟。

01 项目背景

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

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

在本文中,咱们将探讨一种运用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.

— webassembly.org/

Wasm 能够看作一种容器技术,它界说了一种独立的、可移植的虚拟机,能够在各种渠道上履行,类比于docker,但更为轻量。WebAssembly 于2017年粉墨登场,2019年12月正式认证为Web规范之一并被引荐,具有高功用、跨渠道、安全性、多语言高可移植等优势。

业界有许多Wasm虚拟机的完结,包括解说器,单层/多层AOT、JIT形式。

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

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 编译器的优化空间有限。它选用一种简略的1对1映射战略,将 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第2次请求资源时(hot run),Chrome.wasm从资源缓存中加载资源,一起查询代码缓存。假如缓存射中,编译后的module bytes将发送到渲染器进程并传递给 V8,V8将其进行反序列化,与编译比较,反序列化速度更快,占用的 CPU 更少。

  3. 假如.wasm资源发生了改动或是 V8 发生了改动,缓存会失效,缓存的本地代码会从缓存中清除,编译会像进程 1 相同继续进行。

2.2.6 编译管道(Compilation Pipeline)

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

△频作用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)。这样的编码办法既能够进步编码的功率,也能够供给高质量的图画。

基于FFmpeg和Wasm的Web端视频截帧方案
△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帧,因而在第一时刻读取第一帧并回来,让用户瞬间看到一帧,削减视觉等待时刻,其他帧每确定一帧是契合输出帧就立即输出,用户看到的是一帧帧输出的,而不是等到悉数抽帧任务完结再输出。

基于FFmpeg和Wasm的Web端视频截帧方案
△百家号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资源体积。

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

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 、–post-js 等。

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添加。

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

△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位。

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

虽然看起来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信息被存储。

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

△抽帧的关键代码及解说

5.6 编译后产品体积比照

自界说编译

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

运用npm包@ffmpeg/ffmpeg @ffmpeg/core

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

比照全量引进24.5M,咱们只需求4M,体积上的收益仍是十分显着的。

06 总结

运用FFmepg+Wasm计划进行视频抽帧,经过自界说编译FFmpeg削减编译产品的体积;界说关键帧优先战略,第一时刻给到用户抽帧成果,尽或许削减用户等待时刻。在 Emscripten 东西链的加持下,能够方便地将C/C++代码编译成Wasm,并配合产出完好的与web的交互js。在速度和体会以及视频兼容性方面都取得了较为显着的收益,请斗胆拥抱WebAssembly为web赋能吧!

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

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

项目地址:github.com/wanwu/cheet…,欢迎star。

封装好api支撑依照帧数目和秒数抽取。你也挑选自界说编译,经过更改FFmpeg的编译参数让他支撑更多的视频类型,经过更改capture.c文件添加更多api才能,等待你来丰富更多场景。

——END——

引荐阅读

百度研发效能从度量到数字化蜕变之路

百度内容理解推理服务FaaS实战——Punica体系

精准水位在流批一体数据仓库的探究和实践

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

浅谈活动场景下的图算法在反作弊应用

Serverless:依据个性化服务画像的弹性弹性实践