aiortc中的拥塞控制和webrtc中拥塞控制(gcc)的实现高度相似,其主要依据RTP包中的延时信息预测可用带宽,判断带宽是否过载并产生相应信号。与gcc相同,其信号有以下三种:
- NORMAL正常信号 ⇒ Increase 提升码率,增加带宽占用,试探带宽瓶颈
- UNDERUSING未充分利用信号 ⇒ 保持码率,带宽占用不变,等待瓶颈缓冲区排空
- OVERUSING过载信号 ⇒ 降低码率,减少带宽占用
这部分的实现在rate.py
中,
在aiortc中,码率更新规则与gcc略有一些不同,首先是如果出现极端变化的估计带宽,则把估计的最大带宽清除后再计算更新;另一个是码率的增加是在没有接近最大估计带宽积性增加,速度较快,接近最大估计时加性缓慢增加;码率减少与gcc规则相同,直接乘0.85。更新规则对应的代码如下:
# update bitrate
if self.state == RateControlState.INCREASE: #increase bitrate
# if the estimated throughput increases significantly,
# clear estimated max throughput
if self.avg_max_bitrate_kbps is not None:
sigma_kbps = math.sqrt(
self.var_max_bitrate_kbps * self.avg_max_bitrate_kbps
)
if (
estimated_throughput_kbps
>= self.avg_max_bitrate_kbps + 3 * sigma_kbps
):
self.near_max = False
self.avg_max_bitrate_kbps = None
# we use additive or multiplicative rate increase depending on whether
# we are close to the maximum throughput
if self.near_max:
new_bitrate += self._additive_rate_increase(self.last_change_ms, now_ms)
else:
new_bitrate += self._multiplicative_rate_increase(
new_bitrate, self.last_change_ms, now_ms
)
self.last_change_ms = now_ms
elif self.state == RateControlState.DECREASE: #decrease bitrate
# if the estimated throughput drops significantly,
# clear estimated max throughput
if self.avg_max_bitrate_kbps is not None:
sigma_kbps = math.sqrt(
self.var_max_bitrate_kbps * self.avg_max_bitrate_kbps
)
if (
estimated_throughput_kbps
< self.avg_max_bitrate_kbps - 3 * sigma_kbps
):
self.avg_max_bitrate_kbps = None
self._update_max_throughput_estimate(estimated_throughput_kbps)
self.near_max = True
new_bitrate = round(0.85 * estimated_throughput)
self.last_change_ms = now_ms
self.state = RateControlState.HOLD
self.current_bitrate = self._clamp_bitrate(new_bitrate, estimated_throughput)
return self.current_bitrate
这部分规则的调用主要集中在rtcrtpreceiver.py
文件中,相当于在aiortc中的拥塞控制应用在接收方实现基于延时梯度的码率控制策略,该文件类似于webrtc中的rtcrtpreceiver,相关接口管理 RTCPeerConnection 上 MediaStreamTrack 的数据接收和解码。
接收端调用了rate.py
中的码率评估器进行初始化。
而后在处理rtp包的函数_handle_rtp_packet
中调用该码率评估器,获取了remb(接收方最大估计比特率),并将其信息反馈到rtcp包中的FCI中(Feedback Control Information),标准遵循*<https://tools.ietf.org/html/draft-alvestrand-rmcat-remb-03
,*代码如下:>
然后接收端将该最大估计比特率的信息以rtcp
包的形式发送给了发送端。在发送端中_handle_rtcp_packet
函数使用了unpack_remb_fci
函数来获取码率信息,并且该信息在接下来将赋值给encoder的target_bitrate,代码如下:
这个函数unpack_remb_fci(data:bytes)→Tuple[int,List[int]]来自于rtp.py+193:
因此对于发送方中的类class RTCRtpSender
来说,其成员变量self.__encoder: Optional[Encoder]
就具有了一个target_bitrate
属性,接下来就会根据这个成员变量进行帧的编码:
可以看到帧的数据首先是从track的轨道中提取而来,是用recv
函数来获取的,其定义在了mediastreams.py
中,我们只关注视频class VideoStreamTrack
类中的recv函数:
在这里视频帧的定义是来自pyav库中的*av.video.frame.VideoFrame*
,定义帧后,使用pyav中定义的av.buffer.Buffer.update
函数,用给定的缓冲区来替换frame.planes
对象中的数据,(但是不清楚为什么说这个base的实现只读了一个绿色的帧,rgb24中绿色应为#00FF00,没有理解到是怎么填充进去的)然后设定他的pts
(timestamp)和时基time_base
。
此后我们继续回到_next_encoded_frame(self, codec: RTCRtpCodecParameters)
函数中,接下来就是对这个帧进行编码了,这里用的asyncio的多线程操作run_in_executor(self, executor, func, *args)
,这里就开始调用编码器对给定数据进行编码了,关键的代码就是下面这几行:
payloads, timestamp = await self.__loop.run_in_executor(
None, self.__encoder.encode, data, force_keyframe
)
所以我们转到encode函数的实现上:
发现是一个抽象类,具体的实现其实是和选择的编码器是有大关联的。aiortc中编码器也是基于pyav实现的,在aiortc.codecs
下面有不同编码格式的实现,然后我们以h264为例看下h264.py
的实现:
也就是对帧的编码还是要再看_encode_frame
函数:
我们终于找到了生成的target_bitrate
到了编码器的哪里被使用的了,可以看到这个环节它又加了一个规则,预估的target_bitrate
要和原来相比变化了10%以上,才调整编码的码率,也就是按他做的清空了buffer信息和codec重置来改变编码的参数。
然后后面就是重置picture_type,否则不会产生B帧,B帧是双向预测编码帧,记录的是本帧与前后帧的差别B帧,另外还有I 帧和P 帧。
接下来就用create_encoder_context
函数获取codec
和codec_buffering
这两个东西具体代表啥我们再往后看。可以发现这个函数也是在h264.py
中定义的,他调用了pyav类*class* av.codec.context.**CodecContext**
中的方法:
从pyav的源码中找到这个create方法:
相当于它用wrap_codec_context
函数把从ffmpeg中得到的AVCodecContext
转换构建成一个av.CodecContext
并返回。AVCodecContext
是直接调用了ffmpeg中的lib.avcodec_alloc_context3
方法,这个方法分配一个AVCodecContext
并将其字段设置为默认值,然后返回,在ffmpeg文档中可以看到相关说明:
我们再回到h264.py
中,获得了av.CodecContext
后就开始设置编码器的相关属性比如height,width,bitrate,pix_fmt,framerate
等,最后我们返回这个设置好的编码器codec。有了这个编码器后,我们就可以继续进行_encode_frame
后的操作了:
先定义一个要发送的数据data_to_send,编码好的数据就往里面放。这里我们能看到之前配置好带有各种参数的编码器codec
就派上用场了,用了它的encode方法对frame进行编码,看下这个方法的说明:从给定的Frame中编码一个Packet列表。
看一下这个encode源码的实现:
在1中,_send_frame_and_recv
这个函数调用了ffmpeg的方法lib.avcodec_send_frame
:
看一下ffmpeg文档,发现这个方法向ffmpeg的编码器提供了原始视频或音频帧,成功了就返回0:
在2中,_recv_packet()
方法调用了lib.avcodec_receive_packet
:
同样看看ffmpeg文档,发现这个东西不就是上面lib.avcodec_send_frame
文档介绍中说的“Use **avcodec_receive_packet()** to retrieve buffered output packets.”所以这个方法就是从编码器读取编码的数据:
那到这就能知道,原来aiortc是通过解包rtcp,获取预估的bitrate,然后把这个bitrate作为编码器的参数。编码器的构建要通过pyav去调用ffmpeg,一个是上下文的Context设置调用了ffmpeg中的lib.avcodec_alloc_context3
,转换成了pyav认识的Context;然后通过lib.avcodec_send_frame
把原始帧给了ffmpeg,再用avcodec_receive_packet
把编码好的数据拿回来,最终呢再到aiortc中后处理变成能传输的h264bitstream,这个代码还是从https://github.com/aizvorski/h264bitstream/blob/master/h264_nal.c#L134中来的:
至此我们就知道了在aiortc中是怎么通过pyav去调用ffmpeg然后实现获取帧并且对这些帧进行编码的一个过程了,但是其中很多细节的地方没有讨论,自己也没搞的太明白,还需要继续查阅资料来分析;而对解码过程,其与编码过程类似,类比就可以得到流程。