WebRTC帧率调整策略 |
您所在的位置:网站首页 › 30帧降到25帧 › WebRTC帧率调整策略 |
表示数组Tf的大小,当sizeof(Tf) if (now_ms - encoded_frame_samples_.front().time_complete_ms > kBitrateAverageWinMs) { encoded_frame_samples_.pop_front(); } else { break; } } } void MediaOptimization::UpdateSentFramerate() { if (encoded_frame_samples_.size() avg_sent_framerate_ = (90000 * (encoded_frame_samples_.size() - 1) + denom / 2) / denom; } else { avg_sent_framerate_ = encoded_frame_samples_.size(); } } 接收端帧率在WebRTC中,将接收端帧率分为了三种:网络接收帧率——接收端输入帧率、解码器输出帧率、视频渲染帧率。 1、网络接收帧率 网络接收帧率统计的是接收端接收到网络发送过来的视频帧帧率。在完整接收到一帧数据后,由FrameBuffer类调用ReceiveStatisticsProxy::OnCompleteFrame()来统计。具体代码如下: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 void OnCompleteFrame(bool is_keyframe, size_t size_bytes) { int64_t now_ms = clock_->TimeInMilliseconds(); frame_window_.insert(std::make_pair(now_ms, size_bytes)); int64_t old_frames_ms = now_ms - kRateStatisticsWindowSizeMs; while (!frame_window_.empty() && frame_window_.begin()->first < old_frames_ms) { frame_window_accumulated_bytes_ -= frame_window_.begin()->second; frame_window_.erase(frame_window_.begin()); } size_t framerate = (frame_window_.size() * 1000 + 500) / kRateStatisticsWindowSizeMs; stats_.network_frame_rate = static_cast(framerate); } 2、解码器输出帧率 WebRTC实现了RateStatistics来统计解码器输出帧率,在编码结束后由VideoReceiveStream调用ReceiveStatisticsProxy::OnDecodedFrame()来统计。具体代码如下: 1 2 3 4 5 6 7 void OnDecodedFrame() { uint64_t now = clock_->TimeInMilliseconds(); rtc::CritScope lock(&crit_); ++stats_.frames_decoded; decode_fps_estimator_.Update(1, now); stats_.decode_frame_rate = decode_fps_estimator_.Rate(now).value_or(0); } 3、视频渲染帧率 WebRTC实现了RateStatistics来统计视频渲染帧率,在视频渲染结束后由VideoReceiveStream调用ReceiveStatisticsProxy::OnRenderedFrame()来统计。具体代码如下: 1 2 3 4 5 6 7 void OnRenderedFrame(const VideoFrame& frame) { uint64_t now = clock_->TimeInMilliseconds(); rtc::CritScope lock(&crit_); renders_fps_estimator_.Update(1, now); stats_.render_frame_rate = renders_fps_estimator_.Rate(now).value_or(0); ++stats_.frames_rendered; } 发送端帧率策略影响发送端帧率的主要因素包含:视频采集(摄像头/桌面)帧率、编码器性能。 视频采集帧率策略摄像头是视频采集的来源,其帧率决定了视频会议帧率的上限。与摄像头采集相关的参数包含:像素格式、帧率和分辨率。下表列出了ThinkPad T440P自带摄像头支持的部分视频格式: 格式分辨率帧率MJPG1280x72030MJPG640x36030YUY21280x72010YUY2640x36030可以看出对于YUY2格式,1280x720的帧率仅为10帧,要想达到30帧必须要采用MJPG格式。这是因为,同样是1280x720分辨率,30帧YUY2和MJPG格式需要传输的数据量分别为: YUY2:1280x720x30x2x8=421MbpsMJPG:1280x720x30x3x8/20=32MbpsYUY2需要的传输带宽过大,所以很多摄像头对于RGB、YUV等格式1280x720仅支持10帧。然而10帧是远远不能够满足视频会议的帧率需求的,因此在选择视频采集规格时,需要注意像素格式、帧率和分辨率的权衡。在实际应用中,我们可以采集MJPG格式1280x720x30视频规格,然后在应用层转换为YUV格式。WebRTC在“webrtc/modules/video_capture/video_capture_impl.cc”的VideoCaptureImpl类中实现了转换: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 if (frameInfo.codecType == kVideoCodecUnknown) { /* Not encoded, convert to I420. */ const VideoType commonVideoType = RawVideoTypeToCommonVideoVideoType(frameInfo.rawType); if (frameInfo.rawType != kVideoMJPEG && CalcBufferSize(commonVideoType, width, abs(height)) != videoFrameLength) { return -1; } int stride_y = width, stride_uv = (width + 1) / 2; int target_width = width, target_height = height; rtc::scoped_refptr buffer = I420Buffer::Create( target_width, abs(target_height), stride_y, stride_uv, stride_uv); const int conversionResult = ConvertToI420( commonVideoType, videoFrame, 0, 0, width, height, videoFrameLength, apply_rotation ? _rotateFrame : kVideoRotation_0, buffer.get()); } 最终得到的YUV格式的视频数据会被送到编码器中被编码,需要注意:不是所有的视频数据都会被编码器编码,详细内容将在下一节介绍。 采集编码丢帧策略受限于系统硬件性能和编码器性能,视频采集图片的速度有可能比编码器编码速度快,这将导致多余的图片帧在编码器任务队列中累积。由于视频会议需要较低的时延,编码器必须要及时处理最新的帧,此时WebRTC采取丢帧策略——当有多个帧在编码器任务队列时,只编码最新的一帧。WebRTC在“webrtc/video/vie_encoder.cc”文件EncodeTask类中实现了该策略: 1 2 3 4 5 6 7 8 9 10 11 bool Run() override { ++vie_encoder_->captured_frame_count_; if (--vie_encoder_->posted_frames_waiting_for_encode_ == 0) { vie_encoder_->EncodeVideoFrame(frame_, time_when_posted_us_); } else { /* There is a newer frame in flight. Do not encode this frame. */ LOG(LS_VERBOSE) dropped_frame_count_; } return true; } 可以看出,由于编码器是阻塞的,如果编码器性能或系统硬件性能较差,编码器会丢掉因阻塞而累积的帧,进而导致发送端帧率降低。在具体使用场景中,这往往会导致两种现象: 接收端黑屏:如果发送端一开始就卡死在编码器中,接收端会一直黑屏,直到第一个帧编码完成;接收端卡顿:如果发送端运行后经常阻塞在编码器中,接收端会卡顿,严重影响视频质量。因此,摄像头采集帧率并不等于编码器的实际输入帧率,MediaOptimization类中得到的编码器实际输入帧率,需要在下次编码前设置为编码器的输入帧率。 恒定码率丢帧策略除了上文所述的采集编码丢帧策略,WebRTC还实现了一种漏桶算法的变体,用于跟踪何时应该主动丢帧,以避免编码器无法保持其比特率时,产生过高的比特率。漏桶算法的示意图如下: 漏桶算法的实现位于“webrtc/modules/video_coding/frame_dropper.cc”中的FrameDropper类,其实现了三个关键方法: Fill()Leak()DropFrame()从字面上可以看出,这三个方法对应于上图所示漏桶算法的三个操作。这三个方法都在MediaOptimization类被调用。 首先,来看看FrameDropper类的核心参数: 漏桶容积:accumulator_max_,其值为target-bps×kLeakyBucketSizeSeconds,随目标码率改变而改变;漏桶累积:accumulator_,其表示漏桶累积的字节数,每次Fill()时增加,每次Leak()时减少,其最大值为target-bps×kAccumulatorCapBufferSizeSecs;丢帧率:drop_ratio_,其为一个指数滤波器,使丢帧率保持一个平滑的变化过程,每次Leak()后更新丢帧率;关键帧率:key_frame_ratio_,其为一个指数滤波器,使关键帧率保持一个平滑的变化过程,每次Fill()后更新;差分帧码率:delta_frame_size_avg_kbits_,其为一个指数滤波器,使关键帧率保持一个平滑的变化过程,每次Fill()后更新。其次,为了防止关键帧和较大的差分帧立即溢出,进而导致后续较小的帧出现较高丢帧,关键帧和较大的差分帧是不会被立即在桶中累计。相反,这些较大的帧会在漏桶中累计前,会分成若干小块,进而在Leak()操作中逐次累计这些小块,来防止较关键帧和较大的差分帧立即溢出。FrameDropper类增加了额外的几个参数来实现该策略: large_frame_accumulation_spread_:大帧最大拆分块数,四舍五入取整;large_frame_accumulation_count_:大帧剩余拆分块数,四舍五入取整;large_frame_accumulation_chunk_size_:单个块尺寸,其值为framesize/large_frame_accumulation_count_。最后,来看看FrameDropper类的核心操作: 1、Fill() 当视频帧被编码后,MediaOptimization类会调用Fill()方法来填充漏桶。调用顺序很简单,主要关注Fill()方法的实现——将大帧拆分为large_frame_accumulation_count_个小块,并不累加accumulator_;将小帧直接累计accumulator_。Fill()方法同时需要更新key_frame_ratio_和delta_frame_size_avg_kbits_,用以计算大帧拆分块数和大帧判断。具体实现如下: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 void Fill(size_t framesize_bytes, bool delta_frame) { float framesize_kbits = 8.0f * static_cast(framesize_bytes) / 1000.0f; if (!delta_frame) { if (large_frame_accumulation_count_ == 0) { if (key_frame_ratio_.filtered() > 1e-5 && 1 / key_frame_ratio_.filtered() < large_frame_accumulation_spread_) { large_frame_accumulation_count_ = static_cast(1 / key_frame_ratio_.filtered() + 0.5); } else { large_frame_accumulation_count_ = static_cast(large_frame_accumulation_spread_ + 0.5); } large_frame_accumulation_chunk_size_ = framesize_kbits / large_frame_accumulation_count_; framesize_kbits = 0; } } else { if (delta_frame_size_avg_kbits_.filtered() != -1 && (framesize_kbits > kLargeDeltaFactor * delta_frame_size_avg_kbits_.filtered()) && large_frame_accumulation_count_ == 0) { large_frame_accumulation_count_ = static_cast(large_frame_accumulation_spread_ + 0.5); large_frame_accumulation_chunk_size_ = framesize_kbits / large_frame_accumulation_count_; framesize_kbits = 0; } else { delta_frame_size_avg_kbits_.Apply(1, framesize_kbits); } key_frame_ratio_.Apply(1.0, 0.0); } accumulator_ += framesize_kbits; CapAccumulator(); } 2、Leak() Leak()操作按照编码器输入帧率的频率来执行,每次Leak的大小为target_bps/input_fps,每次Leak时需要判断是否需要累计Fill()方法拆分的块,进而更新drop_ratio_。drop_ratio_的更新遵循下列原则: 当accumulator_ > 1.3f accumulator_max_,drop_ratio_基数调整为0.8f*,提高丢帧率调整加速度;当accumulator_ < 1.3f accumulator_max_,drop_ratio_基数调整为0.9f*,降低丢帧率调整加速度。实现代码如下: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 void Leak(uint32_t input_framerate) { float expected_bits_per_frame = target_bitrate_ / input_framerate; if (large_frame_accumulation_count_ > 0) { expected_bits_per_frame -= large_frame_accumulation_chunk_size_; --large_frame_accumulation_count_; } accumulator_ -= expected_bits_per_frame; if (accumulator_ < 0.0f) { accumulator_ = 0.0f; } if (accumulator_ > 1.3f * accumulator_max_) { /* Too far above accumulator max, react faster */ drop_ratio_.UpdateBase(0.8f); } else { /* Go back to normal reaction */ drop_ratio_.UpdateBase(0.9f); } if (accumulator_ > accumulator_max_) { /* We are above accumulator max, and should ideally * drop a frame. Increase the dropRatio and drop * the frame later. */ if (was_below_max_) { drop_next_ = true; } drop_ratio_.Apply(1.0f, 1.0f); drop_ratio_.UpdateBase(0.9f); } else { drop_ratio_.Apply(1.0f, 0.0f); } was_below_max_ = accumulator_ < accumulator_max_; } 3、DropFrame() DropFrame()操作用来判断是否需要将输入到编码器的这一帧丢弃,其利用drop_ratio_来使丢帧率保持一个平滑的变化过程。当drop_ratio_.filtered() >= 0.5f时,表明连续丢弃多个帧(至少一个帧);当0.0f < drop_ratio_.filtered() < 0.5f时,表明多个帧才会丢弃一个帧。具体的丢帧策略见实现: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 bool FrameDropper::DropFrame() { if (drop_ratio_.filtered() >= 0.5f) { float denom = 1.0f - drop_ratio_.filtered(); if (denom < 1e-5) { denom = 1e-5f; } int32_t limit = static_cast(1.0f / denom - 1.0f + 0.5f); int max_limit = static_cast(incoming_frame_rate_ * max_drop_duration_secs_); if (limit > max_limit) { limit = max_limit; } if (drop_count_ < 0) { drop_count_ = -drop_count_; } if (drop_count_ < limit) { drop_count_++; return true; } else { drop_count_ = 0; return false; } } else if (drop_ratio_.filtered() > 0.0f && drop_ratio_.filtered() < 0.5f) { float denom = drop_ratio_.filtered(); if (denom < 1e-5) { denom = 1e-5f; } int32_t limit = -static_cast(1.0f / denom - 1.0f + 0.5f); if (drop_count_ > 0) { drop_count_ = -drop_count_; } if (drop_count_ > limit) { if (drop_count_ == 0) { drop_count_--; return true; } else { drop_count_--; return false; } } else { drop_count_ = 0; return false; } } drop_count_ = 0; return false; } 接收端帧率策略影响接收端帧率的主要因素包含:网络状况、解码器性能、渲染速度。 网络状况导致丢帧网络因素对实时视频流的影响十分严重,当网络出现拥塞,导致较高的丢包率,明显的现象就是视频接收端帧率降到很低。比较严重时,接收端接收帧率可能只有几帧,导致无法进行正常的视频通话。WebRTC在“webrtc/modules/video_coding/packet_buffer.cc”的PacketBuffer中,将接收到的RTP包组合成一个完整的视频帧。之后,该完整的帧会被送到“webrtc/modules/video_coding/rtp_frame_reference_finder.cc”的RtpFrameReferenceFinder中。一个完整的帧可能是关键帧,也可能是参考帧,RtpFrameReferenceFinder类中关键帧直接送到解码器中处理。而对于参考帧,会判断其是否连续,若不连续会一直暂存在队列中,直到连续——送到解码器,或者下一个关键帧来了——从队列中删除。两个类相应的操作见下面两个函数: 1 2 3 4 5 bool PacketBuffer::InsertPacket(VCMPacket* packet) { } void RtpFrameReferenceFinder::ManageFrameGeneric(std::unique_ptr frame, int picture_id) { } 视频会议软件通常会采用NACK和FEC等手段来降低丢包对视频通话质量的影响。同时,解码器一定时间内,没有收到可解码数据,会向发送端请求I帧,这也就在一定程度上保证帧率不会过于低。这部分代码实现与“webrtc/video/video_receive_stream.cc”的VideoReceiveStream类中: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 void VideoReceiveStream::Decode() { static const int kMaxWaitForFrameMs = 3000; std::unique_ptr frame; video_coding::FrameBuffer::ReturnReason res = frame_buffer_->NextFrame(kMaxWaitForFrameMs, &frame); if (res == video_coding::FrameBuffer::ReturnReason::kStopped) return; if (frame) { if (video_receiver_.Decode(frame.get()) == VCM_OK) rtp_stream_receiver_.FrameDecoded(frame->picture_id); } else { RequestKeyFrame(); } } 解码导致丢帧看一下WebRTC内调用解码模块的代码,就可以看出WebRTC解码导致失败的可能原因。这部分代码位于“webrtc/modules/video_coding/video_receiver.cc”,实现如下: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 int32_t Decode(const VCMEncodedFrame& frame) { /* Decode a frame */ int32_t ret = _decoder->Decode(frame, clock_->TimeInMilliseconds()); /* Check for failed decoding, run frame type request callback if needed. */ bool request_key_frame = false; if (ret < 0) { if (ret == VCM_ERROR_REQUEST_SLI) { return RequestSliceLossIndication( _decodedFrameCallback.LastReceivedPictureID() + 1); } else { request_key_frame = true; } } else if (ret == VCM_REQUEST_SLI) { ret = RequestSliceLossIndication( _decodedFrameCallback.LastReceivedPictureID() + 1); } if (!frame.Complete() || frame.MissingFrame()) { request_key_frame = true; ret = VCM_OK; } if (request_key_frame) { rtc::CritScope cs(&process_crit_); _scheduleKeyRequest = true; } return ret; } 通过上面代码可以看出,如果解码器无法将接收到的数据解码,要么发送SLI要么发送PLI,请求重新发送关键帧。从SLI/PLI发出到收到可解码的关键帧这个时间间隔内,接收端的帧率会比正常情况低。 渲染导致丢帧在实际应用中,经过WebRTC处理后显示的帧率较大,但最终的显示效果却比较差,能够感觉到明显的卡顿。这就和应用软件的渲染有关。研究不深,暂不撰写。 |
CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3 |