由于webrtc所用的rtp协议底层是基于udp传输,所以并不能保证数据的可靠性。在发生丢包时,为了保证音视频的质量需要进行重传,而nack机制就是用来处理重传逻辑的,需要注意一点由于udp本身是无序的,所以在进行丢包重传时需要注意一下该包是否乱序,然后再决定是否要重传。
NACK 协议介绍
NACK属于反馈包的一种,具体格式如下:
6.1. Common Packet Format for Feedback Messages
All FB messages MUST use a common packet format that is depicted in
Figure 3:
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 | PT | length |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| SSRC of packet sender |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| SSRC of media source |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
: Feedback Control Information (FCI) :
: :
Figure 3: Common Packet Format for Feedback Messages
其中:
- PT = 205
- FMT = 1
- SSRC of packet sender :发送者ssrc,这个ssrc没有具体的用处,可以填一个默认值
- SSRC of media source :media ssrc,这个值非常重要,服务器就是根据该值判断用户类型的。
反馈包是订阅者发送给发布者,所以该值可以填发布者ssrc,但是有些服务器如licode,它会基于订阅者ssrc找到对应的发布者然后处理该反馈包,所以该值也可以填订阅者ssrc。我们需要明白一点就好,这个包是发给发布者的,服务器需要基于media source ssrc找到对应的发布者。 - FCI:具体的反馈信息
对于nack ,FCI内容如下,就是用下面的格式填充上文提到的FCI字段
The Feedback Control Information (FCI) field has the following Syntax
(Figure 4):
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
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| PID | BLP |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
Figure 4: Syntax for the Generic NACK message
字段内容:
- PID : 该NACK报文反馈的第一个丢失包的序列号,NACK 一次可以反馈16个包的丢失情况, 这16个包通过BLP字段表示
- BLP: bitmask of following lost packets (BLP): 16 bits 。BLP位掩码,表示继第一个PID表示的序号之后16个包的丢失情况,当丢失之后,对应的二进制位被置为1。例如:PID=100, 从101 到116这16个包,如果103,105丢失则BLP内容为:0010100000000000, 103和105对应的位被置为1,其它的位都为0。假如连续丢包超过16个,就需要一个新的FCI了
mediasoup对nack的处理
详情见:NackGenerator.cpp
关键结构:
struct NackInfo
{
uint16_t seq{ 0u };
uint16_t sendAtSeq{ 0u };
uint64_t sentAtMs{ 0u };
uint8_t retries{ 0u };
};
NackInfo:每个需要通过nack通知对方进行重传的包,会先封装为一个NackInfo结构体,记录该包的序列号、发送时间、重传次数
这些重传包会保存到nackList中
std::map<uint16_t, NackInfo, RTC::SeqManager<uint16_t>::SeqLowerThan> nackList;
mediasoup会启动一个定时器,每隔一个rtt时间,从nackList中获取需要进行重传的包通知对方,所有需要重传的包信息都封装为上文提到的BLP中。
关键帧列表
std::set<uint16_t, RTC::SeqManager<uint16_t>::SeqLowerThan> keyFrameList;
会记录组成关键帧的所有包,当nackList中缓存的包数过多或者缓存时间过长时,需要清除一些旧数据。每次清除时需要判断被清除的包是否是关键帧包,如果不是就清除,判断方式就是对比nackList和keyFrameList中序号是否相等,小于关键帧包序号的可以删除。
具体的源码如下:
// Returns true if this is a found nacked packet. False otherwise.
bool NackGenerator::ReceivePacket(RTC::RtpPacket* packet, bool isRecovered)
{
MS_TRACE();
uint16_t seq = packet->GetSequenceNumber();
bool isKeyFrame = packet->IsKeyFrame();
//收到第一个包,判断是否开始,如果没有开始,started置为true,表示开始
if (!this->started)
{
this->started = true;
this->lastSeq = seq;
//判断该包是否是关键帧包,是的话就放入到关键帧列表中
if (isKeyFrame)
this->keyFrameList.insert(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 (SeqManager<uint16_t>::IsSeqLowerThan(seq, this->lastSeq))
{
//首先判断nackList中是否有该包,如果有的话说明该包是nack之后的重传包,把它从
//nackList中删除
auto it = this->nackList.find(seq);
// It was a nacked packet.
if (it != this->nackList.end())
{
MS_DEBUG_DEV(
"NACKed packet received [ssrc:%" PRIu32 ", seq:%" PRIu16 ", recovered:%s]",
packet->GetSsrc(),
packet->GetSequenceNumber(),
isRecovered ? "true" : "false");
this->nackList.erase(it);
return true;
}
// Out of order packet or already handled NACKed packet.
if (!isRecovered)
{
MS_WARN_DEV(
"ignoring older packet not present in the NACK list [ssrc:%" PRIu32 ", seq:%" PRIu16 "]",
packet->GetSsrc(),
packet->GetSequenceNumber());
}
return false;
}
// If we are here it means that we may have lost some packets so seq is
// newer than the latest seq seen.
如果到这里说明上个包到当前包之间有丢包,需要进行nack处理
首先判断该包是否是关键帧包
if (isKeyFrame)
this->keyFrameList.insert(seq);
判断是否超过了关键帧最大缓存列表,超过的话,就需要清除旧的关键帧
// Remove old keyframes.
{
auto it = this->keyFrameList.lower_bound(seq - MaxPacketAge);
if (it != this->keyFrameList.begin())
this->keyFrameList.erase(this->keyFrameList.begin(), it);
}
重传的恢复包
if (isRecovered)
{
this->recoveredList.insert(seq);
// Remove old ones so we don't accumulate recovered packets.
auto it = this->recoveredList.lower_bound(seq - MaxPacketAge);
if (it != this->recoveredList.begin())
this->recoveredList.erase(this->recoveredList.begin(), it);
// Do not let a packet pass if it's newer than last seen seq and came via
// RTX.
return false;
}
上个包lastSeq到当前seq之间出现丢包,把这些丢弃的包放到nackList中
AddPacketsToNackList(this->lastSeq + 1, seq);
this->lastSeq = seq;
判断需要进行nack的包是否超过了最大重试次数,如果超过了就不再进行重传。(默认尝试10次)
// Check if there are any nacks that are waiting for this seq number.
std::vector<uint16_t> nackBatch = GetNackBatch(NackFilter::SEQ);
//封装nack,取16个包到BLP中
if (!nackBatch.empty())
this->listener->OnNackGeneratorNackRequired(nackBatch);
// This is important. Otherwise the running timer (filter:TIME) would be
// interrupted and NACKs would never been sent more than once for each seq.
//启动定时器,一个rtt发送一次
if (!this->timer->IsActive())
MayRunTimer();
return false;
}
详细内容大家可以看mediasoup中的NackGenerator.cpp文件。
作者:音视频之路
版权声明:本文内容转自互联网,本文观点仅代表作者本人。本站仅提供信息存储空间服务,所有权归原作者所有。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至1393616908@qq.com 举报,一经查实,本站将立刻删除。