webrtc FEC 協議

小夕nike發表於2024-05-23

參考:
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 包和保護的原始包的對應關係,判斷缺失包的數量,邏輯比較簡單,這裡不用展示了。

相關文章