- WebRTC 教程
- WebRTC - 首頁
- WebRTC - 概述
- WebRTC - 架構
- WebRTC - 環境
- WebRTC - MediaStream API
- WebRTC - RTCPeerConnection API
- WebRTC - RTCDataChannel API
- WebRTC - 傳送訊息
- WebRTC - 信令
- WebRTC - 瀏覽器支援
- WebRTC - 移動裝置支援
- WebRTC - 影片演示
- WebRTC - 語音演示
- WebRTC - 文字演示
- WebRTC - 安全性
- WebRTC 資源
- WebRTC - 快速指南
- WebRTC - 有用資源
- WebRTC - 討論
WebRTC - 文字演示
在本章中,我們將構建一個客戶端應用程式,允許兩個在不同裝置上的使用者使用 WebRTC 互相傳送訊息。我們的應用程式將有兩個頁面。一個用於登入,另一個用於向另一個使用者傳送訊息。
這兩個頁面將是div標籤。大多數輸入都是透過簡單的事件處理程式完成的。
信令伺服器
要建立 WebRTC 連線,客戶端必須能夠在不使用 WebRTC 對等連線的情況下傳輸訊息。這就是我們將使用 HTML5 WebSockets 的地方 - 兩個端點(一個 Web 伺服器和一個 Web 瀏覽器)之間的雙向套接字連線。現在讓我們開始使用 WebSocket 庫。建立server.js檔案並插入以下程式碼 -
//require our websocket library
var WebSocketServer = require('ws').Server;
//creating a websocket server at port 9090
var wss = new WebSocketServer({port: 9090});
//when a user connects to our sever
wss.on('connection', function(connection) {
console.log("user connected");
//when server gets a message from a connected user
connection.on('message', function(message) {
console.log("Got message from a user:", message);
});
connection.send("Hello from server");
});
第一行需要我們已經安裝的 WebSocket 庫。然後我們在埠 9090 上建立一個套接字伺服器。接下來,我們監聽connection事件。當用戶與伺服器建立 WebSocket 連線時,將執行此程式碼。然後,我們監聽使用者傳送的任何訊息。最後,我們向已連線的使用者傳送一個“來自伺服器的問候”響應。
在我們的信令伺服器中,我們將為每個連線使用基於字串的使用者名稱,以便我們知道將訊息傳送到哪裡。讓我們稍微修改一下我們的connection處理程式 -
connection.on('message', function(message) {
var data;
//accepting only JSON messages
try {
data = JSON.parse(message);
} catch (e) {
console.log("Invalid JSON");
data = {};
}
});
這樣我們只接受 JSON 訊息。接下來,我們需要將所有已連線的使用者儲存在某個地方。我們將為此使用一個簡單的 Javascript 物件。更改我們檔案頂部的內容 -
//require our websocket library
var WebSocketServer = require('ws').Server;
//creating a websocket server at port 9090
var wss = new WebSocketServer({port: 9090});
//all connected to the server users
var users = {};
我們將為來自客戶端的每條訊息新增一個type欄位。例如,如果使用者想要登入,他傳送login型別的訊息。讓我們定義它 -
connection.on('message', function(message) {
var data;
//accepting only JSON messages
try {
data = JSON.parse(message);
} catch (e) {
console.log("Invalid JSON");
data = {};
}
//switching type of the user message
switch (data.type) {
//when a user tries to login
case "login":
console.log("User logged:", data.name);
//if anyone is logged in with this username then refuse
if(users[data.name]) {
sendTo(connection, {
type: "login",
success: false
});
} else {
//save user connection on the server
users[data.name] = connection;
connection.name = data.name;
sendTo(connection, {
type: "login",
success: true
});
}
break;
default:
sendTo(connection, {
type: "error",
message: "Command no found: " + data.type
});
break;
}
});
如果使用者傳送帶有login型別的訊息,我們將 -
- 檢查是否有人已經使用此使用者名稱登入。
- 如果是,則告訴使用者他尚未成功登入。
- 如果沒有人使用此使用者名稱,我們將使用者名稱作為鍵新增到連線物件中。
- 如果命令無法識別,我們將傳送錯誤。
以下程式碼是用於向連線傳送訊息的輔助函式。將其新增到server.js檔案中 -
function sendTo(connection, message) {
connection.send(JSON.stringify(message));
}
當用戶斷開連線時,我們應該清理其連線。當觸發close事件時,我們可以刪除使用者。將以下程式碼新增到connection處理程式中 -
connection.on("close", function() {
if(connection.name) {
delete users[connection.name];
}
});
成功登入後,使用者希望呼叫另一個使用者。他應該向另一個使用者發出offer以實現它。新增offer處理程式 -
case "offer":
//for ex. UserA wants to call UserB
console.log("Sending offer to: ", data.name);
//if UserB exists then send him offer details
var conn = users[data.name];
if(conn != null){
//setting that UserA connected with UserB
connection.otherName = data.name;
sendTo(conn, {
type: "offer",
offer: data.offer,
name: connection.name
});
break;
首先,我們獲取我們嘗試呼叫的使用者connection。如果它存在,我們將向他傳送offer詳細資訊。我們還在connection物件中新增otherName。這是為了簡化以後查詢它。
對響應的答覆具有與我們在offer處理程式中使用的類似模式。我們的伺服器只是將所有訊息作為answer傳遞給另一個使用者。在offer處理程式之後新增以下程式碼 -
case "answer":
console.log("Sending answer to: ", data.name);
//for ex. UserB answers UserA
var conn = users[data.name];
if(conn != null) {
connection.otherName = data.name;
sendTo(conn, {
type: "answer",
answer: data.answer
});
}
break;
最後一部分是在使用者之間處理 ICE 候選。我們使用相同的技術,只是在使用者之間傳遞訊息。主要區別在於候選訊息可能每個使用者多次發生,並且以任何順序發生。新增candidate處理程式 -
case "candidate":
console.log("Sending candidate to:",data.name);
var conn = users[data.name];
if(conn != null) {
sendTo(conn, {
type: "candidate",
candidate: data.candidate
});
}
break;
為了允許我們的使用者斷開與另一個使用者的連線,我們應該實現結束通話功能。它還會告訴伺服器刪除所有使用者引用。新增leave處理程式 -
case "leave":
console.log("Disconnecting from", data.name);
var conn = users[data.name];
conn.otherName = null;
//notify the other user so he can disconnect his peer connection
if(conn != null) {
sendTo(conn, {
type: "leave"
});
}
break;
這也會向另一個使用者傳送leave事件,以便他可以相應地斷開其對等連線。我們還應該處理使用者從信令伺服器斷開連線的情況。讓我們修改我們的close處理程式 -
connection.on("close", function() {
if(connection.name) {
delete users[connection.name];
if(connection.otherName) {
console.log("Disconnecting from ", connection.otherName);
var conn = users[connection.otherName];
conn.otherName = null;
if(conn != null) {
sendTo(conn, {
type: "leave"
});
}
}
}
});
以下是我們信令伺服器的完整程式碼 -
//require our websocket library
var WebSocketServer = require('ws').Server;
//creating a websocket server at port 9090
var wss = new WebSocketServer({port: 9090});
//all connected to the server users
var users = {};
//when a user connects to our sever
wss.on('connection', function(connection) {
console.log("User connected");
//when server gets a message from a connected user
connection.on('message', function(message) {
var data;
//accepting only JSON messages
try {
data = JSON.parse(message);
} catch (e) {
console.log("Invalid JSON");
data = {};
}
//switching type of the user message
switch (data.type) {
//when a user tries to login
case "login":
console.log("User logged", data.name);
//if anyone is logged in with this username then refuse
if(users[data.name]) {
sendTo(connection, {
type: "login",
success: false
});
} else {
//save user connection on the server
users[data.name] = connection;
connection.name = data.name;
sendTo(connection, {
type: "login",
success: true
});
}
break;
case "offer":
//for ex. UserA wants to call UserB
console.log("Sending offer to: ", data.name);
//if UserB exists then send him offer details
var conn = users[data.name];
if(conn != null) {
//setting that UserA connected with UserB
connection.otherName = data.name;
sendTo(conn, {
type: "offer",
offer: data.offer,
name: connection.name
});
}
break;
case "answer":
console.log("Sending answer to: ", data.name);
//for ex. UserB answers UserA
var conn = users[data.name];
if(conn != null) {
connection.otherName = data.name;
sendTo(conn, {
type: "answer",
answer: data.answer
});
}
break;
case "candidate":
console.log("Sending candidate to:",data.name);
var conn = users[data.name];
if(conn != null) {
sendTo(conn, {
type: "candidate",
candidate: data.candidate
});
}
break;
case "leave":
console.log("Disconnecting from", data.name);
var conn = users[data.name];
conn.otherName = null;
//notify the other user so he can disconnect his peer connection
if(conn != null) {
sendTo(conn, {
type: "leave"
});
}
break;
default:
sendTo(connection, {
type: "error",
message: "Command not found: " + data.type
});
break;
}
});
//when user exits, for example closes a browser window
//this may help if we are still in "offer","answer" or "candidate" state
connection.on("close", function() {
if(connection.name) {
delete users[connection.name];
if(connection.otherName) {
console.log("Disconnecting from ", connection.otherName);
var conn = users[connection.otherName];
conn.otherName = null;
if(conn != null) {
sendTo(conn, {
type: "leave"
});
}
}
}
});
connection.send("Hello world");
});
function sendTo(connection, message) {
connection.send(JSON.stringify(message));
}
客戶端應用程式
測試此應用程式的一種方法是開啟兩個瀏覽器選項卡並嘗試互相傳送訊息。
首先,我們需要安裝bootstrap庫。Bootstrap 是一個用於開發 Web 應用程式的前端框架。您可以在https://bootstrap.tw/瞭解更多資訊。建立一個名為“textchat”的資料夾。這將是我們的根應用程式資料夾。在此資料夾內建立一個package.json檔案(它對於管理 npm 依賴項是必要的)並新增以下內容 -
{
"name": "webrtc-textochat",
"version": "0.1.0",
"description": "webrtc-textchat",
"author": "Author",
"license": "BSD-2-Clause"
}
然後執行npm install bootstrap。這將在textchat/node_modules資料夾中安裝 bootstrap 庫。
現在我們需要建立一個基本的 HTML 頁面。在根資料夾中建立一個index.html檔案,其中包含以下程式碼 -
<html>
<head>
<title>WebRTC Text Demo</title>
<link rel = "stylesheet" href = "node_modules/bootstrap/dist/css/bootstrap.min.css"/>
</head>
<style>
body {
background: #eee;
padding: 5% 0;
}
</style>
<body>
<div id = "loginPage" class = "container text-center">
<div class = "row">
<div class = "col-md-4 col-md-offset-4">
<h2>WebRTC Text Demo. Please sign in</h2>
<label for = "usernameInput" class = "sr-only">Login</label>
<input type = "email" id = "usernameInput"
class = "form-control formgroup" placeholder = "Login"
required = "" autofocus = "">
<button id = "loginBtn" class = "btn btn-lg btn-primary btnblock">
Sign in</button>
</div>
</div>
</div>
<div id = "callPage" class = "call-page container">
<div class = "row">
<div class = "col-md-4 col-md-offset-4 text-center">
<div class = "panel panel-primary">
<div class = "panel-heading">Text chat</div>
<div id = "chatarea" class = "panel-body text-left"></div>
</div>
</div>
</div>
<div class = "row text-center form-group">
<div class = "col-md-12">
<input id = "callToUsernameInput" type = "text"
placeholder = "username to call" />
<button id = "callBtn" class = "btn-success btn">Call</button>
<button id = "hangUpBtn" class = "btn-danger btn">Hang Up</button>
</div>
</div>
<div class = "row text-center">
<div class = "col-md-12">
<input id = "msgInput" type = "text" placeholder = "message" />
<button id = "sendMsgBtn" class = "btn-success btn">Send</button>
</div>
</div>
</div>
<script src = "client.js"></script>
</body>
</html>
此頁面應該很熟悉。我們添加了bootstrap css 檔案。我們還定義了兩個頁面。最後,我們建立了幾個文字欄位和按鈕來獲取使用者的輸入。在“聊天”頁面上,您應該看到帶有“chatarea”ID 的 div 標籤,所有我們的訊息都將顯示在此處。請注意,我們添加了指向client.js檔案的連結。
現在我們需要與我們的信令伺服器建立連線。在根資料夾中建立client.js檔案,其中包含以下程式碼 -
//our username
var name;
var connectedUser;
//connecting to our signaling server
var conn = new WebSocket('ws://:9090');
conn.onopen = function () {
console.log("Connected to the signaling server");
};
//when we got a message from a signaling server
conn.onmessage = function (msg) {
console.log("Got message", msg.data);
var data = JSON.parse(msg.data);
switch(data.type) {
case "login":
handleLogin(data.success);
break;
//when somebody wants to call us
case "offer":
handleOffer(data.offer, data.name);
break;
case "answer":
handleAnswer(data.answer);
break;
//when a remote peer sends an ice candidate to us
case "candidate":
handleCandidate(data.candidate);
break;
case "leave":
handleLeave();
break;
default:
break;
}
};
conn.onerror = function (err) {
console.log("Got error", err);
};
//alias for sending JSON encoded messages
function send(message) {
//attach the other peer username to our messages
if (connectedUser) {
message.name = connectedUser;
}
conn.send(JSON.stringify(message));
};
現在透過node server執行我們的信令伺服器。然後,在根資料夾中執行static命令並在瀏覽器中開啟頁面。您應該看到以下控制檯輸出 -
下一步是實現使用唯一使用者名稱的使用者登入。我們只需將使用者名稱傳送到伺服器,然後伺服器告訴我們它是否已被佔用。將以下程式碼新增到您的client.js檔案中 -
//******
//UI selectors block
//******
var loginPage = document.querySelector('#loginPage');
var usernameInput = document.querySelector('#usernameInput');
var loginBtn = document.querySelector('#loginBtn');
var callPage = document.querySelector('#callPage');
var callToUsernameInput = document.querySelector('#callToUsernameInput');
var callBtn = document.querySelector('#callBtn');
var hangUpBtn = document.querySelector('#hangUpBtn');
callPage.style.display = "none";
// Login when the user clicks the button
loginBtn.addEventListener("click", function (event) {
name = usernameInput.value;
if (name.length > 0) {
send({
type: "login",
name: name
});
}
});
function handleLogin(success) {
if (success === false) {
alert("Ooops...try a different username");
} else {
loginPage.style.display = "none";
callPage.style.display = "block";
//**********************
//Starting a peer connection
//**********************
}
};
首先,我們選擇頁面上一些元素的引用。然後,我們隱藏呼叫頁面。然後,我們在登入按鈕上新增一個事件監聽器。當用戶點選它時,我們將他的使用者名稱傳送到伺服器。最後,我們實現handleLogin回撥。如果登入成功,我們將顯示呼叫頁面,設定對等連線,並建立一個數據通道。
要使用資料通道啟動對等連線,我們需要 -
- 建立 RTCPeerConnection 物件
- 在我們的 RTCPeerConnection 物件中建立資料通道
將以下程式碼新增到“UI 選擇器塊”中 -
var msgInput = document.querySelector('#msgInput');
var sendMsgBtn = document.querySelector('#sendMsgBtn');
var chatArea = document.querySelector('#chatarea');
var yourConn;
var dataChannel;
修改handleLogin函式 -
function handleLogin(success) {
if (success === false) {
alert("Ooops...try a different username");
} else {
loginPage.style.display = "none";
callPage.style.display = "block";
//**********************
//Starting a peer connection
//**********************
//using Google public stun server
var configuration = {
"iceServers": [{ "url": "stun:stun2.1.google.com:19302" }]
};
yourConn = new webkitRTCPeerConnection(configuration, {optional: [{RtpDataChannels: true}]});
// Setup ice handling
yourConn.onicecandidate = function (event) {
if (event.candidate) {
send({
type: "candidate",
candidate: event.candidate
});
}
};
//creating data channel
dataChannel = yourConn.createDataChannel("channel1", {reliable:true});
dataChannel.onerror = function (error) {
console.log("Ooops...error:", error);
};
//when we receive a message from the other peer, display it on the screen
dataChannel.onmessage = function (event) {
chatArea.innerHTML += connectedUser + ": " + event.data + "<br />";
};
dataChannel.onclose = function () {
console.log("data channel is closed");
};
}
};
如果登入成功,應用程式將建立RTCPeerConnection物件並設定onicecandidate處理程式,該處理程式將所有找到的 icecandidates 傳送到另一個對等方。它還建立一個 dataChannel。請注意,在建立 RTCPeerConnection 物件時,建構函式中的第二個引數是可選的:[{RtpDataChannels: true}] 如果你正在使用 Chrome 或 Opera,則是強制性的。下一步是向另一個對等方發出 offer。一旦使用者收到 offer,他就會建立一個answer並開始交換 ICE 候選。將以下程式碼新增到client.js檔案中 -
//initiating a call
callBtn.addEventListener("click", function () {
var callToUsername = callToUsernameInput.value;
if (callToUsername.length > 0) {
connectedUser = callToUsername;
// create an offer
yourConn.createOffer(function (offer) {
send({
type: "offer",
offer: offer
});
yourConn.setLocalDescription(offer);
}, function (error) {
alert("Error when creating an offer");
});
}
});
//when somebody sends us an offer
function handleOffer(offer, name) {
connectedUser = name;
yourConn.setRemoteDescription(new RTCSessionDescription(offer));
//create an answer to an offer
yourConn.createAnswer(function (answer) {
yourConn.setLocalDescription(answer);
send({
type: "answer",
answer: answer
});
}, function (error) {
alert("Error when creating an answer");
});
};
//when we got an answer from a remote user
function handleAnswer(answer) {
yourConn.setRemoteDescription(new RTCSessionDescription(answer));
};
//when we got an ice candidate from a remote user
function handleCandidate(candidate) {
yourConn.addIceCandidate(new RTCIceCandidate(candidate));
};
我們在“呼叫”按鈕上添加了一個click處理程式,它會啟動一個 offer。然後,我們實現了onmessage處理程式預期的幾個處理程式。它們將非同步處理,直到兩個使用者都建立了連線。
下一步是實現結束通話功能。這將停止傳輸資料並告訴另一個使用者關閉資料通道。新增以下程式碼 -
//hang up
hangUpBtn.addEventListener("click", function () {
send({
type: "leave"
});
handleLeave();
});
function handleLeave() {
connectedUser = null;
yourConn.close();
yourConn.onicecandidate = null;
};
當用戶點選“結束通話”按鈕時 -
- 它將向另一個使用者傳送“leave”訊息。
- 它將關閉 RTCPeerConnection 以及資料通道。
最後一步是向另一個對等方傳送訊息。將“click”處理程式新增到“傳送訊息”按鈕 -
//when user clicks the "send message" button
sendMsgBtn.addEventListener("click", function (event) {
var val = msgInput.value;
chatArea.innerHTML += name + ": " + val + "<br />";
//sending a message to a connected peer
dataChannel.send(val);
msgInput.value = "";
});
現在執行程式碼。您應該能夠使用兩個瀏覽器選項卡登入到伺服器。然後,您可以與另一個使用者建立對等連線並向他傳送訊息,以及透過點選“結束通話”按鈕關閉資料通道。
以下是完整的client.js檔案 -
//our username
var name;
var connectedUser;
//connecting to our signaling server
var conn = new WebSocket('ws://:9090');
conn.onopen = function () {
console.log("Connected to the signaling server");
};
//when we got a message from a signaling server
conn.onmessage = function (msg) {
console.log("Got message", msg.data);
var data = JSON.parse(msg.data);
switch(data.type) {
case "login":
handleLogin(data.success);
break;
//when somebody wants to call us
case "offer":
handleOffer(data.offer, data.name);
break;
case "answer":
handleAnswer(data.answer);
break;
//when a remote peer sends an ice candidate to us
case "candidate":
handleCandidate(data.candidate);
break;
case "leave":
handleLeave();
break;
default:
break;
}
};
conn.onerror = function (err) {
console.log("Got error", err);
};
//alias for sending JSON encoded messages
function send(message) {
//attach the other peer username to our messages
if (connectedUser) {
message.name = connectedUser;
}
conn.send(JSON.stringify(message));
};
//******
//UI selectors block
//******
var loginPage = document.querySelector('#loginPage');
var usernameInput = document.querySelector('#usernameInput');
var loginBtn = document.querySelector('#loginBtn');
var callPage = document.querySelector('#callPage');
var callToUsernameInput = document.querySelector('#callToUsernameInput');
var callBtn = document.querySelector('#callBtn');
var hangUpBtn = document.querySelector('#hangUpBtn');
var msgInput = document.querySelector('#msgInput');
var sendMsgBtn = document.querySelector('#sendMsgBtn');
var chatArea = document.querySelector('#chatarea');
var yourConn;
var dataChannel;
callPage.style.display = "none";
// Login when the user clicks the button
loginBtn.addEventListener("click", function (event) {
name = usernameInput.value;
if (name.length > 0) {
send({
type: "login",
name: name
});
}
});
function handleLogin(success) {
if (success === false) {
alert("Ooops...try a different username");
} else {
loginPage.style.display = "none";
callPage.style.display = "block";
//**********************
//Starting a peer connection
//**********************
//using Google public stun server
var configuration = {
"iceServers": [{ "url": "stun:stun2.1.google.com:19302" }]
};
yourConn = new webkitRTCPeerConnection(configuration, {optional: [{RtpDataChannels: true}]});
// Setup ice handling
yourConn.onicecandidate = function (event) {
if (event.candidate) {
send({
type: "candidate",
candidate: event.candidate
});
}
};
//creating data channel
dataChannel = yourConn.createDataChannel("channel1", {reliable:true});
dataChannel.onerror = function (error) {
console.log("Ooops...error:", error);
};
//when we receive a message from the other peer, display it on the screen
dataChannel.onmessage = function (event) {
chatArea.innerHTML += connectedUser + ": " + event.data + "<br />";
};
dataChannel.onclose = function () {
console.log("data channel is closed");
};
}
};
//initiating a call
callBtn.addEventListener("click", function () {
var callToUsername = callToUsernameInput.value;
if (callToUsername.length > 0) {
connectedUser = callToUsername;
// create an offer
yourConn.createOffer(function (offer) {
send({
type: "offer",
offer: offer
});
yourConn.setLocalDescription(offer);
}, function (error) {
alert("Error when creating an offer");
});
}
});
//when somebody sends us an offer
function handleOffer(offer, name) {
connectedUser = name;
yourConn.setRemoteDescription(new RTCSessionDescription(offer));
//create an answer to an offer
yourConn.createAnswer(function (answer) {
yourConn.setLocalDescription(answer);
send({
type: "answer",
answer: answer
});
}, function (error) {
alert("Error when creating an answer");
});
};
//when we got an answer from a remote user
function handleAnswer(answer) {
yourConn.setRemoteDescription(new RTCSessionDescription(answer));
};
//when we got an ice candidate from a remote user
function handleCandidate(candidate) {
yourConn.addIceCandidate(new RTCIceCandidate(candidate));
};
//hang up
hangUpBtn.addEventListener("click", function () {
send({
type: "leave"
});
handleLeave();
});
function handleLeave() {
connectedUser = null;
yourConn.close();
yourConn.onicecandidate = null;
};
//when user clicks the "send message" button
sendMsgBtn.addEventListener("click", function (event) {
var val = msgInput.value;
chatArea.innerHTML += name + ": " + val + "<br />";
//sending a message to a connected peer
dataChannel.send(val);
msgInput.value = "";
});