aiortc中的拥塞控制

aiortc中的拥塞控制和webrtc中拥塞控制(gcc)的实现高度相似,其主要依据RTP包中的延时信息预测可用带宽,判断带宽是否过载并产生相应信号。与gcc相同,其信号有以下三种: NORMAL正常信号 ⇒ Increase 提升码率,增加带宽占用,试探带宽瓶颈 UNDERUSING未充分利用信号

aiortc中的拥塞控制和webrtc中拥塞控制(gcc)的实现高度相似,其主要依据RTP包中的延时信息预测可用带宽,判断带宽是否过载并产生相应信号。与gcc相同,其信号有以下三种:

  1. NORMAL正常信号 ⇒ Increase 提升码率,增加带宽占用,试探带宽瓶颈
  2. UNDERUSING未充分利用信号 ⇒ 保持码率,带宽占用不变,等待瓶颈缓冲区排空
  3. 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中的码率评估器进行初始化。

Untitled.png

而后在处理rtp包的函数_handle_rtp_packet中调用该码率评估器,获取了remb(接收方最大估计比特率),并将其信息反馈到rtcp包中的FCI中(Feedback Control Information),标准遵循*<https://tools.ietf.org/html/draft-alvestrand-rmcat-remb-03,*代码如下:>

Untitled.png

然后接收端将该最大估计比特率的信息以rtcp包的形式发送给了发送端。在发送端中_handle_rtcp_packet函数使用了unpack_remb_fci函数来获取码率信息,并且该信息在接下来将赋值给encoder的target_bitrate,代码如下:

Untitled.png

这个函数unpack_remb_fci(data:bytes)→Tuple[int,List[int]]来自于rtp.py+193:

Untitled.png

因此对于发送方中的类class RTCRtpSender来说,其成员变量self.__encoder: Optional[Encoder]就具有了一个target_bitrate属性,接下来就会根据这个成员变量进行帧的编码:

Untitled.png

可以看到帧的数据首先是从track的轨道中提取而来,是用recv函数来获取的,其定义在了mediastreams.py中,我们只关注视频class VideoStreamTrack类中的recv函数:

Untitled.png

在这里视频帧的定义是来自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函数的实现上:

Untitled.png

发现是一个抽象类,具体的实现其实是和选择的编码器是有大关联的。aiortc中编码器也是基于pyav实现的,在aiortc.codecs下面有不同编码格式的实现,然后我们以h264为例看下h264.py的实现:

Untitled.png

也就是对帧的编码还是要再看_encode_frame函数:

Untitled.png

我们终于找到了生成的target_bitrate到了编码器的哪里被使用的了,可以看到这个环节它又加了一个规则,预估的target_bitrate要和原来相比变化了10%以上,才调整编码的码率,也就是按他做的清空了buffer信息和codec重置来改变编码的参数。

然后后面就是重置picture_type,否则不会产生B帧,B帧是双向预测编码帧,记录的是本帧与前后帧的差别B帧,另外还有I 帧和P 帧。

接下来就用create_encoder_context函数获取codeccodec_buffering这两个东西具体代表啥我们再往后看。可以发现这个函数也是在h264.py中定义的,他调用了pyav类*class* av.codec.context.**CodecContext**中的方法:

Untitled.png

从pyav的源码中找到这个create方法:

Untitled.png

相当于它用wrap_codec_context函数把从ffmpeg中得到的AVCodecContext转换构建成一个av.CodecContext并返回。AVCodecContext是直接调用了ffmpeg中的lib.avcodec_alloc_context3方法,这个方法分配一个AVCodecContext 并将其字段设置为默认值,然后返回,在ffmpeg文档中可以看到相关说明:

Untitled.png

我们再回到h264.py中,获得了av.CodecContext后就开始设置编码器的相关属性比如height,width,bitrate,pix_fmt,framerate等,最后我们返回这个设置好的编码器codec。有了这个编码器后,我们就可以继续进行_encode_frame后的操作了:

Untitled.png

先定义一个要发送的数据data_to_send,编码好的数据就往里面放。这里我们能看到之前配置好带有各种参数的编码器codec就派上用场了,用了它的encode方法对frame进行编码,看下这个方法的说明:从给定的Frame中编码一个Packet列表。

Untitled.png

看一下这个encode源码的实现:

Untitled.png

在1中,_send_frame_and_recv这个函数调用了ffmpeg的方法lib.avcodec_send_frame

Untitled.png

看一下ffmpeg文档,发现这个方法向ffmpeg的编码器提供了原始视频或音频帧,成功了就返回0:

Untitled.png

在2中,_recv_packet()方法调用了lib.avcodec_receive_packet

Untitled.png

同样看看ffmpeg文档,发现这个东西不就是上面lib.avcodec_send_frame文档介绍中说的“Use **avcodec_receive_packet()** to retrieve buffered output packets.”所以这个方法就是从编码器读取编码的数据:

Untitled.png

那到这就能知道,原来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中来的:

Untitled.png

至此我们就知道了在aiortc中是怎么通过pyav去调用ffmpeg然后实现获取帧并且对这些帧进行编码的一个过程了,但是其中很多细节的地方没有讨论,自己也没搞的太明白,还需要继续查阅资料来分析;而对解码过程,其与编码过程类似,类比就可以得到流程。

Comment