WebRTC源码分析-呼叫建立过程之四(中)(创建并添加本地视频轨到PeerConnection)

目录

  • 1. 引言
  • 2. 视频轨道的创建和添加
    • 2.1 视频源的创建
      • 2.1.1 创建视频采集
        • 2.1.1.1 视频采集UML
        • 2.1.1.2 采集模块的内部数据流
      • 2.1.2 创建视频源
      • 2.1.3 创建视频轨源
    • 2.2 视频轨的创建
      • 2.2.1 PeerConnection::CreateVideoTrack
      • 2.2.2 视频轨的继承树
    • 2.3 视频数据的流动
    • 2.4 添加视频轨到PeerConnection
  • 3 总结

1. 引言

创建完PeerConnectionFactory 和 PeerConnection这两个API层的操盘对象之后,紧接着需要初始化本地的媒体,也即创建本地的音频轨、视频轨、数据通道,并将这些本地的媒体轨道添加到PeerConnection对象中。如图中红色标注所示。

本文将详细描述上述视频轨道的创建细节 以及 轨道被添加到PeerConnection中的存储情况。

在这里插入图片描述

2. 视频轨道的创建和添加

  rtc::scoped_refptr<CapturerTrackSource> video_device =
      CapturerTrackSource::Create();
  if (video_device) {
    rtc::scoped_refptr<webrtc::VideoTrackInterface> video_track_(
        peer_connection_factory_->CreateVideoTrack(kVideoLabel, video_device));
    main_wnd_->StartLocalRenderer(video_track_);

    result_or_error = peer_connection_->AddTrack(video_track_, {kStreamId});
    if (!result_or_error.ok()) {
      RTC_LOG(LS_ERROR) << "Failed to add video track to PeerConnection: "
                        << result_or_error.error().message();
    }
  } else {
    RTC_LOG(LS_ERROR) << "OpenVideoCaptureDevice failed";
  }
  1. 创建视频轨道源——CapturerTrackSource是示例工程中实现了VideoTrackSourceInterface接口的应用层类,非WebRTC内部提供的对象。该类不是直接实现VideoSourceInterface接口,而是VideoTrackSourceInterface接口。
  2. 创建视频轨——PeerConnectionFactory::CreateVideoTrack。
  3. 添加视频轨到PeerConnection——PeerConnection::AddTrack。

2.1 视频源的创建

rtc::scoped_refptr<CapturerTrackSource> video_device =
      CapturerTrackSource::Create();

static rtc::scoped_refptr<CapturerTrackSource> Create() {
  // 1. 预设视频参数
  const size_t kWidth = 640;
  const size_t kHeight = 480;
  const size_t kFps = 30;
  std::unique_ptr<webrtc::test::VcmCapturer> capturer;
  // 2. 获取视频设备信息对象info,并获取机器上视频设备个数
  std::unique_ptr<webrtc::VideoCaptureModule::DeviceInfo> info(
      webrtc::VideoCaptureFactory::CreateDeviceInfo());
  if (!info) {
    return nullptr;
  }
  int num_devices = info->NumberOfDevices();
  // 3. 枚举音频设备,查找支持预设视频参数的设备,创建对应的视频源,然后创建对应的轨道源
  for (int i = 0; i < num_devices; ++i) {
    // 3.1 创建满足预设视频参数的视频源对象VcmCapturer
    capturer = absl::WrapUnique(
        webrtc::test::VcmCapturer::Create(kWidth, kHeight, kFps, i));
    // 3.2 创建视频轨源对象CapturerTrackSource
    if (capturer) {
      return new rtc::RefCountedObject<CapturerTrackSource>(
          std::move(capturer));
    }
  }

  return nullptr;
}

从上创建视频源的代码可以知道如下几点:

  • 通过设备信息类VideoCaptureModule::DeviceInfo获取音视频设备个数
  • 需要根据预设的视频参数宽高,帧率去创建视频源对象VcmCapturer,一旦音频源对象VcmCapturer创建成功,则认为找到了底层适用于该预设视频参数的音频采集VideoCapture设备。
  • 以创建的视频源对象VcmCapturer为入参,构建视频轨源对象CapturerTrackSource。VcmCapturer成为CapturerTrackSource的私有成员。

CapturerTrackSource、VcmCapturer、VideoCapture对象的继承树,以及这三者的关系如下UML类图所示:
WebRTC源码分析-呼叫建立过程之四(中)(创建并添加本地视频轨到PeerConnection)_第1张图片

2.1.1 创建视频采集

视频采集模块是数据流水线的起始点,负责从视频源采集原始视频帧,推送给流水线的下一站:可以是本地渲染模块进行本地回显,也可以是编码模块进行数据编码压缩。

视频源可以是摄像头,也可以是桌面、窗口抓屏(远程桌面,基于视频流的电子白板等应用),甚至可以是磁盘上的视频文件,图片文件。WebRTC中提供了基于摄像头的视频采集框架,是本文要讨论的重点。当然WebRTC也提供了桌面,窗口抓屏框架,这套框架对外所提供的接口与基于摄像头的采集接口有所不同。整个视频流水线建立是以摄像头采集接口为基础的,从而导致这么个问题:当需要将抓屏数据当做视频源往外推送时,需要使用适配器模式来实现一套基于摄像头的视频采集接口。

视频采集模块是平台相关的模块,MacOS/IOS一般使用AVFoundation框架或者QuikTime框架,Linux平台一般使用V4L2库,Android上一般使用Camera1或者Camera2框架,Windows平台则使用DS(DirectShow)或者是MF(MediaFoundation)。由于WebRTC是个非常活跃的工程,代码架构一直在不停的变动之中,比如2019年4月份的代码还有VideoCaptureMF的代码,并且还注释着Vista及以上的版本建议使用MediaFoundation采集框架,而2019年11月份的代码MediaFoundation相关的代码已经被移除。再比如MacOs/IOS,Android的相关代码已经被移动到sdk/objc和sdk/android目录下。本文以modules/video_capture下的代码来做阐述,平台无关的代码在该直接目录下,平台相关的实现在modules/video_capture/windows,modules/video_capture/linux目录下,如图所示:
WebRTC源码分析-呼叫建立过程之四(中)(创建并添加本地视频轨到PeerConnection)_第2张图片

2.1.1.1 视频采集UML

WebRTC源码分析-呼叫建立过程之四(中)(创建并添加本地视频轨到PeerConnection)_第3张图片
DeviceInfo接口提供了设备枚举相关功能,其平台相关子类实例以组合的形式提供给VideoCapture。

  • 枚举设备个数,获取某个设备名称。
  • 枚举某个设备所支持的所有能力(VideoCaptureCapability: 分辨率,最大帧率,颜色空间,是否逐行扫描)
  • 获取某个设备的所有能力中与外部设置的能力最匹配的那个能力。

VideoCaptureModule视频采集模块的虚基类,它定义一系列视频采集的通用接口函数:

  • Start/StopCapture用来开始/结束视频采集(平台相关);
  • CaptureStarted用来判断当前capture运行状态(平台相关);
  • Register/DeCaptureDataCallback用来注册/注销数据回调模块(平台无关);
  • Set/GetApplyRotation用来设置视频旋转角度(平台无关)。

VideoCaptureImpl类是VideoCaptureModule的实现子类。做了3个事:

  • 声明静态Ctreate方法,用于创建平台相关的VideoCaptureImpl子类,在Windows平台上为VideoCaptureDS,在Linux平台上实现的子类是VideoCaptureV4L2。该方法一处声明,多处实现,在相应平台编译时,只会加载对应平台的实现代码;
  • 平台相关的接口,留待平台相关的子类中实现,主要是开始/结束视频采集;
  • 实现平台无关的接口:注册视频数据回调,应用视频旋转相关函数。其中注册数据回调将一个实现了VideoSinkInterface接口的对象赋予VideoCaptureImpl::_dataCallBack成员。当采集模块得到一帧视频数据,就可以通过该对象的OnFrame()方法推送出来。

2.1.1.2 采集模块的内部数据流

WebRTC源码分析-呼叫建立过程之四(中)(创建并添加本地视频轨到PeerConnection)_第4张图片

  1. 以VideoCaptureDS为例,平台相关的采集模块采集到一帧视频后,平台相关的函数ProcessCapturedFrame()方法进行处理。ProcessCapturedFrame()将视频帧直接传递给VideoCaptureImpl::IncomingFrame()方法
  2. VideoCaptureImpl::IncomingFrame()方法将对视频帧按需求进行旋转,并利用libyuv库转换成I420类型,再给视频帧加上NTP时间戳。经过上述处理后,IncomingFrame()将视频帧进一步传递给VideoCaptureImpl::DeliverCapturedFrame()
  3. VideoCaptureImpl::DeliverCapturedFrame()将调用VideoSinkInterface::OnFrame(),将视频帧传递给回调对象_dataCallBack,即数据的下一站,从而将视频帧推送出采集模块。

2.1.2 创建视频源

VcmCapturer是创建的视频源对象,虽然从名字上来看像是视频采集类,但实质上它实现了VideoSourceInterface接口。我们认为其是一个视频源。
WebRTC源码分析-呼叫建立过程之四(中)(创建并添加本地视频轨到PeerConnection)_第5张图片
VideoCaptureModule作为数据源头组合到视频源对象VcmCapturer中,同时VcmCapturer又实现了VideoSinkInterface接口,可以把自己注册到VideoCaptureModule中。从而实现了视频帧从VideoCaptureModule->VideoSource的流动。

VcmCapturer在构造成功时就会启动VideoCaptureModule进行视频采集。

视频源VcmCapturer持有一个非常重要的成员VideoBroadcaster对象,该对象的UML类图如下。
WebRTC源码分析-呼叫建立过程之四(中)(创建并添加本地视频轨到PeerConnection)_第6张图片
一方面VideoBroadcaster实现了VideoSinkInterface接口,成为一个Sink,这样VideoSource得到采集模块的视频帧后,首先会流入到内部的VideoBroadcaster成员对象,而非直接从VideoSource流出;另一方面VideoSource和VideoBroadcaster都实现了VideoSourceInterface接口,对外VideoSource作为视频源存在,向数据流下一站提供注册方法AddOrUpdateSink();该方法内部调用VideoBroadcaster的AddOrUpdateSink(),从而将数据流下一站VideoSink注册到VideoBroadcaster,存入成员std::vector sinks_中。到此,应该不难想到VideoBroadcaster既有了数据流入,还知道数据的下一站(可能多个),那么VideoBroadcaster::OnFrame()中就可以通过循环调用下一站的OnFrame方法将视频帧广播出去。

为什么要如此设计?因为,在WebRTC 1.0的官方规范中说明了一个视频源是可以被多个视频轨共用的。通过上述方式可以实现共用的概念。

2.1.3 创建视频轨源

WebRTC源码分析-呼叫建立过程之四(中)(创建并添加本地视频轨到PeerConnection)_第7张图片
VideoTrackSource没有实现VideoSinkInterface接口,因此,实质上视频数据是不会流入到VideoTrackSource中的,但其组合了VideoSource对象,并且实现了VideoSourceInterface接口,添加到VideoTrackSource中的VideoSink会被添加到VideoSource,然后进一步添加到VideoBroadcast中。对外部来说,VideoTrackSource就是视频源。

VideoTrackSource另外实现了视频源状态相关的接口,以及状态通告相关的接口NotifierInterface,用于向更高一层(VideoTrack)通告视频源的状态。

2.2 视频轨的创建

2.2.1 PeerConnection::CreateVideoTrack

rtc::scoped_refptr<webrtc::VideoTrackInterface> video_track_(
    peer_connection_factory_->CreateVideoTrack(kVideoLabel, video_device));

rtc::scoped_refptr<VideoTrackInterface> PeerConnectionFactory::CreateVideoTrack(
    const std::string& id,
    VideoTrackSourceInterface* source) {
  RTC_DCHECK(signaling_thread_->IsCurrent());
  rtc::scoped_refptr<VideoTrackInterface> track(
      VideoTrack::Create(id, source, worker_thread_));
  return VideoTrackProxy::Create(signaling_thread_, worker_thread_, track);
}

rtc::scoped_refptr<VideoTrack> VideoTrack::Create(
    const std::string& id,
    VideoTrackSourceInterface* source,
    rtc::Thread* worker_thread) {
  rtc::RefCountedObject<VideoTrack>* track =
      new rtc::RefCountedObject<VideoTrack>(id, source, worker_thread);
  return track;
}

VideoTrack::VideoTrack(const std::string& label,
                       VideoTrackSourceInterface* video_source,
                       rtc::Thread* worker_thread)
    : MediaStreamTrack<VideoTrackInterface>(label),
      worker_thread_(worker_thread),
      video_source_(video_source),
      content_hint_(ContentHint::kNone) {
  video_source_->RegisterObserver(this);
}

由上视频轨创建过程可知,

  • 我们最终创建的实体视频轨是VideoTrack这个类的对象,但是向应用层返回的是相对应的代理对象VideoTrackProxy,这是WebRTC防止线程乱入的常规操作,原因不再多赘述。
  • 另外由代理对象的构建方式可知,VideoTrack的部分方法需要在信令线程执行,部分方法需要在工作者线程执行。
  • VideoTrack成为源的观察者,可以获知源的状态改变。

2.2.2 视频轨的继承树

如下是VideoTrack类的UML类图。
WebRTC源码分析-呼叫建立过程之四(中)(创建并添加本地视频轨到PeerConnection)_第8张图片
从上VideoTrack类的UML类图我们可以得知如下几个结论:

  • VideoTrack一方面要实现ObserverInterface接口,让自己可以成为视频源的观察者,让自己的状态可以随着源的状态进行同步改变;另一方面又要实现NotifierInterface接口,让自己成为一个通知者,当自己的状态改变时,能够通知那些观察自己状态变化的对象。
  • 如同VideoTrackSource一般,VideoTrack也没有实现VideoSinkInterface接口,因此,视频数据也不会流入到VideoTrack中,但其组合了VideoTrackSource,并且间接实现了VideoSourceInterface接口。想要从VideoTrack中获取视频流的站点,只要实现VideoSinkInterface接口,通过VideoTrack的AddOrUpdateSink()注册进来即可,因为该VideoSink会经过VideoTrackSource->VideoSource->VideoBroadcaster,最终可以从VideoBroadcaster获得视频流。
  • VideoTrack还实现了VideoTrackInterface接口,其中提供了一个重要的属性:ContentHint。这个属性告知编码器在码率降低时,应该如何应对:降低帧率?降低分辨率?对于桌面采集应用来说,我们应该设置该属性为kDetailed或者是kText,这样编码器编码该视频流的时候不会降低分辨率,量化参数qp值也不会设置的过大。
  • 我认为VideoTrack没有直接继承实现VideoSourceInterface接口,或者像AudioTrack那样直接提供AddOrUpdateSink/RemoveSink方法,而是继承了VideoSourceBase类,这样,往VideoTrack中的添加的VideoSink在VideoBroadcaster保存一份,也会在VideoTrack中保存一份。在VideoTrack保存这一份实质上没有什么作用,并且这样的继承关系跟AudioTrack的继承关系不对称,毫无美感而言。

2.3 视频数据的流动

纵观上述几个对象,我们可以得出如下的类图:
WebRTC源码分析-呼叫建立过程之四(中)(创建并添加本地视频轨到PeerConnection)_第9张图片
我们可以认为视频数据原始帧从VideoCapture流向了VideoSource,然后又流向了VideoTrackSource,然后流向VideoTrack。向VideoTrack注册的Sink又可以进一步获取到视频原始帧。

虽然比较细致地拆解了视频源为VideoTrackSource、VideoSource、VideoCapture,但从宏观概念上,我们要知晓:可以直接认为VideoTrackSource是视频源。数据从视频源流向视频轨。

视频轨既要从视频源获取视频数据,还要观察视频源地状态,从而同步更改自己地状态。

2.4 添加视频轨到PeerConnection

由于VideoTrack同AudioTrack一样,都实现了MediaStreamTrackInterface接口,对于PC而言,视频轨和音频轨并无实质区别,因为都是通过如下方法进行添加的。因此,在此处就不再赘述VideoTrack如何被添加?存储在PC的何处?是否将引发PC状态的改变? 等等,见上篇文章:WebRTC源码分析-呼叫建立过程之四(上)(创建并添加本地音频轨到PeerConnection)

  RTCErrorOr<rtc::scoped_refptr<RtpSenderInterface>> AddTrack(
      rtc::scoped_refptr<MediaStreamTrackInterface> track,
      const std::vector<std::string>& stream_ids) override;

3 总结

行文知此,大致对如何创建一个视频轨并添加到PC进行了一个较细致的分析。即便忘记了细节,那么如下要点是需要记住的

  • 视频轨的实体对象是VideoTrack类,对于应用层返回的是其代理对象VideoTrackProxy,这是WebRTC为了防止线程乱入所作的常规操作。
  • WebRTC中,媒体数据总是从Source流向Sink。
  • 某个对象要想从VideoTrack获取到视频帧,那么它需要实现VideoSinkInterface,并注册到VideoTrack中。实质上向VideoTrack注册的Sink会被传递到VideoTrack所持有的VideoSource的VideoBroadcast成员对象中,从那儿可以获得从VideoCapture传递来的视频帧。
  • 记住VideoTrack提供了ContentHint这样一个属性以及设置这个属性的接口。该属性描绘了视频帧的内容,这个会影响到编码器的降级策略(当网络带宽资源 && CPU资源受限时,无法按照既定的编码参数进行编码输出,那么就需要降级编码)是保持帧率,还是保持分辨率。当我们做远程桌面、电子白板这种应用时,我们认为内容清晰度比流畅性更重要,此时就需要保持分辨率,降低一些帧率。那么将VideoTrack的ContentHint属性设置为kDetailed or kText,那么就暗示了编码器保持分辨率;当我们做音视频会话时,我们需要保证音视频的流畅性、连贯性,因此,一般会采取保帧率,降分辨率的策略,那么可以将VideoTrack的ContentHint属性设置为kFluid,那么暗示编码器保持帧率。

你可能感兴趣的:(WebRTC源码分析)