WebRTC - 视频演示
在本章中,我们将构建一个客户端应用程序,允许两个使用不同设备的用户使用 WebRTC 进行通信。我们的应用程序将有两个页面。一个用于登录,另一个用于呼叫另一个用户。

这两个页面将是 div 标签。大多数输入都是通过简单的事件处理程序完成的。

信令服务器
要创建 WebRTC 连接,客户端必须能够在不使用 WebRTC 对等连接的情况下传输消息。在这里我们将使用 HTML5 WebSockets − 两个端点之间的双向套接字连接 − Web 服务器和 Web 浏览器。现在让我们开始使用 WebSocket 库。创建 server.js 文件并插入以下代码 −
//需要我们的 websocket 库
var WebSocketServer = require('ws').Server;
//在端口 9090 处创建一个 websocket 服务器
var wss = new WebSocketServer({port: 9090});
//当用户连接到我们的服务器时
wss.on('connection', function(connection) {
console.log("user connected");
//当服务器收到来自已连接用户的消息时
connection.on('message', function(message) {
console.log("Got message from a user:", message);
});
connection.send("Hello from server");
});
第一行需要我们已经安装的 WebSocket 库。然后我们在端口 9090 上创建一个套接字服务器。接下来,我们监听 connection 事件。当用户与服务器建立 WebSocket 连接时,将执行此代码。然后,我们监听用户发送的任何消息。最后,我们向已连接的用户发送响应,说"来自服务器的问候"。
在我们的信令服务器中,我们将为每个连接使用基于字符串的用户名,以便我们知道将消息发送到哪里。让我们稍微更改一下我们的连接处理程序 −
connection.on('message', function(message) {
var data;
//仅接受 JSON 消息
try {
data = JSON.parse(message);
} catch (e) {
console.log("Invalid JSON");
data = {};
}
});
这样我们只接受 JSON 消息。接下来,我们需要将所有连接的用户存储在某个地方。我们将使用一个简单的 Javascript 对象。更改文件顶部的 −
//需要我们的 websocket 库
var WebSocketServer = require('ws').Server;
//在端口 9090 处创建一个 websocket 服务器
var wss = new WebSocketServer({port: 9090});
//所有连接到服务器的用户
var users = {};
我们将为来自客户端的每条消息添加一个 type 字段。例如,如果用户想要登录,他会发送 login 类型的消息。让我们定义它 −
connection.on('message', function(message) {
var data;
//仅接受 JSON 消息
try {
data = JSON.parse(message);
} catch (e) {
console.log("Invalid JSON");
data = {};
}
//切换用户消息类型
switch (data.type) {
//当用户尝试登录时
case "login":
console.log("User logged:", data.name);
//如果有人用这个用户名登录则拒绝
if(users[data.name]) {
sendTo(connection, {
type: "login",
success: false
});
} else {
//在服务器上保存用户连接
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":
//例如,用户 A 想要呼叫用户 B
console.log("Sending offer to: ", data.name);
//如果 UserB 存在则向他发送优惠详情
var conn = users[data.name];
if(conn != null) {
//设置UserA与UserB连接
connection.otherName = data.name;
sendTo(conn, {
type: "offer",
offer: data.offer,
name: connection.name
});
}
break;
首先,我们获取我们尝试呼叫的用户的 connection。如果存在,我们会向他发送 offer 详细信息。我们还将 otherName 添加到 connection 对象。这样做是为了以后查找起来更简单。
对响应的回答与我们在 offer 处理程序中使用的模式类似。我们的服务器只是将所有消息作为 answer 传递给另一个用户。在 offer 处理程序后添加以下代码 −
case "answer":
console.log("Sending answer to: ", data.name);
//例如 UserB 回答 UserA
var conn = users[data.name];
if(conn != null) {
connection.otherName = data.name;
sendTo(conn, {
type: "answer",
answer: data.answer
});
}
break;
最后部分是处理用户之间的 ICE 候选。我们使用相同的技术,只是在用户之间传递消息。主要区别在于候选消息可能以任何顺序在每个用户上发生多次。添加 候选 处理程序 −
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;
//通知其他用户,以便他可以断开他的对等连接
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"
});
}
}
}
});
以下是我们信令服务器的完整代码 −
//需要我们的 websocket 库
var WebSocketServer = require('ws').Server;
//在端口 9090 处创建一个 websocket 服务器
var wss = new WebSocketServer({port: 9090});
//所有连接到服务器的用户
var users = {};
//当用户连接到我们的服务器时
wss.on('connection', function(connection) {
console.log("User connected");
//当服务器收到来自已连接用户的消息时
connection.on('message', function(message) {
var data;
//仅接受 JSON 消息
try {
data = JSON.parse(message);
} catch (e) {
console.log("Invalid JSON");
data = {};
}
//切换用户消息类型
switch (data.type) {
//when a user tries to login
case "login":
console.log("User logged", data.name);
//如果有人用这个用户名登录则拒绝
if(users[data.name]) {
sendTo(connection, {
type: "login",
success: false
});
} else {
//在服务器上保存用户连接
users[data.name] = connection;
connection.name = data.name;
sendTo(connection, {
type: "login",
success: true
});
}
break;
case "offer":
//例如,用户 A 想要呼叫用户 B
console.log("Sending offer to: ", data.name);
//如果 UserB 存在则向他发送优惠详情
var conn = users[data.name];
if(conn != null) {
//设置UserA与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);
//例如 UserB 回答 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;
//通知其他用户,以便他可以断开他的对等连接
if(conn != null) {
sendTo(conn, {
type: "leave"
});
}
break;
default:
sendTo(connection, {
type: "error",
message: "Command not found: " + data.type
});
break;
}
});
//当用户退出时,例如关闭浏览器窗口
//如果我们仍处于"offer","answer" 或 "candidate" 状态,这可能会有所帮助
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 应用程序的前端框架。您可以在 http://getbootstrap.com/ 了解更多信息。创建一个名为"videochat"的文件夹。这将是我们的根应用程序文件夹。在此文件夹中创建一个文件 package.json(它是管理 npm 依赖项所必需的),并添加以下内容 −
{
"name": "webrtc-videochat",
"version": "0.1.0",
"description": "webrtc-videochat",
"author": "Author",
"license": "BSD-2-Clause"
}
然后运行 npm install bootstrap。这将在 videochat/node_modules 文件夹中安装 bootstrap 库。
现在我们需要创建一个基本的 HTML 页面。使用以下代码在根文件夹中创建一个 index.html 文件 −
<html>
<head>
<title>WebRTC Video Demo</title>
<link rel = "stylesheet" href = "node_modules/bootstrap/dist/css/bootstrap.min.css"/>
</head>
<style>
body {
background: #eee;
padding: 5% 0;
}
video {
background: black;
border: 1px solid gray;
}
.call-page {
position: relative;
display: block;
margin: 0 auto;
width: 500px;
height: 500px;
}
#localVideo {
width: 150px;
height: 150px;
position: absolute;
top: 15px;
right: 15px;
}
#remoteVideo {
width: 500px;
height: 500px;
}
</style>
<body>
<div id = "loginPage" class = "container text-center">
<div class = "row">
<div class = "col-md-4 col-md-offset-4">
<h2>WebRTC Video Demo. Please sign in</h2>
<label for = "usernameInput" class = "sr-only">Login</label>
<input type = "email" id = "usernameInput" c
lass = "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">
<video id = "localVideo" autoplay></video>
<video id = "remoteVideo" autoplay></video>
<div class = "row text-center">
<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>
<script src = "client.js"></script>
</body>
</html>
这个页面你应该很熟悉。我们添加了 bootstrap css 文件。我们还定义了两个页面。最后,我们创建了几个文本字段和按钮,用于从用户那里获取信息。你应该看到两个视频元素,分别用于本地和远程视频流。请注意,我们添加了一个指向 client.js 文件的链接。
现在我们需要与信令服务器建立连接。使用以下代码在根文件夹中创建 client.js 文件 −
//我们的用户名
var name;
var ConnectedUser;
//连接到我们的信令服务器
var conn = new WebSocket('ws://localhost:9090');
conn.onopen = function () {
console.log("已连接到信令服务器");
};
//当我们从信令服务器收到消息时
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;
//当有人想打电话给我们时
case "offer":
handleOffer(data.offer, data.name);
break;
case "answer":
handleAnswer(data.answer);
break;
//当远程对等方向我们发送 candidate 时
case "candidate":
handleCandidate(data.candidate);
break;
case "leave":
handleLeave();
break;
default:
break;
}
};
conn.onerror = function (err) {
console.log("Got error", err);
};
//发送 JSON 编码消息的别名
function send(message) {
//将其他对等用户名附加到我们的消息中
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";
// 当用户点击按钮时登录
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";
//启动对等连接
}
};
首先,我们选择页面上元素的一些引用。然后我们隐藏呼叫页面。然后,我们在登录按钮上添加一个事件监听器。当用户单击它时,我们会将他的用户名发送到服务器。最后,我们实现 handleLogin 回调。如果登录成功,我们将显示呼叫页面并开始设置对等连接。
要启动对等连接,我们需要 −
- 从网络摄像头获取流。
- 创建 RTCPeerConnection 对象。
将以下代码添加到"UI 选择器块"−
var localVideo = document.querySelector('#localVideo');
var remoteVideo = document.querySelector('#remoteVideo');
var yourConn;
var stream;
修改 handleLogin 函数 −
function handleLogin(success) {
if (success === false) {
alert("Ooops...try a different username");
} else {
loginPage.style.display = "none";
callPage.style.display = "block";
//**********************
//启动对等连接
//**********************
//获取本地视频流
navigator.webkitGetUserMedia({ video: true, audio: true }, function (myStream) {
stream = myStream;
//在页面上显示本地视频流
localVideo.src = window.URL.createObjectURL(stream);
//使用Google公共stun服务器
var configuration = {
"iceServers": [{ "url": "stun:stun2.1.google.com:19302" }]
};
yourConn = new webkitRTCPeerConnection(configuration);
// 设置流监听
yourConn.addStream(stream);
//当远程用户将流添加到对等连接时,我们会显示它
yourConn.onaddstream = function (e) {
remoteVideo.src = window.URL.createObjectURL(e.stream);
};
// Setup ice handling
yourConn.onicecandidate = function (event) {
if (event.candidate) {
send({
type: "candidate",
candidate: event.candidate
});
}
};
}, function (error) {
console.log(error);
});
}
};
现在,如果您运行代码,页面应该允许您登录并在页面上显示本地视频流。

现在我们准备发起呼叫。首先,我们向另一个用户发送报价。一旦用户收到报价,他就会创建一个答案并开始交易 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");
});
}
});
//当有人向我们发送 offer 时
function handleOffer(offer, name) {
connectedUser = name;
yourConn.setRemoteDescription(new RTCSessionDescription(offer));
//创建对 offer 的答复
yourConn.createAnswer(function (answer) {
yourConn.setLocalDescription(answer);
send({
type: "answer",
answer: answer
});
}, function (error) {
alert("Error when creating an answer");
});
};
//当我们收到远程用户的答复时
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 处理程序,该处理程序会启动一个要约。然后,我们实现 onmessage 处理程序所期望的几个处理程序。它们将被异步处理,直到两个用户都建立连接。
最后一步是实现挂断功能。这将停止传输数据并告诉另一个用户关闭呼叫。添加以下代码 −
//hang up
hangUpBtn.addEventListener("click", function () {
send({
type: "leave"
});
handleLeave();
});
function handleLeave() {
connectedUser = null;
remoteVideo.src = null;
yourConn.close();
yourConn.onicecandidate = null;
yourConn.onaddstream = null;
};
当用户点击"挂断"按钮时 −
- 它将向其他用户发送"离开"消息
- 它将关闭 RTCPeerConnection 并在本地销毁连接
现在运行代码。您应该能够使用两个浏览器选项卡登录到服务器。然后,您可以调用选项卡并挂断电话。

以下是整个 client.js 文件 −
//我们的用户名
var name;
var connectedUser;
//连接到我们的信令服务器
var conn = new WebSocket('ws://localhost:9090');
conn.onopen = function () {
console.log("已连接到信令服务器");
};
//当我们从信令服务器收到消息时
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;
//当有人想打电话给我们时
case "offer":
handleOffer(data.offer, data.name);
break;
case "answer":
handleAnswer(data.answer);
break;
//当远程对等方向我们发送 candidate 时
case "candidate":
handleCandidate(data.candidate);
break;
case "leave":
handleLeave();
break;
default:
break;
}
};
conn.onerror = function (err) {
console.log("Got error", err);
};
//发送 JSON 编码消息的别名
function send(message) {
//将其他对等用户名附加到我们的消息中
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 localVideo = document.querySelector('#localVideo');
var remoteVideo = document.querySelector('#remoteVideo');
var yourConn;
var stream;
callPage.style.display = "none";
// 当用户点击按钮时登录
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";
//**********************
//启动对等连接
//**********************
//获取本地视频流
navigator.webkitGetUserMedia({ video: true, audio: true }, function (myStream) {
stream = myStream;
//在页面上显示本地视频流
localVideo.src = window.URL.createObjectURL(stream);
//使用Google公共stun服务器
var configuration = {
"iceServers": [{ "url": "stun:stun2.1.google.com:19302" }]
};
yourConn = new webkitRTCPeerConnection(configuration);
// 设置流监听
yourConn.addStream(stream);
//当远程用户将流添加到对等连接时,我们会显示它
yourConn.onaddstream = function (e) {
remoteVideo.src = window.URL.createObjectURL(e.stream);
};
// Setup ice handling
yourConn.onicecandidate = function (event) {
if (event.candidate) {
send({
type: "candidate",
candidate: event.candidate
});
}
};
}, function (error) {
console.log(error);
});
}
};
//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");
});
}
});
//当有人向我们发送 offer 时
function handleOffer(offer, name) {
connectedUser = name;
yourConn.setRemoteDescription(new RTCSessionDescription(offer));
//创建对 offer 的答复
yourConn.createAnswer(function (answer) {
yourConn.setLocalDescription(answer);
send({
type: "answer",
answer: answer
});
}, function (error) {
alert("Error when creating an answer");
});
};
//当我们收到远程用户的答复时
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;
remoteVideo.src = null;
yourConn.close();
yourConn.onicecandidate = null;
yourConn.onaddstream = null;
};
总结
此演示提供了每个 WebRTC 应用程序所需的基本功能。为了改进此演示,您可以通过 Facebook 或 Google 等平台添加用户标识,处理用户输入的无效数据。此外,WebRTC 连接可能会因多种原因而失败,例如不支持该技术或无法穿越防火墙。为了让任何 WebRTC 应用程序稳定,我们付出了很多努力。