【网络通信 -- WebRTC】MediaSoup 源码分析 -- NACK 机制
【1】MediaSoup -- NackGenerator 类的结构图示
【2】关键代码分析
【2.1】NackGenerator::ReceivePacket -- 接收到 RTP 数据时的处理逻辑
- 调用时机
- 在 RtpStreamRecv::ReceivePacket 函数中,处理重传的 RTP 数据包时调用
- 在 RtpStreamRecv::ReceivePacket 函数中,处理 RTP 数据包时调用
// Returns true if this is a found nacked packet. False otherwise.
// NACK 生成器处理 RTP 数据包
// 接收到 RTP 数据包后, NACK 生成器对 RTP 数据包的处理逻辑
bool NackGenerator::ReceivePacket(RTC::RtpPacket* packet, bool isRecovered)
{
MS_TRACE();
// 获取 RTP 数据包的序列号
uint16_t seq = packet->GetSequenceNumber();
// 判断 RTP 数据包是否为关键帧
bool isKeyFrame = packet->IsKeyFrame();
// 处理第一个数据包
if (!this->started)
{
this->started = true;
this->lastSeq = seq;
if (isKeyFrame)
// 记录关键帧对应的序列号
// keyFrameList 中记录关键帧序列号用于清除 nack list 中过多的数据包
this->keyFrameList.insert(seq);
return false;
}
// Obviously never nacked, so ignore.
// 此处逻辑为处理非首次的 RTP 数据包
// 当前数据包的序列号与最近一次的序列号相同表明数据包重复
// 不存在丢包重传需求
if (seq == this->lastSeq)
return false;
// May be an out of order packet, or already handled retransmitted packet,
// or a retransmitted packet.
//
// 处理序列号乱序,数据包重传的情况
// 可能是响应 NACK 反馈数据包发送的对应序列号的 RTP 数据包
if (SeqManager<uint16_t>::IsSeqLowerThan(seq, this->lastSeq))
{
// 检查 nackList
auto it = this->nackList.find(seq);
// It was a nacked packet.
// 说明 NACK 请求重传的数据包已经到达
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.
//
// 运行到此处表明当前收到的数据包并非 NACK 重传请求的响应
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.
//
// 运行到此表明当前收到的数据包序列号比最近的数据包序列号大,可能存在丢包现象
if (isKeyFrame)
this->keyFrameList.insert(seq);
// Remove old keyframes.
{
// keyFrameList 中仅仅保留 MaxPacketAge 个数据包
auto it = this->keyFrameList.lower_bound(seq - MaxPacketAge);
if (it != this->keyFrameList.begin())
this->keyFrameList.erase(this->keyFrameList.begin(), it);
}
if (isRecovered)
{
// 恢复重传集合中添加当前 RTP 数据包的序列号
// recoveredList 中保存的序列号对应的数据包不需要做 NACK 保证
this->recoveredList.insert(seq);
// Remove old ones so we don't accumulate recovered packets.
// recoveredList 中仅保留 MaxPacketAge 个数据包
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;
}
// 添加 (this->lastSeq + 1, seq) 区间的数据包到 NackList 中
// 表明需要等待 (this->lastSeq + 1, seq) 区间中的数据包到达
AddPacketsToNackList(this->lastSeq + 1, seq);
this->lastSeq = seq;
// Check if there are any nacks that are waiting for this seq number.
// 检查是否存在等待当前序列号的 NACK
// 检查当前数据包序列号之前有没有丢包
std::vector<uint16_t> nackBatch = GetNackBatch(NackFilter::SEQ);
if (!nackBatch.empty())
// 回调 RtpStreamRecv 实例的 OnNackGeneratorNackRequired 接口函数发送 NACK RTCP 数据包
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.
//
// 启动定时器,定期检查 NackList 列表
if (!this->timer->IsActive())
MayRunTimer();
return false;
}
注意点
- NACK 列表中数据包的数量控制
- 在 NackGenerator::AddPacketsToNackList 函数中添加数据包到 NACK 列表时,会结合 RemoveNackItemsUntilKeyFrame 函数,删除 NACK 列表中直到关键帧序列号的过旧的数据包,从而保证 NACK 列表中数据包数量的稳定;
- 在期间可能会进行请求关键帧的处理;
【2.2】NackGenerator::OnTimer -- 定时检查 NACK 列表时的处理逻辑
// 定时器回调函数,处理定时任务
inline void NackGenerator::OnTimer(Timer* /*timer*/)
{
MS_TRACE();
// 获取需要发送 NACK 反馈的序列号
std::vector<uint16_t> nackBatch = GetNackBatch(NackFilter::TIME);
if (!nackBatch.empty())
// 回调 RtpStreamRecv 实例的 OnNackGeneratorNackRequired 方法发送 NACK 的 RTCP 数据包
this->listener->OnNackGeneratorNackRequired(nackBatch);
// 判断是否启动定时器实例
MayRunTimer();
}
参考致谢
本博客为博主的学习实践总结,并参考了众多博主的博文,在此表示感谢,博主若有不足之处,请批评指正。