Paced Sending (有节奏的发送)
有节奏的发送者, 通常被称为 “节拍器”, 是 WebRTC 的一部分. 在 RTP 栈中主要用于平滑的发送
数据包到网络上【为了避免网络拥塞而牺牲发送时间】.
背景
考虑一个 5Mbps 和 30fps 的视频流. 在理想情况下, 这将导致每帧数据约 21kB 大,
并打包成 18 个 RTP 包. 虽然一秒内滑动窗口的平均比特率是正确的 5Mbps,
在更短的时间尺度上, 它可以被视为每 33 毫秒爆发 167Mbps, 此外,
在突然移动(: 画面剧烈变化)的情况下, 视频编码器超出目标帧大小是很常见的, 特别是在处理屏幕共享时.
比理想尺寸大 10 倍甚至 100 倍的帧是一个非常真实的场景. 这些数据包爆发会导致几个问题,
如网络拥塞, 造成缓冲区膨胀, 甚至丢包. 大多数会议都有一种以上的媒体流, 例如视频和
音频. 如果你一口气把一帧数据放在网络上, 而这些数据包需要 100 毫秒到达另一端 -
这意味着你现在已经阻止了任何音频数据包及时到达远程端.
有节奏的发送者通过一个拥有媒体的队列缓冲区来解决这个问题, 然后使用 leaky bucket
算法将它们分配到网络上. 缓冲包含所有媒体音轨的独立 fifo 流, 这样音频就可以优先于视频
(: 通话中音频比视频优先级更高?) - 相等的 prio 流可以循环发送(:公平发送), 以避免任
何一个流阻塞其他流. 由于节拍器控制在线路上发送的比特率, 它也被用于在需要最小发送率
的情况下生成填充(: 填充冗余数据?) - 如果使用了比特率探测, 则生成分组序列.
数据包的生命周期
当使用定速发送器时, 数据包的典型路径看起来像这样:
RTPSenderVideo
或RTPSenderAudio
将媒体打包成 RTP 包.- RTP 包被发送到 RTPSender 类进行传输.
- 通过 RtpPacketSender 接口调用节拍器, 使数据包进入队列.
- 数据包被放入节拍器内的队列中, 等待适当的时刻发送它们.
- 在一个计算时间里, 节拍器调用
PacingController::PacketSender()
回调方法, 通常
由 PacketRouter 类实现. - 路由器根据数据包的 SSRC 将数据包转发到正确的 RTP 模块,
RTPSenderEgress
类做最后
的时间标记, 可能会记录下来以便重新传输等. - 数据包被发送到低级的
Transport
接口, 之后它就超出了作用域(: 生命周期结束).
与此异步的是, 估计的可用发送带宽是确定的 - 目标发送速率通过
void SetPacingRates(DataRate pacing_rate, DataRate padding_rate)
方法在
RtpPacketPacker
上设置.
数据包优先级
节拍器根据两个条件对数据包进行优先级排序:
- 数据包类型, 优先级从高到低:
- 音频 (Audio)
- 重传 (Retransmissions)
- 视频和FEC (Video and FEC)
- 填充 (Padding)
- 排队的顺序:
排队顺序是按每个流 (SSRC) 执行的. 如果优先级相同,
[RoundRobinPackageQueue][RoundRobinPackageQueue] 在媒体流之间交替,
以确保没有任何流不必要地阻塞其他流.
实现
目前有两种对节拍器的实现 (尽管它们通过 PacingController
类共享大量的逻辑).
传统的 PacedSender 使用一个专用线程以 5ms 的间隔
轮询 pacing controller, 并有一个锁来保护内部状态.
较新的 TaskQueuePacedSender, 顾名思义, 使用 TaskQueue 来
保护状态和调度数据包处理.
后者是动态的, 基于实际的发送速率和约束. 避免在新的应用程序中
使用遗留的 PacedSender, 因为我们计划删除它.
数据包的路由器
一个相邻的叫做 PacketRouter 的组件被用来路由从节拍器进入正确的 RTP
模块的数据包. 它具有以下功能:
SendPacket
方法查找与数据包对应的 RTP 模块, 以便进一步路由到网络.- 如果使用发送端带宽估计, 它将填充 传输范围的序列号 扩展.
- 生成填充. 模块支持 基于有效负载的填充的模块 具有优先级,
最后一个发送媒体的模块始终是首选. - 在发送媒体后返回任何生成的 FEC.
- 转发 REMB 和/或 TransportFeedback 消息到合适的 RTP 模块.
目前, FEC 是在每个 SSRC 的基础上产生的, 所以总是在发送媒体后从 RTP 模块返回.
希望有一天我们能用一个 FlexFEC 流覆盖多个流 - 而包路由器很可能是 FEC 生成器存在的地方.
它甚至可以作为 RTX 的替代方案用于 FEC 填充.
The API
这一节概述了与的节拍器的几个不同用例相关的类和方法.
数据包发送
对于发送数据包, 使用
RtpPacketSender::EnqueuePackets(
std::vector<std::unique_ptr<RtpPacketToSend>> packets
)
作为构造函数的参数, 该回调在实际发送数据包时使用.
发送速率
要控制发送速率, 请使用
void SetPacingRates(DataRate pacing_rate, DataRate padding_rate)
如果包队列为空, 发送速率下降到 padding_rate
以下, 节拍器将从 PacketRouter
请求填充数据包.
为了完全暂停/恢复发送数据(例如由于网络可用), 使用 Pause()
和 Resume()
方法.
在某些情况下, 指定的步调速率可能会被重写, 例如由于极端编码器超调.
使用 void SetQueueTimeLimit(TimeDelta limit)
来指定您希望数据包在节拍器队列中等待
的最长时间(不包括暂停). 然后, 实际发送速率可能会增加到超过 pacing_rate, 以尝试使
average 队列时间小于请求的限制. 这样做的基本原理是, 如果发送队列超过3秒, 最好冒着
数据包丢失的风险, 然后尝试使用关键帧来恢复, 而不是造成严重的延迟.
带宽估算
如果带宽估计器支持带宽探测, 它可能会请求以指定的速率发送一组探测包, 以便评估这是否会导致
网络上的延迟/丢包增加. 使用
void CreateProbeCluster(DataRate bitrate, int cluster_id)
方法 - 通过
PacketRouter
发送的数据包将在附加的 PacedPacketInfo
struct 中标记相应的
cluster_id.
如果使用拥塞窗口推送, 状态可以使用 SetCongestionWindow()
和 UpdateOutstandingData()
更新.
还有一些方法可以控制我们的节奏:
SetAccountForAudioPackets()
确定音频包是否计入所消耗的带宽.SetIncludeOverhead()
确定整个RTP包大小是否计入所使用的带宽(否则只计入媒体负载).SetTransportOverhead()
设置每个包消耗的额外数据大小, 例如UDP/IP报头.
Stats
几种方法可以用来收集节拍器状态的统计信息:
OldestPacketWaitTime()
队列中最老的包被添加的时间.QueueSizeData()
队列中当前的总字节数.FirstSentPacketTime()
发送第一个报文的绝对时间.ExpectedQueueTime()
队列中的总字节数除以发送速率.