webrtc 共享屏幕延时测试

1.可能导致延时的因素

测试方向

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

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

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

2.1 共享音频

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

(1)通过在浏览器上开启在线秒表,观察投递源和目标屏幕上的时间。在开始共享屏幕之前就开启在线秒表,刚开始共享屏幕时,源屏幕上的时间为:
webrtc 共享屏幕延时测试_第2张图片
目标屏幕上接收到共享屏幕显示的时间为:
webrtc 共享屏幕延时测试_第3张图片
可以看到延时 2 秒加 752 毫秒,数量级上肯定达到秒级的。等待一段时间,延时最终会收敛,稳定到某一个较小的数值范围,达到200到400毫秒的范围,当然可能更大或者更小,但是数量级上是100毫秒。
(2)经过等待,进行第二次采样
源屏幕上的时间为:
webrtc 共享屏幕延时测试_第4张图片
目标屏幕上的时间为:
webrtc 共享屏幕延时测试_第5张图片
两个屏幕上的时间差为 264 ms。
(3)进行第三次采样
源屏幕上的时间为:
webrtc 共享屏幕延时测试_第6张图片
目标屏幕上的时间为:
webrtc 共享屏幕延时测试_第7张图片
两个屏幕上的时间差为 269 ms。
这是勾选了音频选项情况下的延时,可见稳定的延时在 200ms 左右。

2.2 不共享音频

(1) 取消勾选共享音频的选项,刚开始共享屏幕时,源端秒表计时为:
webrtc 共享屏幕延时测试_第8张图片
目的端秒表计时为:
webrtc 共享屏幕延时测试_第9张图片
可以看到两者的延时为 1秒加495 毫秒。
(2) 经过一段时间的等待,继续观察延时情况,源端的秒表计时为:
webrtc 共享屏幕延时测试_第10张图片
目的端的秒表计时为:
webrtc 共享屏幕延时测试_第11张图片
延时趋于稳定到某一个范围后,两者的延时为 171ms。
(3) 再次取样,观察延时时间
源端的秒表计时为:
webrtc 共享屏幕延时测试_第12张图片
目的端的秒表计时为:
这里写图片描述
延时还在继续减小,此时的延时为 79ms 。
(4) 第四次取样,观察延时时间
源端秒表计时为:
webrtc 共享屏幕延时测试_第13张图片
目的端秒表计时为:
webrtc 共享屏幕延时测试_第14张图片
延时又增大了,达到 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

相关类图如图所示:
webrtc 共享屏幕延时测试_第15张图片
其中,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 上下文计算当前的延时,是为了统计输出用的,这是延时的结果,而不是延时的起因。

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_ 首次被设置的位置,得到如下图的类继承关系图:
webrtc 共享屏幕延时测试_第16张图片
函数 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)刚开始建立连接时,延时情况
源端秒表计时时间:
webrtc 共享屏幕延时测试_第17张图片
目的端秒表计时时间:
webrtc 共享屏幕延时测试_第18张图片
在刚开始共享屏幕的时候,两者延时达到2秒加454毫秒,将最大和最小播出延迟时间修改为 0,无法降低刚开始的播出延时。
(2)系统投屏一段时间后,延时情况
源端秒表计时时间:
webrtc 共享屏幕延时测试_第19张图片
目的端秒表计时时间:
webrtc 共享屏幕延时测试_第20张图片
在共享屏幕一段时间后,两者的延时惊人的降到 1ms 以下,很明显,设置最大和最小播出延迟时间为 0 起作用了。
(3) 经过一段时间之后,再次采样
源端秒表计时时间:
webrtc 共享屏幕延时测试_第21张图片
目的端秒表计时时间:
webrtc 共享屏幕延时测试_第22张图片
两端的延时为 47ms,可见即使设置了应当尽可能快的渲染播出,但也会有一定的延时。
之后,又经过多次采样,结果延时时间都落在 50ms 左右 或者小于 1ms,总的来说,整个延时将在 100ms 以内。

3.4 小结

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

4. 结论

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

参考:
WebRTC视频JitterBuff

你可能感兴趣的:(webrtc)