当前位置: 首页 > article >正文

webrtc pacer模块(一) 平滑处理的实现

Pacer起到平滑码率的作用,使发送到网络上的码率稳定。如下的这张创建Pacer的流程图,其中PacerSender就是Pacer,其中PacerSender就是Pacer。这篇文章介绍它的核心子类PacingController及Periodic模式下平滑处理的基本流程。平滑处理流程中还有与带宽探测所关联的流程,在本篇文章中并不涉及。

从上图中可以看到,在创建Call对象时,会创建一个RtpTransportControllerSend,它是Call对象中发送数据的大总管,而PacerSender也是属于它管理的对象。

一个Call对象中一个RtpTransportControllerSend,一个RtpTransportControllerSend中一个PacerSender,所以Pacer是作用于Call中所有的stream,这里并不是只处理音视频包,还有fec包,重传包,padding包,Call对象中也发送出去的数据都会经过Pacer。

这篇文章是介绍平滑实现的基本原理和Pacer中的Periodic模式的处理流程。Pacer的流程中还有与带宽探测所关联的流程,在本篇文章中并不涉及。

码率平滑的原理

在视频编码中,虽然编码器会将输出码流的码率控制在所设置的码率范围内。但是在编码器产生关键帧或在画面变化比较大时,码率可能超过设置的码率值。在有fec或重传包时,也可能造成实际发送的码率值超过目标值。这种突发的大码率的数据,可能就会造成网络链路拥塞。

所以引入的pacer就是平滑发送的码率值,在一段时间内,保证发送码率接近设置目标码率值。而避免突发的高码率造成网络链路拥塞。

平滑的基本原理就是**缓存队列+周期发送,将要发送的数据先缓存,在周期性的发送出去,起到平均码率的目的。那么这种周期有两种模式:**

  • **kPeriodic**,周期模式,也是默认模式,以固定间隔时间发送数据。
  • kDynamic,动态模式,根据数据的缓存时长及数据量来计算下一次发送数据的时间点。

组成

pacer的流程都实现在PacingController,包括两个核心类:RoundBoinPacketQueueIntervalBudget

  • RoundBobinPacketQueue 缓存队列,对每条流都会缓存,以ssrc做为流的唯一标识,包括:重传包,fec,padding包。
  • IntervalBudget 根据设置的目标码率值及时间间隔计算可发送的数据量。

PacingController类

所属文件为\modules\pacing\pacing_controller.h,如下类图:

两个核心的成员变量:

  1. RoundRobinPakcetQueue packet_queue_ packet的缓存队列。
  2. IntervalBudget media_buget_可发送数据量计算。

两个核心函数:

  1. NextSendTime,获取每次执行的时间(5毫秒,在kPeriodic模式下)。
  2. ProcessPackets,周期处理包的发送,确定要发送的数据量,从缓存队列中取包。
平滑逻辑的处理流程

整个pacer运行的机制就是靠PacingControllerNextSendTimeProcessPackets两个方法,它们被单独的放在一个ModuleThread线程中执行,周期性的被执行,两个方法调用的堆栈如下:

**NextSendTime**

peerconnection_client.exe!webrtc::PacingController::NextSendTime() 行 348 C++

peerconnection_client.exe!webrtc::PacedSender::TimeUntilNextProcess() 行 171 C++

peerconnection_client.exe!webrtc::PacedSender::ModuleProxy::TimeUntilNextProcess() 行 150 C++

peerconnection_client.exe!webrtc::`anonymous namespace’::GetNextCallbackTime(webrtc::Module * module, __int64 time_now) 行 30 C++

peerconnection_client.exe!webrtc::ProcessThreadImpl::Process() 行 231 C++

peerconnection_client.exe!webrtc::ProcessThreadImpl::Run(void * obj) 行 198 C++

peerconnection_client.exe!rtc::PlatformThread::Run() 行 130 C++

peerconnection_client.exe!rtc::PlatformThread::StartThread(void * param) 行 62 C++

**ProcessPackets**

peerconnection_client.exe!webrtc::PacingController::ProcessPackets() 行 408 C++

peerconnection_client.exe!webrtc::PacedSender::Process() 行 183 C++

peerconnection_client.exe!webrtc::PacedSender::ModuleProxy::Process() 行 152 C++

peerconnection_client.exe!webrtc::ProcessThreadImpl::Process() 行 226 C++

peerconnection_client.exe!webrtc::ProcessThreadImpl::Run(void * obj) 行 198 C++

peerconnection_client.exe!rtc::PlatformThread::Run() 行 130 C++

peerconnection_client.exe!rtc::PlatformThread::StartThread(void * param) 行 62 C++

核心骨架就是下面三个步骤:

  1. 设置目标码率,通过SetPacingRates(...)方法。
  2. 计算每个时间片可以发送的数据,在UpdateBudgetWithElapsedTime(TimeDelta delta)方法中。
  3. 用已发送的数据量来计算还剩多少数据量可以发送,在UpdateBudgetWithSentData(DataSize size)方法中。

详细流程:

(1). 如果媒体数据包处理模式是 kDynamic,则检查期望的发送时间和 前一次数据包处理时间 的对比,当前者大于后者时,则根据两者的差值更新预计仍在传输中的数据量,以及 前一次数据包处理时间;(Periodic模式是5ms执行一次)

(2). 从媒体数据包优先级队列中取一个数据包出来;

(3). 第 (2) 步中取出的数据包为空,但已经发送的媒体数据的量还没有达到码率探测器 webrtc::BitrateProber 建议发送的最小探测数据量,则创建一些填充数据包放入媒体数据包优先级队列,并继续下一轮处理;

(4). 发送取出的媒体数据包;

(5). 获取 FEC 数据包,并放入媒体数据包优先级队列;

(6). 根据发送的数据包的数据量,更新预计仍在传输中的数据量等信息;

(7). 如果是在码率探测期间,且发送的数据量超出码率探测器 webrtc::BitrateProber 建议发送的最小探测数据量,则结束发送过程;

(8). 如果媒体数据包处理模式是 kDynamic,则更新目标发送时间。

RoundBobinPacketQueue

RoundBobinPacketQueue是一个缓存队列, 用于缓存数据包(音视频包,fec,padding,重传包),它有两个特征:

  1. 根据优先级存储包(每种类型包都有优先级)。
  2. 记录缓存时长(记录每个包的入队时间,用于计算缓存的总时长,避免引入过多的延迟)。
类图

上图种的Stream类代表了一路流,QueuePacket类代表了数据包。

RoundBobinPacketQueue三个核心的数据结构:

  • std::map<uint32_t, Stream> streams_

key为ssrc。

  • std::multimap<StreamPrioKey, uint32_t> **stream_priorities_**

**Stream**的优先级信息表,以**priority****DataSize**为比较的key,value是ssrc。通过优先级找Stream,方便优先级变化的实现。越靠前,优先级越高。

Stream类中的std::multimap<StreamPrioKey, uint32_t>::iterator priority_it;它指向 RoundBobinPacketQueue中的stream_priorities_中的某项,可以快速定位到自己的优先级。

  • std::multiset<Timestamp> enqueue_times_

The enqueue time of every packet currently in the queue. Used to figure out the age of the oldest packet in the queue.

记录每一个包的入队时间

QueuedPacket对象中的std::multiset<Timestamp>::iterator enqueue_time_it_;指向enqueue_times_中的项,可以快速定位到自己的入队时间。

Stream,QueuePacket,RoundBobinPacketQueue关系图

如下是Stream对象,QueuePacket对象与RoundBobinPacketQueue对象的关系图。

上图是以Stream为中心,描绘Stream,QueuePacket,RoundBobinPacketQueue的关系。

  • 每个Stream都被记录在RoundRobinPacketQueuestreams_中,以ssrc为key。
  • 每个Stream的优先级都被记录在RoundRobinPacketQueuestream_priorites_中,以优先级为key,ssrc为value。
  • 数据包都被封装成QueuePacket缓存在Stream对象的packet_queue中,它也是一个优先级队列,所以每个数据包都是有优先级的。
  • RoundRobinPacketQueueenqueue_times_记录着每个rtp packet的入队时间。
  • streamstd::multimap<StreamPrioKey,uint32_t>::iterator priority_it迭代器指向该stream在stream_priorites_中的位置,便于快速检索。
  • QueuedPacket中的std::multiset<Timestamp>::iterator enqueue_time_it迭代器指向该packet在enqueue_times_中的位置,便于快速检索。

缓存队列中记录的信息有:

  1. 记录总的缓存包个数。
  2. 记录总的数据量。
  3. 记录包的优先级。
  4. 记录包的入队时间(计算包的缓存总时长,平均缓存时间,最大缓存时间)。
插入队列(push方法)的逻辑
  1. 从streams_中找pakcet所属的Ssrc的stream,如果没有,则在streams_中插入一项。
  2. 查看stream的priority_it是否等于stream_priorities_的end():如果相等,则在stream_priorities插入新的项; 否则,如果新包的优先级高,则更新其ssrc对应队列的优先级。
  3. 更新队列总时长。
  4. 入队时间减去暂停时间(一般不会有暂停)。
  5. 队列总包数+1。
  6. 队列总字节大小+包的负载大小+Padding大小(Packet的大小)。
  7. 插入到steam对象的packet_queue中。

push流程的注意点:

  • stream的size指的是stream发送的size,在Pop中,会加上弹出的PacketSize。
  • 一条stream的packet的priority值都是一样的。
  • 在入队一个stream的新的packet时,并不确定优先级,触发优先级队列中没有记录或packet的优先级发生变化。
取数据(Pop方法)的逻辑
  1. 获得优先级最高的stream。
  2. 从stream的packet_queue中取出第一个Packet。
  3. 将stream在stream_priorites_中的项删除掉。
  4. 计算Packet入队后到现在的时间(不包括暂停时间)。
  5. 将这段时间从队列的总时间中减去。
  6. 从equeue_times_中将Packet的项删除。
  7. 总包数减一。
  8. 总字节数减去包的字节数。
  9. 将包从stream中的queue中弹出。
  10. 如果stream中的队列为空,则令stream的priority_it指向stream_priorities的end()。
  11. 否则,从stream队列头部取Packet,将该Packet的priority插入到stream_priorities_中。
缓存时间的计算

计算缓存时间的目的是控制延迟,包括如下几个方法:

  • 获取缓存时间最长的包
Timestamp RoundRobinPacketQueue::OldestEnqueueTime() const {if (single_packet_queue_.has_value()) {return single_packet_queue_->EnqueueTime();}if (Empty())return Timestamp::MinusInfinity();RTC_CHECK(!enqueue_times_.empty());return *enqueue_times_.begin();
}

这个方法是用于统计,最终会被call对象的GetStats()方法调用。

  • 计算总延时,UpdateQueueTime每次被调用,总时长都会被计算,累加。
void RoundRobinPacketQueue::UpdateQueueTime(Timestamp now) {RTC_CHECK_GE(now, time_last_updated_);if (now == time_last_updated_)return;TimeDelta delta = now - time_last_updated_;if (paused_) {pause_time_sum_ += delta;} else {//有n个包,每调一次UpdateQueueTime就有一个delta值,总数为size of packet乘以deltaqueue_time_sum_ += TimeDelta::Micros(delta.us() * size_packets_);}time_last_updated_ = now;
}
  • 计算平均缓存时间

平均缓存时间=queue的总时间数/包数,用于判断延时(缓存时间)是否过大。

TimeDelta RoundRobinPacketQueue::AverageQueueTime() const {if (Empty())return TimeDelta::Zero();return queue_time_sum_ / size_packets_;
}
控制延时

PacingController::ProcessPackets()方法中,会计算包的缓存时间,如下if分支

if (drain_large_queues_) {//限制延时TimeDelta avg_time_left =std::max(TimeDelta::Millis(1),queue_time_limit - packet_queue_.AverageQueueTime());DataRate min_rate_needed = queue_size_data / avg_time_left;if (min_rate_needed > target_rate) {target_rate = min_rate_needed;RTC_LOG(LS_VERBOSE) << "bwe:large_pacing_queue pacing_rate_kbps="<< target_rate.kbps();}
}

首先会计算缓存队列的的平均缓存时间,通过设置的缓存时间限制值减去它得出应该要在多长时间发送这些数据。

再计算发送速率,最后设置目标码率值。这个目标码率值会被设置到media_buget中去(kPeriodic模式下)。

快速处理

缓存队列缓存数据,肯定会引入延迟,在RonundBobinPacketQeueu有一个absl::optional<QueuedPacket> single_packet_queue_成员变量,它的作用就是快速处理数据包。

只有音频流时的处理

音频对延迟很敏感,需要尽量少引入延迟。在RoundRobinPacketQueue::Push中,有一个分支,如下:

if (size_packets_ == 0) {single_packet_queue_.emplace(QueuedPacket(priority, enqueue_time, enqueue_order,enqueue_times_.end(), std::move(packet)));UpdateQueueTime(enqueue_time);single_packet_queue_->SubtractPauseTime(pause_time_sum_);size_packets_ = 1;size_ += PacketSize(*single_packet_queue_);}

在 size_packets_ == 0,会放到single_packet_queue_。而每取一个数据包,size_packets设置0。对音频包,一次采集周期内,20ms,只会产生一个包,而pacer的执行周期是5ms,音频包始终会走入if (size_packets_ == 0)为0的分支。

在取数据包时,在std::unique_ptr<RtpPacketToSend> PacingController::GetPendingPacket方法中,会有一个判断语句,判断音频是否走pacer。

bool unpaced_audio_packet =!pace_audio_ && packet_queue_.LeadingAudioPacketEnqueueTime().has_value();

LeadingAudioPacketEnqueueTime()是判断single_packet_queue_streams_是否有缓存音频包(下一个包是否是音频包)。

absl::optional<Timestamp> RoundRobinPacketQueue::LeadingAudioPacketEnqueueTime()const {if (single_packet_queue_.has_value()) {if (single_packet_queue_->Type() == RtpPacketMediaType::kAudio) {return single_packet_queue_->EnqueueTime();}return absl::nullopt;}if (stream_priorities_.empty()) {return absl::nullopt;}uint32_t ssrc = stream_priorities_.begin()->second;const auto& top_packet = streams_.find(ssrc)->second.packet_queue.top();if (top_packet.Type() == RtpPacketMediaType::kAudio) {return top_packet.EnqueueTime();}return absl::nullopt;
}

如果这个unpaced_audio_packet变量的值为true,这不会走media_buget_的机制,直接取出数据。

std::unique_ptr<RtpPacketToSend> PacingController::GetPendingPacket(const PacedPacketInfo& pacing_info,Timestamp target_send_time,Timestamp now) {if (packet_queue_.Empty()) {return nullptr;}// First, check if there is any reason _not_ to send the next queued packet.// Unpaced audio packets and probes are exempted from send checks.bool unpaced_audio_packet =!pace_audio_ && packet_queue_.LeadingAudioPacketEnqueueTime().has_value();bool is_probe = pacing_info.probe_cluster_id != PacedPacketInfo::kNotAProbe;//不pace audioif (!unpaced_audio_packet && !is_probe) {if (Congested()) {// Don't send anything if congested.return nullptr;}if (mode_ == ProcessMode::kPeriodic) {if (media_budget_.bytes_remaining() <= 0) {// Not enough budget.RTC_LOG(LS_INFO) << "===> media budget not enough";return nullptr;}} else {// Dynamic processing mode.if (now <= target_send_time) {// We allow sending slightly early if we think that we would actually// had been able to, had we been right on time - i.e. the current debt// is not more than would be reduced to zero at the target sent time.TimeDelta flush_time = media_debt_ / media_rate_;if (now + flush_time > target_send_time) {return nullptr;}}}}//直接取出数据return packet_queue_.Pop();
}

std::unique_ptr<RtpPacketToSend> RoundRobinPacketQueue::Pop()方法中,走下面这个分支。

if (single_packet_queue_.has_value()) {//音频包走这个分支RTC_DCHECK(stream_priorities_.empty());std::unique_ptr<RtpPacketToSend> rtp_packet(single_packet_queue_->RtpPacket());single_packet_queue_.reset();queue_time_sum_ = TimeDelta::Zero();size_packets_ = 0;size_ = DataSize::Zero();return rtp_packet;}

在只有音频的情况下,音频包只会入single_packet_queue_,并且不会走media_buget_的机制,每次时间片内都会马上取出来发送出去,起到降低延迟的作用。

音视频流的处理

PacingController::ProcessPackets是每5ms跑一次(kPeriodic模式)。视频数据,一次会产生一批rtp包,在间隔周期内,会有多个包进入队列。在size_packets_为0时,包会进入single_packet_queue_,不为0时进入包缓存队列。在这个时候media_budget_就起作用了。

音视频流都存在的情况下,音频包也不止会进入single_pakcet_queue_了,这时音频的加速就体现在std::unique_ptr<RtpPacketToSend> PacingController::GetPendingPacket上了,判断为音频包时,则不走media_buget_机制,直接取出数据。

对非音频包,则下面这个分支会起作用,限制包的发送。

if (mode_ == ProcessMode::kPeriodic) {if (media_budget_.bytes_remaining() <= 0) {// Not enough budget.RTC_LOG(LS_INFO) << "===> media budget not enough";return nullptr;}
} 

IntervalBudget

原理

IntervalBudget作用是根据当前PacedSender->Process的调用时间间隔和当前目标码率target bitrate来计算出本次Process理应发送的字节数。

比如当前码率是100 000bps,本次Process调用与上次调用间隔是20ms,则本次理应发送的字节数是100 bits per ms * 20 ms = 2000bits=250 bytes

250bytes为本次发送理应发送的字节数,但实际上视频RTP包差不多是一个MTU大小。我们不可能真的发送250bytes的数据,因此可能会导致理应发送的数据量多或少的问题,如何解决这个问题呢?

IntervalBudget中引入一个bytes_remaining_的变量来记录上次发送后,与理应发送数据量相比,多或少发了多少。其值为负表示上轮我们实际发送的比理应发送的数据量多了,我们本轮应该停止发送。其值为正表示我们上轮实际发送比理应发送的要少,还有富余。

工作原理

void set_target_rate_kbps(int target_rate_kbps);设置总的可用量max_bytes_in_budget_

void IntervalBudget::set_target_rate_kbps(int target_rate_kbps) {target_rate_kbps_ = target_rate_kbps;max_bytes_in_budget_ = (kWindowMs * target_rate_kbps_) / 8;bytes_remaining_ = std::min(std::max(-max_bytes_in_budget_, bytes_remaining_),max_bytes_in_budget_);
}

target_rate_kbps目标码率,max_bytes_in_budget_为半秒钟可发送的码率。

void IncreaseBudget(int64_t delta_time_ms);根据毫秒数增加预算(增加的量计入bytes_remaining),在kPeriodic模式下,这个delta_time_ms的值为5ms。

void IntervalBudget::IncreaseBudget(int64_t delta_time_ms) {int64_t bytes = target_rate_kbps_ * delta_time_ms / 8;if (bytes_remaining_ < 0 || can_build_up_underuse_) {// We overused last interval, compensate this interval.bytes_remaining_ = std::min(bytes_remaining_ + bytes, max_bytes_in_budget_);} else {// If we underused last interval we can't use it this interval.bytes_remaining_ = std::min(bytes, max_bytes_in_budget_);}
}

void UseBudget(size_t bytes);使用预算(bytes_remaining_减去bytes)。

void IntervalBudget::UseBudget(size_t bytes) {bytes_remaining_ = std::max(bytes_remaining_ - static_cast<int>(bytes),-max_bytes_in_budget_);
}

UseBudget(size_t bytes)更新用掉的数据量(就是已发送的数据量),如下调用堆栈

如果bytes_remaining_小于0,那么当然不能在发数据了。

**padding_budget_**的原理也一样,它是用于计算padding的数据量。


码率平滑的实现原理

发包的流程PacingController::ProcessPackets放在一个线程中,会被定时触发。被触发后,会计算当前时间和上次被调用时间的时间差,然后将时间差参数传入media_buget(**IntervalBudget**对象),media_buget_算出当前时间片可以发送多少数据,然后从缓存队列(**RoundBobinPacketQueue**对象)中取出数据进行发送。


**media_buget_**计算时间片发送多少字节的公式如下:

**delta time:**上次检查时间点和这次检查时间点的时间差。

target bitrate: pacer的参考码率,是由probe模块根据网络探测带宽评估出来。

remain_bytes: 每次触发包时会减去发送报文的长度size,如果remain_bytes>0,继续从缓存队列中取下一个报文进行发送,直到remain_bytes<=0或者缓存队列没有更多的报文。

如果缓存队列没有更多待发的报文,但是**media_buget_**(**IntervalBudget**对象)计算出还可以发送更多的数据,这个时候pacer会进行padding报文补充。


四个用于控制发送码率的方法:

**bool PacingController::Congested()**

**void PacingController::OnPacketSent**** 底层socket发送的数据量的回调。**

**void PacingController::UpdateBudgetWithSentData(DataSize size)**

**void PacingController::UpdateOutstandingData(DataSize outstanding_data)**

码率分配

数据包的优先级

前面就提到了缓存队列是一个优先级队列,对数据包会设置一个优先级,在每次插入数据时(PacingController::EnqueuePacket(...)方法),都会调用GetPriorityForType(RtpPacketMediaType type),如下优先级:

int GetPriorityForType(RtpPacketMediaType type) {// Lower number takes priority over higher.switch (type) {case RtpPacketMediaType::kAudio:// Audio is always prioritized over other packet types.return kFirstPriority + 1;case RtpPacketMediaType::kRetransmission:// Send retransmissions before new media.return kFirstPriority + 2;case RtpPacketMediaType::kVideo:case RtpPacketMediaType::kForwardErrorCorrection:// Video has "normal" priority, in the old speak.// Send redundancy concurrently to video. If it is delayed it might have a// lower chance of being useful.return kFirstPriority + 3;case RtpPacketMediaType::kPadding:// Packets that are in themselves likely useless, only sent to keep the// BWE high.return kFirstPriority + 4;}RTC_CHECK_NOTREACHED();
}

QueuedPacket中的operator<(const RoundRobinPacketQueue::QueuedPacket& other)会根据优先级确定QueuedPacket在队列中顺序。priority值越小,代表优先级越高,如下,在QueuedPacket中定义的bool operator<(const QueuedPacket& other) const

bool RoundRobinPacketQueue::QueuedPacket::operator<(const RoundRobinPacketQueue::QueuedPacket& other) const {if (priority_ != other.priority_)return priority_ > other.priority_;if (is_retransmission_ != other.is_retransmission_)return other.is_retransmission_;return enqueue_order_ > other.enqueue_order_;
}

● 优先值小的,排在前面。
● 优先级相同,非重传包在前面。
● 优先级和重传标志均相同,以入队先后顺序排列(enqueue_order_就是一个递增的值)。

相关文章:

webrtc pacer模块(一) 平滑处理的实现

Pacer起到平滑码率的作用&#xff0c;使发送到网络上的码率稳定。如下的这张创建Pacer的流程图&#xff0c;其中PacerSender就是Pacer&#xff0c;其中PacerSender就是Pacer。这篇文章介绍它的核心子类PacingController及Periodic模式下平滑处理的基本流程。平滑处理流程中还有…...

基于角色个人的数据权限控制

一、适用场景 如何有效控制用户对特定数据的访问和操作权限&#xff0c;以确保系统的安全性和数据的隐私性。 二、市场现状 权限管理是现代系统中非常重要的功能&#xff0c;尤其是对于复杂的B端系统或需要灵活权限控制的场景&#xff0c;可以运用一些成熟的工具和框架&…...

河北工程大学e2e平台,python

题目&#xff0c;选择题包100分&#xff01; 题目&#xff0c;选择题包100分&#xff01; 题目&#xff0c;选择题包100分&#xff01; 联系&#x1f6f0;&#xff1a;18039589633...

BeautifulSoup 踩坑笔记:SVG 显示异常的真正原因

“这图是不是糊了&#xff1f;”以为是样式缺了&#xff1f;试试手动复制差异在哪&#xff1f;想用对比工具一探究竟……简单到不能再简单的代码&#xff0c;有问题吗&#xff1f;最后的真相&#xff1a;viewBox vs viewbox&#xff0c;preserveAspectRatio vs preserveaspectr…...

【AI提示词】创业导师提供个性化创业指导

提示说明 以丰富的行业经验和专业的知识为学员提供创业指导&#xff0c;帮助其解决实际问题并实现商业成功 提示词 # Role: 创业导师## Profile - language: 中英文 - description: 以丰富的行业经验和专业的知识为学员提供创业指导&#xff0c;帮助其解决实际问题并实现商业…...

【OpenCV 对图片做旋转操作】仿射=旋转+平移+缩放+剪切

OpenCV 中的旋转相关函数详解 OpenCV 提供了多种函数用于图像的旋转操作&#xff0c;主要分为 任意角度旋转 和 固定角度旋转。以下是常用函数及详细使用说明&#xff1a; 一、任意角度旋转 1. cv2.getRotationMatrix2D() 生成旋转矩阵&#xff0c;用于定义旋转参数。 函数原…...

【browser-use+deepseek】实现简单的web-ui自动化

browser-use Web-UI 一、browser-use是什么 Browser Use 是一款开源Python库&#xff0c;专为大语言模型设计的智能浏览器工具&#xff0c;目的是让 AI 能够像人类一样自然地浏览和操作网页。它支持多标签页管理、视觉识别、内容提取&#xff0c;并能记录和重复执行特定动作。…...

django数据迁移操作受阻

错误信息&#xff1a; django.db.utils.OperationalError: (1227, Access denied; you need (at least one of) the SYSTEM_VARIABLES_ADMIN or SESSION_VARIABLES_ADMIN privilege(s) for this operation)根据错误信息分析&#xff0c;该问题是由于MySQL用户 缺乏SYSTEM_VARI…...

MOS管的发热原因和解决办法

发热来源 如上图&#xff0c;MOS管的工作状态有4种情况&#xff0c;分别是开通过程&#xff0c;导通过程&#xff0c;关断过程和截止过程。 导致发热的损耗主要有两种&#xff1a;开关损耗、导通损耗。 导通损耗 导通损耗比较好计算&#xff0c;根据驱动电压VGS值可以得到MOS…...

4月11日随笔

本来以为大风会很厉害&#xff0c;本来今天早八的微积分不想去了。但是起床发现并没有很大的风&#xff0c;还是去了。 中午回来的路上突然变天&#xff0c;雷阵雨转冰雹。下了大概半小时&#xff0c;所幸挨淋的不是很严重。 中午打了首胜&#xff0c;AI的基本弄完了&#xf…...

科技项目验收测试怎么做?验收测试报告如何获取?

科技项目从研发到上市需要一个很长的周期&#xff0c;并且在上市之前还有一个至关重要的交付过程&#xff0c;那就是项目验收&#xff0c;验收需要通过验收测试来呈现。科技项目验收测试是确保项目成功交付的关键步骤&#xff0c;那么是如何进行的呢?企事业单位想要获取科技项…...

Java面试黄金宝典45

1. 非对称加密 RSA 定义:RSA 是一种广泛使用的非对称加密算法,其安全性基于大整数分解的困难性。它使用一对密钥,即公钥和私钥。公钥可公开用于加密消息,而私钥必须保密,用于解密由相应公钥加密的消息。要点: 公钥公开,私钥保密,二者成对出现。加密和解密使用不同的密钥…...

计算机网络学习前言

前言 该部分说明计算机网络是什么&#xff1f;它有什么作用和功能&#xff1f;值不值得我们去学习&#xff1f;我们该如何学习&#xff1f;这几个部分去大概介绍计算机网络这门课程&#xff0c;往后会介绍计算机网络的具体知识点。 1.计算机网络是什么&#xff1f; 计算机网…...

Vuex 源码

以下是关于 Vuex 源码 的系统梳理: 一、Vuex 核心架构设计 1. 整体架构分层 #mermaid-svg-Eqqp2jldNkQwvgcr {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-Eqqp2jldNkQwvgcr .error-icon{fill:#552222;}#mermaid…...

AutoEval:现实世界中通才机器人操作策略的自主评估

25年3月来自 UC Berkeley 和 Nvidia 的论文“AutoEval: Autonomous Evaluation of Generalist Robot Manipulation Policies in the Real World”。 可规模化且可复现的策略评估一直是机器人学习领域长期存在的挑战。评估对于评估进展和构建更优策略至关重要&#xff0c;但在现…...

IP组播技术与internet

1.MAC地址分为三类&#xff1a;广播地址&#xff1b;组播地址&#xff1b;单播地址 2.由一个源向一组主机发送信息的传输方式称为组播。 3.组播MAC地址&#xff0c;第一个字节的最后一位为1&#xff1b; 单播MAC地址&#xff0c;第一个字节的最后一位为0&#xff1b; 4.不能…...

基于SSM框架的房屋租赁小程序开发与实现

概述 一个基于SSM框架开发的微信小程序房屋租赁管理系统&#xff0c;该项目实现了用户管理、中介管理、房源信息管理等核心功能。 主要内容 一、管理员模块功能实现 ​​用户管理​​ 管理员可对通过微信小程序注册的用户信息进行修改和删除操作&#xff0c;确保用户数据的准…...

oracle 表空间(Tablespace)

在 Oracle 11g 中&#xff0c;表空间&#xff08;Tablespace&#xff09; 是数据库存储架构的核心逻辑单元&#xff0c;其原理基于 逻辑存储与物理存储的分离&#xff0c;通过分层管理数据文件、段&#xff08;Segment&#xff09;、区&#xff08;Extent&#xff09;和数据块&…...

基于YOLOv8的机场跑道异物检测识别系统:提升航空安全的新一代解决方案(主页有源码)

✨个人主页欢迎您的访问 ✨期待您的三连 ✨ ✨个人主页欢迎您的访问 ✨期待您的三连 ✨ ✨个人主页欢迎您的访问 ✨期待您的三连✨ ​​​ ​​​​​​​​​ ​​ 1. 机场跑道异物检测领域概述 机场跑道异物(Foreign Object Debris, FOD)是指存在于机场跑道、滑行道等关…...

23种设计模式生活化场景,帮助理解

以下是 23种设计模式的生活化场景 及其核心对比&#xff0c;通过日常例子和比喻帮助理解它们的本质区别和应用场景&#xff1a; 创建型模式&#xff08;5种&#xff09; 1. 工厂方法&#xff08;Factory Method&#xff09; • 场景&#xff1a;快餐店的点餐系统。 • 问题&a…...

Android学习总结之OKHttp拦截器和缓存

深入理解 OkHttp 拦截器 1. 拦截器接口详解 Interceptor 接口是自定义拦截器的基础&#xff0c;它仅包含一个抽象方法 intercept。以下是对该方法参数和返回值的详细解释&#xff1a; import okhttp3.Interceptor; import okhttp3.Request; import okhttp3.Response; import…...

Wincc管对象的使用

Wincc管对象的使用 管对象的调用多边形管T形管双T形管管弯头管道大小调整 管对象的调用 打开【图形编辑器】 多边形管 多边形管如下&#xff1a; 一根管子的顶点数是两个&#xff0c;如果修改顶点数&#xff0c;管子就有多少个端点。 修改顶点数为5 此时点击端点然后拖动&#…...

Linux-----驱动

一、内核驱动与启动流程 1. Linux内核驱动 Nor Flash: 可线性访问&#xff0c;有专门的数据及地址总线&#xff08;与内存访问方式相同&#xff09;。 Nand Flash: 不可线性访问&#xff0c;访问需要控制逻辑&#xff08;软件&#xff09;。 2. Linux启动流程 ARM架构: IRAM…...

GHG认证是什么,GHG认证的意义?对企业发展好处

GHG认证&#xff08;温室气体认证&#xff09;详解 GHG认证&#xff08;Greenhouse Gas Certification&#xff0c;温室气体认证&#xff09;是指对组织、产品或项目的温室气体&#xff08;GHG&#xff09;排放量进行科学量化、报告&#xff0c;并由第三方机构进行独立核查和认…...

Docker 常用命令指南

Docker 提供了丰富的命令行工具来管理镜像、容器、网络和数据卷等资源。本指南按类别整理 Docker 的常用命令,并为每个命令提供简体中文说明和示例,以帮助您快速查询和掌握日常使用。 1. 镜像管理 Docker 镜像(Image)是打包好的应用程序及其依赖环境,可用于创建容器。常用…...

问问lua怎么写DeepSeek,,,,,

很坦白说&#xff0c;这十年&#xff0c;我几乎没办法从互联网找到这个这样的代码&#xff0c;互联网引擎找不到&#xff0c;我也没有很大的“追求”要传承&#xff0c;或者要宣传什么&#xff1b;直到DeepSeek的出现 兄弟&#xff0c;Deepseek现在已经比你更了解你楼下的超市…...

基于神经环路的神经调控可增强遗忘型轻度认知障碍患者的延迟回忆能力

简要总结 这篇文章提出了一种名为CcSi-MHAHGEL的框架&#xff0c;用于基于多站点、多图谱fMRI的功能连接网络&#xff08;FCN&#xff09;分析&#xff0c;以辅助自闭症谱系障碍&#xff08;ASD&#xff09;的识别。该框架通过多视图超边感知的超图嵌入学习方法&#xff0c;整合…...

C++学习之ORACLE③

1.集合运算符 查询部门号是10和20的员工信息&#xff1a; &#xff1f;思考有几种方式解决该问题 &#xff1f; SQL> select * from emp where deptno in(10, 20) SQL> select * from emp where deptno10 or deptno20 集合运算&#xff1a; Select * from emp …...

UniAD:自动驾驶的统一架构 - 创新与挑战并存

引言 自动驾驶技术正经历一场架构革命。传统上&#xff0c;自动驾驶系统采用模块化设计&#xff0c;将感知、预测和规划分离为独立组件。而上海人工智能实验室的OpenDriveLab团队提出的UniAD&#xff08;Unified Autonomous Driving&#xff09;则尝试将这些任务整合到一个统一…...

transformers 中的 input_ids 和 labels 是什么

transformers 中的 input_ids 和 labels 是什么 input_ids 是输入文本的数字化表示,而 labels 是模型训练的目标值 在自然语言处理(NLP)和使用 transformers 库进行模型训练时,tokenizer = AutoTokenizer.from_pretrained(model_path) 这行代码是用于从预训练模型路径加载…...