使用 WebRTC 构建视频聊天 Web 应用






4.95/5 (36投票s)
WebRTC (Web 实时通信) 是一项新的 Web 标准,它允许浏览器之间进行点对点通信,以实现高质量的 RTC 应用。在我们的教程中,我们将展示如何使用它来构建一个视频聊天应用。
本教程以及其他有趣的在线 Web 开发教程和书籍均由 web-engineering.info 发布。
WebRTC 现状
WebRTC (Web 实时通信) 是一项新的 Web 标准,目前得到 Google、Mozilla 和 Opera 的支持。它允许浏览器之间进行点对点通信。其使命是为浏览器、移动平台和物联网 (WoT) 提供丰富、高质量的 RTC 应用,并允许它们通过一组通用协议进行通信。
Web 面临的最后一大挑战之一是能够在不使用特殊插件且无需付费的情况下,通过语音和视频实现人际通信。第一个 WebRTC 实现由 Ericsson 于 2011 年 5 月构建。WebRTC 定义了用于实时、无插件视频、音频和数据通信的开放标准。目前,许多 Web 服务已使用 RTC,但需要下载、原生应用或插件。其中包括 Skype、Facebook(使用 Skype)和 Google Hangouts(使用 Google Talk 插件)。下载、安装和更新插件可能很复杂、容易出错且令人烦恼,而且用户通常很难一开始就说服他们安装插件。
它是如何工作的?
通常,一个支持 WebRTC 的应用程序需要
- 获取音频、视频或其他数据流;
- 收集网络信息(例如 IP 地址和端口),并与其他 WebRTC 客户端交换这些信息;
- "信令"通信用于报告错误,以及发起或关闭会话;
- 客户端必须交换有关媒体的信息,例如分辨率和编解码器;
- 流式传输音频、视频或数据;
WebRTC 实现了三个 API
- MediaStream:允许客户端(例如 Web 浏览器)访问流,例如来自网络摄像头或麦克风的流;
- RTCPeerConnection:支持加密和带宽管理,实现音频或视频数据的高效传输;
- RTCDataChannel:允许任何通用数据的点对点通信。
理论上,可以在没有任何信令服务器组件的情况下创建简单的 WebRTC 应用程序。实际上,这样的应用程序意义不大,因为它只能在单个页面上使用,因此不支持任何真正的点对点连接。
MediaStream
MediaStream
API 处理一个或多个同步流。每个流都有输入和输出。getUserMedia
方法有三个参数:
- 一个 constraints 对象;
- 一个成功回调方法;
- 一个失败回调方法。
例如,本地网络摄像头流可以在 HTML5 video
元素中显示:
<!DOCTYPE html>
<html>
<head>
<script src="webrtc.js"></script>
<title>WebRTC Test</title>
</head>
<body>
<video id="localVideo" autoplay/>
<script>
window.addEventListener("load", function (evt) {
navigator.getUserMedia({ audio: true, video: true},
function(stream) {
var video = document.getElementById('localVideo');
video.src = window.URL.createObjectURL(stream);
},
function(err) {
console.log("The following error occurred: " + err.name);
}
);
});
</script>
</body>
</html>
RTCPeerConnection
RTCPeerConnection
接口表示本地计算机与远程对等点之间的 WebRTC 连接。它用于处理两个对等点之间的高效数据流。双方(呼叫方和被叫方)都需要设置自己的 RTCPeerConnection
实例来表示它们在点对点连接中的端点。通常,我们使用 RTCPeerConnection::onaddstream
事件回调来处理音频/视频流,例如将其分配给 HTML5 video
元素。
var peerConn= new RTCPeerConnection();
peerConn.onaddstream = function (evt) {
var videoElem = document.createElement("video");
document.appendChild(videoElem);
videoElem.src = URL.createObjectURL(evt.stream);
};
呼叫的发起者(呼叫方)需要创建一个 offer,并使用信令服务(例如,使用 WebSocket 的 NodeJS 服务器应用程序)将其发送给被叫方。
navigator.getUserMedia({video: true}, function(stream) {
videoElem.src = URL.createObjectURL(stream);
peerConn.addStream(stream);
peerConn.createOffer(function(offer) {
peerConn.setLocalDescription(new RTCSessionDescription(offer), function() {
// send the offer to a server to be forwarded to the other peer
}, error);
}, error);
});
被叫方,接收到 offer 并需要“应答”呼叫,必须创建一个 answer 并将其发送给呼叫方。
navigator.getUserMedia({video: true}, function(stream) {
videoElem.src = URL.createObjectURL(stream);
peerConn.addStream(stream);
peerConn.setRemoteDescription(new RTCSessionDescription(offer), function() {
peerConn.createAnswer(function(answer) {
peerConn.setLocalDescription(new RTCSessionDescription(answer), function() {
// send the answer to a server to be forwarded back to the caller
}, error);
}, error);
}, error);
});
setLocalDescription
方法接受三个参数:会话描述、成功回调方法和错误回调方法。此方法更改与连接关联的本地描述。描述定义了连接的属性,例如编解码器。
RTCPeerConnection 和服务器
在实际应用中,WebRTC 需要(通常很简单)服务器用于以下目的:
- 用户管理;
- 对等方之间交换信息;
- 关于媒体(如格式和视频分辨率)的数据交换。
- 连接需要穿越 NAT 网关和防火墙。
STUN 协议及其扩展 TURN 被 ICE 框架使用,以使 RTCPeerConnection
能够处理 NAT 穿透和其他网络特定细节。ICE 是用于连接对等点(例如两个视频聊天客户端)的框架。ICE 尝试通过 UDP 直接以尽可能低的延迟连接对等点。在此过程中,STUN 服务器只有一个任务:使 NAT 后面的对等点能够找出其公共地址和端口。Google 和 Mozilla 提供了一些 STUN 服务器,(目前)可以免费使用。例如,Google STUN 服务器用于获取 ICE 候选者,然后将它们转发给其他对等方。
var peerConnCfg = {'iceServers': [{'url': 'stun:stun.l.google.com:19302'}]},
peerConn= new RTCPeerConnection(peerConnCfg),
signalingChannel = new WebSocket('ws://my-websocket-server:port/');
peerConn.onicecandidate = function (evt) {
// send any ice candidates to the other peer, i.e., evt.candidate
signalingChannel.send(JSON.stringify({ "candidate": evt.candidate }));
};
signalingChannel.onmessage = function (evt) {
var signal = JSON.parse(evt.data);
if (signal.sdp)
peerConn.setRemoteDescription(new RTCSessionDescription(signal.sdp));
else if (signal.candidate)
peerConn.addIceCandidate(new RTCIceCandidate(signal.candidate));
};
signalingChannel
代表基于 WebSockets
、XHR
或其他方式的通信通道,其目的是帮助交换对等连接初始化所需的信息。
setRemoteDescription
方法接受三个参数:会话描述、成功回调方法和错误回调方法。此方法更改与连接关联的远程描述。描述定义了连接的属性,例如编解码器。
RTCDataChannel
RTCDataChannel 接口表示连接的两个对等点之间的双向数据通道。此类型的对象可以通过 RTCPeerConnection.createDataChannel()
创建,或者在现有 RTCPeerConnection
上的 RTCDataChannelEvent
类型的 datachannel
事件中接收。使用数据通道的能力是“自然的”,并利用基于消息样式事件的通信。
var peerConn= new RTCPeerConnection(),
dc = peerConn.createDataChannel("my channel");
dc.onmessage = function (event) {
console.log("received: " + event.data);
};
dc.onopen = function () {
console.log("datachannel open");
};
dc.onclose = function () {
console.log("datachannel close");
};
构建一个简单的视频聊天 Web 应用
在本节中,我们将学习如何构建一个基本的视频聊天 Web 应用。它允许两个对等方之间进行视频通话,并显示本地和远程视频。在实际应用中,您需要处理复杂的情况、用户管理以及各种错误。在本教程中,我们将忽略错误情况,使我们的应用程序保持简单。
- 位于地球不同地点的两位朋友需要进行视频通话;
- 他们可以使用现代 Web 浏览器,例如 Google Chrome 或 Firefox;
- 他们可以通过可用的互联网连接(DSL、3G 或其他类型)访问 Web 应用程序 URL;
- 其中一位用户通过单击“视频通话”按钮发起视频通话;
- 两位用户都允许浏览器访问他们的网络摄像头和麦克风;
- 现在他们可以互相看到和听到对方,直到其中一位用户单击“结束通话”按钮。
HTML Web 用户界面
HTML 代码非常简单。我们只定义了相关的元素,并且为了简化,我们不使用 CSS 进行样式设置。
<!DOCTYPE html>
<html>
<head>
<script src="webrtc.js"></script>
<title>WebRTC Audio/Video-Chat</title>
</head>
<body>
<video id="remoteVideo" autoplay></video>
<video id="localVideo" autoplay muted></video>
<input id="videoCallButton" type="button" disabled value="Video Call"/>
<input id="endCallButton" type="button" disabled value="End Call"/>
<script type="text/javascript">
window.addEventListener("load", pageReady);
</script>
</body>
</html>
这里只有四个 HTML 元素是相关的:两个 video
元素,用于显示远程和本地视频;以及两个 input
元素,用于创建“视频通话”和“结束通话”按钮。代码末尾的 script
元素注册了一个 load
事件侦听器(在页面完全加载时执行)。相关的代码,包括 pageReady
方法的内容,都包含在通过 script
元素包含的 webrtc.js
文件中(参见 head
元素)。
基于 NodeJS 的 WebSocket 信令服务器
NodeJS 服务器应用程序有一个非常简单的任务:接收来自一个客户端的消息,并将其广播给所有其他客户端。这些消息是点对点连接初始化所需的信令信息。为此,我们使用 WebSockets
,这是现代浏览器中的内置 API,但需要为 NodeJS 安装 ws
模块。
首先,我们需要通过在 NodeJS 应用程序的根文件夹中的 shell 中执行 npm install
来安装所需的 NodeJS 模块(例如 ws
)。有关此模块的更多信息,请访问 npm ws 模块页面。
接下来,创建一个名为 server.js
的文件,内容如下:
const WebSocketServer = require('ws').Server, express = require('express'), https = require('https'), app = express(), fs = require('fs'); const pkey = fs.readFileSync('./ssl/key.pem'), pcert = fs.readFileSync('./ssl/cert.pem'), options = {key: pkey, cert: pcert, passphrase: '123456789'}; var wss = null, sslSrv = null; // use express static to deliver resources HTML, CSS, JS, etc) // from the public folder app.use(express.static('public')); // start server (listen on port 443 - SSL) sslSrv = https.createServer(options, app).listen(443); console.log("The HTTPS server is up and running"); // create the WebSocket server wss = new WebSocketServer({server: sslSrv}); console.log("WebSocket Secure server is up and running."); /** successful connection */ wss.on('connection', function (client) { console.log("A new WebSocket client was connected."); /** incomming message */ client.on('message', function (message) { /** broadcast message to all clients */ wss.broadcast(message, client); }); }); // broadcasting the message to all WebSocket clients. wss.broadcast = function (data, exclude) { var i = 0, n = this.clients ? this.clients.length : 0, client = null; if (n < 1) return; console.log("Broadcasting message to all " + n + " WebSocket clients."); for (; i < n; i++) { client = this.clients[i]; // don't send the message to the sender... if (client === exclude) continue; if (client.readyState === client.OPEN) client.send(data); else console.error('Error: the client state is ' + client.readyState); } };
注意: 由于 WebRTC 仅与 SSL 配合使用,为了方便您,我们提供了一个免费的自签名 SSL 证书以及此应用程序。此证书不得用于演示应用程序之外的任何其他用途。此外,Web 浏览器会抱怨 SSL 证书的有效性,因为它不是由受认可的机构签名的。这意味着您应该将其添加到例外列表中,以便能够访问应用程序。否则,您可以随意使用自己的证书,这意味着您需要替换 ssl
子文件夹中的两个 .pem
文件。
该应用程序通过端口 443 上的安全 WebSockets 进行通信。如果需要,您可以将此端口更改为其他端口。上面的代码只是允许 WebSocket 连接,并将从一个客户端接收到的所有消息广播到所有其他客户端(不包括发送者)。
要启动服务器应用程序,请在创建具有上述内容的文件所在的文件夹中执行 node server.js
。如果一切顺利,您应该看不到任何错误消息,并且服务器会等待 WebSocket 连接。最后,使用 Web 浏览器导航到 http://your.domain
,您应该会看到应用程序启动页面。仅使用 localhost
才能在本地玩此应用程序,并且要实现两个具有互联网连接的对等方之间的 WebRTC 连接,需要使用具有公共 IP 地址的实时服务器。
如果您位于公司防火墙后面,则可能除了 80(可能还有 443)端口外,所有端口都已关闭。在这种情况下,可以使用 mod_proxy_stunnel
Apache 模块,该模块允许通过端口 80 代理 WebSocket 通信。该模块从 2.4.5 版本开始随 Apache 一起提供。但是,大多数稳定的 Linux 系统,包括 CentOS 6.x,仅提供早期版本的 Apache,例如 2.2.x。此模块的预编译版本(Apache 2.2.15,可从 CentOS 6.7 存储库获得)可在我们的服务器上下载。此外,您必须修改 Apache 配置文件,即 httpd.conf
文件(通常位于 /etc/httpd/conf/
下),并添加以下行:
LoadModule proxy_wstunnel_module modules/mod_proxy_wstunnel.so ProxyPass /websocket/ ws://:3434/ ProxyPassReverse /websocket/ ws://:3434/
最后,通过执行 service httpd restart
命令重启 Apache Web 服务器,这可能需要 root
权限(即,您可能需要使用 sudo
或以 root
用户登录)。上面配置行中的“websocket”路径可以替换为您喜欢的任何内容,但请记住,这是 WebSocket 客户端应用程序用于访问服务器的 URL 的最后一部分。同时请记住使用与 server.js
中使用的端口号相同(例如 3434)。
注意: 上述信息和示例是为运行 Apache Web 服务器 2.2.15(来自官方 CentOS 6.7 存储库)的 CentOS 6.7 Linux 发行版提供的。不同的 Linux 发行版或其他 Apache 版本可能工作方式相同,也可能不同,因此我们无法保证。
客户端 JavaScript 代码
在本节中,我们将讨论 webrtc.js
文件的内容。该文件的第一部分定义了全局变量:
var localVideoElem = null, remoteVideoElem = null, localVideoStream = null, videoCallButton = null, endCallButton = null, peerConn = null, wsc = new WebSocket('ws://my-web-domain.de/websocket/'), peerConnCfg = {'iceServers': [{'url':'stun:stun.services.mozilla.com'}, {'url':'stun:stun.l.google.com:19302'}] };
相关的变量是 wsc
,代表一个新的 WebSocket
连接(请记住将 ws://my-web-domain.de/websocket/
替换为您自己的 URL),以及 peerConnCfg
,它指定了用于初始化新的 RTCPeerConnection
的配置参数。我们使用 Mozilla(以及作为备选的 Google)STUN 服务。
localVideoElem
、remoteVideoElem
、videoCallButton
和 endCallButton
用于获取对表示本地和远程 video
容器(HTML5 video
元素)以及用于发起和结束通话的两个按钮(具有 type="button"
的 HTML input
元素)的引用。最后,localVideoStream
将保留对本地视频流的引用,以便在通话结束时我们可以关闭它(释放视频和音频设备)。
此外,我们为 load
事件定义了 pageReady
回调方法:
function pageReady() {
videoCallButton = document.getElementById("videoCallButton");
endCallButton = document.getElementById("endCallButton");
localVideo = document.getElementById('localVideo');
remoteVideo = document.getElementById('remoteVideo');
// check browser WebRTC availability
if (navigator.getUserMedia) {
videoCallButton = document.getElementById("videoCallButton");
endCallButton = document.getElementById("endCallButton");
localVideo = document.getElementById('localVideo');
remoteVideo = document.getElementById('remoteVideo');
videoCallButton.removeAttribute("disabled");
videoCallButton.addEventListener("click", initiateCall);
endCallButton.addEventListener("click", function (evt) {
wsc.send(JSON.stringify({"closeConnection": true }));
});
} else {
alert("Sorry, your browser does not support WebRTC!")
}
};
在采取任何进一步行动之前,我们需要检查浏览器是否支持所需的 WebRTC 功能(避免出现无明显原因而无法工作的奇怪情况)。我们通过检查 navigator
全局对象中是否存在 getUserMedia
方法来做到这一点。如果找不到该方法,则“视频通话”按钮将保持禁用状态(无法发起呼叫!),并且我们会使用 alert
提供警告/错误消息。如果支持 WebRTC,则启用“视频通话”按钮,并为其分配一个 click
事件侦听器,以便在单击“视频通话”按钮时执行 initiateCall
方法。同样,为“结束通话”按钮分配一个 click
事件侦听器(有关更多详细信息,请在本教程后面讨论)。
接下来,我们处理呼叫方和被叫方之间的 WebSocket 消息交换:
wsc.onmessage = function (evt) {
var signal = JSON.parse( evt.data);
if (!peerConn) answerCall();
if (signal.sdp) {
peerConn.setRemoteDescription( new RTCSessionDescription( signal.sdp));
} else if (signal.candidate) {
peerConn.addIceCandidate( new RTCIceCandidate( signal.candidate));
} else if (signal.closeConnection){
endCall();
}
};
当单击“视频通话”按钮时,会创建一个对等连接(并将其分配给 peerConn
变量)。如果不存在这样的(RTCPeerConnection
)对象,则意味着我们处理的是被叫方的情况,因此会收到一个呼叫,在我们的简单应用程序中,该呼叫会自动应答,方法是调用 answerCall
方法。在更复杂的实际应用中,可能会使用响铃音频信号,被叫方可以通过单击“接听电话”按钮来接听电话,但在我们的示例中,我们保持简单,因此呼叫会自动应答。嗯,更确切地说,这是一种半自动应答,因为被叫方的 Web 浏览器会请求使用视频和/或音频设备的权限,因此用户可以接受(或拒绝)这些权限,以便应答(或拒绝)呼叫。
两个对等方需要交换本地和远程的音频和视频媒体信息,例如分辨率和编解码器功能。通过使用会话描述协议 (SDP) 交换 offer 和 answer 来进行媒体配置信息的信令。
发起呼叫
现在让我们看看 initiateCall
方法:
function initiateCall() {
prepareCall();
navigator.getUserMedia({"audio": true, "video": true }, function (stream) {
localVideo.src = URL.createObjectURL( stream);
peerConn.addStream( stream);
createAndSendOffer();
}, function (error) { console.log( error);});
};
首先,我们进行一些呼叫的初始准备(稍后会对此进行更详细的解释)。然后,使用 getUserMedia
获取本地视频流,并将其分配给我们希望在页面上显示的 video
元素(例如,在我们的例子中是 id 为 localVideo
的 video
元素)。最后,我们创建一个连接 offer 并通过调用 createAndSendOffer
方法将其发送给另一个对等方,该方法稍后在本教程中进行解释。
prepareCall
方法(如下所示)负责创建 RTCPeerConnection
实例并分配所需的事件侦听器。
function prepareCall() {
peerConn = new RTCPeerConnection( peerConnCfg);
peerConn.onicecandidate = onIceCandidateHandler;
peerConn.onaddstream = onAddStreamHandler;
};
function onIceCandidateHandler( evt) {
if (!evt || !evt.candidate) return;
wsc.send(JSON.stringify({"candidate": evt.candidate }));
};
function onAddStreamHandler( evt) {
videoCallButton.setAttribute("disabled", true);
endCallButton.removeAttribute("disabled");
remoteVideo.src = URL.createObjectURL( evt.stream);
};
任何 ICE 候选者都会被转发到信令服务器,以便发送给另一个对等方(参见 onIceCandidateHandler
),而当接收到远程流时,我们会将其分配给我们的 video
元素进行显示(例如,在我们的例子中是 id 为 remoteVideo
的 video
元素)。
呼叫方还需要最后一步,即创建连接 offer 并将其发送给另一个对等方。
function createAndSendOffer() { peerConn.createOffer( function (offer) { var off = new RTCSessionDescription( offer); peerConn.setLocalDescription( new RTCSessionDescription( off), function() { wsc.send(JSON.stringify({"sdp": off })); }, function(error) { console.log( error); } ); }, function (error) { console.log( error); } ); };
offer 包含有关两个对等方如何连接的信息。offer 消息由信令服务器转发给另一个对等方,后者通过使用 onmessage
事件侦听器(如本教程前面所述)获悉此消息。
接听呼叫
与呼叫发起类似,创建 RTCPeerConnection 并分配事件侦听器。此外,通过使用 getuserMedia
获取本地流,并将其分配给 video
元素。最后,创建一个 answer 并将其发送,以响应收到的 offer。
function answerCall() { prepareCall(); // get the local stream, show it in the local video element and send it navigator.getUserMedia({ "audio": true, "video": true }, function (stream) { localVideo.src = URL.createObjectURL( stream); peerConn.addStream(stream); createAndSendAnswer(); }, function(error) { console.log(error);}); };
createAndSendAnswer
将准备 answer,并通过 WebSocket 通道将其发送给信令服务器,信令服务器随后将其转发给另一个对等方,从而完成连接。
function createAndSendAnswer() { peerConn.createAnswer( function (answer) { var ans = new RTCSessionDescription( answer); peerConn.setLocalDescription( ans, function() { wsc.send(JSON.stringify({"sdp": ans })); }, function (error) { console.log( error); } ); }, function (error) { console.log( error); } ); }
结束通话
注意: 理论上,结束 WebRTC 呼叫可能稍微简单一些:关闭对等连接(即调用 peerConn.close()
),然后使用分配给 peerConn.oniceconnectionstatechange
的回调方法,并检查 peerConn.iceConnectionState === "closed"
。然而,我们发现这种方法存在两个问题:1)它似乎(至少不是每次都)在 Google Chrome 和 Firefox 中都无法正常工作;2)closed
连接状态也可能在对等连接暂时中断时发生(糟糕的互联网连接、一些大的延迟等),在许多情况下可以自动恢复(无需额外代码或管理),因此“通话结束”可能并不完全准确。因此,我们使用信令服务器来通知另一个对等方有关“真实通话结束”的请求。
在 pageReady
方法(在 HTML 页面完全加载时调用)中,我们添加了一个 click
事件侦听器,通过该侦听器,我们将 closeConnection
信号发送到我们的信令服务器,然后信令服务器将其转发给其他对等方。
function pageReady() { if(navigator.getUserMedia) { // ...some more code here... endCallButton.addEventListener("click", function (evt) { wsc.send(JSON.stringify({"closeConnection": true })); }); } else { alert("Sorry, your browser does not support WebRTC!") } };
endCall
方法的代码如下:
function endCall() { peerConn.close(); localVideoStream.getTracks().forEach( function (track) { track.stop(); }); localVideo.src = ""; remoteVideo.src = ""; videoCallButton.removeAttribute("disabled"); endCallButton.setAttribute("disabled", true); };
第一步是通过调用 close 方法来关闭 RTCPeerConnection
。此外,我们停止所有(视频)轨道,并重置远程和本地视频的流源,以便 video
HTML5 元素不再显示任何内容(如果未重置源,则最后一个画面帧将保持可见)。最后,我们负责启用“视频通话”按钮(允许新通话)并禁用“结束通话”按钮。
下载代码
完整的客户端和服务器源代码可在 GitHub 上下载。
当前浏览器支持
并非所有浏览器都支持 WebRTC。主要可以使用 Google Chrome、Firefox 和 Opera。对于 iOS,有 Bowser,一款支持 WebRTC 的开源 Web 浏览器。EDGE 浏览器也提供部分支持,而 Safari 完全不支持此技术。每个 Web 浏览器支持的 WebRTC 功能的完整列表可在 iswebrtcreadyyet.com 上找到。
注意: 从 2016 年 1 月 1 日起,使用 Google Chrome 和 Opera 配合基于 WebRTC 的应用程序仅可通过安全层进行,因此必须使用 HTTPS 而不是 HTTP。