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 拥塞控制:别让重传成为网络杀手
重传是把双刃剑。用好了能恢复丢包,用不好会加剧拥塞。
拥塞控制的核心是:重传包也要算入带宽预算。
我见过太多团队,重传走单独的通道,不参与码率控制。结果网络一丢包,重传和正常数据抢带宽,丢包率直线上升,最后整个会话崩掉。
正确的做法是:
- 统一带宽池:重传包和正常包共享同一个带宽预算。
- 动态降级:当重传比例超过20%时,主动降低视频编码码率。
- 重传优先级:关键帧重传优先级最高,非参考帧最低。
我曾经在一个项目中,把重传和编码码率做了联动控制:
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不是万能的,但用好了能解决80%的丢包问题。关键是把握好三个维度:反馈及时性、重传选择性、带宽友好性。我个人的经验是,先把NACK间隔和RTO调好,再逐步加入选择性重传和拥塞控制,这样调试起来更可控。