WebRTC服务质量(08)- 重传机制(05) RTX机制
一、前言:
RTX协议(Retransmission,即重传协议)是 WebRTC 中用于处理丢包恢复的一部分。由于网络通信中的丢包不可避免,WebRTC RTP协议栈支持多种丢包恢复机制,其中之一便是通过RTX协议实现的重传机制。
RTX协议会根据 RTP 丢包检测机制(具体由 RTCP 的 NACK 包触发),将数据包重传给接收端。RTX 的核心思想是发送端将丢失的数据包重新封装并发送,而接收端根据识别重新封装的数据包进行补偿处理,从而还原完整的流媒体内容。
二、RTX协议的工作原理:
RTX 的工作紧密依赖于 RTP 和 RTCP 协议。以下是 RTX 的通信流程:
- 发送端发送数据包: 发送端按照 RTP 协议发送音视频流,每个 RTP 数据包都有唯一的序列号(
sequence number
),用于接收端检测是否丢包。 - 接收端检测丢包: 接收端通过接收 RTP 包的序列号,以及 RTP/RTCP 的统计信息,检测数据是否丢失。一旦丢包,接收端会发送 RTCP NACK(Negative Acknowledgment)反馈包给发送端,并指定丢失数据包的序列号。
- 发送端重新发送丢失的数据包: 发送端根据接收到的 RTCP NACK 请求,使用 RTX 流重传对应的丢失数据包。RTX 流不同于主流(primary stream),通常会有独立的 SSRC(Synchronization Source Identifier)用于区分两者。
- 接收端合并重传包: 接收端接收到 RTX 包后,根据 RTX 的特殊封装格式提取出原始 Payload(数据载荷)并通过序列号将其合并到主流中。
RTX 的核心点在于:
- 独立的 SSRC:RTX 使用独立的 SSRC,和主流区分开,这样可以识别重传流。
- 修改过的 RTP 包头:RTX 包在 RTP 层修改了一些元信息,用于兼容重传流和主流的处理。
- RTX包有自己的payload type
- RTX包是按照自己的Sequence Number进行排序的。
三、如何找到RTX包:
- 找到Offer/Answer这种SDP信息。
- 从SDP中找到RTX的SSRC。
- 然后抓包,根据SSRC过滤出RTX包。
四、RTX协议格式:
- RTP Header:和前面介绍协议时候的RTP Header是一样的;
- OSN:重传的是哪个原始数据的序列包,注意我们RTX属于RTP协议,这儿的Original指的是RTP包;
- 可以看出RTX就是普通的RTP协议加了2字节的OSN,注意不是RTCP;
五、抓取RTX包:
5.1、获取本机的RTX流:
看看RTX的seq:
seq独立可以更好的统计出丢包。
5.2、看看原始流哪些包丢失了:
BLP是0x0001。知道BLP是什么吗?
1)NACK PID和NACK BLP:
1)概念:
在 NACK 中,NACK PID 和 NACK BLP 是两个关键的字段,用于标识和管理需要重传的数据包。
- NACK PID:NACK PID(Packet ID)是 NACK 报文中的一个字段,用于指示需要重传的丢失数据包的序列号。当接收端检测到数据包丢失时,会发送 NACK 报文,其中 NACK PID 指示了具体丢失的数据包的序列号。
- NACK BLP:NACK BLP(Bitmask of Lost Packets)是 NACK 报文中的另一个字段,用于指示一连串连续丢失的数据包。NACK BLP 是一个比特掩码,每个比特位对应一个数据包序列号,用于表示一段连续的丢失数据包范围。
2)举个例子:
假设在一个 WebRTC 实时通信会话中,发送端发送了一系列 RTP 数据包给接收端,序列号分别为 10、11、12、13、14、15。接收端在接收过程中发现数据包 11 和 13 丢失了。接收端会发送 NACK 报文给发送端,请求重传这两个丢失的数据包。
在这个例子中,NACK 报文中的 NACK PID 和 NACK BLP 可能会被设置如下:
- NACK PID:11, 13
- NACK PID 指示需要重传的具体丢失数据包的序列号,即数据包 11 和数据包 13。
- NACK BLP:010010
- 在这个二进制掩码中,每个比特位对应一个数据包序列号。接收端标记了丢失的数据包范围,从 10 到 15 中的第 2 和第 4 个数据包丢失。
- 这表示数据包 11 和数据包 13 丢失,而其他数据包正常接收。
3)本文丢包情况:
NACK 报文中 NACK PID 的值为 6806,而 NACK BLP 的值为 0x0001,可以解释如下:
- NACK PID:6806
- NACK PID 表示需要重传的具体丢失数据包的序号,即数据包序号为 6806 的数据包需要进行重传。
- NACK BLP:0x0001
- NACK BLP 是一个十六进制数,转换为二进制为 0000 0000 0000 0001。
- 在这个二进制掩码中,每个比特位对应一个数据包序号。由于只有一个比特位为 1,表示只有一个数据包丢失。
- 在这种情况下,第一个(最低位)的 1 表示序号为 6806 的数据包丢失,其它数据包接收正常。
5.3、使用RTX进行重传:
如果你在抓包软件中找不到RTX包,记得使用时间戳
来找。
因此发送的RTX一定在这个点之后;
- 可以看出872691小于收到NACK的时间点876269,肯定不是。
- 881999大于收到NACK的时间点,可能是。
- 通过前面协议我们知道RTX的前2个字节是OSN,表示原始数据的序列号,因此,我们将0x1a96转换为十进制看是多少;
看到没有,重传的额就是6806这个数据包。
六、发送RTX的过程:
我们得先看下NACK接收流程:
会来到这里:
void RTPSender::OnReceivedNack(
const std::vector<uint16_t>& nack_sequence_numbers,
int64_t avg_rtt) {
packet_history_->SetRtt(5 + avg_rtt);
// 遍历nack中每个需要重传的seq,进行重传
for (uint16_t seq_no : nack_sequence_numbers) {
// 发送RTX
const int32_t bytes_sent = ReSendPacket(seq_no);
if (bytes_sent < 0) {
// Failed to send one Sequence number. Give up the rest in this nack.
RTC_LOG(LS_WARNING) << "Failed resending RTP packet " << seq_no
<< ", Discard rest of packets.";
break;
}
}
}
看看具体怎么重传的:
int32_t RTPSender::ReSendPacket(uint16_t packet_id) {
absl::optional<RtpPacketHistory::PacketState> stored_packet =
packet_history_->GetPacketState(packet_id);
if (!stored_packet || stored_packet->pending_transmission) {
return 0;
}
const int32_t packet_size = static_cast<int32_t>(stored_packet->packet_size);
const bool rtx = (RtxStatus() & kRtxRetransmitted) > 0;
std::unique_ptr<RtpPacketToSend> packet =
packet_history_->GetPacketAndMarkAsPending(
packet_id, [&](const RtpPacketToSend& stored_packet) {
std::unique_ptr<RtpPacketToSend> retransmit_packet;
if (retransmission_rate_limiter_ &&
!retransmission_rate_limiter_->TryUseRate(packet_size)) {
return retransmit_packet;
}
if (rtx) {
retransmit_packet = BuildRtxPacket(stored_packet);
} else {
retransmit_packet =
std::make_unique<RtpPacketToSend>(stored_packet);
}
if (retransmit_packet) {
retransmit_packet->set_retransmitted_sequence_number(
stored_packet.SequenceNumber());
}
return retransmit_packet;
});
if (!packet) {
return -1;
}
packet->set_packet_type(RtpPacketMediaType::kRetransmission);
packet->set_fec_protect_packet(false);
std::vector<std::unique_ptr<RtpPacketToSend>> packets;
packets.emplace_back(std::move(packet));
paced_sender_->EnqueuePackets(std::move(packets));
return packet_size;
}
我们根据代码逻辑发现,并不一定要使用RTX:
- 如果启用了 RTX(Retransmission),则构造并发送独立的 RTX 包。
- 如果没有启用 RTX,则直接从包历史记录中取出丢失的原始 RTP 包并重传。
代码比较负责,关键部分展开说明下:
1)构造重传包:
-
匿名函数:
std::unique_ptr<RtpPacketToSend> packet = packet_history_->GetPacketAndMarkAsPending( packet_id, [&](const RtpPacketToSend& stored_packet) { std::unique_ptr<RtpPacketToSend> retransmit_packet; // 检查重传速率限制 if (retransmission_rate_limiter_ && !retransmission_rate_limiter_->TryUseRate(packet_size)) { return retransmit_packet; } if (rtx) { // 如果支持 RTX,构造一个 RTX 包 retransmit_packet = BuildRtxPacket(stored_packet); } else { // 否则直接使用原始 RTP 包 retransmit_packet = std::make_unique<RtpPacketToSend>(stored_packet); } if (retransmit_packet) { retransmit_packet->set_retransmitted_sequence_number( stored_packet.SequenceNumber()); } return retransmit_packet; });
这段代码的核心是获取丢失的历史包并将其打包为重传包:
- 调用
packet_history_->GetPacketAndMarkAsPending
:- 从历史记录中取出丢失的 RTP 包,并标记该包已进入待发送状态(防止重复重传)。
- 使用匿名函数对包进行加工处理:
- 重传速率限制:
- 如果当前重传速率超出了允许的带宽,则直接丢弃该重传包,不做进一步处理。
- RTX 构造:
- 如果启用了 RTX,通过调用
BuildRtxPacket
方法,将原始包封装为 RTX 包。 - 关键点:RTX 包的特殊结构包含原始包的
OSN
(Original Sequence Number),用于表示其原始 RTP 包的序列号。
- 如果启用了 RTX,通过调用
- 普通重传包:
- 如果没有启用 RTX,则直接复制原始 RTP 包来重发。
- 设置序列号:
- 调用
set_retransmitted_sequence_number
,设置重传包所对应的原始序列号。
- 调用
- 重传速率限制:
- 调用
2)重传包的最终处理:
if (!packet) {
return -1;
}
packet->set_packet_type(RtpPacketMediaType::kRetransmission);
packet->set_fec_protect_packet(false);
std::vector<std::unique_ptr<RtpPacketToSend>> packets;
packets.emplace_back(std::move(packet));
// 将重传包加入到 paced sender 的发送队列
paced_sender_->EnqueuePackets(std::move(packets));
- 检查是否成功获取重传包:
- 如果没有生成有效的重传包(可能被带宽限制丢弃),返回
-1
。
- 如果没有生成有效的重传包(可能被带宽限制丢弃),返回
- 设置包类型:
- 通过
set_packet_type
设置为kRetransmission
,标明这是一个重传包。 set_fec_protect_packet(false)
:通知不对重传包应用 FEC(前向纠错)保护,因为 RTX 本身是另一个冗余机制。
- 通过
- 插入发送队列:
- 将重传包添加到
paced_sender_
(节奏发送器)模块中,以有节奏地发送包,避免瞬时大量重传耗尽带宽。
- 将重传包添加到
3)小结:
- RTX 重传的实现流程:
- RTX 重传包通过函数
BuildRtxPacket
生成。 - RTX 中包含一个特殊的字段
OSN
,用于恢复原始包的序列号。 - 通过独立的 SSRC 和 Payload Type 区分 RTX 包与普通主流包。
- RTX 重传包通过函数
- 带宽管理和速率控制:
retransmission_rate_limiter_
限制了重传包的发送速率,防止网络因重传过载。
- 灵活处理重传策略:
- 支持两种模式:RTX 和普通 RTP 重传。
- RTX 是更成熟的丢包恢复机制,但需要双方支持;否则退回到简单的 RTP 重传方式。
七、总结:
RTPSender::ReSendPacket
函数是 WebRTC 中 RTP 协议层实现丢包恢复的重要部分。它利用 RtpPacketHistory
记录和 RTX 协议实现高效的重传机制,同时考虑到带宽管理、速率限制和两种重传策略(RTX 和普通 RTP)的兼容性。在具体代码实现中,它通过灵活的封装和严格的速率控制,保证了无损恢复的同时避免了网络拥塞情况。