流媒体弱网优化之路(FEC+mediasoup)——mediasoup的Nack优化以及FEC引入

流媒体弱网优化之路(FEC+mediasoup)——mediasoup的Nack优化以及FEC引入

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

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

欢迎大家使用
——


一、mediasoup上行情况

  最近对mediasoup进行了一部分的上行弱网优化工作,在这里对之前的优化工作做一些总结和分析。按照惯例,在开始讲关键内容前先把mediasoup上行数据传输以及控制手段进行简单的介绍。
在这里插入图片描述
  例如上述的图中展示的数据流向。发送端中完整地使用了webrtc中的tcc带宽估计以及Nack模块。当数据包传输时,可以识别网络的带宽突变,同时也具备一定的丢包重传能力。

二、纯Nack优化的效果

2.1 方案

  事实上,这远远达不到我们生产开发中的要求。在实际的测试中,我们拉流的播放端在不扩大缓存(增大延迟)、慢放的情况下,10%的丢包就已经出现比较明显的卡顿情况。这类卡顿造成的原因主要来自于下行数据包的延迟到达或者丢失。因此,我们前段时间做的Nack优化提出了——在保证数据包完整到达的同时还要保证足够快地补偿给对端 弱网优化之路(NACK)——纯NACK方案的优化探索
  根据我们的Nack优化思路,我们把 RTC/NackGenerator.cpp 部分函数的代码修改成了这样:

  /* Static. */

  constexpr size_t MaxPacketAge{ 10000u };
  constexpr size_t MaxNackPackets{ 1000u };
  constexpr uint32_t DefaultRtt{ 100u };
  // constexpr uint8_t MaxNackRetries{ 10u };
  // constexpr uint64_t TimerInterval{ 40u };

  // =================== nack test ===================
  constexpr uint8_t MaxNackRetries{ 20u };
  constexpr uint64_t TimerInterval{ 20u };
  // =================== nack test ===================  

...

  // =================== nack test ===================
  double NackGenerator::CalculateRttLimit2SendNack(int tryTimes) {
    return tryTimes < 3 ?  (double)(tryTimes*0.4) + 1 : 2;
  }
  // =================== nack test ===================
  std::vector<uint16_t> NackGenerator::GetNackBatch(NackFilter filter)
  {
    MS_TRACE();

    uint64_t nowMs = DepLibUV::GetTimeMs();
    std::vector<uint16_t> nackBatch;

    auto it = this->nackList.begin();

    while (it != this->nackList.end())
    {
      NackInfo& nackInfo = it->second;
      uint16_t seq       = nackInfo.seq;

      // clang-format off
      if (
          filter == NackFilter::SEQ &&
          nackInfo.sentAtMs == 0 &&
          (
              nackInfo.sendAtSeq == this->lastSeq ||
              SeqManager<uint16_t>::IsSeqHigherThan(this->lastSeq, nackInfo.sendAtSeq)
              )
              )
        // clang-format on
        {
        nackBatch.emplace_back(seq);
        nackInfo.retries++;
        auto oldMs = nackInfo.sentAtMs;
        nackInfo.sentAtMs = nowMs;

        if (oldMs == 0) {
          oldMs = nowMs;
        }

        if (nackInfo.retries >= MaxNackRetries)
        {
          MS_WARN_TAG(
              rtx,
              "sequence number removed from the NACK list due to max retries [filter:seq, seq:%" PRIu16
              "]",
              seq);

          it = this->nackList.erase(it);
        }
        else
        {
          ++it;
        }

        continue;
        }

      // =================== nack test ===================
      auto limit_var = uint64_t(this->rtt/CalculateRttLimit2SendNack(nackInfo.retries));

      if (filter == NackFilter::TIME && nowMs - nackInfo.sentAtMs >= limit_var)
        // =================== nack test ===================
        //   if (filter == NackFilter::TIME && nowMs - nackInfo.sentAtMs >= this->rtt)
        {
        nackBatch.emplace_back(seq);
        nackInfo.retries++;

        auto oldMs = nackInfo.sentAtMs;
        nackInfo.sentAtMs = nowMs;

        if (oldMs == 0) {
          oldMs = nowMs;
        }

        if (nackInfo.retries >= MaxNackRetries)
        {
          MS_WARN_TAG(
              rtx,
              "sequence number removed from the NACK list due to max retries [filter:time, seq:%" PRIu16
              "]",
              seq);

          it = this->nackList.erase(it);
        }
        else
        {
          ++it;
        }

        continue;
        }

      ++it;
    }

...


  也就是:
  1.将定时器间隔从 40ms/次 下调到 20ms/次;
  2.将最大重传次数从 10次 上调至 20次(还可以增大到40次);
  3.最后是根据 rtt 我们重传次数越多的包要的越频繁。

2.2 结果

  整体测试方案:
  1.同一局域网中的两台安卓进行推拉流测试;
  2.弱网限制由 Windows 机器中的 Network Emulator Client 软件进行模拟;
  3.将拉流画面按 15 帧切割后逐帧对比变化程度(使用OpenCV编写的对比demo);
  4.两张图片相同则认为发生130ms的卡顿(15帧每秒——每帧 66.6ms ),后续依次类推计算出卡顿率(具体对比的demo和方法可以参考我的 github);
  5.得出结论包括:
    客观(卡顿率变化、卡顿时长占比、卡顿次数占比、带宽变化);
    主观(观看感受、画面质感)。

客观结果展示:

  卡顿率统计: 20%丢包+100ms延迟场景
在这里插入图片描述
  wireshark抓包信息显示:

    旧方案:无法抵抗20%丢包,没有全补上包导致丢包带宽估计算法降码率又重新上升探测的过程,影响观看体验。

在这里插入图片描述

    新方案:可以抵抗20%丢包,完全补上包没有触发丢包带宽估计,同时nack导致码率上升,从1.6m ~ 1.8m左右(12.5%)

在这里插入图片描述

  由上面的内容变化来看,实测卡顿率有效降低13.25%(取多次测试中间值)。同时,nack补偿会带来12.5%的带宽上涨。

主观体验:

在这里插入图片描述
  事实上,整体画面的质感变化不大,但是Nack新方案缩短了重传间隔,因此中间画面跳跃时长更短。以上介绍的内容只是纯nack方案优化带来的,但实际上10%丢包的环境也无法做到无感知抵抗(在不增加延迟弱网的情况,如果加上延迟卡顿会更严重)。如果需要继续缩短补包间隔来达到降低卡顿率的目的,需要考虑增加FEC。

三、mediasoup中引入FEC

3.1 FEC方案

UlpFEC方案:

  目前WebRTC的FEC方案有两个——UlpFEC、FlexFEC。
  UlpFEC在WebRTC中使用了RED进行封装(客户端使用原生的WebRTC打开,UlpFEC必须使用RED封装),具体的就不展开讨论了,而FlexFEC是可以单独进行封装的。这两种方式的实现难度会有所区别。
    UlpFEC中,FEC包会和普通的RTP包进行统一打包,也就意味着数据包的序列号是一起统计的。例如,现在有三个视频包和一个FEC包,那么这三个视频包的序列号和FEC包的序列号会合在一起——1(RTP)——2(RTP)——3(FEC)——4(RTP)。很显然,这样会影响我们mediasoup中的Nack队列的统计(因为我们的Nack队列是序号相减计算出来的)。
在这里插入图片描述
  而且mediasoup本身就不支持RED,那么实现起来我们还需要调整很多的东西,因此传统的UlpFEC的实现方式暂时不考虑。

FlexFEC方案:

  FlexFEC其实底层的算法支持也还是在使用UlpFEC封装的,在这我们只是把他新的实现方式与旧的做一个区分,因此称为FlexFEC。

客户端

  我们使用FlexFEC则比UlpFEC要方便的多。因为在上层不强制使用RED封装,而是提供了一个开关,例如客户端以下代码展示:

fieldtrials=”WebRTC-FlexFEC-03/Enabled/WebRTC-FlexFEC-03-Advertised/Enabled”

// 1.假设你的sdk在call上层进行封装的
//   那么你只能使用call中提供的设置流codec的接口,将flexfec的保护ssrc设置进去,例如:

...

  cricket::StreamParams stream;
  stream.ssrcs.push_back(ssrc);
  stream.ssrcs.push_back(ssrc);
  stream.AddFecFrSsrc(ssrc, ssrc); // 设置flexfec的保护ssrc和自己的ssrc,可以设置成一样的。

...

  VideoChannel->->media_channel()->AddSendStream(stream);

...


// 2.假设你的sdk在call下层进行封装的
//   那么你只需要在创建 VideoSendStream 时将flexfec一起设置进去即可,例如:

...

  config.suspend_below_min_bitrate = video_config_.suspend_below_min_bitrate;
  config.periodic_alr_bandwidth_probing =
      video_config_.periodic_alr_bandwidth_probing;
  config.encoder_settings.experiment_cpu_load_estimator =
      video_config_.experiment_cpu_load_estimator;
  config.encoder_settings.encoder_factory = encoder_factory_;
  config.encoder_settings.bitrate_allocator_factory =
      bitrate_allocator_factory_;
  config.encoder_settings.encoder_switch_request_callback = this;
  config.crypto_options = crypto_options_;
  config.rtp.extmap_allow_mixed = ExtmapAllowMixed();
  config.rtcp_report_interval_ms = video_config_.rtcp_report_interval_ms;
  
  std::vector<uint32_t> protected_ssrcs;
  protected_ssrcs.push_back(sp.ssrcs[0]);
  config.rtp.flexfec.ssrc = ssrc;
  config.rtp.flexfec.protected_media_ssrcs = protected_ssrcs;
  
...

服务端

  服务端部分需要添加FEC解码模块,并且应该在NACK模块之前,这样可以减少不必要的重传。mediasoup的WebRTC代码还是m72版本的,因此我们将m72的fec解码部分全部迁移到mediasoup的目录中。
在这里插入图片描述

  随后在 RtpStreamRecv 接收rtp包的部分进行解码,解出来的数据上抛回producer。

// RTC/RtpStreamRecv.hpp
// 接收流部分添加flexfec解码对象,同时仿照 ReceivePacket 函数新增一个 FecReceivePacket 函数。
  class RtpStreamRecv : public RTC::RtpStream,
                        public RTC::NackGenerator::Listener,
                        public Timer::Listener
  {
	...
	
    /* ---------- flexfec ---------- */
      bool FecReceivePacket(RTC::RtpPacket* packet, bool isRecover);
      bool IsFlexFecPacket(RTC::RtpPacket* packet);

    std::unique_ptr<webrtc::FlexfecReceiver> flexfecReceiver;
    /* ---------- flexfec ---------- */
    
	...
 }
	
/* -------------------------------- 分割线 -------------------------------- */

// RTC/RtpStreamRecv.cpp
/* ---------- flexfec ---------- */
  bool RtpStreamRecv::IsFlexFecPacket(RTC::RtpPacket* packet) {
  	if (packet == nullptr) return false;
    return packet->GetPayloadType() == params.fecPayloadType ? true : false;
  }

  bool RtpStreamRecv::FecReceivePacket(RTC::RtpPacket* packet, bool isRecover){
  	if (this->params.useFec) {
  				  MS_WARN_TAG(rtp,
  				      "fec packet receive [ssrc:%" PRIu32 ", payloadType:%" PRIu8 ", seq:%" PRIu16,
  				      packet->GetSsrc(),
  				      packet->GetPayloadType(),
  				      packet->GetSequenceNumber());
  		// 所有数据都入 fec 解码模块
  		if (flexfecReceiver) {
  			webrtc::RtpPacketReceived parsed_packet(nullptr);
  			if (!parsed_packet.Parse(packet->GetData(), packet->GetSize())) {
  				MS_WARN_TAG(rtp, "receive fec packet but parsed_packet failed!");
  				return false;
  			}
				// 如果是恢复包打上恢复记号
				parsed_packet.set_recovered(isRecover);

  			flexfecReceiver->OnRtpPacket(parsed_packet);
  		} else {
  			MS_WARN_TAG(rtp, "receive fec packet but receiver is not exit");
  			// do not things
  		}
  	}

  	if (isRecover)
			this->AddFecRepaired();

		// fec包跳过后续流程
  	if (IsFlexFecPacket(packet)) {
  		packetFecCount++;
			return false;
  	}
		return true;
	}
	/* ---------- flexfec ---------- */
  };

  在producer部分,我们需要继承flexfec的回调类,来承接恢复的数据。

// RTC/Producer.hpp

class Producer : public RTC::RtpStreamRecv::Listener,
	                 public RTC::KeyFrameRequestManager::Listener,
	                 public Timer::Listener,
	                 public webrtc::RecoveredPacketReceiver

	{

...
	// 新增传入参数 isRecover
	ReceiveRtpPacketResult ReceiveRtpPacket(RTC::RtpPacket* packet, bool isRecover);
	
	private:
		/* ---------- flexfec ---------- */
		void OnRecoveredPacket(const uint8_t *packet, size_t length) override;
		/* ---------- flexfec ---------- */
...
}

/* -------------------------------- 分割线 -------------------------------- */
// RTC/Producer.cpp


	Producer::ReceiveRtpPacketResult Producer::ReceiveRtpPacket(RTC::RtpPacket* packet, bool isRecover)
	{
	
		...
		
		// Media packet.
		if (packet->GetSsrc() == rtpStream->GetSsrc())
		{
			result = ReceiveRtpPacketResult::MEDIA;

			// FEC 或 media 都包进入模块进行处理
			if(!rtpStream->FecReceivePacket(packet, isRecover)) {
				return result;
			}
			// Process the packet.
			if (!rtpStream->ReceivePacket(packet))
			{
				// May have to announce a new RTP stream to the listener.
				if (this->mapSsrcRtpStream.size() > numRtpStreamsBefore)
					NotifyNewRtpStream(rtpStream);

				return result;}
		}
		// RTX packet.
		else if (packet->GetSsrc() == rtpStream->GetRtxSsrc())
		{
			result = ReceiveRtpPacketResult::RETRANSMISSION;
			isRtx  = true;

			// Process the packet.
			if (!rtpStream->ReceiveRtxPacket(packet))
				return result;
		}
		// Should not happen.
		else
		{
			MS_ABORT("found stream does not match received packet");
		}
		
		...
		
	}

	...

	/* ---------- flexfec ---------- */
	void Producer::OnRecoveredPacket(const uint8_t* packet, size_t length) {
		RTC::RtpPacket* recover_packet = RTC::RtpPacket::Parse(packet, length);
		if (!recover_packet){
			MS_WARN_TAG(rtp, "fec recover packet data is not a valid RTP packet");
			return;
		}

		this->ReceiveRtpPacket(recover_packet, true);

			  MS_WARN_TAG(rtp,
			      "fec packet recover [ssrc:%" PRIu32 ", payloadType:%" PRIu8 ", seq:%" PRIu16,
			      recover_packet->GetSsrc(),
			      recover_packet->GetPayloadType(),
			      recover_packet->GetSequenceNumber());
	}
	/* ---------- flexfec ---------- */

  在Transport中还需要添加调用Producer的接收函数时传入isRecover值为true。

// RTC/Transport.cpp

void Transport::ReceiveRtpPacket(RTC::RtpPacket* packet)
	{
		...

		// Pass the RTP packet to the corresponding Producer.
		auto result = producer->ReceiveRtpPacket(packet, false);
		
		...
	
	}

3.2 结果讨论

  按照上述的方式完成后,我们进行了丢包+100ms延迟的测试。

在这里插入图片描述
  可见整体的卡顿率都有大幅的下降。

  最重要的是,在10%丢包的场景下,已经基本实现了无感知的优化(接收端不做快慢放,不增加jitter)。

  • 8
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 12
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值