业务背景
在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,以及两个关键的结构体:
AVStream: 用于存储视频/音频流信息的结构体,包含编解码器参数、比特率、帧率等
AVPacket: 用于存储解码前的压缩数据包的结构体,包含数据包对应的时间戳、大小等
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上实现更多音视频编辑的可能性。未来,随着原生能力的逐步发展,再逐步替换提升性能,从而实现渐进式的发展。