流媒体学习之路(WebRTC)——GCC分析(4)
流媒体学习之路(WebRTC)——GCC分析(4)
——
我正在的github给大家开发一个用于做实验的项目 —— github.com/qw225967/Bifrost目标:可以让大家熟悉各类Qos能力、带宽估计能力,提供每个环节关键参数调节接口并实现一个json全配置,提供全面的可视化算法观察能力。欢迎大家使用
——
文章目录
- 流媒体学习之路(WebRTC)——GCC分析(4)
- 一、间隔计算(InterArrival)
- 1.1 模块介绍
- 1.2 代码
- 二、码率控制(AimdRateControl)
- 2.1 背景
- 2.2 代码
- 三、总结
在讲具体内容之前插一句嘴,从GCC分析(3)开始,我们将针对GCC的实现细节去分析它设计的原理,让我们理解这些类存在的意义,不再带大家去串具体的流程了。
一、间隔计算(InterArrival)
1.1 模块介绍
WebRTC 的 InterArrival 类是用于计算包之间的到达时间差(Inter-Arrival Time)的类。 如果观察WebRTC的提交记录你会发现,这个类随着卡尔曼滤波器、趋势线等等算法的变更也一直在调整。那么为什么要存在这个接收间隔的计算类呢?
细心的小伙伴在观察我们发送视频数据的时候会发现,数据的发送是一股一股的——常常是一次发送几个包。
这是因为我们采集的数据帧大小是实时变化的每次可发送的数据量都不一样,而pacer发送是依赖于定时器去触发发送事件的,这样的触发模式有两种:周期模式(kPeriodic)、动态模式(kDynamic)——后来好像动态模式的代码被移除了,但是我们今天不是关注pacer的问题,而是补充一些小知识。
周期模式中,定时5ms会触发一次发送;
动态模式中,每次都会计算下一次触发发送的时间;
可以直观的理解为,当数据在接收端产生接收间隔增大时,这个间隔不仅仅是网络导致的,还有可能是发送时就已经造成了间隔增大。那么我们想把这个间隔计算出来也就需要发送端记录自己的发送时间了,InterArrival这个类就做这些变化的校准。
1.2 代码
该部分最重要的代码是在延迟估计IncomingPacketFeedback调用的,下面展示一部分伪代码
void DelayBasedBwe::IncomingPacketFeedback(const PacketResult& packet_feedback,Timestamp at_time) {
...uint32_t ts_delta = 0;int64_t t_delta = 0;int size_delta = 0;// 校准接收间隔bool calculated_deltas = inter_arrival_->ComputeDeltas(timestamp, packet_feedback.receive_time.ms(), at_time.ms(),packet_feedback.sent_packet.size.bytes(), &ts_delta, &t_delta,&size_delta);double ts_delta_ms = (1000.0 * ts_delta) / (1 << kInterArrivalShift);// 把间隔放入检测器进行拥塞检测delay_detector_->Update(t_delta, ts_delta_ms,packet_feedback.sent_packet.send_time.ms(),packet_feedback.receive_time.ms(), calculated_deltas);
}
ComputeDeltas里面细分了每个包组的概念,将包组的间隔区分出来保证我们计算的准确度:
bool InterArrival::ComputeDeltas(uint32_t timestamp, int64_t arrival_time_ms,int64_t system_time_ms, size_t packet_size,uint32_t* timestamp_delta,int64_t* arrival_time_delta_ms,int* packet_size_delta) {// 传入参数:// timestamp 数据发送的时间戳// arrival_time_ms 对端接收到数据的时间戳// system_time_ms 当前系统时间,其实是feedback接到的时间// packet_size 当前数据包的大小// 输出内容 —— timestamp_delta 发送间隔// 输出内容 —— arrival_time_delta_ms 接收间隔// 输出内容 —— packet_size_delta 两个包组直接包数量的差值bool calculated_deltas = false;// 第一个包组,记录信息if (current_timestamp_group_.IsFirstPacket()) {// We don't have enough data to update the filter, so we store it until we// have two frames of data to process.current_timestamp_group_.timestamp = timestamp;current_timestamp_group_.first_timestamp = timestamp;current_timestamp_group_.first_arrival_ms = arrival_time_ms;// 连续包组返回} else if (!PacketInOrder(timestamp, arrival_time_ms)) {return false;// 新包组开始计算间隔} else if (NewTimestampGroup(arrival_time_ms, timestamp)) {// First packet of a later frame, the previous frame sample is ready.// 第一个包组不计算,后续包组开始计算if (prev_timestamp_group_.complete_time_ms >= 0) {// 记录包组之间的发送间隔*timestamp_delta =current_timestamp_group_.timestamp - prev_timestamp_group_.timestamp;// 记录包组之间的接收间隔*arrival_time_delta_ms = current_timestamp_group_.complete_time_ms -prev_timestamp_group_.complete_time_ms;// Check system time differences to see if we have an unproportional jump// in arrival time. In that case reset the inter-arrival computations.// 计算系统时间变化,防止系统时间跳变影响计算int64_t system_time_delta_ms =current_timestamp_group_.last_system_time_ms -prev_timestamp_group_.last_system_time_ms;if (*arrival_time_delta_ms - system_time_delta_ms >=kArrivalTimeOffsetThresholdMs) {Reset();return false;}// 对端的接收时间戳可能已经生变化,影响计算if (*arrival_time_delta_ms < 0) {// The group of packets has been reordered since receiving its local// arrival timestamp.++num_consecutive_reordered_packets_;if (num_consecutive_reordered_packets_ >= kReorderedResetThreshold) {Reset();}return false;} else {num_consecutive_reordered_packets_ = 0;}// 计算两个包组的数据量差值*packet_size_delta = static_cast<int>(current_timestamp_group_.size) -static_cast<int>(prev_timestamp_group_.size);calculated_deltas = true;}// 更新数据prev_timestamp_group_ = current_timestamp_group_;// The new timestamp is now the current frame.current_timestamp_group_.first_timestamp = timestamp;current_timestamp_group_.timestamp = timestamp;current_timestamp_group_.first_arrival_ms = arrival_time_ms;current_timestamp_group_.size = 0;} else {current_timestamp_group_.timestamp =LatestTimestamp(current_timestamp_group_.timestamp, timestamp);}// Accumulate the frame size.current_timestamp_group_.size += packet_size;current_timestamp_group_.complete_time_ms = arrival_time_ms;current_timestamp_group_.last_system_time_ms = system_time_ms;return calculated_deltas;
}bool InterArrival::PacketInOrder(uint32_t timestamp, int64_t arrival_time_ms) {if (current_timestamp_group_.IsFirstPacket()) {return true;} else if (arrival_time_ms < 0) {// NOTE: Change related to// https://github.com/versatica/mediaproxy/issues/357//// Sometimes we do get negative arrival time, which causes BelongsToBurst()// to fail, which may cause anything that uses InterArrival to crash.//// Credits to @sspanak and @Ivaka.return false;} else {// Assume that a diff which is bigger than half the timestamp interval// (32 bits) must be due to reordering. This code is almost identical to// that in IsNewerTimestamp() in module_common_types.h.uint32_t timestamp_diff =timestamp - current_timestamp_group_.first_timestamp;const static uint32_t int_middle = 0x80000000;// 处理跳变if (timestamp_diff == int_middle) {return timestamp > current_timestamp_group_.first_timestamp;}return timestamp_diff < int_middle;}
}// Assumes that |timestamp| is not reordered compared to
// |current_timestamp_group_|.
bool InterArrival::NewTimestampGroup(int64_t arrival_time_ms,uint32_t timestamp) const {if (current_timestamp_group_.IsFirstPacket()) {return false;// 计算突发数据,确认突发数据直接返回} else if (BelongsToBurst(arrival_time_ms, timestamp)) {return false;} else {// 差值大于5ms就认为是下一个发送周期uint32_t timestamp_diff =timestamp - current_timestamp_group_.first_timestamp;return timestamp_diff > kTimestampGroupLengthTicks;}
}bool InterArrival::BelongsToBurst(int64_t arrival_time_ms,uint32_t timestamp) const {if (!burst_grouping_) {return false;}// 计算于上一个发送时间、接收时间的差值int64_t arrival_time_delta_ms =arrival_time_ms - current_timestamp_group_.complete_time_ms;uint32_t timestamp_diff = timestamp - current_timestamp_group_.timestamp;// 当发送间隔转为ms后差值在0.5浮动范围内int64_t ts_delta_ms = timestamp_to_ms_coeff_ * timestamp_diff + 0.5;if (ts_delta_ms == 0) return true;// 一旦接收间隔比发送间隔加上浮动值0.5还小,证明这些包连续发送int propagation_delta_ms = arrival_time_delta_ms - ts_delta_ms;if (propagation_delta_ms < 0 && /* 连续发送 */arrival_time_delta_ms <= kBurstDeltaThresholdMs && /* 处于同一发送周期 */arrival_time_ms - current_timestamp_group_.first_arrival_ms <kMaxBurstDurationMs) /* 最大异常值限制 */return true;return false;
}
经过该模块后就会进入TrendLine模块进行趋势计算,获得当前的拥塞情况。
二、码率控制(AimdRateControl)
2.1 背景
本系列文章GCC(3)提到了ack计算模块以及链路容量计算模块,事实上这两个模块在码率控制类中计算出了最终的码率。ack链路容量计算出完后输入到码率计算模块,码率区分上涨、下调、维持不变三种情况,他们所有的状态变化都取决于趋势线计算出来的状态。
状态机切换大家都很熟悉,就是下面这张图(来自于谷歌公开的Performance Analysis of Google Congestion Control Algorithm for WebRTC文章):
网络状态分为:
enum class BandwidthUsage {// 网络正常kBwNormal = 0,// 网络过度使用kBwUnderusing = 1,// 网络正在排空kBwOverusing = 2,kLast
};
把趋势线的斜率图像化,可以看出来这几个状态在整个趋势计算的过程中是下图这样变化的:
之所以这样设计是因为,GCC思想希望保证整体的流畅性,整体非常敏感。码率下调很快,上涨则是比较缓慢的。状态上,随时可以进入overusing状态,但是上涨却不是立刻进行的,而是先转入hold状态再继续上探。
2.2 代码
这里展示一下代码中状态切换的代码:
void AimdRateControl::ChangeState(const RateControlInput& input,Timestamp at_time) {switch (input.bw_state) {case BandwidthUsage::kBwNormal:// 只有hold状态能进入码率上涨if (rate_control_state_ == kRcHold) {time_last_bitrate_change_ = at_time;rate_control_state_ = kRcIncrease;}break;case BandwidthUsage::kBwOverusing:// 出现网络过度使用时下调码率if (rate_control_state_ != kRcDecrease) {rate_control_state_ = kRcDecrease;}break;case BandwidthUsage::kBwUnderusing:// 当网络开始排空时先转成hold状态rate_control_state_ = kRcHold;break;default:break;}
}
码率计算:
DataRate AimdRateControl::ChangeBitrate(DataRate new_bitrate,const RateControlInput& input,Timestamp at_time) {// 取出吞吐量估计值DataRate estimated_throughput =input.estimated_throughput.value_or(latest_estimated_throughput_);if (input.estimated_throughput)latest_estimated_throughput_ = *input.estimated_throughput;// An over-use should always trigger us to reduce the bitrate, even though// we have not yet established our first estimate. By acting on the over-use,// we will end up with a valid estimate.// 初始阶段,只要不是网络拥塞就不进行以下逻辑计算if (!bitrate_is_initialized_ &&input.bw_state != BandwidthUsage::kBwOverusing)return current_bitrate_;// 状态切换ChangeState(input, at_time);switch (rate_control_state_) {// hold状态直接返回case kRcHold:break;case kRcIncrease:// 吞吐量估计大于了链路容量统计,则重置容量统计if (estimated_throughput > link_capacity_.UpperBound())link_capacity_.Reset();// Do not increase the delay based estimate in alr since the estimator// will not be able to get transport feedback necessary to detect if// the new estimate is correct.// alr状态下可以根据no_bitrate_increase_in_alr_决定是否继续进行码率增长// 当alr状态下持续码率增长,一旦出现码率暴增发送码率就会爆发式增大if (!(send_side_ && in_alr_ && no_bitrate_increase_in_alr_)) {// 计算出链路容量则进入加性增,因为当前瓶颈已知if (link_capacity_.has_estimate()) {// The link_capacity estimate is reset if the measured throughput// is too far from the estimate. We can therefore assume that our// target rate is reasonably close to link capacity and use additive// increase.DataRate additive_increase =AdditiveRateIncrease(at_time, time_last_bitrate_change_);new_bitrate += additive_increase;} else {// If we don't have an estimate of the link capacity, use faster ramp// up to discover the capacity.// 未存在里哪路容量则需要乘性增去做探测DataRate multiplicative_increase = MultiplicativeRateIncrease(at_time, time_last_bitrate_change_, new_bitrate);new_bitrate += multiplicative_increase;}}time_last_bitrate_change_ = at_time;break;case kRcDecrease:// TODO(srte): Remove when |estimate_bounded_backoff_| has been validated.// 取当前链路容量的小值与吞吐量对比取大值,用于激进地下调码率if (network_estimate_ && capacity_deviation_ratio_threshold_ &&!estimate_bounded_backoff_) {estimated_throughput = std::max(estimated_throughput,network_estimate_->link_capacity_lower);}if (estimated_throughput > low_throughput_threshold_) {// Set bit rate to something slightly lower than the measured throughput// to get rid of any self-induced delay.// 新的码率需要略低于吞吐量,避免引入新的排队导致延迟new_bitrate = estimated_throughput * beta_;if (new_bitrate > current_bitrate_) {// Avoid increasing the rate when over-using.// 当此时新的码率仍然高于当前码率,则根据链路容量重新设置新码率if (link_capacity_.has_estimate()) {new_bitrate = beta_ * link_capacity_.estimate();}}// estimate_bounded_backoff_ 称为边界避退,目的是标记在链路容量下限高于当前容量时,使用链路容量下限if (estimate_bounded_backoff_ && network_estimate_) {new_bitrate = std::max(new_bitrate, network_estimate_->link_capacity_lower * beta_);}} else {// 吞吐量小于低吞吐的阈值,则直接使用吞吐量new_bitrate = estimated_throughput;// 已经估计出带宽则取吞吐量、带宽的最大值if (link_capacity_.has_estimate()) {new_bitrate = std::max(new_bitrate, link_capacity_.estimate());}// 超过吞吐阈值都使用阈值new_bitrate = std::min(new_bitrate, low_throughput_threshold_.Get());}// 如果当前码率已经很小,则继续使用当前码率 new_bitrate = std::min(new_bitrate, current_bitrate_);if (bitrate_is_initialized_ && estimated_throughput < current_bitrate_) {// 有可能存在过度下降码率的情况,一旦超过下降的90%,则不使用该码率constexpr double kDegradationFactor = 0.9;if (smoothing_experiment_ &&new_bitrate < kDegradationFactor * beta_ * current_bitrate_) {// If bitrate decreases more than a normal back off after overuse, it// indicates a real network degradation. We do not let such a decrease// to determine the bandwidth estimation period.last_decrease_ = absl::nullopt;} else {// 记录该下降的码率last_decrease_ = current_bitrate_ - new_bitrate;}}if (estimated_throughput < link_capacity_.LowerBound()) {// The current throughput is far from the estimated link capacity. Clear// the estimate to allow an immediate update in OnOveruseDetected.link_capacity_.Reset();}// 更新状态记录bitrate_is_initialized_ = true;link_capacity_.OnOveruseDetected(estimated_throughput);// Stay on hold until the pipes are cleared.rate_control_state_ = kRcHold;time_last_bitrate_change_ = at_time;time_last_bitrate_decrease_ = at_time;break;default:break;}// 选择码率return ClampBitrate(new_bitrate, estimated_throughput);
}DataRate AimdRateControl::ClampBitrate(DataRate new_bitrate,DataRate estimated_throughput) const {// Allow the estimate to increase as long as alr is not detected to ensure// that there is no BWE values that can make the estimate stuck at a too// low bitrate. If an encoder can not produce the bitrate necessary to// fully use the capacity, alr will sooner or later trigger.if (!(send_side_ && no_bitrate_increase_in_alr_)) {// Don't change the bit rate if the send side is too far off.// We allow a bit more lag at very low rates to not too easily get stuck if// the encoder produces uneven outputs.// 每次上涨有一个最大的上涨限度,1.5 * 吞吐量 + 10kbps,避免超出吞吐量上涨过多const DataRate max_bitrate =1.5 * estimated_throughput + DataRate::kbps(10);if (new_bitrate > current_bitrate_ && new_bitrate > max_bitrate) {new_bitrate = std::max(current_bitrate_, max_bitrate);}}if (network_estimate_ &&(estimate_bounded_increase_ || capacity_limit_deviation_factor_)) {DataRate upper_bound = network_estimate_->link_capacity_upper;new_bitrate = std::min(new_bitrate, upper_bound);}new_bitrate = std::max(new_bitrate, min_configured_bitrate_);return new_bitrate;
}DataRate AimdRateControl::MultiplicativeRateIncrease(Timestamp at_time, Timestamp last_time, DataRate current_bitrate) const {// 1.08这个参数与丢包估计的上涨参数一样double alpha = 1.08;if (last_time.IsFinite()) {auto time_since_last_update = at_time - last_time;alpha = pow(alpha, std::min(time_since_last_update.seconds<double>(), 1.0));}DataRate multiplicative_increase =std::max(current_bitrate * (alpha - 1.0), DataRate::bps(1000));return multiplicative_increase;
}DataRate AimdRateControl::AdditiveRateIncrease(Timestamp at_time,Timestamp last_time) const {double time_period_seconds = (at_time - last_time).seconds<double>();double data_rate_increase_bps =GetNearMaxIncreaseRateBpsPerSecond() * time_period_seconds;return DataRate::bps(data_rate_increase_bps);
}double AimdRateControl::GetNearMaxIncreaseRateBpsPerSecond() const {// RTC_DCHECK(!current_bitrate_.IsZero());// 加性增以固定的15帧换算帧间隔,最终根据帧间隔计算出一个大致的平均包大小const TimeDelta kFrameInterval = TimeDelta::seconds(1) / 15;DataSize frame_size = current_bitrate_ * kFrameInterval;const DataSize kPacketSize = DataSize::bytes(1200);double packets_per_frame = std::ceil(frame_size / kPacketSize);DataSize avg_packet_size = frame_size / packets_per_frame;// Approximate the over-use estimator delay to 100 ms.// 使用平均包大小换算出每个计算周期的增长值,最大为4kbpsTimeDelta response_time = rtt_ + TimeDelta::ms(100);if (in_experiment_) response_time = response_time * 2;double increase_rate_bps_per_second =(avg_packet_size / response_time).bps<double>();double kMinIncreaseRateBpsPerSecond = 4000;return std::max(kMinIncreaseRateBpsPerSecond, increase_rate_bps_per_second);
}
三、总结
本文接着前面提到的码率控制,讲述了计算包组间隔的类,该类用于校准发送间隔在周期发送中的误差。后续接着上一篇GCC介绍讲了经过ack模块后,码率是怎么计算出来的。在什么情况下它会上涨?什么情况下会下跌?并且结合状态机图给大家分析了GCC上涨和下降的规律,发现它是个激进下降,缓慢上涨的拥塞控制算法。这样做的好处是:可以尽最大能力保证播放的流畅度,提高交互能力避免微小的拥塞导致卡顿。
相关文章:
流媒体学习之路(WebRTC)——GCC分析(4)
流媒体学习之路(WebRTC)——GCC分析(4) —— 我正在的github给大家开发一个用于做实验的项目 —— github.com/qw225967/Bifrost目标:可以让大家熟悉各类Qos能力、带宽估计能力,提供每个环节关键参数调节接口并实现一个json全配置…...
k8s持久化存储(NFS-StorageClass)
一、StatefulSet由以下几个部分组成: 用于定义网络标志(DNS domain)的Headless Service用于创建PersistentVolumes的volumeClaimTemplates定义具体应用的StatefulSet 二、StatefulSet 特点 StatefulSet 适用于有以下某个或多个需求的应用&a…...
java servlet软件缺陷库管理系统Myeclipse开发mysql数据库web结构java编程计算机网页项目
一、源码特点 java servlet软件缺陷库管理系统是一套完善的java web信息管理系统 系统采用serlvetdaobean(mvc模式),对理解JSP java编程开发语言有帮助,系统具有完整的源代码和数据库,系统主要采用B/S模式开发。开发环境为TOM…...
19|BabyAGI:根据气候变化自动制定鲜花存储策略
19|BabyAGI:根据气候变化自动制定鲜花存储策略 随着 ChatGPT 的崭露头角,我们迎来了一种新型的代理——Autonomous Agents(自治代理或自主代理)。这些代理的设计初衷就是能够独立地执行任务,并持续地追求长…...
面试经典150题(62-64)
leetcode 150道题 计划花两个月时候刷完,今天(第三十天)完成了3道(62-64)150: 62.(226. 翻转二叉树)题目描述: 给你一棵二叉树的根节点 root ,翻转这棵二叉树,并返回其…...
流量困境下,2024年餐饮商家的直播带货生意到底怎么做?
据官方数据显示,截至2023年2月,抖音生活服务餐饮商家直播间数量达到43万,2023年7月,抖音生活服务餐饮行业自播商家数较1月增长134%。可以说,直播带货已经成为餐饮商家的常态化的线上营销模式,也成为各大餐饮…...
C++ 具名要求-基本概念-指定该类型对象可以默认构造
指定该类型对象可以默认构造 要求 以下情况下,类型 T 满足可默认构造 (DefaultConstructible) : 给定 任意标识符 u, 下列表达式必须合法且拥有其指定的效果 表达式后条件T u对象 u 被默认初始化。T u{}对象 u 被值初始化或聚合初始化。…...
T527 Android13遥控适配
T527 Android13遥控的适配和官方提供的文档有些不一样,按照官方的文档不能够正常适配到自己的遥控器。 首先确保驱动是否有打开CONFIG_AW_IR_RX和CONFIG_RC_DECODERSy 以及CONFIG_IR_NEC_DECODERm,这个可以在longan/out/t527对应的目录下的.config查看是…...
第三部分使用脚手架:vue学习(61-65)
文章目录 61 创建vue脚手架![在这里插入图片描述](https://img-blog.csdnimg.cn/direct/f71d4324be0542209e690ab9e886d199.png)62 分析脚手架结构63 render函数64 修改默认配置65 ref 属性 61 创建vue脚手架 写完vue文件,没有脚手架做翻译,浏览器不认识…...
【Linux学习笔记】解析Linux系统内核:架构、功能、工作原理和发展趋势
操作系统是一个用来和硬件打交道并为用户程序提供一个有限服务集的低级支撑软件。一个计算机系统是一个硬件和软件的共生体,它们互相依赖,不可分割。计算机的硬件,含有外围设备、处理器、内存、硬盘和其他的电子设备组成计算机的发动机。但是…...
springboot连接oracle报错ORA-12505解决方案
springboot连接oracle报错ORA-12505解决方案 springboot项目,在测试环境连接正常,生产环境连接数据库报错ORA-12505。 测试环境连接数据库语句为jdbc:oracle:thin:xxxx.xxxx.xxxx.xxxx:1521:orcl 生产环境修改对应ip后报错ORA-12505, TNS:listener does…...
服务器为什么大多用 Linux?
服务器为什么大多用 Linux? 在开始前我有一些资料,是我根据自己从业十年经验,熬夜搞了几个通宵,精心整理了一份「Linux的资料从专业入门到高级教程工具包」,点个关注,全部无偿共享给大家!&#…...
C++上位软件通过Snap7开源库访问西门子S7-200/合信M226ES数据块的方法
前言 上一篇文章中介绍了Snap7访问西门子S7-1200/S7-1500 DB块的方法,对于S7-200PLC是没有数据块访问的。S7-200PLC中Snap7只能通过访问MB块,VB块的方法进行和PLC之间的Snap7通信和数据交换。手头没有S7-200PLC故通过合信CTMC M226ES运动控制器进行测试&…...
通信及信号处理领域期刊影响因子、分区及期刊推荐-2024版
期刊名IF(202401)中科院分区(20231227)备注IEEE Journal on Selected Areas in Communications16.4计算机科学1区Top通信顶刊IEEE Transactions on Signal Processing5.4工程技术2区Top信号处理顶刊IEEE Transactions on Information Theory2.5计算机科学3区信息论顶刊IEEE Tra…...
cfa一级考生复习经验分享系列(十五)
备考背景: 本科211石油理科背景;无金融方面专业知识及工作经验;在职期间备考;有效备考时间2个月;12月一级考试10A。 复习进度及教材选择 首先说明,关于教材的经验分享针对非金融背景考生。 第一阶段&#x…...
如潮好评!优秀选手视角下的第二届粤港澳大湾区(黄埔)国际算法算例大赛
为发挥国家实验室作用、推动地区大数据与人工智能算法的生态体系建设,琶洲实验室(黄埔)受广州市黄埔区政府委托,于 2022 年创办粤港澳大湾区(黄埔)国际算法算例大赛,推动原始创新、赋能社会经济…...
软件测试之冒烟测试
一、什么是冒烟测试 这一术语源自硬件行业。对一个硬件或硬件组件进行更改或修复后,直接给设备加电。如果没有冒烟,则该组件就通过了测试。在软件中,“冒烟测试”这一术语描述的是在将代码更改嵌入到产品的源树中之前对这些更改进行验证的过…...
NE555学习笔记-2024
实物图片 NE555引脚图 内部时序图 示列1,红外接收电路 红外接收电路的工作原理:在上述电路中,TSOP1738构成了该电路的主要组成部分,旨在检测来自任何来源的红外信号。这用于检测38 KHz范围的信号,因此命名为“TSOP173…...
记一次docker中安装redis的过程
1. Docker搜索redis镜像 docker search redis2. Docker搜索redis镜像 docker pull redis3.Docker挂载配置文件 挂载 redis 的配置文件挂载 redis 的持久化文件(为了数据的持久化)。 conf文件位置: /home/redis/myredis/redis.conf data文件…...
Matlab进阶绘图第37期—多色悬浮柱状图
多色悬浮柱状图是一种特殊的柱状图。 与常规柱状图相比,多色悬浮柱状图可以通过悬浮的矩形展示最小值到最大值的范围(或其他范围表达),并通过颜色进行美化/区分/附加信息。 本文使用自己制作的Floatingbar小工具进行多色悬浮柱状…...
【嵌入式】About USB Powering
https://www.embedded.com/usb-type-c-and-power-delivery-101-power-delivery-protocol/https://www.embedded.com/usb-type-c-and-power-delivery-101-power-delivery-protocol/ Type-C接口有多强?PD协议又是什么?-电子发烧友网由于Type-C接口自身的强…...
MySQL——事物
目录 一.发现问题 二.什么时事物 三.事务提交方式 四.事物的常规操作方式 五. 事务隔离级别 1.如何理解隔离性 2.隔离级别 3.查看与设置隔离性 4.读未提交【Read Uncommitted】 5.读提交【Read Committed】 6.可重复读【Repeatable Read】 7.串行化【serializabl…...
Windows搭建RTSP视频流服务(EasyDarWin服务器版)
文章目录 引言1、安装FFmpeg2、安装EasyDarWin3、实现本地\虚拟摄像头推流服务4、使用VLC或PotPlayer可视化播放器播放视频5、RTSP / RTMP系列文章 引言 RTSP和RTMP视频流的区别 RTSP (Real-Time Streaming Protocol)实时流媒体协议。 RTSP定义流格式&am…...
私域营销,让你的生意从线下到线上都火爆!
一、提高流量 获取流量的五个关键要素:目标受众、传播渠道、内容策略、短期利益和长期价值 在当今竞争激烈的市场环境中,获取流量是每个企业和个人都必须面对的挑战。为了有效地吸引潜在客户,我们需要关注五个关键要素:目标受众…...
MATLAB基本绘图操作(二维和三维绘图)
MATLAB基本绘图操作 文章目录 MATLAB基本绘图操作1、二维平面绘图1.1、线条(折线图)1.2、条形图1.3、极坐标图1.4、散点图 2、三维立体绘图2.1、三维曲面图2.2、三维曲线图(点图) 3、图片分区(子图) 1、二维…...
中国社科院与新加坡新跃社科联合培养博士—攻读在职博士的理由是什么?
当今社会,很多人已经深刻的意识到知识是改变自己命运的重要途径,在职教育的完善让很多在职人员选择此方式进修,那么为什么要选择攻读在职博士?中国社科院与新加坡新跃社科联合培养博士与您一起走进,在职博士。 那么其…...
T40N 君正智能处理器T40 BGA 芯片
T40N是一款智能视频应用处理器,适用于移动摄像机、安防等视频设备调查、视频聊天、视频分析等。该SoC引入了一种创新的体系结构满足高性能计算和高质量图像和视频编码的要求通过视频设备解决。T40N提供高速CPU计算能力,出色的图像信号过程中,…...
Profinet转Modbus使设备互联更简单
在工业自动化领域,为了使设备互联更加简单,可以将Modbus和Profinet相互转换。这样一来,设备之间的互联就变得更加方便和高效。通过Modbus转Profinet网关(XD-MDPN100/200),设备之间的通信变得更加灵活和可靠…...
Android 文字垂直排列,文字向右旋转90度
public class VerticalTextView extends View {private final int ROTATION_ANGLE 90; // 旋转角度,用于将文本垂直排列private String text; // 要显示的文本private TextPaint textPaint; // 用于绘制文本的画笔private Rect textBounds;// 文本边界float x, y;/…...
域名授权验证系统PHP源码,盗版追踪、双重授权和在线加密功能,附带安装教程
源码介绍 PHP域名授权验证系统是一个功能强大的系统,提供了多项功能来保护你的域名和软件的合法性。它包括盗版追踪、域名IP双重授权、在线加密等功能,同时还提供了PHP授权验证更新系统的完整版,方便你进行一键更新和生成自助授权。 盗版追…...
17网站一起做网店普宁轻纺城温馨/win10优化大师好用吗
想必大家都想好好的学习一下CMD中的命令吧,但面对黑黑的屏幕毕竟是计算机高手的专利,如果想自学一下又觉得力不从心,下面就让我为大 家介绍一下CMD中的一些命令及参数。在这里只对以前杂志上没有提到或很少提到的命令进行一下补充࿰…...
闵行区做网站/哔哩哔哩推广网站
一.Core标签库 • 核心标签库主要包括通用标签、条件标签、迭代标签和与URL相关的标签。 • 在使用Core标签库的JSP文件的开始部分,添加代码: <%taglib uri"http://java.sun.com/jsp/jstl/core" prefix"c"…...
v2ray wordpress/哪里有永久免费建站
文章目录(1)什么是等待activity出现(2)wait_activity()方法(3)获取当前页面的activity方法(4)综合练习(1)什么是等待activity出现 在启动app的时候ÿ…...
qq教程网站源码/网络营销是什么意思
数据库中char、varchar、varchar2、nvarchar之间的关系 分类: 数据库 varchar varchar2 nvarchar2012-12-06 11:27 2849人阅读 评论(0) 收藏 举报数据库中的char型,用于存储定长字符串,效率比varchar要稍高,但是,占用空…...
oracle数据库网站开发/今天的新闻是什么
之前tensorflow的检测环境一直没有用gpu版的 后来在python3.6cuda8.0cudnn6下装tensorflow1.2/1.3/1.4都有问题报错 ①:1.2、1.3的gpu版在import tensorflow时就直接报错 ②:1.4的TensorFlow-gpu可以import,但是在运行faster的时候就有问…...
wordpress手机版网页/网络营销都具有哪些功能
颜色、样式和阴影 fillStyle 设置或返回用于填充绘画的颜色、渐变或模式 strokeStyle 设置或返回用于笔触的颜色、渐变或模式 shadowColor 设置或返回用于阴影的颜色 shadowBlur 设置或返回用于阴影的模糊级别 shadowOffsetX 设置或返回阴影距形状的水平距离 shadowOffse…...