作者 | 小萱
导读
基于理论业务需要,介绍了自定义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 代码。分层编译模式包含两个次要的编译器:
- 基线编译器(Baseline compiler) Liftoff编译器
- 优化编译器(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领有更好的性能。对于缓存的工作原理:
- 当TurboFan实现编译后,如果.wasm资源足够大(128 kb),Chrome 会将编译后的代码写入 WebAssembly 代码缓存。
- 当.wasm第二次申请资源时(hot run),Chrome.wasm从资源缓存中加载资源,同时查问代码缓存。如果缓存命中,编译后的module bytes将发送到渲染器过程并传递给 V8,V8将其进行反序列化,与编译相比,反序列化速度更快,占用的 CPU 更少。
- 如果.wasm资源产生了变动或是 V8 产生了变动,缓存会生效,缓存的本地代码会从缓存中革除,编译会像步骤 1 一样持续进行。
2.2.6 编译管道(Compilation Pipeline)
△频成果V8编译Wasm的流程图
V8 编译 WASM 代码的整个过程能够概括为以下几个步骤:
- 解码(Decoding):首先,将 WASM 模块解码为二进制可执行代码,并验证其是否合乎 WASM 规范。
- 基线编译(Baseline Compilation):接下来,应用 Liftoff 编译器进行疾速编译。这一阶段生成的代码性能较低,但编译速度快。流式编译在这个阶段发挥作用,使得代码在下载过程中就能进行编译。
- 热点剖析(Hotspot Analysis):V8 引擎会继续监控 WASM 函数的调用频率,以辨认 Hot Function。
- 优化编译(Optimizing Compilation):对于被标记为热门函数的代码,应用 TurboFan 编译器进行优化编译。编译实现后,优化后的代码会替换原有的 Liftoff 代码。这一过程称为分层降级(Tier-up)。
- 执行(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.gitcd 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 MBCONFIG_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_PATHemconfigure ./configure "${CONFIG_ARGS[@]}"
PS:下面咱们容许了C++应用pthread,但因为在浏览器应用pthread多线程须要SharedArrayBuffer 容许多个Web Workers或WebAssembly线程拜访和操作雷同的内存区域,而SharedArrayBuffer的兼容性较差,并且要求https,因而咱们在接下来产出wasm时禁用pthread。
FFmpeg蕴含了很多库,若间接应用@ffmpeg/ffmpeg @ffmpeg/core便是全量的库的wasm版本。
- libavformat:负责多媒体文件和流的格局解决。这个库能够帮忙你读取和写入多种音频和视频文件格式,以及网络流。
- libavcodec:负责音视频编解码。这个库蕴含了泛滥的音频和视频编解码器,能够解决多种格局的音频和视频。
- libavutil:提供一些实用功能,例如内存治理、数学运算、工夫解决等。这个库被 libavformat 和 libavcodec 等其余库所应用,用于辅助解决各种工作。
- libswscale:负责图像的缩放和色彩空间转换。这个库能够帮忙你将视频帧从一种像素格局转换为另一种,或者对图像进行缩放。
- libswresample:负责音频重采样、混合和格局转换。这个库用于解决音频数据,例如扭转采样率、扭转声道数等。
- libavfilter:负责音视频滤镜解决。这个库提供了一系列音视频滤镜,用于解决音频和视频,例如调整色调、裁剪、增加水印等。
- 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'):numberthis.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:基于个性化服务画像的弹性伸缩实际