大家好,欢迎来到IT知识分享网。
流媒体弱网优化之路(NACK)——纯NACK方案的优化探索
—— 我正在的github给大家开发一个用于做实验的项目 —— github.com/qw/Bifrost 目标:可以让大家熟悉各类Qos能力、带宽估计能力,提供每个环节关键参数调节接口并实现一个json全配置,提供全面的可视化算法观察能力。 欢迎大家使用 ——
文章目录
近期在针对纯丢包场景做一些简单的探索实验,一个让我困扰的问题就是:不使用FEC而只是通过纯NACK进行补偿,到底能在实验环境下抵抗多少的丢包呢?每一次丢包补偿的最大时长是多久呢(一个包在发生丢包状态下补回的时长)?就此问题我展开了一系列的分析和简单的实验demo,demo放在我的github上。
一、NACK简介
ACK是到达通知技术。以TCP为例,他可靠因为接收方在收到数据后会给发送方返回一个“已收到数据”的消息(ACK),告诉发送方“我已经收到了”,确保消息的可靠。
NACK也是一种通知技术,只是触发通知的条件刚好的ACK相反,在未收到消息时,通知发送方“我未收到消息”,即通知未达。
二、Nack的理解
2.1 一种补救策略
nack是常用的丢包补救策略。作为最简单、最常用的补救方式,已经被各大音视频厂商进行了大量的实验和研究。集合 zego、网易云信、百度云等厂商的实现/文章,我们将Nack策略作一些关键归纳。总结起来无非以下几点:什么时候要、要多少、怎么要?
2.2 什么时候要?
2.3 要多少?
2.4 怎么要?
三、计算分析
3.1 rtt强相关
上图的过程是一个假设在30%数据丢包率,40ms定时nack的状态下请求包的过程。
■ 时间为0ms的时候:sdk1发送20个数据包,sdk2因为丢包30%接到了14个;
■ 等待40ms定时器触发时:nack里的seq有6个;
■ 到60ms时:sdk1接到了sdk2的nack信令,立刻传出6个数据包;
■ 80ms时:sdk1再次发出16个数据包(10个正常数据+6个丢包数据),但实际到达sdk2的又少了5个——11个。通过计算可知,其中nack索要的包再次丢失的概率为11%。(向上取整的方式)
3.2 补包次数计算
3.3 Nack的定时发送
提示:此处讨论的是定时做nack重传的方式,而非丢失立即重传的方式。(该方式在webrtc中使用)
我们可知,nack定时器的时间40ms和RTT时间是一致的时候我们可以保证在3次重传下抵抗30%的丢包。线上一般rtt可能小于40ms,如果定时器继续维持40ms则有可能导致不能及时重传数据。因此在nack定时器的粒度上我们应该尽量维持较细的粒度,然后通过重传时间计算该次是否需要重传对应的seq数据。
3.4 nack重传限制
四、实验
/* * @author : dog head * @date : Created in 2022/3/2 1:52 下午 * @mail : * @project : receiver * @file : nack_gen.cpp * @description : TODO */ #include "nack_gen.h" #include "pack.h" #include "test_tp.h" namespace transportdemo {
/* Static. */ constexpr size_t MaxPacketAge{
10000u }; constexpr size_t MaxNackPackets{
1000u }; constexpr uint32_t DefaultRtt{
100u }; // constexpr uint8_t MaxNackRetries{ 10u }; // constexpr uint64_t TimerInterval{ 40u }; // =================== nack test =================== constexpr uint8_t MaxNackRetries{
20u }; constexpr uint64_t TimerInterval{
20u }; // =================== nack test =================== /* Instance methods. */ NackGenerator::NackGenerator() : rtt(DefaultRtt) {
} NackGenerator::~NackGenerator() {
} // Returns true if this is a found nacked packet. False otherwise. bool NackGenerator::ReceivePacket(TESTTPPacketPtr packet) {
TESTTPHeader* header = reinterpret_cast<TESTTPHeader *>(packet->mutable_buffer()); uint16_t seq = header->get_sequence(); if (!this->started) {
this->started = true; this->lastSeq = seq; return false; } // Obviously never nacked, so ignore. if (seq == this->lastSeq) return false; // May be an out of order packet, or already handled retransmitted packet, // or a retransmitted packet. if (SeqLowerThan(seq, this->lastSeq)) {
auto it = this->nackList.find(seq); // It was a nacked packet. if (it != this->nackList.end()) {
this->nackList.erase(it); return true; } return false; } AddPacketsToNackList(this->lastSeq + 1, seq); this->lastSeq = seq; return false; } void NackGenerator::AddPacketsToNackList(uint16_t seqStart, uint16_t seqEnd) {
// Remove old packets. auto it = this->nackList.lower_bound(seqEnd - MaxPacketAge); this->nackList.erase(this->nackList.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 numNewNacks = seqEnd - seqStart; if (this->nackList.size() + numNewNacks > MaxNackPackets) {
if (this->nackList.size() + numNewNacks > MaxNackPackets) {
this->nackList.clear(); return; } } for (uint16_t seq = seqStart; seq != seqEnd; ++seq) {
if (this->nackList.find(seq) == this->nackList.end()) this->nackList.emplace(std::make_pair(seq, NackInfo{
seq, seq, 0, 0 })); } } double CalculateRttLimit2SendNack(int tryTimes) {
return tryTimes < 3 ? (double)(tryTimes*0.4) + 1 : 2; } std::vector<uint16_t> NackGenerator::GetNackBatch() {
uint64_t nowMs = GetCurrentStamp64(); NackFilter filter = NackFilter::TIME; std::vector<uint16_t> nackBatch; auto it = this->nackList.begin(); while (it != this->nackList.end()) {
NackInfo& nackInfo = it->second; uint16_t seq = nackInfo.seq; auto limit_var = uint64_t( this->rtt / CalculateRttLimit2SendNack(nackInfo.retries) ); if (filter == NackFilter::TIME && nowMs - nackInfo.sentAtMs >= limit_var) {
// if (filter == NackFilter::TIME && nowMs - nackInfo.sentAtMs >= this->rtt) {
nackBatch.emplace_back(seq); nackInfo.retries++; auto oldMs = nackInfo.sentAtMs; if (oldMs == 0){
oldMs = nowMs; } nackInfo.sentAtMs = nowMs; std::cout << "retry seq:" << seq << ", times:" << unsigned(nackInfo.retries) << ", interval:" << nowMs-oldMs << std::endl; if (nackInfo.retries >= MaxNackRetries) {
it = this->nackList.erase(it); } else {
++it; } continue; } ++it; } return nackBatch; } void NackGenerator::Reset() {
this->nackList.clear(); this->started = false; this->lastSeq = 0u; } } // namespace transport-demo
4.1 实验参数:
这样设计的目的是为了缩短包补偿的间隔,降低卡顿率。
4.2 方案一结果:
如图中, 序号为:11741 —— 11496 中:约245个包。
4.3 方案二结果:
如图中, 序号为:1433 —— 1720 中:约287个包。
4.4 结果分析
五、总结
免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。 本文来自网络,若有侵权,请联系删除,如若转载,请注明出处:https://haidsoft.com/136917.html