webrtc pacer模块(一) 平滑处理的实现

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,包括两个核心类:RoundBoinPacketQueueIntervalBudget

  • RoundBobinPacketQueue 缓存队列,对每条流都会缓存,以ssrc做为流的唯一标识,包括:重传包,fec,padding包。
  • IntervalBudget 根据设置的目标码率值及时间间隔计算可发送的数据量。

PacingController类

所属文件为\modules\pacing\pacing_controller.h,如下类图:

两个核心的成员变量:

  1. RoundRobinPakcetQueue packet_queue_ packet的缓存队列。
  2. IntervalBudget media_buget_可发送数据量计算。

两个核心函数:

  1. NextSendTime,获取每次执行的时间(5毫秒,在kPeriodic模式下)。
  2. ProcessPackets,周期处理包的发送,确定要发送的数据量,从缓存队列中取包。
平滑逻辑的处理流程

整个pacer运行的机制就是靠PacingControllerNextSendTimeProcessPackets两个方法,它们被单独的放在一个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++

核心骨架就是下面三个步骤:

  1. 设置目标码率,通过SetPacingRates(...)方法。
  2. 计算每个时间片可以发送的数据,在UpdateBudgetWithElapsedTime(TimeDelta delta)方法中。
  3. 用已发送的数据量来计算还剩多少数据量可以发送,在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,重传包),它有两个特征:

  1. 根据优先级存储包(每种类型包都有优先级)。
  2. 记录缓存时长(记录每个包的入队时间,用于计算缓存的总时长,避免引入过多的延迟)。
类图

上图种的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都被记录在RoundRobinPacketQueuestreams_中,以ssrc为key。
  • 每个Stream的优先级都被记录在RoundRobinPacketQueuestream_priorites_中,以优先级为key,ssrc为value。
  • 数据包都被封装成QueuePacket缓存在Stream对象的packet_queue中,它也是一个优先级队列,所以每个数据包都是有优先级的。
  • RoundRobinPacketQueueenqueue_times_记录着每个rtp packet的入队时间。
  • streamstd::multimap<StreamPrioKey,uint32_t>::iterator priority_it迭代器指向该stream在stream_priorites_中的位置,便于快速检索。
  • QueuedPacket中的std::multiset<Timestamp>::iterator enqueue_time_it迭代器指向该packet在enqueue_times_中的位置,便于快速检索。

缓存队列中记录的信息有:

  1. 记录总的缓存包个数。
  2. 记录总的数据量。
  3. 记录包的优先级。
  4. 记录包的入队时间(计算包的缓存总时长,平均缓存时间,最大缓存时间)。
插入队列(push方法)的逻辑
  1. 从streams_中找pakcet所属的Ssrc的stream,如果没有,则在streams_中插入一项。
  2. 查看stream的priority_it是否等于stream_priorities_的end():如果相等,则在stream_priorities插入新的项; 否则,如果新包的优先级高,则更新其ssrc对应队列的优先级。
  3. 更新队列总时长。
  4. 入队时间减去暂停时间(一般不会有暂停)。
  5. 队列总包数+1。
  6. 队列总字节大小+包的负载大小+Padding大小(Packet的大小)。
  7. 插入到steam对象的packet_queue中。

push流程的注意点:

  • stream的size指的是stream发送的size,在Pop中,会加上弹出的PacketSize。
  • 一条stream的packet的priority值都是一样的。
  • 在入队一个stream的新的packet时,并不确定优先级,触发优先级队列中没有记录或packet的优先级发生变化。
取数据(Pop方法)的逻辑
  1. 获得优先级最高的stream。
  2. 从stream的packet_queue中取出第一个Packet。
  3. 将stream在stream_priorites_中的项删除掉。
  4. 计算Packet入队后到现在的时间(不包括暂停时间)。
  5. 将这段时间从队列的总时间中减去。
  6. 从equeue_times_中将Packet的项删除。
  7. 总包数减一。
  8. 总字节数减去包的字节数。
  9. 将包从stream中的queue中弹出。
  10. 如果stream中的队列为空,则令stream的priority_it指向stream_priorities的end()。
  11. 否则,从stream队列头部取Packet,将该Packet的priority插入到stream_priorities_中。
缓存时间的计算

计算缓存时间的目的是控制延迟,包括如下几个方法:

  • 获取缓存时间最长的包
Timestamp RoundRobinPacketQueue::OldestEnqueueTime() const {
   
   
  if (single_packet_queue_.has_value()) {
   
   
    return single_packet_queue_->EnqueueTime();
  }

  i
<think>好的,用户问的是WebRTC如何处理多个并发的NACK请求,特别是涉及RTCP NACK和SSRC的部分。我需要结合之前的回答和用户提供的引用来组织内容。 首先,用户之前的问题是关于RTCP的NACK包为什么有两个SSRC,我的回答详细说明了两个SSRC的作用,现在用户进步询问处理多个NACK请求的方法。需要确保回答结构清晰,分点说明。 根据用户提供的引用,有几个关键点需要涵盖: 1. **重传码率控制**:引用1提到WebRTC个重传码率控制器,通过统计单位时间窗口的数据量来限流,避免网络风暴。这应该是处理并发NACK的重要机制。 2. **避免重复请求**:引用2指出WebRTC保证不针对同个数据包重复发送NACK请求,这可能涉及请求合并或去重机制。 3. **RTX重传通道**:引用4提到使用不同的SSRC和PT进行重传,这可能涉及多路复用或独立通道来处理多个请求,避免与原流冲突。 4. **优先级调度**:引用1提到发送端将重传请求放入pacer,可能需要优先级队列或调度策略,如按时间或序列号排序。 另外,用户提到SSRC,需要结合之前的回答中的两个SSRC字段,说明在并发情况下如何区分不同发送源和接收端,确保正确路由重传请求。 还要考虑WebRTC内部模块的协作,比如RTCPReceiver和RTCPSender如何处理多个请求,可能涉及SSRC分组管理(如引用4中的FID分组)或者缓冲区查找机制。 需要确保回答覆盖这些技术点,并且引用相关的引用来源,比如引用1的码率控制,引用2的重复请求避免,引用4的RTX机制。同时保持语言清晰,分步骤或分模块解释处理流程,可能还需要举例说明,比如多方会议中的处理情况。 还要注意用户可能关注的是并发请求带来的挑战,如网络抖动时的处理,所以需要强调WebRTC如何限流和调度,避免瞬时高负载。此外,是否需要提到具体的模块或类,如webrtc::ModuleRtpRtcpImpl2或webrtc::RTCPSender,以增强专业性? 最后,确保相关问题部分与当前主题相关,比如进步询问码率控制器的实现细节,或RTX的具体配置等。</think>在WebRTC处理多个并发NACK请求时,系统通过**分层控制机制**和**协议规范设计**实现高效管理,以下是具体实现方式及其与SSRC的关联: --- ### 、核心处理机制 #### 1. **请求合并与去重**(基于SSRC标识) - **请求聚合**:接收端通过$RTPReceiver$模块对同发送端(由**媒体源SSRC**标识)的多个NACK请求进行合并,将相邻丢失的RTP包序号合并为个NACK报文(如`[10,12-15]`)[^2] - **重复抑制**:通过`webrtc::NackModule2`实现时间窗口过滤,避免对同包(由序列号+SSRC唯标识)重复发送请求[^2] #### 2. **重传优先级调度** - **时间敏感排序**:发送端根据NACK请求中携带的RTP包序号和时间戳信息,按以下规则处理: - 优先补传关键帧(如视频的IDR帧) - 对音频数据采用更严格的延迟约束 - 使用公式计算优先级: $$Priority = \frac{1}{T_{current} - T_{packet\_lost}}$$ 其中$T_{current}$为当前时间,$T_{packet\_lost}$为丢包时间[^1] #### 3. **带宽协同控制** - **码率限流器**:通过`webrtc::RetransmissionRateLimiter`实现动态控制: ```cpp // WebRTC源码示例(简化) if (retransmitted_bytes_in_window > estimated_available_bandwidth) { DeferRetransmission(nack_list); // 延迟重传 } else { SendPacketViaRtx(target_ssrc, packet); // 通过RTX通道发送 } ``` 该机制确保重传流量不超过当前评估带宽的30%[^1] --- ### 二、SSRC的关键作用 #### 1. **媒体源标识分离** - 通过**目标SSRC**(第个SSRC字段)区分不同发送源,例如: - 视频流SSRC: `123456` - 音频流SSRC: `789012` 每个媒体源独立维护重传队列,避免跨流干扰 #### 2. **反馈路径管理** - **反馈端SSRC**(第二个SSRC字段)用于: - 建立接收端质量统计模型(如丢包率计算) - 在SFU架构中识别具体接收节点 - 实现差异化重传策略(例如对关键用户优先处理) #### 3. **RTX通道隔离** - 使用独立SSRC标识重传流(如原视频流SSRC=96,重传流SSRC=97),其关联关系通过SDP声明: ```plaintext a=ssrc-group:FID 1753454336 165688474 // FID表示主备流关系 a=rtpmap:97 rtx/90000 a=fmtp:97 apt=96 // 指明97号SSRC用于重传96号SSRC的包[^4] ``` 这种设计避免原始流与重传流的SSRC冲突 --- ### 三、网络拥塞应对策略 1. **动态窗口调整** 根据RTT变化自动缩放NACK列表的存储窗口,计算公式: $$Window_{size} = \min(RTT \times 2, 1000ms)$$ 2. **Pacer平滑发送** 将重传包注入`webrtc::PacedSender`队列,通过以下方式避免突发流量: - 使用漏桶算法控制发送间隔 - 为NACK包设置独立优先级等级(高于普通数据,低于FEC) 3. **选择性放弃机制** 若包超过最大重传次数(默认3次)或超过播放 deadline,则主动丢弃并触发PLC(丢包隐藏) --- ### 四、典型处理流程示例 以多方视频会议场景为例: 1. 接收端检测到SSRC=101的视频流丢失包#100-#103 2. 生成NACK报文: ```plaintext SSRC of media source = 101 SSRC of packet sender = 200(接收端自身标识) Lost packet list: 100-103 ``` 3. 发送端通过SSRC=101定位到视频发送缓冲区 4. 通过RTX通道(SSRC=102)发送重传包 5. 带宽控制器统计SSRC=200的反馈频率,动态调整该接收端的服务质量 ---
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

mo4776

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值