众所周知,所有基于Chromium内核开发的“标准”浏览器架构的项目,如CEF、Electron甚至是Google Chrome,默认提供的H.264软编码器都是Cisco的OpenH264。那么,如果我们想使用其他H.264编码器,例如x264,甚至是结合特殊芯片进行硬编码,该如何做呢?关于OpenH264和x264的差异,网上有很多相关文章,这里就不赘述了。
OpenH264的具体实现位于WebRTC项目中,本文是基于 CEF 76.3809.132 这个分支的源码基础上进行介绍。因Chromium以及WebRTC代码变化比较频繁,可能在不久的将来,代码就对应不上了,所以本文尽可能只说一下通用性的适合未来版本的思路。不过从git log看,H264编码器的实现变化的不频繁,比较频繁的还是WebRTC的VideoSendStream,里面有不少关于控制编码器行为的代码,并且这部分写的非常有意思,有时间我写篇文章简单说一下……跑题了,言归正传。
H264软编码器代码位于:
third_party\webrtc\modules\video_coding\codecs\h264\h264_encoder_impl.h/.cc
下面是H264EncoderImpl这个类的继承关系:
因为懒,所以上图中的函数参数、以及部分成员变量就不写了。
类关系还是很清楚的,也比较简单。H264EncoderImpl实现了VideoEncoder的5个纯虚函数(InitEncode、Encode、RegisterEncodeCompleteCallback、Release、SetRates),1个虚函数(GetEncoderInfo)。并且添加了若干private方法和成员变量。
H264EncoderImpl实现的这几个虚函数是最核心的几个,我们添加 x264编码器,就是要围绕着这几个虚函数展开。至于其他的部分,大家看到,基本上都是OpenH264的实现,并非通用的。所以,可以这么说,H264EncoderImpl 应该叫 OpenH264EncoderImpl 更合适一点。
OK,接下来我们对H264EncoderImpl进行一下改造。改造后的类结构如下:
我新建了一个基类 H264BaseEncoder,它用来存储及实现一些通用性的方法,而真正有差异的部分,继续保留虚函数的形式,派生出来2个子类:OpenH264EncoderImpl 和 X264EncoderImpl 去实现它们。
改造后,原来H264EncoderImpl中只适用于OpenH264的代码被全部转移到OpenH264EncoderImpl,公共的部分(方法、变量)保留在H264BaseEncoder。最终,X264EncoderImpl是我们着重要编写的类。
OK,下面讲一下在实现X264EncoderImpl这个类中可能会遇到的问题。
WebRTC的OpenH264支持Simulcast,这个通过阅读源码可以看到:
std::vector encoders_;
OpenH264是有一个最大数量为4的vector来存储每一层对应的编码器的。x264实现是否支持,看你的实际使用情况而定,你可以只实现一个编码器,相应地,上层初始化PeerConnection的时候就不要设置simulcast。
OpenH264的源码中,定义了两个静态常量:kLowH264QpThreshold 和 kHighH264QpThreshold,取值分别是 24 和 37。这里要注意,这两个值并不是OpenH264编码参数中设置用于编码的QP范围,而是用于判断是否动态升降发送分辨率或帧率的阈值。 WebRTC会根据一段时间内的编码平均QP,和这两个值进行比较。小等于kLowH264QpThreshold,认为需要升分辨率(或帧率)。而大于kHighH264QpThreshold,则会下调分辨率(或帧率)。平均QP计算的这部分实现也很有意思,它的源码位于 \webrtc\modules\video_coding\utility\quality_scaler.cc。
至于24和37是否适用于x264,这个也需要结合你使用的x264编码参数与实际应用情况来定。我做了一点微小的调整(26, 40)。
上面大家有看到那几个重要的纯虚函数中,有一个 SetRates (这个函数以前叫SetRateAllocation),这个函数的调用堆栈大概是这样的:
→ VideoEncoder::SetRates()
↑ VideoStreamEncoder::SetEncoderRates()
↑ VideoStreamEncoder::OnBitrateUpdated()
↑ VideoSendStreamImpl::OnBitrateUpdated()
↑ BitrateAllocator::OnNetworkChanged()
其中VideoEncoder::SetRates()就会来到具体编码器的SetRates里来。它的参数主要包含了期望调整的目标码率、帧率等。SetRates会调用的非常频繁,所以在这里,我们要不断地重设编码器的参数,以适应WebRTC的请求。
这里要特别说明的一点是,x264似乎并不支持动态帧率。也就是说,如果在InitEncode的时候我们设置了一个60fps的帧率,在SetRates中使用 x264_encoder_reconfig() 尝试改为30fps,似乎是无效的。而动态调整码率则没有这个问题。我不太确定是因为我选用了ABR编码方式引起的,还是其他什么原因。有知道的朋友可以告诉我。
所以,针对这个问题,我的做法是:如果当前编码帧率和目标帧率相差某个阈值(如5)或以上,则关闭当前 x264编码器,重新创建一次(帧率采用本次的目标帧率,其他编码参数使用关闭前的)。
这一条,不得不多说两句。
其实x264编码器我很快就写完了,并且使用了常见的几档分辨率/帧率参数,使用一款罗技摄像头进行了试验,工作的比较良好。但当我把视频源从摄像头换成了一个1080P的mp4文件时,发生一件非常诡异的问题、踩了一个坑:
起初的发送分辨率是1920x1080,但通过webrtc-internals看到,很快发送分辨率下调至1440x810,之后又下降到960x540,在若干秒后,迅速下降到720x405,然后你猜怎么着,x264就出错了。原因是什么?输入了一个奇数分辨率。
那么到底是什么原因引起分辨率在短时间内迅速按照一个阶梯状下降呢?通过阅读源码后才发现里面的奥秘:WebRTC会根据一定时间内的编码平均QP,来决定是否要升降发送分辨率!也就是说,发送分辨率在不断下调的根本原因,是目前的平均QP一直居高不下(超过我们上面提到的QP阈值上限),所以一直在不断地请求下调分辨率。如果不是x264因为输入奇数分辨率出错了,下一个可能到达的就是480x270。(注,下调分辨率是按照输入分辨率 3/4 和 2/3 交替计算的)
OK,原因找到了,接下来问题就好解决了。最后问题的原因是因为我没有在编码后,通过 webrtc::H264BitstreamParser的 ParseBitstream 和 GetLastSliceQp 这两个方法计算编码帧的正确QP送出去。(这两个方法的调用在OpenH264编码后的代码你可以找到)
RTPFragmentationHeader的作用其实就是标识一帧编码后的H264数据各个NALU去掉开始码后的数据起始位置和长度的。举个例子:比如编码出来的一段H264数据是下面的格式:
00 00 00 01 67 aa aa aa aa aa aa 00 00 00 01 68 bb bb bb 00 00 00 01 65 cc cc cc
那么, RTPFragmentationHeader将会存储3个元素:
元素1的offset指针指向67,长度是67开始到下一个开始码之间的字节数
元素2的offset指针指向68,长度是68开始到下一个开始码之间的字节数
元素3的offset指针指向65,长度是65开始到下一个开始码之间的字节数
注,老版本的WebRTC源码中还有 fragmentationPlType 和 fragmentationTimeDiff这两个字段,新版本已经删掉了。
OK,最终,RTPFragmentationHeader以及编码后的数据,将通过 EncodedImageCallback->OnEncodedImage() 送到外面去进行后续处理流程。
OpenH264编码出来的数据,一般都是比较规整的“4字节开始码+NALU类型+编码数据”组成的,如下:
而x264编码器编码输出的数据中,有一些是无用的,需要跳过,如下是x264编码出来的起始部分:
这里还要注意的是,x264编码器送出来的NALU开始码长度有4个的,也有3个的,在填充RTPFragmentationHeader的时候需要注意别搞错了。另外就是有些NALU类型(如SEI、AUD)需要跳过不要。
这里插一句,起初我在填充 RTPFragmentationHeader 的时候,去掉了NALU的开始码。我想着, RTPFragmentationHeader 反正是按照偏移和长度来获取数据的,开始码有没有不重要。但后来我发现我错了。这个开始码不是外面要用的,而是webrtc::H264BitstreamParser在分析流和QP值的时候要读取的,缺少了开始码,将无法读取到正确的QP值,所以万不可去掉。这部分可以自行阅读以下H264BitstreamParser的源码。
在替换了x264以后,我发现debug级别的日志里面有很多这个错误。它来自
third_party\webrtc\common_video\h264\h264_bitstream_parser.cc
我的解决方法是通过以下两句代码:
[x264_param_t].analyse.i_weighted_pred = X264_WEIGHTP_NONE;
[x264_param_t].analyse.b_weighted_bipred = X264_WEIGHTP_NONE;
讲实话我不太清楚关闭它的影响是什么,如果你知道,欢迎告诉我。
OK,差不多就是这些了。本文对改造WebRTC的H264Encoder模块,添加 x264编码器进行了主要思路和注意事项的说明,里面其实有很多小的知识点,都可以专门展开写一篇文章。例如:
- WebRTC对视频的发送策略,有保帧率、保分辨率、平衡模式,它们的区别是什么?
- 在某种发送策略下,影响调高调低(分辨率/帧率)的因素有哪些?
- 实时码率是从何而来?
- 平均QP的计算方法,以及上调下调的极限
- WebRTC是怎么检测CPU过载的(软编码、硬编码下不同的检测策略)
我强烈建议大家有时间多看看WebRTC的实现代码,里面有非常多值得我们学习的地方。