哔哩哔哩技术 2024年12月03日
WASM 助力 WebCodecs:填补解封装能力的空白
index_new5.html
../../../zaker_core/zaker_tpl_static/wap/tpl_guoji1.html

 

B站Web投稿页的封面、分区和标签推荐功能需要视频截帧能力。过去使用WebAssembly + FFmpeg实现,后来引入WebCodecs提升性能。但WebCodecs缺少解封装能力,只能自行实现。为了解决不同视频格式的解封装问题,B站团队设计了一种通用的WASM Demuxer方案,将FFmpeg的Demux能力与WebCodecs的Decode能力结合,支持更多视频格式,并显著提升了截帧性能。该方案通过C++函数获取WebCodecs所需数据,JS胶水代码实现双向通信,最终封装成WASM Demuxer,并提供npm包web-demuxer方便开发者使用。

🤔B站Web投稿页的封面、分区、标签推荐功能需要使用视频截帧能力,历史上通过WebAssembly + FFmpeg实现,后来引入WebCodecs进行高性能截帧,性能显著提升。

💻WebCodecs缺少解封装能力,B站团队基于WebAssembly + FFmpeg的经验,设计了WASM Demuxer方案,将FFmpeg的Demux能力与WebCodecs的Decode能力结合,以支持更多视频格式。

🛠️核心思路是将FFmpeg的Demux能力独立出来,通过C++函数获取WebCodecs解码所需数据,JS胶水代码实现JS与C间的双向通信,最终封装成WASM Demuxer。

📦为了方便开发者使用,B站团队提取了web-demuxer npm包,支持MP4+MKV,体积小巧,可用于视频截帧或更复杂的播放场景。

📈WASM Demuxer上线后,封面推荐耗时P90减少约40%,WebCodecs截帧失败率下降约72%,显著提升了用户体验。

原创 大前端 2024-12-03 12:04

在B站Web投稿页中,封面、分区、标签的推荐功能都需要使用到视频截帧能力。

业务背景


在B站Web投稿页中,封面、分区、标签的推荐功能都需要使用到视频截帧能力。历史上我们通过WebAssembly + FFmpeg来实现视频截帧。从去年开始,开始引入WebCodecs进行高性能截帧,截帧性能有显著提升,从而给用户带来更快速的推荐体验。



但目前WebCodecs只提供了用于解码的能力,并没有提供对应解封装能力,只能自行实现。此前,我们通过mp4box.js以及自行开发的mkv-demuxer,解决了mp4+mkv主流视频格式的解封装问题, 实现了WebCodecs高性能封面截帧方案的落地。但仍存在近 2%的视频格式如flv、avi等,因为无法解封装,而无法体验到WebCodecs的高性能。

针对不同视频格式去做解封装处理,需要进行数据转换,API适配类的工作,存在一定的开发成本。同时,相关的高质量在维护的JS解封装库很少,假如继续针对单个格式去做逐个处理,ROI会很低。

于是,我们期望为WebCodecs低成本定制一种通用的解封装方案,一次性支持尽可能多的视频格式。


方案设想


联想到之前使用WebAssembly + FFmpeg进行截帧的经验,FFmpeg支持的视频格式很广泛,如果能复用FFmpeg的Demux能力,并结合WebCodecs的Decode能力,应该就能实现两者的优势互补。将耗时短的Demux环节交给WebAssembly + FFmpeg去支持更多的视频格式,耗时长的Decode环节交给原生的WebCodecs去提升解码性能。



解决方案


核心思路


基于上述设想,核心目标就是将WebAssembly + FFmpeg中的Demux能力独立出来,实现一个WASM Demuxer,主要步骤如下:

    C中新增获取WebCodecs解码所需数据的函数

    JS胶水代码实现JS与C间的双向通信,传递解封装后的数据

    截帧SDK中基于原始数据进行转换,适配WebCodecs

整体流程如下图所示,下面讲详细介绍下具体实现步骤



C中获取WebCodecs解码所需数据


关键数据结构


FFmpeg包含很多library,这里我们的目标是解封装,所以只需要重点关注用于负责多媒体文件流格式处理的libavformat,以及两个关键的结构体:

WebCodecs视频截帧中,主要用到VideoDecoder中的 configure decode 方法,configure方法用于配置和初始化视频解码器,decode方法则用于向视频解码器提供编码视频数据,以便解码器能够处理和输出解码后的帧。

与configure与decode方法需要的入参做对比后,可以很容易发现,configure方法所需的参数都可以在AVStream中找到,decode方法所需的参数也都可以在AVPacket中找到。

因此,需要在C中实现两个函数,分别用于获取视频文件中视频流的AVStream与视频流中指定时间点的AVPacket。

不过,FFmpeg中的AVStream和AVPacket都比较复杂,而在截帧场景无需用到所有的参数。于是,我们对AVStream和AVPacket进行裁剪,重定义了两个新的结构体 WebAVStream WebAVPacket


生成WebAVStream


裁剪转换后的WebAVStream结构体如下,包含了编解码器参数、开始时间、时长等。



新建一个 get_av_stream 函数用于从文件中查找对应视频流的AVStream信息。首先从视频文件中查找到匹配的视频流信息,读取AVStream,对其进行裁剪与数据适配,生成并返回新定义的结构体WebAVStream。



如何生成codec_string

在构建WebAVStream时发现,WebCodecs VideoDecoder的configure 方法中有一个必要的 codec 参数,需要传入一个有效的 codec_string,即编解码字符串,描述用于编码或解码的特定编解码器格式。浏览器通过解析该参数,才能知道去调用哪一种编解码器。

codec_string参数无法直接从AVStream上获取,需要结合AVStream中的信息去生成。社区里,这部分的资料非常少,并没有现成可用的轮子,只能自行实现。调研后发现,生成codec_string主要需要两个步骤:

    从视频流中解析出视频编解码器的配置信息

    将视频编解码器的配置信息按照codec_string的标准进行转换

首先,对于如何从视频流中去解析出视频编解码器的配置,可以自行按照对应的标准去实现,不过对于不同的codec都需要单独实现,这样成本就会比较高,不符合我们低成本的预期。背靠FFmpeg这个丰富的宝库,相信应该能找到可复用的方法,于是在libavformat中一番探索后,果不其然找到了相关的解析方法。

以VP9为例,与ISOM文件之间的绑定规范中(ISOM 即 ISO Base Media File Format,是一种用于存储多媒体内容的文件格式标准,常见的MP4就是基于这种文件格式),可以看到VP编解码配置信息如下:



在libavformat/vpcc.c中存在一个 ff_isom_write_vpcc 方法,该方法用于将 VP 编解码器配置写入到ISOM文件中(例如VP9会被写入到MP4文件的stsd/vp09/vpcC盒子中)。在写入配置前会通过 ff_isom_get_vpcc_features 方法来解析生成配置参数。

由于 ff_ 开头的方法都是FFmpeg内部的方法,无法直接调用,只能将这部分逻辑复制出来,提取出关键部分,改写后作为生成编解码器配置的逻辑。改写后的 get_vpcc_features 如下,包含了生成codec_string所需的参数。



成功解析出编解码器的配置信息后,还需要将配置信息转换成codec_string,于是再结合VP9的Codecs Parameter String规范,实现codec_string的拼装。



生成codec_string后,对生成的codec_string是否能正确被浏览器解析还是有所疑惑,于是去Chromium中简单探索下,找到解析codec_string的方法video_codec_string_parsers源码,看下浏览器在获取到codec_string以后具体是怎么解析的。



找到解析vp9的函数ParseNewStyleVp9CodecID,可以发现首位的 sample entry 4CC必然是vp09,同时profile、level、bitDepth三个必要参数的解析规则与 ff_isom_get_vpcc_features 中产出的配置信息格式能够正确对应上,辅助映证了生成逻辑无误。

另外,可以看到文档上有提到DASH格式中也有使用到codec_string,在 libavformat/dashenc.c 中可以发现有类似的set_vp9_codec_str方法,也是使用 ff_isom_get_vpcc_features 来实现的。

最后的生成函数如下所示:



其他h264、hevc等编码的codec_string生成逻辑也是同理,都能在FFmpeg中找到可参考的方法,并且更加简单。因为h264、hevc的视频编解码配置信息都会写入到 AVStream→codecpar→extradata 中,所以可以直接按照比特位去读取配置信息。以h264为例,参考 ff_isom_write_avcc,了解到配置信息的字段写入顺序与每个字段所占比特位数,从extradata中反向读取对应配置字段,最后再拼接成codec_string即可。



生成WebAVPacket


裁剪转换后的WebAVPacket结构体很简单,仅需包含关键帧、时间戳、时长、大小及数据



新建一个 get_av_packet 函数,用于获取指定时间点的AVPacket。首先与 get_av_stream 一样,查找到对应的视频流索引。根据传入的截帧时间点与视频流索引,定位至指定时间点的帧,读取AVPacket数据,然后进行裁剪转换,返回新定义的结构体WebAVPacket。



JS与C双向通信


在完成C中的 get_av_stream 与 get_av_packet 方法后,还需要在JS胶水代码中建立JS与C的双向通信。下面以 get_av_packet 方法为例。


JS调用C


首先使用Emscripten提供的Module.cwrap方法,将C函数包装成JS函数,调用包装后的JS函数,将文件路径和时间作为入参传入,执行后的返回值为WebAVPacket结构体指针。



C调用JS


C函数执行完毕后,通过返回的WebAVPacket结构体指针从WASM的内存中读取数据。使用Emscripten提供的Module.getValue传入指针,返回内存中具体的值。最后,将所有值组合成一个JS Object,通过postMessage传出。



截帧SDK新增WASM Demuxer


最后,因为WASM的处理逻辑都运行在Worker上,需要在截帧SDK中对postMessage进行Promise化包装,同时适配WebCodecs的参数格式(WebAVStream => VideoDecoderConfig、WebAVPacket => EncodedVideoChunk),封装成WASM Demuxer。



数据结果


WASM Demuxer上线后,使用WASM Demuxer + WebCodecs截帧对比之前使用WASM + FFMpeg截帧,封面推荐耗时P90减少了约 40%,因为视频封装格式不支持导致WebCodecs截帧失败的错误量下降了约 72%



web-demuxer



考虑到很多项目之前并没有WebAssmbly+FFmpeg的基础,提炼了一个名为web-demuxer 的npm包,将WebAssmbly+FFmpeg中demuxer的部分单独提取编译,大大缩减了WASM的体积,支持MP4+MKV的最小版本gzip后的体积为115KB,对大多数Web项目的使用应该还是可接受的。

通过简单的十几行代码就可以实现视频截帧



同时也提供以ReadableStream逐帧读取的方式,用来进行播放等更复杂的场景



希望能让 WebCodecs 的使用变得更加便捷,详细的介绍可见web-demuxer


写在最后


WebCodecs仓库的issue中也有关于是否支持媒体容器相关API的讨论,但媒体工作组的想法是将这部分工作交给JS/WASM,通过开源库来实现。长期看,原生解封装的能力的支持还遥遥无期。

不过,借助FFmpeg这个丰富的宝库,我们可以将更多的能力进行WASM层面的模块化封装,与WebCodecs等原生能力去结合使用,去补齐原生的不足,在Web上实现更多音视频编辑的可能性。未来,随着原生能力的逐步发展,再逐步替换提升性能,从而实现渐进式的发展。


附录

web-demuxer:https://github.com/ForeverSc/web-demuxer

WebCodecs支持的Video Codecs:https://www.w3.org/TR/webcodecs-codec-registry/#video-codec-registry

VP9 Codecs Parameter String :https://github.com/webmproject/vp9-dash/blob/main/VPCodecISOMediaFileFormatBinding.md#codecs-parameter-string

Chromium video_codec_string_parsers源码:https://chromium.googlesource.com/chromium/src/media/+/refs/heads/main/base/video_codec_string_parsers.cc#33

Chromium video_codec_string_parsers单元测试代码:https://chromium.googlesource.com/chromium/src/media/+/refs/heads/main/base/video_codec_string_parsers_unittest.cc

关于媒体容器API的讨论issue:https://github.com/w3c/webcodecs/issues/24

-End-

作者丨Francis


开发者问答


关于前端音视频处理,大家还有什么优秀的方案和经验?

欢迎在留言区分享你的见解~

转发本文至朋友圈并留言,即可参与抽奖。

小编将抽取1位幸运的朋友获取龙年小电视鼠标垫键盘垫

抽奖截止时间:12月03日12:00

如果喜欢本期内容的话,欢迎点个“在看”吧!



往期精彩指路


通用工程大前端业务线

大数据AI多媒体


跳转微信打开

Fish AI Reader

Fish AI Reader

AI辅助创作,多种专业模板,深度分析,高质量内容生成。从观点提取到深度思考,FishAI为您提供全方位的创作支持。新版本引入自定义参数,让您的创作更加个性化和精准。

FishAI

FishAI

鱼阅,AI 时代的下一个智能信息助手,助你摆脱信息焦虑

联系邮箱 441953276@qq.com

相关标签

WebCodecs 视频截帧 FFmpeg WASM B站
相关文章