啟用與 WebRTC 的即時通訊

1. 事前準備

本程式碼研究室將教您如何建構應用程式,以透過網路攝影機擷取影片及拍攝快照,並透過 WebRTC 點對點分享。同時瞭解如何使用核心 WebRTC API,以及如何使用 Node.js 設定訊息伺服器。

必要條件

  • HTML、CSS 和 JavaScript 基礎知識

建構項目

  • 使用網路攝影機拍攝影片。
  • 透過 RTCPeerConnection 串流播放影片。
  • 使用 RTCDataChannel 串流資料。
  • 設定信號服務以交換訊息。
  • 結合對等點連線和訊號。
  • 拍攝相片並透過資料管道分享。

軟硬體需求

  • Chrome 47 以上版本
  • Chrome 網路伺服器或您選擇的網路伺服器
  • 您選擇的文字編輯器
  • Node.js

2. 取得範例程式碼

下載程式碼

  1. 如果您對 Git 還很熟悉,請執行下列指令,從 GitHub 複製這個程式碼研究室的程式碼:
git clone https://github.com/googlecodelabs/webrtc-web

您也可以按此連結,下載程式碼的 ZIP 檔案:

  1. 開啟已下載的 ZIP 檔案,解壓縮名稱為「webrtc-web-master」的專案資料夾,其中包含每個程式碼研究室步驟和您需要的所有資源所在的資料夾。

您會在「work」目錄中執行所有程式碼工作。

這個 step-nn 資料夾包含每個程式碼研究室步驟的已完成版本。請隨時參考。

安裝並驗證網路伺服器

雖然您可以自由使用自己的網路伺服器,但這個程式碼研究室是專為 Chrome 專用 Web Server 所設計。

  1. 如果您沒有 Chrome 的網路伺服器,請按一下以下連結,透過 Chrome 線上應用程式商店安裝:

d0a4649b4920cf3.png

  1. 按一下 [加到 Chrome] 即可安裝 Chrome 專用的 Web Server,並在新分頁中開啟 Google 應用程式。
  2. 按一下 [網路伺服器]

27fce4494f641883.png

系統隨即會顯示對話方塊,讓您設定本機網路伺服器:

a300381a486b9e22.png

  1. 按一下 [選擇資料夾]。
  2. 選取您建立的 work 資料夾。

在「網路伺服器網址」下,您可以看到處理中網址:

  1. 在「選項 (可能需要重新啟動)」下方,勾選 [自動顯示 index.html] 核取方塊。
  2. 切換「Web Server: Start」(網路伺服器:已啟動) 兩次,即可停止及重新啟動伺服器。

f23cafb3993dfac1.png

  1. 按一下「網路伺服器網址」下方的網址,即可查看您在網路瀏覽器中的工作。

您應該會看到類似 work/index.html 的網頁:

18a705cb6ccc5181.png

當然,這個應用程式目前並未執行任何操作,這個 API 只是最基本的架構,可確保您的網路伺服器正常運作。您可以在後續步驟中新增功能和版面配置功能。

3. 透過網路攝影機串流播放影片

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

新增破折號

複製此程式碼並貼到 work 目錄中的 index.html 檔案中,以新增 videoscript 元素:

<!DOCTYPE html>
<html>

<head>

  <title>Real-time communication with WebRTC</title>

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

</head>

<body>

  <h1>Real-time communication with WebRTC</h1>

  <video autoplay playsinline></video>

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

</body>

</html>

新增一堆 JavaScript

複製此程式碼並貼到 js 資料夾中的 main.js 檔案中:

'use strict';

// In this codelab, you  only stream video, not audio (video: true).
const mediaStreamConstraints = {
  video: true,
};

// The video element where the stream is displayed
const localVideo = document.querySelector('video');

// The local stream that's displayed on the video
let localStream;

// Handle success and add the MediaStream to the video element
function gotLocalMediaStream(mediaStream) {
  localStream = mediaStream;
  localVideo.srcObject = mediaStream;
}

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

// Initialize media stream
navigator.mediaDevices.getUserMedia(mediaStreamConstraints)
  .then(gotLocalMediaStream).catch(handleLocalMediaStreamError);

試試看

在瀏覽器中開啟 index.html 檔案後,你應該會看到類似下方的內容,不過必須透過網路攝影機來觀看:

9297048e43ed0f3d.png

運作方式

getUserMedia() 呼叫之後,如果瀏覽器要求取得目前來源的第一次攝影機存取要求,瀏覽器就會要求存取攝影機。

如果成功,系統會傳回 MediaStream,這個 media 元素可透過 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,,然後按下 Enter (Mac 上為 Return)。如要在 Chrome 中查看主控台,請按下 Control+Shift+J (在 Mac 電腦上為 Command+Option+J)。
  • localStream.getVideoTracks() 會傳回什麼?
  • 歡迎致電localStream.getVideoTracks()[0].stop()
  • 查看限制條件物件。變更為 {audio: true, video: true} 會有什麼影響?
  • 影片元素的大小為何?相較於顯示大小,如何從 JavaScript 取得影片的自然大小?請使用 Google Chrome 開發人員工具進行檢查。
  • 在影片元素中加入 CSS 濾鏡,如下所示:
video {
  filter: blur(4px) invert(1) opacity(0.5);
}
  • 新增 SVG 濾鏡,如下所示:
video {
   filter: hue-rotate(180deg) saturate(200%);
 }

訣竅

最佳做法

確認您的影片元素並未溢出其容器。此程式碼研究室新增了 widthmax-width 檔案,以設定影片的偏好大小和大小上限。瀏覽器會自動計算高度。

video {
  max-width: 100%;
  width: 320px;
}

4. 透過 RTCPeerConnection API 串流播放影片

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

新增影片元素和控制按鈕

index.html 檔案中,將單一 video 元素替換為兩個 video 和三個 button 元素:

<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 串流的相同影片。(在實際應用程式中,一個 video 元素會顯示本機串流,另一個顯示遠端串流。)

新增 Adapter.js shim

複製這個指令碼元素,並貼到 main.js 的指令碼元素上方:

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

您的 index.html 檔案現在應如下所示:

<!DOCTYPE html>
<html>

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

<body>
  <h1>Real-time 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」資料夾中的版本。

撥打電話

  1. 開啟 index.html 檔案。
  2. 按一下 [開始],即可透過網路攝影機取得影片。
  3. 按一下 [通話] 建立對等連線

您應該會在網路攝影機的 video 元素中看見同一部影片。

  1. 查看瀏覽器控制台以查看 WebRTC 記錄。

運作方式

這個步驟非常複雜。

WebRTC 會使用 RTCPeerConnection API 來設定連結,以在 WebRTC 用戶端 (又稱為對等點) 之間串流播放影片。

在這個範例中,兩個 RTCPeerConnection 物件都位於同一個頁面上:pc1pc2

WebRTC 對等點之間的呼叫設定包含三項工作:

  1. 為每通電話建立 RTCPeerConnection,然後在每端新增 getUserMedia() 中的本機串流。
  2. 取得及分享網路資訊。

潛在的連線端點稱為 ICE 候選項目。

  1. 取得及分享本機和遠端說明。

本地媒體的中繼資料採用 Session Description Protocol (SDP) 格式。

假設小莉想使用RTCPeerConnection建立視訊通訊,

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

  1. 小莉使用 onicecandidate (addEventListener('icecandidate')) 處理常式建立 RTCPeerConnection 物件。

這與來自 main.js 的下列程式碼相符:

let localPeerConnection;
localPeerConnection = new RTCPeerConnection(servers);
localPeerConnection.addEventListener('icecandidate', handleConnection);
localPeerConnection.addEventListener(
    'iceconnectionstatechange', handleConnectionChange);
  1. 小艾呼叫 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. 當網路候選項目可用時,系統會呼叫第一個步驟的 onicecandidate 處理常式。
  2. 小莉傳送序列化的候選資料給 Bob。

在實際應用程式中,這個過程稱為「傳送信號」,是透過訊息服務進行。您將在後續步驟中說明如何執行這項動作。當然,在這個步驟中,這兩個 RTCPeerConnection 物件會位於同一個頁面上,而且可直接收發訊息,不需使用外部訊息。

  1. 他收到來自小莉的候選人訊息時,會呼叫 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. 小艾執行 RTCPeerConnection createOffer() 方法。

傳回的承諾提供 RTCSessionDescription—Alice 的本機工作階段說明:

trace('localPeerConnection createOffer start.');
localPeerConnection.createOffer(offerOptions)
  .then(createdOffer).catch(setSessionDescriptionError);
  1. 如果成功,Alice 會使用 setLocalDescription() 設定本機說明,然後透過信號管道將此工作階段說明傳送給小包。
  2. 小包將「小艾」提供的說明設為「setRemoteDescription()」為遙控器說明。
  3. 小包負責執行 RTCPeerConnection createAnswer() 方法並傳送自己從小艾取得的遠端說明,然後產生與自己相容的當地工作階段。
  4. createAnswer()》承諾會通過一個「RTCSessionDescription」,但 Bob 將「:」設為當地說明,並傳送給 Alice。
  5. 後,Alice 得到 Bob's 評論描述,她使用 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. 前往 chrome://webrtc-internals。

本頁提供 WebRTC 統計資料和偵錯資料。(如需完整的 Chrome 網址清單,請前往 chrome://about)。

  1. 使用 CSS 設定網頁樣式:
  2. 將影片並排放置。
  3. 讓按鈕的寬度與寬度較大。
  4. 確保版面配置可在行動裝置上使用。
  5. 在 Chrome 開發人員工具主控台中查看「localStream」、「localPeerConnection」和「remotePeerConnection」。
  6. 在主控台中查看「localPeerConnectionpc1.localDescription」。

SDP 格式是什麼樣子?

訣竅

  • 如要進一步瞭解 Adapter.js 填充碼,請參閱 adapter.js GitHub 存放區
  • 查看 AppRTC程式碼,這是 WebRTC 專案是 WebRTC 呼叫的標準應用程式。通話設定時間少於 500 毫秒。

最佳做法

為了確保您的程式碼日後不受影響,請使用新版 Promise 的 API,並和不支援 adapter.js 的瀏覽器相容。

5. 使用資料管道交換資料

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

更新 HTML

在這個步驟中,您使用 WebRTC 資料管道來在同一個頁面中,在兩個 textarea 元素之間傳送文字。雖然這項功能不實用,但可確實說明 WebRTC 可以用來分享資料及串流播放影片。

videobutton 元素從 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>

一個textarea是輸入文字,另一個則是在對等點之間以串流方式顯示文字。

您的 index.html 檔案現在應如下所示:

<!DOCTYPE html>
<html>

<head>

  <title>Real-time communication with WebRTC</title>

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

</head>

<body>

  <h1>Real-time 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

  1. main.js 替換成 step-03/js/main.js 的內容。
  1. 嘗試在對等點之間串流資料:
  2. 開啟 index.html
  3. 按一下 [開始] 設定對等互連連線。
  4. 在左側的 [textarea] 中輸入文字。
  5. 按一下 [傳送],即可透過 WebRTC 資料管道傳輸文字。

運作方式

這個程式碼會使用 RTCPeerConnectionRTCDataChannel 來收發簡訊。

這個步驟中的大部分的程式碼都與 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 的語法與使用 send() 方法和 message 事件的 WebSocket 很相似。

請注意 dataConstraint 的使用行為。資料管道可設為啟用不同類型的資料分享方式,例如優先顯示穩定傳送的成效優於效能。

累計獎勵積分

  1. 使用 SCTP 時,WebRTC 資料管道所使用的通訊協定預設會啟用可靠且已排序的傳輸資料。在什麼情況下,RTCDataChannel 是否需要提供可靠的資料,且何時應該更重視效能指標 (即使遺失部分資料也是如此)。
  2. 使用 CSS 改善網頁版面配置,並為 dataChannelReceive textarea 新增預留位置屬性。
  3. 透過行動裝置測試網頁。

瞭解詳情

6. 設定信號服務以交換訊息

您已經學習如何在同個網頁上交換彼此的資料,但要如何在不同的電腦之間交換資料?首先,您必須設定信號管道來交換中繼資料訊息。

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

關於應用程式

WebRTC 使用用戶端 JavaScript API,但要用於實際操作,也需要一個訊號 (訊息傳遞) 伺服器,以及 STUN 和 TURN 伺服器。詳情請參閱這篇說明文章

在這個步驟中,您將使用 Socket.IO Node.js 模組和 JavaScript 程式庫來建構簡易型 Node.js 訊號伺服器。

在此範例中,伺服器 (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

  1. 更新 index.html,讓它看起來如下所示:
<!DOCTYPE html>
<html>

<head>

  <title>Real-time communication with WebRTC</title>

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

</head>

<body>

  <h1>Real-time communication with WebRTC</h1>

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

</html>

執行此步驟時,網頁並不會顯示任何內容。所有記錄都會在瀏覽器控制台中執行。如要在 Chrome 中查看主控台,請按下 Control+Shift+J (在 Mac 電腦上為 Command+Option+J)。

  1. 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);
});

設定在 Node.js 上執行的 Socket.IO 檔案

您可能注意到 HTML 檔案使用的是 Socket.IO 檔案:

<script src="/socket.io/socket.io.js"></script>
  1. 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) 哪一項專案

要安裝的依附元件。

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

安裝紀錄應該會如下所示:

3ab06b7bcc7664b9.png

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

  1. 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);
        }
      });
    }
  });

});
  1. 透過指令列的終端機在 work 目錄中執行下列指令:
node index.js
  1. 透過瀏覽器前往 http://localhost:8080

每次您前往這個網址時,系統都會提示您輸入房間名稱。

如要加入相同的會議室,請每次輸入相同的會議室名稱,例如 foo

  1. 開啟新分頁,再瀏覽至 http://localhost:8080,然後再次輸入相同的會議室名稱。
  2. 開啟另一個新分頁,再次瀏覽至 http://localhost:8080,然後重新輸入相同的會議室名稱。
  3. 請檢查各個分頁中的主控台。

您應該會看到來自 JavaScript 的記錄。

累計獎勵積分

  • 您還能使用其他哪些訊息機制?使用 pure WebSocket 時,可能會遇到哪些問題?
  • 擴充該應用程式時可能遇到哪些問題?您能否開發出可行測試的數千種聊天室要求?
  • 這個應用程式使用 JavaScript 提示取得聊天室名稱。瞭解如何從網址取得會議室名稱。舉例來說,http://localhost:8080/foo 會以會議室名稱「foo」命名。

瞭解詳情

7. 結合對等連線和訊號

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

取代 HTML 和 JavaScript

  1. index.html 改成以下內容:
<!DOCTYPE html>
<html>

<head>

  <title>Real-time communication with WebRTC</title>

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

</head>

<body>

  <h1>Real-time 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>
  1. js/main.js 替換成 step-05/js/main.js 的內容。

執行 Node.js 伺服器

如果您不是從 work 目錄追蹤這個程式碼研究室,您可能需要安裝 step-05 資料夾或目前工作資料夾的依附元件。

  1. 在您的工作目錄中執行下列指令:
npm install
  1. 安裝完成後,如果您的 Node.js 伺服器並未執行,請在 work 目錄中執行下列指令,以啟動該伺服器:
node index.js

請確認您使用的是執行 Socket.IO 的步驟的 index.js 版本。如要進一步瞭解節點和 IO IO,請參閱「設定信號服務」以交換訊息。

  1. 透過瀏覽器前往 http://localhost:8080。
  2. 開啟新分頁,然後再次瀏覽至 http://localhost:8080。

其中一個 video 元素顯示來自 getUserMedia() 的本機串流,另一個顯示透過 RTCPeerconnection 串流的遠端影片。

  1. 在瀏覽器主控台中查看記錄。

b 分****分

  • 這個應用程式僅支援一對一視訊通訊功能。如果想讓多人共用相同的視訊通訊聊天室,你該如何調整設計?
  • 範例中的會議室名稱是「foo」,硬式編碼。啟用其他會議室名稱的最佳方式為何?
  • 使用者如何共用會議室名稱?請試著建立替代聊天室名稱的分享方式。
  • 如何變更應用程式?

訣竅

  • 前往 chrome://webrtc-internals 找出 WebRTC 統計資料及偵錯資料。
  • 使用 WebRTC 疑難排解工具來檢查本機環境,並測試攝影機和麥克風。
  • 如果您在快取時遇到問題,請嘗試下列做法:
  1. 按下 Control 並點選 [重新載入此頁面]
  2. 重新啟動瀏覽器。
  3. 透過指令列執行 npm cache clean

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

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

運作方式

您之前已經學會如何使用 RTCDataChannel 交換簡訊。這個步驟可讓您共用整個檔案。在本範例中,相片是以getUserMedia()拍攝。

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

  1. 建立資料管道。

在此步驟中,您並未將任何媒體串流加入對等連線。

  1. 使用 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 the 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 approximately 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 applicable.
  if (len % CHUNK_LEN) {
    console.log('last ' + len % CHUNK_LEN + ' byte(s)');
    dataChannel.send(img.data.subarray(n * CHUNK_LEN));
  }
}

接收端會將資料管道訊息位元組轉換為圖片,並向使用者顯示圖片:

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');
  // The trail is the element that holds 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);
}

取得程式碼

  1. work 資料夾的內容替換為 step-06 的內容。

work」中的「index.html」檔案現在應如下所示:**

<!DOCTYPE html>
<html>

<head>

  <title>Real-time communication with WebRTC</title>

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

</head>

<body>

  <h1>Real-time 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>
  1. 如果您不是從 work 目錄追蹤這個程式碼研究室,您可能需要安裝 step-06 資料夾或目前工作資料夾的依附元件。只要從工作目錄執行下列指令即可:
npm install
  1. 安裝完成後,如果您的 Node.js 伺服器並未執行,請從 work 目錄執行下列指令,以啟動該伺服器:
node index.js
    Make sure that you're using the version of `index.js` that implements Socket.IO and 

完成變更後,請記得重新啟動 Node.js 伺服器。

如要進一步瞭解 Node 和 Socket.IO,請參閱「設定信號」一節

服務來交換訊息。

  1. 如有需要,請按一下 [允許],允許應用程式使用網路攝影機。

應用程式會建立隨機房間 ID,並將 ID 加到網址中。

  1. 在新的瀏覽器分頁或視窗中開啟網址列中的網址。
  2. 按一下 [Snap &&;;傳送],然後查看頁面底部其他分頁中的 [收到的相片]

應用程式會在不同的分頁間移動相片。

如下所示:

911b40f36ba6ba8.png

累計獎勵積分

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

瞭解詳情

9. 恭喜

您開發了一個提供即時影片串流與資料交換的應用程式!

瞭解詳情