webrtc jitter buffer
一、jitter buffer 介绍
二、jitter 估计
三、buffer 处理 rtp 包逻辑
四、接收和解码流程
五、FrameBuffer 类介绍
/*
********************************************************************************
一、jitter buffer 介绍
jitter buffer 抖动缓冲区
当网络不稳定时(发生抖动),增加buffer的长度,多缓存一些数据,以应对将来可能发生的抖动。
它对数据包丢失、乱序、延迟到达等情况进行处理,平滑的向解码模块输出数据包/帧,
抵抗各种弱网情况对播放/渲染造成的影响,降低卡顿,提高用户体验。
视频从采集到渲染的流程如下:
采集 -> 编码 -> 分包 -> 发送 ----------------> 网络
渲染 <- 解码 <- Jitter Buffer <- 组帧 <- 接收 <-|
********************************************************************************
*/
/*
********************************************************************************
二、jitter 估计
jitter 就是一种抖动。
RTP数据包从源地址发送到目的地址,会发生不一样的延迟,这样的延迟变动就是 jitter
jitter 会让音视频的播放不稳定,如音频的颤音、视频的忽快忽慢
解决 jitter 的方法是增加延时,这个延时称为抖动延迟(jitter delay)
抖动延迟由网络延迟、解码延迟、渲染延迟构成。解码、渲染延迟比较稳定,网络抖动延迟是动态变化的。
webrtc 认为网络抖动延迟由两部分构成:
1、网络噪声带来的抖动延迟(网络排队延迟)
2、传输大的视频帧(特别是关键帧)对网络造成冲击带来的抖动延迟
(计算信道速率,根据信道速率计算大的视频帧对网络冲击带来的延迟)
webrtc 使用卡尔曼滤波(kalman) 估算网络排队延迟和信道速率
modules\video_coding\jitter_estimator.cc
1、更新 jitter 估计
void VCMJitterEstimator::UpdateEstimate(
int64_t frameDelayMS, uint32_t frameSizeBytes, bool incompleteFrame = false);
其中 frameDelayMS 帧间延迟,指的是一帧数据因为分包和网络传输所造成的延时。
frameSizeBytes 指当前数据帧大小, incompleteFrame 指是否为完整的帧。
1.1、噪声阈值计算
VCMJitterEstimator::NoiseThreshold()
double noiseThreshold = _noiseStdDevs * sqrt(_varNoise) - _noiseStdDevOffset;
1.2、计算 jitter 估计值
VCMJitterEstimator::CalculateEstimate()
double ret = _theta[0] * (_maxFrameSize - _avgFrameSize) + NoiseThreshold();
即
jitterDelay = _theta[0] * (_maxFrameSize - _avgFrameSize)
+ _noiseStdDevs * sqrt(_varNoise) - _noiseStdDevOffset;
其中:
_theta[0] 为信道传输速率的倒数
_maxFrameSize 自会话开始以来所收到的最大帧大小
_avgFrameSize 平均帧大小
_noiseStdDevs 表示噪声系数,值为2.33
_varNoise 表示噪声方差,默认值为4.0 EstimateRandomJitter()中会不断更新该值
_noiseStdDevOffset 为噪声扣除常数,值为30.0
2、获取 jitter 估计
返回以毫秒为单位的当前抖动估计,并在重传情况下添加一个RTT相关项
int VCMJitterEstimator::GetJitterEstimate(
double rttMultiplier, absl::optional<double> rttMultAddCapMs)
其中: rttMultiplier 为RTT参数乘数
参考: http://www.ctiforum.com/news/guonei/512085.html
********************************************************************************
*/
/*
********************************************************************************
三、buffer 处理 rtp 包逻辑
modules\video_coding\jitter_buffer.h
buffer 接收 rtp 包的处理逻辑主要使用到以下三个队列
class VCMJitterBuffer {
UnorderedFrameList free_frames_ RTC_GUARDED_BY(crit_sect_);
FrameList decodable_frames_ RTC_GUARDED_BY(crit_sect_);
FrameList incomplete_frames_ RTC_GUARDED_BY(crit_sect_);
}
free_frames_ 队列用于管理空的frame,弹出空的frame来存放rtp包,解码完成后的frame重置后再次存入该队列。
incomplete_frames_ 队列用于存放尚未完整的frame,当frame完整时将其push到 decodable_frames_
decodable_frames_ 队列用于存放完整的可以解码的frame
1、第一次接收到一个rtp视频包,从 free_frames_ 队列中弹出一个空 frame 块,用来放置这个包。
之后每次接收一个rtp包,根据时间戳在 incomplete_frames_ 和 decodable_frames_
中寻找,看是否已经接收到过相同时间戳的包,如果找到则弹出该 frame 块。
否则,从 free_frames_ 中弹出一个空 frame
2、根据包的序列号,找到应该插入 frame 的位置将包插入,并更新 frame 的 state (frame中存放多个rtp包)
其中 state 的状态为
enum VCMFrameBufferStateEnum {
kStateEmpty, // frame popped by the RTP receiver
kStateIncomplete, // frame that have one or more packet(s) stored
kStateComplete, // frame that have all packets
};
3、根据不同的 buffer_state 将 frame 帧 push 回到队列中。
如果 buffer_state 为 kCompleteSession 并且 frame 已经在 decodable list (continuous 为 true)
将 frame push 到 decodable_frames_ 队列 decodable_frames_.InsertFrame(frame);
如果 buffer_state 为 kCompleteSession 或 kIncomplete
将 frame push 到 incomplete_frames_ 队列 incomplete_frames_.InsertFrame(frame);
如果 buffer_state 为 kNoError 或 kOutOfBoundsPacket 或 kDuplicatePacket
将 frame push 到 frame_list 队列 frame_list->InsertFrame(frame);
VCMJitterBuffer::InsertPacket -> VCMJitterBuffer::FindAndInsertContinuousFramesWithState()
将 incomplete_frames_ 队列中的 frame push 到 decodable_frames_ 队列
4、free_frames_ 队列初始化大小为 kStartNumberOfFrames = 6 (在构造函数 VCMJitterBuffer() 中初始化)
如果 free_frames_ 队列为空时,增加队列大小,最大值为 kMaxNumberOfFrames = 300
定期从 incomplete_frames_ 和 decodable_frames_ 队列中清除一些过时的frame
push 到 free_frames_ 队列 VCMJitterBuffer::RecycleFramesUntilKeyFrame()
5、解码线程取出 frame 并解码完成后,将 frame 重置并 push 到 free_frames_ 队列
VideoReceiver::Decode() -> VCMReceiver::ReleaseFrame() -> VCMJitterBuffer::ReleaseFrame()
-> VCMJitterBuffer::RecycleFrameBuffer() -> frame->Reset()/free_frames_.push_back(frame)
********************************************************************************
*/
/*
********************************************************************************
四、接收和解码流程
webrtc jitter buffer 被两个线程操作
(1) 写线程负责组帧之后把数据写入 jitter buffer 里
(2) 读线程负责从 jitter buffer 里读取数据然后解码
1、接收数据
1.1、读线程 创建流程
WebRtcVideoChannel::WebRtcVideoReceiveStream::WebRtcVideoReceiveStream()
-> WebRtcVideoChannel::WebRtcVideoReceiveStream::RecreateWebRtcVideoStream()
[调用 stream_ = call_->CreateVideoReceiveStream(std::move(config));
调用 stream_->Start()
其中 stream_ 类型为 webrtc::VideoReceiveStream*
]
[Call::CreateVideoReceiveStream()
VideoReceiveStream::Start()
]
-> webrtc::VideoReceiveStream* Call::CreateVideoReceiveStream()
[调用构造函数
VideoReceiveStream::VideoReceiveStream(module_process_thread_.get())
其中 Call::Call() 设置为 module_process_thread_ 为 ModuleProcessThread 线程
]
[Call::Create(const Call::Config& config)
[调用 Create(config, Clock::GetRealTimeClock(),
ProcessThread::Create("ModuleProcessThread"),
ProcessThread::Create("PacerThread"));
创建两个线程: ModuleProcessThread 和 PacerThread
]
-> Call::Create()
[调用 new internal::Call()]
-> Call::Call()
[其中设置 module_process_thread_ 为 ModuleProcessThread 线程]
]
[VideoReceiveStream::VideoReceiveStream()
[设置 rtp_stream_sync_(this) 即 rtp_stream_sync_ 为 VideoReceiveStream 类型
设置 process_thread_ 为 ModuleProcessThread 线程
调用 process_thread_->RegisterModule(&rtp_stream_sync_, RTC_FROM_HERE);
]
]
-> VideoReceiveStream::Start()
-> VideoReceiveStream::StartNextDecode()
[将数据帧从 jitter buffer 中取出
调用 frame_buffer_->NextFrame()
]
1.2、写线程 创建流程
VideoReceiveStream::VideoReceiveStream()
[创建 rtp_video_stream_receiver_(process_thread_)
其中 rtp_video_stream_receiver_ 类型为 RtpVideoStreamReceiver
process_thread_ 为 ModuleProcessThread 线程
]
-> RtpVideoStreamReceiver::RtpVideoStreamReceiver()
[设置 process_thread_ 为 ModuleProcessThread 线程]
-> RtpVideoStreamReceiver::OnCompleteFrame() [从网络接收到RTP数据]
-> VideoReceiveStream::OnCompleteFrame()
[调用 frame_buffer_->InsertFrame()]
-> FrameBuffer::InsertFrame() [将数据帧存放到 jitter buffer ]
2、解码流程
// 创建一个工作线程,用于解码数据帧
// 从 frame_buffer_ 中获取数据帧进行解码
VideoStreamDecoderImpl::VideoStreamDecoderImpl()
[创建线程 PlatformThread 一个简单的工作线程
decode_thread_(&DecodeLoop,
this,
"video_stream_decoder_decode_thread",
rtc::kHighestPriority)
启动线程 decode_thread_.Start()
]
-> VideoStreamDecoderImpl::DecodeLoop(void* ptr)
[auto* vs_decoder = static_cast<VideoStreamDecoderImpl*>(ptr);
启用循环 while (true)
DecodeResult decode_result =
vs_decoder->DecodeNextFrame(max_wait_time_ms, keyframe_required);
如果 decode_result 为 kShutdown 则返回
]
-> VideoStreamDecoderImpl::DecodeNextFrame()
[调用 frame_buffer_.NextFrame()
VideoDecoder* decoder = GetDecoder(frame->PayloadType());
decoder->Decode()
]
[FrameBuffer::NextFrame()
VideoStreamDecoderImpl::GetDecoder()
VideoDecoder::Decode() 纯虚函数
[视频编码方式(h264 vp8 vp9),下面以vp8为例]
]
-> LibvpxVp8Decoder::Decode()
// 创建一个任务队列,在上面的工作线程中以异步的方式执行任务
// 将 frame 存放到 frame_buffer_
VideoStreamDecoderImpl::VideoStreamDecoderImpl()
[创建任务队列 bookkeeping_queue_(task_queue_factory->CreateTaskQueue())]
VideoStreamDecoderImpl::OnFrame()
[如果 bookkeeping_queue_.IsCurrent() 为 false
调用 bookkeeping_queue_.PostTask(
std::make_unique<OnFrameTask>(std::move(frame), this));
其中 OnFrameTask::Run() 调用
video_stream_decoder_->OnFrame() 即 VideoStreamDecoderImpl::OnFrame()
]
[调用 frame_buffer_.InsertFrame(std::move(frame))]
********************************************************************************
*/
/*
********************************************************************************
五、FrameBuffer 类介绍
********************************************************************************
*/
/**
* jitter buffer实现 frame_buffer2.cc
* class FrameBuffer
*
*/
FrameBuffer(Clock* clock,
VCMTiming* timing,
VCMReceiveStatisticsCallback* stats_callback);
// Insert a frame into the frame buffer. Returns the picture id
// of the last continuous frame or -1 if there is no continuous frame.
int64_t InsertFrame(std::unique_ptr<EncodedFrame> frame);
// Get the next frame for decoding. Will return at latest after
// |max_wait_time_ms|.
// - If a frame is available within |max_wait_time_ms| it will return
// kFrameFound and set |frame_out| to the resulting frame.
// - If no frame is available after |max_wait_time_ms| it will return
// kTimeout.
// - If the FrameBuffer is stopped then it will return kStopped.
ReturnReason NextFrame(int64_t max_wait_time_ms,
std::unique_ptr<EncodedFrame>* frame_out,
bool keyframe_required);
void NextFrame(
int64_t max_wait_time_ms,
bool keyframe_required,
rtc::TaskQueue* callback_queue,
std::function<void(std::unique_ptr<EncodedFrame>, ReturnReason)> handler);
// Tells the FrameBuffer which protection mode that is in use. Affects
// the frame timing. (影响帧定时)
void SetProtectionMode(VCMVideoProtection mode);
// Start the frame buffer, has no effect if the frame buffer is started.
// The frame buffer is started upon construction.
void Start();
// Stop the frame buffer, causing any sleeping thread in NextFrame to
// return immediately.
void Stop();
// Updates the RTT for jitter buffer estimation.
void UpdateRtt(int64_t rtt_ms);
// Clears the FrameBuffer, removing all the buffered frames.
void Clear();
class FrameBuffer {
public:
private:
FrameMap frames_; // 只存储未解码的帧
int64_t latest_return_time_ms_; // 下次返回帧的时间
};
void FrameBuffer::NextFrame(
int64_t max_wait_time_ms,
bool keyframe_required,
rtc::TaskQueue* callback_queue,
std::function<void(std::unique_ptr<EncodedFrame>, ReturnReason)> handler) {
//
int64_t latest_return_time_ms =
clock_->TimeInMilliseconds() + max_wait_time_ms;
latest_return_time_ms_ = latest_return_time_ms;
keyframe_required_ = keyframe_required;
frame_handler_ = handler;
callback_queue_ = callback_queue;
StartWaitForNextFrameOnQueue();
}
void FrameBuffer::StartWaitForNextFrameOnQueue() {
// 查找下一个待解码的帧存放到 frames_to_decode_ 并返回最大等待时间
int64_t wait_ms = FindNextFrame(clock_->TimeInMilliseconds());
// 启动一个重复任务
// 如果在等待时间内有可以解码的帧( frames_to_decode_ 不为空)
// 则调用 frame_handler_ 处理帧
callback_task_ = RepeatingTaskHandle::DelayedStart(
callback_queue_->Get(), TimeDelta::ms(wait_ms), [this] {
// 如果这个任务没有被取消,我们在等待时没有得到任何新的帧,
// 则继续 delivery frame
if (!frames_to_decode_.empty()) {
// 还有 frame 继续 deliver
frame_handler_(absl::WrapUnique(GetNextFrame()), kFrameFound);
CancelCallback();
return TimeDelta::Zero(); // Ignored.
} else if (clock_->TimeInMilliseconds() >= latest_return_time_ms_) {
// 超时,发送信号并停止重复任务
frame_handler_(nullptr, kTimeout);
CancelCallback();
return TimeDelta::Zero(); // Ignored.
} else {
// 如果没有帧用于解码,并且还有时间
// 这意味着在创建和执行此任务之间清除了帧缓冲区
// 继续等待剩余时间
int64_t wait_ms = FindNextFrame(clock_->TimeInMilliseconds());
return TimeDelta::ms(wait_ms);
}
});
}
/**
* 从未解码的帧 frames_ 中查找 superframe 及其剩余帧,并将其存放到 frames_to_decode_
* 计算帧的渲染时间戳和最大需要等待的时间
* 返回等待时间
*/
int64_t FrameBuffer::FindNextFrame(int64_t now_ms) {
// 根据下次返回帧的时间和当前时间计算等待时间
int64_t wait_ms = latest_return_time_ms_ - now_ms;
// 清理容器
frames_to_decode_.clear();
// 遍历未解码的帧 frames_
for (auto frame_it = frames_.begin();
frame_it != frames_.end() && frame_it->first <= last_continuous_frame_;
++frame_it) {
if (!frame_it->second.continuous ||
frame_it->second.num_missing_decodable > 0) {
continue;
}
// 获取已编码的帧
EncodedFrame* frame = frame_it->second.frame.get();
// 如果需要关键帧,但当前帧不是关键帧,则跳过
if (keyframe_required_ && !frame->is_keyframe())
continue;
// 只会返回 superframe 的所有部分
// 因此如果不是 superframe 的开始,则跳过
if (frame->inter_layer_predicted) {
continue;
}
// 收集同一 superframe 的所有剩余帧
std::vector<FrameMap::iterator> current_superframe;
current_superframe.push_back(frame_it);
// 判断下一个帧是否和当前帧为同一 superframe
// 如果是,则将其存放到 current_superframe
bool last_layer_completed = frame_it->second.frame->is_last_spatial_layer;
FrameMap::iterator next_frame_it = frame_it;
// 将同一 superframe 的所有帧存放到 current_superframe
while (true) {
++next_frame_it;
if (next_frame_it == frames_.end() ||
next_frame_it->first.picture_id != frame->id.picture_id ||
!next_frame_it->second.continuous) {
break;
}
// Check if the next frame has some undecoded references other than
// the previous frame in the same superframe.
size_t num_allowed_undecoded_refs =
(next_frame_it->second.frame->inter_layer_predicted) ? 1 : 0;
if (next_frame_it->second.num_missing_decodable >
num_allowed_undecoded_refs) {
break;
}
// All frames in the superframe should have the same timestamp.
if (frame->Timestamp() != next_frame_it->second.frame->Timestamp()) {
RTC_LOG(LS_WARNING) << "Frames in a single superframe have different"
" timestamps. Skipping undecodable superframe.";
break;
}
current_superframe.push_back(next_frame_it);
last_layer_completed = next_frame_it->second.frame->is_last_spatial_layer;
}
// 检查当前的 superframe 是否完整
if (!last_layer_completed) {
continue;
}
// 将当前 superframe 及其剩下的所有帧传递给 frames_to_decode_
frames_to_decode_ = std::move(current_superframe);
// 如果帧的渲染时间戳无效
// 根据当前时间和帧的时间戳计算帧的渲染时间戳,并保存到帧信息中
if (frame->RenderTime() == -1) {
frame->SetRenderTime(timing_->RenderTimeMs(frame->Timestamp(), now_ms));
}
// 根据帧的渲染时间戳和当前时间计算最大需要等待的时间
wait_ms = timing_->MaxWaitingTime(frame->RenderTime(), now_ms);
break;
}
// 更新等待时间
wait_ms = std::min<int64_t>(wait_ms, latest_return_time_ms_ - now_ms);
wait_ms = std::max<int64_t>(wait_ms, 0);
return wait_ms;
}
/**
* 根据首帧的时间戳和帧的接收时间,计算帧的延迟
* 根据帧的延迟和帧的大小,更新抖动估计
*
* 设置 frames_to_decode_ 中帧的渲染时间为首帧的渲染时间
* 返回待编码帧 frames_to_decode_
*
*/
EncodedFrame* FrameBuffer::GetNextFrame() {
int64_t now_ms = clock_->TimeInMilliseconds();
// TODO(ilnik): remove |frames_out| use frames_to_decode_ directly.
std::vector<EncodedFrame*> frames_out;
bool superframe_delayed_by_retransmission = false;
// 统计 superframe 大小,用于更新抖动
size_t superframe_size = 0;
// 获取 frames_to_decode_ 中第一个待解码的帧
EncodedFrame* first_frame = frames_to_decode_[0]->second.frame.get();
// 获取第一个待解码帧的渲染时间和接收时间
int64_t render_time_ms = first_frame->RenderTime();
int64_t receive_time_ms = first_frame->ReceivedTime();
// 优雅地处理坏的RTP时间戳和渲染时间问题
if (HasBadRenderTiming(*first_frame, now_ms)) {
jitter_estimator_.Reset();
timing_->Reset();
render_time_ms = timing_->RenderTimeMs(first_frame->Timestamp(), now_ms);
}
// 遍历 frames_to_decode_
for (FrameMap::iterator& frame_it : frames_to_decode_) {
// 获取待解码的帧
EncodedFrame* frame = frame_it->second.frame.release();
// 设置帧的渲染时间
frame->SetRenderTime(render_time_ms);
superframe_delayed_by_retransmission |= frame->delayed_by_retransmission();
// 计算接收时间
receive_time_ms = std::max(receive_time_ms, frame->ReceivedTime());
// 计算 superframe 帧大小
superframe_size += frame->size();
PropagateDecodability(frame_it->second);
decoded_frames_history_.InsertDecoded(frame_it->first, frame->Timestamp());
// Remove decoded frame and all undecoded frames before it.
if (stats_callback_) {
unsigned int dropped_frames = std::count_if(
frames_.begin(), frame_it,
[](const std::pair<const VideoLayerFrameId, FrameInfo>& frame) {
return frame.second.frame != nullptr;
});
if (dropped_frames > 0) {
stats_callback_->OnDroppedFrames(dropped_frames);
}
}
frames_.erase(frames_.begin(), ++frame_it);
// 将帧存放到 frames_out
frames_out.push_back(frame);
}
if (!superframe_delayed_by_retransmission) {
int64_t frame_delay;
// 根据首帧的时间戳和接收时间 计算帧延迟并保存到 frame_delay
if (inter_frame_delay_.CalculateDelay(first_frame->Timestamp(),
&frame_delay, receive_time_ms)) {
// 根据 帧延迟 和 superframe 帧大小 更新抖动估计
jitter_estimator_.UpdateEstimate(frame_delay, superframe_size);
}
float rtt_mult = protection_mode_ == kProtectionNackFEC ? 0.0 : 1.0;
absl::optional<float> rtt_mult_add_cap_ms = absl::nullopt;
if (rtt_mult_settings_.has_value()) {
rtt_mult = rtt_mult_settings_->rtt_mult_setting;
rtt_mult_add_cap_ms = rtt_mult_settings_->rtt_mult_add_cap_ms;
}
// 设置 jitter 延迟
timing_->SetJitterDelay(
jitter_estimator_.GetJitterEstimate(rtt_mult, rtt_mult_add_cap_ms));
// 更新当前延迟
timing_->UpdateCurrentDelay(render_time_ms, now_ms);
} else {
if (RttMultExperiment::RttMultEnabled() || add_rtt_to_playout_delay_)
jitter_estimator_.FrameNacked();
}
UpdateJitterDelay();
UpdateTimingFrameInfo();
if (frames_out.size() == 1) {
return frames_out[0];
} else {
return CombineAndDeleteFrames(frames_out);
}
}
webrtc jitter buffer相关推荐
- java jitter buffer_android webrtc jitter buffer大小设置
1. PeerConnectionClient.java 设置在如下接口: private void createPeerConnectionInternal(Context context,EglB ...
- 网易干货 | 浅析视频Jitter Buffer
一. 什么是JitterBuffer Jitter Buffer也叫做抖动缓冲区,它是实时音视频里面的一个重要模块,它对数据包丢失.乱序.延迟到达等情况进行处理,平滑的向解码模块输出数据包/帧,抵抗各 ...
- rtcp 的jitter buffer
//@[TOC](jitter buffer) jitter buffer rtcp xr jb 草案 觉得这篇draft写的挺好,适合入门阅读. zhangbin 20191128 翻译 jitte ...
- 音频传输之Jitter Buffer设计与实现
在语音通信中Jitter Buffer(下面简称JB)是接收侧一个非常重要的模块,它是决定音质的重要因素之一.一方面它会把收到的乱序的语音包排好序放在buffer里正确的位置上,另一方面它把接收到的语 ...
- WebRTC内置debug工具,详细参数解读
为了确保这篇文章所写内容尽可能的准确,我决定请来Philipp Hancke来作为此篇文章的共同作者. 当你想要找到你WebRTC产品中的问题时,webrtc-internals是一个非常棒的工具,因 ...
- webrtc fec
转自:http://www.cnblogs.com/webrtc/p/7402570.html WebRTC::FEC [TOC] Tags: WebRTC FEC WebRTC中的 FEC 实现分为 ...
- WebRTC音频预处理单元APM的整体编译及使用
正文 然而本次优化仍然没能用上整套VoE,因为VoE不仅仅包含音频预处理,它将音频编码模块.传输模块一并融入了引擎,而bill的项目需要使用既有的编码.传输层,因此使用整个VoE对我来说显得冗余且不可 ...
- WebRTC详解-zz
1.WebRTC目的 WebRTC(Web Real-Time Communication)项目的最终目的主要是让Web开发者能够基于浏览器(Chrome\FireFox\...) 轻易快捷开发出丰富 ...
- 技术宝典 | 基于标准 WebRTC 低延迟直播的开源实践
导读:2020年,新冠疫情爆发并席卷全球,对包括中国在内的全球经济造成了巨大的冲击,同时深刻影响了社会生活.在这一背景下,以消费市场上轰轰烈烈的直播电商为引爆点,直播行业再次掀起热潮.在中国企业数字化 ...
最新文章
- jquery 实现iframe 自适应高度
- 底层经典书籍-编译原理
- C++之error: cannot bind non-const lvalue reference of type ‘myString’ to an rvalue of type ‘myString
- Nginx/Apache发大招
- Visual Studio Code 1.42 发布
- CodeForces 489A SwapSort (选择排序法)
- python 进度条_2019年的代码都写完了吗?不如做个Python进度条看看还剩多少
- Gmail priority inbox帮助你减少工作量
- express不是内部或外部命令的解决方法
- 串行加法器 并行加法器 超前进位加法器
- 指标之王macd的计算公式和使用方法
- 6.13编一程序,将两个字符串连接起来,不要用strcat函数。
- 用python计算个人所得税计算器_教你使用Python实现新个税计算器
- 图说三极管的三个工作状态
- C语言正数和负数循环右移,左移,把某正数的第m位(从0开始)到n位取反
- 微信小程序校验网络连接是否正常,无网络时给出提示
- 企业项目开发中可遵循的时间管理守则-华为时间管理
- [转]数据库巡检报告模板
- 图片编辑的利器(介绍韩国免费图片工具PhotoScape)
- 基于MATLAB Simulink的三相AC DC整流后的直流电机转速开环控制仿真模型
热门文章
- 高精度减法(C语言实现)
- LeetCode刷题(43)~汉明距离【异或+布赖恩·克尼根算法】
- java startw_java-Jetty(9.4.12):上下文启动失败o.e.j.w.WebAp...
- 上海现有电视频道分配表
- anaconda安装pythonocc
- 二叉树的遍历(先序、中序、后序)
- Linux 系统Trouble shouting及修复FATAL: NO bootale medium found! System halted
- 软件测试的魅力何在?您为什么选择测试一行而不做开发?
- 【洛谷P5514】永夜的报应【模拟】
- SpringMVC基础三