深入浅出WebRTC—ULPFEC
FEC 通过在发送端添加额外的冗余信息,使接收端即使在部分数据包丢失的情况下也能恢复原始数据,从而减轻网络丢包的影响。在 WebRTC 中,FEC 主要有两种实现方式:ULPFEC 和 FlexFEC,FlexFEC 是 ULPFEC 的扩展和升级,两者被纳入同一个实现框架中。本文主要分析 ULPFEC 实现,重点关注 FEC 的实现原理,尽量避开繁杂的实现细节。
1. 静态结构
1)RtpVideoSender 是发送端控制中心,其接收码率更新的通知,使用 FecController 生成 FEC 保护比率,并将 FEC 保护比率设置到 VideoFecGenerator,控制 FEC 保护码率大小。
2)FecController 根据 RtpVideoSender 设置的保护模式(NACK/FEC)来决定选择什么样的保护方法:VCMNackMethod、VCMFecMethod 或 VCMNackFecMethod。基于 RtpVideoSender 更新的目标码率、帧率、丢包率等一系列参数计算得到 FEC 保护比率,
3)VideoFecGenerator 根据 RtpVideoSender 设置的保护比率(还有其他参数,具体参考 FecProtectionParams)生成 FEC 报文。
ULPFEC 和 FlexFEC 是两种不同的 FEC 实现,复用相同的 FEC 处理框架,如下图所示,通过 FlexfecSender 可以看到, FlexFEC 是 ULPFEC 的一个扩展。
2. 流程框架
FEC 框架可以分为发送端和接收端两部分,发送端实现较为复杂,又可以分为参数计算、数据生成和数据发送三个部分;接收端实现相对简单,主要就是做数据恢复。
下图展示了 FEC 发送端相关实现逻辑。
1)参数计算:当估计带宽发生变化或丢包率发生变化时,需要重新计算生成多少比例的 FEC 码率来保护原始码率。码率分配器会通知 VideoSendStream 新的目标码率、丢包率、RTT等参数。此时,会触发 RtpVideoSender 重新计算编码码率和 FEC 保护比率。编码码率会设置到编码器,控制编码器的码率输出。FEC 保护比率会设置到 RTPSenderEgress,用来控制 FEC 报文的生成。
2)数据生成:PacingController 负责平滑发送报文,报文最终通过 RtpSenderEgress 发送到网络,在发送报文的同时,会调用 VideoFecGenerator(UlpfecGenerator::AddPacketAndGenerateFec) 生成 FEC 报文。生成的 FEC 报文会先保存在 VideoFecGenerator。
3)数据发送:PacingController 每次发送完报文后,都会调用 PacketRouter::FetchFec 从 VideoFecGenerator 中拉取所有生成的 FEC 报文,并将这些报文插入发送队列,和普通媒体报文一起发送出去。
以下代码是码率更新时的处理逻辑,总体逻辑非常清晰,重新计算编码码率和保护码率,并把编码码率设置到编码器,实现对编码器输出码率的控制,保护码率的控制在 OnBitrateUpdated 内部实现。
uint32_t VideoSendStreamImpl::OnBitrateUpdated(BitrateAllocationUpdate update) {if (update.stable_target_bitrate.IsZero()) {update.stable_target_bitrate = update.target_bitrate;}// 计算编码码率和保护码率(内部还会计算 FEC 比率)rtp_video_sender_->OnBitrateUpdated(update, stats_proxy_->GetSendFrameRate());// 获取编码码率encoder_target_rate_bps_ = rtp_video_sender_->GetPayloadBitrateBps();// 获取保护码率const uint32_t protection_bitrate_bps =rtp_video_sender_->GetProtectionBitrateBps();...// 更新编码器目标码率(包括其他一系列参数)video_stream_encoder_->OnBitrateUpdated(encoder_target_rate, encoder_stable_target_rate, link_allocation,rtc::dchecked_cast<uint8_t>(update.packet_loss_ratio * 256),update.round_trip_time.ms(), update.cwnd_reduce_ratio);return protection_bitrate_bps;
}
以下是设置到 VideoFecGenerator 的 FEC 参数结构体。
struct FecProtectionParams {// FEC 报文数量 / 原始报文数量int fec_rate = 0;// 一组 FEC 报文保护原始报文最大帧数量int max_fec_frames = 0;// 固定为随机丢包类型FecMaskType fec_mask_type = FecMaskType::kFecMaskRandom;
};
3. 初始化
WebRTC 支持配置是否启用 FEC 和 NACK。在 RtpVideoSender 的构造函数中,会根据相关配置来决定是否创建 FEC 生成器,是创建 UlpfecGenerator 还是 FlexfecGenerator。
是否开启 FEC 和 NACK 会设置到 FecControllerDefault,一般情况,FEC 和 NACK 都会启用。
fec_controller_->SetProtectionMethod(fec_enabled, NackEnabled());
FecControllerDefault 根据设置的参数,确定当前可以使用什么保护方法,不同保护方法会有不同的保护策略。
void FecControllerDefault::SetProtectionMethod(bool enable_fec, bool enable_nack) {media_optimization::VCMProtectionMethodEnum method(media_optimization::kNone);if (enable_fec && enable_nack) {method = media_optimization::kNackFec;} else if (enable_nack) {method = media_optimization::kNack;} else if (enable_fec) {method = media_optimization::kFec;}MutexLock lock(&mutex_);loss_prot_logic_->SetMethod(method);
}
4. 参数计算
4.1. 编码码率
编码码率计算比较有意思,正常思路是计算新的保护码率,用估计码率减去保护码率剩下的就是编码码率。但这里不是这样干的,直接等于新目标码率减去基于历史数据统计的当前保护码率,代码如下所示。虽然这样计算出来的编码码率具有滞后性,由于新的 FEC 保护比率已经更新,真实保护码率会基于新的 FEC 保护比率进行调整,定时器会驱动不断调用 UpdateFecRates,最终会将编码码率调整到位。
uint32_t FecControllerDefault::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) {...// 设置 FEC 参数到 RtpSenderEgress,同时获取当前几个发送速率protection_callback_->ProtectionRequest(&delta_fec_params, &key_fec_params, &sent_video_rate_bps,&sent_nack_rate_bps, &sent_fec_rate_bps);// 计算当前总发送速率uint32_t sent_total_rate_bps =sent_video_rate_bps + sent_nack_rate_bps + sent_fec_rate_bps;// 保护开销(比率)保持不变if (sent_total_rate_bps > 0) {protection_overhead_rate =static_cast<float>(sent_nack_rate_bps + sent_fec_rate_bps) / sent_total_rate_bps;}// 不超过 50%protection_overhead_rate =std::min(protection_overhead_rate, overhead_threshold_);// 编码码率等于估计码率减去当前统计的保护码率return estimated_bitrate_bps * (1.0 - protection_overhead_rate);
}
4.2. 保护比率
FEC 保护比率的计算非常复杂,计算过程分为两步,第一步是基于码率和丢包率查表,如下表所示,这是基于 kFecRateTable 数组绘制的可视化表格,由于数据量太大,省略了大部分数据。
表格中每一行代表一个码率,分 0 - 49 共 50 个级别,对应 30FPS 的码率范围从200kbps 到 8000kbps。每一列代表一个丢包率,分 0 - 128 共 129 个级别,对应丢包率从 0 到 50%。查表时,将码率需要转换为某个码率级别,将丢包率转换为某个丢包率级别,交叉位置的数字即为 FEC 保护比率。
第二步就是对查表得到 FEC 保护比率进行调整,代码如下所示。如果 RTT 很小,则关闭非关键帧的 FEC,优先使用 NACK,但关键帧还是继续使用 FEC 保护。
bool VCMNackFecMethod::ProtectionFactor(const VCMProtectionParameters* parameters) {// 计算 FEC 比率,设置 _protectionFactorK 和 _protectionFactorDVCMFecMethod::ProtectionFactor(parameters);if (_lowRttNackMs == -1 || parameters->rtt < _lowRttNackMs) {// 低 RTT 场景(RTT < 20ms),非关键帧不使用 FEC(保护因子为0)_protectionFactorD = 0;VCMFecMethod::UpdateProtectionFactorD(_protectionFactorD);} else if (_highRttNackMs == -1 || parameters->rtt < _highRttNackMs) {// 中等 RTT 场景VCMFecMethod::UpdateProtectionFactorD(_protectionFactorD);}return true;
}
5. 生成 FEC 数据
5.1. 计算条件
在生成 FEC 报文之前,需要确定使用多少原始报文以及使用哪些原始报文,WebRTC 对此设计了几个约束:
1)最少报文数量
报文数量不能太少了,由 min_num_media_packets_ 决定,由 MinimumMediaPacketsReached() 做出限制。
2)帧边界限制
等到帧结束标志时才会计算 FEC。尽量保证对于一个完整的帧,其保护策略是一致的。
3)最大帧数量
这是一个保护条件,记录的帧数量超过设定值,不再管其他数量限制了,必须计算 FEC。
4)开销误差限制
FEC 保护比率决定 FEC 开销,但由于四舍五入等计算精度问题,使得 FEC 目标保护比率和 FEC 真实保护比率会有一定误差。当原始报文数量较多时,这个差异会比较小,原始报文较少时,这个差异可能会很大。假设 FEC 目标保护比率为 10%,原始报文数量是 10 个,生成目标 FEC 报文为 10 * 10% = 1 个,真实计算时会进行 Q8 格式转换,向上取整,强制至少 1 个 FEC 报文等,可能计算需要 2 个 FEC 报文,此时,目标保护比率和计算得到的保护比率达到了 100%,远超设置的最大误差,这是不行的。
void UlpfecGenerator::AddPacketAndGenerateFec(const RtpPacketToSend& packet) {{MutexLock lock(&mutex_);// 如果有等待更新的FEC参数,则更新当前参数并清除待更新标记。if (pending_params_) {current_params_ = *pending_params_;pending_params_.reset();// FEC 比率 > 31.4%(80/255),至少需要 4 个包才能计算 FECif (CurrentParams().fec_rate > kHighProtectionThreshold) {min_num_media_packets_ = kMinMediaPackets;} else { // 否则,允许至少1个媒体包参与FEC计算。min_num_media_packets_ = 1;}}}// 记录当前分组中包含关键帧if (packet.is_key_frame()) {media_contains_keyframe_ = true;}// 读取 RTP 头的 marker 标志const bool complete_frame = packet.Marker();// 将用来计算 FEC 报文的原始报文缓存起来,ulpfec 的 mask 最多记录 48 个报文if (media_packets_.size() < kUlpfecMaxMediaPackets) {auto fec_packet = std::make_unique<ForwardErrorCorrection::Packet>();fec_packet->data = packet.Buffer();media_packets_.push_back(std::move(fec_packet));last_media_packet_ = packet; // 用于复制 RTP 头}// 记录帧数量if (complete_frame) {++num_protected_frames_;}auto params = CurrentParams();if (complete_frame &&// 已经保护足够多的帧(num_protected_frames_ >= params.max_fec_frames ||// 实际开销与目标开销之差小于最大允许偏差,并且已经收集到足够数量的媒体包(ExcessOverheadBelowMax() && MinimumMediaPacketsReached()))) {constexpr int kNumImportantPackets = 0;// 为什么不使用 unequal protection?constexpr bool kUseUnequalProtection = false;// FEC 编码fec_->EncodeFec(media_packets_, params.fec_rate, kNumImportantPackets,kUseUnequalProtection, params.fec_mask_type,&generated_fec_packets_);}
}
5.2. 掩码生成
由于 WebRTC 的 FEC 采用的是 Charity Code,虽然有了 FEC 比率,但安排哪个 FEC 报文去保护哪几个原始报文,即如何确定 FEC 报文的掩码表,也是一个头疼的事情。
掩码的设置非常灵活,针对随机丢包和突发丢包,WebRTC 提前准备了两张掩码表:kPacketMaskRandomTbl和kPacketMaskBurstyTbl,多少原始报文,生成多少 FEC 报文,直接查表就能得到每个 FEC 报文的掩码,大大简化了掩码的生成过程,提高了程序处理效率。
WebRTC 目前只使用 kPacketMaskRandomTbl 掩码表,如下所示。“kPacketMaskRandomX”中 X 表示原始包文数量,可以看到此表最多覆盖 12 个原始报文。
const uint8_t kPacketMaskRandomTbl[] = {12,kPacketMaskRandom1, // 2 byte entries.kPacketMaskRandom2,kPacketMaskRandom3,kPacketMaskRandom4,kPacketMaskRandom5,kPacketMaskRandom6,kPacketMaskRandom7,kPacketMaskRandom8,kPacketMaskRandom9,kPacketMaskRandom10,kPacketMaskRandom11,kPacketMaskRandom12,
};
以 kPacketMaskRandom3 为例,kMaskRandom3_1 表示 3 个原始报文生成 1 个 FEC 报文,FEC 保护比率为 33%;kMaskRandom3_2 表示 3 个原始报文生成 2 个 FEC 报文,FEC 保护比率为 66%;kMaskRandom3_3 表示 3 个原始报文生成 3 个 FEC 报文,FEC 保护比率为 100%。表项已经到头了,WebRTC 允许最大的 FEC 保护比率为 100%。
#define kPacketMaskRandom3 3,
kMaskRandom3_1,
kMaskRandom3_2,
kMaskRandom3_3
以 kMaskRandom3_3 为例,每一行对应一个 FEC 报文的掩码,取前 3bits。
#define kMaskRandom3_3
0xc0, 0x00,
0xa0, 0x00,
0x60, 0x00
5.3. 掩码应用
下面以 12个 原始媒体报文使用 4 个 FEC 报文的随机保护为例,讲解掩码的应用,查表结果如下:
#define kMaskRandom12_4
0x8b, 0x20,
0x14, 0xb0,
0x22, 0xd0,
0x45, 0x50
转换为二进制如下所示,灰色填充部分为未启用 bit:
保护逻辑示意图如下所示,实线框为原始媒体报文,虚线框为 FEC 报文:
如果增加一个原始媒体报文,则超过12个限制,不能再查表了,掩码需要由程序代码动态生成。生成逻辑比较简单:每个 FEC 报文只保护“索引对 FEC 报文总数取模与 FEC 报文索引相等”的原始媒体报文,生成的掩码如下图所示:
保护逻辑示意图如下所示。0 号 FEC 报文保护 0、4、8、12 号原始报文;1 号 FEC 报文保护 1、5、9 号原始报文;2 号 FEC 报文保护 2、6、10 号原始报文;3 号 FEC 报文保护 3、7、11 号原始报文。显然,超过12个报文的保护更加均匀,而且每个原始媒体报文只会被一个 FEC 报文保护。
另外,WebRTC 支持分级保护,分级保护分如下几种模式:
enum ProtectionMode {// 重点保护和非重点保护不交叉,重点保护保护重要报文,非重点保护保护剩余报文kModeNoOverlap,// 重点保护和非重点保护交叉,重点保护只会保护重要报文,非重点保护会保护所有报文kModeOverlap,// 在kModeOverlap之上,加强对首个报文的保护力度kModeBiasFirstPacket,
};
kModeNoOverlap模式
假设前四个报文为重要报文,分配 2 个 FEC 报文进行保护,剩余的 9个 报文分配 2 个FEC 报文进行保护,查表结果如下:
#define kMaskRandom4_2
0xc0, 0x00,
0xb0, 0x00
#define kMaskRandom9_2
0xaa, 0x80,
0xd5, 0x00
4 个 FEC 的掩码转换为二进制如下所示:
保护逻辑示意图如下所示:
kModeOverlap模式
假设前四个报文为重要报文,分配 2 个 FEC 报文进行保护,另外 2 个 FEC 报文要保护所有原始报文。前 2 个 FEC 报文的掩码可以通过查表得到,后面 2 个 FEC 报文保护的原始报文数量超过 12 个,只能动态生成,最终掩码转换为二进制如下图所示:
保护逻辑示意图如下所示:
kModeBiasFirstPacket
kModeBiasFirstPacket 模式,是在kModeOverlap之上,加强对第一个报文的保护,掩码表如下所示:
保护逻辑示意图如下所示,3 号 FEC 报文增加了对 0 号报文的保护。
6. 发送 FEC 数据
6.1. 视频报文封装
编码出来的视频报文,如果协商了 RED,会使用 RED 封装。
bool RTPSenderVideo::SendVideo(int payload_type,absl::optional<VideoCodecType> codec_type,uint32_t rtp_timestamp,Timestamp capture_time,rtc::ArrayView<const uint8_t> payload,size_t encoder_output_size,RTPVideoHeader video_header,TimeDelta expected_retransmission_time,std::vector<uint32_t> csrcs)
{...if (red_enabled()) {std::unique_ptr<RtpPacketToSend> red_packet(new RtpPacketToSend(*packet));BuildRedPayload(*packet, red_packet.get());red_packet->SetPayloadType(*red_payload_type_);red_packet->set_is_red(true);red_packet->set_packet_type(RtpPacketMediaType::kVideo);red_packet->set_allow_retransmission(packet->allow_retransmission());rtp_packets.emplace_back(std::move(red_packet));} else {packet->set_packet_type(RtpPacketMediaType::kVideo);rtp_packets.emplace_back(std::move(packet));}...
}
6.2. FEC 报文封装
FEC 报文也是使用 RED 封装。
std::vector<std::unique_ptr<RtpPacketToSend>> UlpfecGenerator::GetFecPackets() {if (generated_fec_packets_.empty()) {return std::vector<std::unique_ptr<RtpPacketToSend>>();}last_media_packet_->SetPayloadSize(0);std::vector<std::unique_ptr<RtpPacketToSend>> fec_packets;fec_packets.reserve(generated_fec_packets_.size());size_t total_fec_size_bytes = 0;for (const auto* fec_packet : generated_fec_packets_) {// 创建一个新的 RTP 报文std::unique_ptr<RtpPacketToSend> red_packet =std::make_unique<RtpPacketToSend>(*last_media_packet_);// 使用 RED 封装red_packet->SetPayloadType(red_payload_type_);// FEC 包的 mark 标记无意义red_packet->SetMarker(false);// 增加一个字节的 RED 头uint8_t* payload_buffer = red_packet->SetPayloadSize(kRedForFecHeaderLength + fec_packet->data.size());// Primary RED header with F bit unset.// See https://tools.ietf.org/html/rfc2198#section-3// // 0 1 2 3 4 5 6 7// +-+-+-+-+-+-+-+-+ // | 0 | Block PT |// +-+-+-+-+-+-+-+-+ //// 设置 RED 头的 Block PTpayload_buffer[0] = ulpfec_payload_type_; // RED header.// 拷贝 FEC 数据memcpy(&payload_buffer[1], fec_packet->data.data(), fec_packet->data.size());// 累加 FEC 数据total_fec_size_bytes += red_packet->size();// 设置 RTP 头负载类型red_packet->set_packet_type(RtpPacketMediaType::kForwardErrorCorrection);// FEC 报文不重传,不会存放到 history 列表red_packet->set_allow_retransmission(false);// 设置 RED 报文red_packet->set_is_red(true);// FEC 报文不需要再被保护red_packet->set_fec_protect_packet(false);fec_packets.push_back(std::move(red_packet));}// 进入下一轮编码ResetState();MutexLock lock(&mutex_);// 更新 FEC 码率fec_bitrate_.Update(total_fec_size_bytes, clock_->CurrentTime());return fec_packets;
}
6.3. 发送流程
如下图所示,PacingController 不停的发送报文,发送的报文都要经过 RtpSenderEgress,RtpSenderEgress 根据设置的 FEC 参数,调用 VideoFecGenerator 不停的生成 FEC 报文,这些生成 FEC 报文都临时缓存在 VideoFecGenerator 中。PacingController 在发送报文的过程中,会不停的拉取 FEC 报文,将 FEC 报文插入发送队列,最后跟随原始报文一起发送出去。
7. 恢复原始数据
发送端的逻辑比较简单,收到报文后如何恢复丢失报文,内部具体如何恢复的代码逻辑暂未分析。
1)接收端收到的视频报文都会送给 RtpVideoStreamReceiver2 进行处理。
2)RtpVideoStreamReceiver2 会将所有报文都扔给 UlpfecReceiver 处理。
3)UlpfecReceiver 内部会进行判断,如果发现有丢包,会尝试进行 FEC 解码,然后将原始报文和恢复后的报文都回调给 RtpVideoStreamReceiver2。
4)RtpVideoStreamReceiver2 将收到的原始报文插入到 PacketBuffer 等待解码。
接收端 RtpVideoStreamReceiver2 会根据报文的 payload type 判断是否是 RED 报文,如果是 RED 报文,则将报文一股脑都扔给 UlpFecReceiver 处理,代码实现如下。由此可见,ulpfec 必须搭配 RED 才能生效。
void RtpVideoStreamReceiver2::ReceivePacket(const RtpPacketReceived& packet) {if (packet.payload_size() == 0) {NotifyReceiverOfEmptyPacket(packet.SequenceNumber());return;}// payload type 为 RED 的数据包,都要先交给 UlpfecReceiver 进行处理// 发送端已将所有所有视频报文和 FEC 报文都使用 RED 封装if (packet.PayloadType() == red_payload_type_) {ParseAndHandleEncapsulatingHeader(packet);return;}// 从 FEC 逛一圈回来的报文(“原始报文”+“恢复报文”),此时 payload type 已被替换为// 真实媒体数据类型// 根据 payload type 获取解包器const auto type_it = payload_type_map_.find(packet.PayloadType());if (type_it == payload_type_map_.end()) {return;}// 如果是 H264 报文,则可能要做 STAP-A、FU-A、Single 解包absl::optional<VideoRtpDepacketizer::ParsedRtpPayload> parsed_payload =type_it->second->Parse(packet.PayloadBuffer());if (parsed_payload == absl::nullopt) {return;}OnReceivedPayloadData(std::move(parsed_payload->video_payload), packet,parsed_payload->video_header);
}
8. 总结
ULPFEC 实现的核心是 FEC 保护比率的计算和掩码表的生成,FEC 保护比率决定了能使用多少 FEC 报文来保护原始报文,掩码表决定了 FEC 报文如何保护原始报文。围绕这两个核心概念,涉及如何生成 FEC 报文,如何打包和解包、如何发送和接收以及如何恢复原始报文等相关处理逻辑。基于 Parity Code 的 FEC 算法具有兼容性好、灵活性高、计算开销小等优点,但也有一些不足之处,相比 Red-Solomon 算法,其保护码率的信息冗余度更高,这样会带来更高的空间开销,而且生成应对各种丢包模式的掩码表及恢复逻辑也会比较复杂。
相关文章:
深入浅出WebRTC—ULPFEC
FEC 通过在发送端添加额外的冗余信息,使接收端即使在部分数据包丢失的情况下也能恢复原始数据,从而减轻网络丢包的影响。在 WebRTC 中,FEC 主要有两种实现方式:ULPFEC 和 FlexFEC,FlexFEC 是 ULPFEC 的扩展和升级&…...
SQL 在线格式化 - 加菲工具
SQL 在线格式化 打开网站 加菲工具 选择“SQL 在线格式化” 或者直接访问 https://www.orcc.online/tools/sql 输入sql,点击上方的格式化按钮即可 输入框得到格式化后的sql结果...
到达率的变化动态调整服务器的服务率,实现负载均衡,提高资源利用效率
中心可以根据任务到达率的变化动态调整服务器的服务率,实现负载均衡,提高资源利用效率 服务率和到达率 中心可以根据任务到达率的变化动态调整服务器的服务率,实现负载均衡,提高资源利用效率服务率(Service Rate)到达率(Arrival Rate)控制参数实现负载均衡的方法在云计…...
【Golang】Go语言编程思想(六):Channel,第四节,Select
使用 Select 如果此时我们有多个 channel,我们想从多个 channel 接收数据,谁来的快先输出谁,此时应该怎么做呢?答案是使用 select: package mainimport "fmt"func main() {var c1, c2 chan int // c1 and …...
认证插件介绍
本文档是针对 UOS 登录器插件给出开发指南,目的是为了让开发人员了解如何在 UOS 登录器上增加一种自定义认证方式,对插件接口做了详细说明以及实战练习。 文章目录 一、认证插件可以做什么?二、认证流程三、术语说明四、安全性五、可靠性六、…...
MindSearch深度解析实践
1. 课程内容 1.1 MindSearch 简介 MindSearch 是一个开源的 AI 搜索引擎框架,具有与 Perplexity.ai Pro 相同的性能。我们可以轻松部署它来构建自己的专属搜索引擎,可以基于闭源的LLM(如GPT、Claude系列),也可以使用…...
[oeasy]python052_[系统开发语言为什么默认是c语言
[系统开发语言为什么默认是c语言 [趣味拓展]c语言发祥史_c是如何成为第一系统语言的 上次我们了解了 标识符 要求 首字符 为 字母和下划线 后面字符 为 字母和下划线 外加 数字 添加图片注释,不超过 140 字(可选) 使用isidentifier函数 可…...
【java常用算法和应用场景】
java常用算法和应用场景 Java中常用的算法涵盖多个领域,包括排序算法、查找算法、字符串匹配算法、图论算法、动态规划算法、贪心算法、分治算法等。以下是Java中一些常用算法及其应用场景和示例代码: 一、排序算法 排序算法是计算机科学中的一种基本…...
D96【python 接口自动化学习】- pytest进阶之fixture用法
day96 pytest的fixture详解(三) 学习日期:20241211 学习目标:pytest基础用法 -- pytest的fixture详解(三) 学习笔记: fixture(scop"class") (scop"class") 每一个类调…...
知从科技总经理受邀参加上海临港新片区商会“湖畔TECS”技术分享沙龙(第五期)
11月26日,上海知从科技有限公司创始人陈荣波先生受邀出席临港新片区商会 “湖畔TECS”技术分享沙龙(第五期)活动,并在活动上为参会嘉宾们做了主题分享。本次活动由临港新片区商会主办,智能网联汽车创新联盟协办&#x…...
scala的泛型应用场景
用代码表示泛型类://泛型:类型参数化 //Pair约定一对数据 class Pair[T](var x:T,var y:T) //class 类名(成员名:数据类型)--->class 类名[泛型名](成员名:泛型名)/.参考代码&…...
三种策略改进的沙猫群优化算法(MSCSO)
三种策略改进的沙猫群优化算法(MSCSO) 目录 三种策略改进的沙猫群优化算法(MSCSO)效果一览基本介绍程序设计参考资料效果一览 基本介绍 改进点: 三角形游走策略Levy飞行游走策略透镜成像反向学习SCSO是 2022年提出的元启发式优化算法,该算法灵感来源于沙猫的捕食行为,沙猫群…...
Google Cloud Database Option(数据库选项说明)
关系数据库 在关系数据库中,信息存储在表、行和列中,这通常最适合结构化数据。因此,它们用于数据结构不经常更改的应用程序。与大多数关系数据库交互时使用 SQL(结构化查询语言)。它们为数据提供 ACID 一致性模式&am…...
java抽奖系统登录下(四)
6.4 关于登录 最简单的登录: 1、web登录页填写登录信息,前端发送登录信息到后端; 2、后端接受登录信息,并校验。校验成功,返回成功结果。 这种登录会出现一个问题,用户1成功登录之后,获取到后台…...
Linux入门攻坚——41、Linux集群系统入门-lvs(2)
lvs-dr:GATEWAY Director只负责请求报文,响应报文不经过Director,直接由RS返回给Client。 lvs-dr的报文路线如上图,基本思路就是报文不会回送Director,第①种情况是VIP、DIP、RIP位于同一个网段,这样&…...
Android Freezer
Freezer原理 Android按照优先级将一般的APP从高到低分为: 前台进程 --> 可感知进程–> 服务进程 --> Cached进程。 Freezer通过冻住cached进程,来迫使这些进程让出CPU,以达到优化系统资源使用的目的。 Cached进程是怎么判定的呢? 由于andro…...
GeeCache-单体并发缓存
实现LRU中value接口的缓存类 使用互斥锁封装LRU缓存类,实现并发访问 实现Group组,用名称对缓存分类 Getter为缓存击穿时调用的回调函数 若缓存击穿则调用回调函数,并把读取到的值加载到缓存中...
ctfshow-web 151-170-文件上传
151. 我们首先想到就是上传一句话木马。但是看源代码限制了png。 (1)改前端代码。 这里是前端限制了上传文件类型,那我们就改一下就好了嘛,改成php。 这里直接修改不行,给大家推荐一篇简短文章,大家就会了(…...
汽车车牌标记支持YOLO,COCO,VOC三种格式标记,4000张图片的数据集
本数据集支持YOLO,COCO,VOC三种格式标记汽车车牌,无论是新能源汽车还是油车都能识别标记,该数据集一共包含4000张图片 数据集分割 4000总图像数 训练组 70% 2800图片 有效集 20% 800图片 测…...
解决VSCode无法识别相对路径的问题
前言: 近日在学习python文件操作时,发现使用VSCode作为编辑器时,文件的相对路径会出问题,报错“指定路径下找不到文件”,无法找到想要的文件。 知识点①:不同操作系统所使用的路径斜杠不同:Lin…...
OCR 技术在验证码识别中的应用
OCR 技术在验证码识别中的应用 一、验证码识别的背景与挑战二、OCR 技术简介三、验证码识别的环境搭建四、使用 OCR 进行验证码识别的方法五、DdddOcr 子项在验证码识别中的应用六、验证码识别的应用场景与注意事项七、总结 在当今数字化时代,验证码作为一种安全验证…...
恶补英语初级第21天,《讨论天气变化》
对话 What’s the weather like in spring? It’s often windy in March. It’s always warm in April and May, but it rains sometimes. What’s it like in summer? It’s always hot in June, July and August. The sun shines every day. Is it cold or warm in autumn?…...
牛客网刷题SQL--高级查询
目录 SQL16--查找GPA最高值 描述 示例1 答案 其他方法: SQL17--计算男生人数以及平均GPA 描述 示例1 答案 SQL18--分组计算练习题 描述 示例1 答案 SQL19--分组过滤练习题 描述 示例1 答案 SQL20--分组排序练习题 描述 示例1 答案 SQL16--查找GP…...
用ffmpeg将MP4视频转换为m3u8格式
原文网址:用ffmpeg将MP4视频转换为m3u8格式_IT利刃出鞘的博客-CSDN博客 简介 说明 本文介绍如何使用ffmpeg将MP4视频转换为m3u8格式。 什么是m3u8 M3U8视频格式是一种M3U,只是它的编码格式是UTF-8格式。M3U用Latin-1字符集编码。M3U8格式特点是带有…...
【Qt】qt基础
目录 一、使用Qt Creator创建qt项目 二、项目文件解析 三、Qt中创建图形化界面的程序的两种方法 四、对象树 一、使用Qt Creator创建qt项目 1.选择项目模板 选中第一类模板Application(Qt应用程序,包含普通窗体程序和QtQuick程序), 然后选中右侧的第…...
VLC还是SmartPlayer?Windows平台RTSP播放器低延迟探讨
技术背景 好多开发者在用过大牛直播SDK的RTSP播放器后,都希望我们也分享下,如何在Windows平台实现低延迟的RTSP播放?低延迟的RTSP播放器,说起来容易做起来难,下面,我们从以下维度做个探讨: 播…...
极验决策引擎如何凭借独特优势,弯道超车传统风控?
前言 市场上的规则决策引擎产品众多,但大多局限于IP、设备、账号等层面,提供的是现成的风控标签和规则。然而,真正的风控,需要的不仅仅是标签和规则。 极验的业务规则决策引擎与众不同,这款决策引擎以界面流程编排为…...
Spring Boot集成Knife4j文档工具
Knife4j 搭建 Knife4j环境的的搭建和Swagger一样都比较简单,只需要极简的配置即可。 maven依赖 我使用的是较高版本的基于openapi规范的依赖包,OpenAPI2(Swagger)规范是Knife4j之前一直提供支持的版本,底层依赖框架为Springfox。 此次在4…...
html|本地实现浏览器m3u8播放器,告别网络视频卡顿
前言 网络上经常是以m3u8文件传输视频流的 ,但是有时网络慢往往导致视频播放卡顿。于是我在想能不能先下载然后再播放呢?于是尝试下载然后实现本地播放m3u8视频。 正文 1.找到网络视频流的m3u8连接 一般在浏览器按F12就可以看到有请求视频流的连接。 …...
vue3监听横向滚动条的位置;鼠标滚轮滑动控制滚动条滚动;监听滚动条到顶端
1.横向取值scrollLeft 竖向取值scrollTop 2.可以监听到最左最右侧 3.鼠标滚轮滑动控制滚动条滚动 效果 <template><div><div class"scrollable" ref"scrollableRef"><!-- 内容 --><div style"width: 2000px; height: 100…...
JSON
文章目录 一、概念1.json官网2.json的概念3.序列化方案:xml、json 二、json的数据类型1.json的键:必须是带双引号的字符串2.json的值:6种数据类型(1)对象 { }(2)数组 [ ] 三、Python中的json1.序列化:Python对象 转 json2.反序列化…...
前端常用的方法
时间处理 moment时间处理函数 // 时间日期相关常用的方法变量 import moment from moment;// 获取当前时间 moment export const nowDateMoment moment(new Date()); export const nowDateY moment(new Date()).format(YYYY); export const nowDateM moment(new Date()).f…...
JavaScript 对话框的魔法与艺术
在Web开发的世界里,JavaScript 对话框是用户与网页互动的重要桥梁。它们不仅能够提供即时反馈,还能收集用户的输入信息,从而增强用户体验。本文将深入探讨JavaScript对话框的各种类型、用法及其背后的原理,并通过丰富的实例展示如…...
java+springboot+mysql私人会所管理系统
项目介绍: 使用javaspringbootmysql开发的私人会所管理系统,系统包含管理员、技师、用户角色,功能如下: 管理员:用户管理;服务项目;技师管理;房间管理;预约管理&#x…...
Scrapy 爬虫框架全解析
一、Scrapy 框架概述 基本定义 Scrapy 是一个用 Python 编写的开源网络爬虫框架。它旨在快速、高效地抓取网页数据,可处理大规模的数据抓取任务。基于 Twisted 异步网络库构建,能够并发地处理多个请求,大大提高了数据抓取的速度。遵循 “请求…...
Hive3.X——异常处理Could not create ServerSocket on address 0.0.0.0/0.0.0.0:10000
Hive3.X——异常处理Could not create ServerSocket on address 0.0.0.0/0.0.0.0:10000 01 前言 大数据系列,学到了Hive,搭建环境的时候,因为使用的是本机WSL2(别问为啥不用VMware,问就是条件有限,而且WS…...
【跨库查询、多库查询】.NET开源 ORM 框架 SqlSugar 系列
文章目录 一、跨库方式1:跨库导航二、手动跨库查询三、同服务器:自动查询跨库查询3.1 Mysql和SqlServer自动3.2 自动: PgSql跨Scheme查询3.3 其他库同服务器 四、跨服务器:自动跨库查询4.1 配置SqlServer dblink4.2 配置 Oracle dblink4.3 配…...
【JAVA】Java项目实战—Java 数据库应用项目:学生信息管理系统
本项目将实现一个简单的学生信息管理系统,功能包括学生信息的录入、查询、修改和删除。通过本项目,读者将深入理解Java与数据库交互的基本原理,掌握JDBC(Java Database Connectivity)技术,以及如何构建一个…...
中电金信携手中远海科,共启贸易金融数智新篇章
在数智化转型成为驱动经济社会高质量发展的新引擎背景下,“数智方案”栏目聚焦金融等国计民生重点行业场景,依托中电金信“源启筑基咨询引领应用重构”的产品及服务体系,输出市场洞察和行业解决方案、应用案例,旨在全面推动行业IT…...
有没有办法让爬虫更加高效,比如多线程处理?
要让Python爬虫更加高效,确实可以采用多线程处理。多线程可以显著提高爬虫的效率,因为它允许程序同时执行多个任务,从而减少等待时间。以下是一些提高爬虫效率的方法,特别是通过多线程技术: 1. 多线程爬虫 多线程爬虫…...
Android历史版本主要更新说明
Android 15 Android 15 继续致力于构建注重隐私和安全保护的平台,助您提高效率,同时还引入了多项新功能,帮您打造精美应用、卓越的媒体和相机体验,并提供直观的用户体验。在平板电脑和可折叠设备上更能凸显出这些优势。 Android…...
测试岗位应该学什么
以下是测试岗位需要学习的一些关键内容: 1. 测试理论和方法 - 了解不同类型的测试,如功能测试、性能测试、压力测试、安全测试、兼容性测试等。 - 掌握测试策略和测试计划的制定。 2. 编程语言 - 至少熟悉一种编程语言,如 Python、Java…...
华为HarmonyOS NEXT 原生应用开发: 数据持久化存储(用户首选项)的使用 token令牌存储鉴权!
Preferences 数据持久化存储 用户首选项(Preferences) 1. 封装 仓库工具类 ● 这里可以选择将 数据字段 key 抽取为一个静态方法,这里选择让用户传参,看起来较容易理解! /*** 首选项 preferences - 实现数据持久化…...
【AIStarter】告别复杂转换 - MinerU整合包实现PDF到Markdown的无缝转变
在数字化时代,信息的传递与共享变得愈发重要。文档格式之间的转换成为了日常工作中不可或缺的一部分。为了满足用户对高效工作流程的需求,新版MinerU整合包应运而生,它不仅简化了从PDF到Markdown的转换过程,还为用户带来了前所未有…...
Jenkins参数化构建详解(This project is parameterized)
本文详细介绍了Jenkins中不同类型的参数化构建方法,包括字符串、选项、多行文本、布尔值和git分支参数的配置,以及如何使用ActiveChoiceParameter实现动态获取参数选项。通过示例展示了传统方法和声明式pipeline的语法 文章目录 1. Jenkins的参数化构建1…...
服务器批量清理redis keys,无法适用客户端必须直连的情况
在 Redis 中,批量清理指定模式的键(例如 memberCardData:*)可以通过多种方法来实现。需要注意的是,Redis 的命令执行是单线程的,因此对大量键进行操作时可能会阻塞服务器。以下是几种常见的方法: shell K…...
单元测试SpringBoot
添加测试专用属性 加载测试专用bean Web环境模拟测试 数据层测试回滚 测试用例数据设定...
牛客网刷题 | BC126 小乐乐查找数字
😁博客主页😁:🚀从0至1-CSDN博客🚀 🤑博客内容🤑:🍭C语言、C、数据结构、嵌入式、Linux🍭 😎本文内容🤣:🍭BC1…...
Node一、fs 模块、path 模块、端口号、 http 模块、
一、Node.js了解 Node.js是一个跨平台JavaScript运行环境,使开发者可以搭建服务器端的JavaScript应用程序。 概念:使用 Node.js 编写后端程序 / 支持前端工程化 ✓ 后端程序:提供接口和数据,网页资源等 ✓ 前端工程化 &#x…...
k8s service 配置AWS nlb load_balancing.cross_zone.enabled
在Kubernetes中配置NLB(Network Load Balancer)的跨区域负载均衡(cross-zone load balancing),需要使用服务注解(service annotations)来实现。根据AWS官方文档,以下是配置NLB跨区域…...