深入浅出WebRTC—FEC
WebRTC 最新代码提供了 FlexFEC 实现,FlexFEC 是 UlpFEC 的一个扩展和升级,两者被纳入同一个实现框架,理解 UlpFEX 实现后,再去理解 FexFEC 会更加容易。本文主要分析 UlpFEC 的实现,重点关注逻辑和原理,也会涉及一些实现细节。由于相关细节实在太多,且有些细节实现特别复杂,加上没有文档说明,深入分析会耗费大量时间,因此只能有选择的分析,理解其大致意思,点到为止。
1. 静态结构
RtpVideoSender 作为发送端控制中心,其接收码率更新的通知,使用 FecController 生成 FEC 保护比率,并将 FEC 保护比率设置到 VideoFecGenerator,控制 FEC 保护码率大小。

UlpFEC 和 FlexFEC 是两种不同的 FEC 实现,复用相同的 FEC 处理框架,且通过 FlexfecSender 可以看到, FlexFEC 是 UlpFEC 的一个扩展。

2. 初始化
WebRTC 支持配置是否启用 FEC 和 NACK。在 RtpVideoSender 的构造函数中,会根据相关配置来决定是否创建 FEC 生成器,是创建 UlpfecGenerator 还是 FlexfecGenerator。

是否开启 FEC 和 NACK 需要设置到 FecControllerDefault。FecControllerDefault 用来生成 FEC 保护比率。正常情况,FEC 和 NACK 都会启用。
fec_controller_->SetProtectionMethod(fec_enabled, NackEnabled());
FecControllerDefault 根据设置的参数,确定当前可以使用什么保护方法,不同保护方法会有不同的保护策略。
void FecControllerDefault::SetProtectionMethod(bool enable_fec, bool enable_nack) {
media_optimization::VCMProtectionMethodEnum method(media_optimization::kNone);
if (enable_fec && enable_nack) {
method = media_optimization::kNackFec;
} else if (enable_nack) {
method = media_optimization::kNack;
} else if (enable_fec) {
method = media_optimization::kFec;
}
MutexLock lock(&mutex_);
loss_prot_logic_->SetMethod(method);
}
3. 参数计算
当估计带宽发生变化或丢包率发生变化时,需要重新计算生成多少比例的 FEC 码率来保护原始码率。如下图所示,码率分配器会通知 VideoSendStream 新的目标码率、丢包率、RTT等参数。此时,会触发 RtpVideoSender 重新计算编码码率和 FEC 比率。编码码率会设置到编码器,控制编码器的码率输出。FEC 比率会设置到 RTPSenderEgress,用来控制 FEC 报文的生成。

uint32_t VideoSendStreamImpl::OnBitrateUpdated(BitrateAllocationUpdate update) {
if (update.stable_target_bitrate.IsZero()) {
update.stable_target_bitrate = update.target_bitrate;
}
// 计算编码码率和保护码率(内部还会计算 FEC 比率)
rtp_video_sender_->OnBitrateUpdated(update, stats_proxy_->GetSendFrameRate());
// 获取编码码率
encoder_target_rate_bps_ = rtp_video_sender_->GetPayloadBitrateBps();
// 获取保护码率
const uint32_t protection_bitrate_bps =
rtp_video_sender_->GetProtectionBitrateBps();
...
// 更新编码器目标码率(包括其他一系列参数)
video_stream_encoder_->OnBitrateUpdated(
encoder_target_rate,
encoder_stable_target_rate,
link_allocation,
rtc::dchecked_cast<uint8_t>(update.packet_loss_ratio * 256),
update.round_trip_time.ms(),
update.cwnd_reduce_ratio);
return protection_bitrate_bps;
}
编码码率计算比较有意思,直接等于新目标码率减去基于历史数据统计的保护码率,代码如下所示。虽然这样计算出来的编码码率具有滞后性,但新的 FEC 比率已经设置下去,下一轮定时器驱动,会及时更新真实保护码率,从而继续调整编码码率。
uint32_t FecControllerDefault::UpdateFecRates(uint32_t estimated_bitrate_bps,
int actual_framerate_fps, uint8_t fraction_lost, std::vector<bool> loss_mask_vector,
int64_t round_trip_time_ms) {
...
// 设置 FEC 参数到 RtpSenderEgress,同时获取当前几个发送速率
protection_callback_->ProtectionRequest(
&delta_fec_params,
&key_fec_params,
&sent_video_rate_bps,
&sent_nack_rate_bps,
&sent_fec_rate_bps);
// 计算当前总发送速率
uint32_t sent_total_rate_bps =
sent_video_rate_bps + sent_nack_rate_bps + sent_fec_rate_bps;
// 保护开销(比率)保持不变
if (sent_total_rate_bps > 0) {
protection_overhead_rate =
static_cast<float>(sent_nack_rate_bps + sent_fec_rate_bps) / sent_total_rate_bps;
}
// 不超过 50%
protection_overhead_rate =
std::min(protection_overhead_rate, overhead_threshold_);
// 编码码率等于估计码率减去当前统计的保护码率
return estimated_bitrate_bps * (1.0 - protection_overhead_rate);
}
FEC 比率的计算非常复杂,但总体思路可以概括为两部分,第一个部分是基于码率和丢包率查表得到 FEC 比率。下表是基于 kFecRateTable 数组绘制的可视化表格。每一行代表一个码率,分 0 - 49 共 50 个级别,对应 30FPS 的码率范围从200kbps 到 8000kbps,当前码率需要转换为某个码率级别。每一列代表一个丢包率,分 0 - 128 共 129 个级别,对应丢包率从 0 到 50%,当前丢包率需要转换为某个丢包率级别。

查表得到 FEC 比率后,还需要根据 RTT 进行调整,代码如下。如果 RTT 很小,则关闭非关键帧的 FEC,优先使用 NACK,但关键帧还是继续使用 FEC 保护。
bool VCMNackFecMethod::ProtectionFactor(const VCMProtectionParameters* parameters) {
// 计算 FEC 比率,设置 _protectionFactorK 和 _protectionFactorD
VCMFecMethod::ProtectionFactor(parameters);
if (_lowRttNackMs == -1 || parameters->rtt < _lowRttNackMs) {
// 低 RTT 场景(RTT < 20ms),非关键帧不使用 FEC(保护因子为0)
_protectionFactorD = 0;
VCMFecMethod::UpdateProtectionFactorD(_protectionFactorD);
} else if (_highRttNackMs == -1 || parameters->rtt < _highRttNackMs) {
// 中等 RTT 场景
VCMFecMethod::UpdateProtectionFactorD(_protectionFactorD);
}
return true;
}
4. 生成 FEC 数据
4.1. 计算条件
在生成 FEC 报文之前,需要知道多少报文以及哪些报文作为输入。WebRTC 设计了几个约束:
1)最少报文数量
由 min_num_media_packets_ 决定,由 MinimumMediaPacketsReached() 做出限制。
2)帧边界限制
等到帧结束标志时才会计算 FEC。尽量保证对于一个完整的帧,其保护策略是一致的。
3)最大帧数量
这是一个保护条件,记录的帧数量超过设定值,不再管其他数量限制了,必须计算 FEC。
4)开销误差限制
FEC 比率决定 FEC 开销,但由于四舍五入等计算精度问题,使得 FEC 目标比率和 FEC 真实比率会有一定差异。当原始报文数量较多时,这个差异会比较小,原始报文较少时,这个差异可能会很大。假设 FEC 目标比率时 10%,原始报文数量是 10 个,目标 FEC 报文为 10 * 10% = 1 个,由于计算精度进行舍入,计算得到需要 FEC 报文为 2 个,这两者之间的差异达到 100% 了。
void UlpfecGenerator::AddPacketAndGenerateFec(const RtpPacketToSend& packet) {
{
MutexLock lock(&mutex_);
// 如果有等待更新的FEC参数,则更新当前参数并清除待更新标记。
if (pending_params_) {
current_params_ = *pending_params_;
pending_params_.reset();
// FEC 比率 > 31.4%(80/255),至少需要 4 个包才能计算 FEC
if (CurrentParams().fec_rate > kHighProtectionThreshold) {
min_num_media_packets_ = kMinMediaPackets;
} else { // 否则,允许至少1个媒体包参与FEC计算。
min_num_media_packets_ = 1;
}
}
}
// 记录当前分组中包含关键帧
if (packet.is_key_frame()) {
media_contains_keyframe_ = true;
}
// 读取 RTP 头的 marker 标志
const bool complete_frame = packet.Marker();
// 将用来计算 FEC 报文的原始报文缓存起来,ulpfec 的 mask 最多记录 48 个报文
if (media_packets_.size() < kUlpfecMaxMediaPackets) {
auto fec_packet = std::make_unique<ForwardErrorCorrection::Packet>();
fec_packet->data = packet.Buffer();
media_packets_.push_back(std::move(fec_packet));
last_media_packet_ = packet; // 用于复制 RTP 头
}
// 记录帧数量
if (complete_frame) {
++num_protected_frames_;
}
auto params = CurrentParams();
if (complete_frame &&
// 已经保护足够多的帧
(num_protected_frames_ >= params.max_fec_frames ||
// 实际开销与目标开销之差小于最大允许偏差,并且已经收集到足够数量的媒体包
(ExcessOverheadBelowMax() && MinimumMediaPacketsReached()))) {
constexpr int kNumImportantPackets = 0;
// 为什么不使用 unequal protection?
constexpr bool kUseUnequalProtection = false;
// FEC 编码
fec_->EncodeFec(media_packets_,
params.fec_rate,
kNumImportantPackets,
kUseUnequalProtection,
params.fec_mask_type,
&generated_fec_packets_);
}
}
4.2. 掩码生成
由于 WebRTC 的 FEC 采用的是 Charity Code,虽然有了 FEC 比率,但安排哪个 FEC 报文去保护哪几个原始报文,即如何确定 FEC 报文的掩码表,也是一个头疼的事情。
掩码的设置非常灵活,针对随机丢包和突发丢包,WebRTC 提前准备了两张掩码表:kPacketMaskRandomTbl和kPacketMaskBurstyTbl,多少原始报文,生成多少 FEC 报文,直接查表就能得到每个 FEC 报文的掩码,大大简化了掩码的生成过程,提高了程序处理效率。WebRTC 目前只使用 kPacketMaskRandomTbl 掩码表,如下所示。kPacketMaskRandomX 中 X 表示原始包文数量,可以看到此表最多覆盖 12 个原始报文。
const uint8_t kPacketMaskRandomTbl[] = {
12,
kPacketMaskRandom1, // 2 byte entries.
kPacketMaskRandom2,
kPacketMaskRandom3,
kPacketMaskRandom4,
kPacketMaskRandom5,
kPacketMaskRandom6,
kPacketMaskRandom7,
kPacketMaskRandom8,
kPacketMaskRandom9,
kPacketMaskRandom10,
kPacketMaskRandom11,
kPacketMaskRandom12,
};
以 kPacketMaskRandom3 为例,kMaskRandom3_1 表示 3 个原始报文生成 1 个 FEC 报文,FEC 比率为 25%;kMaskRandom3_2 表示 3 个原始报文生成 2 个 FEC 报文,FEC 比率为 40%;kMaskRandom3_3 表示 3 个原始报文生成 3 个 FEC 报文,FEC 比率为 50%。表项已经到头了,因为,WebRTC 允许最大的 FEC 比率为 50%。
#define kPacketMaskRandom3 3, \ kMaskRandom3_1, \ kMaskRandom3_2, \ kMaskRandom3_3
以 kMaskRandom3_3 为例,每一行对应一个 FEC 报文的掩码,取前 3bits。
#define kMaskRandom3_3 \ 0xc0, 0x00, \ 0xa0, 0x00, \ 0x60, 0x00
4.3. 掩码应用
下面以 12个 原始媒体报文使用 4 个 FEC 报文的随机保护为例,讲解掩码的应用,查表结果如下:
#define kMaskRandom12_4 \ 0x8b, 0x20, \ 0x14, 0xb0, \ 0x22, 0xd0, \ 0x45, 0x50
转换为二进制如下所示,灰色填充部分为未启用 bit:

保护逻辑示意图如下所示,实线框为原始媒体报文,虚线框为 FEC 报文:

如果增加一个原始媒体报文,则超过12个限制,不能再查表,由程序代码动态生成掩码。生成逻辑非常简单:每个 FEC 报文只保护“索引对 FEC 报文总数取模与 FEC 报文索引相等”的原始媒体报文,生成的掩码如下图所示:

保护逻辑示意图如下所示。0 号 FEC 报文保护 0、4、8、12 号原始报文;1 号 FEC 报文保护 1、5、9 号原始报文;2 号 FEC 报文保护 2、6、10 号原始报文;3 号 FEC 报文保护 3、7、11 号原始报文。显然,超过12个报文的保护更加均匀,而且每个原始媒体报文只会被一个 FEC 报文保护。

另外,WebRTC 支持分级保护,分级保护分如下几种模式:
enum ProtectionMode
{
// 重点保护和非重点保护不交叉,重点保护保护重要报文,非重点保护保护剩余报文
kModeNoOverlap,
// 重点保护和非重点保护交叉,重点保护只会保护重要报文,非重点保护会保护所有报文
kModeOverlap,
// 在kModeOverlap之上,加强对首个报文的保护力度
kModeBiasFirstPacket,
};
kModeNoOverlap模式
假设前四个报文为重要报文,分配 2 个 FEC 报文进行保护,剩余的 9个 报文分配 2 个FEC 报文进行保护,查表结果如下:
#define kMaskRandom4_2 \ 0xc0, 0x00, \ 0xb0, 0x00 #define kMaskRandom9_2 \ 0xaa, 0x80, \ 0xd5, 0x00
4 个 FEC 的掩码转换为二进制如下所示:

保护逻辑示意图如下所示:

kModeOverlap模式
假设前四个报文为重要报文,分配 2 个 FEC 报文进行保护,另外 2 个 FEC 报文要保护所有原始报文。前 2 个 FEC 报文的掩码可以通过查表得到,后面 2 个 FEC 报文保护的原始报文数量超过 12 个,只能动态生成,最终掩码转换为二进制如下图所示:

保护逻辑示意图如下所示:

kModeBiasFirstPacket
kModeBiasFirstPacket模式,在kModeOverlap之上,加强对第一个报文的保护,掩码表如下所示:

保护逻辑示意图如下所示:

5. 发送 FEC 数据
5.1. 视频报文封装
编码出来的视频帧,如果协商了 RED,会使用 RED 封装。
bool RTPSenderVideo::SendVideo(int payload_type,
absl::optional<VideoCodecType> codec_type,
uint32_t rtp_timestamp,
Timestamp capture_time,
rtc::ArrayView<const uint8_t> payload,
size_t encoder_output_size,
RTPVideoHeader video_header,
TimeDelta expected_retransmission_time,
std::vector<uint32_t> csrcs)
{
...
if (red_enabled()) {
std::unique_ptr<RtpPacketToSend> red_packet(new RtpPacketToSend(*packet));
BuildRedPayload(*packet, red_packet.get());
red_packet->SetPayloadType(*red_payload_type_);
red_packet->set_is_red(true);
red_packet->set_packet_type(RtpPacketMediaType::kVideo);
red_packet->set_allow_retransmission(packet->allow_retransmission());
rtp_packets.emplace_back(std::move(red_packet));
} else {
packet->set_packet_type(RtpPacketMediaType::kVideo);
rtp_packets.emplace_back(std::move(packet));
}
...
}
5.2. FEC 报文封装
FEC 报文也是使用 RED 封装。
std::vector<std::unique_ptr<RtpPacketToSend>> UlpfecGenerator::GetFecPackets() {
if (generated_fec_packets_.empty()) {
return std::vector<std::unique_ptr<RtpPacketToSend>>();
}
last_media_packet_->SetPayloadSize(0);
std::vector<std::unique_ptr<RtpPacketToSend>> fec_packets;
fec_packets.reserve(generated_fec_packets_.size());
size_t total_fec_size_bytes = 0;
for (const auto* fec_packet : generated_fec_packets_) {
// 创建一个新的 RTP 报文
std::unique_ptr<RtpPacketToSend> red_packet =
std::make_unique<RtpPacketToSend>(*last_media_packet_);
// 使用 RED 封装
red_packet->SetPayloadType(red_payload_type_);
// FEC 包的 mark 标记无意义
red_packet->SetMarker(false);
// 增加一个字节的 RED 头
uint8_t* payload_buffer = red_packet->SetPayloadSize(
kRedForFecHeaderLength + fec_packet->data.size());
// Primary RED header with F bit unset.
// See https://tools.ietf.org/html/rfc2198#section-3
//
// 0 1 2 3 4 5 6 7
// +-+-+-+-+-+-+-+-+
// | 0 | Block PT |
// +-+-+-+-+-+-+-+-+
//
// 设置 RED 头的 Block PT
payload_buffer[0] = ulpfec_payload_type_; // RED header.
// 拷贝 FEC 数据
memcpy(&payload_buffer[1], fec_packet->data.data(), fec_packet->data.size());
// 累加 FEC 数据
total_fec_size_bytes += red_packet->size();
// 设置 RTP 头负载类型
red_packet->set_packet_type(RtpPacketMediaType::kForwardErrorCorrection);
// FEC 报文不重传,不会存放到 history 列表
red_packet->set_allow_retransmission(false);
// 设置 RED 报文
red_packet->set_is_red(true);
// FEC 报文不需要再被保护
red_packet->set_fec_protect_packet(false);
fec_packets.push_back(std::move(red_packet));
}
// 进入下一轮编码
ResetState();
MutexLock lock(&mutex_);
// 更新 FEC 码率
fec_bitrate_.Update(total_fec_size_bytes, clock_->CurrentTime());
return fec_packets;
}
5.3. 发送流程
如下图所示,PacingController 不停的发送报文,发送的报文都要经过 RtpSenderEgress,RtpSenderEgress 根据设置的 FEC 参数,调用 VideoFecGenerator 不停的生成 FEC 报文,这些生成 FEC 报文都临时缓存在 VideoFecGenerator 中。PacingController 在发送报文的过程中,会不停的拉取 FEC 报文,将 FEC 报文插入发送队列,最后跟随原始报文一起发送出去。

6. 接收 FEC 数据
收到的视频 RTP 报文会送给 RtpVideoStreamReceiver2 进行处理,RtpVideoStreamReceiver2 会将所有报文都扔给 UlpfecReceiver 处理,UlpfecReceiver 内部会判断是否需要进行恢复以及如何进行恢复,然后将原始报文和恢复后的报文都回调给 RtpVideoStreamReceiver2,FEC 解码工作完成。

接收端 RtpVideoStreamReceiver2 根据报文的 payload type 判断是否是 RED 报文,如果是 RED 报文,则将报文一股脑都扔给 UlpFecReceiver 处理,代码实现如下。由此可见,ulpfec 必须搭配 RED 才能生效。
void RtpVideoStreamReceiver2::ReceivePacket(const RtpPacketReceived& packet) {
if (packet.payload_size() == 0) {
NotifyReceiverOfEmptyPacket(packet.SequenceNumber());
return;
}
// payload type 为 RED 的数据包,都要先交给 UlpfecReceiver 进行处理
// 发送端已将所有所有视频报文和 FEC 报文都使用 RED 封装
if (packet.PayloadType() == red_payload_type_) {
ParseAndHandleEncapsulatingHeader(packet);
return;
}
// 从 FEC 逛一圈回来的报文(“原始报文”+“恢复报文”),此时 payload type 已被替换为
// 真实媒体数据类型
// 根据 payload type 获取解包器
const auto type_it = payload_type_map_.find(packet.PayloadType());
if (type_it == payload_type_map_.end()) {
return;
}
// 如果是 H264 报文,则可能要做 STAP-A、FU-A、Single 解包
absl::optional<VideoRtpDepacketizer::ParsedRtpPayload> parsed_payload =
type_it->second->Parse(packet.PayloadBuffer());
if (parsed_payload == absl::nullopt) {
return;
}
OnReceivedPayloadData(std::move(parsed_payload->video_payload), packet,
parsed_payload->video_header);
}
7. 总结
UlpFEC 的核心是 FEC 保护比率和掩码表,FEC 保护比率决定了能使用多少 FEC 报文来保护原始报文,掩码表决定了每个 FEC 报文要去保护哪些原始报文。围绕这两个核心概念,设计如何生成 FEC 报文,如何打包和解包、如何发送和接收以及如何恢复原始报文。
深入探索WebRTC实现原理

上海得物信息集团有限公司公司福利 1166人发布