【网络通信 -- WebRTC】MediaSoup 源码分析 -- NACK 机制

【网络通信 -- 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();
}

参考致谢

本博客为博主的学习实践总结,并参考了众多博主的博文,在此表示感谢,博主若有不足之处,请批评指正。

【1】mediasoup 源码分析(十三) 丢包重传 webrtc nack 机制

【2】mediasoup中nack的调用机制

【3】【网络通信 -- 直播】网络通信协议简介 -- RTCP 协议的 WebRTC 扩展

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值