前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >结合 AppRTC 源码分析 WebRTC 建立连接的过程

结合 AppRTC 源码分析 WebRTC 建立连接的过程

作者头像
JoeyBlue
发布2021-09-07 14:53:19
1.6K0
发布2021-09-07 14:53:19
举报
文章被收录于专栏:代码手工艺人

这两年来,WebRTC 越来越多地出现在人们的视野,在在线教育,在线医疗等领域的应用也越来越多。大家研究 WebRTC 的热情也越来越高涨,不过 WebRTC 的入门门槛个人觉得稍微有些高,特别是各种概念,比如 NAT 穿越,ICE,STUN,TURN,Signaling server等等,刚开始可能会觉得比较繁杂,不易理解。然后建立连接的整个过程,异步调用比较多,很容易搞混。那么这篇文章里我们会根据 WebRTC 的官方 demo AppRTC 的 iOS 版本来分析一下 WebRTC 从进入房间到建立音视频连接的过程,为了便于了解,我们本次的讨论不涉及到底层的具体实现。

1. 相关概念

我们首先来简单地了解几个概念:

1.1 NAT 穿越(NAT Traversal)

因为 WebRTC 是 P2P 的,很多时候 peer 是隐藏在 NAT 之后的,没有外网的 IP 地址,如果两个 peer 都在 NAT 后面,都没有外网的 IP (或者说都不知道自己的外网 IP),是无法建立连接的。那么 NAT 穿越就是用来解决这个问题的,NAT 穿越也俗称 “P2P 打洞”。常见的两种穿越方式是 STUN 和 TURN。

1.2 STUN server

最新的 STUN 定义是 Session Traversal Utilities for NAT,可以参考 RFC5389 (https://tools.ietf.org/html/rfc5389),顾名思义,他是一个 NAT 穿越的工具,既然上面我们知道了 NAT 之后的 peer 可能不知道自己的外网 IP 是多少,那么 STUN 这个工具就可以帮助内网的主机拿到,并告诉他外网对应的 IP 地址。

1.3 TURN server

TURN 是 Traversal Using Relays around NAT 的缩写,可以参考 RFC5766 (https://tools.ietf.org/html/rfc5766)。有些内网类型比较复杂,比如对称型的 NAT,STUN server 拿到的外网对应的 IP 之后,还是无法通信,这时候就需要一个服务器来做数据的中转 (也叫中继,或者 relay),这个中转服务器就叫做 TURN server。

根据统计数据表明,STUN 可以解决 85% 左右情况下的 NAT 问题,剩余的就需要 TURN 来解决。这两种穿越方式对比来看,STUN 更简单,服务器的消耗和成本比较低,但是能解决问题的场景受限制,TURN 服务器可以解决几乎所有场景下的问题(包含 STUN 可以解决的场景),但是因为需要做数据中转,所以对服务器的性能要求比较高,成本也会比较高。一般情况下会两者兼用,首先尝试 STUN,STUN 解决不了的 case 用 TURN。

1.4 ICE

ICE 是 Interactive Connectivity Establishment 的缩写,可以参考最新的RFC8445 (https://tools.ietf.org/html/rfc8445) 规范。顾名思义,ICE 就是 交互式连接建立 的意思,ICE 描述了一种使用 STUN 和 TURN 来穿越 NAT 建立 P2P 连接的一种规范。

每个 peer 可以收集到 3 种服务器地址,一个是自己网卡上绑定的 IP 地址,也叫Local Address, 第二个是 STUN server 告诉自己的外网的地址,比如路由器上绑定的外网IP地址,叫做 Server Reflexive Address,第三个是 TURN server 给自己创建的中转服务器 IP 地址,叫做Relayed Address

在 ICE 标准里,每个 peer 收集所有上述三种种类的 IP 地址和端口,并发送到对方 peer(体现了“交互”),对方也收集所有三种类型的 IP 地址和端口,然后发送给自己。这样,自己和对方都有了彼此的所有 IP 地址和端口之后,开始按照优先级建立连通性检查,一旦找到一个可以互通的连接,就开始用该连接进行音视频数据传输。

1.5 ICE candidate

ICE 候选人,可以简单理解为就是上面所说的每个 peer 收集到的 IP 地址和端口(实际要比这个复杂,包含传输方式等等)。收集的过程,叫做 ICE candidate gathering.

1.6 SDP

然后我们说一下 SDP,SDP 是 Session Description Protocol 的缩写,可以参考 RFC4566 (https://tools.ietf.org/html/rfc4566) 规范。在介绍这个之前,我们来思考一个问题,如果我们要用 WebRTC 来进行 P2P 的视频通话,可能两端所支持的音视频格式集合不完全一致,比如一端支持 H264 和 VP8,另一端支持 VP8 和 VP9,那如何选择呢?SDP 就是来描述每个 peer 所支持的音视频格式,以及如何决定传输的音视频格式的。

1.7 Signaling Server

上面提到的 ICE candidate 和 SDP 都需要传给对方,因为没办法直接传给对方,所以一般通过服务器来中转,这个中转的过程,并不在 WebRTC 规范里,所以使用者可以自己来实现。一般来说,可以使用 WebSocket 服务器来实现。比如建立连接的A、B 双方都连接到同样一个 WebSocket 服务器,A 发到服务器的 ICE candidate 或者 SDP,服务器都直接转发给 B,同理也会把 B 的消息转发给 A,达到交换的目的。

2. 整体流程图

假设有两个人,我们姑且称作为 Alice 和 Bob,通过 AppRTC 这个 demo app,进入了同一个房间。下面这个序列图就是客户端上的整体流程,为了简化理解,这里只设定了 5 个参与者,首先是两端(Alice 和 Bob),然后是 Signaling server & webserver,最后是两个端的底层 SDK 或者所在的浏览器(浏览器场景和客户端很类似,所以放一起了)。

我们逐步的来说:

  • 1-2, 3-4:Alice 和 Bob 都通过 API 进入房间,joinRoom 获得 roomId,并且通过requestIceServers 获得 Ice server (也就是 STUN 和 TURN server)的地址,然后连接到 WebSocket 服务器上。
  • 5-6,7-8:Alice 和 Bob 都创建一个 PeerConnection,这个是个很重要的类,PeerConnection 负责编解码和传输的所有处理。
  • 9-10:createOffer,创建 offer,通过创建 offer,底层会返回 SDP,就是自己所支持的音视频格式等描述信息。
  • 10-12:createOffer 之后,把得到的 SDP,设置为 local description,同时会触发底层的 ICE candidate gathering,此时开始收集自己的 ICE candidate,也就前面提到的三种 IP 地址和端口。
  • 13-14:把 SDP 通过 signaling server 发送给 Bob
  • 15,16-17:Bob 收到 SDP 之后,设置为 remote description,之后 createAnswer,createAnswer 和 createOffer 很类似,也会收到底层返回的自己的 SDP。并设置为 local description。设置 local description 会触发底层 ICE candidate gathering. 此时,Bob 有了自己和对方的 SDP.
  • 20-21:Bob 把自己的 SDP 通过 signaling server 发给 Alice
  • 22: Alice 把收到的 Bob 的 SDP,并设置为 remote description,此时,Alice 也有了自己和对方的 SDP 了。
  • 23-26:因为在 step 12 的时候开始了 ICE candidate gathering,当每收集到一个 candidate 之后,会触发 didGenerateIceCandidate 的回调, 此时 Alice 不做处理,直接通过 Signaling server 传递给 Bob,Bob 把 candidate 添加到 peer connection 里。
  • 27-30: 同 23-26 ,Bob 把自己的 IP 地址告诉 Alice。

此后,Alice 和 Bob 的底层就开始进行连通性测试和协商,一旦找到一对互通的传输地址,就开始传输音视频数据,彼此就能看到对方了。

实际中的流程比这个要复杂,像 candidate gathering 是异步的,可能穿插在整个过程里面。所以有些状态需要判断。

3. 结合源码分析

OK 既然大致的流程我们看完了,我们来看看具体的代码实现。AppRTC 源码可以在官网 (https://webrtc.org/native-code/ios/) 上找到,这里不详细地介绍如何下载源码了

PS: AppRTC 和 Web 端 Demo (https://appr.tc) 是互通的。STUN 和 TURN server 默认用的都是 Google 提供的。

主要的逻辑集中在 ARDAppClient 这个类里面。当进入 app,输入房间号,点 Call Room 之后就执行到了 connectToRoomWithId:settings:isLoopback: 方法里。

下图是 AppRTC 的主界面

3.1 创建 RTCPeerConnectionFactory

ARDAppClient: connectToRoomWithId:settings:isLoopback:

代码语言:javascript
复制
//创建peerConnectionFactory,以及对应的 video codec factory
RTCDefaultVideoDecoderFactory *decoderFactory = [[RTCDefaultVideoDecoderFactory alloc] init];
RTCDefaultVideoEncoderFactory *encoderFactory = [[RTCDefaultVideoEncoderFactory alloc] init];
encoderFactory.preferredCodec = [settings currentVideoCodecSettingFromStore];
_factory = [[RTCPeerConnectionFactory alloc] initWithEncoderFactory:encoderFactory decoderFactory:decoderFactory];

在这里,首先初始化视频编解码器的工厂类,并通过他们创建出一个 RTCPeerConnectionFactory 的实例。

3.2 获得 TURN/STUN server 地址

随后,通过调用 appr.tc 的 API,获得 ICE server 地址,这个后面会用到。

代码语言:javascript
复制
// Request TURN.
__weak ARDAppClient *weakSelf = self;
[_turnClient requestServersWithCompletionHandler:^(NSArray *turnServers,
                                                 NSError *error) {
    //此处省略错误检查代码 
    ARDAppClient *strongSelf = weakSelf;
    [strongSelf.iceServers addObjectsFromArray:turnServers];
    strongSelf.isTurnComplete = YES;
    [strongSelf startSignalingIfReady];
}];

3.3 加入房间,并连接上 WebSocket

加入房间,获得 room id,有服务端返回自己是发起者还是接受者(先加入房间的是发起者,后加入的是非发起者),并连接 websocket. 这里发起者和接受者的逻辑是一样的。

代码语言:javascript
复制
// Join room on room server.
[_roomServerClient joinRoomWithRoomId:roomId
                         isLoopback:isLoopback
  completionHandler:^(ARDJoinResponse *response, NSError *error) {
    ARDAppClient *strongSelf = weakSelf;
	
	// 此处省略错误检查代码 
    
    RTCLog(@"Joined room:%@ on room server.", roomId);
    strongSelf.roomId = response.roomId; //获得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];
        }
    }
    //获得 WebSocket 地址
    strongSelf.webSocketURL = response.webSocketURL;
    strongSelf.webSocketRestURL = response.webSocketRestURL;
    [strongSelf registerWithColliderIfReady]; // 连接WebSocket 服务器
    [strongSelf startSignalingIfReady]; 
}];

3.4 创建 RTCPeerConnection 的实例

在上面的最后一个方法调用里,也就是startSignalingIfReady 方法,开始主要的流程:

代码语言:javascript
复制
//RTCMediaConstraints 主要是描述音视频媒体的参数,比如分辨率大小,音频声道数等等
RTCMediaConstraints *constraints = [self defaultPeerConnectionConstraints]; 
RTCConfiguration *config = [[RTCConfiguration alloc] init];
RTCCertificate *pcert = [RTCCertificate generateCertificateWithParams:@{
    @"expires" : @100000,
    @"name" : @"RSASSA-PKCS1-v1_5"
}];
config.iceServers = _iceServers; //上面从服务器获得的 ICE server 地址
config.sdpSemantics = RTCSdpSemanticsUnifiedPlan; //这里使用 unified plan
config.certificate = pcert;
    
//创建 PeerConnection!!!
❶_peerConnection = [_factory peerConnectionWithConfiguration:config
                                              constraints:constraints
                                                 delegate:self];
    
❷[self createMediaSenders]; 
  if (_isInitiator) { //只有发起方创建 offer,接收方只需等待对方的offer,然后创建answer即可
    // Send offer.
    __weak ARDAppClient *weakSelf = self;
    
  ❸[_peerConnection offerForConstraints:[self defaultOfferConstraints]
                       completionHandler:^(RTCSessionDescription *sdp,
                                           NSError *error) {
      ARDAppClient *strongSelf = weakSelf;
      [strongSelf peerConnection:strongSelf.peerConnection
          didCreateSessionDescription:sdp
                                error:error];
    }];
  } else {
    // Check if we've received an offer.
  ❹[self drainMessageQueueIfReady];
  }

❶ 通过 RTCPeerConnectionFactory 创建 RTCPeerConnection 的实例,RTCPeerConnection 是核心类之一,把控从采集到发送这个流程。 ❷ createMediaSenders 方法的实现如下,首先根据 constraints 创建 source,接着用 source 创建 track,最后把创建好的 track 添加到 peer connection 里,到这一步,底层会自动创建 media sender,所以这个方法的名字叫 ‘createMediaSenders’

source 可以理解为音视频源,track 用来描述音视频轨道,sender 是用来发送音视频数据的类,这三个概念对于音视频是独立的。这里不多做描述,有兴趣的可以看一下源码。

RTCCameraVideoCapturer 类负责视频的采集,音频的采集是使用 SDK 默认的方法,这里没有单独创建。

代码语言:javascript
复制
- (void)createMediaSenders {
	//创建 local audio track,并添加到 peerconnection 中
  RTCMediaConstraints *constraints = [self defaultMediaAudioConstraints];
  RTCAudioSource *source = [_factory audioSourceWithConstraints:constraints];
  RTCAudioTrack *track = [_factory audioTrackWithSource:source
                                                trackId:kARDAudioTrackId];
  [_peerConnection addTrack:track streamIds:@[ kARDMediaStreamId ]];
	
	//创建 local video track 并添加到 peerconnection 中(最终会添加到 peerconnection.transceiver.sender 里)
  _localVideoTrack = [self createLocalVideoTrack];
  if (_localVideoTrack) {
    [_peerConnection addTrack:_localVideoTrack streamIds:@[ kARDMediaStreamId ]];
    [_delegate appClient:self didReceiveLocalVideoTrack:_localVideoTrack];
    
    // We can set up rendering for the remote track right away since the transceiver already has an
    // RTCRtpReceiver with a track. The track will automatically get unmuted and produce frames
    // once RTP is received.
    RTCVideoTrack *track = (RTCVideoTrack *)([self videoTransceiver].receiver.track);
    [_delegate appClient:self didReceiveRemoteVideoTrack:track];
  }
}

//只保留了核心逻辑,其他省略了
- (RTCVideoTrack *)createLocalVideoTrack {
	//factory 创建一个新的 source
  RTCVideoSource *source = [_factory videoSource];
  //用 source 创建一个 RTCCameraVideoCapturer,RTCCameraVideoCapturer 负责 iOS 上的采集,
  //采集到的视频帧发送给 delegate。RTCVideoSource 实现了 RTCVideoCapturerDelegate,
  //所以可以接收到 RTCCameraVideoCapturer 采集到的视频帧
  RTCCameraVideoCapturer *capturer = [[RTCCameraVideoCapturer alloc] initWithDelegate:source];
    
	//这里的delegate 就是 ARDVideoCallViewController,它收到消息后,开始通过 capturer 进行视频采集
  [_delegate appClient:self didCreateLocalCapturer:capturer];
    
  return [_factory videoTrackWithSource:source trackId:kARDVideoTrackId];
}

3.5 发起方,create offer,获得 SDP

❸ 发起方创建 offer,callback 里拿到 SDP,然后调 delegate 的 didCreateSessionDescription 回调方法,delegate 里setLocalDescription,然后通过 signaling server(WebSocket)发送给 remote peer. setLocalDescription 之后,就启动了 ICE candidate gathering,gather 之后 delegate 就会收到 - (void)peerConnection:(RTCPeerConnection *)peerConnection didGenerateIceCandidate:(RTCIceCandidate *)candidate 方法的调用❺。

代码语言:javascript
复制
- (void)peerConnection:(RTCPeerConnection *)peerConnection
    didCreateSessionDescription:(RTCSessionDescription *)sdp
                          error:(NSError *)error {
  dispatch_async(dispatch_get_main_queue(), ^{
		//...省略错误检查
    __weak ARDAppClient *weakSelf = self;
		//设置 setLocalDescription
    [self.peerConnection setLocalDescription:sdp
                           completionHandler:^(NSError *error) {
                             ARDAppClient *strongSelf = weakSelf;
                             [strongSelf peerConnection:strongSelf.peerConnection
                                 didSetSessionDescriptionWithError:error];
                           }];
    ARDSessionDescriptionMessage *message =
        [[ARDSessionDescriptionMessage alloc] initWithDescription:sdp];
		//发送 SDP 到 remote peer
    [self sendSignalingMessage:message];
		//设置 video sender 的最大码率
    [self setMaxBitrateForPeerConnectionVideoSender];
  });
}

3.6 接收方 create answer,获得 SDP

❹ 接收方在 drainMessageQueueIfReady 方法里处理 WebSocket 的消息,如果有发起方发来的 Offer 消息的话,则创建 Answer 发给对方。

代码语言:javascript
复制
case kARDSignalingMessageTypeAnswer: {
      ARDSessionDescriptionMessage *sdpMessage =
          (ARDSessionDescriptionMessage *)message;
      RTCSessionDescription *description = sdpMessage.sessionDescription;
      __weak ARDAppClient *weakSelf = self;
			
			//把对方的 SDP 设置为 remote SDP
      [_peerConnection setRemoteDescription:description
                          completionHandler:^(NSError *error) {
                            ARDAppClient *strongSelf = weakSelf;
														//回调 delegate
                            [strongSelf peerConnection:strongSelf.peerConnection
                                didSetSessionDescriptionWithError:error];
                          }];
      break;
    }

接收方 create answer,并把 SDP 发送给发起方。

代码语言:javascript
复制

- (void)peerConnection:(RTCPeerConnection *)peerConnection
    didSetSessionDescriptionWithError:(NSError *)error {
  dispatch_async(dispatch_get_main_queue(), ^{
		//省略错误处理
    // If we're answering and we've just set the remote offer we need to create
    // an answer and set the local description.
    if (!self.isInitiator && !self.peerConnection.localDescription) {
      RTCMediaConstraints *constraints = [self defaultAnswerConstraints];
      __weak ARDAppClient *weakSelf = self;
			//创建 Answer
      [self.peerConnection answerForConstraints:constraints
                              completionHandler:^(RTCSessionDescription *sdp, NSError *error) {
                                ARDAppClient *strongSelf = weakSelf;
																//回调 delegate 的 didCreateSessionDescription 方法(和发起者创建完 offer 回调的方法一致,行为也一致,
																//首先 setLocalDescription,然后发送到 remote peer,然后设置最大发送码率)
                                [strongSelf peerConnection:strongSelf.peerConnection
                                    didCreateSessionDescription:sdp
                                                          error:error];
                              }];
    }
  });
}

- (void)peerConnection:(RTCPeerConnection *)peerConnection
    didCreateSessionDescription:(RTCSessionDescription *)sdp
                          error:(NSError *)error {
  dispatch_async(dispatch_get_main_queue(), ^{
		//...省略错误检查
    __weak ARDAppClient *weakSelf = self;
		//设置 setLocalDescription
    [self.peerConnection setLocalDescription:sdp
                           completionHandler:^(NSError *error) {
                             ARDAppClient *strongSelf = weakSelf;
                             [strongSelf peerConnection:strongSelf.peerConnection
                                 didSetSessionDescriptionWithError:error];
                           }];
    ARDSessionDescriptionMessage *message =
        [[ARDSessionDescriptionMessage alloc] initWithDescription:sdp];
		//发送 SDP 到 remote peer
    [self sendSignalingMessage:message];
		//设置 video sender 的最大码率
    [self setMaxBitrateForPeerConnectionVideoSender];
  });
}

3.7 获得 ICE candidate

didGenerateIceCandidate 回调里,不管事发起方还是接收方,都把获取到的 ICE candidate,通过 signaling server 发给 remote peer 即可, 对于获取到的 ICE candidate 无需做其他任何的处理。

代码语言:javascript
复制
- (void)peerConnection:(RTCPeerConnection *)peerConnection
    didGenerateIceCandidate:(RTCIceCandidate *)candidate {
  dispatch_async(dispatch_get_main_queue(), ^{
    ARDICECandidateMessage *message =
        [[ARDICECandidateMessage alloc] initWithCandidate:candidate];
    [self sendSignalingMessage:message];
  });
}

对方收到之后,添加到 peer connection 里,也无需做其他的处理。

代码语言:javascript
复制
- (void)processSignalingMessage:(ARDSignalingMessage *)message {
  NSParameterAssert(_peerConnection ||
      message.type == kARDSignalingMessageTypeBye);
  switch (message.type) {
    //...省略
    case kARDSignalingMessageTypeCandidate: {
      ARDICECandidateMessage *candidateMessage =
          (ARDICECandidateMessage *)message;
      [_peerConnection addIceCandidate:candidateMessage.candidate];
      break;
    }
    //...省略
  }
}

同理,对方在 setLocalDescription 之后,也开始 gather ICE candidate,并把收集到的 ICE candidate 发给对方。当两端都有了双方的 candidate,就开始做连通性检查,找到一条双方都可以通信的通道之后(这一部分底层帮我们做了,如果是 Web 端程序,是浏览器做的,如果对连通性检查这块感兴趣,可以参考 rfc-8445 - 2.2 Connectivity Checks (https://tools.ietf.org/html/rfc8445#section-2.2)),双方即可相互传递发送数据了,至此彼此也可以看到对方了。

如果发生异常情况,比如网络条件恶化,单方或者双方建议切换到低分辨率或者修改编码器,将会触发新的一轮 ICE candidate 的收集和交换。

Web 端上的整个过程和这个很相似,区别是采集阶段,获取摄像头和麦克风的输入调用的是 getUserMedia() 方法,app 里是用的 native 的方式,比如 iOS 是用 RTCCameraVideoCapturer 采集视频的(内部实现是通过 AVCaptureSession 来实现的)。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2019-02-242,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1. 相关概念
    • 1.1 NAT 穿越(NAT Traversal)
      • 1.2 STUN server
        • 1.3 TURN server
          • 1.4 ICE
            • 1.5 ICE candidate
              • 1.6 SDP
                • 1.7 Signaling Server
                • 2. 整体流程图
                • 3. 结合源码分析
                  • 3.1 创建 RTCPeerConnectionFactory
                    • 3.2 获得 TURN/STUN server 地址
                      • 3.3 加入房间,并连接上 WebSocket
                        • 3.4 创建 RTCPeerConnection 的实例
                          • 3.5 发起方,create offer,获得 SDP
                            • 3.6 接收方 create answer,获得 SDP
                              • 3.7 获得 ICE candidate
                              相关产品与服务
                              NAT 网关
                              NAT 网关(NAT Gateway)提供 IP 地址转换服务,为腾讯云内资源提供高性能的 Internet 访问服务。通过 NAT 网关,在腾讯云上的资源可以更安全的访问 Internet,保护私有网络信息不直接暴露公网;您也可以通过 NAT 网关实现海量的公网访问,最大支持1000万以上的并发连接数;NAT 网关还支持 IP 级流量管控,可实时查看流量数据,帮助您快速定位异常流量,排查网络故障。
                              领券
                              问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档