流媒体弱网优化之路(FEC+mediasoup)——mediasoup的Nack优化以及FEC引入
流媒体弱网优化之路(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引入相关推荐
- 流媒体弱网优化之路(FEC+mediasoup)——FEC引入的问题收尾
文章目录 一.前情提要 二.相关知识 2.1 ulpfec封包格式 2.2 flexfec封包格式 三.冗余原理 3.1 大帧随机丢包 3.2 大帧聚簇丢包 四.代码导读 4.1 封包调用 4.2 解 ...
- 流媒体弱网优化之路(FEC)——FEC原理简介
流媒体弱网优化之路(FEC)--FEC原理简介 文章目录 流媒体弱网优化之路(FEC)--FEC原理简介 一.信道保护措施背景介绍 1.1.ARQ丢包重传 1.2.ABC码率自适应 1.3.FEC前向 ...
- 流媒体弱网优化之路(FEC)——FEC的应用奥秘(附demo)
流媒体弱网优化之路(FEC)--FEC的应用奥秘(附demo) 文章目录 流媒体弱网优化之路(FEC)--FEC的应用奥秘(附demo) 一.FEC应用简析 1.1 FEC原理简述 1.2 FEC编码 ...
- Android 网络性能优化(4)弱网优化
系列文章目录 1. Android 网络性能优化(1)概述 2. Android 网络性能优化(2)DNS优化 3. Android 网络性能优化(3)复用连接池 4. Android 网络性能优化( ...
- 阿里云 RTC QoS 弱网对抗之 LTR 及其硬件解码支持
LTR 弱网对抗由于需要解码器的反馈,因此用硬件解码器实现时需要做一些特殊处理.另外,一些硬件解码器对 LTR 的实现不是特别完善,会导致出现解码错误.本文为 QoS 弱网优化系列的第三篇,将为您详解 ...
- 视频会议场景下的弱网优化
疫情将远程办公,视频会议推上了风口的同时,同样也为视频会议平台的运作带来了更多的挑战.蓝猫微会创始人兼CEO 邓昀泽在LiveVideoStack线上分享中针对视频会议系统优化中弱网定义,算法评估及技 ...
- 弱网优化,GCC 动态带宽评估算法(内附详细公式)
通过上次的<RTC 系统音视频传输弱网对抗技术概览>,我们知道 RTC 的三大核心指标为实时性.清晰度.流畅度.在整个通话过程中核心表现达标,才能给用户一个基本的良好体验.关注[融云全球互 ...
- 网易云信即时通讯推送保障及网络优化详解(三):如何在弱网环境下优化大数据传输
对于移动 APP 来说,IM 功能正变得越来越重要,它能够创建起人与人之间的连接.社交类产品中,用户与用户之间的沟通可以产生出更好的用户粘性. 在复杂的 Android 生态环境下,多种因素都会造成消 ...
- IM 推送保障及网络优化详解(三):如何在弱网环境下优化大数据传输?
对于移动 App 来说,IM 功能正变得越来越重要,它能够创建起人与人之间的连接.社交类产品中,用户与用户之间的沟通可以产生出更好的用户粘性. 在复杂的 Android 生态环境下,多种因素都会造成消 ...
- 腾讯云低延时直播系统架构设计与弱网优化实践
"直播带货"可能是2020年最具代表性的词汇之一,那么传统电商该如何融合直播系统,直播过程如何保障用户的最佳观看体验?本文由腾讯云资深架构师何书照在LiveVideoStack线上 ...
最新文章
- 数据结构——维基百科
- Symbian学习笔记(4)——在GUI应用中使用图像
- Jin Ge Jin Qu hao UVA - 12563 (劲歌金曲)01背包,求装入的东西最多(相同多时价值大)
- TCP协议-如何保证传输可靠性
- mysql5.7.17二进制包_mysql5.7二进制包安装方法
- 四管前级怎么去掉高低音音调_一些歌曲音调太高怎么才能唱上去??
- python实践项目(九)
- 汉文博士 0.5.7.2356 版发布
- iOS Crash常规跟踪方法及Bugly集成运用
- 用计算机装扮校园图片,第4课装扮我们的校园——在场景中运用图形元件教学设计.doc...
- 干净地卸载QTP的小工具 - QTPCleanUninstaller
- Win11如何给系统盘瘦身?Win11系统盘瘦身方法
- 自己研发的系统给rtx发消息
- 延安.居民家庭计算机普及率,2004~2014年家庭互联网普及率及电脑持有率
- win7配置TomCat环境
- mysql identity属性_Mysql中Identity 详细介绍
- 计算机c盘空间满了应该怎么办,C盘空间满了怎么办?我来教你你如何解决
- 2021哔哩哔哩1024程序员节日第一弹:算法与安全
- python实时监控文件目录_教你三种方法,用 Python实时监控文件
- Android友盟+U-APM快速集成与极致体验
热门文章
- excel的if函数嵌套使用
- 平板电脑也可以学python吗?10 个Python 编辑器,,让编程更贴近生活~
- ai怎么渐变颜色_ai渐变工具怎么用?Adobe Illustrator渐变颜色实操教程
- 算是目前PAYPAL最全最完整的开发方式了
- DDOS学习+网络钓鱼+验证码攻击
- python基于朴素贝叶斯算法实现新闻分类
- 战神引擎传奇手游源码【诛仙玛法单职业五大陆】
- php怎么把图片设置为背景,ppt怎么把图片设为背景
- 台式计算机搜索不到无线信号,win7电脑搜不到无线信号怎么办_win7找不到无线网络怎么解决-win7之家...
- 上海理工大计算机学研究生怎么样,上海理工大学(专业学位)计算机技术考研难吗...