同網頁的WebRTC實現與源碼分析

  • 基本按照Real time communication with WebRTC搭建(下面簡稱該網站爲官方tutorial)
  • 本文重視WebRTC的基於同頁面通訊的代碼實現,主要講述順序是WebRTC的三大API順序,一些原理、拓展的部分在連接和後續中

基本環境搭建

已有環境

  • Mac OS 10 & Windows 10 & Ubuntu 18.04 (均實現,WebRTC支持跨平臺)
  • Chrome 76 & Firefox
  • Webstorm IDE

搭建須要環境

下載源碼

git clone https://github.com/googlecodelabs/webrtc-web

getUserMedia

  • 源碼的Step01跑一下,瀏覽器獲取前置攝像頭就能成功,不展現具體效果了,看看源碼和一些其餘的應用html

    源碼分析

  • 源碼項目所給的代碼結構,可能是以下圖,因此常會看到js/main.js css/main.css這種src前端

proj_structure

  • 分析源碼關鍵調用部分
<!-- core src code of index.html -->
<head>
  <title>Realtime communication with WebRTC</title>
  <link rel="stylesheet" href="css/main.css" />
</head>

<body>
  <h1>Realtime communication with WebRTC</h1>
<!-- add video and script element in this .html file -->
  <video autoplay playsinline></video>
  <script src="js/main.js"></script>
</body>
/* core src code of main.css */
body {
  font-family: sans-serif;
}

video {
  max-width: 100%;
  width: 800px;
}
  • html css做爲標記型語言,瞭解其基本語法特徵與調用(我是經過閱讀DOM Sripting的前三章後比較清楚的,閱讀這部分還有一個好處是,把我不理解的簡潔代碼到頁面奇幻效果的轉化,推鍋給了DOM和瀏覽器廠商~),上面的兩個代碼就不難理解,着重分析下面js代碼
'use strict';

// On this codelab, you will be streaming only video (video: true).
const mediaStreamConstraints = {
  video: true,
};

// Video element where stream will be placed.
const localVideo = document.querySelector('video');

// Local stream that will be reproduced on the video.
let localStream;

// Handles success by adding the MediaStream to the video element.
function gotLocalMediaStream(mediaStream) {
  localStream = mediaStream;
  localVideo.srcObject = mediaStream;
}

// Handles error by logging a message to the console with the error message.
function handleLocalMediaStreamError(error) {
  console.log('navigator.getUserMedia error: ', error);
}

// Initializes media stream.
navigator.mediaDevices.getUserMedia(mediaStreamConstraints)
  .then(gotLocalMediaStream).catch(handleLocalMediaStreamError);
  • 在官方的tutorial中,對於代碼第一行就有解釋ECMAScript 5 Strict Mode, JSON, and More,能夠認爲是一種語法、異常檢查更嚴格的模式
  • 對於第3行以後的代碼部分,功能上能夠看做
    • 一個constraint只讀變量
    • gotLocalMediaStream() 處理視頻流函數
    • handleLocalMediaStreamError() 異常處理函數
    • getUserMedia() 調用
  • MediaDevices.getUserMedia()的API調用規則,也就大致明白了上面接近30行代碼的架構
navigator.mediaDevices.getUserMedia(constraints)
/* produces a MediaStream */
.then(function(stream) {
  /* use the stream */
})
.catch(function(err) {
  /* handle the error */
});
  • 在看第14-18行,how to use the mediaStream?
    • 先看mediaStream 的相關APIhtml5

      The MediaStream interface represents a stream of media content. A stream consists of several tracks such as video or audio tracks. Each track is specified as an instance of MediaStreamTracknode

    • 看代碼,從整個main.js 文件中,我沒有看出let localStream 有什麼特殊的用途,這一行註釋掉對網頁也沒有什麼影響(也許在以後的源碼中有用)git

    • 但17行的代碼就至關關鍵了(能夠把這一行的代碼註釋看看是個什麼效果~獲取了媒體流,可是網頁上沒有視頻顯示)github

    • 從第9行的const localVideo = document.querySelector('video') 提及
      • const 只讀變量
      • Document.querySelector() 理解這個函數,須要對DOM有一些認識
      • DOM(Document Object Model),既然是model就會有必定的邏輯表達形式,DOM文檔的表示就是一棵家譜樹
      • querySelector(selectors) 也正是基於樹形數據結構,來對document 中的 object 進行深度優先的前序遍歷,來獲取document 中符合selectorsHTMLElement 並返回web

        The matching is done using depth-first pre-order traversal of the document's nodes starting with the first element in the document's markup and iterating through sequential nodes by order of the number of child nodes.chrome

    • 17行的HTMLMediaElement.srcObject 則是對'video' 流媒體的賦值,使頁面顯示videoshell

getUserMedia()++

  • 在step01的demo裏,前置攝像頭的調用很是成功,但要刨根問底,step01中的代碼並無說明要調什麼攝像頭,什麼類型的視頻流(constraints裏面只要求video: true
  • 在官方tutorial裏面有Bonus points,回答理解這些問題來加深對getUserMedia() 的理解
  • 因爲不想把這篇博文寫的太長,上面兩個問題,都會在基於瀏覽器的WebRTC的getUserMedia()相關補充中補充說明

RTCPeerConnection

  • Let's move on to Step-02

源碼分析

HTML

<body>
  <h1>Realtime communication with WebRTC</h1>

  <video id="localVideo" autoplay playsinline></video>
  <video id="remoteVideo" autoplay playsinline></video>

  <div>
    <button id="startButton">Start</button>
    <button id="callButton">Call</button>
    <button id="hangupButton">Hang Up</button>
  </div>

  <script src="https://webrtc.github.io/adapter/adapter-latest.js"></script>
  <script src="js/main.js"></script>
</body>
  • 在HTML文檔中<head> 以及調用main.css 的部分和Step-01相比幾乎沒有改變
  • 新加入的video button scriptid & src命名都有很好的解釋說明效果,在下文對main.js 的分析中,相關內容會有更清楚的解釋
  • 代碼接近300行的樣子,按頁面的操做順序,分析一下相關代碼

三個button

  • 從三個button 開始,這是代碼183-192行
// Define and add behavior to buttons.

// Define action buttons.
const startButton = document.getElementById('startButton');
const callButton = document.getElementById('callButton');
const hangupButton = document.getElementById('hangupButton');

// Set up initial action buttons status: disable call and hangup.
callButton.disabled = true;
hangupButton.disabled = true;
  • 上面的代碼和querySelector()有相似功能,比較清晰
  • 代碼259-262行
// Add click event handlers for buttons.
startButton.addEventListener('click', startAction);
callButton.addEventListener('click', callAction);
hangupButton.addEventListener('click', hangupAction);

startAction()

  • startAction 開始
// Handles start button action: creates local MediaStream.
function startAction() {
  startButton.disabled = true;
  navigator.mediaDevices.getUserMedia(mediaStreamConstraints)
    .then(gotLocalMediaStream).catch(handleLocalMediaStreamError);
  trace('Requesting local stream.');
}
  • 一經頁面開啓startButton 只能click一次,以後獲取getUserMedia()
  • mediaStreamConstraints() 函數幾乎沒有變化,gotLocalMediaStream() & handleLocalMediaStreamError() 有些許變化,在19-43部分行
// Define peer connections, streams and video elements.
const localVideo = document.getElementById('localVideo');
const remoteVideo = document.getElementById('remoteVideo');

let localStream;
let remoteStream;

// Define MediaStreams callbacks.

// Sets the MediaStream as the video element src.
function gotLocalMediaStream(mediaStream) {
  localVideo.srcObject = mediaStream;
  localStream = mediaStream;
  trace('Received local stream.');
  callButton.disabled = false;  // Enable call button.
}

// Handles error by logging a message to the console.
function handleLocalMediaStreamError(error) {
  trace(`navigator.getUserMedia error: ${error.toString()}.`);
}
  • 代碼的總體邏輯很是清晰,惟獨新加入的trace() 函數比較新穎,看看
// Logs an action (text) and the time when it happened on the console.
function trace(text) {
  text = text.trim();
  const now = (window.performance.now() / 1000).toFixed(3);

  console.log(now, text);
}

callAction()

  • 再看callAction() ,代碼203-246行
// Code from line 16-17
// Define initial start time of the call (defined as connection between peers).
let startTime = null;

// Code from line19-27
// Define peer connections
let localPeerConnection;
let remotePeerConnection;

// Handles call button action: creates peer connection.
function callAction() {
  callButton.disabled = true;   // disenable call button
  hangupButton.disabled = false;    // enable hangup button

  trace('Starting call.');
  startTime = window.performance.now(); // assign startTime with concrete time

  // Get local media stream tracks.
  const videoTracks = localStream.getVideoTracks();
  const audioTracks = localStream.getAudioTracks();
  if (videoTracks.length > 0) {
    trace(`Using video device: ${videoTracks[0].label}.`);
  }
  if (audioTracks.length > 0) {
    trace(`Using audio device: ${audioTracks[0].label}.`);
  }

  const servers = null;  // Allows for RTC server configuration.

  // Create peer connections and add behavior.
  localPeerConnection = new RTCPeerConnection(servers);
  trace('Created local peer connection object localPeerConnection.');

  localPeerConnection.addEventListener('icecandidate', handleConnection);
  localPeerConnection.addEventListener(
    'iceconnectionstatechange', handleConnectionChange);

  remotePeerConnection = new RTCPeerConnection(servers);
  trace('Created remote peer connection object remotePeerConnection.');

  remotePeerConnection.addEventListener('icecandidate', handleConnection);
  remotePeerConnection.addEventListener(
    'iceconnectionstatechange', handleConnectionChange);
  remotePeerConnection.addEventListener('addstream', gotRemoteMediaStream);

  // Add local stream to connection and create offer to connect.
  localPeerConnection.addStream(localStream);
  trace('Added local stream to localPeerConnection.');

  trace('localPeerConnection createOffer start.');
  localPeerConnection.createOffer(offerOptions)
    .then(createdOffer).catch(setSessionDescriptionError);
}
  • 按上文代碼的函數,從第28行開始,就是極其關鍵的RTCPeerConnection,解析下面所說的三個步驟,以創建鏈接時序展開

Setting up a call between WebRTC peers involves three tasks:

  • Create a RTCPeerConnection for each end of the call and, at each end, add the local stream from getUserMedia().
  • Get and share network information: potential connection endpoints are known as ICE candidates.
  • Get and share local and remote descriptions: metadata about local media in SDP format.

RTCPeerConnection關鍵部分——Local & Remote peer創建

  • getUserMedia() 部分,再也不贅述
let localPeerConnection;
  const servers = null;  // Allows for RTC server configuration. This is where you could specify STUN and TURN servers.

  // Create peer connections and add behavior.
  localPeerConnection = new RTCPeerConnection(servers);
  
  remotePeerConnection = new RTCPeerConnection(servers);
  • 關於servers,官網tutorial給了一篇說明WebRTC in the real world: STUN, TURN and signaling,這個我也會在隨着項目系統通訊搭建的深刻,學習實踐到servers層面再記錄
  • 能夠認爲在上述代碼以後,一個RTCPeerConnection的端就實例化成功了
// Add local stream to connection and create offer to connect.
  localPeerConnection.addStream(localStream);
  trace('Added local stream to localPeerConnection.');
  • addStream() 以後,能夠認爲Local & Remote Peer已經所有建好(RTCPeerConnection實例化成功,media傳輸也能夠開始進行)

RTCPeerConnection關鍵部分——ICE candidate創建

localPeerConnection.addEventListener('icecandidate', handleConnection);
  localPeerConnection.addEventListener(
    'iceconnectionstatechange', handleConnectionChange);
  • addEventListener() method在button相關中已經瞭解,關於'icecandidate' Event,看RTCPeerConnection: icecandidate event,而其中的setLocalDescription()在下面一個section中有介紹
  • 這一部分,須要對計算機網絡有一些瞭解,以及對WebRTC signaling的過程爛熟於心,我初學是很是費解的,探索後其中內容解釋在WebRTC的RTCPeerConnection()原理探析(連接中文章重原理,這篇重視基礎的代碼實現)
  • 一樣Remote Peer的創建也是相似
remotePeerConnection.addEventListener('icecandidate', handleConnection);
  remotePeerConnection.addEventListener(
    'iceconnectionstatechange', handleConnectionChange);
  remotePeerConnection.addEventListener('addstream', gotRemoteMediaStream);
  • 繼續看ICE candidate創建過程當中用到的三個函數
// Connects with new peer candidate.
function handleConnection(event) {
  const peerConnection = event.target;
  const iceCandidate = event.candidate;

  if (iceCandidate) {
    const newIceCandidate = new RTCIceCandidate(iceCandidate);
    const otherPeer = getOtherPeer(peerConnection);

    otherPeer.addIceCandidate(newIceCandidate)
      .then(() => {
        handleConnectionSuccess(peerConnection);
      }).catch((error) => {
        handleConnectionFailure(peerConnection, error);
      });

    trace(`${getPeerName(peerConnection)} ICE candidate:\n` +
          `${event.candidate.candidate}.`);
  }
}

// Logs changes to the connection state.
function handleConnectionChange(event) {
  const peerConnection = event.target;
  console.log('ICE state change event: ', event);
  trace(`${getPeerName(peerConnection)} ICE state: ` +
        `${peerConnection.iceConnectionState}.`);
}

// Handles remote MediaStream success by adding it as the remoteVideo src.
function gotRemoteMediaStream(event) {
  const mediaStream = event.stream;
  remoteVideo.srcObject = mediaStream;
  remoteStream = mediaStream;
  trace('Remote peer connection received remote stream.');
}
  • 我猜想前兩個函數,是針對於本機連本機的特殊應用搭建的,不具備廣泛性,因此不具體分析
  • gotRemoteMediaStream() 函數,最終將Local Peer的addStream() 顯示
  • 還有一個API值得看一下,就是RTCPeerConnection.addIceCandidate()

RTCPeerConnection關鍵部分——Get and share local and remote descriptions

  • 開啓一個SDP offer,以進行遠程鏈接
trace('localPeerConnection createOffer start.');
  localPeerConnection.createOffer(offerOptions)
    .then(createdOffer).catch(setSessionDescriptionError);
// Set up to exchange only video.
const offerOptions = {
  offerToReceiveVideo: 1,
};

// Logs offer creation and sets peer connection session descriptions.
function createdOffer(description) {
  trace(`Offer from localPeerConnection:\n${description.sdp}`);

  trace('localPeerConnection setLocalDescription start.');
  localPeerConnection.setLocalDescription(description)
    .then(() => {       // The parameter list for a function with no parameters should be written with a pair of parentheses.
      setLocalDescriptionSuccess(localPeerConnection);
      // just logs successful info on the console
    }).catch(setSessionDescriptionError);
}

// Logs error when setting session description fails.
function setSessionDescriptionError(error) {
  trace(`Failed to create session description: ${error.toString()}.`);
}
  • 解釋一下createOffer()函數
  • 先看setLocalDescriptionAPI中也沒有講的特別清楚,簡單的說,能夠認爲這個函數通過調用後,Local Peer的offer就發送成功(可參見RTCPeerConnection.signalingState),但實際上發送的信息是什麼、向誰發...等一系列問題,都是在官方教程中的源碼裏面未涉及的,這部分我寫在了WebRTC的RTCPeerConnection()原理探析
  • Local Peer已經提供了offer,來而不往非禮也,下面就是Remote Peer的迴應了
trace('remotePeerConnection setRemoteDescription start.');
  remotePeerConnection.setRemoteDescription(description)
    .then(() => {
      setRemoteDescriptionSuccess(remotePeerConnection);
    }).catch(setSessionDescriptionError);

  trace('remotePeerConnection createAnswer start.');
  remotePeerConnection.createAnswer()
    .then(createdAnswer)
    .catch(setSessionDescriptionError);

function createdAnswer(description) {
  trace(`Answer from remotePeerConnection:\n${description.sdp}.`);

  trace('remotePeerConnection setLocalDescription start.');
  remotePeerConnection.setLocalDescription(description)
    .then(() => {
      setLocalDescriptionSuccess(remotePeerConnection);
    }).catch(setSessionDescriptionError);
    
    trace('localPeerConnection setRemoteDescription start.');
  localPeerConnection.setRemoteDescription(description)
    .then(() => {
      setRemoteDescriptionSuccess(localPeerConnection);
    }).catch(setSessionDescriptionError);
}
  • Remote Peer的createAnswer()的API以及setRemoteDescriptionAPI,Local Peer與Remote Peer之間的互相通訊基本創建了
  • 在Google Dev Tools Console裏面截取了一張SDP的圖片,感受比較複雜,以前有作WebRTC底層優化的準備,如今以爲...可能在十分十分須要的時候纔會去作QAQ

WebRTC_SDP

hangupAction()

  • 最後來看hangup button對應什麼函數
// Handles hangup action: ends up call, closes connections and resets peers.
function hangupAction() {
  localPeerConnection.close();
  remotePeerConnection.close();
  localPeerConnection = null;
  remotePeerConnection = null;
  hangupButton.disabled = true;
  callButton.disabled = false;
  trace('Ending call.');
}
  • 很是清晰易懂,不解釋

如何PC 2 PC

  • 源碼分析終於分析完了~
  • 但還有一些問題,源碼中的網頁本地P2P通訊如何改成PC 2 PC的通訊?這個我記錄在原理一文中

RTCDataChannel

  • RTCPeerConnection部分須要寫的實在太多了,到這裏,全文長度已經超過3000.orz...這部分腳步代碼量略少一些,也儘可能寫的簡潔一點,其他拓展見補充
  • 仍是從源碼分析開始

源碼分析

HTML

  • HTML代碼部分較RTCPeerConnection部分,增長了兩個文本區
<textarea id="dataChannelSend" disabled
    placeholder="Press Start, enter some text, then press Send."></textarea>
  <textarea id="dataChannelReceive" disabled></textarea>
  • 標記性語言,語法、效果很是容易理解,可見
  • 在HTML語言中,咱們也看到了和上一節相似的三個button,仍是按button順序來分析

三個button

  • 此次三個button的寫法較上一節的有比較新奇的改變
var startButton = document.querySelector('button#startButton');
var sendButton = document.querySelector('button#sendButton');
var closeButton = document.querySelector('button#closeButton');

startButton.onclick = createConnection;
sendButton.onclick = sendData;
closeButton.onclick = closeDataChannels;
  • 首先是querySelector()括號裏面的內容很是有範式,因此查到一個參考連接CSS 選擇器,而後onclick method也是一種很是簡潔的寫法
  • 下面看三個button對應的功能

startButton

var localConnection;
var remoteConnection;
var sendChannel;
var dataConstraint;
var dataChannelSend = document.querySelector('textarea#dataChannelSend');

// Offerer side 
function createConnection() {
  dataChannelSend.placeholder = '';
  var servers = null;
  pcConstraint = null;
  dataConstraint = null;
  trace('Using SCTP based data channels');
  // For SCTP, reliable and ordered delivery is true by default.
  // Add localConnection to global scope to make it visible
  // from the browser console.
  window.localConnection = localConnection =
      new RTCPeerConnection(servers, pcConstraint); // constructor
  trace('Created local peer connection object localConnection');

  sendChannel = localConnection.createDataChannel('sendDataChannel',
      dataConstraint);
  trace('Created send data channel');

  localConnection.onicecandidate = iceCallback1;
  sendChannel.onopen = onSendChannelStateChange;
  sendChannel.onclose = onSendChannelStateChange;

  // Add remoteConnection to global scope to make it visible
  // from the browser console.
  window.remoteConnection = remoteConnection =
      new RTCPeerConnection(servers, pcConstraint);
  trace('Created remote peer connection object remoteConnection');

  remoteConnection.onicecandidate = iceCallback2;
  remoteConnection.ondatachannel = receiveChannelCallback;

  localConnection.createOffer().then(
    gotDescription1,
    onCreateSessionDescriptionError
  );
  startButton.disabled = true;
  closeButton.disabled = false;
}

function iceCallback1(event) {
  trace('local ice callback');
  if (event.candidate) {
    remoteConnection.addIceCandidate(
      event.candidate
    ).then(
      onAddIceCandidateSuccess,
      onAddIceCandidateError
    );
    trace('Local ICE candidate: \n' + event.candidate.candidate);
  }
}

function iceCallback2(event) {
  trace('remote ice callback');
  if (event.candidate) {
    localConnection.addIceCandidate(
      event.candidate
    ).then(
        // print out info on the console
      onAddIceCandidateSuccess,
      onAddIceCandidateError
    );
    trace('Remote ICE candidate: \n ' + event.candidate.candidate);
  }
}

function onSendChannelStateChange() {
  var readyState = sendChannel.readyState;
  trace('Send channel state is: ' + readyState);
  if (readyState === 'open') {
    dataChannelSend.disabled = false;
    dataChannelSend.focus();
    sendButton.disabled = false;
    closeButton.disabled = false;
  } else {
    dataChannelSend.disabled = true;
    sendButton.disabled = true;
    closeButton.disabled = true;
  }
}
  • 先看幾個API,RTCPeerConnection.createDataChannel()

    The createDataChannel() method on the RTCPeerConnection interface creates a new channel linked with the remote peer, over which any kind of data may be transmitted. This can be useful for back-channel content such as images, file transfer, text chat, game update packets, and so forth.

  • RTCPeerConnection.onicecandidate

    This happens whenever the local ICE agent needs to deliver a message to the other peer through the signaling server. This lets the ICE agent perform negotiation with the remote peer without the browser itself needing to know any specifics about the technology being used for signaling; simply implement this method to use whatever messaging technology you choose to send the ICE candidate to the remote peer.

  • RTCIceCandidate.candidate

The read-only property candidate on the RTCIceCandidate interface returns a DOMString describing the candidate in detail. Most of the other properties of RTCIceCandidate are actually extracted from this string.

When a web site or app using RTCPeerConnection receives a new ICE candidate from the remote peer over its signaling channel, it delivers the newly-received candidate to the browser's ICE agent by calling RTCPeerConnection.addIceCandidate(). This adds this new remote candidate to the RTCPeerConnection's remote description, which describes the state of the remote end of the connection.

The RTCDataChannel.onopen property is an EventHandler which specifies a function to be called when the open event is fired; this is a simple Event which is sent when the data channel's underlying data transport—the link over which the RTCDataChannel's messages flow—is established or re-established.

  • HTMLElement.focus(),這個功能比我想的有趣,並且咱們在平時使用瀏覽器的時候,碰見的特別多~可是,有一點問題,就是基於DOM的Web前端開發太「高級」了,以致於我看不到底層的實現...

The HTMLElement.focus() method sets focus on the specified element, if it can be focused. The focused element is the element which will receive keyboard and similar events by default.

  • 以後的部分就是和RTCPeerConnection相相似的createOffer() createAnswer()部分,在學習完RTCPeerConnection以後,是很是容易的,因此不贅述

sendButton

function sendData() {
  var data = dataChannelSend.value;
  sendChannel.send(data);
  trace('Sent Data: ' + data);
}
  • 基於上一步創建的鏈接上,實現data傳輸功能

closeButton

function closeDataChannels() {
  trace('Closing data channels');
  sendChannel.close();
  trace('Closed data channel with label: ' + sendChannel.label);
  receiveChannel.close();
  trace('Closed data channel with label: ' + receiveChannel.label);
  localConnection.close();
  remoteConnection.close();
  localConnection = null;
  remoteConnection = null;
  trace('Closed peer connections');
  startButton.disabled = false;
  sendButton.disabled = true;
  closeButton.disabled = true;
  dataChannelSend.value = '';
  dataChannelReceive.value = '';
  dataChannelSend.disabled = true;
  disableSendButton();
  enableStartButton();
}

function enableStartButton() {
  startButton.disabled = false;
}

function disableSendButton() {
  sendButton.disabled = true;
}
  • 這部分基本上也是明晰的,不贅述

And then...

  • 關於WebRTC官網代碼三大模塊同一網頁實現的分析,就寫到這個地方,也寫了不少不少了,換一個文本繼續寫跨PC的WebRTC實現,請見PC 2 PC的WebRTC實現
相關文章
相關標籤/搜索