流媒体学习之路(WebRTC)——FEC逻辑分析(6)
流媒体学习之路(WebRTC)——FEC逻辑分析(6)
——
我正在的github给大家开发一个用于做实验的项目 —— github.com/qw225967/Bifrost目标:可以让大家熟悉各类Qos能力、带宽估计能力,提供每个环节关键参数调节接口并实现一个json全配置,提供全面的可视化算法观察能力。欢迎大家使用
——
文章目录
- 流媒体学习之路(WebRTC)——FEC逻辑分析(6)
- 一、FecControllerDefault
- 1.1 背景介绍
- 1.2 VCMLossProtectionLogic
- 1.3 VCMProtectionMethod
- 二、FlexfecSender
- 2.1 ForwardErrorCorrection
- 2.2 码表换算
- 三、总结
在讲具体内容之前插一句嘴,从GCC分析(3)开始,我们将针对GCC的实现细节去分析它设计的原理,让我们理解这些类存在的意义,不再带大家去串具体的流程了。
一、FecControllerDefault
1.1 背景介绍
前面为大家介绍了GCC的部分逻辑,GCC作为码率控制的第一环所有的数据都会收到它输出的参考码率的限制。在发送的数据内容中有很多的分类:视频数据、音频数据、重传数据、FEC数据。FEC前面有向大家介绍过,中文称为:前向纠错。在WebRTC中有一个很好的Fec应用例子,它通过RR回复的网络信息进行动态调整,使得在传输中抵抗不同程度的网络损伤。下面我针对FEC给大家介绍一下动态变化的逻辑。
FecController是WebRTC提供的接口,如果需要自定义自己的控制类,那么继承它之后进行开发就可以了。
class FecController {public:virtual ~FecController() {}virtual void SetProtectionCallback(VCMProtectionCallback* protection_callback) = 0;virtual void SetProtectionMethod(bool enable_fec, bool enable_nack) = 0;// Informs loss protectoin logic of initial encoding state.virtual void SetEncodingData(size_t width,size_t height,size_t num_temporal_layers,size_t max_payload_size) = 0;// Returns target rate for the encoder given the channel parameters.// Inputs: estimated_bitrate_bps - the estimated network bitrate in bits/s.// actual_framerate - encoder frame rate.// fraction_lost - packet loss rate in % in the network.// loss_mask_vector - packet loss mask since last time this method// was called. round_trip_time_ms - round trip time in milliseconds.virtual uint32_t UpdateFecRates(uint32_t estimated_bitrate_bps,int actual_framerate,uint8_t fraction_lost,std::vector<bool> loss_mask_vector,int64_t round_trip_time_ms) = 0;// Informs of encoded output.virtual void UpdateWithEncodedData(size_t encoded_image_length,VideoFrameType encoded_image_frametype) = 0;// Returns whether this FEC Controller needs Loss Vector Mask as input.virtual bool UseLossVectorMask() = 0;
};
在WebRTC中提供了一个默认的Fec控制类:FecControllerDefault。
class FecControllerDefault : public FecController {public:FecControllerDefault(Clock* clock,VCMProtectionCallback* protection_callback);explicit FecControllerDefault(Clock* clock);~FecControllerDefault() override;FecControllerDefault(const FecControllerDefault&) = delete;FecControllerDefault& operator=(const FecControllerDefault&) = delete;void SetProtectionCallback(VCMProtectionCallback* protection_callback) override;void SetProtectionMethod(bool enable_fec, bool enable_nack) override;void SetEncodingData(size_t width,size_t height,size_t num_temporal_layers,size_t max_payload_size) override;uint32_t UpdateFecRates(uint32_t estimated_bitrate_bps,int actual_framerate_fps,uint8_t fraction_lost,std::vector<bool> loss_mask_vector,int64_t round_trip_time_ms) override;void UpdateWithEncodedData(size_t encoded_image_length,VideoFrameType encoded_image_frametype) override;bool UseLossVectorMask() override;float GetProtectionOverheadRateThreshold();private:enum { kBitrateAverageWinMs = 1000 };Clock* const clock_;VCMProtectionCallback* protection_callback_;Mutex mutex_;std::unique_ptr<media_optimization::VCMLossProtectionLogic> loss_prot_logic_RTC_GUARDED_BY(mutex_);size_t max_payload_size_ RTC_GUARDED_BY(mutex_);const float overhead_threshold_;
};
Fec的控制类会根据丢包和RTT进行保护因子的计算,最终获得一个动态的保护因子。
丢包和RTT的数据需要根据对端回复的RR而进行动态调整,因此可以理解为:Fec动态调整的粒度依赖于RR回复的频率。我们可以考虑调整RR的反馈频率,来获得自己理想的动态调整粒度,做进一步的优化。
下面我展示一部分cpp的代码(modules/video_coding/media_opt_util.cc),进行解析。
1.2 VCMLossProtectionLogic
// 可以设置不同的保护方法,Nack/Fec/Nack+Fec,在下一节分析一下几个方法
void VCMLossProtectionLogic::SetMethod(enum VCMProtectionMethodEnum newMethodType) {if (_selectedMethod && _selectedMethod->Type() == newMethodType)return;switch (newMethodType) {case kNack:_selectedMethod.reset(new VCMNackMethod());break;case kFec:_selectedMethod.reset(new VCMFecMethod());break;case kNackFec:_selectedMethod.reset(new VCMNackFecMethod(kLowRttNackMs, -1));break;case kNone:_selectedMethod.reset();break;}UpdateMethod();
}...// 过滤丢包数据,可以 平均过滤/最大值过滤/不过滤
uint8_t VCMLossProtectionLogic::FilteredLoss(int64_t nowMs,FilterPacketLossMode filter_mode,uint8_t lossPr255) {// Update the max window filter.UpdateMaxLossHistory(lossPr255, nowMs);// Update the recursive average filter._lossPr255.Apply(rtc::saturated_cast<float>(nowMs - _lastPrUpdateT),rtc::saturated_cast<float>(lossPr255));_lastPrUpdateT = nowMs;// Filtered loss: default is received loss (no filtering).uint8_t filtered_loss = lossPr255;switch (filter_mode) {case kNoFilter:break;case kAvgFilter:filtered_loss = rtc::saturated_cast<uint8_t>(_lossPr255.filtered() + 0.5);break;case kMaxFilter:filtered_loss = MaxFilteredLossPr(nowMs);break;}return filtered_loss;
}...
1.3 VCMProtectionMethod
下面提到了保护因子计算的逻辑,该逻辑是通过查询 kFecRateTable 表,这个表分了很多个部分。每部分的数据都是从 0 ~ X作为一个部分。因为原先这个表是二维的表:kFecRateTable[rate][loss],在数学上WebRTC团队把它简化成了一维表:kFecRateTable[k],其实逻辑差不多,大家可以换算一下:k = rate_i*129 + loss_j。
// VCMNackMethod 比较简单,这里不展开。
// VCMFecMethod 这个类就有部分计算的逻辑:// 该函数调用的 parameters 参数是从上述的loss中的丢包、RTT、帧率等更新进去的
// _protectionFactorK 关键帧保护因子
// _protectionFactorD p帧保护因子
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%// firstPartitionProt 换算出来是 51,该值应用在表中。// kFecRateTable 是一个每帧包数和丢包率对应的一个表,根据两个参数换算出一个fec_rate。// 这个表的第一部分就是 0 - 51,因此这个参数代表的是第一部分的最大fec_rateuint8_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)// 在 ForwardErrorCorrection 类中 NumFecPackets 计算了至少生成一个Fec的码率,// 假设 媒体数据为 1 个,那么经过换算之后必须得把 fec_rate 设置为 85 才能生成一个Fec包。uint8_t minProtLevelFec = 85;// Threshold on packetLoss and bitRrate/frameRate (=average #packets),// above which we allocate protection to cover at least first partition.// 每帧包数量 和 丢包率// 用于计算查表,表的k为:k = rate_i*129 + loss_juint8_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;// Spatial resolution size, relative to a reference size.// 计算:rate_i*129// 换算表的参数根据 宽高 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.// powf 是平方函数,spatialSizeToRef 的 0.3 平方可以获得一个缩小增长趋势的值,保证了平滑性const float resolnFac = 1.0 / powf(spatialSizeToRef, 0.3f);// 计算帧的包数量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 frameuint8_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%// 计算:loss_j// 当前默认只支持到50%的丢包if (packetLoss >= kPacketLossMax) {packetLoss = kPacketLossMax - 1;}// 最终:k = rate_i*129 + loss_juint16_t indexTable = rateIndexTable * kPacketLossMax + packetLoss;// Check on table indexRTC_DCHECK_LT(indexTable, kFecRateTableSize);// Protection factor for P framecodeRateDelta = 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.// 关键帧 和 p帧 的保护需要区分开,一是关键帧更重要、二是关键帧的数据量更多,因此保护的内容需要给更多倾斜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 indexassert(indexTableKey < kFecRateTableSize);// Protection factor for I framecodeRateKey = 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;}_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.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;if (estNumFecGen < 1.1f && _protectionFactorD < minProtLevelFec) {_corrFecCost = 0.5f;}if (estNumFecGen < 0.9f && _protectionFactorD < minProtLevelFec) {_corrFecCost = 0.0f;}// DONE WITH FEC PROTECTION SETTINGSreturn true;
}
二、FlexfecSender
FlexfecSender是目前默认使用的类,其实底层还在使用UlpfecGenerator来进行fec的封包。每次RR回复后都需要调用SetFecParameters把上面计算出来Fec信息设置到其中。在发送数据时会编译调用AddRtpPacketAndGenerateFec函数来产生Fec数据。然后发送数据前Fec数据和Nack数据都会塞入pacer中,而Fec数据的优先级比Nack低。
// FlexfecSender中的两个函数:SetFecParameters、AddRtpPacketAndGenerateFec。
// 在地下都是 ulpfec_generator_ 直接调用对应的,SetFecParameters、AddRtpPacketAndGenerateFec。constexpr size_t kUlpfecMaxMediaPackets = 48;
constexpr uint8_t kHighProtectionThreshold = 80;
constexpr size_t kMinMediaPackets = 4;...void UlpfecGenerator::SetFecParameters(const FecProtectionParams& params) {RTC_DCHECK_GE(params.fec_rate, 0);RTC_DCHECK_LE(params.fec_rate, 255);// Store the new params and apply them for the next set of FEC packets being// produced.new_params_ = params;// 当fec_rate大于阈值,那么媒体包数最低也要 4 个if (params.fec_rate > kHighProtectionThreshold) {min_num_media_packets_ = kMinMediaPackets;} else {min_num_media_packets_ = 1;}
}...int UlpfecGenerator::AddRtpPacketAndGenerateFec(const uint8_t* data_buffer,size_t payload_length,size_t rtp_header_length) {RTC_DCHECK(generated_fec_packets_.empty());if (media_packets_.empty()) {params_ = new_params_;}bool complete_frame = false;// marker_bit 代表了帧的结尾const bool marker_bit = (data_buffer[1] & kRtpMarkerBitMask) ? true : false;// 每次最大只能保护 80 个包if (media_packets_.size() < kUlpfecMaxMediaPackets) {// Our packet masks can only protect up to |kUlpfecMaxMediaPackets| packets.std::unique_ptr<ForwardErrorCorrection::Packet> packet(new ForwardErrorCorrection::Packet());packet->length = payload_length + rtp_header_length;memcpy(packet->data, data_buffer, packet->length);media_packets_.push_back(std::move(packet));// Keep track of the RTP header length, so we can copy the RTP header// from |packet| to newly generated ULPFEC+RED packets.RTC_DCHECK_GE(rtp_header_length, kRtpHeaderSize);last_media_packet_rtp_header_length_ = rtp_header_length;}// 当帧完整了就进行Fec保护,每次保护都是按帧级别进行if (marker_bit) {++num_protected_frames_;complete_frame = true;}// 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;int ret = fec_->EncodeFec(media_packets_, params_.fec_rate,kNumImportantPackets, kUseUnequalProtection,params_.fec_mask_type, &generated_fec_packets_);if (generated_fec_packets_.empty()) {ResetState();}return ret;}return 0;
}
2.1 ForwardErrorCorrection
最重要的函数是EncodeFec,大家如果对编码感兴趣可以看看前面的文章:流媒体弱网优化之路(FEC)——FEC原理简介、流媒体弱网优化之路(FEC+mediasoup)——FEC引入的问题收尾。以上两篇文章介绍了Fec的一些基础原理以及WebRTC中的UlpFec编码原理。
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.// 根据媒体队列进行非平衡编码,编码的内容则是针对payload进行。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.// 根据保护因子和媒体数据包获取fec的包数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]);}// 创建编码表,编码表会根据fec_mask_type的类型,分两类:随机丢包 和 聚簇丢包// 同时媒体包数量越多编码的表更大。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_);// 开始根据掩码表换算重点保护包数和非平衡保护数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);// Write FEC packets to `generated_fec_packets_`.// 生成Fec保护PayloadGenerateFecPayloads(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.// 这个逻辑很有意思,protection_factor是8位的 最大是256, 1<<7 是128。// 媒体包数*protection_factor + 128 之后再 >>8 就是 媒体包数*(protection_factor/255)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);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.rtc::ArrayView<const uint8_t> mask =mask_table->LookUp(num_media_packets, num_fec_packets);memcpy(packet_mask, &mask[0], mask.size());} else { // UEP caseUnequalProtectionMask(num_media_packets, num_fec_packets, num_imp_packets,num_mask_bytes, packet_mask, mask_table);} // End of UEP modification
} // End of GetPacketMasks...void ForwardErrorCorrection::GenerateFecPayloads(const PacketList& media_packets,size_t num_fec_packets) {RTC_DCHECK(!media_packets.empty());for (size_t i = 0; i < num_fec_packets; ++i) {Packet* const fec_packet = &generated_fec_packets_[i];size_t pkt_mask_idx = i * packet_mask_size_;const size_t min_packet_mask_size = fec_header_writer_->MinPacketMaskSize(&packet_masks_[pkt_mask_idx], packet_mask_size_);const size_t fec_header_size =fec_header_writer_->FecHeaderSize(min_packet_mask_size);size_t media_pkt_idx = 0;auto media_packets_it = media_packets.cbegin();uint16_t prev_seq_num =ParseSequenceNumber((*media_packets_it)->data.data());while (media_packets_it != media_packets.end()) {Packet* const media_packet = media_packets_it->get();// Should `media_packet` be protected by `fec_packet`?if (packet_masks_[pkt_mask_idx] & (1 << (7 - media_pkt_idx))) {size_t media_payload_length =media_packet->data.size() - kRtpHeaderSize;size_t fec_packet_length = fec_header_size + media_payload_length;if (fec_packet_length > fec_packet->data.size()) {// Recall that XORing with zero (which the FEC packets are prefilled// with) is the identity operator, thus all prior XORs are// still correct even though we expand the packet length here.fec_packet->data.SetSize(fec_packet_length);}// 根据之前提到的编码逻辑,多个媒体包会生成一个fec包的payload,丢弃的数据需要几个媒体包一起进行恢复XorHeaders(*media_packet, fec_packet);XorPayloads(*media_packet, media_payload_length, fec_header_size,fec_packet);}media_packets_it++;if (media_packets_it != media_packets.end()) {uint16_t seq_num =ParseSequenceNumber((*media_packets_it)->data.data());media_pkt_idx += static_cast<uint16_t>(seq_num - prev_seq_num);prev_seq_num = seq_num;}pkt_mask_idx += media_pkt_idx / 8;media_pkt_idx %= 8;}RTC_DCHECK_GT(fec_packet->data.size(), 0)<< "Packet mask is wrong or poorly designed.";}
}
2.2 码表换算
Fec码表是根据 fec_mask_type 的类型和媒体数据包数量决定使用哪个表。
const uint8_t* PacketMaskTable::PickTable(FecMaskType fec_mask_type,int num_media_packets) {RTC_DCHECK_GE(num_media_packets, 0);RTC_DCHECK_LE(static_cast<size_t>(num_media_packets), kUlpfecMaxMediaPackets);if (fec_mask_type != kFecMaskRandom &&num_media_packets <=static_cast<int>(fec_private_tables::kPacketMaskBurstyTbl[0])) {return &fec_private_tables::kPacketMaskBurstyTbl[0];}return &fec_private_tables::kPacketMaskRandomTbl[0];
}
上述函数返回一个表的地址,在使用的时候通过下面的索引函数查找合适的Fec编码表。
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.// Find the specific data we're looking for.// 索引出fec的表for (int i = 0; i < fec_index; ++i)entry += entry_size_increment * (i + 1); // skip over the data.size_t size = entry_size_increment * (fec_index + 1);return {&entry[0], size};
}
当数据小于12个包时,Fec掩码表可以直接计算。
当数据大于12个包时,Fec则需要拆分出来进行掩码计算,下面就是例子。
举例:假设用13个媒体包+3个Fec包。
根据选表原则:任意一列的fec编码占8位,把每个媒体包分配到fec包里的方式是根据表的每一位是否为1来确定的。而确定一个fec保护多少个媒体包则是根据——媒体%Fec包——这个方式来确定的。下面代码中给出了一个非常长的一串位运算逻辑,这串位运算逻辑的意思是:
例如有3个fec包,那么我们需要把当前的媒体数据包都分到fec包中进行保护才能完全放进去。通过下方的位运算之后得到一个以下的码表:10010010
01001000
01001001
00100000
00100100
10010000为1的位就是保护媒体数据的位一共13个。
大家发现我们一共给了3个fec包数量,但是编出来了6行掩码数据,这是为什么呢?
原因是掩码有两种:
媒体包数 < 16个时:我们是可以确认最大16个,因此使用2行来进行运算;
媒体包数 > 16个时:是一组数据,因此用6行来进行换算。
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) {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.// 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)};
}
三、总结
上面补充了WebRTC查表的流程,大家可以结合前面的内容流媒体弱网优化之路(FEC+mediasoup)——FEC引入的问题收尾这篇文章提供了码表换算的一个过程,相对来说Fec里面的逻辑有很多算法的细节,使用起来还需要多多分析,下次实测一下。
相关文章:
流媒体学习之路(WebRTC)——FEC逻辑分析(6)
流媒体学习之路(WebRTC)——FEC逻辑分析(6) —— 我正在的github给大家开发一个用于做实验的项目 —— github.com/qw225967/Bifrost目标:可以让大家熟悉各类Qos能力、带宽估计能力,提供每个环节关键参数调节接口并实现一个json全…...
command failed: npm install --loglevel error --legacy-peer-deps
在使用vue create xxx创建vue3项目的时候报错。 解决方法,之前使用的https://registry.npm.taobao.org 证书过期更换镜像地址即可 操作如下: 1.cd ~2.执行rm .npmrc3. sudo npm install -g cnpm --registryhttp://registry.npmmirror.com…...
KubeSphere集群安装-nfs分布式文件共享-对接Harbor-对接阿里云镜像仓库-遇到踩坑记录
KubeSphere安装和使用集群版 官网:https://www.kubesphere.io/zh/ 使用 KubeKey 内置 HAproxy 创建高可用集群:https://www.kubesphere.io/zh/docs/v3.3/installing-on-linux/high-availability-configurations/internal-ha-configuration/ 特别注意 安装前注意必须把当前使…...
Epuck2机器人固件更新及IP查询
文章目录 前言一、下载固件更新软件包:二、查询机器人在局域网下的IP 前言 前面进行了多机器人编队仿真包括集中式和分布式,最近打算在实物机器人上跑一跑之前的编队算法。但由于Epuck2机器人长时间没使用,故对其进行固件的更新,…...
C goto 语句
C 语言中的 goto 语句允许把控制无条件转移到同一函数内的被标记的语句。 注意:在任何编程语言中,都不建议使用 goto 语句。因为它使得程序的控制流难以跟踪,使程序难以理解和难以修改。任何使用 goto 语句的程序可以改写成不需要使用 goto 语…...
【排序算法】-- 深入理解桶排序算法
概述 在计算机科学中,排序算法是一种对数据进行有序排列的重要技术。桶排序(Bucket Sort)是一种常见的排序算法,它通过将数据分到有限数量的桶中,并对每个桶中的数据分别排序,最后按照顺序将所有桶中的数据…...
【Linux】Ubuntu使用Netplan配置静态/动态IP
1、说明 Ubuntu 18.04开始,Ubuntu和Debian移除了以前的ifup/ifdown命令和/etc/network/interfaces配置文件,转而使用ip link set或者/etc/netplan/01-netcfg.yaml模板和sudo netplan apply命令实现网络管理。 Netplan 是抽象网络配置描述器,用于配置Linux网络。 通过netpla…...
chatGLM3+chatchat实现本地知识库
背景 由于客服存在大量的问题为FAQ问题,需要精准回复客户,所以针对此类精准问题,通过自建同量数量库进行回复。 落地方案 通过chatGLM3-6Blangchain-chatchatbge-large-zh实现本地知识库库。 注意:相关介绍和说明请看官网~ 配置要…...
webpack5零基础入门-11处理html资源
1.目的 主要是为了自动引入打包后的js与css资源,避免手动引入 2.安装相关包 npm install --save-dev html-webpack-plugin 3.引入插件 const HtmlWebpackPlugin require(html-webpack-plugin); 4.添加插件(通过new方法调用) /**插件 *…...
el-input设置max、min无效的解决方案
目录 一、方式1:type“number” 二、方式2:oninput(推荐) 三、计算属性 如下表所示,下面为官方关于max,min的介绍: el-input: max原生属性,设置最大值min原生属性&a…...
C语言经典面试题目(十八)
1、如何在C语言中实现堆排序算法? 堆排序是一种利用堆数据结构进行排序的算法。它的基本思想是首先将待排序的数组构建成一个最大堆(或最小堆),然后逐步将堆顶元素与堆中最后一个元素交换,并重新调整堆,使…...
[数据集][目标检测]零售柜零食检测数据集VOC+YOLO格式5422张113类
数据集格式:Pascal VOC格式YOLO格式(不包含分割路径的txt文件,仅仅包含jpg图片以及对应的VOC格式xml文件和yolo格式txt文件) 图片数量(jpg文件个数):5422 标注数量(xml文件个数):5422 标注数量(txt文件个数):5422 标注…...
Flask vs. Django:选择适合你的Web开发框架【第134篇—Flask vs. Django】
👽发现宝藏 前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。【点击进入巨牛的人工智能学习网站】。 Flask vs. Django:选择适合你的Web开发框架 在选择一个适合你项目的Web开发框架…...
你能解释一下Spring AOP(面向切面编程)的概念和用法吗?在Spring中,如何使用事务管理?
你能解释一下Spring AOP(面向切面编程)的概念和用法吗? Spring AOP(面向切面编程)是Spring框架中一个非常重要的功能模块,它允许开发者通过预编译方式和运行期动态代理来实现程序功能的统一维护。AOP并不是…...
时序分解 | Matlab实现GWO-CEEMDAN基于灰狼算法优化CEEMDAN时间序列信号分解
时序分解 | Matlab实现GWO-CEEMDAN基于灰狼算法优化CEEMDAN时间序列信号分解 目录 时序分解 | Matlab实现GWO-CEEMDAN基于灰狼算法优化CEEMDAN时间序列信号分解效果一览基本介绍程序设计参考资料 效果一览 基本介绍 1.CEEMDAN方法的分解效果取决于白噪声幅值权重(Nstd)和噪声添…...
Spring Boot(七十):利用Jasypt对数据库连接进行加密
1 Jasypt简介 Jasypt(Java Simplified Encryption)是一个专注于简化Java加密操作的工具。它提供了一种简单而强大的方式来处理数据的加密和解密,使开发者能够轻松地保护应用程序中的敏感信息,如数据库密码、API密钥等。 Jasypt的设计理念是简化加密操作,使其对开发者更加…...
Mysql设计规范
主键推荐默认用递增字符串大小合理设置数据库默认字段: 主键、创建人、创建时间、修改人、修改时间、逻辑删除(可选)、乐观锁(可选)冗余字段: 严禁冗余变更字段;例如: 创建人名称,租…...
Vue3项目部署安装
Vue3ts部署 查看官网安装项目vue3的命令(四个)其中有: yarn create vuelatest 我执行时遇到报错,可能是我yarn版本不是最新 的问题, 改用这个命令去掉latest即可 yarn create vue 新项目先要安装yarn依赖,才能yarn …...
Oracle P6 Professional 配置连接数据库总结
前言 P6 Professional作为Oracle P6计划管理系统的重要套件之一,其操作出色,体检佳,是非常多的计划工程师跟踪项目进度计划的辅助工具。自20年前,Professional一直在不断的演变更新,以适应当前的新技术,从…...
WPF —— Grid网格布局
1 :Grid网格布局简介 Grid为WPF中最常用的布局容器, 作为View中的主要组成部分, 负责框架中整体的页面布局。 2:网格标签Grid.ColumnDef Grid.ColumnDefinitions自定义列 只能设置宽度 不能设置高度ColumnDefinition 每一个列可以设置宽度,…...
docker详细操作--未完待续
docker介绍 docker官网: Docker:加速容器应用程序开发 harbor官网:Harbor - Harbor 中文 使用docker加速器: Docker镜像极速下载服务 - 毫秒镜像 是什么 Docker 是一种开源的容器化平台,用于将应用程序及其依赖项(如库、运行时环…...
黑马Mybatis
Mybatis 表现层:页面展示 业务层:逻辑处理 持久层:持久数据化保存 在这里插入图片描述 Mybatis快速入门  是一种创新的协议,旨在无缝连接 AI 模型与应用程序。 MCP 是一个开源协议,它标准化了我们的 LLM 应用程序连接所需工具和数据源并与之协作的方式。 可以把它想象成你的 AI 模型 和想要使用它…...
Leetcode 3577. Count the Number of Computer Unlocking Permutations
Leetcode 3577. Count the Number of Computer Unlocking Permutations 1. 解题思路2. 代码实现 题目链接:3577. Count the Number of Computer Unlocking Permutations 1. 解题思路 这一题其实就是一个脑筋急转弯,要想要能够将所有的电脑解锁&#x…...
基于Docker Compose部署Java微服务项目
一. 创建根项目 根项目(父项目)主要用于依赖管理 一些需要注意的点: 打包方式需要为 pom<modules>里需要注册子模块不要引入maven的打包插件,否则打包时会出问题 <?xml version"1.0" encoding"UTF-8…...
全面解析各类VPN技术:GRE、IPsec、L2TP、SSL与MPLS VPN对比
目录 引言 VPN技术概述 GRE VPN 3.1 GRE封装结构 3.2 GRE的应用场景 GRE over IPsec 4.1 GRE over IPsec封装结构 4.2 为什么使用GRE over IPsec? IPsec VPN 5.1 IPsec传输模式(Transport Mode) 5.2 IPsec隧道模式(Tunne…...
HDFS分布式存储 zookeeper
hadoop介绍 狭义上hadoop是指apache的一款开源软件 用java语言实现开源框架,允许使用简单的变成模型跨计算机对大型集群进行分布式处理(1.海量的数据存储 2.海量数据的计算)Hadoop核心组件 hdfs(分布式文件存储系统)&a…...
视觉slam十四讲实践部分记录——ch2、ch3
ch2 一、使用g++编译.cpp为可执行文件并运行(P30) g++ helloSLAM.cpp ./a.out运行 二、使用cmake编译 mkdir build cd build cmake .. makeCMakeCache.txt 文件仍然指向旧的目录。这表明在源代码目录中可能还存在旧的 CMakeCache.txt 文件,或者在构建过程中仍然引用了旧的路…...
SQL慢可能是触发了ring buffer
简介 最近在进行 postgresql 性能排查的时候,发现 PG 在某一个时间并行执行的 SQL 变得特别慢。最后通过监控监观察到并行发起得时间 buffers_alloc 就急速上升,且低水位伴随在整个慢 SQL,一直是 buferIO 的等待事件,此时也没有其他会话的争抢。SQL 虽然不是高效 SQL ,但…...
在Mathematica中实现Newton-Raphson迭代的收敛时间算法(一般三次多项式)
考察一般的三次多项式,以r为参数: p[z_, r_] : z^3 (r - 1) z - r; roots[r_] : z /. Solve[p[z, r] 0, z]; 此多项式的根为: 尽管看起来这个多项式是特殊的,其实一般的三次多项式都是可以通过线性变换化为这个形式…...
