webrtc FEC 协议

参考:
https://www.cnblogs.com/ishen/p/15333271.html
https://zhuanlan.zhihu.com/p/603421239

1. 生成

1.1 等待并筹齐多个原始包

webrtc 会等待筹齐多个 rtp 包后,再统一生成冗余包,参看 UlpfecGenerator::AddPacketAndGenerateFec() 函数:

void UlpfecGenerator::AddPacketAndGenerateFec(const RtpPacketToSend& packet) {
  ...

  const bool complete_frame = packet.Marker();
  if (media_packets_.size() < kUlpfecMaxMediaPackets) {
    // 构造 fec packet 放入等待队列中
    // Our packet masks can only protect up to |kUlpfecMaxMediaPackets| packets.
    auto fec_packet = std::make_unique<ForwardErrorCorrection::Packet>();
    fec_packet->data = packet.Buffer();
    media_packets_.push_back(std::move(fec_packet));
  }

  // 这里每帧计数加1
  if (complete_frame) {
    ++num_protected_frames_;
  }

  // 动态计算得到 fec 配置参数
  auto params = CurrentParams();

  // 判断是否满足 fec 的条件
  // 1. 必须从 complete_frame 开始
  // 2. 包数量大于等于 params.max_fec_frames 阈值
  // 3. 或者 目前冗余度低于设定的冗余度,同时还筹齐了最少 4 个包
  if (complete_frame &&
      (num_protected_frames_ >= params.max_fec_frames ||
       (ExcessOverheadBelowMax() && MinimumMediaPacketsReached()))) {
    // 开始 fec 编码
    fec_->EncodeFec(media_packets_, params.fec_rate, kNumImportantPackets,
                    kUseUnequalProtection, params.fec_mask_type,
                    &generated_fec_packets_);
  }
  ...
}

需要注意的是:

  • complete_frame 变量的存在,表明如果一个编码帧生成了多个 rtp 包,那么 fec 不会分开他们,同一帧的多个 rtp 包总是被一个 fec 保护

1.2 重新累积

每 k 个数据包生成 fec 后,需要重新累积,参看 UlpfecGenerator::ResetState() 函数:

void UlpfecGenerator::ResetState() {
  // 清空原始数据包
  media_packets_.clear();
  last_media_packet_.reset();
  generated_fec_packets_.clear();
  num_protected_frames_ = 0;
  media_contains_keyframe_ = false;
}

1.3 算法原理

一个待发送的 rtp 包,可以看做 [1, n] 即 1 行 n 列形式的数据,其中 n 是字节数,有 k 个原始数据包,那么可以得到 D = [k, n] 的数据。
fec 生成可以看做一个生成矩阵 G 乘以 D,现在假设 G = [q, k],那么 G * D = F,即 F = [q, n]。
以上公式可以描述为:k 个原始数据包,生成了 q 个冗余包,其中 q <= k。
那么对于 webrtc fec,G 生成矩阵是如何设计的呢?实际上 G 只由 0 和 1 构成,表示选择或不选择,且 G * D 的矩阵加法变为 xor 异或运算,解决数据溢出的问题。

1.4 掩码表选择

上一节说了生成矩阵的选择,在 webrtc 的实现中,预先配置了大量生成矩阵(或者称为掩码),例如 fec_private_tables_random.cc 文件内:

...

#define kMaskRandom8_4 \
  0x45, 0x00, \
  0xb4, 0x00, \
  0x6a, 0x00, \
  0x89, 0x00
...

#define kMaskRandom10_3 \
  0xa4, 0x40, \
  0xc9, 0x00, \
  0x52, 0x80

...

这里生成矩阵每一行由 16 进制数表示,kMaskRandom8_4 表示 8 个原始包生成 4 个 fec 包。
实际上 webrtc 对 fec 丢包模型有两种生成矩阵配置表,即突发丢包和随机丢包,对应不同掩码表。这两种配置表数值设计的规则是什么,暂时还不清楚

掩码表的选择,在 ForwardErrorCorrection::EncodeFec() 函数中:

int ForwardErrorCorrection::EncodeFec(const PacketList& media_packets,
                                      uint8_t protection_factor,
                                      int num_important_packets,
                                      bool use_unequal_protection,
                                      FecMaskType fec_mask_type,
                                      std::list<Packet*>* fec_packets) {
  // 原始包数量
  const size_t num_media_packets = media_packets.size();
  // 根据原始包数量和实时配置的冗余率,得到 fec 包数量
  int num_fec_packets = NumFecPackets(num_media_packets, protection_factor);
  // 找到掩码表
  internal::GeneratePacketMasks(num_media_packets, num_fec_packets,
                                num_important_packets, use_unequal_protection,
                                &mask_table, packet_masks_);
  ...
}

1.5 计算生成

现在生成矩阵和原始数据都有了,只需要按矩阵乘法生成 fec 即可,参看 ForwardErrorCorrection::GenerateFecPayloads() 函数:

void ForwardErrorCorrection::GenerateFecPayloads(
    const PacketList& media_packets,
    size_t num_fec_packets) {
  // 遍历生成每个冗余包
  for (size_t i = 0; i < num_fec_packets; ++i) {
    ...
    // 对于每个冗余包,都需要遍历一遍原始数据包,根据掩码表确定哪些数据包需要参与本轮 fec 包生成
    while (media_packets_it != media_packets.end()) {
      Packet* const media_packet = media_packets_it->get();
      // Should `media_packet` be protected by `fec_packet`?
      // 查掩码表,如果为 1,表示当前数据包要参与当前 fec 包生成
      if (packet_masks_[pkt_mask_idx] & (1 << (7 - media_pkt_idx))) {
        ...
        XorHeaders(*media_packet, fec_packet);
        XorPayloads(*media_packet, media_payload_length, fec_header_size,
                    fec_packet);
      }
      media_packets_it++;
      ...
    }
  }
}

对于数据包的 header,webrtc 内部会 xor 前 12 字节(webrtc 不会包含 csrc),参看 ForwardErrorCorrection::XorHeaders() 函数:

void ForwardErrorCorrection::XorHeaders(const Packet& src, Packet* dst) {
  uint8_t* dst_data = dst->data.MutableData();
  const uint8_t* src_data = src.data.cdata();
  // XOR the first 2 bytes of the header: V, P, X, CC, M, PT fields.
  dst_data[0] ^= src_data[0];
  dst_data[1] ^= src_data[1];

  // XOR the length recovery field.
  uint8_t src_payload_length_network_order[2];
  ByteWriter<uint16_t>::WriteBigEndian(src_payload_length_network_order,
                                       src.data.size() - kRtpHeaderSize);
  dst_data[2] ^= src_payload_length_network_order[0];
  dst_data[3] ^= src_payload_length_network_order[1];

  // XOR the 5th to 8th bytes of the header: the timestamp field.
  dst_data[4] ^= src_data[4];
  dst_data[5] ^= src_data[5];
  dst_data[6] ^= src_data[6];
  dst_data[7] ^= src_data[7];

  // Skip the 9th to 12th bytes of the header.
}

对于数据包的 payload,webrtc 内部实际上是从 12 字节的 header 后开始 xor,即 header extension 也会被保护,参看 ForwardErrorCorrection::XorPayloads() 函数:

void ForwardErrorCorrection::XorPayloads(const Packet& src,
                                         size_t payload_length,
                                         size_t dst_offset,
                                         Packet* dst) {
  // 如果当前数据包比较长,fec 包需要扩容
  if (dst_offset + payload_length > dst->data.size()) {
    size_t old_size = dst->data.size();
    size_t new_size = dst_offset + payload_length;
    dst->data.SetSize(new_size);
    memset(dst->data.MutableData() + old_size, 0, new_size - old_size);
  }
  uint8_t* dst_data = dst->data.MutableData();
  const uint8_t* src_data = src.data.cdata();
  // 注意 payload_length 即数据包减去 12 字节后的结果
  for (size_t i = 0; i < payload_length; ++i) {
    dst_data[dst_offset + i] ^= src_data[kRtpHeaderSize + i];
  }
}

1.6 ulp fec 和 flex fec 有什么区别

实际上 webrtc 生成 fec 的代码,即 ForwardErrorCorrection 类,对 ulp 和 flex 都是相同的,即 ulp fec 只存在 level 0 级的全量保护。
唯一区别就是两者生成的 fec 头部格式不同:

std::unique_ptr<ForwardErrorCorrection> ForwardErrorCorrection::CreateUlpfec(
    uint32_t ssrc) {
  std::unique_ptr<FecHeaderReader> fec_header_reader(new UlpfecHeaderReader());
  std::unique_ptr<FecHeaderWriter> fec_header_writer(new UlpfecHeaderWriter());
  return std::unique_ptr<ForwardErrorCorrection>(new ForwardErrorCorrection(
      std::move(fec_header_reader), std::move(fec_header_writer), ssrc, ssrc));
}

std::unique_ptr<ForwardErrorCorrection> ForwardErrorCorrection::CreateFlexfec(
    uint32_t ssrc,
    uint32_t protected_media_ssrc) {
  std::unique_ptr<FecHeaderReader> fec_header_reader(new FlexfecHeaderReader());
  std::unique_ptr<FecHeaderWriter> fec_header_writer(new FlexfecHeaderWriter());
  return std::unique_ptr<ForwardErrorCorrection>(new ForwardErrorCorrection(
      std::move(fec_header_reader), std::move(fec_header_writer), ssrc,
      protected_media_ssrc));
}

如上可以看到,两者只是在 fec_header_reader 和 fec_header_writer 上不一样。同时 ulp fec 与原始数据流共用同一个 ssrc、序列号,只是 pt 值不同;flex fec 有自己独立的 ssrc、序列号、pt 值。

2. 打包传输

2.1 ulp fec 打包方式

在 UlpfecGenerator::GetFecPackets() 函数中,会先将生成的 fec 包打包为 red 封装格式,然后打包为 rtp 格式(为什么不单独传输而是要用 red 封装?暂时不清楚原因):

std::vector<std::unique_ptr<RtpPacketToSend>> UlpfecGenerator::GetFecPackets() {
  ...
  // 遍历生成的 fec 包
  for (const auto* fec_packet : generated_fec_packets_) {
    std::unique_ptr<RtpPacketToSend> red_packet =
        std::make_unique<RtpPacketToSend>(*last_media_packet_);

    // red 打包,具体参看源码
    // 实际上这里 red 打包格式很简单,只使用了一个 block header,即一个 F + PT 的结构
    ...

    fec_packets.push_back(std::move(red_packet));
  }

  ResetState();

  ...

  return fec_packets;
}

注意这里会调用 ResetState() 清空状态,参考前文。

2.2 flex fec 打包方式

在 FlexfecSender::GetFecPackets() 函数中,会将生成的 fec 包打包为 rtp,并且需要注意的是其拥有与原始包独立的 ssrc、pt、seq 值等字段:

std::vector<std::unique_ptr<RtpPacketToSend>> FlexfecSender::GetFecPackets() {
  ...
  // 遍历生成的 fec 包
  for (const auto* fec_packet : ulpfec_generator_.generated_fec_packets_) {
    std::unique_ptr<RtpPacketToSend> fec_packet_to_send(
        new RtpPacketToSend(&rtp_header_extension_map_));

    // rtp 打包,具体参看源码
    ...
    
    fec_packets_to_send.push_back(std::move(fec_packet_to_send));
  }

  if (!fec_packets_to_send.empty()) {
    ulpfec_generator_.ResetState();
  }

  ...

  return fec_packets_to_send;
}

注意这里也会调用 ResetState() 清空状态,参考前文。

3. 丢包恢复

在接收到数据包后,会来到 ForwardErrorCorrection::DecodeFec() 函数:

void ForwardErrorCorrection::DecodeFec(const ReceivedPacket& received_packet,
                                       RecoveredPacketList* recovered_packets) {
  ...

  InsertPacket(received_packet, recovered_packets);
  AttemptRecovery(recovered_packets);
}

ForwardErrorCorrection::InsertPacket() 会将原始数据包插入到 recovered_packets 队列,并将 fec 包插入到 received_fec_packets_ 队列:

void ForwardErrorCorrection::InsertPacket(
    const ReceivedPacket& received_packet,
    RecoveredPacketList* recovered_packets) {
  ...

  if (received_packet.is_fec) {
    InsertFecPacket(*recovered_packets, received_packet);
  } else {
    InsertMediaPacket(recovered_packets, received_packet);
  }

  DiscardOldRecoveredPackets(recovered_packets);
}

同时在调用 InsertFecPacket() 和 InsertMediaPacket() 函数的时候,会根据 fec 包的 fec header 中的 mask 字段,将 fec 包与其保护的原始数据包对应起来,以 ForwardErrorCorrection::UpdateCoveringFecPackets() 函数为例:

void ForwardErrorCorrection::UpdateCoveringFecPackets(
    const RecoveredPacket& packet) {
  for (auto& fec_packet : received_fec_packets_) {
    // Is this FEC packet protecting the media packet `packet`?
    auto protected_it = absl::c_lower_bound(
        fec_packet->protected_packets, &packet, SortablePacket::LessThan());
    if (protected_it != fec_packet->protected_packets.end() &&
        (*protected_it)->seq_num == packet.seq_num) {
      // Found an FEC packet which is protecting `packet`.
      (*protected_it)->pkt = packet.pkt;
    }
  }
}

之后就是调用 ForwardErrorCorrection::AttemptRecovery() 函数试图恢复缺失包了:

void ForwardErrorCorrection::AttemptRecovery(
    RecoveredPacketList* recovered_packets) {
  // 遍历收到的 fec 包列表
  auto fec_packet_it = received_fec_packets_.begin();
  while (fec_packet_it != received_fec_packets_.end()) {
    // Search for each FEC packet's protected media packets.
    int packets_missing = NumCoveredPacketsMissing(**fec_packet_it);

    // We can only recover one packet with an FEC packet.
    if (packets_missing == 1) {
      // Recovery possible.
      // 缺失包恢复
      std::unique_ptr<RecoveredPacket> recovered_packet(new RecoveredPacket());
      recovered_packet->pkt = nullptr;
      if (!RecoverPacket(**fec_packet_it, recovered_packet.get())) {
        // Can't recover using this packet, drop it.
        fec_packet_it = received_fec_packets_.erase(fec_packet_it);
        continue;
      }
      auto* recovered_packet_ptr = recovered_packet.get();
      // 将恢复的缺失包,加入到原始包队列
      recovered_packets->push_back(std::move(recovered_packet));
      recovered_packets->sort(SortablePacket::LessThan());
      // 更新 fec 和原始包的关联关系
      UpdateCoveringFecPackets(*recovered_packet_ptr);
      DiscardOldRecoveredPackets(recovered_packets);
      // 当前 fec 包已经不需要了
      fec_packet_it = received_fec_packets_.erase(fec_packet_it);

      // A packet has been recovered. We need to check the FEC list again, as
      // this may allow additional packets to be recovered.
      // Restart for first FEC packet.
      // 由于新的缺失包的恢复,可能前面的 fec 包也能进行丢包恢复了,所以这里重新开始遍历
      fec_packet_it = received_fec_packets_.begin();

      ...
    }
  }
}

可以看到,只有 packets_missing == 1 即当前 fec 包对应保护的原始包,只缺失一个时,才能恢复,这与 XOR 算法吻合。
而 NumCoveredPacketsMissing() 函数就是根据 fec 包和保护的原始包的对应关系,判断缺失包的数量,逻辑比较简单,这里不用展示了。

posted @ 2024-05-23 17:40  小夕nike  阅读(166)  评论(0编辑  收藏  举报