丢包检测与重传作为WebRTC中一个抗弱网机制,对提高QOS起了很大作用。WebRTC中目前主要靠NACK机制实现丢包检测与重传,接收端判断丢包情况并记录,然后通过NACK RTCP报文(RFC 4585)反馈丢包信息给发送端,发送端根据NACK报文信息进行包的重传。
NACK机制针对的是某个SSRC的单独流,对于某个SSRC流,对于一个包N
,接收端要检测它是否丢了,需要前面收到的该流的包N-1
,以及后面收到该流的包N+1
的信息,通过检测N-1
以及N+1
间是否有空隙判断N
是否丢了。如果因为一些特殊原因,包N+1
到晚了些,那么我们就无法判断N
是否丢失了,假如N
真丢了,会造成包N
的恢复时间变长。如果该流所在传输通道,还有其它SSRC流,那么可否借助其它流信息更早检测丢包?
目前WebRTC默认都是采用基于发送端的带宽估计(Send-side BWE),通过报头扩展携带的传输序列号Transport sequence number以及Transport Feedback RTCP报文携带的信息进行相关处理。对于单个传输通道,包含的所有流,携带的传输序列号采用统一的计数。Transport Feedback RTCP中基于传输序列号记录着包的详细信息,例如是否收到,到达时间等。既然Transport Feedback RTCP记录着接收端收到的所有流的包信息,那么就可以利用这个进行丢包检测。这样对于某个SSRC流,要检测丢包,就可以借助其他SSRC流的传输序列号了。对于单个传输通道多条流场景,例如单PC或者Simulcast,相比NACK机制,能够更早地检测到丢包情况。在WebRTC这个机制叫做早期丢包检测(Early loss detection)。
与NACK机制差异
我们通过某个例子解释下早期丢包检测机制为什么会快些。假设某个传输通道下有视频流A与B,它们的RTP包记为Ra(n,m)
,Rb(n,m)
,n
表示RTP sequence number
,m
表示Transport sequence number
,这样同一个传输通道(PeerConnection)下,视频流按如下形式传输:
Ra(1,1),Ra(2,2),Rb(1,3),Rb(2,4),Ra(3,5),Ra(4,6),Rb(3,7),Rb(4,8)
假设只有Ra(2,2)丢了,在NACK机制中,只能处理单个SSRC的流,通过RTP序列号判断丢包。需要收到Ra(3,5)或者更靠后的Ra包,才能判断Ra(2,2)丢失。而在早期丢包检测机制中,可以直接通过Rb(1,3)的传输序列号判断Ra(2,2)丢失,所以能更早地检测到。
这里我们总结下跟NACK机制的差异:
1)NACK机制处理的是某个SSRC流,通过报头携带的RTP序列号判断
2)Early loss detection机制处理的是某个传输通道所有流,通过报头扩展携带的传输序列号判断。
接下来我们详细介绍下Early loss detection机制。
Transport Feedback RTCP
首先我们简单复习下Transport Feedback RTCP报文格式。Transport Feedback RTCP中记录着一系列包的到达状态以及时间。
记录的第一个RTP包传输序列号Transport sequence number为base sequence number,总共记录着packet status count个RTP包信息,这些RTP包Transport sequence number以base sequence number为基准,并依次递增。
通过packet chunk记录各个RTP包的到达状态,主要有:packet received,packet not received这两种状态。
以reference time为基准,通过recv delta记录各个"packet received"状态的包的到达时间信息。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
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 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |V=2|P| FMT=15 | PT=205 | length | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | SSRC of packet sender | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | SSRC of media source | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | base sequence number | packet status count | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | reference time | fb pkt. count | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | packet chunk | packet chunk | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ . . . . +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | packet chunk | recv delta | recv delta | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ . . . . +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | recv delta | recv delta | zero padding | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |
检测原理
前面我们简单回顾了Transport Feedback RTCP的格式,那么通过这个RTCP检测丢包就很简单了。"packet not received"状态的包就是我们所认为的丢包。只要我们解析Transport Feedback RTCP,获取这些"packet not received"状态的包,然后通过这些RTP包的Transport sequence number,从发送端记录的RTP包发送历史中获取对应RTP包的SSRC以及RTP sequence number,告知相应发送模块,即可实现重传。
接收端
接收端主要处理代码位于RemoteEstimatorProxy::MaybeBuildFeedbackPacket
中,packet_arrival_times_
记录着收到的RTP包的Transport Sequence Number以及到达时间。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
for (int64_t seq = start_seq; seq < end_seq; ++seq) { int64_t arrival_time_ms = packet_arrival_times_.get(seq); if (arrival_time_ms == 0) { continue; } if (feedback_packet == nullptr) { feedback_packet = std::make_unique<rtcp::TransportFeedback>(include_timestamps); feedback_packet->SetMediaSsrc(media_ssrc_); feedback_packet->SetBase( static_cast<uint16_t>(begin_sequence_number_inclusive & 0xFFFF), arrival_time_ms * 1000); feedback_packet->SetFeedbackSequenceNumber(feedback_packet_count_++); } // 添加收到的包的传输序列号以及到达时间 if (!feedback_packet->AddReceivedPacket(static_cast<uint16_t>(seq & 0xFFFF), arrival_time_ms * 1000)) { break; } } |
发送端
发送端处理代码位于RtpVideoSender::OnPacketFeedbackVector
中。主要处理代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
// Map from SSRC to vector of RTP sequence numbers that are indicated as // lost by feedback, without being trailed by any received packets. std::map<uint32_t, std::vector<uint16_t>> early_loss_detected_per_ssrc; for (const StreamPacketInfo& packet : packet_feedback_vector) { // Only include new media packets, not retransmissions/padding/fec. if (!packet.received && packet.ssrc && !packet.is_retransmission) { early_loss_detected_per_ssrc[*packet.ssrc].push_back( packet.rtp_sequence_number); } else { // Packet received, so any loss prior to this is already detectable. early_loss_detected_per_ssrc.erase(*packet.ssrc); } } for (const auto& kv : early_loss_detected_per_ssrc) { const uint32_t ssrc = kv.first; auto it = ssrc_to_rtp_module_.find(ssrc); RTC_CHECK(it != ssrc_to_rtp_module_.end()); RTPSender* rtp_sender = it->second->RtpSender(); for (uint16_t sequence_number : kv.second) { rtp_sender->ReSendPacket(sequence_number); } } |
通过SSRC找到对应的RTPSender
,传入RTP序列号完成重传。
可能引起的问题
由于WebRTC中存在这种机制,那么我们自己编写接收端代码时需要注意一些事(一般是服务端),否则会引起问题。
举个例子,客户端上行推Simulcast流。大流B表示,小流S表示,后面跟着传输序列号。某次传输情况如下:
1 |
B1 B2 B3 B4 S5 S6 B7 B8 B9 B10 S11 S12... |
假如我们服务端是单独处理大小流,构造大小流Transport Feecback RTCP报文也是单独处理,那么就会有个问题。
对大流的处理模块,收到的流为B1 B2 B3 B4 B7 B8 B9 B10,构造的Transport Feecback报文中记录的收到的传输序列号为:1 2 3 4 7 8 9 10,其中5,6由于属于小流导致记录为没收到。所以在发送端处理该Transport Feedback RTCP报文时,会将传输序列号5,6所属的RTP包重传,直到收到小流反馈的Transport Feecback报文。这样就会导致每收到反馈的Transport Feedback RTCP,就会重传RTP包,虽然并没有丢包。如果通过浏览器推流,那么在chrome://webrtc-internals/中会看到:
其中retransmittedPacketsSent
一直在增加。
不过对于接收端而言,这些重传的包会被SRTP处理过滤掉,在libSRTP库中会报srtp_err_status_replay_fail
错误,这是因为libSRTP的一个安全措施。libSRTP中有个窗口,记录着收到的包,如果后面收到的包在这个窗口内,会被认为是重复的包,从而报srtp_err_status_replay_fail
错误,然后被丢弃。
所以编写服务端代码时,同一个传输通道的流,无论单PC,还是Simulcast场景,Transport Feecback RTCP构造得统一处理,这样就可以利用到早期丢包检测机制优势,同时避免增加不必要的带宽消耗。
总结
本文主要介绍了NACK机制与早期丢包检测机制差异,回顾了Transport Feedback RTCP报文格式,并结合代码简单介绍了早期丢包检测机制,最后举例讲了下这个机制可能引起的问题。WebRTC本身在不断演进,我们服务端开发的得时刻关注,做好兼容处理。
文章评论