使用 WebRTC 進行即時通訊

1. 簡介

WebRTC 是開放原始碼專案,可針對網頁和原生應用程式中的音訊、影片和資料進行即時通訊。

WebRTC 有多個 JavaScript API,點選連結即可查看示範。

我可以在哪裡使用 WebRTC?

Firefox、Opera 以及電腦版和 Android 版的 Chrome。iOS 和 Android 上的原生應用程式也支援 WebRTC。

什麼是訊號?

WebRTC 會使用 RTCPeerConnection 在瀏覽器之間通訊串流資料,但也需要使用機制協調通訊及傳送控制訊息 (稱為信號的程序)。WebRTC 不會指定訊號方法和通訊協定。在本程式碼研究室中,您將使用 Socket.IO 傳送訊息,但有許多替代方案

什麼是「STUN」和「TURN」?

WebRTC 經過特別設計,能以點對點方式運作,因此使用者可透過可能的最直接路徑進行連線。不過,WebRTC 可與真實世界的網路相輔相成:用戶端應用程式需要穿越 NAT 閘道和防火牆,且對等互連至對等互連網路需要備用連線,以防直接連線失敗。在這個程序中,WebRTC API 會使用 STUN 伺服器取得電腦的 IP 位址,而 TURN 伺服器則做為轉發伺服器,以防點對點通訊失敗。(詳情請參閱實際環境中的 WebRTC)。

WebRTC 是否安全?

所有 WebRTC 元件都必須加密,且其中的 JavaScript API 只能透過安全來源 (HTTPS 或 localhost) 使用。信號機制並非 WebRTC 標準定義,因此你必須確認使用安全通訊協定。

2. 總覽

建構應用程式來取得使用網路攝影機拍攝的影片及拍攝快照,並透過 WebRTC 以點對點方式分享這些內容。在過程中,您將瞭解如何使用核心 WebRTC API,以及使用 Node.js 設定訊息傳遞伺服器。

課程內容

  • 從網路攝影機取得視訊
  • 透過 RTCPeerConnection 串流播放影片
  • 使用 RTCDataChannel 串流資料
  • 設定用來交換訊息的訊號服務
  • 結合對等互連連線與信號功能
  • 拍攝相片,並透過資料管道分享

軟硬體需求

  • Chrome 47 以上版本
  • Chrome 版網路伺服器,或使用自己的網路伺服器。
  • 程式碼範例
  • 文字編輯器
  • 具備 HTML、CSS 和 JavaScript 的基本知識

3. 取得程式碼範例

下載程式碼

如果您熟悉 Git,可以複製程式碼,從 GitHub 下載本程式碼研究室的程式碼:

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

或者,您也可以點選下方按鈕來下載程式碼的 .zip 檔案:

開啟下載的 ZIP 檔案。這麼做會將專案資料夾 (Adaptive-web-media) 解壓縮,其中包含本程式碼研究室中每個步驟適用的一個資料夾,以及所有必要資源。

您將在名為 work 的目錄中執行所有程式碼工作。

step-nn 資料夾包含本程式碼研究室每個步驟中完成的版本。這些資訊僅供參考。

安裝並驗證網路伺服器

雖然你可以免費使用自己的網路伺服器,但本程式碼研究室適用於 Chrome 網路伺服器。如果你尚未安裝這個應用程式,可以前往 Chrome 線上應用程式商店進行安裝。

6ddeb4aee53c0f0e.png

安裝 Chrome 網路伺服器應用程式後,請在書籤列、新分頁或應用程式啟動器中按一下 Chrome 應用程式捷徑:

1d2b4aa977ab7e24.png

按一下「網路伺服器」圖示:

27fce4494f641883.png

接下來,系統會顯示這個對話方塊,讓您設定本機網路伺服器:

Screen Shot 2016-02-18 at 11.48.14 AM.png

按一下「選取資料夾」按鈕,然後選取剛才建立的「work」資料夾。這樣就能透過「網路伺服器網址」部分,醒目顯示的網址在 Chrome 中查看處理中的工作。

在「Options」下方,勾選「自動顯示 index.html」旁的方塊,如下所示:

Screen Shot 2016-02-18 at 11.56.30 AM.png

接著,將標示「Web Server: STARTED」(網路伺服器:STARTED) 的切換鈕滑動至左側,再回到右側,藉此停止並重新啟動伺服器。

Screen Shot 2016-02-18 at 12.22.18 PM.png

現在,按一下醒目顯示的網路伺服器網址,透過網路瀏覽器造訪你的工作網站。您應該會看到與 work/index.html 對應的頁面,如下所示:

18a705cb6ccc5181.png

顯然這個應用程式並沒有什麼效果。目前為止,這只是我們用來確認網路伺服器運作是否正常的最基本架構。您將在後續步驟中新增功能和版面配置功能。

4. 串流播放網路攝影機影片

課程內容

這個步驟將說明如何:

  • 從您的網路攝影機取得影片串流。
  • 操控串流播放。
  • 使用 CSS 和 SVG 操控影片。

此步驟的完整版本位於 step-01 資料夾中。

點號 HTML...

video 元素和 script 元素新增至 work 目錄中的 index.html

<!DOCTYPE html>
<html>

<head>

  <title>Realtime communication with WebRTC</title>

  <link rel="stylesheet" href="css/main.css" />

</head>

<body>

  <h1>Realtime communication with WebRTC</h1>

  <video autoplay playsinline></video>

  <script src="js/main.js"></script>

</body>

</html>

...還有一小群 JavaScript

將以下內容新增至 js 資料夾的 main.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);

立即試用

在瀏覽器中開啟 index.html,畫面應如下所示 (當然是透過網路攝影機拍攝畫面!):

9297048e43ed0f3d.png

運作方式

getUserMedia() 呼叫之後,瀏覽器會要求使用者授予相機存取權 (如果該是第一次針對目前來源要求存取攝影機)。如果成功,系統會傳回 MediaStream,讓媒體元素可透過 srcObject 屬性使用:

navigator.mediaDevices.getUserMedia(mediaStreamConstraints)
  .then(gotLocalMediaStream).catch(handleLocalMediaStreamError);


}
function gotLocalMediaStream(mediaStream) {
  localVideo.srcObject = mediaStream;
}

constraints 引數可讓您指定要取得的媒體。本範例僅包含影片,因為音訊預設為停用:

const mediaStreamConstraints = {
  video: true,
};

您可以設定一些其他需求條件,例如影片解析度:

const hdConstraints = {
  video: {
    width: {
      min: 1280
    },
    height: {
      min: 720
    }
  }
}

MediaTrackConstraints 規格列出了所有可能的限制類型 (不過並非所有選項都支援所有選項)。如果目前選取的相機不支援要求解析度,getUserMedia() 將透過 OverconstrainedError 拒絕,也不會提示使用者授予相機存取權。

如果 getUserMedia() 成功,系統會將網路攝影機的影片串流設為影片元素來源:

function gotLocalMediaStream(mediaStream) {
  localVideo.srcObject = mediaStream;
}

獎勵積分

  • 傳遞至 getUserMedia()localStream 物件屬於全域範圍,因此您可以透過瀏覽器控制台加以檢查:開啟控制台,輸入 stream,然後按下 Return 鍵。(如要在 Chrome 中查看控制台,請按下 Ctrl-Shift-J 鍵;Mac 使用者請按 Command-Option-J 鍵)。
  • localStream.getVideoTracks() 會傳回什麼?
  • 請嘗試撥打 localStream.getVideoTracks()[0].stop()
  • 查看限制條件物件:將物件變更為 {audio: true, video: true} 會有什麼影響?
  • 影片元素大小為何?如何從 JavaScript 取得影片的自然大小,而非多媒體廣告大小?請使用 Chrome 開發人員工具進行檢查。
  • 請嘗試在影片元素中加入 CSS 篩選器。例如:
video {
  filter: blur(4px) invert(1) opacity(0.5);
}
  • 請嘗試新增 SVG 濾鏡。例如:
video {
   filter: hue-rotate(180deg) saturate(200%);
 }

您學到的內容

在這個步驟中,您已瞭解如何:

  • 從網路攝影機取得視訊。
  • 設定媒體限制條件。
  • 會混入影片元素。

此步驟的完整版本位於 step-01 資料夾中。

提示

最佳做法

  • 確認影片元素不會溢位容器。新增 widthmax-width,可為影片設定偏好的大小和大小上限。瀏覽器會自動計算高度:
video {
  max-width: 100%;
  width: 320px;
}

下一步

你準備了影片,但該如何串流播放呢?快來看看下一個步驟吧!

5. 透過 RTCPeerConnection 串流播放影片

課程內容

這個步驟將說明如何:

  • 使用 WebRTC 輔助程式 adapter.js 簡化瀏覽器差異。
  • 使用 RTCPeerConnection API 串流播放影片。
  • 控制媒體擷取和串流作業。

此步驟的完整版本位於 step-2 資料夾中。

什麼是 RTCPeerConnection?

RTCPeerConnection 是一個 API,可用於發出 WebRTC 呼叫串流視訊和音訊,以及交換資料。

這個範例會在相同網頁上,在兩個 RTCPeerConnection 物件 (稱為對等體) 之間設定連線。

這不太實用,但很適合瞭解 RTCPeerConnection 的運作方式。

新增影片元素和控制按鈕

index.html 會將單一影片元素替換成兩個影片元素和三個按鈕:

<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>

其中一個影片元素會顯示「getUserMedia()」的串流,另一個則透過 RTCPeerconnection 串流播放同一部影片。(在實際的應用程式中,一個影片元素會顯示本機串流和另一個遠端串流)。

新增 Adapter.js 輔助程式

main.js 連結上方新增 adapter.js 目前版本的連結:

<script src="https://webrtc.github.io/adapter/adapter-latest.js"></script>

Index.html 程式碼現在看起來會像這樣:

<!DOCTYPE html>
<html>

<head>
  <title>Realtime communication with WebRTC</title>
  <link rel="stylesheet" href="css/main.css" />
</head>

<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>

安裝 RTCPeerConnection 程式碼

main.js 替換為 step-02 資料夾中的版本。

撥打電話

開啟 index.html,按一下 [開始] 按鈕來擷取網路攝影機視訊畫面,然後按一下 [通話] 建立對等連線。你應該會看到相同的影片 (以網路攝影機擷取的內容) 顯示在兩個視訊元素中。透過瀏覽器控制台查看 WebRTC 記錄。

運作方式

這個步驟非常重要...

WebRTC 會使用 RTCPeerConnection API 設定在 WebRTC 用戶端 (又稱為「對等互連」) 之間串流影片的連線。

這個範例中的兩個 RTCPeerConnection 物件位於相同頁面:pc1pc2。不太實用,但很適合示範 API 的運作方式。

在 WebRTC 對等體之間設定通話作業包含三項工作:

  • 請在通話的兩端建立 RTCPeerConnection,然後在結尾處新增來自 getUserMedia() 的本機串流。
  • 取得及分享網路資訊:潛在連線端點稱為 ICE 候選項目。
  • 取得並分享本機和遠端的說明:SDP 格式的本機媒體中繼資料。

假設 Alice 和 Bob 想使用 RTCPeerConnection 設定視訊通訊,

首先,Alice 和 Bob 交換網路資訊。「尋找候選人」運算式是指使用 ICE 架構尋找網路介面和通訊埠的程序。

  1. Alice 使用 onicecandidate (addEventListener('icecandidate')) 處理常式建立 RTCPeerConnection 物件。這會對應至 main.js 的下列程式碼:
let localPeerConnection;
localPeerConnection = new RTCPeerConnection(servers);
localPeerConnection.addEventListener('icecandidate', handleConnection);
localPeerConnection.addEventListener(
    'iceconnectionstatechange', handleConnectionChange);
  1. Alice 會呼叫 getUserMedia(),並新增傳遞到該內容的串流:
navigator.mediaDevices.getUserMedia(mediaStreamConstraints).
  then(gotLocalMediaStream).
  catch(handleLocalMediaStreamError);
function gotLocalMediaStream(mediaStream) {
  localVideo.srcObject = mediaStream;
  localStream = mediaStream;
  trace('Received local stream.');
  callButton.disabled = false;  // Enable call button.
}
localPeerConnection.addStream(localStream);
trace('Added local stream to localPeerConnection.');
  1. 網路候選時,系統會呼叫步驟 1 中的 onicecandidate 處理常式。
  2. Alice 將序列化的候選資料傳送給 Bob。在實際的應用程式中,這項程序 (稱為「信號」) 是透過訊息服務進行,稍後您將在後續步驟中瞭解相關操作方法。當然,在這個步驟中,兩個 RTCPeerConnection 物件都位於相同頁面,不需要外部訊息就能直接通訊。
  3. 當小明收到來自 Alice 的候選人訊息時,會呼叫 addIceCandidate(),以便在遠端同類應用程式說明中加入這位候選人:
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}.`);
  }
}

WebRTC 同儕也需要尋找並交換本機和遠端的音訊和視訊媒體資訊,例如解析度和轉碼器功能。使用工作階段說明通訊協定格式 (又稱為 SDP) 交換中繼資料 blob (稱為「方案」和「答案」),要求交換媒體設定資訊:

  1. Alice 會執行 RTCPeerConnection createOffer() 方法。傳回的承諾會提供 RTCSessionDescription:Alice 的本機工作階段說明:
trace('localPeerConnection createOffer start.');
localPeerConnection.createOffer(offerOptions)
  .then(createdOffer).catch(setSessionDescriptionError);
  1. 如果成功,Alice 會使用 setLocalDescription() 設定本機說明,然後透過信號管道,將此工作階段說明傳送給 Bob。
  2. Bob 使用 setRemoteDescription() 將說明設為遠端說明,
  3. Bob 執行 RTCPeerConnection createAnswer() 方法,並將他從 Alice 提供的遠端說明傳給該方法,這樣系統就能產生與她相容的本機工作階段。createAnswer() 承諾會傳遞 RTCSessionDescription:Bob 將這項承諾設為本機說明,並傳送給 Alice。
  4. 她在取得 Bob 的工作階段說明時,將這段說明設為 setRemoteDescription() 的遠端說明。
// 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(() => {
      setLocalDescriptionSuccess(localPeerConnection);
    }).catch(setSessionDescriptionError);

  trace('remotePeerConnection setRemoteDescription start.');
  remotePeerConnection.setRemoteDescription(description)
    .then(() => {
      setRemoteDescriptionSuccess(remotePeerConnection);
    }).catch(setSessionDescriptionError);

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

// Logs answer to offer creation and sets peer connection session descriptions.
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);
}
  1. 重要通知!

獎勵積分

  1. 請造訪 chrome://webrtc-internals。這會提供 WebRTC 統計資料和偵錯資料。(如需 Chrome 網址的完整清單,請前往 chrome://about)。
  2. 使用 CSS 設定頁面樣式:
  • 並排顯示影片。
  • 將按鈕的寬度放大,文字越大。
  • 確保版面配置可在行動裝置上正常運作。
  1. 透過 Chrome 開發人員工具控制台查看 localStreamlocalPeerConnectionremotePeerConnection
  2. 從控制台查看 localPeerConnectionpc1.localDescription。SDP 格式是什麼樣子?

您學到的內容

在這個步驟中,您已瞭解如何:

  • 使用 WebRTC 輔助程式 adapter.js 簡化瀏覽器差異。
  • 使用 RTCPeerConnection API 串流播放影片。
  • 控制媒體擷取和串流作業。
  • 在對等端之間共用媒體和網路資訊,以啟用 WebRTC 呼叫。

此步驟的完整版本位於 step-2 資料夾中。

提示

  • 這個步驟有許多需要學習的地方!如要尋找其他資源詳細說明 RTCPeerConnection,請前往 webrtc.org。本頁提供 JavaScript 架構建議,如果您想使用 WebRTC,但不想疊加 API。
  • 如要進一步瞭解 Adapter.js 輔助程式,請前往 adapter.js GitHub 存放區
  • 想知道世界最出色的視訊通訊應用程式是什麼樣子嗎?查看 AppRTC,WebRTC 專案的標準應用程式 WebRTC 呼叫:appcode。通話設定時間不到 500 毫秒。

最佳做法

  • 為了讓程式碼永不過時,請使用全新的 Promise 式 API,並透過 adapter.js 與不支援這些 API 的瀏覽器相容。

下一步

這個步驟說明如何使用 WebRTC 在同業之間串流影片,但本程式碼研究室同樣適用於資料!

在下一個步驟中,您將瞭解如何使用 RTCDataChannel 串流任意資料。

6. 使用 RTCDataChannel 交換資料

課程內容

  • 如何在 WebRTC 端點 (對等點) 之間交換資料。

此步驟的完整版本位於 step-03 資料夾中。

更新 HTML

在這個步驟中,您將使用 WebRTC 資料管道,在同一頁面的兩個 textarea 元素之間傳送文字。這項功能沒有實用之處,但是能展示瞭如何使用 WebRTC 來分享資料和串流影片。

index.html 中移除影片和按鈕元素,並替換成以下 HTML:

<textarea id="dataChannelSend" disabled
    placeholder="Press Start, enter some text, then press Send."></textarea>
<textarea id="dataChannelReceive" disabled></textarea>

<div id="buttons">
  <button id="startButton">Start</button>
  <button id="sendButton">Send</button>
  <button id="closeButton">Stop</button>
</div>

其中一個文字區域可用來輸入文字,另一個則會在同業之間串流顯示文字。

index.html 現在應該像這樣:

<!DOCTYPE html>
<html>

<head>

  <title>Realtime communication with WebRTC</title>

  <link rel="stylesheet" href="css/main.css" />

</head>

<body>

  <h1>Realtime communication with WebRTC</h1>

  <textarea id="dataChannelSend" disabled
    placeholder="Press Start, enter some text, then press Send."></textarea>
  <textarea id="dataChannelReceive" disabled></textarea>

  <div id="buttons">
    <button id="startButton">Start</button>
    <button id="sendButton">Send</button>
    <button id="closeButton">Stop</button>
  </div>

  <script src="https://webrtc.github.io/adapter/adapter-latest.js"></script>
  <script src="js/main.js"></script>

</body>

</html>

更新 JavaScript

main.js 替換為 step-03/js/main.js 的內容。

嘗試在對等端之間串流資料:開啟 index.html,按下「Start」來設定對等連線,在左側輸入一些文字,然後按一下「傳送」,即可使用 WebRTC 資料管道傳輸文字。textarea

運作方式

這個程式碼會使用 RTCPeerConnection 和 RTCDataChannel 啟用簡訊交換功能。

這個步驟中的許多程式碼與 RTCPeerConnection 範例一樣。

sendData()createConnection() 函式有大部分的新程式碼:

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);
  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 sendData() {
  var data = dataChannelSend.value;
  sendChannel.send(data);
  trace('Sent Data: ' + data);
}

RTCDataChannel 的語法刻意與 WebSocket 相似,其中包含 send() 方法和 message 事件。

請注意,dataConstraint 的用法。資料管道經過設定後,就能啟用不同類型的資料共用方式,例如以可靠的方式推送資料,優於成效。您可以前往 Mozilla Developer Network 進一步瞭解選項。

獎勵積分

  1. 使用 SCTP 時,WebRTC 資料管道使用的通訊協定,會預設啟用可靠且已排序的資料傳送功能。RTCDataChannel 何時需要提供可靠的資料傳送?在何種情況下效能可能更重要,即使這表示資料遺失也沒關係?
  2. 使用 CSS 改善網頁版面配置,並在「dataChannelReceive」中加入預留位置屬性文字區域
  3. 在行動裝置上測試頁面。

您學到的內容

在這個步驟中,您已瞭解如何:

  • 在兩個 WebRTC 對等端之間建立連線。
  • 在對等端之間交換文字資料。

此步驟的完整版本位於 step-03 資料夾中。

瞭解詳情

下一步

您已經瞭解如何在同一個網頁上交換同儕的資料,但要如何在不同的機器之間交換資料?首先,您需要設定信號管道,以便交換中繼資料訊息。瞭解下一個步驟吧!

7. 設定用來交換訊息的訊號服務

課程內容

這個步驟將說明如何:

  • 使用 npm 安裝 package.json 中指定的專案依附元件
  • 執行 Node.js 伺服器,並使用節點靜態服務提供靜態檔案。
  • 使用 Socket.IO 在 Node.js 上設定訊息傳遞服務。
  • 請使用這組號碼建立「房間」以及交換訊息

此步驟的完整版本位於 step-04 資料夾中。

概念

為了設定及維護 WebRTC 呼叫,WebRTC 用戶端 (對等互連) 需要交換中繼資料:

  • 候選人 (聯播網) 資訊,
  • 提供答案訊息,提供解析度和轉碼器等媒體相關資訊。

換句話說,您必須先交換中繼資料,才能進行音訊、影片或資料的點對點串流。這項程序稱為信號

在上述步驟中,傳送者和接收方 RTCPeerConnection 物件都位於同一個網頁,因此「signaling」就是在物件之間傳遞中繼資料

在實際應用程式中,傳送者和接收者 RTCPeerConnections 會在不同裝置的網頁上執行,而您需要讓對方與中繼資料通訊。

針對這類作業,您需要使用「信號伺服器」:一種伺服器,可在 WebRTC 用戶端 (對等互連) 之間傳送訊息。實際的訊息為純文字:字串化的 JavaScript 物件。

事前準備:安裝 Node.js

為執行本程式碼研究室的後續步驟 (資料夾 step-04step-06),您必須使用 Node.js 在 localhost 上執行伺服器。

您可以透過這個連結或慣用的套件管理員下載及安裝 Node.js。

安裝完成後,您就可以匯入後續步驟所需的依附元件 (執行 npm install),以及執行小型 localhost 伺服器來執行程式碼研究室 (執行 node index.js)。我們會在之後需要時指定這些指令。

關於應用程式

WebRTC 使用用戶端 JavaScript API,但如果要實際使用,同樣需要通訊 (訊息) 伺服器,以及 STUN 和 TURN 伺服器。詳情請參閱這篇說明文章

在這個步驟中,您會使用 Socket.IO Node.js 模組和 JavaScript 程式庫建立簡易的 Node.js 信號伺服器,以便透過這個模組傳送訊息。具備 Node.js 和 Socket.IO 的經驗會很有幫助,但其實並不重要;訊息元件非常簡單

在這個範例中,伺服器 (Node.js 應用程式) 是在 index.js 中實作,其執行的用戶端 (網頁應用程式) 則是使用 index.html

此步驟中的 Node.js 應用程式有兩個工作。

首先,做為郵件轉發工具:

socket.on('message', function (message) {
  log('Got message: ', message);
  socket.broadcast.emit('message', message);
});

其次,它會管理 WebRTC 視訊通訊「聊天室」:

if (numClients === 0) {
  socket.join(room);
  socket.emit('created', room, socket.id);
} else if (numClients === 1) {
  socket.join(room);
  socket.emit('joined', room, socket.id);
  io.sockets.in(room).emit('ready');
} else { // max two clients
  socket.emit('full', room);
}

我們簡易的 WebRTC 應用程式允許最多兩個對等連線共用一個房間。

HTML 和JavaScript

更新 index.html,如下所示:

<!DOCTYPE html>
<html>

<head>

  <title>Realtime communication with WebRTC</title>

  <link rel="stylesheet" href="css/main.css" />

</head>

<body>

  <h1>Realtime communication with WebRTC</h1>

  <script src="/socket.io/socket.io.js"></script>
  <script src="js/main.js"></script>
  
</body>

</html>

在這個步驟中,您不會在網頁中看到任何內容,所有記錄皆已在瀏覽器控制台完成。(如要在 Chrome 中查看控制台,請按下 Ctrl-Shift-J 鍵;Mac 使用者請按 Command-Option-J 鍵)。

js/main.js 取代為以下內容:

'use strict';

var isInitiator;

window.room = prompt("Enter room name:");

var socket = io.connect();

if (room !== "") {
  console.log('Message from client: Asking to join room ' + room);
  socket.emit('create or join', room);
}

socket.on('created', function(room, clientId) {
  isInitiator = true;
});

socket.on('full', function(room) {
  console.log('Message from client: Room ' + room + ' is full :^(');
});

socket.on('ipaddr', function(ipaddr) {
  console.log('Message from client: Server IP address is ' + ipaddr);
});

socket.on('joined', function(room, clientId) {
  isInitiator = false;
});

socket.on('log', function(array) {
  console.log.apply(console, array);
});

設定 Socket.IO,在 Node.js 上執行

您可能已看過 HTML 檔案中正在使用 Socket.IO 檔案:

<script src="/socket.io/socket.io.js"></script>

work 目錄的頂層建立名為 package.json 的檔案,其中含有下列內容:

{
  "name": "webrtc-codelab",
  "version": "0.0.1",
  "description": "WebRTC codelab",
  "dependencies": {
    "node-static": "^0.7.10",
    "socket.io": "^1.2.0"
  }
}

這個應用程式資訊清單會指示 Node Package Manager (npm) 要安裝哪些專案依附元件。

如要安裝依附元件 (例如 /socket.io/socket.io.js),請在 work 目錄中的指令列終端機執行下列指令:

npm install

您應該會看到結尾類似下方的安裝記錄:

3ab06b7bcc7664b9.png

如您所見,npm 已安裝 package.json 中定義的依附元件。

work 目錄的頂層 (而非 js 目錄) 中建立新檔案 index.js,並新增下列程式碼:

'use strict';

var os = require('os');
var nodeStatic = require('node-static');
var http = require('http');
var socketIO = require('socket.io');

var fileServer = new(nodeStatic.Server)();
var app = http.createServer(function(req, res) {
  fileServer.serve(req, res);
}).listen(8080);

var io = socketIO.listen(app);
io.sockets.on('connection', function(socket) {

  // convenience function to log server messages on the client
  function log() {
    var array = ['Message from server:'];
    array.push.apply(array, arguments);
    socket.emit('log', array);
  }

  socket.on('message', function(message) {
    log('Client said: ', message);
    // for a real app, would be room-only (not broadcast)
    socket.broadcast.emit('message', message);
  });

  socket.on('create or join', function(room) {
    log('Received request to create or join room ' + room);

    var clientsInRoom = io.sockets.adapter.rooms[room];
    var numClients = clientsInRoom ? Object.keys(clientsInRoom.sockets).length : 0;

    log('Room ' + room + ' now has ' + numClients + ' client(s)');

    if (numClients === 0) {
      socket.join(room);
      log('Client ID ' + socket.id + ' created room ' + room);
      socket.emit('created', room, socket.id);

    } else if (numClients === 1) {
      log('Client ID ' + socket.id + ' joined room ' + room);
      io.sockets.in(room).emit('join', room);
      socket.join(room);
      socket.emit('joined', room, socket.id);
      io.sockets.in(room).emit('ready');
    } else { // max two clients
      socket.emit('full', room);
    }
  });

  socket.on('ipaddr', function() {
    var ifaces = os.networkInterfaces();
    for (var dev in ifaces) {
      ifaces[dev].forEach(function(details) {
        if (details.family === 'IPv4' && details.address !== '127.0.0.1') {
          socket.emit('ipaddr', details.address);
        }
      });
    }
  });

});

在指令列終端機的 work 目錄中執行下列指令:

node index.js

透過瀏覽器開啟 localhost:8080

每次開啟這個網址時,系統都會提示你輸入房間名稱。如要加入同一個房間,每次加入相同的房間名稱,例如「foo」。

開啟新分頁,然後再次開啟 localhost:8080。請選擇相同的房間名稱。

在第三個分頁或視窗中開啟 localhost:8080。請再次選擇相同的房間名稱。

檢查每個分頁的主控台:您應該會看到上述 JavaScript 的記錄。

獎勵積分

  1. 以下是可能的替代訊息機制?使用「純粹」時可能會遇到哪些問題WebSocket?
  2. 擴大這個應用程式規模時可能會發生哪些問題?您是否能開發出測試數千或數百萬項同步聊天室要求的方法?
  3. 這個應用程式會使用 JavaScript 提示取得房間名稱。設法從網址取得會議室名稱。例如,localhost:8080/foo 會將會議室名稱設為 foo

您學到的內容

在這個步驟中,您已瞭解如何執行下列作業:

  • 使用 npm 安裝 package.json 中指定的專案依附元件
  • 執行 Node.js 伺服器對伺服器靜態檔案。
  • 使用 socket.io 在 Node.js 上設定訊息傳遞服務。
  • 請使用這組號碼建立「房間」以及交換訊息

此步驟的完整版本位於 step-04 資料夾中。

瞭解詳情

下一步

瞭解如何使用信號功能,讓兩位使用者建立對等互連連線。

8. 結合對等互連連線與信號功能

課程內容

這個步驟將說明如何:

  • 使用在 Node.js 上執行的 Socket.IO 執行 WebRTC 信號服務
  • 使用該服務在對等互連項目之間交換 WebRTC 中繼資料。

此步驟的完整版本位於 step-05 資料夾中。

取代 HTML 和 JavaScript

index.html 的內容替換成以下內容:

<!DOCTYPE html>
<html>

<head>

  <title>Realtime communication with WebRTC</title>

  <link rel="stylesheet" href="/css/main.css" />

</head>

<body>

  <h1>Realtime communication with WebRTC</h1>

  <div id="videos">
    <video id="localVideo" autoplay muted></video>
    <video id="remoteVideo" autoplay></video>
  </div>

  <script src="/socket.io/socket.io.js"></script>
  <script src="https://webrtc.github.io/adapter/adapter-latest.js"></script>
  <script src="js/main.js"></script>
  
</body>

</html>

js/main.js 替換為 step-05/js/main.js 的內容。

執行 Node.js 伺服器

如果您並未從 work 目錄遵循本程式碼研究室,則可能需要安裝 step-05 資料夾或當前工作資料夾的依附元件。從工作目錄執行下列指令:

npm install

安裝完成後,如果 Node.js 伺服器未執行,請在 work 目錄中呼叫以下指令,啟動伺服器:

node index.js

請務必使用上一個步驟中導入 Socket.IO 的 index.js 版本。若要進一步瞭解節點和通訊端 IO,請參閱「設定訊號服務以交換訊息」一節。

透過瀏覽器開啟 localhost:8080

在新分頁或視窗中再次開啟 localhost:8080。一個影片元素會顯示 getUserMedia() 的本機串流,另一個影片則顯示「remote」透過 RTCPeerconnection 串流播放影片

查看瀏覽器控制台中的記錄功能。

獎勵積分

  1. 這個應用程式僅支援一對一視訊通訊。您希望讓多人共用同一個視訊聊天室,您希望如何改變設計?
  2. 範例中的會議室名稱是 foo 硬式編碼。啟用其他房間名稱的最佳方式為何?
  3. 使用者如何分享會議室名稱?請嘗試建立共用會議室名稱的替代方案。
  4. 如何變更應用程式

您學到的內容

在這個步驟中,您已瞭解如何:

  • 使用在 Node.js 上執行的 Socket.IO 執行 WebRTC 信號服務。
  • 使用該服務在對等互連項目之間交換 WebRTC 中繼資料。

此步驟的完整版本位於 step-05 資料夾中。

提示

  • chrome://webrtc-internals 提供 WebRTC 統計資料和偵錯資料。
  • test.webrtc.org 可用於檢查本機環境及測試攝影機和麥克風。
  • 如果無法順利快取,請嘗試下列做法:
  • 按住 Ctrl 鍵並點選「重新載入」按鈕,強制重新整理
  • 重新啟動瀏覽器
  • 從指令列執行 npm cache clean

下一步

瞭解如何拍照、取得圖片資料,並與遠端的親朋好友分享。

9. 拍攝相片,並透過資料管道分享

課程內容

這個步驟將說明如何:

  • 拍攝相片,並使用畫布元素取得相片資料。
  • 與遠端使用者交換圖片資料。

此步驟的完整版本位於 step-06 資料夾中。

運作方式

您之前已經學會如何使用 RTCDataChannel 交換簡訊。

這個步驟可讓您分享整個檔案,在這個範例中為透過 getUserMedia() 拍攝的相片。

這個步驟的核心部分如下:

  1. 建立資料管道。請注意,在這個步驟中,您不會在對等點連線中新增任何媒體串流。
  2. 使用 getUserMedia() 擷取使用者的網路攝影機影片串流:
var video = document.getElementById('video');

function grabWebCamVideo() {
  console.log('Getting user media (video) ...');
  navigator.mediaDevices.getUserMedia({
    video: true
  })
  .then(gotStream)
  .catch(function(e) {
    alert('getUserMedia() error: ' + e.name);
  });
}
  1. 使用者按下「Snap」按鈕後,從影片串流取得快照 (影片影格),並在 canvas 元素中顯示:
var photo = document.getElementById('photo');
var photoContext = photo.getContext('2d');

function snapPhoto() {
  photoContext.drawImage(video, 0, 0, photo.width, photo.height);
  show(photo, sendBtn);
}
  1. 當使用者按一下「Send」按鈕時,請將圖片轉換為位元組,並透過資料管道傳送:
function sendPhoto() {
  // Split data channel message in chunks of this byte length.
  var CHUNK_LEN = 64000;
  var img = photoContext.getImageData(0, 0, photoContextW, photoContextH),
    len = img.data.byteLength,
    n = len / CHUNK_LEN | 0;

  console.log('Sending a total of ' + len + ' byte(s)');
  dataChannel.send(len);

  // split the photo and send in chunks of about 64KB
  for (var i = 0; i < n; i++) {
    var start = i * CHUNK_LEN,
      end = (i + 1) * CHUNK_LEN;
    console.log(start + ' - ' + (end - 1));
    dataChannel.send(img.data.subarray(start, end));
  }

  // send the reminder, if any
  if (len % CHUNK_LEN) {
    console.log('last ' + len % CHUNK_LEN + ' byte(s)');
    dataChannel.send(img.data.subarray(n * CHUNK_LEN));
  }
}
  1. 接收端會將資料管道訊息位元組轉換回圖片,並向使用者顯示圖片:
function receiveDataChromeFactory() {
  var buf, count;

  return function onmessage(event) {
    if (typeof event.data === 'string') {
      buf = window.buf = new Uint8ClampedArray(parseInt(event.data));
      count = 0;
      console.log('Expecting a total of ' + buf.byteLength + ' bytes');
      return;
    }

    var data = new Uint8ClampedArray(event.data);
    buf.set(data, count);

    count += data.byteLength;
    console.log('count: ' + count);

    if (count === buf.byteLength) {
      // we're done: all data chunks have been received
      console.log('Done. Rendering photo.');
      renderPhoto(buf);
    }
  };
}

function renderPhoto(data) {
  var canvas = document.createElement('canvas');
  canvas.width = photoContextW;
  canvas.height = photoContextH;
  canvas.classList.add('incomingPhoto');
  // trail is the element holding the incoming images
  trail.insertBefore(canvas, trail.firstChild);

  var context = canvas.getContext('2d');
  var img = context.createImageData(photoContextW, photoContextH);
  img.data.set(data);
  context.putImageData(img, 0, 0);
}

取得程式碼

work 資料夾的內容替換為 step-06 的內容。「work」中的 index.html 檔案現在應如下所示:****

<!DOCTYPE html>
<html>

<head>

  <title>Realtime communication with WebRTC</title>

  <link rel="stylesheet" href="/css/main.css" />

</head>

<body>

  <h1>Realtime communication with WebRTC</h1>

  <h2>
    <span>Room URL: </span><span id="url">...</span>
  </h2>

  <div id="videoCanvas">
    <video id="camera" autoplay></video>
    <canvas id="photo"></canvas>
  </div>

  <div id="buttons">
    <button id="snap">Snap</button><span> then </span><button id="send">Send</button>
    <span> or </span>
    <button id="snapAndSend">Snap &amp; Send</button>
  </div>

  <div id="incoming">
    <h2>Incoming photos</h2>
    <div id="trail"></div>
  </div>

  <script src="/socket.io/socket.io.js"></script>
  <script src="https://webrtc.github.io/adapter/adapter-latest.js"></script>
  <script src="js/main.js"></script>

</body>

</html>

如果您並未從 work 目錄遵循本程式碼研究室,則可能需要安裝 step-06 資料夾或目前的工作資料夾依附元件。只要在工作目錄中執行下列指令:

npm install

安裝完成後,如果 Node.js 伺服器未執行,請從「work」目錄中呼叫下列指令來啟動伺服器:

node index.js

請務必使用實作 Socket.IO 的 index.js 版本;如果進行變更,請記得重新啟動 Node.js 伺服器。若要進一步瞭解節點和通訊端 IO,請參閱「設定訊號服務以交換訊息」一節。

如有需要,可以按一下「允許」按鈕,允許應用程式使用網路攝影機。

應用程式會隨機建立房間 ID,並將該 ID 新增至網址。在新的瀏覽器分頁或視窗中,開啟網址列中的網址。

按一下 [Snap &傳送按鈕,然後查看頁面底部其他標籤中的「收到的項目」區域。應用程式會在分頁之間轉移相片。

畫面應如下所示:

911b40f36ba6ba8.png

獎勵積分

  1. 如何修改程式碼,以便分享任何類型的檔案?

瞭解詳情

您學到的內容

  • 如何使用畫布元素拍攝相片,並從中取得資料。
  • 如何與遠端使用者交換資料。

此步驟的完整版本位於 step-06 資料夾中。

10. 恭喜

您建構了一個應用程式來支援即時影片串流和資料交換!

您學到的內容

在本程式碼研究室中,您瞭解如何:

  • 從網路攝影機取得視訊。
  • 透過 RTCPeerConnection 串流播放影片。
  • 使用 RTCDataChannel 串流資料。
  • 設定用來交換訊息的訊號服務。
  • 結合對等互連連線與信號。
  • 拍攝相片,並透過資料管道分享。

後續步驟

瞭解詳情

  • webrtc.org 提供了一系列開始使用 WebRTC 的資源。