使用 WebRTC 進行即時通訊

1. 簡介

WebRTC 是一項開放原始碼專案,可讓網頁和原生應用程式即時傳輸音訊、視訊和資料。

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

我可以在哪裡使用 WebRTC?

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

什麼是信號?

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

安裝 Web Server for Chrome 應用程式後,按一下書籤列、新分頁或應用程式啟動器中的 Chrome 應用程式捷徑:

1d2b4aa977ab7e24.png

按一下「Web Server」圖示:

27fce4494f641883.png

接著,您會看到這個對話方塊,可供您設定本機網路伺服器:

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

按一下「選擇資料夾」按鈕,然後選取剛建立的「work」資料夾。這樣一來,您就能透過「網頁伺服器」對話方塊「網頁伺服器網址」部分中醒目顯示的網址,在 Chrome 中查看進行中的工作。

在「選項」下方,勾選「Automatically show index.html」(自動顯示 index.html) 旁邊的方塊,如下所示:

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

接著,將標示為「Web Server: STARTED」(網路伺服器:已啟動) 的切換鈕滑動至左側,然後再滑動至右側,即可停止並重新啟動伺服器。

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

現在點選醒目顯示的 Web Server URL,在網路瀏覽器中造訪工作網站。您應該會看到類似下方的頁面,對應於 work/index.html

18a705cb6ccc5181.png

顯然,這個應用程式還不會執行任何有趣的操作,目前只是用來確保網路伺服器正常運作的最小骨架。您會在後續步驟中新增功能和版面配置功能。

4. 從網路攝影機串流影片

課程內容

這個步驟會說明如何:

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

這個步驟的完整版本位於「step-01」step-01資料夾中。

一點點 HTML...

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

<!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」step-01資料夾中。

訣竅

最佳做法

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

下一步

你已經有影片,但該如何串流播放?請參閱下一個步驟!

5. 使用 RTCPeerConnection 串流播放影片

課程內容

這個步驟會說明如何:

  • 使用 WebRTC 墊片 adapter.js,抽象化瀏覽器差異。
  • 使用 RTCPeerConnection API 串流播放影片。
  • 控制媒體擷取和串流。

這個步驟的完整版本位於「step-2」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 shim

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,按一下「Start」按鈕,從網路攝影機取得影片,然後按一下「Call」建立對等互連連線。兩個影片元素中應會顯示相同的影片 (來自網路攝影機)。查看瀏覽器控制台,瞭解 WebRTC 記錄。

運作方式

這個步驟會執行許多作業...

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

在本例中,兩個 RTCPeerConnection 物件位於同一頁面:pc1pc2。實用性不高,但很適合用來示範 API 的運作方式。

在 WebRTC 對等互連之間設定通話時,需要完成三項工作:

  • 為通話的每一端建立 RTCPeerConnection,並在每一端新增 getUserMedia() 的本機串流。
  • 取得及分享網路資訊:潛在連線端點稱為 ICE 候選項目。
  • 取得及分享本機和遠端說明:以 SDP 格式呈現本機媒體的中繼資料。

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

首先,愛麗絲和鮑伯會交換網路資訊。「尋找候選項目」是指使用 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. 當 Bob 收到 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 對等互連也需要找出並交換本機和遠端音訊與視訊媒體資訊,例如解析度和轉碼器功能。如要交換媒體設定資訊,請使用「工作階段描述通訊協定」(Session Description Protocol,簡稱 SDP) 格式,交換中繼資料 Blob,也就是「提案」和「回覆」

  1. 小莉執行 RTCPeerConnection createOffer() 方法。傳回的 Promise 會提供 RTCSessionDescription:Alice 的本機工作階段說明:
trace('localPeerConnection createOffer start.');
localPeerConnection.createOffer(offerOptions)
  .then(createdOffer).catch(setSessionDescriptionError);
  1. 如果成功,Alice 會使用 setLocalDescription() 設定本機說明,然後透過信號傳輸管道將工作階段說明傳送給 Bob。
  2. Bob 使用 setRemoteDescription() 將 Alice 傳送給他的說明設為遠端說明。
  3. Bob 執行 RTCPeerConnection createAnswer() 方法,並將從 Alice 取得的遠端說明傳遞給該方法,以便產生與 Alice 說明相容的本機工作階段。createAnswer() 承諾會傳遞 RTCSessionDescription:Bob 會將其設為本機說明,並傳送給 Alice。
  4. 小莉收到志明的會期說明後,會使用 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. Ping!

獎勵分數

  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」step-2資料夾中。

訣竅

  • 這個步驟的內容非常豐富,如要尋找其他資源,進一步瞭解 RTCPeerConnection,請參閱 webrtc.org。如果您想使用 WebRTC,但不想處理 API,本頁面提供 JavaScript 架構建議。
  • 如要進一步瞭解 adapter.js shim,請前往 adapter.js GitHub 存放區
  • 想看看全球最佳視訊通訊應用程式的樣貌嗎?請參閱 AppRTC,這是 WebRTC 專案的標準應用程式,用於 WebRTC 通話:應用程式程式碼。通話設定時間少於 500 毫秒。

最佳做法

  • 為確保程式碼能因應未來變化,請使用新的 Promise 型 API,並透過 adapter.js 啟用與不支援這些 API 的瀏覽器相容性。

下一步

這個步驟說明如何使用 WebRTC 在對等互連裝置之間串流播放影片,但本程式碼研究室也涵蓋資料!

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

6. 使用 RTCDataChannel 交換資料

課程內容

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

這個步驟的完整版本位於「step-03」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」設定對等連線,在左側的 textarea 中輸入一些文字,然後按一下「Send」,透過 WebRTC 資料通道傳輸文字。

運作方式

這段程式碼使用 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 開發人員網路

獎勵分數

  1. WebRTC 資料通道使用的通訊協定 SCTP 預設會可靠地依序傳送資料。RTCDataChannel 何時需要提供可靠的資料傳輸服務?何時效能更重要 (即使這表示會遺失部分資料)?
  2. 使用 CSS 改善網頁版面配置,並在「dataChannelReceive」文字區域中新增預留位置屬性。
  3. 在行動裝置上測試網頁。

您學到的內容

這個步驟說明如何:

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

這個步驟的完整版本位於「step-03」step-03資料夾中。

瞭解詳情

下一步

您已瞭解如何在同一網頁上的對等互連裝置之間交換資料,但如何在不同機器之間交換資料呢?首先,您需要設定信號傳輸管道,交換中繼資料訊息。請參閱下一個步驟瞭解詳情!

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

課程內容

在本步驟中,您將瞭解如何:

  • 使用 npm 安裝 package.json 中指定的專案依附元件
  • 執行 Node.js 伺服器,並使用 node-static 提供靜態檔案。
  • 使用 Socket.IO 在 Node.js 上設定訊息服務。
  • 並用來建立「聊天室」和交換訊息。

這個步驟的完整版本位於「step-04」step-04資料夾中。

概念

如要設定及維護 WebRTC 通話,WebRTC 用戶端 (對等互連) 必須交換中繼資料:

  • 候選人 (電視網) 資訊。
  • Offeranswer 訊息,提供媒體相關資訊,例如解析度和轉碼器。

換句話說,必須先交換中繼資料,才能進行音訊、視訊或資料的對等串流。這項程序稱為「信號傳輸」

在先前的步驟中,傳送者和接收者的 RTCPeerConnection 物件位於同一網頁上,因此「信號」只是在物件之間傳遞中繼資料。

在實際應用程式中,傳送者和接收者 RTCPeerConnection 會在不同裝置的網頁上執行,您需要讓兩者傳輸中繼資料。

為此,您需要使用信號伺服器:這類伺服器可在 WebRTC 用戶端 (對等互連) 之間傳遞訊息。實際訊息是純文字:字串化的 JavaScript 物件。

必要條件:安裝 Node.js

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

您可以從這個連結下載並安裝 Node.js,或透過偏好的套件管理工具安裝。

安裝完成後,您就能匯入後續步驟 (執行 npm install) 所需的依附元件,以及執行小型本機主機來執行程式碼研究室 (執行 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」step-04資料夾中。

瞭解詳情

下一步

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

8. 合併對等互連和信號傳輸

課程內容

這個步驟會說明如何:

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

這個步驟的完整版本位於「step-05」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 伺服器

如果您不是從工作目錄執行本程式碼研究室,可能需要為 step-05 資料夾或目前的工作資料夾安裝依附元件。在工作目錄中執行下列指令:

npm install

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

node index.js

請確認您使用的是上一個步驟中實作 Socket.IO 的 index.js 版本。如要進一步瞭解 Node 和 Socket IO,請參閱「設定訊號服務來交換訊息」一節。

在瀏覽器中開啟 localhost:8080

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

在瀏覽器控制台中查看記錄。

獎勵積分

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

您學到的內容

這個步驟說明如何:

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

這個步驟的完整版本位於「step-05」step-05資料夾中。

訣竅

  • 您可以透過 chrome://webrtc-internals 取得 WebRTC 統計資料和偵錯資料。
  • 您可以使用 test.webrtc.org 檢查本機環境,並測試攝影機和麥克風。
  • 如果快取發生奇怪的問題,請嘗試下列方法:
  • 按住 Ctrl 鍵並點選「重新載入」按鈕,進行強制重新整理
  • 重新啟動瀏覽器
  • 透過指令列執行 npm cache clean

下一步

瞭解如何拍照、取得圖片資料,以及在遠端對等互連裝置之間分享圖片。

9. 拍照並透過資料管道分享

課程內容

這個步驟將說明如何:

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

這個步驟的完整版本位於「step-06」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. 使用者點選「傳送」按鈕時,請將圖片轉換為位元組,並透過資料管道傳送:
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>

如果您不是從工作目錄進行本程式碼研究室,可能需要為 step-06 資料夾或目前的工作資料夾安裝依附元件。只要從工作目錄執行下列指令即可:

npm install

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

node index.js

請確認您使用的是實作 Socket.IO 的 index.js 版本,並記得在變更後重新啟動 Node.js 伺服器。如要進一步瞭解 Node 和 Socket IO,請參閱「設定訊號服務來交換訊息」一節。

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

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

按一下「Snap & Send」(拍攝並傳送) 按鈕,然後查看另一個分頁頁面底部的「Incoming」(傳入) 區域。應用程式會在分頁之間轉移相片。

畫面應如下所示:

911b40f36ba6ba8.png

獎勵分數

  1. 如何變更程式碼,以便共用任何檔案類型?

瞭解詳情

您學到的內容

  • 如何使用畫布元素拍照並取得資料。
  • 如何與遠端使用者交換資料。

這個步驟的完整版本位於「step-06」step-06資料夾中。

10. 恭喜

您已建構應用程式,可進行即時視訊串流和資料交換!

您學到的內容

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

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

後續步驟

瞭解詳情

  • 如要開始使用 WebRTC,請前往 webrtc.org 取得各種資源。