当前位置:网站首页>WebRTC源码分析 nack详解
WebRTC源码分析 nack详解
2022-08-10 17:23:00 【音视频开发老舅】
1、Nack过程
1.1 nack是什么
丢包重传(NACK)是抵抗网络错误的重要手段。NACK在接收端检测到数据丢包后,发送NACK报文到发送端;发送端根据NACK报文中的序列号,在发送缓冲区找到对应的数据包,重新发送到接收端。NACK需要发送端,发送缓冲区的支持。
1.2 nack流程
发送端发送rtp,到达接收端时,发现丢包,接收端发送nack请求,发送端会从历史队列中取出数据重发。
2、Nack协议实现
2.1 rfc协议
在rfc4585协议中定义可重传未到达数据的类型有二种:
1)RTPFB:rtp报文丢失重传(nack)。
2)PSFB:指定净荷重传,指定净荷重传里面又分如下三种(关键帧请求):
1、PLI (Picture Loss Indication) 视频帧丢失重传。
2、SLI (Slice Loss Indication) slice丢失重转。
3、RPSI (Reference Picture Selection Indication)参考帧丢失重传。
在创建视频连接的SDP协议里面,会协商以上述哪种类型进行NACK重转。以webrtc为例,会协商两种NACK,一个rtp报文丢包重传的nack(nack后面不带参数,默认RTPFB)、PLI 视频帧丢失重传的nack。

rtcp包格式
本文福利, C++音视频学习资料包、技术视频,内容包括(音视频开发,面试题,FFmpeg ,webRTC ,rtmp ,hls ,rtsp ,ffplay ,srs)↓↓↓↓↓↓见下面↓↓文章底部点击领取↓↓

nack rtcp报文格式如上图所示,pt=205。Packet identifier(PID) 为丢包起始参考值,Bitmap of Lost Packets(BLP)为16位的bitmap,对应为1的为表示丢包数据,具体如下抓包分析:
Packet identifier(PID)为176。Bitmap of Lost Packets(BLP):0x6ae1。解析的时候需要按照小模式解析,0x6ae1对应二进制:110101011100001倒过来看1000 0111 0101 0110。按照1bit是丢包,0bit是没有丢包解析,丢失报文序列号分别是:176 177 182 183 184 186 188 190 191与wireshark解析一致,当然pid和blp可以有多个。
3、发送流程
3.1 数据流
以视频为例
3.2 调用堆栈
H264EncoderImpl::Encode
VideoStreamEncoder::OnEncodedImage
VideoSendStreamImpl::OnEncodedImage
RtpVideoSender::OnEncodedImage
RTPSenderVideo::SendEncodedImage
RTPSenderVideo::SendVideo
RTPSenderVideo::LogAndSendToNetwork
RTPSender::EnqueuePackets
pacer //
RtpSenderEgress::SendPacket
//放入队列
RtpPacketHistory::PutRtpPacket
3.3 RtpPacketHistory
RtpPacketHistory 负责缓存历史数据,有nack请求时,从此队列发送
4、视频Nack过程
4.1 rtp接收数据流程

4.2 nack对报序号的管理
接收到rtp数据包后,会调用nack模块对报序号进行记录,同时检测是否需要nack请求
看代码
int NackRequester::OnReceivedPacket(uint16_t seq_num,
bool is_keyframe,
bool is_recovered) {
RTC_DCHECK_RUN_ON(worker_thread_);
bool is_retransmitted = true;
if (!initialized_) {
newest_seq_num_ = seq_num;
if (is_keyframe)
keyframe_list_.insert(seq_num);
initialized_ = true;
return 0;
}
// Since the `newest_seq_num_` is a packet we have actually received we know
// that packet has never been Nacked.
if (seq_num == newest_seq_num_)
return 0;
//如果接收的报序号小于之前接收到的,可能是乱序的包,可能是重传包
//如果nack列表有,则清除
if (AheadOf(newest_seq_num_, seq_num)) {
// An out of order packet has been received.
auto nack_list_it = nack_list_.find(seq_num);
int nacks_sent_for_packet = 0;
if (nack_list_it != nack_list_.end()) {
nacks_sent_for_packet = nack_list_it->second.retries;
nack_list_.erase(nack_list_it);
}
if (!is_retransmitted)
UpdateReorderingStatistics(seq_num);
return nacks_sent_for_packet;
}
// Keep track of new keyframes.
//保留最新的关键帧包
if (is_keyframe)
keyframe_list_.insert(seq_num);
// And remove old ones so we don't accumulate keyframes.
auto it = keyframe_list_.lower_bound(seq_num - kMaxPacketAge);
if (it != keyframe_list_.begin())
keyframe_list_.erase(keyframe_list_.begin(), it);
if (is_recovered) {
recovered_list_.insert(seq_num);
// Remove old ones so we don't accumulate recovered packets.
auto it = recovered_list_.lower_bound(seq_num - kMaxPacketAge);
if (it != recovered_list_.begin())
recovered_list_.erase(recovered_list_.begin(), it);
// Do not send nack for packets recovered by FEC or RTX.
return 0;
}
AddPacketsToNack(newest_seq_num_ + 1, seq_num);
newest_seq_num_ = seq_num;
// Are there any nacks that are waiting for this seq_num.
//获取nack序号,如果有则触发nack
std::vector<uint16_t> nack_batch = GetNackBatch(kSeqNumOnly);
if (!nack_batch.empty()) {
// This batch of NACKs is triggered externally; the initiator can
// batch them with other feedback messages.
nack_sender_->SendNack(nack_batch, /*buffering_allowed=*/true);
}
return 0;
}
这部分逻辑主要是收到包,查一下是不是乱序的,可能是网络造成乱序,也可能是重发过来的,收到了就把nack list里面的记录删掉
void NackRequester::AddPacketsToNack(uint16_t seq_num_start,
uint16_t seq_num_end) {
// Called on worker_thread_.
// Remove old packets.
//kMaxPacketAge=1000,删除超出队列数量,删除最老的
auto it = nack_list_.lower_bound(seq_num_end - kMaxPacketAge);
nack_list_.erase(nack_list_.begin(), it);
// If the nack list is too large, remove packets from the nack list until
// the latest first packet of a keyframe. If the list is still too large,
// clear it and request a keyframe.
//nack_list 的最大容量为 kMaxNackPackets = 1000,
//如果满了会删除最后一个 KeyFrame 之前的所有nacked 序号,
//如果删除之后还是满的那么清空 nack_list 并请求KeyFrame
uint16_t num_new_nacks = ForwardDiff(seq_num_start, seq_num_end);
if (nack_list_.size() + num_new_nacks > kMaxNackPackets) {
while (RemovePacketsUntilKeyFrame() &&
nack_list_.size() + num_new_nacks > kMaxNackPackets) {
}
if (nack_list_.size() + num_new_nacks > kMaxNackPackets) {
nack_list_.clear();
RTC_LOG(LS_WARNING) << "NACK list full, clearing NACK"
" list and requesting keyframe.";
keyframe_request_sender_->RequestKeyFrame();
return;
}
}
for (uint16_t seq_num = seq_num_start; seq_num != seq_num_end; ++seq_num) {
// Do not send nack for packets that are already recovered by FEC or RTX
if (recovered_list_.find(seq_num) != recovered_list_.end())
continue;
NackInfo nack_info(seq_num, seq_num + WaitNumberOfPackets(0.5),
clock_->TimeInMilliseconds());
RTC_DCHECK(nack_list_.find(seq_num) == nack_list_.end());
nack_list_[seq_num] = nack_info;
}
}
我们可以看到AddPacketsToNack()函数主要实现了:
nack_list 的最大容量为 kMaxNackPackets = 1000, 如果满了会删除最后一个 KeyFrame 之前的所有nacked 序号, 如果删除之后还是满的那么清空 nack_list 并请求KeyFrame。
本文福利, C++音视频学习资料包、技术视频,内容包括(音视频开发,面试题,FFmpeg ,webRTC ,rtmp ,hls ,rtsp ,ffplay ,srs)↓↓↓↓↓↓见下面↓↓文章底部点击领取↓↓
获取需要nack的
std::vector<uint16_t> NackRequester::GetNackBatch(NackFilterOptions options) {
// Called on worker_thread_.
//只考虑根据序列号获取nacklist
bool consider_seq_num = options != kTimeOnly;
//只考虑根据时间戳获取nacklist
bool consider_timestamp = options != kSeqNumOnly;
//当前时间
Timestamp now = clock_->CurrentTime();
std::vector<uint16_t> nack_batch;
auto it = nack_list_.begin();
//遍历nack
while (it != nack_list_.end()) {
//初始化rtt为重发延时间隔
TimeDelta resend_delay = TimeDelta::Millis(rtt_ms_);
//如果使用了nack的配置
if (backoff_settings_) {
//设置最大的重发延时间隔
resend_delay =
std::max(resend_delay, backoff_settings_->min_retry_interval);
// 如果重试次数超过了1次,则重新计算幂指后的重发延迟间隔(避免重试频繁)
//每次延时增大25%,1.25的n次幂
if (it->second.retries > 1) {
TimeDelta exponential_backoff =
std::min(TimeDelta::Millis(rtt_ms_), backoff_settings_->max_rtt) *
std::pow(backoff_settings_->base, it->second.retries - 1);
resend_delay = std::max(resend_delay, exponential_backoff);
}
}
// 判断当前包seq_num是否该发送了(即超过了最大发送延迟时间)
bool delay_timed_out =
now.ms() - it->second.created_at_time >= send_nack_delay_ms_;
// 判断基于rtt延迟时间时是否该发送了(即超过了重发延迟时间)
bool nack_on_rtt_passed =
now.ms() - it->second.sent_at_time >= resend_delay.ms();
// 判断基于序列号时是否该发送了(即超过了重发延迟时间)
bool nack_on_seq_num_passed =
// 初次发送时有效,避免重复重发
it->second.sent_at_time == -1 &&
// 当前包序列号比较老
AheadOrAt(newest_seq_num_, it->second.send_at_seq_num);
// 如果该发送了
if (delay_timed_out && ((consider_seq_num && nack_on_seq_num_passed) ||
(consider_timestamp && nack_on_rtt_passed))) {
// 当前包seq_num加入到nack list
nack_batch.emplace_back(it->second.seq_num);
++it->second.retries; // 累积重试次数
// 设置发送时间
it->second.sent_at_time = now.ms();
// 超过最大重试次数了则从nack_list_移除
if (it->second.retries >= kMaxNackRetries) {
RTC_LOG(LS_WARNING) << "Sequence number " << it->second.seq_num
<< " removed from NACK list due to max retries.";
it = nack_list_.erase(it);
} else {
++it;
}
continue;
}
++it;
}
return nack_batch;
}
1、delay_timed_out :加入nacklist的时间大于要发送nack的延时
2、nack_on_rtt_passed :该序号上次发送NACK的时间到当前时间要超过前面计算出来的延时。
3:nack_on_seq_num_passed :确定有最新的包序号比这个大,是被丢失的
4.3 nack的触发时机
逻辑图

有两个地方触发nack,用红方框框出来了
- 1 当收到rtp数据,nack模块会记录包序号,包类型
- 2 线程定期检测是否存在丢包,需要nack请求
5、nack响应
5.1 rtcp数据流

5.2 源码
void ModuleRtpRtcpImpl2::OnReceivedNack(
const std::vector<uint16_t>& nack_sequence_numbers) {
if (!rtp_sender_)
return;
if (!StorePackets() || nack_sequence_numbers.empty()) {
return;
}
// Use RTT from RtcpRttStats class if provided.
int64_t rtt = rtt_ms();
if (rtt == 0) {
rtcp_receiver_.RTT(rtcp_receiver_.RemoteSSRC(), NULL, &rtt, NULL, NULL);
}
//取得rtt,把请求和rtt时间调用rtp补包
rtp_sender_->packet_generator.OnReceivedNack(nack_sequence_numbers, rtt);
}
void RTPSender::OnReceivedNack(
const std::vector<uint16_t>& nack_sequence_numbers,
int64_t avg_rtt) {
//设置历史队列rtt,取包时根据rtt计算
packet_history_->SetRtt(5 + avg_rtt);
for (uint16_t seq_no : nack_sequence_numbers) {
const int32_t bytes_sent = ReSendPacket(seq_no);
if (bytes_sent < 0) {
// Failed to send one Sequence number. Give up the rest in this nack.
RTC_LOG(LS_WARNING) << "Failed resending RTP packet " << seq_no
<< ", Discard rest of packets.";
break;
}
}
}
int32_t RTPSender::ReSendPacket(uint16_t packet_id) {
// Try to find packet in RTP packet history. Also verify RTT here, so that we
// don't retransmit too often.
absl::optional<RtpPacketHistory::PacketState> stored_packet =
packet_history_->GetPacketState(packet_id);
if (!stored_packet || stored_packet->pending_transmission) {
// Packet not found or already queued for retransmission, ignore.
return 0;
}
const int32_t packet_size = static_cast<int32_t>(stored_packet->packet_size);
const bool rtx = (RtxStatus() & kRtxRetransmitted) > 0;
std::unique_ptr<RtpPacketToSend> packet =
packet_history_->GetPacketAndMarkAsPending(
packet_id, [&](const RtpPacketToSend& stored_packet) {
// Check if we're overusing retransmission bitrate.
// TODO(sprang): Add histograms for nack success or failure
// reasons.
std::unique_ptr<RtpPacketToSend> retransmit_packet;
if (retransmission_rate_limiter_ &&
!retransmission_rate_limiter_->TryUseRate(packet_size)) {
return retransmit_packet;
}
if (rtx) {
retransmit_packet = BuildRtxPacket(stored_packet);
} else {
retransmit_packet =
std::make_unique<RtpPacketToSend>(stored_packet);
}
if (retransmit_packet) {
retransmit_packet->set_retransmitted_sequence_number(
stored_packet.SequenceNumber());
}
return retransmit_packet;
});
if (!packet) {
return -1;
}
packet->set_packet_type(RtpPacketMediaType::kRetransmission);
packet->set_fec_protect_packet(false);
std::vector<std::unique_ptr<RtpPacketToSend>> packets;
packets.emplace_back(std::move(packet));
paced_sender_->EnqueuePackets(std::move(packets));
return packet_size;
}
主要看一下RtpPacketHistory::GetPacketAndMarkAsPending 函数
有两个调用:
GetStoredPacket //按照序列号拿到packet
VerifyRtt //距离上次发送,超过rtt时间才能再次发送bool RtpPacketHistory::VerifyRtt(const RtpPacketHistory::StoredPacket& packet,
int64_t now_ms) const {
if (packet.send_time_ms_) {
// Send-time already set, this check must be for a retransmission.
if (packet.times_retransmitted() > 0 &&
now_ms < *packet.send_time_ms_ + rtt_ms_) {
// This packet has already been retransmitted once, and the time since
// that even is lower than on RTT. Ignore request as this packet is
// likely already in the network pipe.
return false;
}
}
return true;
}
6、注意
nack请求次数限制,kMaxNackRetries ,不会一直请求,超过10次就不在请求
nack间隔越来越大, 如果重试次数超过了1次,则重新计算幂指后的重发延迟间隔(避免重试频繁),每次延时增大25%,1.25的n次幂
nack最大数量是1000,大于的不会重传 kMaxPacketAge
nack线程会间隔20ms检测一次, kProcessIntervalMs 默认为20ms,
发送端收到nack请求后,检测距离上次时间超过rtt才能再次发送
本文福利, C++音视频学习资料包、技术视频,内容包括(音视频开发,面试题,FFmpeg ,webRTC ,rtmp ,hls ,rtsp ,ffplay ,srs)↓↓↓↓↓↓见下面↓↓文章底部点击领取↓↓
边栏推荐
- Embedded Development: Embedded Basics - Mapping Peripherals Using Arrays of Pointers
- TradingView_学习笔记
- leetcode:339 嵌套列表权重和
- 事务的隔离级别,MySQL的默认隔离级别
- skywalking漏洞学习
- 不止跑路,拯救误操作rm -rf /*的小伙儿
- Pytorch GPU模型推理时间探讨2——显卡warm up
- 中国芯片的营收首破万亿,优势凸显的成熟工艺产能将称霸全球
- Your local docbook2man was found to work with SGML rather than XML
- Trie字典树
猜你喜欢
随机推荐
Embedded Development: Embedded Basics - Mapping Peripherals Using Arrays of Pointers
leetcode:339 嵌套列表权重和
神经网络全连接层的作用,各种神经网络的优缺点
R语言使用oneway.test函数执行单因素方差分析(One-Way ANOVA)、使用数据集的子集数据进行单因素方差分析(subset函数筛选数据子集)
R语言检验时间序列的平稳性:使用fUnitRoots包中的adfTest函数检验时间序列数据是否具有平稳性(设置参数type为nc时、既不去除趋势也不进行中心化处理)
Your local docbook2man was found to work with SGML rather than XML
「企业架构」企业架构师,解决方案架构师和软件架构师有何不同
文件包含漏洞复习总结
DASCTF2022.07赋能赛 WEB题目复现
最详解决:jupyter notebook不会自动打开浏览器问题
深度学习培训二笔记
如何构建一个自己的代理ip池
电力系统潮流【牛顿-拉夫逊法】(4节点、5节点、6节点、9节点)(Matlab代码实现)
Talking about Taishan crowdfunding system development technical description and dapp chain crowdfunding system development analysis
Error creating bean with name ‘sqlSessionFactory‘ defined in class path reso「建议收藏」
aliexpress API 接入说明
router.afterEach()
JNDI and RMI, LDAP
轮询以及webSocket与socket.io原理
shell中判断文件目录是否存在









