WebRTC 架构

WebRTC Native框架

我的书:

购买链接:

京东购买链接

淘宝购买链接

当当购买链接

WebRTC还是比较庞大的,乍一看无从下手,本篇以WebRTC自带的例子,阐述WebRTC Native核心的音频、视频和信令三个部分,WebRTC本身架构是P2P的,信令的部分也是围绕P2P展开的,好了,废话不多,直接上正文了。

WebRTC是Google开源的Web实时音视频通信框架。其提供P2P的音频、视频和一般数据传输协议栈的支持,其音频主要包括:采集播放、众多音频编解码器、语音增强、回声消除、网络均衡和拥塞控制等音频处理单元,其视频主要包括:采集播放,丢包隐藏,视频增强和编解码几个部分,支持的编解码有H264、VP8,VP9,AV1、H265在2020年4月已经基本集成完毕;在网络方面WebRTC提供针对音视频的动态抖动buffer管理和丢包隐藏处理,另外也提供基于STURN和TRUN的P2P的多媒体数据传输。

理解WebRTC Native层需要非常多的知识,包括各种网络协议,网络通信,音频视频编解码,音视频处理,音视频数据采集和播放,C/S(client service)通信架构以及多平台等,具体到编程实现上,包括object c 、c/c++,java,涉及平台包括MAC、IOS、Linux、windows以及android,由于官方WebRTC给的nija编译架构,这一编译系统屏蔽了较多的细节,这使得基于vs2019、Xcode以及androidstudio和GNU make编译需要重新搞一遍,很多新手在编译WebRTC是遇到的难题就退缩了。

本篇文章以Native层为出发点,对于Web浏览器不涉及,WebRTC Natvie代码的核心部分是音频、视频以及信令三个部分。为了便于理解这里以WebRTC自带的例子入手,涉及到Ubuntu和IOS两个平台的编译,例子在exmaple目录文件夹下。

视频会议服务端架构

在视频会议场景中,主要有三种类型的服务器架构,Mesh,MCU和SFU,WebRTC主推的是无中心的P2P架构,会为每一个端建立一个PeerConnection对象,这就是Mesh架构。

Mesh架构由于不需要多媒体(音视频)服务器,因而成本是最低的,安全性好,然而当人数较多时,可以看到P2P的链接数和带宽需求量变大,这在人数较少(如5人之内)是较为实用的,MCU架构需要服务器进行视频的解码、转码、混合和编码,但是上述视频的处理是比较消耗服务器资源的,因而成本较高,且会引入通信延迟,但是由于连接数量少,带宽压力小,因而在参会人数在数十人的场景中使用到,SFU架构和MCU架构一样都需要中心节点服务器,不同的是改服务器只负责转发,不负责视频处理,这在上百人场景中会用到该架构。

STURN & TRUN &ICE

在上述的Mesh架构下,需要进行透传以便通信双方能够正常进行,这就使用到的STURN和TURN以及ICE技术。

WebRTC的信令传输可以慢些但需要可靠,而流媒体则实时性优,可以存在丢包、错包;这就意味着需要两套网络传输协议,一套是基于TCP的可靠传输协议用于信令等传输,一套是基于UDP的TRP协议用于实时多媒体数据的传输。其协议栈如下图所示:

ICE(Interactive Connectivity Establishment)协议(RFC 5245)

STUN(Session Traversal Utilities for NAT)(RFC5389)

TURN(Traversal Using Relays around NAT)(RFC 5766)

这三个协议是基于UDP协议建立和维持P2P连接的必要网络组件;DTLS(Datagram Transport Protocol)用于P2P双方数据安全传输。

WebRTC代码架构

WebRTC目录结构如下所示,其中紫色的为目录。

各个目录的功能如下:

api目录:是对WebRTC功能件的封装,以更方便应用层调用,这里封装的内容包括audio、video、数据通道以及RTP传输,并在create_peerconnection_factory.h文件中定义了P2P通信的核心类PeerConnectionFactoryInterface;

audio目录:这里的audio层是用于发送和接收音频数据流的网络层,真实硬件的采集播放放在adm(audio device module),增强处理放在apm(audio processing module)里,adm和apm并不在这一目录下;

base目录:提供了一些依赖OS的基础函数,比如内存管理等;

build相关目录:使用与编译WebRTC的,在编译小节中会有编译说明;

call目录:从字面可以知道是用于通信用的,主要是RTP和RTCP相关协议的封装一遍WebRTC使用;

common_audio和common_video目录:音视频的各种算法都可能用到的,比如fir滤波,环形缓冲区,窗函数等;

examples:P2P等各个平台各种例子所在的目录;

media:是对video和audio增强和编解码的封装层,即video engine和audio engine。

modules:音视频具体功能实现所在的目录,如音视频编解码实现;音频混音、处理以及设备管理,视频采集播放以及数据发送和占用带宽估计等;

pc(peer connection)目录:P2P连接实现的核心目录;

sdk目录:android平台应用层Java和MAC平台应用层Object C访问natvie层的桥接层;

server端

STUN服务器:用于获取设备的外网地址;

TURN服务器:在P2P通信失败后用于中继;

ICE框架,整合了TURN和STUN。

信令服务器:负责端到端的连接,如SDP,candidate等;

因为由于IPv4地址数量不够用和安全的问题,开会的双方基本都在防火墙和NAT之后,通过运营商接入公网,当在家时手机、电脑、网络电视通过电信路由器上网时,路由器分配给我们的地址就是“192.168.XXX.XXX”,但是在公网上我们的数据头地址被转换成电信服务商提供的地址了,如果双发希望直接通信而不需要公共服务器中转(加大了延迟和丢包的不确定性)数据包,这时需要NAT穿透技术,STUN和TURN就是这种透传协议,ICE是一套整合了这两个协议的框架。

client端

Android IOS Windows MAC Linux 浏览器。

由于不同平台使用了不同的UI库和编程语言,他们的实现差异很大,但是不同的平台都会支持c/c++,所以为了适配不同的平台,WebRTC提供了SDK层,Linux平台基于使用GTK的c++,MAC和IOS平台基于使用cocoa库的Object c,android平台基于JAVA,为了让这些平台都能够调用c/c++核心函数,WebRTC提供了android和object c的封装层,这称为SDK层,android中使用了JNI机制使得UI层的JAVA程序和实现核心功能的c/c++程序可以互相调用,类似的object c使用了.mm扩展程序是得UI的.m程序可以和c/c++互相调用。由于访问各个平台都提供了C API(为了效率),所有已在音视频以及网络API都可以直接通过包含不同操作系统的头文件来实现跨平台差异化编译。

WebRTC官方工程里包括了一些应用程序例子,它们位于src/examples目录下,首先看下各个目录的作用。

peerconnection:windows和linux平台下使用Native API进行P2P通信的例子,其中客户端应用程序在client目录,服务器端应用程序在server目录下。客户端具有简单的音视频功能,服务器端使得客户端程序能够通过信令开启会议。整个会议过程需要服务器先启动服务,如./peerconnection_server --port=8888,正确启动后会有如下输出:

Server listening on port 8888

然后启动客户端,客户端UI包括如下部分,connecting to a server:在客户端程序启动时,需要指定服务器IP地址,然后可以点击connect按钮;其中localhost表示本机地址。

select a peer:当不同的client端连接到server时,他们就会互相发现对方如下所示(这是因为两个client是在同一个电脑不同终端启动的,所以两边看到的都是gsc@240),这是可以直接双击或者选中后回车建立P2P连接;

视频会议:当成功建立端到端的链接后,桌面会以全屏的形式显示视频界面,如下;

Ending chat session:当按Esc键时会推回到P2P列表选择界面;

Ending connection:继续按Esc将回到server连接界面;

工程编译

export PKG_CONFIG_PATH=$WEBRTCBUILDS_FOLDER/lib/Release/pkgconfig
Go to the peerconnection server folder
g++ -o peerconnection_server main.cc data_socket.cc peer_channel.cc utils.cc \
  $(pkg-config --cflags --libs --define-variable=prefix=$WEBRTCBUILDS_FOLDER libwebrtc_full)
Go to the peerconnection client folder
g++ -o peerconnection_client linux/main.cc linux/main_wnd.cc conductor.cc defaults.cc peer_connection_client.cc \
  $(pkg-config --cflags --libs --define-variable=prefix=$WEBRTCBUILDS_FOLDER libwebrtc_full) \
  $(pkg-config --cflags --libs gtk+-2.0) \
  $(pkg-config --cflags --libs x11)

WebRTC peerconnection分析

网络一部分是用于和signal服务器交互,一部分是P2P之间用,在会议开始前,需要知道通信双方的多媒体能力(不如支不支持视频,视频的编码能力等)以便双方能够通信,这被称为握手通信,为了实时性和效率,还会进行打洞(即上文说的TUN、STUN、ICE等)。

在WebRTC自带的端到端连接的例子中,客户端A和客户端B均通过socket和信令服务器建立TCP连接,object C下可以使用cocoaAsyncSocket框架;

然后客户端A通过voiceegine和videoengine获取多媒体数据并创建PeerConnection对象,然后通过AddTracks将音视频添加到PeerConnection中;

接下来客户端A调用PeerConnection对象的CreateOffer方法创建SDP offer,然后将这个SDP通过signaling server转发给客户端B,B在收到服务端转发的SDP offer之后,调用CreateAnswer创建SDP 应答,并通过信令服务器转发给A,客户端A收到SDP 应答之后,两边就完成了多媒体能力的协商,为音视频流的接收做好了准备;

接下来打洞以便进行P2P多媒体数据传输,首先客户端A通过PeerConnection创建时的参数等待来自ICE服务器的通信,获取自己的candidate,当获取到candidate时客户端会自动调用OnIceCandidate,客户端A通过signaling server将自己的candidate发送给客户端B,客户端B用类似的逻辑(同一套代码)将自己的candidate发送给客户端A,至此candidate握手完成,在SDP和candidate握手完成之后,两个客户端之间就建立了一条P2P连接,视频流就可以不通过Server端而直接进行传输;

PeerConnection应用层

这小节主要讲述应用程序是如何和native层代码交互的,使用WebRTC工具编译的native层仓库代码会链接到libjingle_peerconnection.so库中,上层调用这个库提供的音频、视频以及网络功能实现控制。其中linux和windows的上层界面开发较为相似,MAC/IOS(涉及object c)和android(涉及java)较为相似,Linux和windows通过c++直接调用natvie层代码,相对而言比较容易入门。

server端逻辑

server端首先启动,使用了socket编程,然后等待客户端连接,当客户端A连接时,会记录客户端A的信息,然后又有客户端B接入(和A一样会有HTTP的GET请求),这是会记录客户端B的信息,并将客户端A推动给客户端B,客户端B推送给客户端A,使得它们互相之间能够发现对方,然后客户端A点解连接客户端B,这是它们会启动SDP协议,以协商通信能力,并最终启动视频会议,服务端的逻辑较为简单,这里以核心代码片段说明:

int main(int argc, char* argv[]) {
  //首先创建侦听套接字,默认端口号位8888
  ListeningSocket listener;
  if (!listener.Create()) {
    
  PeerChannel clients;
  typedef std::vector<DataSocket*> SocketArray;
  SocketArray sockets;
  //quit 标识是是否退出的标志,否则这个循环一直会侦听网络端口数据情况
  while (!quit) {
    fd_set socket_set;
    FD_ZERO(&socket_set);
    if (listener.valid())
      FD_SET(listener.socket(), &socket_set);
//在新客户端接入时,会进入以下逻辑,
    if (FD_ISSET(listener.socket(), &socket_set)) {
      DataSocket* s = listener.Accept();
      if (sockets.size() >= kMaxConnections) {
        delete s;  // sorry, that's all we can take.
        printf("Connection limit reached\n");
      } else {
        //如果新连接被成功添加到SocketArray中,则中断会打印改信息
        sockets.push_back(s);
        printf("New connection...\n");
      }
    } 
    //在后续的循环中,由于SocketArray中有新的连接,所以会进入如下逻辑
   for (SocketArray::iterator i = sockets.begin(); i != sockets.end(); ++i) {
      DataSocket* s = *i;
      if (FD_ISSET(s->socket(), &socket_set)) {
        if (s->OnDataAvailable(&socket_done) && s->request_received()) {
          //从服务端注册列表中,检索这个client,由于是for循环从begin到end的遍历,
          //所以遍历完了之后,所有客户端界面都会显示连上服务器的其它客户端,这样想和谁通信,
          //客户端上点击对应的客户端项就行了,如果未找到,返回NULL
          ChannelMember* member = clients.Lookup(s);
          if (member || PeerChannel::IsPeerConnection(s)) {
            //如果发现s是注册,并且这个s并没有在服务端的注册列表中,则会用AddMember将其添加到注册列表中
            if (!member) {
              if (s->PathEquals("/sign_in")) {
                clients.AddMember(s);//这里的AddMember会打印记录的客户端名,如截图的gsc@240
              } 
              //如果客户端是等待状态,这是什么也不用做,等到有message时才进行处理
            } else if (member->is_wait_request(s)) {
              // no need to do anything.
              socket_done = false;
            //流程到这里,说明这个客户端已经在服务器注册了,并且这次收到的HTTP消息是message,不是wait,sign_out之类的
            } else {
              //这里找到想要通信客户端(设为B)的ChannelMember对象,如果server端没找到,则返回NULL
              ChannelMember* target = clients.IsTargetedRequest(s);
              if (target) {
                //这里将包含客户端A SDP协议信息转送给B
                member->ForwardRequestToPeer(s, target);
              } else if (s->PathEquals("/sign_out")) {
                s->Send("200 OK", true, "text/plain", "", "");
              } 
            }
          } 
        }
      } 

当client点击connect之后,客户端给服务器发过去"/sign_in"的get请求,服务器给当前客户列表中的每个都发一遍新用户的信息,格式是"<client_name>,<client_id>,1\r\n",服务器将现有的peers列表信息以如下格式连接"<client_name>,<client_id>,if_connected?1:0\r\n"发送给新来的client.得到这些信息后客户端就会把他们显示在pees列表中。客户端每次给服务器发送消息之后都要发送一次"/wait"的get请求,目的是提供response对象,以便服务器在需要给客户端发消息时使用。当客户端双击某个peer发起连接时,会给服务器发来"/message"的post请求,请求的peer_id参数是发起方的id,to参数是要连接的peer的id,内容是sdp信息;服务器把内容转发给相应id的客户端,客户端接收到后解析sdp,得到媒体流信息(是否含音视频,音视频参数等),在本地建立媒体流通道,以便之后接收媒体数据和传给主窗体显示;完了创建本地的sdp,并发回给对方。客户端从STUN/TURN服务器获取本地地址(candidate),经由服务器中转发给对方(同样走/messgae),对方接收到后会向该地址发送消息等待回应以验证是否可连通,联通成功peerconnection就此建立完成,音视频数据就可以通过基于udp的rtp/rtcp协议在peerconnection之间传输。

客户端逻辑

客户端稍微复杂些,和server端相比,多了多媒体内容,但是WebRTC的例子非常巧妙,使用较少的代码就实现待UI的视频通信的完整例子。

Conductor是封装了windows和PeerConnection两个主要类,是该工程的核心,conductor通过CreatePeerConnectionFactory方法创建PeerConnectionFactoryInterface接口的实现对象,通过改接口创建WebRTC核心协议的PeerConnectionInterface接口对象,PeerConnectionFactoryInterface还提供了创建本地音视频功能的接口,conductor的回调通过PeerConnectionObserver接口完成。

Linux UI

linux UI层使用了GTK显示框架,将点击、输入等事件使用信号槽的机制,将界面和需要执行的动作关联起来,如点击加入会议涉及到PeerConnection创建和连接,这里clicked是Button触发的信号,而Button触发时会自动触发row-activated信号,所以下面的两个回调都会被调用到,OnClickedCallback获取用户输入的登录服务IP地址和端口号,并向服务器发起HTTP登录请求,而row-activated信号的回调PeerConnection初始化并创建和发送SDP包。代码实现上将PeerConnection和windows作为两个独立的组件,用conductor类管理起来。

g_signal_connect(button, "clicked", G_CALLBACK(OnClickedCallback), this);
g_signal_connect(peer_list_, "row-activated",
                     G_CALLBACK(OnRowActivatedCallback), this);

P2P的创建过程有些琐碎,这里罗列如下:

  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 */);
//这里config的一些配置包括dtls是否启用等标识
  peer_connection_ = peer_connection_factory_->CreatePeerConnection(
      config, nullptr, nullptr, this);
 
  rtc::scoped_refptr<webrtc::AudioTrackInterface> audio_track(
      peer_connection_factory_->CreateAudioTrack(
          kAudioLabel, peer_connection_factory_->CreateAudioSource(
                           cricket::AudioOptions())));
  auto result_or_error = peer_connection_->AddTrack(audio_track, {kStreamId});

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

发送完登录请求和SDP请求之后,由网络线程一直检测来自网络的数据,并触发相应的函数,如更新列表和对端发来的数据,最终调用OnMessageFromPeer处理对端发来的数据,如果对端是新来的,则会创建InitializePeerConnection对象,这在主动发起会议时已经见过,如果是主动发起方回调了这个函数,就不会再创建这个对象了,但是依然要解析对端的SDP信息,SDP信息是JSON格式。

    std::unique_ptr<webrtc::SessionDescriptionInterface> session_description =
        webrtc::CreateSessionDescription(type, sdp, &error);
            peer_connection_->SetRemoteDescription(
        DummySetSessionDescriptionObserver::Create(),
        session_description.release());
    if (type == webrtc::SdpType::kOffer) {
      peer_connection_->CreateAnswer(
          this, webrtc::PeerConnectionInterface::RTCOfferAnswerOptions());
    }

拿到的信息除了SDP外还可能是打洞信息,则也需要将他们保存下来。

    std::unique_ptr<webrtc::IceCandidateInterface> candidate(
        webrtc::CreateIceCandidate(sdp_mid, sdp_mlineindex, sdp, &error));
    if (!peer_connection_->AddIceCandidate(candidate.get())) {
      RTC_LOG(WARNING) << "Failed to apply the received candidate";
      return;
    }

MAC UI

这里以MAC平台示例说明如何使用WebRTC,下图是object c所写的应用程序的界面,即上一节UI层相关内容:

程序最开始的执行函数在examples/objc/AppRTCMobile/mac目录的main.m(object c下的c扩展文件以.m结尾,c++扩展以.mm结尾),这个main函数如下:

APPRTCAppDelegate.h文件
@interface APPRTCAppDelegate : NSObject <NSApplicationDelegate>
@end
  APPRTCAppDelegate.m文件
  @implementation APPRTCAppDelegate {
  APPRTCViewController* _viewController;
  NSWindow* _window;
}

  main.m文件
#import <AppKit/AppKit.h>
#import "APPRTCAppDelegate.h"
int main(int argc, char* argv[]) {
  @autoreleasepool {
    [NSApplication sharedApplication];
    APPRTCAppDelegate* delegate = [[APPRTCAppDelegate alloc] init];
    [NSApp setDelegate:delegate];
    [NSApp run];
  }
}

这个函数是object的语法,NSApplicationDelegate是cocoa库里的应用程序代理,本身这个应用程序定理很多方法,如run运行等,@interface 表明APPRTCAppDelegate是继承?了NSApplicationDelegate协议,这个类本在实现时(@implementation)定义了两种组件,APPRTCViewController和NSWindow,NSWindow是上图看到的图像界面,比如长宽之类都在这里,APPRTCViewController用于和用户交互,如输入框和按钮。当应用程序在mian.m里执行到run时,会触发APPRTCAppDelegate.m(这和cocoa里的应用程序生命周期有关)。

- (void)applicationDidFinishLaunching:(NSNotification*)notification {
  RTCInitializeSSL();
  NSScreen* screen = [NSScreen mainScreen];
  NSRect visibleRect = [screen visibleFrame];
  NSRect windowRect = NSMakeRect(NSMidX(visibleRect),
                                 NSMidY(visibleRect),
                                 1320,
                                 1140);
  NSUInteger styleMask = NSTitledWindowMask | NSClosableWindowMask;
  _window = [[NSWindow alloc] initWithContentRect:windowRect
                                        styleMask:styleMask
                                          backing:NSBackingStoreBuffered
                                            defer:NO];
  _window.delegate = self;
  [_window makeKeyAndOrderFront:self];
  [_window makeMainWindow];
  _viewController = [[APPRTCViewController alloc] initWithNibName:nil
                                                           bundle:nil];
  [_window setContentView:[_viewController view]];
}

其中-号表示的私有实现,这里就是设置显示窗的参数,并初始化控制组件(APPRTCViewController),APPRTCViewController负责显示local和remote视频流,会议号和会议控制,当点击加入会议后会执行APPRTCViewController.m中的如下方法:

- (void)startCall:(id)sender {
  NSString* roomString = _roomField.stringValue;
  // Generate room id for loopback options.
  if (_loopbackButton.intValue && [roomString isEqualToString:@""]) {
    roomString = [NSUUID UUID].UUIDString;
    roomString = [roomString stringByReplacingOccurrencesOfString:@"-" withString:@""];
  }
  [self.delegate appRTCMainView:self
                 didEnterRoomId:roomString
                       loopback:_loopbackButton.intValue];
  [self setNeedsUpdateConstraints:YES];
}

roomString是前面输入的房间号字符串,如果空的就是会测试模式.

@interface APPRTCViewController ()
    <ARDAppClientDelegate, APPRTCMainViewDelegate>
@property(nonatomic, readonly) APPRTCMainView* mainView;
@end

这个控件因为要管理signaling和多媒体传输以及状态和多媒体显示,这里使用ARDAppClientDelegate实现前者,而APPRTCMainViewDelegate实现状态和多媒体显示;尖括号表示APPRTCViewController遵循上面两个protocol,点击start call最终会调到ARDAppClientDelegate里的connectToRoomWithId方法,下面的代码片段是mac目录掉到上一级目录AppRTCMobile的核心代码段。

  mac/APPRTCViewController.m
  ARDAppClient* client = [[ARDAppClient alloc] initWithDelegate:self];
  [client connectToRoomWithId:roomId
                     settings:[[ARDSettingsModel alloc] init]  // Use default settings.

这个方法定义于src/examples/objc/AppRTCMobile/ARDAppClient.h

- (void)connectToRoomWithId:(NSString *)roomId
                   settings:(ARDSettingsModel *)settings
                 isLoopback:(BOOL)isLoopback {
  NSParameterAssert(roomId.length);
  NSParameterAssert(_state == kARDAppClientStateDisconnected);
  _settings = settings;
  _isLoopback = isLoopback;
  self.state = kARDAppClientStateConnecting;

//初始化视频编解码参数
  RTCDefaultVideoDecoderFactory *decoderFactory = [[RTCDefaultVideoDecoderFactory alloc] init];
  RTCDefaultVideoEncoderFactory *encoderFactory = [[RTCDefaultVideoEncoderFactory alloc] init];
  encoderFactory.preferredCodec = [settings currentVideoCodecSettingFromStore];
  _factory = [[RTCPeerConnectionFactory alloc] initWithEncoderFactory:encoderFactory
                                                       decoderFactory:decoderFactory];

#if defined(WEBRTC_IOS)
  if (kARDAppClientEnableTracing) {
    NSString *filePath = [self documentsFilePathForFileName:@"webrtc-trace.txt"];
    RTCStartInternalCapture(filePath);
  }
#endif

  // 申请 TURN.
  __weak ARDAppClient *weakSelf = self;
  [_turnClient requestServersWithCompletionHandler:^(NSArray *turnServers,
                                                     NSError *error) {
    if (error) {
      RTCLogError(@"Error retrieving TURN servers: %@", error.localizedDescription);
    }
    ARDAppClient *strongSelf = weakSelf;
    [strongSelf.iceServers addObjectsFromArray:turnServers];
    strongSelf.isTurnComplete = YES;
    [strongSelf startSignalingIfReady];//关键
  }];

  // 通过room server加入开会房间
  [_roomServerClient joinRoomWithRoomId:roomId
                             isLoopback:isLoopback
      completionHandler:^(ARDJoinResponse *response, NSError *error) {
    ARDAppClient *strongSelf = weakSelf;
    if (error) {
      [strongSelf.delegate appClient:strongSelf didError:error];
      return;
    }
    NSError *joinError =
        [[strongSelf class] errorForJoinResultType:response.result];
    if (joinError) {
      RTCLogError(@"Failed to join room:%@ on room server.", roomId);
      [strongSelf disconnect];
      [strongSelf.delegate appClient:strongSelf didError:joinError];
      return;
    }
    RTCLog(@"Joined room:%@ on room server.", roomId);
    strongSelf.roomId = response.roomId;
    strongSelf.clientId = response.clientId;
    strongSelf.isInitiator = response.isInitiator;
    for (ARDSignalingMessage *message in response.messages) {
      if (message.type == kARDSignalingMessageTypeOffer ||
          message.type == kARDSignalingMessageTypeAnswer) {
        strongSelf.hasReceivedSdp = YES;
        [strongSelf.messageQueue insertObject:message atIndex:0];
      } else {
        [strongSelf.messageQueue addObject:message];
      }
    }
    strongSelf.webSocketURL = response.webSocketURL;
    strongSelf.webSocketRestURL = response.webSocketRestURL;
    [strongSelf registerWithColliderIfReady];
    [strongSelf startSignalingIfReady];
  }];
  // Join room on room server.
  [_roomServerClient joinRoomWithRoomId:roomId
                             isLoopback:isLoopback
      completionHandler:^(ARDJoinResponse *response, NSError *error) {
    ARDAppClient *strongSelf = weakSelf;
    if (error) {
      [strongSelf.delegate appClient:strongSelf didError:error];
      return;
    }
    NSError *joinError =
        [[strongSelf class] errorForJoinResultType:response.result];
    if (joinError) {
      RTCLogError(@"Failed to join room:%@ on room server.", roomId);
      [strongSelf disconnect];
      [strongSelf.delegate appClient:strongSelf didError:joinError];
      return;
    }
    RTCLog(@"Joined room:%@ on room server.", roomId);
    strongSelf.roomId = response.roomId;
    strongSelf.clientId = response.clientId;
    strongSelf.isInitiator = response.isInitiator;
    for (ARDSignalingMessage *message in response.messages) {
      if (message.type == kARDSignalingMessageTypeOffer ||
          message.type == kARDSignalingMessageTypeAnswer) {
        strongSelf.hasReceivedSdp = YES;
        [strongSelf.messageQueue insertObject:message atIndex:0];
      } else {
        [strongSelf.messageQueue addObject:message];
      }
    }
    strongSelf.webSocketURL = response.webSocketURL;
    strongSelf.webSocketRestURL = response.webSocketRestURL;
    [strongSelf registerWithColliderIfReady];
    [strongSelf startSignalingIfReady];
  }];
}

SDK桥接层

SDK层负责将UI的视窗控件和Natvie层的相关组件(audio/video/network)相关连,这里的关联有两层意义,一层是调用Native层的相关功能,一层是将natvie层的相关类和方法进行聚合使用(如将audio和video组成一个mediastream使用)。在P2P的应用场景中,SDK层主要有video source、video track、audio source、audio track、DataChannel、PeerConnection、RTP、ICE、SessionDescription等组成。从字面即可了解他们的意义,其中PeerConnection是核心中的核心,SDK层的提供的音视频、网络等都或多或少和其相关。

P2P会议的状态由PeerConnection进行管理,signaling state用于信令握手管理,ice连接状态用于透传管理,

/** Represents the signaling state of the peer connection. */
typedef NS_ENUM(NSInteger, RTCSignalingState) {
  RTCSignalingStateStable,
  RTCSignalingStateHaveLocalOffer,
  RTCSignalingStateHaveLocalPrAnswer,
  RTCSignalingStateHaveRemoteOffer,
  RTCSignalingStateHaveRemotePrAnswer,
  // Not an actual state, represents the total number of states.
  RTCSignalingStateClosed,
};

/** Represents the ice connection state of the peer connection. */
typedef NS_ENUM(NSInteger, RTCIceConnectionState) {
  RTCIceConnectionStateNew,
  RTCIceConnectionStateChecking,
  RTCIceConnectionStateConnected,
  RTCIceConnectionStateCompleted,
  RTCIceConnectionStateFailed,
  RTCIceConnectionStateDisconnected,
  RTCIceConnectionStateClosed,
  RTCIceConnectionStateCount,
};

/** Represents the combined ice+dtls connection state of the peer connection. */
typedef NS_ENUM(NSInteger, RTCPeerConnectionState) {
  RTCPeerConnectionStateNew,
  RTCPeerConnectionStateConnecting,
  RTCPeerConnectionStateConnected,
  RTCPeerConnectionStateDisconnected,
  RTCPeerConnectionStateFailed,
  RTCPeerConnectionStateClosed,
};

/** Represents the ice gathering state of the peer connection. */
typedef NS_ENUM(NSInteger, RTCIceGatheringState) {
  RTCIceGatheringStateNew,
  RTCIceGatheringStateGathering,
  RTCIceGatheringStateComplete,
};

natvie API

在WebRTC应用程序示例中,多次使用到了API层,P2P连接的核心对象是peer_connection_factory_,其定义如下:

RTC_EXPORT 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);

}  // namespace webrtc

其中三个线程分别是网络通信,工作者线程和信号线程;这之后分别是audio和video,本节就在API层展开多媒体的核心内,至于类是如何管理和实现多媒体功能的在video和audio章节中会有详细叙述。

adm

adm是audio device module的简写,这个类的定义在./modules/audio_device/include/audio_device.h,adm要适配不同的操作系统,所以定义了一个枚举类型来表征,这个枚举类型,显示了在所有平台上OS支持的audio API。

  enum AudioLayer {
    kPlatformDefaultAudio = 0,
    kWindowsCoreAudio,
    kWindowsCoreAudio2,
    kLinuxAlsaAudio,
    kLinuxPulseAudio,
    kAndroidJavaAudio,
    kAndroidOpenSLESAudio,
    kAndroidJavaInputAndOpenSLESOutputAudio,
    kAndroidAAudioAudio,
    kAndroidJavaInputAndAAudioOutputAudio,
    kDummyAudio,
  };

WebRTC线程模型

WebRTC Native API使用信令线程(Signaling thread)和工作者线程(worker thread)这两个全局线程,应用程序可以自行实现这两个线程或者由WebRTC内部创建。Stream API和PeerConnection API的调用都会被代理到信令线程,这就意味着应用程序可以任何线程调用这些API。

Stream API定义于:media_stream_interface.h

PeerConnection API定义于:peer_connection_interface.h

由于不同平台的图形界面实现方式并不一样,MAC/IOS使用Cocoa SDK,安卓平台使用Android SDK,Linux 平台使用GTK,而Windows平台使用windows SDK,它们的编程差异比较大,实现的机理也有差异,为方便不同平台调用Stream API和Peerconnection API来使用Natvie 层的相关组件,都会在这个原生的基础上封装出一套代码以便使用。

P2P Native层 API

  1. MediaStream:获取本地麦克风和camera的音视频同步多媒体流;

  2. RTCPeerConnection:构建点对点之间稳定、高效的流传输组件;

  3. RTCDataChannel:P2P之间构建一个高吞吐量、低延迟的信道,用于传输任意数据;

MediaSteam API

WebRTC中每一种多媒体流类型都可以使用流接口来表示(MediaStreamTrackInterface类,可以用来表示一种或多种语音或者视频流,定义于media_stream_interface.h ),而流媒体的实现则用(MediaStream类实现,定义于media_stream.h),这是个聚合了若干多媒体流(多语音流,多视频流)的接口。

api/media_stream_interface.h 和pc/media_stream.h

PeerConnection API

由于这个类比较长,所以这里不再展示其和其它类之间的关系了。

Voice Engine

处理流程是采集、降噪增强、编码、分段、加密、传输、接收、解密、重组、解码播放。以下给出了之间的关系(不包括加密解密)

多媒体类对音视频的封装关系如下图所示;

video

audio

Video Engine

视频编解码

RGB:三基色,原色。

生物学发现人眼对明亮比较敏感(有细胞对这个敏感),所以一般使用YUV表示图像(这样可以压缩)。

YUV(YCbCr,YPbPr):颜色编码方法,Y(luma)明亮度(灰阶值),U(chroma blue),V(chroma red),UV用于描述视频的色彩和饱和度,用于指定像素的颜色;从历史的演变来说,其中YUV和Y'UV通常用来编码电视的模拟信号,而YCbCr则是用来描述数字的视频信号,适合视频与图片压缩以及传输,例如MPEG、JPEG。但在现今,YUV通常已经在电脑系统上广泛使用。

RGB转换为YCbCr

Y = 0.299R + 0.587G + 0.114B

Cb = 0.564(B - Y) | Cr = 0.713(R - Y)

YCbCr转为RGB

R = Y + 1.402Cr | B = Y + 1.772Cb | G = Y - 0.344Cb - 0.714Cr

YUV 分为紧缩和平面两种格式,紧缩格式将Y,U,V值存储成MacroPixels数组,和RGB存储方式类似,平面格式将Y,U,V三个分量放在不同的矩阵中。

这是像素点级别的压缩。

时域相关性压缩

由于图片时间上是相关的,比如电影里常有片段,一个人在大街上走,人是动的,但是街是一直不懂的,几秒钟的画面有些是一直不变的,所以可以使用时域相关性减少数据的传输,实现上使用I、P、B帧来处理时域相关性。

I帧是帧内编码(Intra-coded picture)帧,包含了一帧的完整信息,像传统的静态图片;P和B帧只含有部分信息,因而存储空间减少,压缩了数据量。P帧紧包括和先前帧的差异,I帧不易压缩,但是不需要解码,P帧可以结合前帧解压缩,B帧可以使用前后帧信息进行高度压缩。

空域相关性压缩

空域相关性比如在蓝天白云的场景,白云是一大块,这一大块核心区的像素点的值是一样的,这时可以用一个点表示一块区域,对于蓝天也是一样,对于颜色非常相似的块可以用一个点表示。在编码时,通常第一步分块就是利用空域相关性压缩。

编码过程和audio编码过程非常相似,此外针对视频还有运动补偿模块,绝大多数编码工作流程如下。

264编码过程

编码硬件加速

增加新的编码器

 

©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页