1. 引言

当WebRTC的端与信令服务器建立连接之后,可以通过与服务器的信令交互获知对等端点的存在,进一步通过信令向对端发起呼叫。在发起呼叫之前,发起方需要在本地做一些初始化工作,创建两个重要的对象:PeerConnectionFactory和PeerConnection,这两个C++对象提供了建立WebRTC会话的API(注意:在JS层,没有PeerConnectionFactory,只有PeerConnection,是基于C++ API层的全局方法以及PeerConnectionFactory和PeerConnection对象的进一步封装)。

本文将重点介绍PeerConnectionFactory对象详细的创建过程及其提供的能力,典型的WebRTC p2p会话建立过程如下图所示,其中红色标注了PeerConnectionFactory创建的位置。
在这里插入图片描述

2 PeerConnectionFactory对象的创建

在example/peerconnection_client工程中,发起方调用如下代码来创建PeerConnectionFactory对象。

  peer_connection_factory_ = webrtc::CreatePeerConnectionFactory(
      nullptr /* network_thread */, nullptr /* worker_thread */,
      nullptr /* signaling_thread */, nullptr /* default_adm */,
      webrtc::CreateBuiltinAudioEncoderFactory(),
      webrtc::CreateBuiltinAudioDecoderFactory(),
      webrtc::CreateBuiltinVideoEncoderFactory(),
      webrtc::CreateBuiltinVideoDecoderFactory(), nullptr /* audio_mixer */,
      nullptr /* audio_processing */);

2.1 CreatePeerConnectionFactory方法解析

关于该方法,有以下几点需要注意:

首先, 该方法位于源码的API层,是一个全局方法。头文件为api/create_peerconnection_factory.h;源文件为api/create_peerconnection_factory.cc。对于应用层而言,对WebRTC可调用的方法,要么是API层的全局方法,要么是PeerConnectionFactory和PeerConnection这两个对象的公有方法,比如JS层API就是基于WebRTC的C++ API层来进行封装的。当然,也不能排除第三方的RTC SDK使用别的方式封装,而没有基于API层的PeerConnection

其次, 该方法有10个入参,其中3个是WebRTC最重要的线程(WebRTC是个多线程架构,而非多进程架构),7个是音视频多媒体引擎相关对象。

  • 3个重要线程:
    – Thread* signaling_thread:信令线程,示例工程中为UI主线程(当然也可以不是主线程);
    – Thread* worker_thread:工作者线程,负责耗时操作;
    – Thread* network_thread:网络线程,处理网络相关的操作;
  • 7个音视频相关对象(5个音频相关,2个视频相关),用于创建多媒体引擎:
    – scoped_refptr<AudioDeviceModule> default_adm:音频设备模块adm;
    – scoped_refptr<AudioEncoderFactory> audio_encoder_factory:音频编码器工厂;
    – scoped_refptr<AudioDecoderFactory> audio_decoder_factory:音频解码器工厂;
    – scoped_refptr<AudioMixer> audio_mixer:混音器;
    – scoped_refptr<AudioProcessing> audio_processing:音频处理器;
    – unique_ptr<VideoEncoderFactory> video_encoder_factory:视频编码器工厂;
    – unique_ptr<VideoDecoderFactory> video_decoder_factory:视频解码器工厂;

最后, 该方法的返回是PeerConnectionFactoryInterface对象,我们会发现WebRTC的API层暴露的都是某某Interface,指向一个具体实现类,具体实现类是WebRTC内部其他层的internal class,不对外暴露,这符合C++封装的一般思想,对外只暴露必要的接口,隐藏内部实现。我们可以大胆猜测,该方法返回的是PeerConnectionFactory这个类实例,该实体类位于pc/peer_connection_factory.h和pc/peer_connection_factory.cc中。果然如此嘛?先卖个关子,继续往后看

源码如下:

rtc::scoped_refptr<PeerConnectionFactoryInterface> CreatePeerConnectionFactory(
    rtc::Thread* network_thread,
    rtc::Thread* worker_thread,
    rtc::Thread* signaling_thread,
    rtc::scoped_refptr<AudioDeviceModule> default_adm,
    rtc::scoped_refptr<AudioEncoderFactory> audio_encoder_factory,
    rtc::scoped_refptr<AudioDecoderFactory> audio_decoder_factory,
    std::unique_ptr<VideoEncoderFactory> video_encoder_factory,
    std::unique_ptr<VideoDecoderFactory> video_decoder_factory,
    rtc::scoped_refptr<AudioMixer> audio_mixer,
    rtc::scoped_refptr<AudioProcessing> audio_processing) {
    
  PeerConnectionFactoryDependencies dependencies;
  dependencies.network_thread = network_thread;
  dependencies.worker_thread = worker_thread;
  dependencies.signaling_thread = signaling_thread;
  dependencies.task_queue_factory = CreateDefaultTaskQueueFactory();
  dependencies.call_factory = CreateCallFactory();
  dependencies.event_log_factory = std::make_unique<RtcEventLogFactory>(
      dependencies.task_queue_factory.get());

  cricket::MediaEngineDependencies media_dependencies;
  media_dependencies.task_queue_factory = dependencies.task_queue_factory.get();
  media_dependencies.adm = std::move(default_adm);
  media_dependencies.audio_encoder_factory = std::move(audio_encoder_factory);
  media_dependencies.audio_decoder_factory = std::move(audio_decoder_factory);
  if (audio_processing) {
    media_dependencies.audio_processing = std::move(audio_processing);
  } else {
    media_dependencies.audio_processing = AudioProcessingBuilder().Create();
  }
  media_dependencies.audio_mixer = std::move(audio_mixer);
  media_dependencies.video_encoder_factory = std::move(video_encoder_factory);
  media_dependencies.video_decoder_factory = std::move(video_decoder_factory);
  dependencies.media_engine =
      cricket::CreateMediaEngine(std::move(media_dependencies));

  return CreateModularPeerConnectionFactory(std::move(dependencies));
}

该方法中大致创建了两个Struct对象,调用了两个Create方法来最终创建PeerConnectionFactoryInterface实体对象。

为了调用CreateModularPeerConnectionFactory方法来实际创建PeerConnectionFactoryInterface对象,需要先构造其入参——结构体PeerConnectionFactoryDependencies对象。我们发现最初传入的三个线程对象传递给了PeerConnectionFactoryDependencies;另外,还为其提供了三个工厂类对象TaskQueueFactory、CallFactory、RtcEventLogFactory;最后,还需要MediaEngine对象。实际上,如果查看PeerConnectionFactoryDependencies结构体,发现其他依赖成员不止如此,不过,要么是为空亦可,要么在后续的过程中会继续创建一个内部默认的,而绝大多数是后者。

为了给PeerConnectionFactoryDependencies对象提供多媒体引擎对象MediaEngine,调用了CreateMediaEngine方法,当然需要先构造其入参——结构体MediaEngineDependencies。我们发现最初传入的7个音视频相关对象都是构建MediaEngine的依赖项,这为我们提供了一个思路或者途径:如果我们不想使用WebRTC内置的音视频编解码器,我们可以自行提供。比如,WebRTC内置音频处理不支持AAC,那么可以自己引入AAC的编解码器,构造自己的工厂类,通过该方法传入。另外,再传递这7个音视频相关对象时,使用了C++11的std::move语义,进行了所属权的转移(应用层–>WebRTC内部),使得应用层无法再次通过这几个对象的指针来访问他们,只能通过API层既定的接口来间接访问,从而提高安全性。另外,别忘了,MediaEngineDependencies还使用了与PeerConnectionFactoryDependencies相同的任务队列工厂TaskQueueFactory。

接下来,我们具体看看媒体引擎MediaEngine的方法CreateMediaEngine 和 具体创建PeerConnectionFactoryInterface的方法CreateModularPeerConnectionFactory是如何工作的。

2.1.1 创建媒体引擎MediaEngine——CreateMediaEngine方法

CreateMediaEngine方法比较简单,但是也有这么几点需要注意:

  • 首先,CreateMediaEngine是位于media层的导出方法,方法前使用了RTC_EXPORT进行了修饰。正如方法前的注释所言,CreateMediaEngine可以在任意线程中被调用,MediaEngine本身可以在任意线程中创建,但是需要注意的是MediaEngine相关的一些方法只能在"worker thread"中被调用,包括MediaEngine本身的init方法,也包括VoiceEngine和VideoEngine相关的方法。

  • 其次,CreateMediaEngine中分别创建了音频引擎对象WebRtcVoiceEngine和视频引擎对象WebRtcVideoEngine,创建过程中进一步的将外部传入的7个视频对象通过std::move语义转移到各自归属的引擎对象

  • 最后,CreateMediaEngine最终构建的是CompositeMediaEngine对象实例,它通过sdt::move将上面创建的音频引擎和视频引擎转移到其内部持有,也即告知这么一个实事:想要访问音频、视频相关的编解码功能,音频处理,音频混音等功能来找我MediaEngine吧,只有我能提供,无他处可寻。

  • 另外,MediaEngine只处理音频和视频,对于Data它是不管的,WebRTC数据通道是由专门的RtpDataEngine来处理的。

// CreateMediaEngine may be called on any thread, though the engine is
// only expected to be used on one thread, internally called the "worker
// thread". This is the thread Init must be called on.
std::unique_ptr<MediaEngineInterface> CreateMediaEngine(
    MediaEngineDependencies dependencies) {
  auto audio_engine = std::make_unique<WebRtcVoiceEngine>(
      dependencies.task_queue_factory, std::move(dependencies.adm),
      std::move(dependencies.audio_encoder_factory),
      std::move(dependencies.audio_decoder_factory),
      std::move(dependencies.audio_mixer),
      std::move(dependencies.audio_processing));
#ifdef HAVE_WEBRTC_VIDEO
  auto video_engine = std::make_unique<WebRtcVideoEngine>(
      std::move(dependencies.video_encoder_factory),
      std::move(dependencies.video_decoder_factory));
#else
  auto video_engine = std::make_unique<NullWebRtcVideoEngine>();
#endif
  return std::make_unique<CompositeMediaEngine>(std::move(audio_engine),
                                                std::move(video_engine));
}

2.1.2 创建实体类PeerConnectionFactory——CreateModularPeerConnectionFactory方法

改方法大致做了三件事:

  1. 创建了一个PeerConnectionFactory实体对象;
  2. 在信令线程上同步调用PeerConnectionFactory的Initialize方法,使其在信令线程上进行初始化;
  3. 创建PeerConnectionFactoryProxy对象,并返回该对象。

看到这儿,令人疑惑的点就出来了,原来最终返给应用层使用的实体对象居然不是PeerConnectionFactory,而是PeerConnectionFactoryProxy,为什么?跟着源码继续分析。

rtc::scoped_refptr<PeerConnectionFactoryInterface>
CreateModularPeerConnectionFactory(
    PeerConnectionFactoryDependencies dependencies) {
  // 创建PeerConnectionFactory对象
  rtc::scoped_refptr<PeerConnectionFactory> pc_factory(
      new rtc::RefCountedObject<PeerConnectionFactory>(
          std::move(dependencies)));
          
  // 在信令线程上同步调用PeerConnectionFactory的Initialize方法
  // Call Initialize synchronously but make sure it is executed on
  // |signaling_thread|.
  MethodCall0<PeerConnectionFactory, bool> call(
      pc_factory.get(), &PeerConnectionFactory::Initialize);
  bool result = call.Marshal(RTC_FROM_HERE, pc_factory->signaling_thread());
  if (!result) {
    return nullptr;
  }
  
  // 创建PeerConnectionFactoryProxy代理对象,并返回该对象
  return PeerConnectionFactoryProxy::Create(pc_factory->signaling_thread(),
                                            pc_factory);
}
1)PeerConnectionFactory的创建——构造

首先,我们看到在创建PeerConnectionFactory对象时,使用了引用计数对象rtc::RefCountedObject,并且将创建的带引用计数的PeerConnectionFactory传递给了WebRTC中的智能指针对象rtc::scoped_refptr pc_factory。WebRTC中的rtc::scoped_refptr类似于C++11中的共享指针std::shared_ptr的作用。关于WebRTC中的引用计数以及共享指针分别在另外两篇文章中分析:

其次,PeerConnectionFactory构造方法源码如下(省略了成员赋值)。一方面,在成员赋值时,仍然按照std:move的移动语义,将PeerConnectionFactoryDependencies成员所有权继续移动到PeerConnectionFactory中,由于代码过长,所以省略。另一方面,构造函数中主要处理WebRTC中的3个重要线程对象:network_thread_ ,worker_thread_ ,signaling_thread_。有以下几点需要注意:

  • network_thread_,worker_thread_的创建:如果应用层未提供这两个线程的实例,那么此处将创建二者的实例。 使用Thread::CreateWithSocketServer()方法来创建network_thread_ ,意味着network_thread_可以处理网络IO,可以处理线程间消息投递;使用Thread::Create()方法创建worker_thread_,意味着worker_thread_无法处理网络IO,但可以处理线程间消息投递;
  • network_thread_,worker_thread_的赋值:创建的实例赋值给C++11的智能指针std::unique_ptr<rtc::Thread>,表示这两个线程的所有权为我所有,两个线程的生命周期归我管理,也即销毁的事让我智能指针来干吧。
  • network_thread_,worker_thread_的启动:调用线程rtc::Thread的Start()方法,将启动两个线程的消息循环。关于WebRTC的线程及其消息循环工作原理,可以参见 WebRTC源码分析-线程基础之消息循环,消息投递 以及 WebRTC源码分析-线程基础之线程基本功能
  • signaling_thread_有所不同,此处并不会创建新的线程对象给signaling_thread_,而是指向当前线程所关联的Thread对象,如无Thread对象与当前线程关联,那么将Wrap当前线程。而当前线程就是调用CreateModularPeerConnectionFactory()方法的线程,在示例工程中为UI主线程,注意该线程与前两者的不同,该线程没有运行自己Thread对象带来的消息循环,但对于信令线程来说,消息循环是必不可少的。正如示例工程examples/peerconnection,UI主线程总是运行着消息循环,这个消息循环代理了signaling_thread_对象消息投递和处理。
PeerConnectionFactory::PeerConnectionFactory(PeerConnectionFactoryDependencies dependencies) {
  if (!network_thread_) {
    owned_network_thread_ = rtc::Thread::CreateWithSocketServer();
    owned_network_thread_->SetName("pc_network_thread", nullptr);
    owned_network_thread_->Start();
    network_thread_ = owned_network_thread_.get();
  }

  if (!worker_thread_) {
    owned_worker_thread_ = rtc::Thread::Create();
    owned_worker_thread_->SetName("pc_worker_thread", nullptr);
    owned_worker_thread_->Start();
    worker_thread_ = owned_worker_thread_.get();
  }

  if (!signaling_thread_) {
    signaling_thread_ = rtc::Thread::Current();
    if (!signaling_thread_) {
      // If this thread isn't already wrapped by an rtc::Thread, create a
      // wrapper and own it in this class.
      signaling_thread_ = rtc::ThreadManager::Instance()->WrapCurrentThread();
      wraps_current_thread_ = true;
    }
  }
}
2)PeerConnectionFactory初始化——Initialize方法

创建PeerConnectionFactory之后紧接着就是调用PeerConnectionFactory的初始化函数Initialize。不过这儿看起来稍微有点复杂,原因在于PeerConnectionFactory的Initialize必须在signaling_thread_线程上执行,并且此处还需要阻塞的获取执行结果,以便根据结果确定是否要执行后续的代码。

为了实现上述意图:即在signaling_thread_线程上同步调用PeerConnectionFactory的Initialize()方法,并获取方法的执行结果。此处利用了MethodCall0类的能力来实现该功能,具体是如何实现的,可以参考该文:WebRTCWebRTC源码分析-跨线程同步MethodCall

回归正题,如下是Initialize()方法的源码,此处不做深究,只要知道在PeerConnectionFactory的初始化时,根据当前时间提供了初始的随机数,创建了网络管理对象BasicNetworkManager,创建了默认的Socket工厂类对象BasicPacketSocketFactory,创建了通道管理类ChannelManager并初始化。至于这几个对象是提供什么功能,如何工作的, 待后续分析到相关功能时,我们再来分析。

bool PeerConnectionFactory::Initialize() {
  // 断言,本函数只能在signaling_thread_线程上执行
  RTC_DCHECK(signaling_thread_->IsCurrent());
  // 初始化随机数
  rtc::InitRandom(rtc::Time32());
  // 创建网络管理类BasicNetworkManager
  default_network_manager_.reset(new rtc::BasicNetworkManager());
  if (!default_network_manager_) {
    return false;
  }
  // 创建BasicPacketSocketFactory
  default_socket_factory_.reset(
      new rtc::BasicPacketSocketFactory(network_thread_));
  if (!default_socket_factory_) {
    return false;
  }
  // 创建通道管理类ChannelManager,并初始化
  channel_manager_ = absl::make_unique<cricket::ChannelManager>(
      std::move(media_engine_), absl::make_unique<cricket::RtpDataEngine>(),
      worker_thread_, network_thread_);
  channel_manager_->SetVideoRtxEnabled(true);
  if (!channel_manager_->Init()) {
    return false;
  }
  return true;
}
3)PeerConnectionFactoryProxy代理的创建与返回——防止线程乱入

也许,PeerConnectionFactoryProxy代理创建及其作用才是本文最需要重点阐述的地方。随着代码分析我们发现最初的猜想,即:CreatePeerConnectionFactory()方法返回给应用层的实例对象并非是PeerConnectionFactory对象。而是调用PeerConnectionFactoryProxy::Create(pc_factory->signaling_thread(), pc_factory)创建了一个PeerConnectionFactoryProxy对象,并返回了该对象。

为什么如此设计?如果不这么做会有什么问题?

线程乱入问题:

由于WebRTC中存在着信令线程、工作者线程、网络线程,为了让他们各司其职(从性能和安全性上考虑),WebRTC内部实现各个模块时,会明确某些模块的某些部分必须在某个线程中执行。比如, PeerConnectionFactory的方法必须在signaling_thread_线程上执行。为了确保这点,PeerConnectionFactory对象的几乎所有公有方法内部第一句就是类似于RTC_DCHECK(signaling_thread_->IsCurrent()); 这样的一个断言。 同样,有这样的需求的对象有很多,不止PeerConnectionFactory一个。并且,对象的某些方法要求在signaling线程上执行,其他方法可能是要求在worker线程上执行,这种现象也不少见。为了确保这点,方法内部也是类似的断言。

断言确保了代码在正确的线程上执行这点,否则触发断言,程序中断,这说明程序写得有BUG,必须调试消除BUG。断言本身的做法是没问题的,但是,如果我们直接如此使用的话会带来很多的不便。

考虑当前的情形:如果WebRTC对外返回了PeerConnectionFactory对象,势必使用方得很清楚PeerConnectionFactory的哪些方法得在哪个线程上执行。一方面,要么使用方记得所有方法该如何正确调用,要么有非常详细的接口文档,小心翼翼的按照文档来做;另一方面,即便你记忆高超,记得所有方法的使用方式,但也会遇到使用上的不便,因为在某个别的线程上需要调用PeerConnectionFactory的某个方法时,方法调用处不一定能够得着对应线程(持有对应线程的引用)。

线程乱入解决:

WebRTC中使用代理完美的解决了上述线程安全问题(有的地方被成为线程乱入问题)。

正如CreatePeerConnectionFactory()方法所做这样,对应用层返回的不是PeerConnectionFactory对象,而是对应的代理对象PeerConnectionFactoryProxy。对于应用层,我们想调用PeerConnectionFactory的某个方法时,实际上都是调用代理类PeerConnectionFactoryProxy的对应方法(那是当然,因为应用层是拿不到PeerConnectionFactory对象的,拿到的是PeerConnectionFactoryProxy对象,真正的PeerConnectionFactory对象被封装在此代理对象之中,使用者只是不自知而已),然后这个方法内部代理地调用PeerConnectionFactory对应方法,并且是在指定的signaling_thread中执行。通过这层代理,可以让用户侧可以非常容易地、安全地使用接口,达到预期目标。

那么,神奇的PeerConnectionFactoryProxy是如何做到这点的?详细分析见WebRTCWebRTC源码分析-线程安全之Proxy,防止线程乱入

3 PeerConnectionFactory对象简析

PeerConnectionFactory对象创建过程已经分析完毕,虽然出现了Proxy层,最终的实际功能仍然由PeerConnectionFactory对象提供。那么,PeerConnectionFactory对象到底提供了哪些能力呢?其实,从该类的名称上就很容易了解到:作为PeerConnection的工厂类存在,必然可以用来创建PeerConnection对象。也不止如此,它还能做其他事。先看看该对象持有哪些成员吧,毕竟,它提供的能力需要得到其成员的支持。

3.1 PeerConnectionFactory拥有的资源——私有成员

我们用一张图来展示PeerConnectionFactory所持有的成员:
在这里插入图片描述

3.1 PeerConnectionFactory提供的能力——公有方法

PeerConnectionFactory提供的能力并不太多,重要的有以下几点:

  • 创建PeerConnection:CreatePeerConnection();
  • 创建音频源:CreateAudioSource(),为创建音频Track提供参数;
  • 创建视频轨:CreateVideoTrack();
  • 创建音频轨:CreateAudioTrack();
  • 获取ChannelManager:channel_manager();

其他的不太常用:

  • 获取发送的媒体类型相关的RTP能力RtpCapabilities:GetRtpSenderCapabilities(); 这些能力包含对应媒体所支持的编解码器类型,RTP头扩展以及支持的前向纠错算法FEC。
  • 获取接收的媒体类型星官的RTP能力RtpCapabilities:GetRtpReceiverCapabilities();
  • 创建媒体流:CreateLocalMediaStream(),目前C++层很少使用MediaStream,一般都是直接使用AudioTrack和VideoTrack。
  • dump前向纠错包到文件:StartAecDump() && StopAecDump();

4 总结

至此,PeerConnectionFactory对象的创建过程已经阐述完毕,PeerConnectionFactory对象提供的能力也做了基本的介绍。简单回顾下整个源码分析过程,有这么几点提炼出来以作最后的总结:

  • 创建PeerConnectionFactory时,外部提供对象的所有权会被转移到WebRTC内部对象中,C++11的std::move转移语义在这个过程中起着关键作用
  • 除了PeerConnectionFactory,我们在源码分析过程中还重点关注了MediaEngine对象的创建,该对象由WebRtcVoiceEngine和WebRtcVideoEngine构成,传入的那7个音视频相关参数分别用来构建音频引擎和视频引擎。音视频编码、音频处理、音频混音等相关的功能的入口可以从该类为顶层线索来查找。
  • WebRTC是个多线程架构的程序,有3个基础的线程:信令线程,工作者线程,网络线程。三者各司其职,WebRTC内部很多对象的方法都必须在规定的线程中执行,否则会触发断言。
  • 本文最重要的点也许是WebRTC代理模式的使用,对于应用层而言,得到的实体对象是一个Proxy对象,也即在用户和实际提供功能的类之间插入了一层代理层。代理层能正确地将用户侧的功能调用代理到目标线程中去执行目标类的目标方法,从而使得用户侧不必了解细节地使用API层接口,方便又安全。也即,代理模式解决了线程乱入问题,保证了线程安全。
  • PeerConnectionFactory提供的重要的功能比较少:创建PeerConnection,创建音频源CreateAudioSource,创建音频轨CreateAudioTrack,创建视频轨CreateVideoTrack。
Logo

致力于链接即构和开发者,提供实时互动和元宇宙领域的前沿洞察、技术分享和丰富的开发者活动,共建实时互动世界。

更多推荐