1. 程式人生 > >webrtc 點對點會話建立過程分析

webrtc 點對點會話建立過程分析

關於 webrtc 建立點對點連線的文章很多,其中都提到了如何利用 stun 伺服器獲取本機的公網地址,本文側重區域網(兩臺裝置之間可以直接 ping 通)下webrtc 點對點連線建立問題分析。

1.區域網內連線建立過程

瞭解過 webrtc 的都知道,要在公網上使用 webrtc 建立 p2p 連線,必須要有 stun 伺服器的支援才行,但在區域網內使用 webrtc 建立 p2p 連線,可以不需要 stun 伺服器,但是信令伺服器還是必須的。在區域網內,要獲取 IceCandidate,只需要獲取本機的地址和埠即可。除此之外,與在公網上建立 p2p 連線沒有什麼區別。
webrtc 區域網p2p連線

本文是通過 chromium 瀏覽器中的前端應用,來調起瀏覽器中內嵌的 webrtc,所以在分析過程中,會有涉及 chromium 和 webrtc 兩部分的程式碼。接下來會對 CreateAnswer 和 OnIceCandidate 的流程進行分析。

2. webrtc 訊號機制

webrtc 中大量採用了訊號機制,類似 QT 的訊號槽。後面的程式碼分析中不會顯示指出呼叫是否由訊號串起流程,所以這裡會先介紹訊號機制,後面很多地方都有用到。訊號機制舉例如下:
(1)定義訊號
D:\chromium\code\src\third_party\webrtc\p2p\base\portallocator.h

sigslot::signal2<PortAllocatorSession*,
                   const std::vector<Candidate>&> SignalCandidatesReady;

(2)繫結訊號
D:\chromium\code\src\third_party\webrtc\p2p\base\p2ptransportchannel.cc

void P2PTransportChannel::AddAllocatorSession(
    std::unique_ptr<PortAllocatorSession> session) {
...
  session->SignalCandidatesReady.connect(
      this, &P2PTransportChannel::OnCandidatesReady);
  ...
  }
  allocator_sessions_.push_back(std::move(session));

  // We now only want to apply new candidates that we receive to the ports
  // created by this new session because these are replacing those of the
  // previous sessions.
  PruneAllPorts();
}

在這個函式中聲明瞭 SignalCandidatesReady 的處理函式為 P2PTransportChannel::OnCandidatesReady()。當然,一個訊號可以有多個處理函式,也就是可以在多處進行繫結,一旦傳送訊號,多處的處理函式都會被調起。
(3)傳送訊號
D:\chromium\code\src\third_party\webrtc\p2p\client\basicportallocator.cc

void BasicPortAllocatorSession::OnCandidateReady(
    Port* port, const Candidate& c) {
...
  if (data->ready() && CheckCandidateFilter(c)) {
    std::vector<Candidate> candidates;
    candidates.push_back(SanitizeRelatedAddress(c));
    SignalCandidatesReady(this, candidates);
  } else {
    RTC_LOG(LS_INFO) << "Discarding candidate because it doesn't match filter.";
  }
...
}

3. CreateAnswer 流程

CreateAnswer 這個動作是應答端執行,用於生成該端的會話描述資訊,會話描述資訊主要包括:媒體型別、編解碼器、頻寬等元資料,下面給出一個 SDP 的示例:

v=0
o=- 6220557467521116672 2 IN IP4 127.0.0.1
s=-
t=0 0
a=group:BUNDLE audio video
a=msid-semantic: WMS
m=audio 9 UDP/TLS/RTP/SAVPF 111 103 104 9 0 8 106 105 13 110 112 113 126
c=IN IP4 0.0.0.0
a=rtcp:9 IN IP4 0.0.0.0
a=ice-ufrag:LjWt
a=ice-pwd:1/eNkEa0sLVOz0wm0krK7sot
a=ice-options:trickle
a=fingerprint:sha-256 85:2D:B2:69:9C:85:26:82:96:D5:87:C6:40:4B:DE:C5:CB:47:4E:06:57:20:88:1F:11:C4:B9:5A:7B:EB:D3:9A
a=setup:active
a=mid:audio
a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level
a=recvonly
a=rtcp-mux
a=rtpmap:111 opus/48000/2
a=rtcp-fb:111 transport-cc
a=fmtp:111 minptime=10;useinbandfec=1
a=rtpmap:103 ISAC/16000
a=rtpmap:104 ISAC/32000
a=rtpmap:9 G722/8000
a=rtpmap:0 PCMU/8000
a=rtpmap:8 PCMA/8000
a=rtpmap:106 CN/32000
a=rtpmap:105 CN/16000
a=rtpmap:13 CN/8000
a=rtpmap:110 telephone-event/48000
a=rtpmap:112 telephone-event/32000
a=rtpmap:113 telephone-event/16000
a=rtpmap:126 telephone-event/8000
m=video 9 UDP/TLS/RTP/SAVPF 96 97 98 99 100 101 127 123 125
c=IN IP4 0.0.0.0
a=rtcp:9 IN IP4 0.0.0.0
a=ice-ufrag:LjWt
a=ice-pwd:1/eNkEa0sLVOz0wm0krK7sot
a=ice-options:trickle
a=fingerprint:sha-256 85:2D:B2:69:9C:85:26:82:96:D5:87:C6:40:4B:DE:C5:CB:47:4E:06:57:20:88:1F:11:C4:B9:5A:7B:EB:D3:9A
a=setup:active
a=mid:video
a=extmap:2 urn:ietf:params:rtp-hdrext:toffset
a=extmap:3 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time
a=extmap:4 urn:3gpp:video-orientation
a=extmap:5 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01
a=extmap:6 http://www.webrtc.org/experiments/rtp-hdrext/playout-delay
a=extmap:7 http://www.webrtc.org/experiments/rtp-hdrext/video-content-type
a=extmap:8 http://www.webrtc.org/experiments/rtp-hdrext/video-timing
a=recvonly
a=rtcp-mux
a=rtcp-rsize
a=rtpmap:96 VP8/90000
a=rtcp-fb:96 goog-remb
a=rtcp-fb:96 transport-cc
a=rtcp-fb:96 ccm fir
a=rtcp-fb:96 nack
a=rtcp-fb:96 nack pli
a=rtpmap:97 rtx/90000
a=fmtp:97 apt=96
a=rtpmap:98 VP9/90000
a=rtcp-fb:98 goog-remb
a=rtcp-fb:98 transport-cc
a=rtcp-fb:98 ccm fir
a=rtcp-fb:98 nack
a=rtcp-fb:98 nack pli
a=rtpmap:99 rtx/90000
a=fmtp:99 apt=98
a=rtpmap:100 H264/90000
a=rtcp-fb:100 goog-remb
a=rtcp-fb:100 transport-cc
a=rtcp-fb:100 ccm fir
a=rtcp-fb:100 nack
a=rtcp-fb:100 nack pli
a=fmtp:100 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=420032
a=rtpmap:101 rtx/90000
a=fmtp:101 apt=100
a=rtpmap:127 red/90000
a=rtpmap:123 rtx/90000
a=fmtp:123 apt=127
a=rtpmap:125 ulpfec/90000

CreateAnswer 生成會話描述資訊的流程如下圖所示:
在這裡插入圖片描述
從流程可以看出, CreateAnswer 是由 js 程式碼發起的,其中 RTCPeerConnection 是瀏覽器提供的前端 api,之後傳入 webkit 處理,再進入瀏覽器的 renderer 程序處理,最後還是到了 webrtc 程式碼中執行真正的生成 SDP 的動作。在 src\third_party\webrtc\pc\mediasession.cc 這個檔案的 CreateAnswer 函式中,會確定本地支援的音視訊編解碼等具體資訊。
CreateOffer 執行的流程與上圖類似。

4. AddIceCandidate 流程

AddIceCandidate 是在對端發來了 Candidate 後,本地來新增儲存這些 candidate,candidate 資訊主要包括 IP 和埠號,以及所採用的協議型別等。Candidate 從前端發起新增流程到真正被儲存,這個過程的流程圖如下:
在這裡插入圖片描述
整個流程很長,最終遠端傳來的 Candidate 被儲存 P2PTransportChannel 中。

5. OnIceCandidate 流程

OnIceCandidate 代表收集本地 Candidate 的過程,起始於 PeerConnection::SetLocalDescription() 函式,具體啟動程式碼如下:

transport_controller_->MaybeStartGathering();

收集本地 Candidate 的執行流程如下,過程比較長,其中有些環節比較難以連貫起來,對這些環節後面會做簡單介紹。
在這裡插入圖片描述

分配埠
src\third_party\webrtc\p2p\client\basicportallocator.cc
void BasicPortAllocatorSession::DoAllocate() 中的 sequence->Start() 這句程式碼啟動針對某個網絡卡的 Candidate 進行收集,Start() 函式如下:

void AllocationSequence::Start() {
  state_ = kRunning;
  session_->network_thread()->Post(RTC_FROM_HERE, this, MSG_ALLOCATION_PHASE);
  // Take a snapshot of the best IP, so that when DisableEquivalentPhases is
  // called next time, we enable all phases if the best IP has since changed.
  previous_best_ip_ = network_->GetBestIP();
}

其中post 的原型位於檔案 src\jingle\glue\thread_wrapper.cc

void JingleThreadWrapper::Post(const rtc::Location& posted_from,
                               rtc::MessageHandler* handler,
                               uint32_t message_id,
                               rtc::MessageData* data,
                               bool time_sensitive)

AllocationSequence::Start() 中呼叫 Post 設定的 MessageHandler 為 this 指標,也就是 AllocationSequence 物件本身,因此呼叫會進入到 AllocationSequence::OnMessage() 函式中,AllocationSequence 的 phase_ 成員在物件建立時初始化為 0, 等於 PHASE_UDP ,所以首先會進入 PHASE_UDP 的處理過程,在處理完畢後,會呼叫:

if (state() == kRunning) {
    ++phase_;
    session_->network_thread()->PostDelayed(RTC_FROM_HERE,
                                            session_->allocator()->step_delay(),
                                            this, MSG_ALLOCATION_PHASE);
  } 

之後,會進入下一個 phase,也就是 PHASE_RELAY 。

上面的流程執行到最後,會一路回撥 OnIceCandidate() ,最終通過 webkit 傳遞到前端程式碼中。

6.小結

本文對 webrtc 建立連線過程中的一些步驟進行了具體分析,還有不完善的地方以後再補充吧。