4、基于ARQ的丢包重传机制:RTCP NACK反馈、选择性重传、重传超时与拥塞控制

ARQ,说白了就是“丢了再发一次”。

在H265实时传输里,网络丢包是家常便饭。你想想看,一个I帧丢了,可能整个画面都要花屏好几秒。这时候,ARQ(自动重传请求)就成了最直接的救命稻草。我做了这么多年流媒体,ARQ是我用得最多、也最依赖的恢复手段。

4.1 RTCP NACK反馈:告诉发送端“我丢了哪个包”

接收端怎么告诉发送端丢了包?靠的就是RTCP NACK。

NACK全称是Negative Acknowledgment,就是“我没收到”的意思。它不像ACK那样每个包都确认,而是只报告丢失的包。这样能大幅减少反馈开销。

核心要点:NACK包携带的是丢包的序列号(Sequence Number)。发送端收到后,就知道该重传哪个RTP包。

我记得有一次做直播项目,接收端反馈延迟特别高。查了半天,发现是NACK发送间隔配置得太保守了。默认是100ms发一次,但网络抖动时,丢包集中爆发,100ms才反馈一次,黄花菜都凉了。

RTCP NACK的格式其实很简单:

// RTCP NACK Payload (RFC 4585)
// PID: 丢失的第一个包序列号
// BLP: 位掩码,表示后续16个包的丢失情况

struct RTCP_NACK {
    uint16_t pid;      // Packet ID
    uint16_t blp;      // Bitmask of Lost Packets
};

BLP这个字段很巧妙。它用16位掩码表示PID之后的16个包是否丢失。1表示丢,0表示没丢。这样一次NACK最多能报告17个丢包(PID + 16个掩码位)。

实战建议:如果丢包率超过5%,建议把NACK间隔缩短到30-50ms。但别低于20ms,否则反馈风暴会把发送端压垮。

4.2 选择性重传:只重传真正需要的包

发送端收到NACK后,是不是所有丢包都要重传?当然不是。

选择性重传(Selective Retransmission)的核心思想是:只重传那些对解码有用的包。

举个例子:

  • 如果丢的是一个P帧的参考包,那必须重传,否则后续帧全废。
  • 如果丢的是一个B帧的非参考包,那可以跳过,因为解码器能靠前后帧插值。
  • 如果丢包已经超时(比如超过了播放缓冲区的等待时间),那重传也没意义了。

我曾经在一个低延迟会议系统中踩过这个坑。当时不管三七二十一,收到NACK就重传。结果网络本来就差,重传包又把带宽占满了,导致更多丢包。这就是典型的“重传风暴”。

选择性重传的判断逻辑,我一般这样写:

bool should_retransmit(NackInfo& nack, StreamState& state) {
    // 1. 检查是否超时
    if (nack.timestamp + MAX_RETRANSMIT_DELAY < now()) {
        return false;  // 太晚了,放弃
    }
    
    // 2. 检查包类型
    RtpPacket* pkt = get_packet_from_history(nack.sequence_number);
    if (!pkt) return false;
    
    // 3. 只重传关键帧或参考帧
    if (pkt->is_keyframe() || pkt->is_reference()) {
        return true;
    }
    
    // 4. 非参考帧,且丢包率高于阈值,放弃重传
    if (state.loss_rate > 0.1f) {
        return false;
    }
    
    return true;
}

注意:选择性重传不能太激进。如果频繁丢弃非参考帧,画面会出现局部马赛克。我建议保留一个“最低质量保障”策略:即使是非参考帧,如果连续丢包超过3个,也强制重传一次。

4.3 重传超时:什么时候该放弃?

重传不是无限等的。每个丢包都有一个“生存时间”。超过了这个时间,就算重传回来,解码器也用不上了。

重传超时(Retransmit Timeout, RTO)的计算,我习惯用这个公式:

RTO = RTT + 4 * RTT_var + 处理延迟

其中:

  • RTT:往返时延,通过RTCP SR/RR计算
  • RTT_var:RTT的方差,反映网络抖动
  • 处理延迟:编码、解码、缓冲的固定开销

RTO设得太短,重传还没到就放弃了,浪费带宽。设得太长,播放缓冲区会卡顿。我一般把RTO控制在1.5倍RTT到3倍RTT之间。

网络条件 RTT (ms) 推荐RTO (ms) 最大重传次数
局域网 1-5 10-20 3
城域网 10-30 50-100 2
互联网(国内) 30-80 100-250 2
互联网(跨国) 100-300 300-900 1

经验之谈:跨国传输时,我建议最多重传1次。因为RTT已经很高了,重传2次以上,延迟会突破500ms,用户能明显感觉到卡顿。

4.4 拥塞控制:别让重传成为网络杀手

重传是把双刃剑。用好了能恢复丢包,用不好会加剧拥塞。

拥塞控制的核心是:重传包也要算入带宽预算。

我见过太多团队,重传走单独的通道,不参与码率控制。结果网络一丢包,重传和正常数据抢带宽,丢包率直线上升,最后整个会话崩掉。

正确的做法是:

  1. 统一带宽池:重传包和正常包共享同一个带宽预算。
  2. 动态降级:当重传比例超过20%时,主动降低视频编码码率。
  3. 重传优先级:关键帧重传优先级最高,非参考帧最低。

我曾经在一个项目中,把重传和编码码率做了联动控制:

void adjust_bitrate_for_retransmit(float retransmit_ratio) {
    float target_bitrate = current_bitrate;
    
    if (retransmit_ratio > 0.3f) {
        // 重传占比超过30%,紧急降码率
        target_bitrate *= 0.7f;
    } else if (retransmit_ratio > 0.15f) {
        // 超过15%,温和降码率
        target_bitrate *= 0.9f;
    }
    
    // 确保码率不低于最低阈值
    target_bitrate = max(target_bitrate, MIN_BITRATE);
    
    encoder->set_target_bitrate(target_bitrate);
}

警告:不要用重传比例作为拥塞控制的唯一指标。结合RTT趋势和丢包率一起判断。如果RTT持续上升但丢包率不高,可能是网络缓冲区膨胀,这时候降码率比降重传更有效。

4.5 整体流程:一张图看懂ARQ重传

下面这张图,是我自己总结的ARQ重传完整流程。从接收端检测丢包,到发送端选择性重传,再到拥塞控制介入,每一步都标清楚了。

ARQ重传完整流程图 接收端检测丢包 生成RTCP NACK 发送端接收NACK 选择性重传判断 不重传 重传 重传超时检查 (RTO) 拥塞控制:带宽预算检查 发送重传包 / 放弃 重传次数 > 最大限制? 直接放弃

这张图里,我特别想强调两个关键点:

  • 选择性重传和重传超时是并行的判断。先决定要不要重传,再决定能不能在超时前到达。
  • 拥塞控制是最后一道防线。即使前面都通过了,带宽不够也得放弃。

总结:ARQ不是万能的,但用好了能解决80%的丢包问题。关键是把握好三个维度:反馈及时性、重传选择性、带宽友好性。我个人的经验是,先把NACK间隔和RTO调好,再逐步加入选择性重传和拥塞控制,这样调试起来更可控。