webrtc 会话建立过程

  • Post author:
  • Post category:其他


本文所指的 webrtc 代码位于 chromium(64) 的第三方仓库中,webrtc 更新比较频繁,所以不同的版本代码可能改动较大。

1. 会话建立标准流程(需要 NAT 穿透)

在通过 webrtc 实现点对点的连接时,需要遵循如下流程,

图片来自


这里写图片描述

1. Client A 创建一个 PeerConnection 对象,然后打开本地音视频设备,将音视频数据封装成 MediaStream 添加到 PeerConnection 中;

2. Client A 调用 PeerConnection 的CreateOffer 方法创建一个 offer 的 SDP 对象,SDP 对象中保存当前音视频的相关参数;

3. Client A 调用 SetLocalDescription 方法将 SDP 保存起来;

4. Client A 通过信令机制将自己的 SDP 发送给 Client B;

5. Client B 接收到 Client A 发送过来的 offer SDP对象,通过自己的 PeerConnection 的 SetRemoteDescription 方法将其保存起来;

6. Client B 调用 PeerConnection 的 CreateAnswer 方法创建一个应答的 SDP 对象;

7. Client B 调用 SetLocalDescription 方法保存 answer SDP 对象;

8. Client B 通过信令机制将自己的 SDP 发送给 Client A;

9. Client A 调用 PeerConnection 对象的 SetRemoteDescription 方法保存 Client B 发来的 SDP;

10. 在 SDP 信息的 offer/answer 流程中,Client A 和Client B 已经根据 SDP 信息创建好相应的音频Channel和视频Channel 并开启 Candidate 数据的收集,Candidate 数据可以简单地理解成 Client 端的 IP 地址信息(本地 IP 地址、公网IP地址、Relay 服务端分配的地址);

11. Client A 收集到自己的 Candidate 信息后,PeerConnection 会通过 OnIceCandidate 接口给 Client A 发送通知;

12. Client A 将收集到的 Candidate 信息通过信令机制发送给 Client B;

13. Client B 通过 PeerConnection 的 AddIceCandidate 方法将 Client A 的 Candidate 保存起来;

14. Client B 收集自己的 Candidate 信息,PeerConnection 通过 OnIceCandidate 接口给 Client B 发送通知;

15. Client B 将收集到的 Candidate 信息通过信令机制发送给 Client A;

16. Client A 通过 PeerConnection 的 AddIceCandidate 方法将 Client B Candidate 保存起来。

至此,Client A 和 Client B 就建立了点对点的连接。

备注:通信双方都必须有自己独立的 PeerConnection 对象。

2.接收端会话建立相关代码


Client B 设置远端会话描述信息

void PeerConnection::SetRemoteDescription(
    std::unique_ptr<SessionDescriptionInterface> desc,
    rtc::scoped_refptr<SetRemoteDescriptionObserverInterface> observer) {
  ...
  // Find all audio rtp streams and create corresponding remote AudioTracks
  // and MediaStreams.
  if (audio_content) {
    if (audio_content->rejected) {
      RemoveSenders(cricket::MEDIA_TYPE_AUDIO);
    } else {
      bool default_audio_track_needed =
          !remote_peer_supports_msid_ &&
          MediaContentDirectionHasSend(audio_desc->direction());
      UpdateRemoteSendersList(GetActiveStreams(audio_desc),
                              default_audio_track_needed, audio_desc->type(),
                              new_streams);
    }
  }
  ...
}

其中,UpdateRemoteSendersList 函数用于添加远端媒体流的信息。

rtc::scoped_refptr<MediaStreamInterface> stream = 
        remote_streams_->find(stream_label);
    if (!stream) {
      // This is a new MediaStream. Create a new remote MediaStream.
      stream = MediaStreamProxy::Create(rtc::Thread::Current(),
                                        MediaStream::Create(stream_label));
      remote_streams_->AddStream(stream);
      new_streams->AddStream(stream);
    }

如果在 remote_streams_ 变量里没有找到 stream_label 标记的流,那么说明新到来的 会话描述信息尚未记录,需要根据 stream_label 创建一个新的流,并添加到 remote_streams_ 里。


Client B 创建自己的会话描述信息


Client B 作为接受连接的一端,通过 PeerConnection 的 CreateAnswer 方法开始创建自己的会话描述信息,代码位置src\third_party\webrtc\pc\peerconnection.cc ,代码如下:

void PeerConnection::CreateAnswer(CreateSessionDescriptionObserver* observer,
                                  const RTCOfferAnswerOptions& options) {
  ...

  webrtc_session_desc_factory_->CreateAnswer(observer, session_options);
}

函数内部 CreateAnswer 调用 WebRtcSessionDescriptionFactory 类的 CreateAnswer 方法,跟踪下去,最终进入 MediaSessionDescriptionFactory 的 CreateAnswer 方法完成最终的 answer SDP 创建。


Client B 发送自己的 answer SDP


在完成 answer SDP 创建后,会通过信令机制将 answer SDP 发送给 Client A,代码位置:src\third_party\webrtc\pc\webrtcsessiondescriptionfactory.cc,发送的代码如下:

void WebRtcSessionDescriptionFactory::InternalCreateAnswer(
    CreateSessionDescriptionRequest request) {
  ...

  cricket::SessionDescription* desc(session_desc_factory_.CreateAnswer(
      pc_->remote_description() ? pc_->remote_description()->description()
                                : nullptr,
      request.options,
      pc_->local_description() ? pc_->local_description()->description()
                               : nullptr));
  ...

  // 完成 answer SDP 的创建,并通过信令机制发送给 Client A
  PostCreateSessionDescriptionSucceeded(request.observer, answer);
}

进入 PostCreateSessionDescriptionSucceeded 函数,其实现为:

void WebRtcSessionDescriptionFactory::PostCreateSessionDescriptionSucceeded(
    CreateSessionDescriptionObserver* observer,
    SessionDescriptionInterface* description) {
  CreateSessionDescriptionMsg* msg = new CreateSessionDescriptionMsg(observer);
  msg->description.reset(description);
  signaling_thread_->Post(RTC_FROM_HERE, this,
                          MSG_CREATE_SESSIONDESCRIPTION_SUCCESS, msg);
}


Client B 将 Client A 的 candidate 信息保存起来

bool PeerConnection::AddIceCandidate(
    const IceCandidateInterface* ice_candidate) {
   ...
    // Add this candidate to the remote session description.
  if (!mutable_remote_description()->AddCandidate(ice_candidate)) {
    RTC_LOG(LS_ERROR) << "ProcessIceMessage: Candidate cannot be used.";
    return false;
  }
  ...
}

真正执行添加 Candidate 操作的是 JsepSessionDescription 的 AddCandidate 方法:

bool JsepSessionDescription::AddCandidate(
    const IceCandidateInterface* candidate) {
...
  std::unique_ptr<JsepIceCandidate> updated_candidate_wrapper(
      new JsepIceCandidate(candidate->sdp_mid(),
                           static_cast<int>(mediasection_index),
                           updated_candidate));
  if (!candidate_collection_[mediasection_index].HasCandidate(
          updated_candidate_wrapper.get())) {
    candidate_collection_[mediasection_index].add(
        updated_candidate_wrapper.release());
    UpdateConnectionAddress(
        candidate_collection_[mediasection_index],
        description_->contents()[mediasection_index].description);
  }
...
}

在添加 Client A 发送过来的 Candidate 信息后,执行 UpdateConnectionAddress 方法:

// Update the connection address for the MediaContentDescription based on the
// candidates.
static void UpdateConnectionAddress(
    const JsepCandidateCollection& candidate_collection,
    cricket::ContentDescription* content_description) {
...
rtc::SocketAddress connection_addr;
  connection_addr.SetIP(ip);
  connection_addr.SetPort(port);
  static_cast<cricket::MediaContentDescription*>(content_description)
      ->set_connection_address(connection_addr);
...
}

3. stun服务器

3.1 非同一局域网

如果两台机器处于不同的局域网中,要实现点对点的通信,必须借助于 stun 服务器,webrtc 中给出的例子(src\third_party\webrtc\examples\peerconnection\client\defaults.cc)中给出了一个 stun 服务器的网址,在可以连接外网的情况下能够使用。

stun:stun.l.google.com:19302

网上有网友整理了可用的

stun 服务器

地址,粘贴如下:

{url:'stun:stun01.sipphone.com'},
{url:'stun:stun.ekiga.net'},
{url:'stun:stun.fwdnet.net'},
{url:'stun:stun.ideasip.com'},
{url:'stun:stun.iptel.org'},
{url:'stun:stun.rixtelecom.se'},
{url:'stun:stun.schlund.de'},
{url:'stun:stun.l.google.com:19302'},
{url:'stun:stun1.l.google.com:19302'},
{url:'stun:stun2.l.google.com:19302'},
{url:'stun:stun3.l.google.com:19302'},
{url:'stun:stun4.l.google.com:19302'},
{url:'stun:stunserver.org'},
{url:'stun:stun.softjoys.com'},
{url:'stun:stun.voiparound.com'},
{url:'stun:stun.voipbuster.com'},
{url:'stun:stun.voipstunt.com'},
{url:'stun:stun.voxgratia.org'},
{url:'stun:stun.xten.com'},
{
    url: 'turn:numb.viagenie.ca',
    credential: 'muazkh',
    username: 'webrtc@live.com'
},
{
    url: 'turn:192.158.29.39:3478?transport=udp',
    credential: 'JZEOEt2V3Qb0y27GRntt2u2PAYA=',
    username: '28224511:1379330808'
},
{
    url: 'turn:192.158.29.39:3478?transport=tcp',
    credential: 'JZEOEt2V3Qb0y27GRntt2u2PAYA=',
    username: '28224511:1379330808'
}

3.2 同一局域网

当两台机器处于同一局域网中时,不需要借助 stun 服务器,信令交换后就已经可以传递媒体流了。

参考:


Android IOS WebRTC 音视频开发总结(九)– webrtc入门001



WebRTC手记之初探



P2P通信标准协议(三)之ICE



WebRTC 实现Android点到点互连(含Demo)



版权声明:本文为zhuiyuanqingya原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。