NACK & RTX
Tags: WebRTC NACK RTX
written by cjqu
webrtc\video\video_receive_stream.cc
webrtc\video\rtp_stream_receiver.cc
webrtc\modules\video_coding\nack_module.cc
@(NACK)[RTCP-FB, WebRTC]
概述
相关RFC:
RFC 4585 Extended RTP Profile for RTCP-BaseFeedback (RTP/AVPF)
RFC 4588 RTP Retransmission Payload Format
RFC 4585 中定义了两种模式, "ACK mode" "NACK mode", 本文只描述 NACK mode
RTCP FeedBack Syntax
a=rtcp-fb:" rtcp-fb-pt SP rtcp-fb-val CRLF
rtcp-fb-pt = "*"; wildcard: applies to all formats
/ fmt; as defined in SDP spec
rtcp-fb-val = "ack" rtcp-fb-ack-param
/ "nack" rtcp-fb-nack-param
/ "trr-int" SP 1*DIGIT
/ rtcp-fb-id rtcp-fb-param
//NACK MODE
rtcp-fb-nack-param = SP "pli"
/ SP "sli"
/ SP "rpsi"
/ SP "app" [SP byte-string]
/ SP token [SP byte-string]
/ ; empty
//ACK MODE
rtcp-fb-ack-param = SP "rpsi"
/ SP "app" [SP byte-string]
/ SP token [SP byte-string]
/ ; empty
NACK Sender
WebRTC 源码细节
- video::VideoReceiverStream 继承NACK发送类
NackSender
, 实现了其纯虚接口SendNack
- SendNack 使用VideoReceiverStream 内部的
RtpStreamReceiver
对象的接口RequestPacketRetransmit
发送 NACK, 其内部使用RtpRtcp
对象的SendNack发送接口实现最终的 NACK 发送 - VideoReceiverStream 将 NackSender 传入底层 RtpStreamReceiver 中, 在这里创建了
NackModule
模块.
创建NackModule
需要指定 nack 和 keyframe 的sender. 会在 RtpStreamReceiver::OnReceivedPayloadData
中将收到的RTP传入NackModule 模块中(通过调用NackModule::OnReceivedPacket
), 在这里检测是否需要发送 NACK 请求.
void VideoReceiveStream::SendNack(
const std::vector<uint16_t>& sequence_numbers) {
rtp_stream_receiver_.RequestPacketRetransmit(sequence_numbers);
}
void RtpStreamReceiver::RequestPacketRetransmit(
const std::vector<uint16_t>& sequence_numbers) {
rtp_rtcp_->SendNack(sequence_numbers);
}
void ModuleRtpRtcpImpl::SendNack(
const std::vector<uint16_t>& sequence_numbers) {
for (auto it = sequence_numbers.begin(); it != sequence_numbers.end(); it++)
{
LOG(LS_WARNING) << "[RtpRtcp] NackList SendNack seq:" << *it;
}
rtcp_sender_.SendRTCP(GetFeedbackState(), kRtcpNack, sequence_numbers.size(),
sequence_numbers.data());
}
NACK 请求发送细节
WebRTC中使用 nack_list_
存储所有已经或者将要发送NACK请求出去过的序号, 以及该序号请求过的次数.
list 的
在 NackModule::OnReceivedPacket
中检测NACK的发送, 如果发现乱序的包(当前收到的包序号seq_num 在 seq_last_recv 的后面)需要检测 nack_list 中是否已经存在该序号, 如果是则从list中删除该序号.
如果包没有乱序, 则将 seq_last_recv + 1 到 seq_num 之间的包加入 nack_list 中.
-
nack_list 的最大容量为 kMaxNackPackets = 1000, 如果满了会删除最后一个 KeyFrame 之前的所有nacked 序号, 如果删除之后还是满的那么清空 nack_list 并请求KeyFrame.
-
GetNackBatch
接口 从nack_list 中取出需要发送 NACK 的序号列表, 如果某个 seq 请求次数超过kMaxNackRetries = 10
次则会从nack_list 中删除. -
最后在 NackModule 中触发使用
NackSender::SednNack
发送 NACK 请求 -
从
GetNackBatch
中获取需要批量发送的 nack_list 时有两个不同的逻辑:(1) 只考虑序号 kSeqNumOnly
这种策略在OnReceivedPacket
中检测那些需要发送NACk时会触发,
此时获取需要发送nack的列表的条件为:- 当前序号是第一次发送(本地记录的send_at_time == -1)
- 当前最新收到的包序号在这个需要发送NAKC的序号的前面(避免当前还在收之前没收到的包)
比如当前最新收到100, 当前检测是否需要发送NACK的序号为小于等于100的才满足条件, 比如 99
如果当前检测是否需要发送nack的序号为 103, 那么此时不满足条件. 因为很有可能接下来会收到丢失的103.
如果触发了NACK的发送那么记录发送时间以及重发次数, 用于后面实现发送间隔一个RTT以及超过重发此时(10)后不 再重发.
(2) 只考虑时间 kTimeOnly
这种策略在NackModule的Process()定时处理中使用
此时获取需要发送NACK的条件为:
* 该序号上次发送NACK的时间到当前时间要超过1个RTT(该序号一次也没发送过NACK(send_at_time == -1)也满足 这个条件)
*
NACK Receiver
WebRTC中处理 NACK 请求流程:
- 首先是正常的 RTCP 处理流程: RTCPReceiver 中解析处理RTCP 详细, 在
TriggerCallbacksFromRtcpPacket
处理不同的RTCP消息. - 如果是 kRtcpNack 消息则触发 RtpRtcp 对象的
ModuleRtpRtcpImpl::OnReceivedNack
处理流程:
将丢包的序号 记录到PacketLossStats
, 获取RTT后进入 RTPSedner.OnReceivedNack
. - RTPSender中完成在
PacketHistory
中查找需要发送的RTP seq, 并决定重发时间. 重发也需要经过重发的比特率限制的检查. RTPSedner 初始化话时可以配置是否使用(PacedSend, 均匀发送), 最后检查重发格式(RtxStatus() 可以获取是否使用 RTX 封装)后使用RTPSedner::PrepareAndSendPacket
进行立即重发. 如果是使用 PacedSend, 则使用PacedSender::InsertPacket
先加入发送列表中, 它的process会定时处理发送任务.
RTX
概述
RTP Retransmission Payload Format RFC 4588
RTX 结构
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| RTP Header |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| OSN | |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |
| Original RTP Packet Payload |
| |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
RFC 4588 中定义了两种 RTX scheme, Session-multiplexing / SSRC-multiplexing
多路会话
RTX重传流使用与主流不一样的RTP session来传输重传流, 媒体流的目的地址应该应该不一样, 使用相同的SSRC.
多路SSRC
主流和重传流在同一个 RTP Session中, 使用不同的 PT 和不同的SSRC 来区分RTX流和主流.
WebRTC 中使用多路SSRC这种方式.
OSN Orignal Sequence Number, 主流对应的 SN.
- PT 使用协商RTX时使用的PT, SN 使用当前 RTP session 变化规则, 递增.
- 其他RTP header 信息均使用重传的包对应的信息(主流的).
CNAME
RTX流和主流使用应该使用相同的CNAME
协商
Syntax
a=fmtp:<number> apt=<apt-value>;rtx-time=<rtx-time-val>
number : RTX流的PT
apt(Associated Payload Type) RTX流对应的主流的PT
rtx-time: 单位毫秒, 可用于RTX 重传的本地存储的 RTP 的历史时长, 可选, 默认值3000.
WebRTC 中的RTX特性
RTX 发送
- 在
RtcpReceiver
中处理 RTCP 消息, 获取需要重发的的包序号,RtpRtcp::OnReceiverNack
中处理NACK. - 将 nack_sequence_numbers 和 RTT 送入
RTPSender::OnReceivedNack
中处理. RTPSender
中有重发包的接口:ReSendPacket
, WebRTC中的重发包时间使用 RTT+5 ms- RTPSender 中实现了重发的比特率限制, 使用
RateLimiter *retransmission_rate_limiter_
实现判断重发某个包时是否满足比特率限制.
以上流程与收到 NACK 重传RTP包流程一致, 后续 在 RTPSender::PrepareAndSendPacket
中重发时判断是否使用 RTX 的封装格式.
WebRTC中的 RTX 配置
主要跟 rtx_ssrc , rtx_pt, rtx_mode相关.
-
RtpRtcp中使用 RtpSender 中提供的接口设置RTX相关参数:
//设置RTPSender中的 RTX 使用开关模式 void ModuleRtpRtcpImpl::SetRtxSendStatus(int mode) enum RtxMode { kRtxOff = 0x0, kRtxRetransmitted = 0x1, // Only send retransmissions over RTX. kRtxRedundantPayloads = 0x2 // Preventively send redundant payloads // instead of padding. }; //设置RTPSender中的RTX发送时的Payload, 包括 **rtx_pt 和 apt** ModuleRtpRtcpImpl::SetRtxSendPayloadType(int payload_type,int associated_payload_type) //用于设置RTPSender中的 RTX SSRC ModuleRtpRtcpImpl::SetRtxSsrc(uint32_t ssrc)
-
VideoSendStream 中在构造时使用VideoSendStream::Config中的配置信息设置RtpRtcp相关信息
(1) 检查 config.rtp.rtx.ssrcs是否配置, 如果没有则不设置 RTX 相关参数. 如果有配置则分别使用上述接口配置 SSRC, PT, RTX_MODE参数. 每个RtpRtcp 的rtx_ssrc使用不同配置, 但是 PT 和 rtx_mode 使用相同配置.
NOTE:
VideoSendStream中是根据 config.rtp.ssrcs 中存储的ssrc个数来创建对应数量的 RtpRtcp 模块的, 即一个SSRC对应一个 RtpRtcp而 config.rtp.rtx.ssrcs应该跟 config.rtp.ssrcs 相互对应,
即:RtpRtcp[i].SetRtxSsrc( config.rtp.rtx.ssrcs[i])
相关接口:
video_send_stream.cc
void VideoSendStreamImpl::ConfigureSsrcs()
rtp_rtcp->SetRtxSsrc(ssrc)
rtp_rtcp->SetRtxSendPayloadType(config_->rtp.rtx.payload_type,
config_->encoder_settings.payload_type);
rtp_rtcp->SetRtxSendStatus(kRtxRetransmitted | kRtxRedundantPayloads);Payloads);
-
VideoSendStream 中的配置信息 VideoSendStream::Config 是从
WebRtcVideoChannel2::WebRtcVideoSendStream
中创建 VideoSendStream 时copy过来的. 这里的Config信息都是从 StreamParams(SP) 中获取的(很多信息)(在WebRtcVideoSendStream的构造阶段).比如: config.rtp.ssrcs 和 config.rtp.rtx.ssrcs 就是SP中获取的.
rtp.ssrcs 从 ssrc-group:MID获取(如果没有MID则使用第一个SSRC),
再根据rtp.ssrcs从ssrc-group:FID中获取 rtp.rtx.ssrcsrtx_pt 从codec_setting中获取
config.rtp.rtx.payload_type = codec_setting.rtx_payload_type开启WebRTC的RTX的SDP协商需要包括:
a=rtpmap:97 rtx/90000
a=fmtp:97 apt=96
a=ssrc-group:FID 2736695910 239189782
其中 2736695910 是rtp.ssrcs中, 从MID中获取到的 PrimarySSRC , 239189782 是RTX对应SSRC.这里发现WebRTC一个BUG, 在RED(ULPFEC)开启, 本端使用 RED 封装媒体以及FEC流的, 如果需要本端重传
包使用RTX的格式, 那么对端的SDP不能只有 RED对应的RTX a行. 还需要其他媒体流的PT也有对应的RTX a行.
我提交的patch 点这里
NOTE
VideoEngine 中使用WebRTCVideoSendStream
封装VideoSendStream的, 也就是说在 VideoEngine 中是使用 WebRTCVideoSendStream来创建 VideoSendStream, WebRtc开头的接口是更上层的接口
相关接口
webrtcvideoengine2.cc
stream_ = call_->CreateVideoSendStream(std::move(config),
parameters_.encoder_config.Copy());
//各种SSRC group 语义
const char kFecSsrcGroupSemantics[] = "FEC";
const char kFecFrSsrcGroupSemantics[] = "FEC-FR";
const char kFidSsrcGroupSemantics[] = "FID";
const char kSimSsrcGroupSemantics[] = "SIM";