webrtc源码分析(7)-fec

1.前言

本文介绍了webrtc中的fec相关封装原理, 协议,分析其在webrtc中的应用过程和使用策略。

2.正文

2.1 red

为什么做red封装呢?Ulpfec编码后的内容会做Red封装后再放入RtpPacket,可fec在RFC5109已经定义好自己的传输格式,而且sdp协商过程中也有Ulpfec的PT为a=rtpmap:125 ulpfec/90000, 可以通过RtpHeader的PT进行区分出Ulpfec包,看起来是多此一举. 其实不然,fec过程中要传输的包有两种,一种是冗余包,一种是原始包,将他们统一的放在red中,表示这些包属于一个fec流,然后red提供的pt可以分辨出那些是fec包,哪些是原始数据包

2.1.1 red封装格式

Red的封装如下,由block header + data block组成,payload会被放在data block中

0                   1                    2                   3
0 1 2 3 4 5 6 7 8 9 0 1 2 3  4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|F|   block PT  |  timestamp offset         |   block length    | : block header
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                             block                             | : data block
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

介绍一下各个标志位

F: 为1时标识后面是否有别的block,如果有则会出现多个这样的header line
  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
  |F|   block PT  |  timestamp offset         |   block length    |
  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
  |F|   block PT  |  timestamp offset         |   block length    |
  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
  .
  .
  .

  为0时,标识后面不会再有这个header line,同时最后block的timestamp和block
length能够通过rtp推断出,所以会省略:
 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 |1| block PT=7  |  timestamp offset         |   block length    |
 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 |0| block PT=5  |                                               |
 +-+-+-+-+-+-+-+-+

block PT: block内容中的rtp payload type
block length: block的大小
timestamp offset: block 的timestam 相对于rtp header timestamp 的offset

一个完整的red如下所示:


//   0                   1                    2                   3
//   0 1 2 3 4 5 6 7 8 9 0 1 2 3  4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
//  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
//  |V=2|P|X| CC=0  |M|      PT     |   sequence number of primary  |
//  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
//  |              timestamp  of primary encoding                   |
//  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
//  |           synchronization source (SSRC) identifier            |
//  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
//  |1| block PT=7  |  timestamp offset         |   block length    |
//  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
//  |0| block PT=5  |                                               |
//  +-+-+-+-+-+-+-+-+                                               +
//  |                                                               |
//  +                LPC encoded redundant data (PT=7)              +
//  |                (14 bytes)                                     |
//  +                                               +---------------+
//  |                                               |               |
//  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+               +
//  |                                                               |
//  +                                                               +
//  |                                                               |
//  +                                                               +
//  |                                                               |
//  +                                                               +
//  |                DVI4 encoded primary data (PT=5)               |
//  +                (84 bytes, not to scale)                       +
//  /                                                               /
//  +                                                               +
//  |                                                               |
//  +                                                               +
//  |                                                               |
//  +                                               +---------------+
//  |                                               |

由于webrtc只用到了单个block的red封装,就是在payload前加多1Byte,把payloadtype放进去

2.1.2 red封装流程

RTPSenderVideo::SendVideo()是对packet进行RTP打包的地方,会检查是否开启了red封装,会将打好的包重新设置成red_packet

bool RTPSenderVideo::SendVideo(
    int payload_type,
    absl::optional<VideoCodecType> codec_type,
    uint32_t rtp_timestamp,
    int64_t capture_time_ms,
    rtc::ArrayView<const uint8_t> payload,
    RTPVideoHeader video_header,
    absl::optional<int64_t> expected_retransmission_time_ms,
    absl::optional<int64_t> estimated_capture_clock_offset_ms) {
    
    if (red_enabled()) {
      // 将packet替换成red_packet,将media payload放进red_packet中
      // TODO(sprang): Consider packetizing directly into packets with the RED
      // header already in place, to avoid this copy.
      std::unique_ptr<RtpPacketToSend> red_packet(new RtpPacketToSend(*packet));
      BuildRedPayload(*packet, red_packet.get());	// 生成red包
      red_packet->SetPayloadType(*red_payload_type_);// 设定payloadType为Red
      red_packet->set_is_red(true);// 标识此包为一个red包

      // Append |red_packet| instead of |packet| to output.
      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));
    }    
    
    ....
    
}

这里首先会受到enable red的影响,去标识当前包要做red封包(rfc2189), 其中:

  • red_packet本身的类型和普通包的类型都是RtpPacketToSend

  • 调用BuildRedPayload() 去做Red封装, 首先在此介绍一下Red封装

    void BuildRedPayload(const RtpPacketToSend& media_packet,
                         RtpPacketToSend* red_packet) {
      // 新增1Byte,用于放Red Header                 
      uint8_t* red_payload = red_packet->AllocatePayload(
          kRedForFecHeaderLength + media_packet.payload_size());
      RTC_DCHECK(red_payload);
      red_payload[0] = media_packet.PayloadType(); //填入payload type
    
      // 拷贝payload 到block上
      auto media_payload = media_packet.payload();
      memcpy(&red_payload[kRedForFecHeaderLength], media_payload.data(),
             media_payload.size());
    }
    
  • 通过red_packet->set_is_red(true)表示了这是一个red封装的包

  • 通过red_packet->SetPayloadType(*red_payload_type_) 设置了当前包的payloadType为red_payload_type,表征了payload为red,这个值一般是127, 这个config来自于struct UlpfecConfig

  struct UlpfecConfig {
    UlpfecConfig()
        : ulpfec_payload_type(-1),
          red_payload_type(-1),
          red_rtx_payload_type(-1) {}
    std::string ToString() const;
    bool operator==(const UlpfecConfig& other) const;
  
    // Payload type used for ULPFEC packets.
    int ulpfec_payload_type;
  
    // Payload type used for RED packets.
    int red_payload_type;
  
    // RTX payload type for RED payload.
    int red_rtx_payload_type;
  };

2.2 fec

2.2.1 ulpfec原理

fec的原理很简单大家都知道,就是异或,比如说传输两个包 P1P2, 通过 异或P1,P2得到FEC包 F1, 那么传输过程中能收到任意两个,都可以通过异或恢复出原来的 P1P2

两个包的问题好考虑,但是多个包的时候会引入第二个问题,为了带宽控制fec包的数量时候,应该选择哪一部分包作为一个集合去异或出一个fec包呢? 比如有10个数据包,当前一段传输一段时间内丢包率为30%,此时可以引入一个简单的处理策略: 将fec包的数量控制为丢包率也就是3个,但这3个FEC包由哪一块做异或呢? 如下所示,有两种分配方式,一种是以相邻的包为一组做fec,这时候每3个包可能丢一个,不影响恢复,这种模型能较好的对抗随机丢包的网络,第二种则是取等间隔的包做一组,突发性丢包会导致连续的一块包丢失,这种模型对突发丢包的网络比较好

2.2.2 ulpfec封装格式

详见RFC5109, 在Rtp传输时上,跟在RTPheader后,可以理解作为payload吧

   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |                RTP Header (12 octets or more)                 |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |                    FEC Header (10 octets)                     |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |                      FEC Level 0 Header                       |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |                     FEC Level 0 Payload                       |
   |                                                               |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |                      FEC Level 1 Header                       |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |                     FEC Level 1 Payload                       |
   |                                                               |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |                            Cont.                              |
   |                                                               |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

其中 FEC Header如下所示:

    0                   1                   2                   3
    0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |E|L|P|X|  CC   |M| PT recovery |            SN base            |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |                          TS recovery                          |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |        length recovery        |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

E: 标识是否启用扩展头,当前值为0,保留位,

L: 使用的mask表的长度,0-->16bits, 1-->48bits

P, X, CC, M, PT recovery: 进行异或的Rtp包头上的P, X, CC, M, PT的异或结果

SN base: 进行异或的Rtp包的最小sequence号

Ts recovery: 进行异或的Rtp包的Timestamp的异或结果

length: 进行异或的Rtp包的payload长度的异或结果

ulpfec中可以将不同的数据使用不同的level级别去做保护, 如下:

         Packet A          #####################
                                  :        :
         Packet B          ############### :
                                  :        :
         ULP FEC Packet #1 @@@@@@@@        :
                                  :        :
         Packet C          ###########     :
                                  :        :
         Packet D          ###################################
                                  :        :
         ULP FEC Packet #2 @@@@@@@@@@@@@@@@@
                           :      :        :
                           :<-L0->:<--L1-->:

         Payload packet #  |  ULP FEC packet that protects at level
                           |          L0             L1
      ---------------------+---------------------------------------
                A          |          #1             #2
                B          |          #1             #2
                C          |          #2             #2
                D          |          #2             #2

对A和B的前半段数据使用#1包去保护,对C和D的前半段数据用#2包去保护,这前半段数据都处于L0级别

对于A,B,C,D的后半段数据使用#2包保护,这些保护的数据被设在L1级别

这里认为A,B,C,D的后半段的数据没那么重要,用4:1的比例做冗余就够了,而前半段的数据很重要,所以需要2:1的比例做冗余(一直觉得RFC上这图画的是不是有问题,L1的长度这么短,后面的数据是不要了吗,嘻);

因为一个FEC包中可能有不同level级别的数据,为此在Fec header中引入了 FEC Level 0 Header 和 FEC Level 1 Header,结构如下所示,主要是标记了保护的数据长度和参与fec包的掩码表

    0                   1                   2                   3
    0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |       Protection Length       |             mask              |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |              mask cont. (present only when L = 1)             |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

Protection Length: L0包保护的长度

mask: 标记packet是否参与异或的掩码表,每个bit标记着以fec header的SN base作为偏移起始的rtp Sequence对应的包是否参与当前fec block的编码

mask cont:当L被设置成1的时候 mask cont会出现和mask连起来形成48bit的掩码表

但这里需要明确一下,webrtc中没有用到L1这个级别,因为packet中的信息都是帧的一部分优先级都是一样的,但并不是说L1这个东西不能用,可以把上面的粒度替换一下,packet换成Frame,后面的数据换成包也可以尝试一番。

2.2.3 ulpfec编码过程

2.2.3.1 解red封装

webrtc做fec冗余的地方在随后发送链路中的RtpSenderEgress::SendPacket()

void RtpSenderEgress::SendPacket(RtpPacketToSend* packet,
                                 const PacedPacketInfo& pacing_info) {
  ....
//fec处理
  if (fec_generator_ && packet->fec_protect_packet()) {
    // This packet should be protected by FEC, add it to packet generator.
    RTC_DCHECK(fec_generator_);
    RTC_DCHECK(packet->packet_type() == RtpPacketMediaType::kVideo);
    absl::optional<std::pair<FecProtectionParams, FecProtectionParams>>
        new_fec_params;
    {
      MutexLock lock(&lock_);
      new_fec_params.swap(pending_fec_params_);
    }
    // fec_rate和fec_max_frame 可能被更新了
    if (new_fec_params) {
      fec_generator_->SetProtectionParameters(new_fec_params->first,
                                              new_fec_params->second);
    }
    if (packet->is_red()) {
      // 对packet做了red封装(rfc2198),需要解red封装得到原始rtp包
      // 很奇怪,会进行red封包的应该只有fec,普通包也会进行fec封装?


      // 复制整个包
      RtpPacketToSend unpacked_packet(*packet);

      const rtc::CopyOnWriteBuffer buffer = packet->Buffer();
      // Grab media payload type from RED header.
      const size_t headers_size = packet->headers_size();
      unpacked_packet.SetPayloadType(buffer[headers_size]);

      // 对copyonwirte的payload进行拷贝
      // Copy the media payload into the unpacked buffer.
      uint8_t* payload_buffer =
          unpacked_packet.SetPayloadSize(packet->payload_size() - 1);
      std::copy(&packet->payload()[0] + 1,
                &packet->payload()[0] + packet->payload_size(), payload_buffer);

      // 对包做fec
      fec_generator_->AddPacketAndGenerateFec(unpacked_packet);
    } else {
      // If not RED encapsulated - we can just insert packet directly.
      fec_generator_->AddPacketAndGenerateFec(*packet);
    }
  }
  .....
}

主要做了两件事:

  • 通过packet->fec_protect_packet()检查packet是否启用了fec,是则使用fec_generator_->AddPacketAndGenerateFec()将包加入到fec的队列中准备做fec
  • 如果packet做了red封装,需要对payload解red封装,也就是那个1 Byte的red header给去掉

2.2.3.2 更新fec params

此处用的是ulpfec,接下来的函数是UlpfecGenerator::AddPacketAndGenerateFec()

void UlpfecGenerator::AddPacketAndGenerateFec(const RtpPacketToSend& packet) {
  RTC_DCHECK_RUNS_SERIALIZED(&race_checker_);
  RTC_DCHECK(generated_fec_packets_.empty());

  {
    MutexLock lock(&mutex_);
    // 更新fec params
    if (pending_params_) {
      current_params_ = *pending_params_;
      pending_params_.reset();

      // 重设的阈值高于kHighProtectionThreshold, 更改最少需要4个包去做fec encode
      if (CurrentParams().fec_rate > kHighProtectionThreshold) {
        min_num_media_packets_ = kMinMediaPackets;
      } else {
        min_num_media_packets_ = 1;
      }
    }
  }

  if (packet.is_key_frame()) {
    media_contains_keyframe_ = true;
  }
  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));

    // Keep a copy of the last RTP packet, so we can copy the RTP header
    // from it when creating newly generated ULPFEC+RED packets.
    RTC_DCHECK_GE(packet.headers_size(), kRtpHeaderSize);
    last_media_packet_ = packet;
  }

  if (complete_frame) {
    ++num_protected_frames_;
  }

  auto params = CurrentParams();
  // Produce FEC over at most |params_.max_fec_frames| frames, or as soon as:
  // (1) the excess overhead (actual overhead - requested/target overhead) is
  // less than |kMaxExcessOverhead|, and
  // (2) at least |min_num_media_packets_| media packets is reached.
  if (complete_frame &&
      (num_protected_frames_ >= params.max_fec_frames ||
       (ExcessOverheadBelowMax() && MinimumMediaPacketsReached()))) {
    // We are not using Unequal Protection feature of the parity erasure code.
    constexpr int kNumImportantPackets = 0;
    constexpr bool kUseUnequalProtection = false;
    // fec编码
    fec_->EncodeFec(media_packets_, params.fec_rate, kNumImportantPackets,
                    kUseUnequalProtection, params.fec_mask_type,
                    &generated_fec_packets_);
    if (generated_fec_packets_.empty()) {
      ResetState();
    }
  }
}

主要做了:

  • 更新ulpfec的params,结构如下,对于关键帧和P帧各有一套独立的控制参数,控制参数主要有

    fec_rate: 为(fec包数)/(媒体包数) * 256,调整策略见 2.3

    max_fec_frames: fec编码是针对收集到的一段packet进行fec编码的,此参数规定到了这个数量时一定要fec编码了

    fec_mask_type:webrtc根据丢包模型准备了两个fec的掩码表,随机丢包掩码表和突发丢包掩码表

     struct Params {
        Params();
        Params(FecProtectionParams delta_params,
               FecProtectionParams keyframe_params);
    
        FecProtectionParams delta_params;	// p帧
        FecProtectionParams keyframe_params;// 关键帧
      };
    
    struct FecProtectionParams {
      int fec_rate = 0;			// fec_packet_num / media_packet_num * 256
      int max_fec_frames = 0;	// 经过max_fec_frames一定要生成fec包
      FecMaskType fec_mask_type = FecMaskType::kFecMaskRandom;// 选择使用给的fec掩码表
    };
    
  • 当fec_rate大于kHighProtectionThreshold(80)时,重设了最小FEC编码packet数为4,不太清楚这样改动的意义,是怕太频繁重复encode吗

  • 构建Fec packet,将payload拷贝进去

  • 检查是否立即进行FEC encode,满足的条件有一下两种,满足任意一个就调用fec_->EncodeFec()进行编码

    1.完整帧 + 收集的帧数量超过设定值

    2.完整帧 + 达到设定最小收集包数 + 当前做FEC所得到的rate和预设的fec_rate相减小于一个阈值

    关于加黑的条件也就是ExcessOverheadBelowMax(),有一些细节, 在实际计算要用到的fec包的数量时,在fec_rate的基础上还加了个0.5的上取整,这是导致实际和预设出现差值的原因

    
    // 通过fec_rate计算实际需要用到的fec包数
    int ForwardErrorCorrection::NumFecPackets(int num_media_packets,
                                              int protection_factor) {
      // Result in Q0 with an unsigned round.
      // fec = media * fec_rate / 256 + 0.5 此处做了个上取整
      int num_fec_packets = (num_media_packets * protection_factor + (1 << 7)) >> 8;
      // Generate at least one FEC packet if we need protection.
      if (protection_factor > 0 && num_fec_packets == 0) {
        num_fec_packets = 1;
      }
      RTC_DCHECK_LE(num_fec_packets, num_media_packets);
      return num_fec_packets;
    }
    
    
    int UlpfecGenerator::Overhead() const {
      RTC_DCHECK_RUNS_SERIALIZED(&race_checker_);
      RTC_DCHECK(!media_packets_.empty());
      int num_fec_packets =
          fec_->NumFecPackets(media_packets_.size(), CurrentParams().fec_rate);
    
      // overheade的计算,用实际需要使用的 fec包数 * 256 / media包数
      return (num_fec_packets << 8) / media_packets_.size();
    }
    
    
    bool UlpfecGenerator::ExcessOverheadBelowMax() const {
      RTC_DCHECK_RUNS_SERIALIZED(&race_checker_);
      // 实际 - 预设 < 50
      return ((Overhead() - CurrentParams().fec_rate) < kMaxExcessOverhead);
    }
    

    同时,注意到注释中说没有使用Unequal Protection feature of the parity erasure code.没有启动L1级别做FEC;

2.2.3.3 开始Encodefec

接下来到了实际的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();

  // Sanity check arguments.
  RTC_DCHECK_GT(num_media_packets, 0);
  RTC_DCHECK_GE(num_important_packets, 0);
  RTC_DCHECK_LE(num_important_packets, num_media_packets);
  RTC_DCHECK(fec_packets->empty());
  const size_t max_media_packets = fec_header_writer_->MaxMediaPackets();
  if (num_media_packets > max_media_packets) {
    RTC_LOG(LS_WARNING) << "Can't protect " << num_media_packets
                        << " media packets per frame. Max is "
                        << max_media_packets << ".";
    return -1;
  }

  // Error check the media packets.
  for (const auto& media_packet : media_packets) {
    RTC_DCHECK(media_packet);
    if (media_packet->data.size() < kRtpHeaderSize) {
      RTC_LOG(LS_WARNING) << "Media packet " << media_packet->data.size()
                          << " bytes "
                             "is smaller than RTP header.";
      return -1;
    }
    // Ensure the FEC packets will fit in a typical MTU.
    if (media_packet->data.size() + MaxPacketOverhead() + kTransportOverhead >
        IP_PACKET_SIZE) {
      RTC_LOG(LS_WARNING) << "Media packet " << media_packet->data.size()
                          << " bytes "
                             "with overhead is larger than "
                          << IP_PACKET_SIZE << " bytes.";
    }
  }

  // Prepare generated FEC packets.
  int num_fec_packets = NumFecPackets(num_media_packets, protection_factor);
  if (num_fec_packets == 0) {
    return 0;
  }
  for (int i = 0; i < num_fec_packets; ++i) {
    generated_fec_packets_[i].data.EnsureCapacity(IP_PACKET_SIZE);
    memset(generated_fec_packets_[i].data.MutableData(), 0, IP_PACKET_SIZE);
    // Use this as a marker for untouched packets.
    generated_fec_packets_[i].data.SetSize(0);
    fec_packets->push_back(&generated_fec_packets_[i]);
  }

  // 根据丢包模型选择mask表,生成mask
  internal::PacketMaskTable mask_table(fec_mask_type, num_media_packets);
  packet_mask_size_ = internal::PacketMaskSize(num_media_packets);
  memset(packet_masks_, 0, num_fec_packets * packet_mask_size_);
  // 生成mask表
  internal::GeneratePacketMasks(num_media_packets, num_fec_packets,
                                num_important_packets, use_unequal_protection,
                                &mask_table, packet_masks_);

  // Adapt packet masks to missing media packets.
  int num_mask_bits = InsertZerosInPacketMasks(media_packets, num_fec_packets);
  if (num_mask_bits < 0) {
    RTC_LOG(LS_INFO) << "Due to sequence number gaps, cannot protect media "
                        "packets with a single block of FEC packets.";
    fec_packets->clear();
    return -1;
  }
  packet_mask_size_ = internal::PacketMaskSize(num_mask_bits);


  // 开始FEC
  // Write FEC packets to |generated_fec_packets_|.
  GenerateFecPayloads(media_packets, num_fec_packets);
  // TODO(brandtr): Generalize this when multistream protection support is
  // added.
  const uint32_t media_ssrc = ParseSsrc(media_packets.front()->data.data());
  const uint16_t seq_num_base =
      ParseSequenceNumber(media_packets.front()->data.data());
  FinalizeFecHeaders(num_fec_packets, media_ssrc, seq_num_base);

  return 0;
}

ForwardErrorCorrection::EncodeFec()主要:

  • 如2.2.1中所介绍的,随机丢包和突发丢包下fec组包模式不同,所以webrtc准备了两张mask表 kFecMaskRandom(随机丢包), kFecMaskBursty(突发丢包)去生成mask, 调用internal::GeneratePacketMasks()去生成mask
  • 根据mask和packet, 调用GenerateFecPayloads()生成fec包
  • 调用FinalizeFecHeaders()填入ssrc

2.2.3.4 生成mask

以下为调用要生成mask的internal::GeneratePacketMasks()中

void GeneratePacketMasks(int num_media_packets,
                         int num_fec_packets,
                         int num_imp_packets,
                         bool use_unequal_protection,
                         PacketMaskTable* mask_table,
                         uint8_t* packet_mask) {
  RTC_DCHECK_GT(num_media_packets, 0);
  RTC_DCHECK_GT(num_fec_packets, 0);
  RTC_DCHECK_LE(num_fec_packets, num_media_packets);
  RTC_DCHECK_LE(num_imp_packets, num_media_packets);
  RTC_DCHECK_GE(num_imp_packets, 0);

  const int num_mask_bytes = PacketMaskSize(num_media_packets);

  // Equal-protection for these cases.
  if (!use_unequal_protection || num_imp_packets == 0) {
    // Retrieve corresponding mask table directly:for equal-protection case.
    // Mask = (k,n-k), with protection factor = (n-k)/k,
    // where k = num_media_packets, n=total#packets, (n-k)=num_fec_packets.
    // 调用PacketMaskTable::LookUp
    rtc::ArrayView<const uint8_t> mask =
        mask_table->LookUp(num_media_packets, num_fec_packets);
    memcpy(packet_mask, &mask[0], mask.size());
  } else {  // UEP case
    UnequalProtectionMask(num_media_packets, num_fec_packets, num_imp_packets,
                          num_mask_bytes, packet_mask, mask_table);
  }  // End of UEP modification
}  // End of GetPacketMasks

由于没有使用unequal protection,所以直接进入PacketMaskTable::LookUp()

PacketMaskTable::PacketMaskTable(FecMaskType fec_mask_type,
                                 int num_media_packets)
    : table_(PickTable(fec_mask_type, num_media_packets)) {}

PacketMaskTable::~PacketMaskTable() = default;

rtc::ArrayView<const uint8_t> PacketMaskTable::LookUp(int num_media_packets,
                                                      int num_fec_packets) {
  RTC_DCHECK_GT(num_media_packets, 0);
  RTC_DCHECK_GT(num_fec_packets, 0);
  RTC_DCHECK_LE(num_media_packets, kUlpfecMaxMediaPackets);
  RTC_DCHECK_LE(num_fec_packets, num_media_packets);

  if (num_media_packets <= 12) {
    // 小于12直接查表
    return LookUpInFecTable(table_, num_media_packets - 1, num_fec_packets - 1);
  }
  int mask_length =
      static_cast<int>(PacketMaskSize(static_cast<size_t>(num_media_packets)));

  // Generate FEC code mask for {num_media_packets(M), num_fec_packets(N)} (use
  // N FEC packets to protect M media packets) In the mask, each FEC packet
  // occupies one row, each bit / coloumn represent one media packet. E.g. Row
  // A, Col/Bit B is set to 1, means FEC packet A will have protection for media
  // packet B.

  // 大于12,使用interleaved,也就是间隔包为一组(X % N)
  // Loop through each fec packet.
  for (int row = 0; row < num_fec_packets; row++) {
    // Loop through each fec code in a row, one code has 8 bits.
    // Bit X will be set to 1 if media packet X shall be protected by current
    // FEC packet. In this implementation, the protection is interleaved, thus
    // media packet X will be protected by FEC packet (X % N)
    for (int col = 0; col < mask_length; col++) {
      fec_packet_mask_[row * mask_length + col] =
          ((col * 8) % num_fec_packets == row && (col * 8) < num_media_packets
               ? 0x80
               : 0x00) |
          ((col * 8 + 1) % num_fec_packets == row &&
                   (col * 8 + 1) < num_media_packets
               ? 0x40
               : 0x00) |
          ((col * 8 + 2) % num_fec_packets == row &&
                   (col * 8 + 2) < num_media_packets
               ? 0x20
               : 0x00) |
          ((col * 8 + 3) % num_fec_packets == row &&
                   (col * 8 + 3) < num_media_packets
               ? 0x10
               : 0x00) |
          ((col * 8 + 4) % num_fec_packets == row &&
                   (col * 8 + 4) < num_media_packets
               ? 0x08
               : 0x00) |
          ((col * 8 + 5) % num_fec_packets == row &&
                   (col * 8 + 5) < num_media_packets
               ? 0x04
               : 0x00) |
          ((col * 8 + 6) % num_fec_packets == row &&
                   (col * 8 + 6) < num_media_packets
               ? 0x02
               : 0x00) |
          ((col * 8 + 7) % num_fec_packets == row &&
                   (col * 8 + 7) < num_media_packets
               ? 0x01
               : 0x00);
    }
  }
  return {&fec_packet_mask_[0],
          static_cast<size_t>(num_fec_packets * mask_length)};
}

PacketMaskTable::LookUp()主要:

  • 在media packet <= 12时,直接从系统预设的两张表查mask

  • 在media packet > 12时,采用间隔的方式进行分组fec


其中查表的过程有一些细节,以kPacketMaskRandomTbl表为例,其定义如下:

const uint8_t kPacketMaskRandomTbl[] = {
    12,
    1, kMaskRandom1_1,  
    2, kMaskRandom2_1, kMaskRandom2_2
    3, kMaskRandom3_1, kMaskRandom3_2, kMaskRandom3_3
    4, kMaskRandom4_1, kMaskRandom4_2, kMaskRandom4_3, kMaskRandom4_4
    kPacketMaskRandom5,
    kPacketMaskRandom6,
    kPacketMaskRandom7,
    kPacketMaskRandom8,
    kPacketMaskRandom9,
    kPacketMaskRandom10,
    kPacketMaskRandom11,
    kPacketMaskRandom12,
};

以上手动对其中一部分做了宏定义展开,最高12表示该表支持的原始包的长度为12,然后接下来的每一行,是每个原始包的对应数量下不同数量fec包的mask表,比如说kMaskRandom4_3是原始包数量为4fec包数量为3情况下的一个大小为(4x3)掩码表的起始,理解这个表后,下面的找表函数就好理解了

rtc::ArrayView<const uint8_t> LookUpInFecTable(const uint8_t* table,
                                               int media_packet_index,
                                               int fec_index) {
  RTC_DCHECK_LT(media_packet_index, table[0]);

  // Skip over the table size.
  const uint8_t* entry = &table[1];

  uint8_t entry_size_increment = 2;  // 0-16 are 2 byte wide, then changes to 6.

  // Hop over un-interesting array entries.
  // 跳过行
  for (int i = 0; i < media_packet_index; ++i) {
    if (i == 16)
      entry_size_increment = 6;
    uint8_t count = entry[0];
    ++entry;  // skip over the count.
    for (int j = 0; j < count; ++j) {
      entry += entry_size_increment * (j + 1);  // skip over the data.
    }
  }

  if (media_packet_index == 16)
    entry_size_increment = 6;

  RTC_DCHECK_LT(fec_index, entry[0]);
  ++entry;  // Skip over the size.

  // Find the appropriate data in the second dimension.

  // 跳过列
  for (int i = 0; i < fec_index; ++i)
    entry += entry_size_increment * (i + 1);  // skip over the data.

  // 确定目的掩码表的起始地址和size
  size_t size = entry_size_increment * (fec_index + 1);
  return {&entry[0], size};
}

2.2.4 webrtc中的Unequal Protection

非均等保护在webrtc中也实现了,但不是使用L1的方式,而采用了另一种方式,在指定的fec包数量下,分配更多的fec包给更重要的包,是通过掩码表的方式实现Unequal Protection的,这个东西实现在UnequalProtectionMask()

void UnequalProtectionMask(int num_media_packets,
                           int num_fec_packets,
                           int num_imp_packets,
                           int num_mask_bytes,
                           uint8_t* packet_mask,
                           PacketMaskTable* mask_table) {
  // Set Protection type and allocation
  // TODO(marpan): test/update for best mode and some combinations thereof.

  ProtectionMode mode = kModeOverlap;
  int num_fec_for_imp_packets = 0;

  if (mode != kModeBiasFirstPacket) {
    num_fec_for_imp_packets = SetProtectionAllocation(
        num_media_packets, num_fec_packets, num_imp_packets);
  }

  int num_fec_remaining = num_fec_packets - num_fec_for_imp_packets;
  // Done with setting protection type and allocation

  //
  // Generate sub_mask1
  //
  if (num_fec_for_imp_packets > 0) {
    ImportantPacketProtection(num_fec_for_imp_packets, num_imp_packets,
                              num_mask_bytes, packet_mask, mask_table);
  }

  //
  // Generate sub_mask2
  //
  if (num_fec_remaining > 0) {
    RemainingPacketProtection(num_media_packets, num_fec_remaining,
                              num_fec_for_imp_packets, num_mask_bytes, mode,
                              packet_mask, mask_table);
  }
}

这种unequal就主要体现在给重要保护的包分配多少fec包了,假设现在有m个原始包,n个fec包,m个原始包的前k个是需要重要保护的包,有三种分配方式:

kModeNoOverlap: 分配t个fec包给k个重要原始包做fec,剩下的fec包给剩下的普通原始包 (非重叠版)

kModeOverlap: 分配t个fec包给k个重要原始包做fec,剩下的fec包同时给普通版和重要包做fec (重叠版)

kModeBiasFirstPacket: 全部fec包都对第一个包做编码。

2.2.5 flexfec

ulpfec在某些丢包的情况下会导致无法恢复, 如下图所示,p2和p3如果在传输的过程中丢失了,就无法通过p1和f1去恢复了

此时如果再引入了列方向上的fec就能够比较好的解决这个问题,如下图,对列进行fec后,哪怕p2和p3同时丢失,

也能通过f5和f6对它进行复原, 这就是flexfec, 通过允许更直观灵活的组包fec应该是flexfec设计的目的,但丢包太多还是存在无法恢复的情况。

2.3 fec调整策略

fec的调整策略在细节上至今都没有看太明白,只能说一个大概;

fec_rate这个值为fec_packet_num / media_packet_num * 256,设定之后就能确定fec包的个数了,在2.2.3.2中也曾提到过。这个值是综合码率,丢包率得到的,具体的计算在VCMFecMethod::ProtectionFactor()

bool VCMFecMethod::ProtectionFactor(const VCMProtectionParameters* parameters) {
  // FEC PROTECTION SETTINGS: varies with packet loss and bitrate

  // No protection if (filtered) packetLoss is 0
  uint8_t packetLoss = rtc::saturated_cast<uint8_t>(255 * parameters->lossPr);
  if (packetLoss == 0) {
    _protectionFactorK = 0;
    _protectionFactorD = 0;
    return true;
  }

  // Parameters for FEC setting:
  // first partition size, thresholds, table pars, spatial resoln fac.

  // First partition protection: ~ 20%
  uint8_t firstPartitionProt = rtc::saturated_cast<uint8_t>(255 * 0.20);

  // Minimum protection level needed to generate one FEC packet for one
  // source packet/frame (in RTP sender)
  uint8_t minProtLevelFec = 85;

  // Threshold on packetLoss and bitRrate/frameRate (=average #packets),
  // above which we allocate protection to cover at least first partition.
  uint8_t lossThr = 0;
  uint8_t packetNumThr = 1;

  // Parameters for range of rate index of table.
  const uint8_t ratePar1 = 5;
  const uint8_t ratePar2 = 49;


  // webrtc提供了一个二维表kFecRateTable[rate][loss],用于查询不同码率丢包下的
  // fec_rate第一维是码率,第二维是丢包, 值是fec_rate

  // Spatial resolution size, relative to a reference size.
  // 计算当前分辨率和参考基分辨率 704 * 576的比
  float spatialSizeToRef = rtc::saturated_cast<float>(parameters->codecWidth *
                                                      parameters->codecHeight) /
                           (rtc::saturated_cast<float>(704 * 576));
  // resolnFac: This parameter will generally increase/decrease the FEC rate
  // (for fixed bitRate and packetLoss) based on system size.
  // Use a smaller exponent (< 1) to control/soften system size effect.
  // 使用分辨率比得到一个变换系数,用于下文对实际分辨率的变换
  // 使用这个变换系数是为了根据
  const float resolnFac = 1.0 / powf(spatialSizeToRef, 0.3f);

  // 通过parameter计算出每帧的码率
  const int bitRatePerFrame = BitsPerFrame(parameters);

  // Average number of packets per frame (source and fec):
  const uint8_t avgTotPackets = rtc::saturated_cast<uint8_t>(
      1.5f + rtc::saturated_cast<float>(bitRatePerFrame) * 1000.0f /
                 rtc::saturated_cast<float>(8.0 * _maxPayloadSize));

  // FEC rate parameters: for P and I frame
  uint8_t codeRateDelta = 0;
  uint8_t codeRateKey = 0;

  // Get index for table: the FEC protection depends on an effective rate.
  // The range on the rate index corresponds to rates (bps)
  // from ~200k to ~8000k, for 30fps
  // 将码率进行变换,根据每一帧码率变换出表的行
  const uint16_t effRateFecTable =
      rtc::saturated_cast<uint16_t>(resolnFac * bitRatePerFrame);
  uint8_t rateIndexTable = rtc::saturated_cast<uint8_t>(
      VCM_MAX(VCM_MIN((effRateFecTable - ratePar1) / ratePar1, ratePar2), 0));

  // Restrict packet loss range to 50:
  // current tables defined only up to 50%
  // 只支持50%的丢包
  if (packetLoss >= kPacketLossMax) {
    packetLoss = kPacketLossMax - 1;
  }
  // 计算查表得坐标
  uint16_t indexTable = rateIndexTable * kPacketLossMax + packetLoss;

  // Check on table index
  RTC_DCHECK_LT(indexTable, kFecRateTableSize);

  // Protection factor for P frame
  // 从kFecRateTable[rateIndexTable][packetLoss] 查到对应的factor
  codeRateDelta = kFecRateTable[indexTable];

  if (packetLoss > lossThr && avgTotPackets > packetNumThr) {
    // Set a minimum based on first partition size.
    if (codeRateDelta < firstPartitionProt) {
      codeRateDelta = firstPartitionProt;
    }
  }

  // Check limit on amount of protection for P frame; 50% is max.
  if (codeRateDelta >= kPacketLossMax) {
    codeRateDelta = kPacketLossMax - 1;
  }

  // For Key frame:
  // Effectively at a higher rate, so we scale/boost the rate
  // The boost factor may depend on several factors: ratio of packet
  // number of I to P frames, how much protection placed on P frames, etc.
  // 实际码率上I帧大于P帧,所以会根据I、P帧中的packet数之比进行扩大
  const uint8_t packetFrameDelta =
      rtc::saturated_cast<uint8_t>(0.5 + parameters->packetsPerFrame);
  const uint8_t packetFrameKey =
      rtc::saturated_cast<uint8_t>(0.5 + parameters->packetsPerFrameKey);
  const uint8_t boostKey = BoostCodeRateKey(packetFrameDelta, packetFrameKey);

  rateIndexTable = rtc::saturated_cast<uint8_t>(VCM_MAX(
      VCM_MIN(1 + (boostKey * effRateFecTable - ratePar1) / ratePar1, ratePar2),
      0));
  uint16_t indexTableKey = rateIndexTable * kPacketLossMax + packetLoss;

  indexTableKey = VCM_MIN(indexTableKey, kFecRateTableSize);

  // Check on table index
  assert(indexTableKey < kFecRateTableSize);

  // Protection factor for I frame
  codeRateKey = kFecRateTable[indexTableKey];

  // Boosting for Key frame.
  int boostKeyProt = _scaleProtKey * codeRateDelta;
  if (boostKeyProt >= kPacketLossMax) {
    boostKeyProt = kPacketLossMax - 1;
  }

  // Make sure I frame protection is at least larger than P frame protection,
  // and at least as high as filtered packet loss.
  codeRateKey = rtc::saturated_cast<uint8_t>(
      VCM_MAX(packetLoss, VCM_MAX(boostKeyProt, codeRateKey)));

  // Check limit on amount of protection for I frame: 50% is max.
  if (codeRateKey >= kPacketLossMax) {
    codeRateKey = kPacketLossMax - 1;
  }

  // 设置I帧和P帧的fec rate
  _protectionFactorK = codeRateKey;
  _protectionFactorD = codeRateDelta;

  // Generally there is a rate mis-match between the FEC cost estimated
  // in mediaOpt and the actual FEC cost sent out in RTP module.
  // This is more significant at low rates (small # of source packets), where
  // the granularity of the FEC decreases. In this case, non-zero protection
  // in mediaOpt may generate 0 FEC packets in RTP sender (since actual #FEC
  // is based on rounding off protectionFactor on actual source packet number).
  // The correction factor (_corrFecCost) attempts to corrects this, at least
  // for cases of low rates (small #packets) and low protection levels.
  // 由于fec rate的预估和实际的fec的使用存在出入,这一点在低码率的时候,fec rate
  // 很小时影响很大,在这种情况下,预测一个非0的fec rate,可能导致rtp sender
  // 产生-个fec包(因为fec包的实际计算会使用四舍五入),所以引入了_corrFecCost 0.5
  // 去做一个上修正
  float numPacketsFl =
      1.0f + (rtc::saturated_cast<float>(bitRatePerFrame) * 1000.0 /
                  rtc::saturated_cast<float>(8.0 * _maxPayloadSize) +
              0.5);

  const float estNumFecGen =
      0.5f +
      rtc::saturated_cast<float>(_protectionFactorD * numPacketsFl / 255.0f);

  // We reduce cost factor (which will reduce overhead for FEC and
  // hybrid method) and not the protectionFactor.
  _corrFecCost = 1.0f;
  // 对于单packet,预估系数小于产生一个fec包最小系数,预估产生的fec包小于[0.9, 1.1],引入0.5上修正
  if (estNumFecGen < 1.1f && _protectionFactorD < minProtLevelFec) {
    _corrFecCost = 0.5f;
  }

  // 小于0.9 无需上修正
  if (estNumFecGen < 0.9f && _protectionFactorD < minProtLevelFec) {
    _corrFecCost = 0.0f;
  }

  // DONE WITH FEC PROTECTION SETTINGS
  return true;
}

主要:

  • webrtc提供了一个二维表kFecRateTable[rate][loss],用于查询不同码率丢包下的fec_rate, 第一维是码率,第二维是丢包率, 值是fec_rate
  • 计算P帧的码率,基于参考做一个变换(这个变换暂时理解不了),根据这个变换后的码率作为第一码率,丢包率作为第二维查表得到fec_rate
  • 鉴于I帧的码率比P帧大,使用I帧和P帧的packet数的倍数比,扩大P帧的第一维坐标作为I帧第一维坐标去查表,得到fec_rate
  • 由于rtp module实际使用fec_rate对算出来的fec包数会做四舍五入(这点也很奇怪,应该是上取整),在预估要产生一个fec包,但是预估的fec_rate无法无法达到单packet是最小能产生一个fec包的值,引入一个上修正系数_corrFecCost =0.5(但实际上是没用上的,因为rtp模块做的是上取整吧)

3.Ref

1.ULPFEC在WebRTC中的实现 -- weizhenwei

2.webrtc QOS方法二.2(ulpfec rfc5109简介)--CrystalShaw

3.RFC2198 RED

4.WebRTC FEC 冗余策略

5.RFC5109 RTP Payload Format for Generic Forward Error Correction

6. RTP Payload Format for Flexible Forward Error Correction (FlexFEC)

原文地址:https://www.cnblogs.com/ishen/p/15333271.html