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

文章目录

  • 流媒体弱网优化之路(FEC+mediasoup)——mediasoup的Nack优化以及FEC引入
  • 一、mediasoup上行情况
  • 二、纯Nack优化的效果
    • 2.1 方案
    • 2.2 结果
      • 客观结果展示:
      • 主观体验:
  • 三、mediasoup中引入FEC
    • 3.1 FEC方案
      • UlpFEC方案:
      • FlexFEC方案:
        • 客户端
        • 服务端
    • 3.2 结果讨论

一、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 offif (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.hppclass Producer : public RTC::RtpStreamRecv::Listener,public RTC::KeyFrameRequestManager::Listener,public Timer::Listener,public webrtc::RecoveredPacketReceiver{...// 新增传入参数 isRecoverReceiveRtpPacketResult ReceiveRtpPacket(RTC::RtpPacket* packet, bool isRecover);private:/* ---------- flexfec ---------- */void OnRecoveredPacket(const uint8_t *packet, size_t length) override;/* ---------- flexfec ---------- */
...
}/* -------------------------------- 分割线 -------------------------------- */
// RTC/Producer.cppProducer::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;}if (!isRecover) {// 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;}}}...}.../* ---------- 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.cppvoid Transport::ReceiveRtpPacket(RTC::RtpPacket* packet){...// Pass the RTP packet to the corresponding Producer.auto result = producer->ReceiveRtpPacket(packet, false);...}

3.2 结果讨论

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


  可见整体的卡顿率都有大幅的下降。

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

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

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

    文章目录 一.前情提要 二.相关知识 2.1 ulpfec封包格式 2.2 flexfec封包格式 三.冗余原理 3.1 大帧随机丢包 3.2 大帧聚簇丢包 四.代码导读 4.1 封包调用 4.2 解 ...

  2. 流媒体弱网优化之路(FEC)——FEC原理简介

    流媒体弱网优化之路(FEC)--FEC原理简介 文章目录 流媒体弱网优化之路(FEC)--FEC原理简介 一.信道保护措施背景介绍 1.1.ARQ丢包重传 1.2.ABC码率自适应 1.3.FEC前向 ...

  3. 流媒体弱网优化之路(FEC)——FEC的应用奥秘(附demo)

    流媒体弱网优化之路(FEC)--FEC的应用奥秘(附demo) 文章目录 流媒体弱网优化之路(FEC)--FEC的应用奥秘(附demo) 一.FEC应用简析 1.1 FEC原理简述 1.2 FEC编码 ...

  4. Android 网络性能优化(4)弱网优化

    系列文章目录 1. Android 网络性能优化(1)概述 2. Android 网络性能优化(2)DNS优化 3. Android 网络性能优化(3)复用连接池 4. Android 网络性能优化( ...

  5. 阿里云 RTC QoS 弱网对抗之 LTR 及其硬件解码支持

    LTR 弱网对抗由于需要解码器的反馈,因此用硬件解码器实现时需要做一些特殊处理.另外,一些硬件解码器对 LTR 的实现不是特别完善,会导致出现解码错误.本文为 QoS 弱网优化系列的第三篇,将为您详解 ...

  6. 视频会议场景下的弱网优化

    疫情将远程办公,视频会议推上了风口的同时,同样也为视频会议平台的运作带来了更多的挑战.蓝猫微会创始人兼CEO 邓昀泽在LiveVideoStack线上分享中针对视频会议系统优化中弱网定义,算法评估及技 ...

  7. 弱网优化,GCC 动态带宽评估算法(内附详细公式)

    通过上次的<RTC 系统音视频传输弱网对抗技术概览>,我们知道 RTC 的三大核心指标为实时性.清晰度.流畅度.在整个通话过程中核心表现达标,才能给用户一个基本的良好体验.关注[融云全球互 ...

  8. 网易云信即时通讯推送保障及网络优化详解(三):如何在弱网环境下优化大数据传输

    对于移动 APP 来说,IM 功能正变得越来越重要,它能够创建起人与人之间的连接.社交类产品中,用户与用户之间的沟通可以产生出更好的用户粘性. 在复杂的 Android 生态环境下,多种因素都会造成消 ...

  9. IM 推送保障及网络优化详解(三):如何在弱网环境下优化大数据传输?

    对于移动 App 来说,IM 功能正变得越来越重要,它能够创建起人与人之间的连接.社交类产品中,用户与用户之间的沟通可以产生出更好的用户粘性. 在复杂的 Android 生态环境下,多种因素都会造成消 ...

  10. 腾讯云低延时直播系统架构设计与弱网优化实践

    "直播带货"可能是2020年最具代表性的词汇之一,那么传统电商该如何融合直播系统,直播过程如何保障用户的最佳观看体验?本文由腾讯云资深架构师何书照在LiveVideoStack线上 ...

最新文章

  1. 数据结构——维基百科
  2. Symbian学习笔记(4)——在GUI应用中使用图像
  3. Jin Ge Jin Qu hao UVA - 12563 (劲歌金曲)01背包,求装入的东西最多(相同多时价值大)
  4. TCP协议-如何保证传输可靠性
  5. mysql5.7.17二进制包_mysql5.7二进制包安装方法
  6. 四管前级怎么去掉高低音音调_一些歌曲音调太高怎么才能唱上去??
  7. python实践项目(九)
  8. 汉文博士 0.5.7.2356 版发布
  9. iOS Crash常规跟踪方法及Bugly集成运用
  10. 用计算机装扮校园图片,第4课装扮我们的校园——在场景中运用图形元件教学设计.doc...
  11. 干净地卸载QTP的小工具 - QTPCleanUninstaller
  12. Win11如何给系统盘瘦身?Win11系统盘瘦身方法
  13. 自己研发的系统给rtx发消息
  14. 延安.居民家庭计算机普及率,2004~2014年家庭互联网普及率及电脑持有率
  15. win7配置TomCat环境
  16. mysql identity属性_Mysql中Identity 详细介绍
  17. 计算机c盘空间满了应该怎么办,C盘空间满了怎么办?我来教你你如何解决
  18. 2021哔哩哔哩1024程序员节日第一弹:算法与安全
  19. python实时监控文件目录_教你三种方法,用 Python实时监控文件
  20. Android友盟+U-APM快速集成与极致体验

热门文章

  1. excel的if函数嵌套使用
  2. 平板电脑也可以学python吗?10 个Python 编辑器,,让编程更贴近生活~
  3. ai怎么渐变颜色_ai渐变工具怎么用?Adobe Illustrator渐变颜色实操教程
  4. 算是目前PAYPAL最全最完整的开发方式了
  5. DDOS学习+网络钓鱼+验证码攻击
  6. python基于朴素贝叶斯算法实现新闻分类
  7. 战神引擎传奇手游源码【诛仙玛法单职业五大陆】
  8. php怎么把图片设置为背景,ppt怎么把图片设为背景
  9. 台式计算机搜索不到无线信号,win7电脑搜不到无线信号怎么办_win7找不到无线网络怎么解决-win7之家...
  10. 上海理工大计算机学研究生怎么样,上海理工大学(专业学位)计算机技术考研难吗...