Pacer起到平滑码率的作用,使发送到网络上的码率稳定。如下的这张创建Pacer的流程图,其中PacerSender就是Pacer。

从上图中可以看到,在创建Call对象时,会创建一个RtpTransportControllerSend,它是Call对象中发送数据的大总管,而PacerSender也是属于它管理的对象。
一个Call对象中一个RtpTransportControllerSend,一个RtpTransportControllerSend中一个PacerSender,所以Pacer是作用于Call中所有的stream,这里并不是只处理音视频包,还有fec包,重传包,padding包,Call对象中也发送出去的数据都会经过Pacer。
这篇文章是介绍平滑实现的基本原理和Pacer中的Periodic模式的处理流程。Pacer的流程中还有与带宽探测所关联的流程,在本篇文章中并不涉及。
码率平滑的原理
在视频编码中,虽然编码器会将输出码流的码率控制在所设置的码率范围内。但是在编码器产生关键帧或在画面变化比较大时,码率可能超过设置的码率值。在有fec或重传包时,也可能造成实际发送的码率值超过目标值。这种突发的大码率的数据,可能就会造成网络链路拥塞。
所以引入的pacer就是平滑发送的码率值,在一段时间内,保证发送码率接近设置目标码率值。而避免突发的高码率造成网络链路拥塞。
平滑的基本原理就是**缓存队列+周期发送,将要发送的数据先缓存,在周期性的发送出去,起到平均码率的目的。那么这种周期有两种模式:**
**kPeriodic**,周期模式,也是默认模式,以固定间隔时间发送数据。kDynamic,动态模式,根据数据的缓存时长及数据量来计算下一次发送数据的时间点。
组成
pacer的流程都实现在PacingController,包括两个核心类:RoundBoinPacketQueue,IntervalBudget。
RoundBobinPacketQueue缓存队列,对每条流都会缓存,以ssrc做为流的唯一标识,包括:重传包,fec,padding包。IntervalBudget根据设置的目标码率值及时间间隔计算可发送的数据量。
PacingController类
所属文件为\modules\pacing\pacing_controller.h,如下类图:

两个核心的成员变量:
RoundRobinPakcetQueue packet_queue_packet的缓存队列。IntervalBudget media_buget_可发送数据量计算。
两个核心函数:
NextSendTime,获取每次执行的时间(5毫秒,在kPeriodic模式下)。ProcessPackets,周期处理包的发送,确定要发送的数据量,从缓存队列中取包。
平滑逻辑的处理流程
整个pacer运行的机制就是靠PacingController的NextSendTime和ProcessPackets两个方法,它们被单独的放在一个ModuleThread线程中执行,周期性的被执行,两个方法调用的堆栈如下:
**NextSendTime**
peerconnection_client.exe!webrtc::PacingController::NextSendTime() 行 348 C++
peerconnection_client.exe!webrtc::PacedSender::TimeUntilNextProcess() 行 171 C++
peerconnection_client.exe!webrtc::PacedSender::ModuleProxy::TimeUntilNextProcess() 行 150 C++
peerconnection_client.exe!webrtc::`anonymous namespace’::GetNextCallbackTime(webrtc::Module * module, __int64 time_now) 行 30 C++
peerconnection_client.exe!webrtc::ProcessThreadImpl::Process() 行 231 C++
peerconnection_client.exe!webrtc::ProcessThreadImpl::Run(void * obj) 行 198 C++
peerconnection_client.exe!rtc::PlatformThread::Run() 行 130 C++
peerconnection_client.exe!rtc::PlatformThread::StartThread(void * param) 行 62 C++
**ProcessPackets**
peerconnection_client.exe!webrtc::PacingController::ProcessPackets() 行 408 C++
peerconnection_client.exe!webrtc::PacedSender::Process() 行 183 C++
peerconnection_client.exe!webrtc::PacedSender::ModuleProxy::Process() 行 152 C++
peerconnection_client.exe!webrtc::ProcessThreadImpl::Process() 行 226 C++
peerconnection_client.exe!webrtc::ProcessThreadImpl::Run(void * obj) 行 198 C++
peerconnection_client.exe!rtc::PlatformThread::Run() 行 130 C++
peerconnection_client.exe!rtc::PlatformThread::StartThread(void * param) 行 62 C++
核心骨架就是下面三个步骤:
- 设置目标码率,通过
SetPacingRates(...)方法。 - 计算每个时间片可以发送的数据,在
UpdateBudgetWithElapsedTime(TimeDelta delta)方法中。 - 用已发送的数据量来计算还剩多少数据量可以发送,在
UpdateBudgetWithSentData(DataSize size)方法中。
详细流程:
(1). 如果媒体数据包处理模式是 kDynamic,则检查期望的发送时间和 前一次数据包处理时间 的对比,当前者大于后者时,则根据两者的差值更新预计仍在传输中的数据量,以及 前一次数据包处理时间;(Periodic模式是5ms执行一次)
(2). 从媒体数据包优先级队列中取一个数据包出来;
(3). 第 (2) 步中取出的数据包为空,但已经发送的媒体数据的量还没有达到码率探测器 webrtc::BitrateProber 建议发送的最小探测数据量,则创建一些填充数据包放入媒体数据包优先级队列,并继续下一轮处理;
(4). 发送取出的媒体数据包;
(5). 获取 FEC 数据包,并放入媒体数据包优先级队列;
(6). 根据发送的数据包的数据量,更新预计仍在传输中的数据量等信息;
(7). 如果是在码率探测期间,且发送的数据量超出码率探测器 webrtc::BitrateProber 建议发送的最小探测数据量,则结束发送过程;
(8). 如果媒体数据包处理模式是 kDynamic,则更新目标发送时间。
RoundBobinPacketQueue
RoundBobinPacketQueue是一个缓存队列, 用于缓存数据包(音视频包,fec,padding,重传包),它有两个特征:
- 根据优先级存储包(每种类型包都有优先级)。
- 记录缓存时长(记录每个包的入队时间,用于计算缓存的总时长,避免引入过多的延迟)。
类图

上图种的Stream类代表了一路流,QueuePacket类代表了数据包。
RoundBobinPacketQueue三个核心的数据结构:
std::map<uint32_t, Stream> streams_
key为ssrc。
std::multimap<StreamPrioKey, uint32_t> **stream_priorities_**
**Stream**的优先级信息表,以**priority**和**DataSize**为比较的key,value是ssrc。通过优先级找Stream,方便优先级变化的实现。越靠前,优先级越高。
Stream类中的std::multimap<StreamPrioKey, uint32_t>::iterator priority_it;它指向 RoundBobinPacketQueue中的stream_priorities_中的某项,可以快速定位到自己的优先级。
std::multiset<Timestamp> enqueue_times_
The enqueue time of every packet currently in the queue. Used to figure out the age of the oldest packet in the queue.
记录每一个包的入队时间
QueuedPacket对象中的std::multiset<Timestamp>::iterator enqueue_time_it_;指向enqueue_times_中的项,可以快速定位到自己的入队时间。
Stream,QueuePacket,RoundBobinPacketQueue关系图
如下是Stream对象,QueuePacket对象与RoundBobinPacketQueue对象的关系图。

上图是以Stream为中心,描绘Stream,QueuePacket,RoundBobinPacketQueue的关系。
- 每个
Stream都被记录在RoundRobinPacketQueue的streams_中,以ssrc为key。 - 每个
Stream的优先级都被记录在RoundRobinPacketQueue的stream_priorites_中,以优先级为key,ssrc为value。 - 数据包都被封装成
QueuePacket缓存在Stream对象的packet_queue中,它也是一个优先级队列,所以每个数据包都是有优先级的。 RoundRobinPacketQueue的enqueue_times_记录着每个rtp packet的入队时间。stream中std::multimap<StreamPrioKey,uint32_t>::iterator priority_it迭代器指向该stream在stream_priorites_中的位置,便于快速检索。QueuedPacket中的std::multiset<Timestamp>::iterator enqueue_time_it迭代器指向该packet在enqueue_times_中的位置,便于快速检索。
缓存队列中记录的信息有:
- 记录总的缓存包个数。
- 记录总的数据量。
- 记录包的优先级。
- 记录包的入队时间(计算包的缓存总时长,平均缓存时间,最大缓存时间)。
插入队列(push方法)的逻辑
- 从streams_中找pakcet所属的Ssrc的stream,如果没有,则在streams_中插入一项。
- 查看stream的priority_it是否等于stream_priorities_的end():如果相等,则在stream_priorities插入新的项; 否则,如果新包的优先级高,则更新其ssrc对应队列的优先级。
- 更新队列总时长。
- 入队时间减去暂停时间(一般不会有暂停)。
- 队列总包数+1。
- 队列总字节大小+包的负载大小+Padding大小(Packet的大小)。
- 插入到steam对象的packet_queue中。
push流程的注意点:
- stream的size指的是stream发送的size,在Pop中,会加上弹出的PacketSize。
- 一条stream的packet的priority值都是一样的。
- 在入队一个stream的新的packet时,并不确定优先级,触发优先级队列中没有记录或packet的优先级发生变化。
取数据(Pop方法)的逻辑
- 获得优先级最高的stream。
- 从stream的packet_queue中取出第一个Packet。
- 将stream在stream_priorites_中的项删除掉。
- 计算Packet入队后到现在的时间(不包括暂停时间)。
- 将这段时间从队列的总时间中减去。
- 从equeue_times_中将Packet的项删除。
- 总包数减一。
- 总字节数减去包的字节数。
- 将包从stream中的queue中弹出。
- 如果stream中的队列为空,则令stream的priority_it指向stream_priorities的end()。
- 否则,从stream队列头部取Packet,将该Packet的priority插入到stream_priorities_中。
缓存时间的计算
计算缓存时间的目的是控制延迟,包括如下几个方法:
- 获取缓存时间最长的包
Timestamp RoundRobinPacketQueue::OldestEnqueueTime() const {
if (single_packet_queue_.has_value()) {
return single_packet_queue_->EnqueueTime();
}
i

最低0.47元/天 解锁文章
604

被折叠的 条评论
为什么被折叠?



