1. 簡介
WebRTC 是開放原始碼專案,可針對網頁和原生應用程式中的音訊、影片和資料進行即時通訊。
WebRTC 有多個 JavaScript API,點選連結即可查看示範。
getUserMedia()
:擷取音訊和影片。MediaRecorder
:錄製音訊和影片。RTCPeerConnection
:在使用者之間串流播放音訊和影片。RTCDataChannel
:在使用者之間串流資料。
我可以在哪裡使用 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 線上應用程式商店進行安裝。
安裝 Chrome 網路伺服器應用程式後,請在書籤列、新分頁或應用程式啟動器中按一下 Chrome 應用程式捷徑:
按一下「網路伺服器」圖示:
接下來,系統會顯示這個對話方塊,讓您設定本機網路伺服器:
按一下「選取資料夾」按鈕,然後選取剛才建立的「work」資料夾。這樣就能透過「網路伺服器網址」部分,醒目顯示的網址在 Chrome 中查看處理中的工作。
在「Options」下方,勾選「自動顯示 index.html」旁的方塊,如下所示:
接著,將標示「Web Server: STARTED」(網路伺服器:STARTED) 的切換鈕滑動至左側,再回到右側,藉此停止並重新啟動伺服器。
現在,按一下醒目顯示的網路伺服器網址,透過網路瀏覽器造訪你的工作網站。您應該會看到與 work/index.html 對應的頁面,如下所示:
顯然這個應用程式並沒有什麼效果。目前為止,這只是我們用來確認網路伺服器運作是否正常的最基本架構。您將在後續步驟中新增功能和版面配置功能。
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,畫面應如下所示 (當然是透過網路攝影機拍攝畫面!):
運作方式
在 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 資料夾中。
提示
- 別忘了
video
元素上的autoplay
屬性。如果不這麼做,您只會看到單一影格! getUserMedia()
限制條件還有其他選項。前往 webrtc.github.io/samples/src/content/peerconnection/constraints 查看示範。如您所見,網站上有許多有趣的 WebRTC 範例。
最佳做法
- 確認影片元素不會溢位容器。新增
width
和max-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 物件位於相同頁面:pc1
和 pc2
。不太實用,但很適合示範 API 的運作方式。
在 WebRTC 對等體之間設定通話作業包含三項工作:
- 請在通話的兩端建立 RTCPeerConnection,然後在結尾處新增來自
getUserMedia()
的本機串流。 - 取得及分享網路資訊:潛在連線端點稱為 ICE 候選項目。
- 取得並分享本機和遠端的說明:SDP 格式的本機媒體中繼資料。
假設 Alice 和 Bob 想使用 RTCPeerConnection 設定視訊通訊,
首先,Alice 和 Bob 交換網路資訊。「尋找候選人」運算式是指使用 ICE 架構尋找網路介面和通訊埠的程序。
- Alice 使用
onicecandidate (addEventListener('icecandidate'))
處理常式建立 RTCPeerConnection 物件。這會對應至 main.js 的下列程式碼:
let localPeerConnection;
localPeerConnection = new RTCPeerConnection(servers);
localPeerConnection.addEventListener('icecandidate', handleConnection);
localPeerConnection.addEventListener(
'iceconnectionstatechange', handleConnectionChange);
- 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 中的
onicecandidate
處理常式。 - Alice 將序列化的候選資料傳送給 Bob。在實際的應用程式中,這項程序 (稱為「信號」) 是透過訊息服務進行,稍後您將在後續步驟中瞭解相關操作方法。當然,在這個步驟中,兩個 RTCPeerConnection 物件都位於相同頁面,不需要外部訊息就能直接通訊。
- 當小明收到來自 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 (稱為「方案」和「答案」),要求交換媒體設定資訊:
- Alice 會執行 RTCPeerConnection
createOffer()
方法。傳回的承諾會提供 RTCSessionDescription:Alice 的本機工作階段說明:
trace('localPeerConnection createOffer start.');
localPeerConnection.createOffer(offerOptions)
.then(createdOffer).catch(setSessionDescriptionError);
- 如果成功,Alice 會使用
setLocalDescription()
設定本機說明,然後透過信號管道,將此工作階段說明傳送給 Bob。 - Bob 使用
setRemoteDescription()
將說明設為遠端說明, - Bob 執行 RTCPeerConnection
createAnswer()
方法,並將他從 Alice 提供的遠端說明傳給該方法,這樣系統就能產生與她相容的本機工作階段。createAnswer()
承諾會傳遞 RTCSessionDescription:Bob 將這項承諾設為本機說明,並傳送給 Alice。 - 她在取得 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);
}
- 重要通知!
獎勵積分
- 請造訪 chrome://webrtc-internals。這會提供 WebRTC 統計資料和偵錯資料。(如需 Chrome 網址的完整清單,請前往 chrome://about)。
- 使用 CSS 設定頁面樣式:
- 並排顯示影片。
- 將按鈕的寬度放大,文字越大。
- 確保版面配置可在行動裝置上正常運作。
- 透過 Chrome 開發人員工具控制台查看
localStream
、localPeerConnection
和remotePeerConnection
。 - 從控制台查看
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 呼叫:app 和 code。通話設定時間不到 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 進一步瞭解選項。
獎勵積分
- 使用 SCTP 時,WebRTC 資料管道使用的通訊協定,會預設啟用可靠且已排序的資料傳送功能。RTCDataChannel 何時需要提供可靠的資料傳送?在何種情況下效能可能更重要,即使這表示資料遺失也沒關係?
- 使用 CSS 改善網頁版面配置,並在「dataChannelReceive」中加入預留位置屬性文字區域
- 在行動裝置上測試頁面。
您學到的內容
在這個步驟中,您已瞭解如何:
- 在兩個 WebRTC 對等端之間建立連線。
- 在對等端之間交換文字資料。
此步驟的完整版本位於 step-03 資料夾中。
瞭解詳情
- WebRTC 資料管道 (幾年前但仍值得閱讀)
- 為什麼 WebRTC 資料管道選擇 SCTP?
下一步
您已經瞭解如何在同一個網頁上交換同儕的資料,但要如何在不同的機器之間交換資料?首先,您需要設定信號管道,以便交換中繼資料訊息。瞭解下一個步驟吧!
7. 設定用來交換訊息的訊號服務
課程內容
這個步驟將說明如何:
- 使用
npm
安裝 package.json 中指定的專案依附元件 - 執行 Node.js 伺服器,並使用節點靜態服務提供靜態檔案。
- 使用 Socket.IO 在 Node.js 上設定訊息傳遞服務。
- 請使用這組號碼建立「房間」以及交換訊息
此步驟的完整版本位於 step-04 資料夾中。
概念
為了設定及維護 WebRTC 呼叫,WebRTC 用戶端 (對等互連) 需要交換中繼資料:
- 候選人 (聯播網) 資訊,
- 提供與答案訊息,提供解析度和轉碼器等媒體相關資訊。
換句話說,您必須先交換中繼資料,才能進行音訊、影片或資料的點對點串流。這項程序稱為信號。
在上述步驟中,傳送者和接收方 RTCPeerConnection 物件都位於同一個網頁,因此「signaling」就是在物件之間傳遞中繼資料
在實際應用程式中,傳送者和接收者 RTCPeerConnections 會在不同裝置的網頁上執行,而您需要讓對方與中繼資料通訊。
針對這類作業,您需要使用「信號伺服器」:一種伺服器,可在 WebRTC 用戶端 (對等互連) 之間傳送訊息。實際的訊息為純文字:字串化的 JavaScript 物件。
事前準備:安裝 Node.js
為執行本程式碼研究室的後續步驟 (資料夾 step-04 到 step-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
您應該會看到結尾類似下方的安裝記錄:
如您所見,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 的記錄。
獎勵積分
- 以下是可能的替代訊息機制?使用「純粹」時可能會遇到哪些問題WebSocket?
- 擴大這個應用程式規模時可能會發生哪些問題?您是否能開發出測試數千或數百萬項同步聊天室要求的方法?
- 這個應用程式會使用 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 串流播放影片
查看瀏覽器控制台中的記錄功能。
獎勵積分
- 這個應用程式僅支援一對一視訊通訊。您希望讓多人共用同一個視訊聊天室,您希望如何改變設計?
- 範例中的會議室名稱是 foo 硬式編碼。啟用其他房間名稱的最佳方式為何?
- 使用者如何分享會議室名稱?請嘗試建立共用會議室名稱的替代方案。
- 如何變更應用程式
您學到的內容
在這個步驟中,您已瞭解如何:
- 使用在 Node.js 上執行的 Socket.IO 執行 WebRTC 信號服務。
- 使用該服務在對等互連項目之間交換 WebRTC 中繼資料。
此步驟的完整版本位於 step-05 資料夾中。
提示
- chrome://webrtc-internals 提供 WebRTC 統計資料和偵錯資料。
- test.webrtc.org 可用於檢查本機環境及測試攝影機和麥克風。
- 如果無法順利快取,請嘗試下列做法:
- 按住 Ctrl 鍵並點選「重新載入」按鈕,強制重新整理
- 重新啟動瀏覽器
- 從指令列執行
npm cache clean
。
下一步
瞭解如何拍照、取得圖片資料,並與遠端的親朋好友分享。
9. 拍攝相片,並透過資料管道分享
課程內容
這個步驟將說明如何:
- 拍攝相片,並使用畫布元素取得相片資料。
- 與遠端使用者交換圖片資料。
此步驟的完整版本位於 step-06 資料夾中。
運作方式
您之前已經學會如何使用 RTCDataChannel 交換簡訊。
這個步驟可讓您分享整個檔案,在這個範例中為透過 getUserMedia()
拍攝的相片。
這個步驟的核心部分如下:
- 建立資料管道。請注意,在這個步驟中,您不會在對等點連線中新增任何媒體串流。
- 使用
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);
});
}
- 使用者按下「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);
}
- 當使用者按一下「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));
}
}
- 接收端會將資料管道訊息位元組轉換回圖片,並向使用者顯示圖片:
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 & 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 &傳送按鈕,然後查看頁面底部其他標籤中的「收到的項目」區域。應用程式會在分頁之間轉移相片。
畫面應如下所示:
獎勵積分
- 如何修改程式碼,以便分享任何類型的檔案?
瞭解詳情
- MediaStream Image Capture API:這個 API 可用來拍照及控制相機。不久後就會透過鄰近的瀏覽器推出!
- MediaRecorder API,可用於錄製音訊和視訊:示範、說明文件。
您學到的內容
- 如何使用畫布元素拍攝相片,並從中取得資料。
- 如何與遠端使用者交換資料。
此步驟的完整版本位於 step-06 資料夾中。
10. 恭喜
您建構了一個應用程式來支援即時影片串流和資料交換!
您學到的內容
在本程式碼研究室中,您瞭解如何:
- 從網路攝影機取得視訊。
- 透過 RTCPeerConnection 串流播放影片。
- 使用 RTCDataChannel 串流資料。
- 設定用來交換訊息的訊號服務。
- 結合對等互連連線與信號。
- 拍攝相片,並透過資料管道分享。
後續步驟
- 查看標準 WebRTC 即時通訊應用程式應用程式 RTC 的程式碼和架構:app、程式碼。
- 試試看前往 github.com/webrtc/samples 瀏覽即時示範。
瞭解詳情
- webrtc.org 提供了一系列開始使用 WebRTC 的資源。