1.可能导致延时的因素

测试方向

  • 音频对延时的影响,音频的处理耗时,以及音视频同步耗时;
  • 视频抖动缓冲延时,在局域网内,如果缩小抖动缓冲区,可能会减少延时;

测试方法
由于测试目的是为了分析 webrtc 在局域网内的延时情况,所以选择在本地主机和虚拟机之间测试通信延时。
因为是双屏,所以本地主机和虚拟机各占一个显示屏,将虚拟机的屏幕投递到本地主机,通过点击键盘上的 Print Screen键,可以同时捕获两个屏幕,再将捕获的图片粘贴到画图板或者通过其他软件打开,可以比较两者的延时。

2.测试音频共享屏幕延时

2.1 共享音频

在开始共享屏幕的时候,会弹出对话框让选择是否共享音频,通过比较勾选和取消勾选共享音频选项,判断音频对共享屏幕延时的影响。

(1)通过在浏览器上开启在线秒表,观察投递源和目标屏幕上的时间。在开始共享屏幕之前就开启在线秒表,刚开始共享屏幕时,源屏幕上的时间为:

目标屏幕上接收到共享屏幕显示的时间为:

可以看到延时 2 秒加 752 毫秒,数量级上肯定达到秒级的。等待一段时间,延时最终会收敛,稳定到某一个较小的数值范围,达到200到400毫秒的范围,当然可能更大或者更小,但是数量级上是100毫秒。
(2)经过等待,进行第二次采样
源屏幕上的时间为:

目标屏幕上的时间为:

两个屏幕上的时间差为 264 ms。
(3)进行第三次采样
源屏幕上的时间为:

目标屏幕上的时间为:

两个屏幕上的时间差为 269 ms。
这是勾选了音频选项情况下的延时,可见稳定的延时在 200ms 左右。

2.2 不共享音频

(1) 取消勾选共享音频的选项,刚开始共享屏幕时,源端秒表计时为:

目的端秒表计时为:

可以看到两者的延时为 1秒加495 毫秒。
(2) 经过一段时间的等待,继续观察延时情况,源端的秒表计时为:

目的端的秒表计时为:

延时趋于稳定到某一个范围后,两者的延时为 171ms。
(3) 再次取样,观察延时时间
源端的秒表计时为:

目的端的秒表计时为:

延时还在继续减小,此时的延时为 79ms 。
(4) 第四次取样,观察延时时间
源端秒表计时为:

目的端秒表计时为:

延时又增大了,达到 143ms。
后续又观察了很多次,延时时间围绕 100ms 波动,但一般都在 50 ~ 200 毫秒的范围内。

2.3 小结

通过多次测试比较,发现共享音频会造成额外的几十到几百毫秒的延时,根据之前博客的分析,在一次测试中, webrtc 的日志文件中记录的 WebRTC.Video.AVSyncOffsetInMs 平均时间是 65 毫秒。因此,猜测可能是因为音视频同步造成了额外的延时。

3.更改抖动缓冲区的大小

在 webrtc 中视频抖动缓冲区是动态变化的,因为目前的屏幕共享仅限于局域网,因此可以考虑将抖动缓冲区设置为较小的固定值。

3.1 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 threaddecode_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<video_coding::FrameObject> 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<FrameObject>* frame_out,bool keyframe_required) {...do {// 等待新的连续帧的到来...} while (new_continuous_frame_event_.Wait(wait_ms));{...std::unique_ptr<FrameObject> 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 上下文计算当前的延时,是为了统计输出用的,这是延时的结果,而不是延时的起因。

3.2 播放延时

在 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};
}

这里就是播放延时最开始赋值的位置,根据打印的日志可以看出,后面基本上都是直接使用这个默认的播出延时设置。

3.3 延时效果测试

根据上面修改的最大和最小播出延时时间,编译运行,在选择共享音频的条件下,测试延时情况。
(1)刚开始建立连接时,延时情况
源端秒表计时时间:

目的端秒表计时时间:

在刚开始共享屏幕的时候,两者延时达到2秒加454毫秒,将最大和最小播出延迟时间修改为 0,无法降低刚开始的播出延时。
(2)系统投屏一段时间后,延时情况
源端秒表计时时间:

目的端秒表计时时间:

在共享屏幕一段时间后,两者的延时惊人的降到 1ms 以下,很明显,设置最大和最小播出延迟时间为 0 起作用了。
(3) 经过一段时间之后,再次采样
源端秒表计时时间:

目的端秒表计时时间:

两端的延时为 47ms,可见即使设置了应当尽可能快的渲染播出,但也会有一定的延时。
之后,又经过多次采样,结果延时时间都落在 50ms 左右 或者小于 1ms,总的来说,整个延时将在 100ms 以内。

3.4 小结

抖动缓冲区以及 FrameBuffer 这些缓冲区的大小,都是由最大最小放出延时决定的。当我们设置了期望的播出延时时,webrtc 会自己分解延时目标,根据内部机制调整这些缓冲区的大小。

4. 结论

经过以上局域网内的延时测试,在局域网内使用 webrtc 能够得出以下结论:
(1) 延时主要是由接收端决定的;
(2) 音频对共享屏幕的延时影响甚微;
(3) 编解码耗时都在 10ms 以内;
(4) 根据最后的稳定下来的结果来看,局域网内传输延时非常小;
(5) webrtc 在局域网内的延时很大程度上受最大和最小播出延时决定。

参考:
WebRTC视频JitterBuff

webrtc 共享屏幕延时测试相关推荐

  1. 通过 WebRTC 共享屏幕很容易

    简介 网络会议中常用的屏幕共享功能使用 WebRTC 提供的 getDisplayMedia API 就能轻松实现,接口如下 var promise = navigator.mediaDevices. ...

  2. 传统大华海康宇视安防摄像头RTSP流如何转webrtc直播低延时无插件浏览器视频播放

    传统大华海康宇视安防摄像头RTSP流如何转webrtc直播低延时无插件浏览器视频播放 1.问题场景 2.WEBRTC延时对比 3.LiveNVR支持WEBRTC输出 4.RTSP/HLS/FLV/RT ...

  3. TSINGSEE青犀视频使用Vue.js搭建前端启动后共享屏幕无法获取音视频流问题解决

    TSINGSEE青犀视频云边端架构产品的前端搭建大多是通过Vue来完成的,Vue的核心库只关注视图层,非常容易与其它库或已有项目整合,并且有能力驱动采用单文件组件和Vue生态系统支持的库开发的复杂单页 ...

  4. LiveQing视频点播RTMP推流直播服务支持H5无插件WebRTC超低延时视频直播

    LiveQing视频点播RTMP推流直播服务支持H5无插件WebRTC超低延时视频直播 1.WebRTC超低延时视频直播 2.WebRTC延时对比 3.LiveQing播放WebRTC流 4.分屏页面 ...

  5. windows录屏html文件,录音、录屏、共享屏幕怎么玩?

    为什么要录屏.录音? 有的时候产品.开发同学不在现场,用研需要一些证据给他们看,还有就是帮助我们自己回忆整理.但很多时候产品和设计都在现场,我们当时就会把问题记录下来,也可以不录屏,直接录音就好了. ...

  6. 量化延时法时间测量_「交易技术前沿」交易系统低延时测试与分析

    本文选自 <交易技术前沿>总第三十三期文章(2018年12月) 证券期货行业测试中心(中金所) 魏畅 陈冬严 张鸿晔 摘要:订单延时(Latency)是衡量交易系统性能的重要指标.本文利用 ...

  7. 毕业生共享屏幕3小时 被骗70万

    常州的小王刚研究生毕业,近日,他接到一个电话,让他更新支付软件信息,并指导他下载了一款APP. 通过这款APP的共享屏幕功能,对方实时掌握了小王的支付信息,并指导他从网贷平台贷款,3个小时共操作18次 ...

  8. 共享屏幕,录屏的方法

    不管是录制屏幕还是共享屏幕首先要有chrome浏览器. 请自行在百度搜索下载. 录制屏幕的方法: 安装拓展工具: 安装地址:https://chrome.google.com/webstore/det ...

  9. android 图片 色温,屏幕色温测试及测试结果

    屏幕色温测试及测试结果 在拍摄的照片中,经常能听到人们评价照片是偏冷或者是偏暖,这个"冷"和"暖"就是色温的表现.色温是屏幕的一个很重要的标准,如果红辐射相对说 ...

  10. mac设置共享屏幕 苹果mac屏幕共享设置详细教程

    2019独角兽企业重金招聘Python工程师标准>>> 苹果mac怎么共享屏幕?OS X 自带屏幕共享功能,支持拖拽以及文本拷贝,操作还相当简单,仅需要点几个按钮就搞定:另外,还可以 ...

最新文章

  1. 零基础自学python的建议-零基础学python是学2还是3好
  2. nil 与 release
  3. leetcode55. 跳跃游戏
  4. 求解相机参数Camera Calibration
  5. jq之fadeIn()
  6. 【MySQL通过视图(或临时表)实现动态SQL(游标】
  7. 人工智能是否未来可期?3本人工智能书带你优雅升级
  8. CMake编译protobuf
  9. GitHub 使用教程图文详解(转)
  10. 暴风陨落,再无影音​
  11. 阿里如何实现海量数据实时分析?
  12. 用Python删除含有特定字符串的行
  13. 35岁,转行AI年薪100万,牛逼的人生无需解释!
  14. 22张图带你了解IP地址有什么作用
  15. ant 的详细的入门教程
  16. 《自己动手写嵌入式操作系统》阅读笔记之操作系统小知识
  17. Ubuntu1804安装及基本配置
  18. java 关键字 保留字_什么是Java关键字和保留字?
  19. C模板:十进制和十六进制数据批量转换
  20. WMS 原型详解 | 产品经理最讨厌的系统

热门文章

  1. 测试淘宝购物流程图,梳理基本流和备选流,测试用例
  2. 贾维斯雨滴桌面(素材跟教程都有)
  3. 最新emoji表情代码大全_三十而已表情包下载-三十而已表情包大全最新下载
  4. vue+IOS9页面白屏
  5. 使用MongoVUE
  6. AD9 PCB文件黑色区域如何改变?
  7. 如何删除服务中不存在在服务
  8. java借书_用java语言实现借书系统
  9. 苹果平板怎么录屏_使用平板快速设计制作书写类教学视频
  10. ubuntu处理openproj不能使用问题