1.NACK的含义

丢包重传(NACK)是抵抗网络错误的重要手段。NACK在接收端检测到数据丢包后,发送NACK报文到发送端;发送端根据NACK报文中的序列号,在发送缓冲区找到对应的数据包,重新发送到接收端。NACK需要发送端,发送缓冲区的支持。

WebRTC中支持音频和视频的NACK重传。我们这里只分析nack机制,不分析jitterbuffer或者neteq的更多实现。

2.WebRTC中NACK请求发送的条件

这里以视频为例。

下面是webrtc中接收端触发nack的条件,我们看下nack_module.cc文件中OnReceivedPacket的实现。

void NackModule::OnReceivedPacket(const VCMPacket& packet) {rtc::CritScope lock(&crit_);if (!running_)return;//获取包的seqnumuint16_t seq_num = packet.seqNum;// TODO(philipel): When the packet includes information whether it is//                 retransmitted or not, use that value instead. For//                 now set it to true, which will cause the reordering//                 statistics to never be updated.bool is_retransmitted = true;//判断第一帧是不是关键帧bool is_keyframe = packet.isFirstPacket && packet.frameType == kVideoFrameKey;
//拿到第一个包的时候判断,把第一个包的seqnum赋值给最新的last_seq_num,如果是关键帧的话,插入到关键帧列表中,同时把initialized_设置为trueif (!initialized_) {last_seq_num_ = seq_num;if (is_keyframe)keyframe_list_.insert(seq_num);initialized_ = true;return;}if (seq_num == last_seq_num_)return;
//判断有无乱序,乱序了,如来1,2,3,6包,然后来4包,就乱序了,就把4从nack_list中去掉,不再通知发送端重新发送4了if (AheadOf(last_seq_num_, seq_num)) {// An out of order packet has been received.//把重新收到的包从nack_list中移除掉nack_list_.erase(seq_num);if (!is_retransmitted)UpdateReorderingStatistics(seq_num);return;} else {//没有乱序,如1,2,3,6包,就把(3+1,6)之间的包加入到nack_list中AddPacketsToNack(last_seq_num_ + 1, seq_num);last_seq_num_ = seq_num;// 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);// Are there any nacks that are waiting for this seq_num.//从nack_list 中取出需要发送 NACK 的序号列表, 如果某个 seq 请求次数超过 kMaxNackRetries = 10次则会从nack_list 中删除.std::vector<uint16_t> nack_batch = GetNackBatch(kSeqNumOnly);//LOG(LS_INFO) << "nack_batch size[" << nack_batch.size() << "].";//在 NackModule 中触发使用 NackSender::SednNack 发送 NACK 请求if (!nack_batch.empty())nack_sender_->SendNack(nack_batch);}
}

我们继续跟踪流程看下AddPacketsToNack函数的实现

void NackModule::AddPacketsToNack(uint16_t seq_num_start,uint16_t seq_num_end) {//LOG(LS_INFO) << "AddPacketsToNack. "//             << "start seq[" << seq_num_start//             << "],end seq[" << 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.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();LOG(LS_WARNING) << "NACK list full, clearing NACK"" list and requesting keyframe.";//触发关键帧请求keyframe_request_sender_->RequestKeyFrameNack();return;}}for (uint16_t seq_num = seq_num_start; seq_num != seq_num_end; ++seq_num) {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;}//LOG(LS_INFO) << "nack_list size[" << nack_list_.size() << "]";
}

我们可以看到AddPacketsToNack()函数主要实现了:
nack_list 的最大容量为 kMaxNackPackets = 1000, 如果满了会删除最后一个 KeyFrame 之前的所有nacked 序号, 如果删除之后还是满的那么清空 nack_list 并请求KeyFrame。

我们继续跟踪流程,我们看下GetNackBatch函数实现

std::vector<uint16_t> NackModule::GetNackBatch(NackFilterOptions options) {bool consider_seq_num = options != kTimeOnly;bool consider_timestamp = options != kSeqNumOnly;int64_t now_ms = clock_->TimeInMilliseconds();std::vector<uint16_t> nack_batch;auto it = nack_list_.begin();//LOG(LS_INFO) << "nack_list size[" << nack_list_.size() << "]";while (it != nack_list_.end()) {bool delay_timed_out =now_ms - it->second.created_at_time >= kDefaultSendNackDelayMs;//只考虑时间模式//当前序号是第一次发送(本地记录的send_at_time == -1)//当前最新收到的包序号在这个需要发送NAKC的序号的后面(避免当前还在收之前没收到的包)
//比如当前最新收到100, 当前检测是否需要发送NACK的序号为小于等于100的才满足条件, 比如 99if (delay_timed_out && consider_seq_num && it->second.sent_at_time == -1 &&AheadOrAt(last_seq_num_, it->second.send_at_seq_num)) {nack_batch.emplace_back(it->second.seq_num);++it->second.retries;it->second.sent_at_time = now_ms;//从nack_list 中取出需要发送 NACK 的序号列表, 如果某个 seq 请求次数超过 kMaxNackRetries = 10次则会从nack_list 中删除if (it->second.retries >= kMaxNackRetries) {LOG(LS_WARNING) << "Sequence number " << it->second.seq_num<< " removed from NACK list due to max retries.";//从nack_list_列表中移除it = nack_list_.erase(it);} else {++it;}continue;}//只考虑时间模式//发送nack的条件变成,该序号上次发送NACK的时间到当前时间要超过1个RTT(该序号一次也没发送过NACK(send_at_time == -1)也满足if (delay_timed_out && consider_timestamp && it->second.sent_at_time + rtt_ms_ <= now_ms) {nack_batch.emplace_back(it->second.seq_num);++it->second.retries;it->second.sent_at_time = now_ms;if (it->second.retries >= kMaxNackRetries) {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;
}

从上面GetNackBatch函数我们可以知道,获取nack_list存在2种控制逻辑。

3.WebRTC中处理NACK请求的实现

  1. 首先是正常的 RTCP 处理流程: RTCPReceiver 中解析处理RTCP, 在 rtcp_receiver.cc中的TriggerCallbacksFromRtcpPacket函数中处理不同的RTCP消息.
  2. 如果nackSequenceNumbers.size大于0,则触发 RtpRtcp 对象的ModuleRtpRtcpImpl::OnReceivedNack 处理流程。

我们看下OnReceivedNACK/rtp_rtcp_imp.cc函数

void ModuleRtpRtcpImpl::OnReceivedNACK(int64_t id, const std::list<uint16_t>& nack_sequence_numbers) {//将丢包的序号 记录到PacketLossStats, 获取RTT后进入 RTPSedner.OnReceivedNack.for (uint16_t nack_sequence_number : nack_sequence_numbers) {send_loss_stats_.AddLostPacket(nack_sequence_number);}if (!rtp_sender_.StorePackets() ||nack_sequence_numbers.size() == 0) {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);}rtp_sender_.OnReceivedNACK(id, nack_sequence_numbers, rtt);
}

我们继续跟踪流程,看下rtp_sender.cc下OnReceivedNACK()函数


void RTPSender::OnReceivedNACK(int64_t id,const std::list<uint16_t>& nack_sequence_numbers,int64_t avg_rtt) {TRACE_EVENT2(TRACE_DISABLED_BY_DEFAULT("webrtc_rtp"),"RTPSender::OnReceivedNACK", "num_seqnum",nack_sequence_numbers.size(), "avg_rtt", avg_rtt);const int64_t now = clock_->TimeInMilliseconds();uint32_t bytes_re_sent = 0;uint32_t target_bitrate = GetTargetBitrate();//比特率限制检查// Enough bandwidth to send NACK?if (!ProcessNACKBitRate(now)) {LOG(LS_INFO) << "NACK bitrate reached. Skip sending NACK response. Target "<< target_bitrate;return;}for (std::list<uint16_t>::const_iterator it = nack_sequence_numbers.begin();it != nack_sequence_numbers.end(); ++it) {const int32_t bytes_sent = ReSendPacket(id, *it, 5 + avg_rtt);if (bytes_sent > 0) {bytes_re_sent += bytes_sent;} else if (bytes_sent == 0) {// The packet has previously been resent.// Try resending next packet in the list.continue;} else {// Failed to send one Sequence number. Give up the rest in this nack.LOG(LS_WARNING) << "Failed resending RTP packet " << *it<< ", Discard rest of packets";break;}// Delay bandwidth estimate (RTT * BW).if (target_bitrate != 0 && avg_rtt) {// kbits/s * ms = bits => bits/8 = bytessize_t target_bytes =(static_cast<size_t>(target_bitrate / 1000) * avg_rtt) >> 3;if (bytes_re_sent > target_bytes) {break;  // Ignore the rest of the packets in the list.}}}if (bytes_re_sent > 0) {UpdateNACKBitRate(bytes_re_sent, now);}
}

我们继续看下ReSendPacket()函数重新发送数据的实现

int32_t RTPSender::ReSendPacket(int64_t id, uint16_t packet_id, int64_t min_resend_time) {size_t length = IP_PACKET_SIZE;uint8_t data_buffer[IP_PACKET_SIZE];int64_t capture_time_ms;
//从缓存包中去获取数据包if (!packet_history_.GetPacketAndSetSendTime(packet_id, min_resend_time, true,data_buffer, &length,&capture_time_ms)) {// Packet not found.LOG(LS_INFO) << "ReSendPacket not found.seq[" << packet_id << "].";return 0;}
//如果开启平滑发送的话if (paced_sender_) {RtpUtility::RtpHeaderParser rtp_parser(data_buffer, length);RTPHeader header;if (!rtp_parser.Parse(&header)) {assert(false);return -1;}// Convert from TickTime to Clock since capture_time_ms is based on// TickTime.int64_t corrected_capture_tims_ms = capture_time_ms + clock_delta_ms_;paced_sender_->InsertPacket(id, RtpPacketSender::kNormalPriority, header.ssrc, header.sequenceNumber,corrected_capture_tims_ms, length - header.headerLength, true);return length;}int rtx = kRtxOff;{rtc::CritScope lock(&send_critsect_);rtx = rtx_;}//重新发送数据if (!PrepareAndSendPacket(id, data_buffer, length, capture_time_ms,(rtx & kRtxRetransmitted) > 0, true)) {return -1;}return static_cast<int32_t>(length);
}

3.通过上面我们可以知道,RTPSender中完成在PacketHistory中查找需要发送的RTP seq, 并决定重发时间. 重发也需要经过重发的比特率限制的检查. RTPSedner 初始化话时可以配置是否使用(PacedSend, 均匀发送), 最后检查重发格式(RtxStatus() 可以获取是否使用 RTX 封装)后使用 RTPSedner::PrepareAndSendPacket进行立即重发. 如果是使用 PacedSend, 则使用 PacedSender::InsertPacket 先加入发送列表中, 它的process会定时处理发送任务.

WebRTC-nack机制详解相关推荐

  1. PHP autoload机制详解

    PHP autoload机制详解 转载自 jeakccc PHP autoload机制详解 (1) autoload机制概述 在使用PHP的OO模式开发系统时,通常大家习惯上将每个类的实现都存放在一个 ...

  2. 模糊匹配 读音_onenote搜索机制详解②:两种搜索模式,模糊与精确匹配

    先从纯文本搜索讲起,这是最基本也是最重要的. 从这篇开始,以及接下来连续几篇文章,都会介绍搜索的基础功能.注意,这几篇文章中谈论的都是基本的.正常的搜索功能,暂时不考虑Bug等因素. 在很多软件(例如 ...

  3. Java类加载机制详解【java面试题】

    Java类加载机制详解[java面试题] (1)问题分析: Class文件由类装载器装载后,在JVM中将形成一份描述Class结构的元信息对象,通过该元信息对象可以获知Class的结构信息:如构造函数 ...

  4. Numpy的广播机制详解(broadcasting)

    Numpy的广播机制详解(broadcasting) 广播(Broadcast)是 numpy 对不同形状(shape)的数组进行数值计算的方式, 对数组的算术运算通常在相应的元素上进行. 如果两个数 ...

  5. Session机制详解及分布式中Session共享解决方案

    Session机制详解及分布式中Session共享解决方案 参考文章: (1)Session机制详解及分布式中Session共享解决方案 (2)https://www.cnblogs.com/jing ...

  6. java异常处理机制详解

    java异常处理机制详解 参考文章: (1)java异常处理机制详解 (2)https://www.cnblogs.com/vaejava/articles/6668809.html 备忘一下.

  7. SpringMVC异常处理机制详解[附带源码分析]

    SpringMVC异常处理机制详解[附带源码分析] 参考文章: (1)SpringMVC异常处理机制详解[附带源码分析] (2)https://www.cnblogs.com/fangjian0423 ...

  8. 动态代理机制详解(JDK 和CGLIB,Javassist,ASM)

    2019独角兽企业重金招聘Python工程师标准>>> 在运行时期可以按照Java虚拟机规范对class文件的组织规则生成对应的二进制字节码.当前有很多开源框架可以完成这些功能,如A ...

  9. JAVA之JVM垃圾回收(GC)机制详解

    一.为什么需要垃圾回收 如果不进行垃圾回收,内存迟早都会被消耗空,因为我们在不断的分配内存空间而不进行回收.除非内存无限大,我们可以任性的分配而不回收,但是事实并非如此.所以,垃圾回收是必须的. 二. ...

  10. java的动态代理机制详解

    2019独角兽企业重金招聘Python工程师标准>>> 参考资料 1.java的动态代理机制详解 转载于:https://my.oschina.net/Howard2016/blog ...

最新文章

  1. centos防火墙端口配置
  2. html container显示边框,伪元素实现边框设置.html
  3. 备忘:C语言void *
  4. mysql join on 索引_连接查询,表关联查询join on,索引,触发器,视图
  5. np.meshgrid
  6. 执行ldapadd 命令时报错:ldap_bind: Invalid credentials (49)
  7. ssl提高组国庆模拟赛【2018.10.7】
  8. 高密自智,体小量大,希捷Exos Corvault存储系统为数据洞察赋能
  9. 4怎么放大字体_win8.1系统如何放大所有字体?
  10. Linux上安装Hadoop集群(CentOS7+hadoop-2.8.3)
  11. 【肌电信号】基于matlab GUI脉搏信号脉率存档【含Matlab源码 237期】
  12. Android--Menus
  13. 说说百度iOS人脸识别的痛
  14. AWGN信道条件下,基于16QAM+OFDM的误码率计算,并与理论值对比
  15. linux va start,linux下strftime(),va_start(),va_end()函数的用法
  16. Python基础知识笔记(三)——字典、集合
  17. Unity3d八 Unity使用的坐标系
  18. 阿里腾讯神仙打架之要命DNS(草泥马飘过)
  19. Codeforces Round #606 (Div. 2) A ~ C
  20. 2012年CCS云计算高峰论坛

热门文章

  1. ResNet(残差网络)之残差模块
  2. endnote如何设置文献样式
  3. 陆辰是一名初级药剂师,16西药执业药师一次过17中药一次过 考中级药师#医学生
  4. vue + echarts 省份地图 以及打包后地图加载不出来(比较详细)
  5. sha1 file ‘<stdout>‘ write error: Broken pipe
  6. 我国iPS细胞事业支援促进委员会成立
  7. CAD命令输入、结束、重复与撤销
  8. mysql系统表存放表结构_mysql数据库表结构
  9. Linux之ClamAV杀毒软件YUM安装和使用
  10. 综述论文要写英文摘要吗_攻略|一些小论文撰写的建议