流媒体弱网优化之路(FEC+mediasoup)——FEC引入的问题收尾

13 篇文章 36 订阅
9 篇文章 10 订阅


——
我正在的github给大家开发一个用于做实验的项目 —— github.com/qw225967/Bifrost

目标:可以让大家熟悉各类Qos能力、带宽估计能力,提供每个环节关键参数调节接口并实现一个json全配置,提供全面的可视化算法观察能力。

欢迎大家使用
——

一、前情提要

  前面有一篇文章——流媒体弱网优化之路(FEC+mediasoup)——mediasoup的Nack优化以及FEC引入。介绍了mediasoup引入FEC并且进行了测试,有很多朋友比较感兴趣。在后续的测试过程中我发现一些问题,今天来分析一下问题的产生原因,尝试解答一下大家的疑惑。

二、相关知识

  之前的文章篇幅有限,今天我们详细介绍一下整个FlexFEC的封包原理和解包原理,让大家可以更清楚地理解之前我提到的问题出在哪,下面分两段来介绍 ——在我的github上放了纯封包解包的测试demo,有兴趣可以下下来玩玩,只有mac版的(可以直接调起来下断点)

2.1 ulpfec封包格式

  我们知道FEC在webrtc中的实现有两种,一个是基于通一条流red封装的ulpfec、一个是独立流flexfec。在我集成flexfec时,底层还是用的ulpfec的算法,因此我一并进行简单介绍(具体的代码就不说了,后面集中说代码)。
  直接看一下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
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|F|   block PT  |  timestamp offset         |   block length    | : block header
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                             block                             | : data block
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

  它是在rtp头之下的,由 block header 和 block data 两部分组成。很好理解,把它和rtp头画一起就是这样:

 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)                       +
/                                                               /
+                                                               +
|                                                               |
+                                                               +
|                                                               |
+                                               +---------------+
|                                               |

  我们来看看它 block header 里的字段的意义:
  F:标识是否存在多个red封装的标志:
    1.为1时标识后面是否有别的block,如果有则会出现多个同样的header;
    2.为0时,标识后面不会再有这个header line,同时最后block的timestamp和block length能够通过rtp推断出,所以会省略。

  block PT: block内容中的rtp payload type(也就是包原始的数据pt);
  block length: block的大小;
  timestamp offset: block 的timestam 相对于rtp header timestamp 的偏移量,如:red冗余的包序号是3,而rtp包的序号是5,那么3恢复出来的包头就需要通过该字段去计算。

2.2 flexfec封包格式

  而对于flexfec除了有单独的一条流标识之外,还有seq也是独立的,同时整个协议的格式也进行了重新设计。在rfc8627中定义的flexfec的格式如下:

 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
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|R|F|P|X|  CC   |M| PT recovery | ...varies depending on R/F... |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                                                               |
|                 ...varies depending on R/F...                 |
|                                                               |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
:                Repair Payload follows FEC header              :
:                                                               :

  在这里我先解释字段的意思:
  R:这个字段为了标识该包是否是重传包,如果是重传包则必须把R置为1,对于正常的FEC包则必须为0;
  F:表示为FEC包,当 R 位为 1时, F 位必须设置为 0 。如果R 位为 1 还把F置为1,那么这个包不会被处理。
  P、X、CC、M 和 PT 字段用于确定恢复数据包的相应包头字段信息(事实上就是RTP头对应的几个标识的意思),换算过程比较复杂我们简单凝练了一下:

假设在整个数据集T中丢失了一个数据包,我们需要恢复丢失序号包的包头内容,则需进行以下操作:

1.根据数据集T中其他已接收到数据包的包头前64位,换算出一个80位的字符串;
2.取出FEC包中前80位作为另一个字符串;
3.将两个字符串进行异或操作,来得到恢复出来的rtp包头。
4.根据恢复出来的字符串依次放入到rtp头中:
	V填入2(恢复字符串依次便宜对应大小);
	P为Padding位填入恢复字符串的对应内容;
	X为Extension位填入恢复字符串的对应内容;
	CC字段同理;
	MM字段同理;
	PT字段同理。

这样,整个丢失的RTP包头就实现了完整恢复了。

  上面介绍完了前面几个协议字段的意义,后面就跟着flexfec具体标识内容的部分。flexfec的头有下面四种变体,实现了它所需求的灵活性:

+---+---+-----------------------------------------------------+
| R | F | FEC header variant                                  |
+---+---+-----------------------------------------------------+
| 0 | 0 | Flexible FEC Mask fields indicate source packets    |
| 0 | 1 | Fixed FEC L/D (cols/rows) indicate source packets   |
| 1 | 0 | Retransmission of a single source packet            |
| 1 | 1 | Reserved for future use, MUST NOT send, MUST ignore |
+---+---+-----------------------------------------------------+

  上面说的变体大家可以看看英文的描述,其实对应以下形式(R=1 F=1的形式会被忽略,不展示):

R=0 F=0

 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
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|0|0|P|X|  CC   |M| PT recovery |        length recovery        |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                          TS recovery                          |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|           SN base_i           |k|          Mask [0-14]        |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|k|                   Mask [15-45] (optional)                   |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                     Mask [46-109] (optional)                  |
|                                                               |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|   ... next SN base and Mask for CSRC_i in CSRC list ...       |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
:                Repair Payload follows FEC header              :
:                                                               :
R=0 F=1

 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
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|0|1|P|X|  CC   |M| PT recovery |         length recovery       |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                          TS recovery                          |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|           SN base_i           |  L (columns)  |    D (rows)   |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|    ... next SN base and L/D for CSRC_i in CSRC list ...       |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
:                Repair Payload follows FEC header              :
:                                                               :
R=1 F=0
 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
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|1|0|P|X|  CC   |M| Payload Type|        Sequence Number        |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                           Timestamp                           |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                              SSRC                             |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
:           Retransmission Payload follows FEC header           :
:                                                               :

三、冗余原理

  由于我们使用的代码是webrtc85和webrtc77,下层仍让使用的是ulpfec,因此本篇不对flexfec的冗余原理做详细解释,而只详细介绍ulpfec的冗余原理。

FEC主要用于提前冗余来抵抗丢包,在当前的网络环境中,最常见的就是随性性的丢包以及聚簇性的丢包。

网络中造成丢包的场景主要有:
	1.拥塞: 这个比较好理解, 就是交换机/路由器中多个端口向一个端口发送数据包, 最后导致数据包数量超过了这个端口的缓冲区的容量, 从而导致丢包;
	2.持续性的黑洞: 在交换机中, 和特定模式匹配的所有数据包都会丢失. 这种现象的成因是匹配-动作表的损毁或者控制器造成的转发规则配置错误;
	3.暂时性的黑洞: 在交换机中, 在一段较短的时间内, 和特定模式匹配的所有数据包都会丢失. 这种现象的成因是, 转发规则的非原子化更新, 或者网络级别的转发规则更新不一致;
	4.随机丢包: 交换机随机地但是持续性地丢包. 这种现象的成因是可能是线卡松动, 或者链路折损。

  也就是说基本可以抽象为随机丢包和聚簇丢包两种,而WebRTC也是根据这两种丢包方式进行了冗余。这两种方式在帧比较小的情况下冗余的结果相似,只有在帧比较大的情况下才会出现不同。
在这里插入图片描述

3.1 大帧随机丢包

  随机丢包伴随的特质是:随机、不连续。因此该方案的冗余策略多为不相邻的包进行冗余,来避免离散丢包造成的恢复率下降问题;
  在代码中,WebRTC是根据每一帧的大小进行冗,固定码表最大只能从12个包进行。具体的文件是:modules/rtp_rtcp/source/fec_private_tables_random.h 和 modules/rtp_rtcp/source/fec_private_tables_random.cc。怎么理解呢?我直接举个举个例子来串:

假设一个普通帧有7个RTP组成,那么我们需要根据具体的包大小以及他的冗余度去查一个对应的表:
假设我们需要 7 个包产生 4 个fec包那么对应的码表就是

#define kMaskRandom7_4 \
  0xd0, 0x00, \
  0xaa, 0x00, \
  0x64, 0x00, \
  0x1c, 0x00

换算成二进制就是:
11010000 00000000
10101010 00000000
01100100 00000000
00011100 00000000

生成几个fec包就会有几行、而1的位置则代表对应的RTP包会被放进去进行冗余计算。
11010000 对应包的序号是:1、2、4
10101010对应的包序号是:1、3、5、7
01100100对应的包序号是:2、3、6
00011100对应的包序号是:4、5、6

  这样就生成了我们的FEC包,在对端如果接到的包在保护内就能恢复出来。
在这里插入图片描述

3.2 大帧聚簇丢包

  对于聚簇丢包,特征就是:连续性。那么我们需要尽量分开去对数据进行冗余,这点在帧比较小的情况下看不出来,但是一但帧比较大,RTP数据多就体现的比较明显了。

假设一个普通帧有7个RTP组成,那么我们需要根据具体的包大小以及他的冗余度去查一个对应的表:
假设我们需要 7 个包产生 4 个fec包那么对应的码表就是

#define kMaskBursty7_4 \
  0x38, 0x00, \
  0x8a, 0x00, \
  0xc4, 0x00, \
  0x62, 0x00

换算成二进制就是:
00111000 00000000
10001010 00000000
11000100 00000000
01100010 00000000

生成几个fec包就会有几行、而1的位置则代表对应的RTP包会被放进去进行冗余计算。
00111000 对应包的序号是:3、4、5
10001010对应的包序号是:1、5、7
11000100对应的包序号是:1、2、6
01100010对应的包序号是:2、3、7

  这样就生成了我们的FEC包,在对端如果接到的包在保护内就能恢复出来。
在这里插入图片描述
  两个看起来差了不少,聚簇的确实看起来更加倾向聚合的形状、而随机的更离散。

四、代码导读

4.1 封包调用

  我们将flexfec抽离出来进行封包,使用的是 FlexfecSender 这个类进行测试。

// modules/rtp_rtcp/include/flexfec_sender.h

class FlexfecSender : public VideoFecGenerator {
 public:
  FlexfecSender(int payload_type,
                uint32_t ssrc,
                uint32_t protected_media_ssrc,
                const std::string& mid,
                const std::vector<webrtc::RtpExtension>& rtp_header_extensions,
                rtc::ArrayView<const webrtc::RtpExtensionSize> extension_sizes,
                const webrtc::RtpState* rtp_state,
                webrtc::Clock* clock);
  ~FlexfecSender();

  FecType GetFecType() const override {
    return VideoFecGenerator::FecType::kFlexFec;
  }
  absl::optional<uint32_t> FecSsrc() override { return ssrc_; }

  // Sets the FEC rate, max frames sent before FEC packets are sent,
  // and what type of generator matrices are used.
  void SetProtectionParameters(const webrtc::FecProtectionParams& delta_params,
                               const webrtc::FecProtectionParams& key_params) override;

  // Adds a media packet to the internal buffer. When enough media packets
  // have been added, the FEC packets are generated and stored internally.
  // These FEC packets are then obtained by calling GetFecPackets().
  void AddPacketAndGenerateFec(const RtpPacketToSend& packet) override;

  // Returns generated FlexFEC packets.
  std::vector<std::unique_ptr<RtpPacketToSend>> GetFecPackets() override;

  // Returns the overhead, per packet, for FlexFEC.
  size_t MaxPacketOverhead() const override;

  webrtc::DataRate CurrentFecRate() const override;

  // Only called on the VideoSendStream queue, after operation has shut down.
  absl::optional<webrtc::RtpState> GetRtpState() override;

 private:
  // Utility.
  webrtc::Clock* const clock_;
  webrtc::Random random_;
  int64_t last_generated_packet_ms_;

  // Config.
  const int payload_type_;
  const uint32_t timestamp_offset_;
  const uint32_t ssrc_;
  const uint32_t protected_media_ssrc_;
  // MID value to send in the MID header extension.
  const std::string mid_;
  // Sequence number of next packet to generate.
  uint16_t seq_num_;

  // Implementation.
  webrtc85::UlpfecGenerator ulpfec_generator_;
  const webrtc::RtpHeaderExtensionMap rtp_header_extension_map_;
  const size_t header_extensions_size_;

  rtc::CriticalSection crit_;
  webrtc::RateStatistics fec_bitrate_ RTC_GUARDED_BY(crit_);
};

  其中比较重要的两个接口函数就是:AddPacketAndGenerateFec、GetFecPackets。

// modules/rtp_rtcp/include/flexfec_sender.cc

void FlexfecSender::AddPacketAndGenerateFec(const RtpPacketToSend& packet) {
  // TODO(brandtr): Generalize this SSRC check when we support multistream
  // protection.
  RTC_DCHECK_EQ(packet.Ssrc(), protected_media_ssrc_);
  ulpfec_generator_.AddPacketAndGenerateFec(packet); // 直接调用ulpfec
}

std::vector<std::unique_ptr<RtpPacketToSend>> FlexfecSender::GetFecPackets() {
  RTC_CHECK_RUNS_SERIALIZED(&ulpfec_generator_.race_checker_);
  std::vector<std::unique_ptr<RtpPacketToSend>> fec_packets_to_send;
  fec_packets_to_send.reserve(ulpfec_generator_.generated_fec_packets_.size());
  size_t total_fec_data_bytes = 0;
  for (const auto* fec_packet : ulpfec_generator_.generated_fec_packets_) {

  // 按上述格式封装每个fec包头
    std::unique_ptr<RtpPacketToSend> fec_packet_to_send(
        new RtpPacketToSend(&rtp_header_extension_map_));
    fec_packet_to_send->set_packet_type(
        webrtc::RtpPacketMediaType::kForwardErrorCorrection);
    fec_packet_to_send->set_allow_retransmission(false);

    // RTP header.
    fec_packet_to_send->SetMarker(false);
    fec_packet_to_send->SetPayloadType(payload_type_);
    fec_packet_to_send->SetSequenceNumber(seq_num_++);
    fec_packet_to_send->SetTimestamp(
        timestamp_offset_ +
        static_cast<uint32_t>(kMsToRtpTimestamp *
                              clock_->TimeInMilliseconds()));
    // Set "capture time" so that the TransmissionOffset header extension
    // can be set by the RTPSender.
    fec_packet_to_send->set_capture_time_ms(clock_->TimeInMilliseconds());
    fec_packet_to_send->SetSsrc(ssrc_);
    // Reserve extensions, if registered. These will be set by the RTPSender.
    fec_packet_to_send->ReserveExtension<AbsoluteSendTime>();
    fec_packet_to_send->ReserveExtension<TransmissionOffset>();
    fec_packet_to_send->ReserveExtension<TransportSequenceNumber>();
    // Possibly include the MID header extension.
    if (!mid_.empty()) {
      // This is a no-op if the MID header extension is not registered.
      fec_packet_to_send->SetExtension<RtpMid>(mid_);
    }

    // RTP payload.
    uint8_t* payload =
        fec_packet_to_send->AllocatePayload(fec_packet->data.size());
    memcpy(payload, fec_packet->data.cdata(), fec_packet->data.size());

    total_fec_data_bytes += fec_packet_to_send->size();
    fec_packets_to_send.push_back(std::move(fec_packet_to_send));
  }

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

  int64_t now_ms = clock_->TimeInMilliseconds();
  if (!fec_packets_to_send.empty() &&
      now_ms - last_generated_packet_ms_ > kPacketLogIntervalMs) {
//    RTC_LOG(LS_VERBOSE) << "Generated " << fec_packets_to_send.size()
//                        << " FlexFEC packets with payload type: "
//                        << payload_type_ << " and SSRC: " << ssrc_ << ".";
    last_generated_packet_ms_ = now_ms;
  }

  rtc::CritScope cs(&crit_);
  fec_bitrate_.Update(total_fec_data_bytes, now_ms);

  return fec_packets_to_send;
}

  在ulpfec的AddPacketAndGenerateFec函数中,EncodeFec为主要函数。

//  modules/rtp_rtcp/source/ulpfec_generator.cc

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

  if (media_packets_.empty()) {
    rtc::CritScope cs(&crit_);
    if (pending_params_) {
      current_params_ = *pending_params_;
      pending_params_.reset();

      if (CurrentParams().fec_rate > kHighProtectionThreshold) {
        min_num_media_packets_ = kMinMediaPackets;
      } else {
        min_num_media_packets_ = 1;
      }
    }

    keyframe_in_process_ = packet.is_key_frame();
  }
  RTC_DCHECK_EQ(packet.is_key_frame(), keyframe_in_process_);

  bool complete_frame = false;
  const bool marker_bit = packet.Marker();
  if (media_packets_.size() < webrtc::kUlpfecMaxMediaPackets) {
    // 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(), webrtc::kRtpHeaderSize);
    last_media_packet_ = packet;
  }

  // 根据marker位来确定是否是一个完整的帧
  if (marker_bit) {
    ++num_protected_frames_;
    complete_frame = true;
  }

  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_->EncodeFec(media_packets_, params.fec_rate, kNumImportantPackets,
                    kUseUnequalProtection, params.fec_mask_type,
                    &generated_fec_packets_);
    if (generated_fec_packets_.empty()) {
      ResetState();
    }
  }
}
// modules/rtp_rtcp/source/forward_error_correction.cc

int ForwardErrorCorrection::EncodeFec(const PacketList& media_packets,
                                      uint8_t protection_factor,
                                      int num_important_packets,
                                      bool use_unequal_protection,
                                      webrtc::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() < webrtc::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.
  // 根据保护因子和媒体包来确定fec包的个数
  int num_fec_packets = NumFecPackets(num_media_packets, protection_factor);
  if (num_fec_packets == 0) {
    return 0;
  }
  
  // 每个fec包都会进入到产生队列中
  for (int i = 0; i < num_fec_packets; ++i) {
    generated_fec_packets_[i].data.EnsureCapacity(IP_PACKET_SIZE);
    memset(generated_fec_packets_[i].data.data(), 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]);
  }

  // 这个位置就是根据我们的网络类型和媒体包数去查对应的表
  webrtc::internal::PacketMaskTable mask_table(fec_mask_type, num_media_packets);
  packet_mask_size_ = webrtc::internal::PacketMaskSize(num_media_packets);
  memset(packet_masks_, 0, num_fec_packets * packet_mask_size_);
  
  // 根据不同的表产生不同的mask掩码 
  webrtc::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_ = webrtc::internal::PacketMaskSize(num_mask_bits);

  // Write FEC packets to |generated_fec_packets_|.
  // 产生fec数据
  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;
}

...

int ForwardErrorCorrection::NumFecPackets(int num_media_packets,
                                          int protection_factor) {
  // Result in Q0 with an unsigned round.
  // 该算法基本保证最大48g
  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;
}

...

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);

  // 根据帧大小进行mask size分配,分两种:2bit和6bit。
  const int num_mask_bytes = PacketMaskSize(num_media_packets);

  // Equal-protection for these cases.
  if (!use_unequal_protection || num_imp_packets == 0) {
   // 默认没使用非均衡保护,则根据mask表保护数据
    // 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.
    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

...

size_t PacketMaskSize(size_t num_sequence_numbers) {
  RTC_DCHECK_LE(num_sequence_numbers, 8 * kUlpfecPacketMaskSizeLBitSet);
  // 一旦超过16
  if (num_sequence_numbers > 8 * kUlpfecPacketMaskSizeLBitClear) {
    return kUlpfecPacketMaskSizeLBitSet;
  }
  return kUlpfecPacketMaskSizeLBitClear;
}

...

// 使用非均衡保护(默认是关闭的)
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): mytest/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);
  }
}

4.2 解包调用

  解包的过程则需要使用FlexfecReceiver。

// modules/rtp_rtcp/include/flexfec_receiver.h

class FlexfecReceiver {
 public:
  FlexfecReceiver(uint32_t ssrc,
                  uint32_t protected_media_ssrc,
                  RecoveredPacketReceiver* recovered_packet_receiver);
  FlexfecReceiver(Clock* clock,
                  uint32_t ssrc,
                  uint32_t protected_media_ssrc,
                  RecoveredPacketReceiver* recovered_packet_receiver);
  ~FlexfecReceiver();

  // Inserts a received packet (can be either media or FlexFEC) into the
  // internal buffer, and sends the received packets to the erasure code.
  // All newly recovered packets are sent back through the callback.
  void OnRtpPacket(const RtpPacketReceived& packet);

  // Returns a counter describing the added and recovered packets.
  FecPacketCounter GetPacketCounter() const;

  // Protected to aid testing.
 protected:
  std::unique_ptr<ForwardErrorCorrection::ReceivedPacket> AddReceivedPacket(
      const RtpPacketReceived& packet);
  void ProcessReceivedPacket(
      const ForwardErrorCorrection::ReceivedPacket& received_packet);

 private:
  // Config.
  const uint32_t ssrc_;
  const uint32_t protected_media_ssrc_;

  // Erasure code interfacing and callback.
  std::unique_ptr<ForwardErrorCorrection> erasure_code_
      RTC_GUARDED_BY(sequence_checker_);
  ForwardErrorCorrection::RecoveredPacketList recovered_packets_
      RTC_GUARDED_BY(sequence_checker_);
  RecoveredPacketReceiver* const recovered_packet_receiver_;

  // Logging and stats.
  Clock* const clock_;
  int64_t last_recovered_packet_ms_ RTC_GUARDED_BY(sequence_checker_);
  FecPacketCounter packet_counter_ RTC_GUARDED_BY(sequence_checker_);

  SequenceChecker sequence_checker_;
};

  主要逻辑在OnRtpPacket函数中。

// modules/rtp_rtcp/include/flexfec_receiver.cc

void FlexfecReceiver::OnRtpPacket(const RtpPacketReceived& packet) {
  RTC_DCHECK_RUN_ON(&sequence_checker_);

  // If this packet was recovered, it might be originating from
  // ProcessReceivedPacket in this object. To avoid lifetime issues with
  // |recovered_packets_|, we therefore break the cycle here.
  // This might reduce decoding efficiency a bit, since we can't disambiguate
  // recovered packets by RTX from recovered packets by FlexFEC.
  if (packet.recovered())
    return;

  // 把接到的包进行记录
  std::unique_ptr<ForwardErrorCorrection::ReceivedPacket> received_packet =
      AddReceivedPacket(packet);
  if (!received_packet)
    return;
  
  // 尝试恢复数据 
  ProcessReceivedPacket(*received_packet);
}

...

std::unique_ptr<ForwardErrorCorrection::ReceivedPacket>
FlexfecReceiver::AddReceivedPacket(const RtpPacketReceived& packet) {
  RTC_DCHECK_RUN_ON(&sequence_checker_);

  // RTP packets with a full base header (12 bytes), but without payload,
  // could conceivably be useful in the decoding. Therefore we check
  // with a non-strict inequality here.
  RTC_DCHECK_GE(packet.size(), kRtpHeaderSize);

  // Demultiplex based on SSRC, and insert into erasure code decoder.
  std::unique_ptr<ForwardErrorCorrection::ReceivedPacket> received_packet(
      new ForwardErrorCorrection::ReceivedPacket());
  received_packet->seq_num = packet.SequenceNumber();
  received_packet->ssrc = packet.Ssrc();
  // 这段代码中 && packet.PayloadType() == 113 是因为我们mediasoup引入fec时使用同一条流,这里会引入一个bug导致无法解包
  // 只有fec包能进入这里
  if (received_packet->ssrc == ssrc_ && packet.PayloadType() == 113) {
    // This is a FlexFEC packet.
    if (packet.payload_size() < kMinFlexfecHeaderSize) {
//      RTC_LOG(LS_WARNING) << "Truncated FlexFEC packet, discarding.";
      return nullptr;
    }
    received_packet->is_fec = true;
    ++packet_counter_.num_fec_packets;

    // Insert packet payload into erasure code.
    // TODO(brandtr): Remove this memcpy when the FEC packet classes
    // are using COW buffers internally.
    received_packet->pkt = rtc::scoped_refptr<ForwardErrorCorrection::Packet>(
        new ForwardErrorCorrection::Packet());
    auto payload = packet.payload();
    memcpy(received_packet->pkt->data, payload.data(), payload.size());
    received_packet->pkt->length = payload.size();
  } else {
    // This is a media packet, or a FlexFEC packet belonging to some
    // other FlexFEC stream.
    if (received_packet->ssrc != protected_media_ssrc_) {
      return nullptr;
    }
    received_packet->is_fec = false;

    // Insert entire packet into erasure code.
    received_packet->pkt = rtc::scoped_refptr<ForwardErrorCorrection::Packet>(
        new ForwardErrorCorrection::Packet());
    // Create a copy and fill with zeros all mutable extensions.
    packet.CopyAndZeroMutableExtensions(received_packet->pkt->data);
    received_packet->pkt->length = packet.size();
  }

  ++packet_counter_.num_packets;

  return received_packet;
}

...

void FlexfecReceiver::ProcessReceivedPacket(
    const ForwardErrorCorrection::ReceivedPacket& received_packet) {
  RTC_DCHECK_RUN_ON(&sequence_checker_);

  // Decode.
  erasure_code_->DecodeFec(received_packet, &recovered_packets_);

  // Return recovered packets through callback.
  for (const auto& recovered_packet : recovered_packets_) {
    RTC_CHECK(recovered_packet);
    if (recovered_packet->returned) {
      continue;
    }
    ++packet_counter_.num_recovered_packets;
    // Set this flag first, since OnRecoveredPacket may end up here
    // again, with the same packet.
    recovered_packet->returned = true;
    RTC_CHECK(recovered_packet->pkt);
    recovered_packet_receiver_->OnRecoveredPacket(
        recovered_packet->pkt->data, recovered_packet->pkt->length);
    // Periodically log the incoming packets.
    int64_t now_ms = clock_->TimeInMilliseconds();
    if (now_ms - last_recovered_packet_ms_ > kPacketLogIntervalMs) {
      uint32_t media_ssrc =
          ForwardErrorCorrection::ParseSsrc(recovered_packet->pkt->data);
//      RTC_LOG(LS_VERBOSE) << "Recovered media packet with SSRC: " << media_ssrc
//                          << " from FlexFEC stream with SSRC: " << ssrc_ << ".";
      last_recovered_packet_ms_ = now_ms;
    }
  }
}

  上述过程中,最主要的逻辑是在DecodeFec函数中:

void ForwardErrorCorrection::DecodeFec(const ReceivedPacket& received_packet,
                                       RecoveredPacketList* recovered_packets) {
  RTC_DCHECK(recovered_packets);
	
  // 根据fec包头获得最大包数
  const size_t max_media_packets = fec_header_reader_->MaxMediaPackets();
//  const size_t max_media_packets = 50;
  if (recovered_packets->size() == max_media_packets) {
    const RecoveredPacket* back_recovered_packet =
        recovered_packets->back().get();

    // 这里的 && !received_packet.is_fec 代码原版没有,因为测试使用同一条ssrc需要在这里区分fec包,否则会触发异常情况导致恢复队列重置
    if (received_packet.ssrc == back_recovered_packet->ssrc && !received_packet.is_fec) {
      const unsigned int seq_num_diff =
          MinDiff(received_packet.seq_num, back_recovered_packet->seq_num);
      if (seq_num_diff > max_media_packets) {
        // A big gap in sequence numbers. The old recovered packets
        // are now useless, so it's safe to do a reset.
//        RTC_LOG(LS_INFO) << "Big gap in media/ULPFEC sequence numbers. No need "
//                            "to keep the old packets in the FEC buffers, thus "
//                            "resetting them.";
        ResetState(recovered_packets);
      }
    }
  }

  // 根据接收的包类型分别插入不同的队列中
  InsertPacket(received_packet, recovered_packets);
  
  // 尝试恢复数据
  AttemptRecovery(recovered_packets);
}

...

void ForwardErrorCorrection::InsertPacket(
    const ReceivedPacket& received_packet,
    RecoveredPacketList* recovered_packets) {
  // Discard old FEC packets such that the sequence numbers in
  // |received_fec_packets_| span at most 1/2 of the sequence number space.
  // This is important for keeping |received_fec_packets_| sorted, and may
  // also reduce the possibility of incorrect decoding due to sequence number
  // wrap-around.
  // TODO(marpan/holmer): We should be able to improve detection/discarding of
  // old FEC packets based on timestamp information or better sequence number
  // thresholding (e.g., to distinguish between wrap-around and reordering).
  // 此处 && received_packet.is_fec的代码原先没有,是为了解决同ssrc问题
  if (!received_fec_packets_.empty() &&
  received_packet.ssrc == received_fec_packets_.front()->ssrc && received_packet.is_fec) {
    // It only makes sense to detect wrap-around when |received_packet|
    // and |front_received_fec_packet| belong to the same sequence number
    // space, i.e., the same SSRC. This happens when |received_packet|
    // is a FEC packet, or if |received_packet| is a media packet and
    // RED+ULPFEC is used.
    auto it = received_fec_packets_.begin();
    while (it != received_fec_packets_.end()) {
      uint16_t seq_num_diff = MinDiff(received_packet.seq_num, (*it)->seq_num);
      if (seq_num_diff > 0x3fff) {
        it = received_fec_packets_.erase(it);
      } else {
        // No need to keep iterating, since |received_fec_packets_| is sorted.
        break;
      }
    }
  }
  // 根据不同数据类型插入不同队列,媒体包直接当做已恢复包
  if (received_packet.is_fec) {
    InsertFecPacket(*recovered_packets, received_packet);
  } else {
    InsertMediaPacket(recovered_packets, received_packet);
  }
  
  // 清除旧数据
  DiscardOldRecoveredPackets(recovered_packets);
}

...

void ForwardErrorCorrection::AttemptRecovery(
    RecoveredPacketList* recovered_packets) {
  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) {
//      std::cout << "test fec packet ssrc:" << (*fec_packet_it)->ssrc
//                << ", seq:" << (*fec_packet_it)->seq_num
//                << ", seq_base:" <<(*fec_packet_it)->seq_num_base
//                << ", packets_missing:" << packets_missing << std::endl;

      // 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();
      // Add recovered packet to the list of recovered packets and update any
      // FEC packets covering this packet with a pointer to the data.
      // TODO(holmer): Consider replacing this with a binary search for the
      // right position, and then just insert the new packet. Would get rid of
      // the sort.
      recovered_packets->push_back(std::move(recovered_packet));
      recovered_packets->sort(SortablePacket::LessThan());
      UpdateCoveringFecPackets(*recovered_packet_ptr);
      DiscardOldRecoveredPackets(recovered_packets);
      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_packet_it = received_fec_packets_.begin();
    } else if (packets_missing == 0) {
//      std::cout << "test fec packet ssrc:" << (*fec_packet_it)->ssrc
//      << ", seq:" << (*fec_packet_it)->seq_num
//      << ", seq_base:" <<(*fec_packet_it)->seq_num_base
//      << ", packets_missing:" << packets_missing << std::endl;
      // Either all protected packets arrived or have been recovered. We can
      // discard this FEC packet.
      fec_packet_it = received_fec_packets_.erase(fec_packet_it);
    } else {
//      std::cout << "test fec packet ssrc:" << (*fec_packet_it)->ssrc
//      << ", seq:" << (*fec_packet_it)->seq_num
//      << ", seq_base:" <<(*fec_packet_it)->seq_num_base
//      << ", packets_missing:" << packets_missing << std::endl;
      fec_packet_it++;
    }
  }
}

五、总结与问题解答

  本文主要简单解释了一下FEC的编码和解码的实现,同时展示了经过我修改的webrtc源码。之前文章中引入fec的部分还存在问题。主要是因为使用了同一条ssrc导致flexfec内部出现异常。我们需要在解码的部分所有判断ssrc的地方都区别一下media包和fec包才行。如果不区别这个问题会造成小部分fec包无法恢复,但大部分都是正常的。

5.1 问题解答

问题1:在测试过程中,发现效果不理想,挨个数据信息查看后发现有部分数据包无法恢复导致卡顿了上升。
  解:在FlexfecReceiver以及使用到ssrc的地方都加上fec包的判断即可。

问题2:在测试过程中,发现效果不理想,对比数据包内容后发现解码存在异常。
  原因:fec部分上抛的OnRecoveredPacket函数中传上来的是fec解码模块内部的指针,我们直接用来进行构建发送数据包。而在mediasoup中,该数据包在下游会被修改seq等包头内容。导致fec内部的指针被操作,在使用该数据包进行后续恢复的时候出现异常导致无法播放。
  修改部分的代码为:

	void Producer::OnRecoveredPacket(const uint8_t* packet, size_t length) {
		uint8_t packet_temp[length] = {0};
		
		// 把内存重新拷贝一份。
		memcpy(packet_temp, packet, length);
		RTC::RtpPacket* recover_packet = RTC::RtpPacket::Parse(packet_temp, length);
		if (!recover_packet){
			MS_WARN_TAG(rtp, "fecrtest recover packet data is not a valid RTP packet length: %d", length);
			return;
		}
		
		this->ReceiveRtpPacket(recover_packet, true);
	}

5.2 简单测试

  因此我们修复后,得到fec_rate 在255 级别具体恢复率的测试效果:

场景fec包丢控制率真实丢包率
只丢rtp数据 5%0%0%
只丢rtp数据 10%0%0%
只丢rtp数据 20%0%0%
只丢rtp数据 30%0%0%
所有数据都丢 5%5%0.7%
所有数据都丢10%10%3.9%
所有数据都丢 20%20%8.2%
所有数据都丢 30%30%13.6%

  可见FEC在自身不丢失的情况下可以达到比较理想的恢复能力,但是在FEC和RTP包都丢的情况下,即使使用了最大冗余度也没法满足完全不丢包的传输需求。

  • 3
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值