测试方向
测试方法
由于测试目的是为了分析 webrtc 在局域网内的延时情况,所以选择在本地主机和虚拟机之间测试通信延时。
因为是双屏,所以本地主机和虚拟机各占一个显示屏,将虚拟机的屏幕投递到本地主机,通过点击键盘上的 Print Screen键,可以同时捕获两个屏幕,再将捕获的图片粘贴到画图板或者通过其他软件打开,可以比较两者的延时。
在开始共享屏幕的时候,会弹出对话框让选择是否共享音频,通过比较勾选和取消勾选共享音频选项,判断音频对共享屏幕延时的影响。
(1)通过在浏览器上开启在线秒表,观察投递源和目标屏幕上的时间。在开始共享屏幕之前就开启在线秒表,刚开始共享屏幕时,源屏幕上的时间为:
目标屏幕上接收到共享屏幕显示的时间为:
可以看到延时 2 秒加 752 毫秒,数量级上肯定达到秒级的。等待一段时间,延时最终会收敛,稳定到某一个较小的数值范围,达到200到400毫秒的范围,当然可能更大或者更小,但是数量级上是100毫秒。
(2)经过等待,进行第二次采样
源屏幕上的时间为:
目标屏幕上的时间为:
两个屏幕上的时间差为 264 ms。
(3)进行第三次采样
源屏幕上的时间为:
目标屏幕上的时间为:
两个屏幕上的时间差为 269 ms。
这是勾选了音频选项情况下的延时,可见稳定的延时在 200ms 左右。
(1) 取消勾选共享音频的选项,刚开始共享屏幕时,源端秒表计时为:
目的端秒表计时为:
可以看到两者的延时为 1秒加495 毫秒。
(2) 经过一段时间的等待,继续观察延时情况,源端的秒表计时为:
目的端的秒表计时为:
延时趋于稳定到某一个范围后,两者的延时为 171ms。
(3) 再次取样,观察延时时间
源端的秒表计时为:
目的端的秒表计时为:
延时还在继续减小,此时的延时为 79ms 。
(4) 第四次取样,观察延时时间
源端秒表计时为:
目的端秒表计时为:
延时又增大了,达到 143ms。
后续又观察了很多次,延时时间围绕 100ms 波动,但一般都在 50 ~ 200 毫秒的范围内。
通过多次测试比较,发现共享音频会造成额外的几十到几百毫秒的延时,根据之前博客的分析,在一次测试中, webrtc 的日志文件中记录的 WebRTC.Video.AVSyncOffsetInMs 平均时间是 65 毫秒。因此,猜测可能是因为音视频同步造成了额外的延时。
在 webrtc 中视频抖动缓冲区是动态变化的,因为目前的屏幕共享仅限于局域网,因此可以考虑将抖动缓冲区设置为较小的固定值。
音视频通信过程中,接收方在收到对方的音视频流数据后,数据流会进入缓冲区,缓冲一定的时间才开始播放,这样可以消除网络抖动对通信质量的影响,缓冲时间越长,应对网络抖动的能力越强,但是延迟也越大。不同的应用场景,不同的网络环境,可以设置不同的缓冲时间。
webrtc 的抖动缓冲代码主要在:
src\third_party\webrtc\modules\video_coding\jitter_estimator.cc
src\third_party\webrtc\modules\video_coding\jitter_estimator.h
src\third_party\webrtc\modules\video_coding\frame_buffer2.cc
src\third_party\webrtc\modules\video_coding\frame_buffer2.h
相关类图如图所示:
其中,ref 表示只是引用,但不拥有这个对象;而 possess 则表示拥有这个对象。
当 VideoReceiveStream 对象在开始接收数据之前,会执行如下准备工作,这里只列出关心的部分代码:
void VideoReceiveStream::Start() {
...
frame_buffer_->Start();
...
// Start the decode thread
decode_thread_.Start();
rtp_video_stream_receiver_.StartReceive();
}
准备工作中,启动 FrameBuffer 对象,开启解码线程,之后,开始接收数据。所有接收到的数据首先进入缓冲区,
这里写代码片void VideoReceiveStream::OnCompleteFrame(
std::unique_ptr<video_coding::FrameObject> frame) {
int last_continuous_pid = frame_buffer_->InsertFrame(std::move(frame));
if (last_continuous_pid != -1)
rtp_video_stream_receiver_.FrameContinuous(last_continuous_pid);
}
解码线程从 FrameBuffer 中取出数据进行解码,
bool VideoReceiveStream::Decode() {
...
std::unique_ptr frame;
video_coding::FrameBuffer::ReturnReason res =
frame_buffer_->NextFrame(wait_ms, &frame);
if (frame) {
int64_t now_ms = clock_->TimeInMilliseconds();
RTC_DCHECK_EQ(res, video_coding::FrameBuffer::ReturnReason::kFrameFound);
if (video_receiver_.Decode(frame.get()) == VCM_OK) {
keyframe_required_ = false;
frame_decoded_ = true;
rtp_video_stream_receiver_.FrameDecoded(frame->picture_id);
}
...
}
return true;
}
代码 frame_buffer_->NextFrame(wait_ms, &frame) 是从 FrameBuffer 中取出数据。
代码 video_receiver_.Decode(frame.get()) 是执行对 frame 的解码操作。
FrameBuffer 的 NextFrame 函数定义如下:
// 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.
FrameBuffer::ReturnReason FrameBuffer::NextFrame(
int64_t max_wait_time_ms,
std::unique_ptr* frame_out,
bool keyframe_required) {
...
do {
// 等待新的连续帧的到来
...
} while (new_continuous_frame_event_.Wait(wait_ms));
{
...
std::unique_ptr frame =
std::move(next_frame_it_->second.frame);
if (inter_frame_delay_.CalculateDelay(frame->timestamp, &frame_delay,
frame->ReceivedTime())) {
jitter_estimator_->UpdateEstimate(frame_delay, frame->size());
}
float rtt_mult = protection_mode_ == kProtectionNackFEC ? 0.0 : 1.0;
timing_->SetJitterDelay(jitter_estimator_->GetJitterEstimate(rtt_mult));
timing_->UpdateCurrentDelay(frame->RenderTime(), now_ms);
} else {
if (webrtc::field_trial::IsEnabled("WebRTC-AddRttToPlayoutDelay"))
jitter_estimator_->FrameNacked();
}
// Gracefully handle bad RTP timestamps and render time issues.
if (HasBadRenderTiming(*frame, now_ms)) {
jitter_estimator_->Reset();
timing_->Reset();
frame->SetRenderTime(timing_->RenderTimeMs(frame->timestamp, now_ms));
}
UpdateJitterDelay();
UpdateTimingFrameInfo();
PropagateDecodability(next_frame_it_->second);
...
*frame_out = std::move(frame);
...
}
在 FrameBuffer 的 NextFrame 函数中,最晚会在 max_wait_time_ms 后退出,如果在 max_wait_time_ms 时间内取得数据帧,就返回 kFrameFound,否则就返回 kTimeout,如果 FrameBuffer 被停止了,那么会返回 kStopped。之后,VCMJitterEstimator 计算并更新抖动估计延时的估计时间。
看了这段代码后,以为控制延时是通过 timing_->SetJitterDelay() 来设置的,但是尝试之后没有任何效果。查看代码,发现这个设置只是根据 webrtc 上下文计算当前的延时,是为了统计输出用的,这是延时的结果,而不是延时的起因。
在 FrameBuffer 中,有一个成员函数 UpdatePlayoutDelays(),根据注释可知这个函数是“根据数据帧来更新最大和最小播放延迟”。其实现为:
void FrameBuffer::UpdatePlayoutDelays(const FrameObject& frame) {
TRACE_EVENT0("webrtc", "FrameBuffer::UpdatePlayoutDelays");
PlayoutDelay playout_delay = frame.EncodedImage().playout_delay_;
if (playout_delay.min_ms >= 0)
timing_->set_min_playout_delay(playout_delay.min_ms);
if (playout_delay.max_ms >= 0)
timing_->set_max_playout_delay(playout_delay.max_ms);
}
添加日志,打印 playout_delay.min_ms 和 playout_delay.max_ms ,发现这两个值均为 -1,分析 -1 代表的含义。查看 PlayoutDelay 的实现:
struct PlayoutDelay {
int min_ms;
int max_ms;
};
分析代码中对这个结构体的注释:
// Minimum and maximum playout delay values from capture to render.
// These are best effort values.
//
// A value < 0 indicates no change from previous valid value.
//
// min = max = 0 indicates that the receiver should try and render
// frame as soon as possible.
//
// min = x, max = y indicates that the receiver is free to adapt
// in the range (x, y) based on network jitter.
//
// Note: Given that this gets embedded in a union, it is up-to the owner to
// initialize these values.
-1 表示播出最大和最小延时和前面的有效值保持一致。
如果最大值和最小值均为0,表示接收端应当尽可能快的渲染当前数据帧。
修改前面的 UpdatePlayoutDelays() 代码,修改后代码如下:
void FrameBuffer::UpdatePlayoutDelays(const FrameObject& frame) {
TRACE_EVENT0("webrtc", "FrameBuffer::UpdatePlayoutDelays");
PlayoutDelay playout_delay = frame.EncodedImage().playout_delay_;
if (playout_delay.min_ms >= 0)
timing_->set_min_playout_delay(playout_delay.min_ms);
if (playout_delay.max_ms >= 0)
timing_->set_max_playout_delay(playout_delay.max_ms);
timing_->set_min_playout_delay(0);
timing_->set_max_playout_delay(0);
}
跟踪代码 frame.EncodedImage().playout_delay_ 寻找 playout_delay_ 首次被设置的位置,得到如下图的类继承关系图:
函数 UpdatePlayoutDelays() 的参数 FrameObject 其实是引用了 RtpFrameObject 的对象,收到数据包构建数据帧时,在 RtpFrameObject 的构造函数中有如下代码:
// Setting frame's playout delays to the same values
// as of the first packet's.
SetPlayoutDelay(first_packet->video_header.playout_delay);
继续跟踪下去,在 VCMPacket 的构造函数中,有如下初始化的代码:
VCMPacket::VCMPacket()
: payloadType(0),
...
video_header(),
receive_time_ms(0) {
video_header.playout_delay = {-1, -1};
}
这里就是播放延时最开始赋值的位置,根据打印的日志可以看出,后面基本上都是直接使用这个默认的播出延时设置。
根据上面修改的最大和最小播出延时时间,编译运行,在选择共享音频的条件下,测试延时情况。
(1)刚开始建立连接时,延时情况
源端秒表计时时间:
目的端秒表计时时间:
在刚开始共享屏幕的时候,两者延时达到2秒加454毫秒,将最大和最小播出延迟时间修改为 0,无法降低刚开始的播出延时。
(2)系统投屏一段时间后,延时情况
源端秒表计时时间:
目的端秒表计时时间:
在共享屏幕一段时间后,两者的延时惊人的降到 1ms 以下,很明显,设置最大和最小播出延迟时间为 0 起作用了。
(3) 经过一段时间之后,再次采样
源端秒表计时时间:
目的端秒表计时时间:
两端的延时为 47ms,可见即使设置了应当尽可能快的渲染播出,但也会有一定的延时。
之后,又经过多次采样,结果延时时间都落在 50ms 左右 或者小于 1ms,总的来说,整个延时将在 100ms 以内。
抖动缓冲区以及 FrameBuffer 这些缓冲区的大小,都是由最大最小放出延时决定的。当我们设置了期望的播出延时时,webrtc 会自己分解延时目标,根据内部机制调整这些缓冲区的大小。
经过以上局域网内的延时测试,在局域网内使用 webrtc 能够得出以下结论:
(1) 延时主要是由接收端决定的;
(2) 音频对共享屏幕的延时影响甚微;
(3) 编解码耗时都在 10ms 以内;
(4) 根据最后的稳定下来的结果来看,局域网内传输延时非常小;
(5) webrtc 在局域网内的延时很大程度上受最大和最小播出延时决定。
参考:
WebRTC视频JitterBuff