WebRTC - 快速指南

WebRTC - 概述

随着 WebRTC(Web 实时通信) 的出现,Web 对实时通信已不再陌生。尽管它于 2011 年 5 月发布,但它仍在发展,其标准也在不断变化。 IETF(互联网工程任务组)http://tools.ietf.org/wg/rtcweb/下的WEB 浏览器实时通信工作组对一组协议进行了标准化,而W3C(万维网联盟)http://www.w3.org/2011/04/webrtc/下的Web 实时通信工作组对一组新的 API 进行了标准化。随着 WebRTC 的出现,现代网络应用程序可以轻松地将音频和视频内容流式传输给数百万人。

基本方案

WebRTC 允许您快速轻松地与其他网络浏览器建立点对点连接。要从头开始构建这样的应用程序,您需要大量的框架和库来处理数据丢失、连接中断和 NAT 遍历等常见问题。使用 WebRTC,所有这些都内置在浏览器中。这项技术不需要任何插件或第三方软件。它是开源的,其源代码可在http://www.webrtc.org/免费获取。

WebRTC API 包括媒体捕获、音频和视频的编码和解码、传输层和会话管理。

Basic Sc​​heme

媒体捕获

第一步是获取用户设备的摄像头和麦克风的访问权限。我们检测可用设备的类型,获取用户访问这些设备的权限并管理流。

音频和视频的编码和解码

通过互联网发送音频和视频数据流并非易事。这就是使用编码和解码的地方。这是将视频帧和音频波分割成更小的块并压缩它们的过程。此算法称为编解码器。有大量不同的编解码器,由具有不同业务目标的不同公司维护。WebRTC 内部也有许多编解码器,如 H.264、iSAC、Opus 和 VP8。当两个浏览器连接在一起时,它们会在两个用户之间选择最优化支持的编解码器。幸运的是,WebRTC 在后台完成大部分编码。

传输层

传输层管理数据包的顺序,处理数据包丢失和连接到其他用户。同样,WebRTC API 让我们可以轻松访问事件,这些事件会告诉我们连接何时出现问题。

会话管理

会话管理处理管理、打开和组织连接。这通常称为信令。如果您将音频和视频流传输给用户,那么传输附加数据也是有意义的。这是通过 RTCDataChannel API 实现的。

来自 Google、Mozilla、Opera 等公司的工程师们已经做出了巨大贡献,将这种实时体验带到了 Web 上。

浏览器兼容性

WebRTC 标准是 Web 上发展最快的标准之一,因此并不意味着每个浏览器都同时支持所有相同的功能。要检查您的浏览器是否支持 WebRTC,您可以访问 http://caniuse.com/#feat=rtcpeerconnection。 在所有教程中,我建议您使用 Chrome 来完成所有示例。

试用 WebRTC

现在就开始使用 WebRTC 吧。

单击加入

单击"加入"按钮。您应该会看到一个下拉通知。

单击允许

单击"允许"按钮开始将您的视频和音频流式传输到网页。您应该会看到自己的视频流。

打开 URL

现在在新的浏览器选项卡中打开您当前所在的 URL,然后单击"加入"。您应该会看到两个视频流 −一个来自您的第一个客户端,另一个来自第二个客户端。

Video Stream

现在您应该了解为什么 WebRTC 是一个强大的工具。

用例

实时网络为一系列全新的应用打开了大门,包括基于文本的聊天、屏幕和文件共享、游戏、视频聊天等。除了通信之外,您还可以将 WebRTC 用于其他目的,例如 −

  • 实时营销
  • 实时广告
  • 后台通信(CRM、ERP、SCM、FFM)
  • 人力资源管理
  • 社交网络
  • 约会服务
  • 在线医疗咨询
  • 金融服务
  • 监视
  • 多人游戏
  • 现场直播
  • 电子学习

总结

现在您应该清楚地了解了 WebRTC 一词。您还应该知道可以使用 WebRTC 构建哪些类型的应用程序,因为您已经在浏览器中尝试过它。总而言之,WebRTC 是一项非常有用的技术。

WebRTC - 架构

整体 WebRTC 架构非常复杂。

WebRTC 架构

在这里您可以找到三个不同的层 −

  • 面向 Web 开发人员的 API − 此层包含 Web 开发人员所需的所有 API,包括 RTCPeerConnection、RTCDataChannel 和 MediaStrean 对象。

  • 面向浏览器制造商的 API

  • 可覆盖 API,浏览器制造商可以挂钩。

传输组件允许跨各种类型的网络建立连接,而语音和视频引擎是负责将音频和视频流从声卡和摄像头传输到网络的框架。对于 Web 开发人员来说,最重要的部分是 WebRTC API。

如果我们从客户端-服务器端查看 WebRTC 架构,我们可以看到最常用的模型之一是受 SIP(​​会话发起协议)梯形启发的。

SIP 梯形

在此模型中,两个设备都运行来自不同服务器的 Web 应用程序。RTCPeerConnection 对象配置流,以便它们可以相互连接,点对点。此信令通过 HTTP 或 WebSockets 完成。

但最常用的模型是 Triangle −

Triangle Model

在此模型中,两个设备都使用相同的 Web 应用程序。它为 Web 开发人员在管理用户连接时提供了更大的灵活性。

WebRTC API

它由几个主要的 javascript 对象组成 −

  • RTCPeerConnection
  • MediaStream
  • RTCDataChannel

RTCPeerConnection 对象

此对象是 WebRTC API 的主要入口点。它帮助我们连接到对等点、初始化连接并附加媒体流。它还管理与另一个用户的 UDP 连接。

RTCPeerConnection 对象的主要任务是设置和创建对等连接。我们可以轻松地挂钩连接的关键点,因为当它们出现时,此对象会触发一组事件。这些事件使您可以访问我们的连接配置 −

RTCPeerConnection object

RTCPeerConnection 是一个简单的 javascript 对象,您可以通过这种方式轻松创建它−

[code] 
var conn = new RTCPeerConnection(conf); 

conn.onaddstream = function(stream) { 
   // use stream here 
}; 

[/code]

RTCPeerConnection 对象接受 conf 参数,我们将在后面的教程中介绍该参数。当远程用户向其对等连接添加视频或音频流时,将触发 onaddstream 事件。

MediaStream API

现代浏览器允许开发人员访问 getUserMedia API,也称为 MediaStream API。有三个关键功能点 −

  • 它允许开发人员访问代表视频和音频流的 stream 对象

  • 如果用户的设备上有多个摄像头或麦克风,它会管理输入用户设备的选择

  • 它提供了一个安全级别,在用户想要获取流时始终询问用户

为了测试此 API,让我们创建一个简单的 HTML 页面。它将显示单个 <video> 元素,请求用户允许使用摄像头并在页面上显示来自摄像头的实时流。创建一个 index.html 文件并添加 −

[code] 
<html>
 
   <head> 
      <meta charset = "utf-8"> 
   </head>
	
   <body> 
      <video autoplay></video> 
      <script src = "client.js"></script> 
   </body> 
	 
</html> 
[/code]

然后添加一个 client.js 文件 −

[code]
//检查浏览器是否支持 WebRTC

function hasUserMedia() { 
   navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia 
      || navigator.mozGetUserMedia || navigator.msGetUserMedia; 
   return !!navigator.getUserMedia; 
}
 
if (hasUserMedia()) { 
   navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia
      || navigator.mozGetUserMedia || navigator.msGetUserMedia;
		
   //从用户摄像头获取视频和音频流
   navigator.getUserMedia({ video: true, audio: true }, function (stream) { 
      var video = document.querySelector('video'); 
		
      //将流插入到视频标签中
      video.src = window.URL.createObjectURL(stream); 
   }, function (err) {}); 
	
}else {
   alert("Error. WebRTC is not supported!"); 
}
[/code]

现在打开 index.html,您应该会看到视频流显示您的脸部。

但要小心,因为 WebRTC 仅在服务器端工作。如果您只是用浏览器打开此页面,它将无法工作。您需要将这些文件托管在 Apache 或 Node 服务器上,或者您喜欢哪个服务器。

RTCDataChannel 对象

除了在对等点之间发送媒体流外,您还可以使用 DataChannel API 发送其他数据。此 API 与 MediaStream API 一样简单。主要工作是创建一个来自现有 RTCPeerConnection 对象的通道 −

[code]
var peerConn = new RTCPeerConnection();

//建立对等连接
//...
//建立对等连接结束
var dataChannel = peerConnection.createDataChannel("myChannel", dataChannelOptions);

// 在这里我们可以开始向另一个对等端发送直接消息
[/code]

这就是您所需要的,只需两行代码。其他一切都在浏览器的内部层完成。您可以在任何对等连接上创建通道,直到 RTCPeerConnectionobject 关闭。

摘要

您现在应该已经牢牢掌握了 WebRTC 架构。我们还介绍了 MediaStream、RTCPeerConnection 和 RTCDataChannel API。WebRTC API 是一个不断变化的目标,因此请始终跟上最新规范。

WebRTC - 环境

在开始构建 WebRTC 应用程序之前,我们应该设置编码环境。首先,您应该有一个文本编辑器或 IDE,您可以在其中编辑 HTML 和 Javascript。在阅读本教程时,您可能已经选择了首选的 IDE。至于我,我使用的是 WebStorm IDE。您可以在 https://www.jetbrains.com/webstorm/ 下载其试用版。我还使用 Linux Mint 作为我的首选操作系统。

常见 WebRTC 应用程序的另一个要求是拥有一个服务器来托管 HTML 和 Javascript 文件。代码无法通过双击文件来工作,因为除非文件由实际服务器提供,否则浏览器不允许连接到摄像头和麦克风。这样做显然是因为安全问题。

有很多不同的 Web 服务器,但在本教程中,我们将使用带有 node-static 的 Node.js。 −

  • 访问 https://nodejs.org/en/ 并下载最新的 Node.js 版本。

  • 将其解压到 /usr/local/nodejs 目录。

  • 打开 /home/YOUR_USERNAME/.profile 文件并将以下行添加到末尾 − export PATH=$PATH:/usr/local/nodejs/bin

  • 然后您可以重启电脑或者运行 source /home/YOUR_USERNAME/.profile

  • 现在命令行中应该可以使用 node 命令。npm 命令也可用。NMP 是 Node.js 的包管理器。您可以在 https://www.npmjs.com/ 了解更多信息。

  • 打开终端并运行 sudo npm install -g node-static。这将安装 Node.js 的静态 Web 服务器。

  • 现在导航到包含 HTML 文件的任何目录,并在目录中运行 static 命令以启动 Web 服务器。

  • 您可以导航到 http://localhost:8080 查看您的文件。

还有另一种安装 nodejs 的方法。只需在终端窗口中运行 sudo apt-get install nodejs

要测试您的 Node.js 安装,请打开您的终端并运行 node 命令。输入一些命令来检查它是如何工作的 −

打开终端

Node.js 运行 Javascript 文件以及在终端中输入的命令。创建一个 index.js 文件,其中包含以下内容 −

console.log("Testing Node.js");

然后运行 ​​node index 命令。您将看到以下内容 −

Run Node Terminal

在构建我们的信令服务器时,我们将使用 Node.js 的 WebSockets 库。要在终端中安装,请运行 npm install ws

为了测试我们的信令服务器,我们将使用 wscat 实用程序。要安装它,请在终端窗口中运行 npm install -g wscat

S.No 协议和说明
1 WebRTC 协议

WebRTC 应用程序使用 UDP(用户数据报协议)作为传输协议。当今大多数 Web 应用程序都是使用 TCP(传输控制协议)构建的

2 会话描述协议

SDP 是 WebRTC 的重要组成部分。它是一种用于描述媒体通信会话的协议。

3 寻找路线

为了连接到另一个用户,您应该在您自己的网络和其他用户的网络周围找到一条畅通的路径。但您使用的网络有可能有多个级别的访问控制,以避免安全问题。

4 流控制传输协议

通过对等连接,我们能够快速发送视频和音频数据。目前,使用 RTCDataChannel 对象时,SCTP 协议用于在我们当前设置的对等连接之上发送 blob 数据。

总结

在本章中,我们介绍了几种支持对等连接的技术,例如 UDP、TCP、STUN、TURN、ICE 和 SCTP。您现在应该对 SDP 的工作原理及其用例有了初步的了解。

WebRTC - MediaStream API

MediaStream API 旨在轻松访问本地摄像头和麦克风的媒体流。getUserMedia() 方法是访问本地输入设备的主要方式。

该 API 有几个关键点 −

  • 实时媒体流由视频或音频形式的 stream 对象表示

  • 它通过用户权限提供安全级别,在 Web 应用程序开始获取流之前询问用户

  • 输入设备的选择由 MediaStream API 处理(例如,当有两个摄像头或麦克风连接到设备时)

每个 MediaStream 对象包含多个 MediaStreamTrack 对象。它们代表来自不同输入设备的视频和音频。

每个 MediaStreamTrack 对象可能包含多个通道(右音频通道和左音频通道)。这些是 MediaStream API 定义的最小部分。

有两种方法可以输出 MediaStream 对象。首先,我们可以将输出渲染为视频或音频元素。其次,我们可以将输出发送到 RTCPeerConnection 对象,然后将其发送到远程对等体。

使用 MediaStream API

让我们创建一个简单的 WebRTC 应用程序。它将在屏幕上显示一个视频元素,请求用户使用摄像头的权限,并在浏览器中显示实时视频流。创建一个 index.html 文件 −

<!DOCTYPE html> 
<html lang = "en">
 
   <head> 
      <meta charset = "utf-8" /> 
   </head> 
	
   <body> 
      <video autoplay></video> 
      <script src = "client.js"></script> 
   </body>
	
</html>

然后创建 client.js 文件 并添加以下内容;

function hasUserMedia() { 
   //检查浏览器是否支持WebRTC
   return !!(navigator.getUserMedia || navigator.webkitGetUserMedia || 
      navigator.mozGetUserMedia); 
} 

if (hasUserMedia()) { 
   navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia
      || navigator.mozGetUserMedia; 
		
   //启用视频和音频通道
   navigator.getUserMedia({ video: true, audio: true }, function (stream) { 
      var video = document.querySelector('video'); 
		
      //将我们的流插入到视频标签中
      video.src = window.URL.createObjectURL(stream); 
   }, function (err) {}); 
} else { 
   alert("WebRTC is not supported"); 
}

在这里我们创建 hasUserMedia() 函数来检查是否支持 WebRTC。然后我们访问 getUserMedia 函数,其中第二个参数是接受来自用户设备的流的回调。然后我们使用 window.URL.createObjectURL 将我们的流加载到 video 元素中,这会创建一个表示参数中给出的对象的 URL。

现在刷新您的页面,单击"允许",您应该会在屏幕上看到您的脸。

Media Stream API

请记住使用 Web 服务器运行所有脚本。我们已经在 WebRTC 环境教程中安装了一个。

MediaStream API

属性

  • MediaStream.active(只读) − 如果 MediaStream 处于活动状态,则返回 true,否则返回 false。

  • MediaStream.ended(只读,已弃用) − 如果已在对象上触发结束 事件,则返回 true,这意味着已完全读取流,如果尚未到达流的末尾,则返回 false。

  • MediaStream.id(只读) − 对象的唯一标识符。

  • MediaStream.label(只读,已弃用) −用户代理分配的唯一标识符。

您可以在我的浏览器中看到上述属性的外观 −

Properties

事件处理程序

  • MediaStream.onactive − 当 MediaStream 对象变为活动状态时触发的 active 事件的处理程序。

  • MediaStream.onaddtrack − 当添加新的 MediaStreamTrack 对象时触发的 addtrack 事件的处理程序。

  • MediaStream.onended(已弃用) −当流式传输终止时触发的 ended 事件的处理程序。

  • MediaStream.oninactive − 当 MediaStream 对象变为非活动状态时触发的 inactive 事件的处理程序。

  • MediaStream.onremovetrack − 当从其中移除 MediaStreamTrack 对象时触发的 removetrack 事件的处理程序。

方法

  • MediaStream.addTrack() − 将作为参数给出的 MediaStreamTrack 对象添加到 MediaStream。如果已添加该轨道,则不会发生任何事情。

  • MediaStream.clone() − 返回具有新 ID 的 MediaStream 对象的克隆。

  • MediaStream.getAudioTracks() − 从 MediaStream 对象返回音频 MediaStreamTrack 对象的列表。

  • MediaStream.getTrackById() − 根据 ID 返回轨道。如果参数为空或未找到 ID,则返回 null。如果多个轨道具有相同的 ID,则返回第一个轨道。

  • MediaStream.getTracks() −返回 MediaStream 对象中的所有 MediaStreamTrack 对象的列表。

  • MediaStream.getVideoTracks() − 返回 MediaStream 对象中的视频 MediaStreamTrack 对象的列表。

  • MediaStream.removeTrack() − 从 MediaStream 中删除作为参数给出的 MediaStreamTrack 对象。如果轨道已被删除,则不会发生任何事情。

要测试上述 API,请按以下方式更改 index.html

<!DOCTYPE html> 
<html lang = "en">
 
   <head> 
      <meta charset = "utf-8" /> 
   </head>
	
   <body> 
      <video autoplay></video> 
      <div><button id = "btnGetAudioTracks">getAudioTracks()
         </button></div> 
      <div><button id = "btnGetTrackById">getTrackById()
         </button></div> 
      <div><button id = "btnGetTracks">getTracks()</button></div> 
      <div><button id = "btnGetVideoTracks">getVideoTracks()
         </button></div> 
      <div><button id = "btnRemoveAudioTrack">removeTrack() - audio
         </button></div> 
      <div><button id = "btnRemoveVideoTrack">removeTrack() - video
         </button></div> 
      <script src = "client.js"></script> 
   </body> 
	
</html>

我们添加了几个按钮来试用几个 MediaStream API。然后我们应该为新创建的按钮添加事件处理程序。按如下方式修改 client.js 文件 −

var stream;
  
function hasUserMedia() { 
   //检查浏览器是否支持WebRTC
   return !!(navigator.getUserMedia || navigator.webkitGetUserMedia || 
      navigator.mozGetUserMedia); 
} 
 
if (hasUserMedia()) {
   navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia
      || navigator.mozGetUserMedia; 
		
   //启用视频和音频通道
   navigator.getUserMedia({ video: true, audio: true }, function (s) { 
      stream = s; 
      var video = document.querySelector('video'); 
		
      //将我们的流插入到视频标签中     
      video.src = window.URL.createObjectURL(stream); 
   }, function (err) {}); 
	
} else { 
   alert("WebRTC is not supported"); 
}
  
btnGetAudioTracks.addEventListener("click", function(){ 
   console.log("getAudioTracks"); 
   console.log(stream.getAudioTracks()); 
});
  
btnGetTrackById.addEventListener("click", function(){ 
   console.log("getTrackById"); 
   console.log(stream.getTrackById(stream.getAudioTracks()[0].id)); 
});
  
btnGetTracks.addEventListener("click", function(){ 
   console.log("getTracks()"); 
   console.log(stream.getTracks()); 
});
 
btnGetVideoTracks.addEventListener("click", function(){ 
   console.log("getVideoTracks()"); 
   console.log(stream.getVideoTracks()); 
});

btnRemoveAudioTrack.addEventListener("click", function(){ 
   console.log("removeAudioTrack()"); 
   stream.removeTrack(stream.getAudioTracks()[0]); 
});
  
btnRemoveVideoTrack.addEventListener("click", function(){ 
   console.log("removeVideoTrack()"); 
   stream.removeTrack(stream.getVideoTracks()[0]); 
});

现在刷新页面。单击 getAudioTracks() 按钮,然后单击 removeTrack() - audio 按钮。现在应该删除音频轨道。然后对视频轨道执行相同操作。

单击 getAudioTracks

如果单击 getTracks() 按钮,您应该会看到所有 MediaStreamTracks(所有连接的视频和音频输入)。然后点击 getTrackById() 获取音频 MediaStreamTrack。

点击 getTrackById

总结

在本章中,我们使用 MediaStream API 创建了一个简单的 WebRTC 应用程序。现在您应该对使 WebRTC 工作的各种 MediaStream API 有一个清晰的概述。

WebRTC - RTCPeerConnection API

RTCPeerConnection API 是每个浏览器之间对等连接的核心。要创建 RTCPeerConnection 对象,只需写入

var pc = RTCPeerConnection(config);

其中 config 参数至少包含一个键 iceServers。它是一个 URL 对象数组,包含有关 STUN 和 TURN 服务器的信息,用于查找 ICE 候选服务器。您可以在 code.google.com 找到可用的公共 STUN 服务器列表

根据您是呼叫者还是被呼叫者,RTCPeerConnection 对象在连接的每一端以略有不同的方式使用。

以下是用户流程的示例 −

  • 注册 onicecandidate 处理程序。它会在收到任何 ICE 候选后将其发送给另一个对等方。

  • 注册 onaddstream 处理程序。一旦从远程对等端接收到视频流,它就会处理视频流的显示。

  • 注册消息处理程序。您的信令服务器还应有一个用于处理从其他对等端收到的消息的处理程序。如果消息包含 RTCSessionDescription 对象,则应使用 setRemoteDescription() 方法将其添加到 RTCPeerConnection 对象。如果消息包含 RTCIceCandidate 对象,则应使用 addIceCandidate() 方法将其添加到 RTCPeerConnection 对象。

  • 利用 getUserMedia() 设置本地媒体流,并使用 addStream() 方法将其添加到 RTCPeerConnection 对象。

  • 启动提供/应答协商过程。这是呼叫方流程与被呼叫方流程不同的唯一步骤。呼叫方使用 createOffer() 方法开始协商,并注册接收 RTCSessionDescription 对象的回调。然后,此回调应使用 setLocalDescription() 将此 RTCSessionDescription 对象添加到您的 RTCPeerConnection 对象。最后,调用者应使用信令服务器将此 RTCSessionDescription 发送到远程对等方。另一方面,被调用者在 createAnswer() 方法中注册相同的回调。请注意,只有在收到调用者的报价后,才会启动被调用者流程。

RTCPeerConnection API

属性

  • RTCPeerConnection.iceConnectionState(只读) − 返回描述连接状态的 RTCIceConnectionState 枚举。当此值发生变化时,将触发 iceconnectionstatechange 事件。可能的值 −

    • new − ICE 代理正在等待远程候选或收集地址

    • 正在检查 − ICE 代理有远程候选,但尚未找到连接

    • 已连接 − ICE 代理已找到可用连接,但仍在检查更多远程候选以获得更好的连接。

    • 已完成 − ICE 代理已找到可用连接并停止测试远程候选。

    • 失败 − ICE 代理已检查所有远程候选,但未找到至少一个组件的匹配项。

    • 已断开连接 − 至少一个组件不再处于活动状态。

    • 已关闭 − ICE 代理已关闭。

  • RTCPeerConnection.iceGatheringState(只读) − 返回一个 RTCIceGatheringState 枚举,描述连接的 ICE 收集状态 −

    • new − 对象刚刚创建。

    • gathering − ICE 代理正在收集候选对象

    • complete ICE 代理已完成收集。

  • RTCPeerConnection.localDescription(只读) − 返回一个描述本地会话的 RTCSessionDescription。如果尚未设置,则可能为空。

  • RTCPeerConnection.peerIdentity(只读) −返回一个 RTCIdentityAssertion。它由一个 idp(域名)和一个表示远程对等方身份的名称组成。

  • RTCPeerConnection.remoteDescription(只读) − 返回一个描述远程会话的 RTCSessionDescription。如果尚未设置,则可能为空。

  • RTCPeerConnection.signalingState(只读) − 返回一个描述本地连接信令状态的 RTCSignalingState 枚举。此状态描述 SDP 提议。当此值发生变化时,将触发 signalingstatechange 事件。可能的值 −

    • stable − 初始状态。没有正在进行的 SDP 提议/应答交换。

    • have-local-offer −连接的本地端已在本地应用了 SDP 提议。

    • have-remote-offer − 连接的远程端已在本地应用了 SDP 提议。

    • have-local-pranswer − 已应用远程 SDP 提议,并在本地应用了 SDP pranswer。

    • have-remote-pranswer − 已应用本地 SDP,并在远程应用了 SDP pranswer。

    • closed −连接已关闭。

事件处理程序

S.No. 事件处理程序和说明
1

RTCPeerConnection.onaddstream

触发 addstream 事件时会调用此处理程序。当远程对等方将 MediaStream 添加到此连接时会发送此事件。

2

RTCPeerConnection.ondatachannel

触发 datachannel 事件时会调用此处理程序。当将 RTCDataChannel 添加到此连接时会发送此事件。

3

RTCPeerConnection.onicecandidate

触发 icecandidate 事件时会调用此处理程序。当 RTCIceCandidate 对象添加到脚本时,会发送此事件。

4

RTCPeerConnection.oniceconnectionstatechange

当触发 iceconnectionstatechange 事件时,会调用此处理程序。当 iceConnectionState 的值发生变化时,会发送此事件。

5

RTCPeerConnection.onidentityresult

当触发identityresult 事件时,会调用此处理程序。在通过 getIdentityAssertion() 创建要约或答案期间生成身份断言时,将发送此事件。

6

RTCPeerConnection.onidpassertionerror

触发 idpassertionerror 事件时将调用此处理程序。当 IdP(身份提供者)在生成身份断言时发现错误时,将发送此事件。

7

RTCPeerConnection.onidpvalidation

触发 idpvalidationerror 事件时将调用此处理程序。当 IdP(身份提供者)在验证身份断言时发现错误时,将发送此事件。

8

RTCPeerConnection.onnegotiationneeded

当 negotiationneeded 事件触发时,将调用此处理程序。浏览器发送此事件以通知将来某个时间点将需要协商。

9

RTCPeerConnection.onpeeridentity

当 peeridentity 事件触发时,将调用此处理程序。当在此连接上设置并验证了对等身份时,将发送此事件。

10

RTCPeerConnection.onremovestream

当触发 signalingstatechange 事件时,将调用此处理程序。当 signalingState 的值发生变化时,将发送此事件。

11

RTCPeerConnection.onsignalingstatechange

当触发 removestream 事件时,将调用此处理程序。当从此连接中删除 MediaStream 时,将发送此事件。

方法

S.No. 方法和说明
1

RTCPeerConnection()

返回一个新的 RTCPeerConnection 对象。

2

RTCPeerConnection.createOffer()

创建一个要约(请求)以查找远程对等方。此方法的前两个参数是成功和错误回调。可选的第三个参数是选项,例如启用音频或视频流。

3

RTCPeerConnection.createAnswer()

在要约/答案协商过程中,创建对远程对等方收到的要约的答案。此方法的前两个参数是成功和错误回调。可选的第三个参数是要创建的答案的选项。

4

RTCPeerConnection.setLocalDescription()

更改本地连接描述。描述定义连接的属性。连接必须能够支持旧描述和新描述。该方法有三个参数,RTCSessionDescription对象,更改描述成功的回调,更改描述失败的回调。

5

RTCPeerConnection.setRemoteDescription()

更改远程连接描述。描述定义了连接的属性。连接必须能够支持新旧描述。该方法有三个参数,RTCSessionDescription对象,如果更改描述成功则回调,如果更改描述失败则回调。

6

RTCPeerConnection.updateIce()

更新 ICE 代理对远程候选进行 ping 和收集本地候选的过程。

7

RTCPeerConnection.addIceCandidate()

向 ICE 代理提供远程候选。

8

RTCPeerConnection.getConfiguration()

返回 RTCConfiguration 对象。它表示 RTCPeerConnection 对象的配置。

9

RTCPeerConnection.getLocalStreams()

返回本地 MediaStream 连接的数组。

10

RTCPeerConnection.getRemoteStreams()

返回远程 MediaStream 连接的数组。

11

RTCPeerConnection.getStreamById()

根据给定的 ID 返回本地或远程 MediaStream。

12

RTCPeerConnection.addStream()

添加 MediaStream 作为本地视频或音频源。

13

RTCPeerConnection.removeStream()

删除 MediaStream 作为本地视频或音频源音频。

14

RTCPeerConnection.close()

关闭连接。

15

RTCPeerConnection.createDataChannel()

创建一个新的 RTCDataChannel。

16

RTCPeerConnection.createDTMFSender()

创建一个新的 RTCDTMFSender,与特定的 MediaStreamTrack 关联。允许通过连接发送 DTMF(双音多频)电话信令。

17

RTCPeerConnection.getStats()

创建一个新的 RTCStatsReport,其中包含有关连接的统计信息。

18

RTCPeerConnection.setIdentityProvider()

设置 IdP。采用三个参数 - 名称、用于通信的协议和可选用户名。

19

RTCPeerConnection.getIdentityAssertion()

收集身份断言。应用程序中不需要处理此方法。因此,您可能仅在需要时才显式调用它。

建立连接

现在让我们创建一个示例应用程序。首先,通过"节点服务器"运行我们在"信令服务器"教程中创建的信令服务器。

页面上将有两个文本输入,一个用于登录,一个用于我们要连接的用户名。创建一个 index.html 文件并添加以下代码 −

<html lang = "en"> 
   <head> 
      <meta charset = "utf-8" /> 
   </head>
	
   <body> 
	
      <div> 
         <input type = "text" id = "loginInput" /> 
         <button id = "loginBtn">Login</button> 
      </div> 
	
      <div> 
         <input type = "text" id = "otherUsernameInput" />
         <button id = "connectToOtherUsernameBtn">Establish connection</button> 
      </div> 
		
      <script src = "client2.js"></script>
		
   </body>
	
</html>

您可以看到,我们已添加用于登录的文本输入、登录按钮、用于其他对等用户名的文本输入以及连接到他的按钮。现在创建一个 client.js 文件并添加以下代码 −

var connection = new WebSocket('ws://localhost:9090'); 
var name = ""; 
 
var loginInput = document.querySelector('#loginInput'); 
var loginBtn = document.querySelector('#loginBtn'); 
var otherUsernameInput = document.querySelector('#otherUsernameInput'); 
var connectToOtherUsernameBtn = document.querySelector('#connectToOtherUsernameBtn'); 
var connectedUser, myConnection;
  
//当用户点击登录按钮时
loginBtn.addEventListener("click", function(event){ 
   name = loginInput.value; 
	
   if(name.length > 0){ 
      send({ 
         type: "login", 
         name: name 
      }); 
   } 
	
});
  
//处理来自服务器的消息 
connection.onmessage = function (message) { 
   console.log("Got message", message.data);
   var data = JSON.parse(message.data); 
	
   switch(data.type) { 
      case "login": 
         onLogin(data.success); 
         break; 
      case "offer": 
         onOffer(data.offer, data.name); 
         break; 
      case "answer": 
         onAnswer(data.answer); 
         break; 
      case "candidate": 
         onCandidate(data.candidate); 
         break; 
      default: 
         break; 
   } 
};
  
//当用户登录时
function onLogin(success) { 

   if (success === false) { 
      alert("oops...try a different username"); 
   } else { 
      //创建我们的 RTCPeerConnection 对象
		
      var configuration = { 
         "iceServers": [{ "url": "stun:stun.1.google.com:19302" }] 
      }; 
		
      myConnection = new webkitRTCPeerConnection(configuration); 
      console.log("RTCPeerConnection object was created"); 
      console.log(myConnection); 
  
      //setup ice handling
      //when the browser finds an ice candidate we send it to another peer 
      myConnection.onicecandidate = function (event) { 
		
         if (event.candidate) { 
            send({ 
               type: "candidate", 
               candidate: event.candidate 
            }); 
         } 
      }; 
   } 
};
  
connection.onopen = function () { 
   console.log("Connected"); 
};
  
connection.onerror = function (err) { 
   console.log("Got error", err); 
};
  
// 以 JSON 格式发送消息的别名
function send(message) { 

   if (connectedUser) { 
      message.name = connectedUser; 
   } 
	
   connection.send(JSON.stringify(message)); 
};

您可以看到我们建立了与信令服务器的套接字连接。当用户单击登录按钮时,应用程序会将其用户名发送到服务器。如果登录成功,应用程序将创建 RTCPeerConnection 对象并设置 onicecandidate 处理程序,该处理程序将所有找到的 icecandidate 发送给另一个对等方。现在打开页面并尝试登录。您应该看到以下控制台输出 −

建立连接

下一步是向另一个对等方创建要约。将以下代码添加到您的 client.js 文件 −

//与另一个用户建立对等连接
connectToOtherUsernameBtn.addEventListener("click", function () { 
 
   var otherUsername = otherUsernameInput.value; 
   connectedUser = otherUsername;
	
   if (otherUsername.length > 0) { 
      //make an offer 
      myConnection.createOffer(function (offer) { 
         console.log(); 
         send({ 
            type: "offer", 
            offer: offer 
         });
			
         myConnection.setLocalDescription(offer); 
      }, function (error) { 
         alert("An error has occurred."); 
      }); 
   } 
}); 
 
//当有人想打电话给我们时
function onOffer(offer, name) { 
   connectedUser = name; 
   myConnection.setRemoteDescription(new RTCSessionDescription(offer)); 
	
   myConnection.createAnswer(function (answer) { 
      myConnection.setLocalDescription(answer); 
		
      send({ 
         type: "answer", 
         answer: answer 
      }); 
		
   }, function (error) { 
      alert("oops...error"); 
   }); 
}
  
//当另一个用户回答我们的提议时
function onAnswer(answer) { 
   myConnection.setRemoteDescription(new RTCSessionDescription(answer)); 
} 
 
//when we got ice candidate from another user 
function onCandidate(candidate) { 
   myConnection.addIceCandidate(new RTCIceCandidate(candidate)); 
}	

您可以看到,当用户单击"建立连接"按钮时,应用程序会向另一个对等点发出 SDP 请求。我们还设置了 onAnsweronCandidate 处理程序。重新加载页面,在两个选项卡中打开它,使用两个用户登录并尝试在它们之间建立连接。您应该看到以下控制台输出 −

控制台输出

现在建立了对等连接。在接下来的教程中,我们将添加视频和音频流以及文本聊天支持。

WebRTC - RTCDataChannel API

WebRTC 不仅擅长传输音频和视频流,还擅长传输我们可能拥有的任何任意数据。这就是 RTCDataChannel 对象发挥作用的地方。

RTCDataChannel API

属性

  • RTCDataChannel.label(只读) − 返回包含数据通道名称的字符串。

  • RTCDataChannel.ordered(只读) − 如果消息传递的顺序有保证,则返回 true;如果不能保证,则返回 false。

  • RTCDataChannel.protocol(只读) −返回包含用于此通道的子协议名称的字符串。

  • RTCDataChannel.id(只读) − 返回在创建 RTCDataChannel 对象时设置的通道的唯一 ID。

  • RTCDataChannel.readyState(只读) − 返回表示连接状态的 RTCDataChannelState 枚举。可能的值 −

    • connecting − 表示连接尚未激活。这是初始状态。

    • open − 表示连接正在运行。

    • closing − 表示连接正在关闭。缓存的消息正在发送或接收过程中,但没有新创建的任务正在接受。

    • closed − 表示无法建立连接或已关闭。

  • RTCDataChannel.bufferedAmount(只读) − 返回已排队等待发送的字节数。这是尚未通过 RTCDataChannel.send() 发送的数据量。

  • RTCDataChannel.bufferedAmountLowThreshold − 返回 RTCDataChannel.bufferedAmount 占用的字节数。当 RTCDataChannel.bufferedAmount 低于此阈值时,将触发 bufferedamountlow 事件。

  • RTCDataChannel.binaryType − 返回连接传输的二进制数据的类型。可以是"blob"或"arraybuffer"。

  • RTCDataChannel.maxPacketLifeType(只读) − 返回一个无符号短整型,表示消息传递处于不可靠模式时窗口的长度(以毫秒为单位)。

  • RTCDataChannel.maxRetransmits(只读) −返回一个无符号短整型值,表示如果数据未送达,通道将重新传输数据的最大次数。

  • RTCDataChannel.negotiated(只读) − 返回一个布尔值,表示通道是否已由用户代理或应用程序协商。

  • RTCDataChannel.reliable(只读) − 返回一个布尔值,表示连接是否可以在不可靠模式下发送消息。

  • RTCDataChannel.stream(只读) − RTCDataChannel.id 的同义词

事件处理程序

  • RTCDataChannel.onopen −当触发 open 事件时,将调用此事件处理程序。当建立数据连接时,将发送此事件。

  • RTCDataChannel.onmessage − 当触发 message 事件时,将调用此事件处理程序。当数据通道上有消息时,将发送此事件。

  • RTCDataChannel.onbufferedamountlow − 当触发 bufferedamoutlow 事件时,将调用此事件处理程序。当 RTCDataChannel.bufferedAmount 降低到 RTCDataChannel.bufferedAmountLowThreshold 属性以下时,将发送此事件。

  • RTCDataChannel.onclose − 当触发 close 事件时,将调用此事件处理程序。当数据连接关闭时,会发送此事件。

  • RTCDataChannel.onerror − 当触发错误事件时,会调用此事件处理程序。当遇到错误时,会发送此事件。

方法

  • RTCDataChannel.close() − 关闭数据通道。

  • RTCDataChannel.send() − 通过通道发送参数中的数据。数据可以是 blob、字符串、ArrayBuffer 或 ArrayBufferView。

WebRTC - 发送消息

现在让我们创建一个简单的示例。首先,通过"节点服务器"运行我们在"信令服务器"教程中创建的信令服务器。

页面上将有三个文本输入,一个用于登录,一个用于用户名,一个用于我们想要发送给对方的消息。创建一个 index.html 文件并添加以下代码 −

<html lang = "en"> 
   <head> 
      <meta charset = "utf-8" /> 
   </head>
	
   <body> 
      <div> 
         <input type = "text" id = "loginInput" /> 
         <button id = "loginBtn">Login</button> 
      </div> 
		
      <div> 
         <input type = "text" id = "otherUsernameInput" /> 
         <button id = "connectToOtherUsernameBtn">Establish connection</button> 
      </div> 
		
      <div> 
         <input type = "text" id = "msgInput" /> 
         <button id = "sendMsgBtn">Send text message</button> 
      </div> 
		
      <script src = "client.js"></script>
   </body>
	
</html>

我们还添加了三个按钮,分别用于登录、建立连接和发送消息。现在创建一个 client.js 文件并添加以下代码 −

var connection = new WebSocket('ws://localhost:9090'); 
var name = "";

var loginInput = document.querySelector('#loginInput'); 
var loginBtn = document.querySelector('#loginBtn'); 

var otherUsernameInput = document.querySelector('#otherUsernameInput'); 
var connectToOtherUsernameBtn = document.querySelector('#connectToOtherUsernameBtn'); 
var msgInput = document.querySelector('#msgInput'); 
var sendMsgBtn = document.querySelector('#sendMsgBtn'); 
var connectedUser, myConnection, dataChannel;
  
//当用户点击登录按钮时
loginBtn.addEventListener("click", function(event) { 
   name = loginInput.value; 
	
   if(name.length > 0) { 
      send({ 
         type: "login", 
         name: name 
      }); 
   } 
}); 
 
//处理来自服务器的消息 
connection.onmessage = function (message) { 
   console.log("Got message", message.data); 
   var data = JSON.parse(message.data); 
	
   switch(data.type) { 
      case "login": 
         onLogin(data.success); 
         break; 
      case "offer": 
         onOffer(data.offer, data.name); 
         break; 
      case "answer":
         onAnswer(data.answer); 
         break; 
      case "candidate": 
         onCandidate(data.candidate); 
         break; 
      default: 
         break; 
   } 
}; 
 
//当用户登录时
function onLogin(success) { 

   if (success === false) { 
      alert("oops...try a different username"); 
   } else { 
      //创建我们的 RTCPeerConnection 对象
      var configuration = { 
         "iceServers": [{ "url": "stun:stun.1.google.com:19302" }] 
      }; 
		
      myConnection = new webkitRTCPeerConnection(configuration, { 
         optional: [{RtpDataChannels: true}] 
      }); 
		
      console.log("RTCPeerConnection object was created"); 
      console.log(myConnection); 
  
      //setup ice handling 
      //when the browser finds an ice candidate we send it to another peer 
      myConnection.onicecandidate = function (event) { 
		
         if (event.candidate) { 
            send({ 
               type: "candidate", 
               candidate: event.candidate 
            });
         } 
      }; 
		
      openDataChannel();
		
   } 
};
  
connection.onopen = function () { 
   console.log("Connected"); 
}; 
 
connection.onerror = function (err) { 
   console.log("Got error", err); 
};
  
// 以 JSON 格式发送消息的别名
function send(message) { 
   if (connectedUser) { 
      message.name = connectedUser; 
   }
	
   connection.send(JSON.stringify(message)); 
};

您可以看到我们建立了与信令服务器的套接字连接。当用户单击登录按钮时,应用程序会将其用户名发送到服务器。如果登录成功,应用程序将创建 RTCPeerConnection 对象并设置 onicecandidate 处理程序,该处理程序将所有找到的 icecandidate 发送到另一个对等体。它还运行 openDataChannel() 函数,该函数创建一个数据通道。请注意,在创建 RTCPeerConnection 对象时,如果您使用的是 Chrome 或 Opera,则构造函数中的第二个参数(可选)[{RtpDataChannels: true}] 是必需的。下一步是创建对另一个对等体的报价。将以下代码添加到您的 client.js 文件中−

//与另一个用户建立对等连接
connectToOtherUsernameBtn.addEventListener("click", function () {
  
   var otherUsername = otherUsernameInput.value;
   connectedUser = otherUsername;
	
   if (otherUsername.length > 0) { 
      //make an offer 
      myConnection.createOffer(function (offer) { 
         console.log(); 
			
         send({ 
            type: "offer", 
            offer: offer 
         }); 
			
         myConnection.setLocalDescription(offer); 
      }, function (error) { 
         alert("An error has occurred."); 
      }); 
   } 
});
  
//当有人想打电话给我们时
function onOffer(offer, name) { 
   connectedUser = name; 
   myConnection.setRemoteDescription(new RTCSessionDescription(offer));
	
   myConnection.createAnswer(function (answer) { 
      myConnection.setLocalDescription(answer); 
		
      send({ 
         type: "answer", 
         answer: answer 
      }); 
		
   }, function (error) { 
      alert("oops...error"); 
   }); 
}

//当另一个用户回答我们的提议时
function onAnswer(answer) { 
   myConnection.setRemoteDescription(new RTCSessionDescription(answer)); 
}
  
//when we got ice candidate from another user 
function onCandidate(candidate) { 
   myConnection.addIceCandidate(new RTCIceCandidate(candidate)); 
}

您可以看到,当用户单击"建立连接"按钮时,应用程序会向另一个对等点发出 SDP 请求。我们还设置了 onAnsweronCandidate 处理程序。最后,让我们实现创建数据通道的 openDataChannel() 函数。将以下代码添加到您的 client.js 文件中 −

//创建数据通道
function openDataChannel() { 

   var dataChannelOptions = { 
      reliable:true 
   }; 
	
   dataChannel = myConnection.createDataChannel("myDataChannel", dataChannelOptions);
	
   dataChannel.onerror = function (error) { 
      console.log("Error:", error); 
   };
	
   dataChannel.onmessage = function (event) { 
      console.log("Got message:", event.data); 
   };  
}
  
//当用户点击发送消息按钮时
sendMsgBtn.addEventListener("click", function (event) { 
   console.log("send message");
   var val = msgInput.value; 
   dataChannel.send(val); 
});

在这里,我们为连接创建数据通道,并为"发送消息"按钮添加事件处理程序。现在在两个选项卡中打开此页面,使用两个用户登录,建立连接,然后尝试发送消息。您应该在控制台输出中看到它们。请注意,上述示例是在 Opera 中测试的。

Opera 中的示例

现在您可能会看到 RTCDataChannel 是 WebRTC API 中非常强大的部分。此对象还有许多其他用例,例如点对点游戏或基于 torrent 的文件共享。

WebRTC - 信令

大多数 WebRTC 应用程序不仅能够通过视频和音频进行通信。它们还需要许多其他功能。在本章中,我们将构建一个基本的信令服务器。

信令和协商

要连接到另一个用户,您应该知道他在网络上的位置。设备的 IP 地址允许支持 Internet 的设备直接在彼此之间发送数据。RTCPeerConnection 对象负责此操作。一旦设备知道如何通过 Internet 找到彼此,它们就会开始交换有关每个设备支持的协议和编解码器的数据。

要与另一个用户通信,您只需交换联系信息,其余的将由 WebRTC 完成。连接到其他用户的过程也称为信令和协商。它由几个步骤组成 −

  • 创建对等连接的潜在候选人列表。

  • 用户或应用程序选择要建立连接的用户。

  • 信令层通知另一个用户有人想连接到他。他可以接受或拒绝。

  • 第一个用户被告知接受要约。

  • 第一个用户与另一个用户发起 RTCPeerConnection

  • 两个用户通过信令服务器交换软件和硬件信息。

  • 两个用户交换位置信息。

  • 连接成功或失败。

WebRTC 规范不包含有关交换信息的任何标准。因此请记住,以上只是信令可能发生的一个例子。您可以使用任何您喜欢的协议或技术。

构建服务器

我们要构建的服务器将能够将不在同一台计算机上的两个用户连接在一起。我们将创建自己的信令机制。我们的信令服务器将允许一个用户呼叫另一个用户。一旦用户呼叫了另一个用户,服务器就会在他们之间传递要约、答案、ICE 候选并建立 WebRTC 连接。

构建服务器

上图是使用信令服务器时用户之间的消息流。首先,每个用户在服务器上注册。在我们的例子中,这将是一个简单的字符串用户名。一旦用户注册,他们就可以互相呼叫。用户 1 使用他希望呼叫的用户标识符发出要约。另一个用户应该回答。最后,在用户之间发送 ICE 候选,直到他们能够建立连接。

要创建 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 连接时,将执行此代码。然后,我们监听用户发送的任何消息。最后,我们向已连接的用户发送响应"来自服务器的问候"。

现在运行 node server,服务器应该开始监听套接字连接。

要测试我们的服务器,我们将使用我们已经安装的 wscat 实用程序。此工具有助于直接连接到 WebSocket 服务器并测试命令。在一个终端窗口中运行我们的服务器,然后打开另一个终端窗口并运行 wscat -c ws://localhost:9090 命令。您应该在客户端看到以下内容 −

使用 wscat 实用程序

服务器还应记录已连接的用户 −

记录已连接的用户

用户注册

在我们的信令服务器中,我们将为每个连接使用基于字符串的用户名,以便我们知道将消息发送到何处。让我们稍微更改一下我们的连接处理程序 −

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

The above function ensures that all our messages are sent in the JSON format.

当用户断开连接时,我们应该清理其连接。我们可以在触发 close 事件时删除用户。将以下代码添加到 connection 处理程序 −

connection.on("close", function() { 
   if(connection.name) { 
      delete users[connection.name]; 
   } 
});

现在让我们使用登录命令测试我们的服务器。请记住,所有消息都必须以 JSON 格式编码。运行我们的服务器并尝试登录。您应该看到类似这样的内容 −

Test with Login Command

Making a Call

成功登录后,用户想要呼叫另一个用户。他应该向另一个用户发出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;

您可以看到这与 offer 处理程序有多么相似。请注意,此代码位于 RTCPeerConnection 对象上的 createOffercreateAnswer 函数之后。

现在我们可以测试我们的提供/回答机制。同时连接两个客户端并尝试提供和回答。您应该看到以下 −

连接两个客户端

在此示例中,offeranswer 是简单字符串,但在实际应用中,它们将用 SDP 数据填充。

ICE 候选者

最后部分是处理用户之间的 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;

它的工作方式应与 offeranswer 处理程序类似。

离开连接

为了允许我们的用户断开与另一个用户的连接,我们应该实现挂断功能。它还会告诉服务器删除所有用户引用。添加 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" 
            }); 
         }  
      } 
   } 
});

现在,如果连接终止,我们的用户将断开连接。当用户关闭浏览器窗口时(我们仍处于 offeranswercandidate 状态),将触发 close 事件。

完整的信令服务器

以下是我们的信令服务器的完整代码 −

//需要我们的 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) { 
         //当用户尝试登录时
			
         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)); 
}

因此,工作已完成,我们的信令服务器已准备就绪。请记住,在建立 WebRTC 连接时,如果操作顺序不正确,可能会导致问题。

总结

在本章中,我们构建了简单直接的信令服务器。我们介绍了信令过程、用户注册和提供/回答机制。我们还实现了在用户之间发送候选。

WebRTC - 浏览器支持

Web 发展如此之快,而且一直在不断改进。每天都有新的标准诞生。浏览器允许在用户不知情的情况下安装更新,因此您应该跟上 Web 和 WebRTC 世界的最新动态。以下是目前的最新动态概述。

浏览器支持

每个浏览器不会同时拥有所有相同的 WebRTC 功能。不同的浏览器可能走在时代前沿,这使得某些 WebRTC 功能在一个浏览器中有效,而在另一个浏览器中无效。浏览器中对 WebRTC 的当前支持情况如下图所示。

浏览器支持

您可以在 http://caniuse.com/#feat=rtcpeerconnection 查看最新的 WebRTC 支持状态。

Chrome、Firefox 和 Opera

主流 PC 操作系统(如 Mac OS X、Windows 和 Linux)上最新版本的 Chrome、Firefox 和 Opera 均支持开箱即用的 WebRTC。最重要的是,Chrome 和 Firefox 开发团队的工程师一直在合作解决问题,以便这两个浏览器能够轻松相互通信。

Android 操作系统

在 Android 操作系统上,Chrome 和 Firefox 的 WebRTC 应用程序应该可以开箱即用。在 Android Ice Cream Sandwich 版本 (4.0) 之后,它们能够与其他浏览器配合使用。这是由于桌面版和移动版之间的代码共享。

Apple

Apple 尚未宣布他们计划在 OS X 上的 Safari 中支持 WebRTC。混合原生 iOS 应用程序操作系统的一种可能解决方法是将 WebRTC 代码直接嵌入到应用程序中,并将此应用程序加载到 WebView 中。

Internet Explorer

Microsoft 不支持桌面上的 WebRTC。但他们已正式确认他们将在未来版本的 IE(Edge)中实现 ORTC(对象实时通信)。他们不打算支持 WebRTC 1.0。他们将他们的 ORTC 标记为 WebRTC 1.1,尽管这只是社区增强功能,而不是官方标准。最近,他们已将 ORTC 支持添加到最新的 Microsoft Edge 版本中。您可以在 https://blogs.windows.com/msedgedev/2015/09/18/ortc-api-is-now-available-in-microsoftedge/ 了解更多信息。

摘要

请注意,WebRTC 是 API 和协议的集合,而不是单个 API。对每个 API 和协议的支持都在不同的浏览器和操作系统上以不同的级别进行开发。检查最新支持级别的一种好方法是通过 http://canisue.com。 它跟踪多种浏览器对现代 API 的采用情况。您还可以在 http://www.webrtc.org 上找到有关浏览器支持的最新信息以及 WebRTC 演示,该网站由 Mozilla、Google 和 Opera 提供支持。

WebRTC - 移动支持

在移动世界中,WebRTC 支持与桌面上的支持并不处于同一水平。移动设备有自己的方式,因此 WebRTC 在移动平台上也有所不同。

移动支持

在为桌面开发 WebRTC 应用程序时,我们考虑使用 Chrome、Firefox 或 Opera。它们都开箱即用地支持 WebRTC。一般来说,你只需要一个浏览器,而不必担心桌面的硬件。

在移动世界中,当今的 WebRTC 有三种可能的模式 −

  • 本机应用程序
  • 浏览器应用程序
  • 本机浏览器

Android

2013 年,Android 版 Firefox 网络浏览器开箱即用,支持 WebRTC。现在,您可以使用 Firefox 移动浏览器在 Android 设备上进行视频通话。

它有三个主要的 WebRTC 组件 −

  • PeerConnection − 支持浏览器之间的通话

  • getUserMedia −提供对摄像头和麦克风的访问

  • DataChannels − 提供点对点数据传输

Android 版 Google Chrome 也提供 WebRTC 支持。正如您已经注意到的,最有趣的功能通常首先出现在 Chrome 中。

在过去的一年里,Opera 移动浏览器支持 WebRTC。因此,对于 Android,您有 Chrome、Firefox 和 Opera。其他浏览器不支持 WebRTC。

iOS

不幸的是,iOS 现在不支持 WebRTC。虽然使用 Firefox、Opera 或 Chrome 时 WebRTC 在 Mac 上运行良好,但它不受 iOS 支持。

如今,您的 WebRTC 应用程序无法在开箱即用的 Apple 移动设备上运行。但有一个浏览器 − Bowser。它是由爱立信开发的一款 Web 浏览器,开箱即用,支持 WebRTC。您可以在 http://www.openwebrtc.org/bowser/. 查看其主页。

目前,这是在 iOS 上支持 WebRTC 应用程序的唯一友好方式。另一种方式是自己开发原生应用程序。

Windows Phone

微软不支持移动平台上的 WebRTC。但他们已正式确认,他们将在未来版本的 IE 中实现 ORTC(对象实时通信)。他们不打算支持 WebRTC 1.0。他们将 ORTC 标记为 WebRTC 1.1,尽管这只是社区增强功能,而不是官方标准。

因此,如今 Windows Phone 用户无法使用 WebRTC 应用程序,而且没有办法解决这种情况。

黑莓

黑莓也不支持 WebRTC 应用程序。

使用 WebRTC 原生浏览器

用户使用 WebRTC 最方便、最舒适的情况是使用设备的原生浏览器。在这种情况下,设备已准备好运行任何其他配置。

目前,只有版本 4 或更高版本的 Android 设备提供此功能。Apple 仍然没有显示任何支持 WebRTC 的活动。因此 Safari 用户无法使用 WebRTC 应用程序。微软也没有在 Windows Phone 8 中引入它。

通过浏览器应用程序使用 WebRTC

这意味着使用第三方应用程序(非原生网络浏览器)来提供 WebRTC 功能。目前,有两个这样的第三方应用程序。Bowser 是将 WebRTC 功能引入 iOS 设备的唯一方法,而 Opera 是 Android 平台的一个不错的替代品。其余可用的移动浏览器不支持 WebRTC。

原生移动应用程序

如您所见,WebRTC 尚未在移动世界中得到广泛支持。因此,可能的解决方案之一是开发利用 WebRTC API 的原生应用程序。但这并不是更好的选择,因为主要的 WebRTC 功能是跨平台解决方案。无论如何,在某些情况下,这是唯一的方法,因为原生应用程序可以利用 HTML5 浏览器不支持的设备特定功能或特性。

限制移动和桌面设备的视频流

getUserMedia API 的第一个参数需要一个键和值对象,告诉浏览器如何处理流。您可以在 https://tools.ietf.org/html/draft-alvestrand-constraints-resolution-03 查看完整的约束集。您可以设置视频宽高比、帧速率和其他可选参数。

支持移动设备是最大的难题之一,因为移动设备的屏幕空间有限,资源也有限。您可能希望移动设备仅捕获 480x320 分辨率或更小的视频流,以节省电量和带宽。使用浏览器中的用户代理字符串是测试用户是否使用移动设备的好方法。让我们看一个例子。创建 index.html 文件 −

<!DOCTYPE html> 
<html lang = "en">
 
   <head> 
      <meta charset = "utf-8" /> 
   </head> 
	
   <body> 
      <video autoplay></video> 
      <script src = "client.js"></script> 
   </body>
	
</html>

然后创建以下 client.js 文件 −

//桌面浏览器的限制
var desktopConstraints = { 

   video: { 
      mandatory: { 
         maxWidth:800,
         maxHeight:600   
      }  
   }, 
	
   audio: true 
}; 
 
//移动浏览器的限制
var mobileConstraints = { 

   video: { 
      mandatory: { 
         maxWidth: 480, 
         maxHeight: 320, 
      } 
   }, 
	
   audio: true 
}
  
//如果用户正在使用移动浏览器 
if(/Android|iPhone|iPad/i.test(navigator.userAgent)) { 
   var constraints = mobileConstraints;   
} else { 
   var constraints = desktopConstraints; 
}
  
function hasUserMedia() { 
   //检查浏览器是否支持WebRTC
   return !!(navigator.getUserMedia || navigator.webkitGetUserMedia || 
      navigator.mozGetUserMedia); 
}
  
if (hasUserMedia()) {
  
   navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || 
      navigator.mozGetUserMedia;
	
   //启用视频和音频通道
   navigator.getUserMedia(constraints, function (stream) { 
      var video = document.querySelector('video');
		
      //将我们的流插入到视频标签中
      video.src = window.URL.createObjectURL(stream);
		
   }, function (err) {}); 
} else { 
   alert("WebRTC is not supported"); 
}

使用 static 命令运行 Web 服务器并打开页面。您应该看到它是 800x600。然后使用 chrome 工具在移动视口中打开此页面并检查分辨率。它应该是 480x320。

运行 Web 服务器

约束是提高 WebRTC 应用程序性能的最简单方法。

总结

在本章中,我们了解了为移动设备开发 WebRTC 应用程序时可能出现的问题。我们发现了在移动平台上支持 WebRTC API 的不同限制。我们还启动了一个演示应用程序,其中我们为桌面和移动浏览器设置了不同的约束。

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 Application

下一步是使用唯一用户名实现用户登录。我们只需将用户名发送到服务器,然后服务器会告诉我们该用户名是否已被使用。将以下代码添加到您的 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 并在本地销毁连接

现在运行代码。您应该能够使用两个浏览器选项卡登录到服务器。然后,您可以调用选项卡并挂断电话。

call and hang up

以下是整个 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 应用程序稳定,我们付出了很多努力。

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 处理程序 −

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); 
   //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 候选。我们使用相同的技术,只是在用户之间传递消息。主要区别在于候选消息可能以任何顺序在每个用户上发生多次。添加 候选 处理程序 −

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) { 
         //当用户尝试登录时
         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/ 了解更多信息。创建一个名为"audiochat"的文件夹。这将是我们的根应用程序文件夹。在此文件夹中创建一个文件 package.json(它是管理 npm 依赖项所必需的),并添加以下内容 −

{ 
   "name": "webrtc-audiochat", 
   "version": "0.1.0", 
   "description": "webrtc-audiochat", 
   "author": "Author", 
   "license": "BSD-2-Clause" 
}

然后运行 ​​npm install bootstrap。这将在 audiochat/node_modules 文件夹中安装 bootstrap 库。

现在我们需要创建一个基本的 HTML 页面。使用以下代码在根文件夹中创建一个 index.html 文件 −

<html>
 
   <head> 
      <title>WebRTC Voice 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 Voice 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">
		
         <div class = "row"> 
			
            <div class = "col-md-6 text-right"> 
               Local audio: <audio id = "localAudio" 
               controls autoplay></audio> 
            </div>
				
            <div class = "col-md-6 text-left"> 
               Remote audio: <audio id = "remoteAudio" 
                  controls autoplay></audio> 
            </div> 
				
         </div> 
			
         <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 命令并在浏览器中打开页面。您应该看到以下控制台输出 −

Run Signaling Server

下一步是使用唯一用户名实现用户登录。我们只需将用户名发送到服务器,然后服务器会告诉我们该用户名是否已被使用。将以下代码添加到您的 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 localAudio = document.querySelector('#localAudio');
var remoteAudio = document.querySelector('#remoteAudio');

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: false, audio: true }, function (myStream) { 
         stream = myStream; 
			
         //在页面上显示本地音频流
         localAudio.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) { 
            remoteAudio.src = window.URL.createObjectURL(e.stream); 
         }; 
			
         // Setup ice handling 
         yourConn.onicecandidate = function (event) { 
            if (event.candidate) { 
               send({ 
                  type: "candidate", 
               }); 
            } 
         };  
			
      }, function (error) { 
         console.log(error); 
      }); 
		
   } 
};

现在,如果您运行代码,页面应该允许您登录并在页面上显示您的本地音频流。

允许登录

现在我们准备发起呼叫。首先,我们向另一个用户发送报价。一旦用户收到报价,他就会创建一个答案并开始交易 ICE 候选人。将以下代码添加到client.js文件 −

//发起呼叫 
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)); 
};

我们向 Call 按钮添加一个 click 处理程序,用于发起要约。然后我们实现 onmessage 处理程序所期望的几个处理程序。它们将被异步处理,直到两个用户都建立连接。

最后一步是实现挂断功能。这将停止传输数据并告诉另一个用户关闭通话。添加以下代码 −

//挂断
hangUpBtn.addEventListener("click", function () { 
   send({ 
      type: "leave" 
   });  
	
   handleLeave(); 
});
  
function handleLeave() { 
   connectedUser = null; 
   remoteAudio.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 localAudio = document.querySelector('#localAudio'); 
var remoteAudio = document.querySelector('#remoteAudio'); 

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: false, audio: true }, function (myStream) { 
         stream = myStream; 
			
         //在页面上显示本地音频流 
         localAudio.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) { 
            remoteAudio.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; 
   remoteAudio.src = null; 
	
   yourConn.close(); 
   yourConn.onicecandidate = null; 
   yourConn.onaddstream = null; 
};

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 处理程序 −

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) { 
         //当用户尝试登录时
         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/ 了解更多信息。创建一个名为"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 文件 −

//我们的用户名
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 命令并在浏览器中打开页面。您应该看到以下控制台输出 −

Console Output

下一步是使用唯一用户名实现用户登录。我们只需将用户名发送到服务器,然后服务器会告诉我们该用户名是否已被使用。将以下代码添加到您的 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 对象
  • 在我们的 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";
		
      //********************** 
      //启动对等连接
      //**********************
		
      //使用Google公共stun服务器
      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); 
      };
		
      //当我们收到对方的消息时,将其显示在屏幕上 
      dataChannel.onmessage = function (event) { 
         chatArea.innerHTML += connectedUser + ": " + event.data + "<br />"; 
      };
		
      dataChannel.onclose = function () { 
         console.log("data channel is closed"); 
      };  
   } 
};

如果登录成功,应用程序将创建 RTCPeerConnection 对象并设置 onicecandidate 处理程序,该处理程序将所有找到的 icecandidate 发送给另一个对等体。它还会创建一个数据通道。请注意,在创建 RTCPeerConnection 对象时,构造函数中的第二个参数(可选):[{RtpDataChannels: true}](如果您使用的是 Chrome 或 Opera)是必需的。下一步是向另一个对等体创建要约。一旦用户收到要约,他就会创建一个 answer 并开始交易 ICE 候选者。将以下代码添加到 client.js 文件 −

//发起呼叫
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)); 
};

我们向 Call 按钮添加一个 click 处理程序,该处理程序启动一个要约。然后我们实现 onmessage 处理程序所期望的几个处理程序。它们将被异步处理,直到两个用户都建立连接。

下一步是实现挂断功能。这将停止传输数据并告诉另一个用户关闭数据通道。添加以下代码 −

//挂断
hangUpBtn.addEventListener("click", function () { 
   send({ 
      type: "leave" 
   }); 
	
   handleLeave(); 
}); 
 
function handleLeave() { 
   connectedUser = null; 
   yourConn.close(); 
   yourConn.onicecandidate = null; 
};

当用户点击"挂断"按钮时 −

  • 它将向其他用户发送"离开"消息。
  • 它将关闭 RTCPeerConnection 以及数据通道。

最后一步是向另一个对等方发送消息。将"点击"处理程序添加到"发送消息"按钮 −

//当用户点击"发送消息"按钮时
sendMsgBtn.addEventListener("click", function (event) { 
   var val = msgInput.value; 
   chatArea.innerHTML += name + ": " + val + "<br />"; 
	
   //向已连接的对等体发送消息
   dataChannel.send(val); 
   msgInput.value = ""; 
});

现在运行代码。您应该能够使用两个浏览器选项卡登录到服务器。然后,您可以建立与其他用户的对等连接并向他发送消息,以及通过单击"挂断"按钮关闭数据通道。

代码输出

以下是整个 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 msgInput = document.querySelector('#msgInput'); 
var sendMsgBtn = document.querySelector('#sendMsgBtn'); 

var chatArea = document.querySelector('#chatarea'); 
var yourConn; 
var dataChannel; 
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"; 
		
      //********************** 
      //启动对等连接
      //********************** 
		
      //使用Google公共stun服务器
      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); 
      }; 
		
      //当我们收到对方的消息时,将其显示在屏幕上 
      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"); 
      }); 
   } 
	
});
 
//当有人向我们发送 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; 
   yourConn.close(); 
   yourConn.onicecandidate = null; 
};
 
//当用户点击"发送消息"按钮时 
sendMsgBtn.addEventListener("click", function (event) { 
   var val = msgInput.value; 
   chatArea.innerHTML += name + ": " + val + "<br />"; 
	
   //向已连接的对等体发送消息
   dataChannel.send(val); 
   msgInput.value = ""; 
});

WebRTC - 安全

在本章中,我们将为我们在"WebRTC 信令"一章中创建的信令服务器添加安全功能。将会有两个增强功能 −

  • 使用 Redis 数据库进行用户身份验证
  • 启用安全套接字连接

首先,您应该安装 Redis。

  • http://redis.io/download 下载最新稳定版本(我的情况是 3.05)

  • 解压

  • 在下载的文件夹中运行 sudo make install

  • 安装完成后,运行 make test 检查一切是否正常运行。

Redis 有两个可执行命令 −

  • redis-cli − Redis 的命令行界面(客户端部分)

  • redis-server − Redis 数据存储

要运行 Redis 服务器,请在终端控制台中输入 redis-server。您应该看到以下 −

Redis Server

现在打开一个新的终端窗口并运行 redis-cli 以打开客户端应用程序。

Redis-cli

基本上,Redis 是一个键值数据库。要创建具有字符串值的键,您应该使用 SET 命令。要读取密钥值,您应该使用 GET 命令。让我们为它们添加两个用户和密码。密钥将是用户名,这些密钥的值将是相应的密码。

添加用户和密码

现在我们应该修改我们的信令服务器以添加用户身份验证。将以下代码添加到 server.js 文件的顶部 −

//在 Node.js 中需要 redis 库
var redis = require("redis");

//创建 redis 客户端对象
var redisClient = redis.createClient();

在上面的代码中,我们需要 Node.js 的 Redis 库并为我们的服务器创建一个 redis 客户端。

要添加身份验证,请修改连接对象上的 message 处理程序 −

//当用户连接到我们的服务器时
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 = {}; 
      }
		
      //检查用户是否已经通过身份验证
      if(data.type != "login") { 
		
         //如果用户没有经过身份验证
         if(!connection.isAuth) { 
            sendTo(connection, { 
               type: "error", 
               message: "You are not authenticated" 
            }); 
            return; 
         } 
      } 
		
      //切换用户消息类型
      switch (data.type) { 
         //当用户尝试登录时
         case "login": 
            console.log("User logged:", data.name); 
            //从 redis 数据库获取此用户名的密码
				
            redisClient.get(data.name, function(err, reply) {  
               //检查密码是否与redis中存储的密码匹配
               var loginSuccess = reply === data.password;
				
               //如果有人用这个用户名或错误的密码登录
                  then refuse 
               if(users[data.name] || !loginSuccess) { 
                  sendTo(connection, { 
                     type: "login", 
                     success: false 
                  }); 
               } else { 
                  //在服务器上保存用户连接 
                  users[data.name] = connection; 
                  connection.name = data.name;
                  connection.isAuth = true; 
						
                  sendTo(connection, { 
                     type: "login", 
                     success: true 
                  }); 
               }  
            }); 
				
            break;
      }
   });
	
}		
		
//... 
//*****other handlers*******

在上面的代码中,如果用户尝试登录,我们会从 Redis 获取他的密码,检查它是否与存储的密码匹配,如果成功,我们会将他的用户名存储在服务器上。我们还将 isAuth 标志添加到连接以检查用户是否经过身份验证。请注意此代码 −

//检查用户是否已经通过身份验证
if(data.type != "login") { 

   //如果用户没有经过身份验证
   if(!connection.isAuth) { 
      sendTo(connection, { 
         type: "error", 
         message: "You are not authenticated" 
      });
		
      return; 
   } 
}

如果未经身份验证的用户尝试发送要约或离开连接,我们只会发回错误。

下一步是启用安全套接字连接。强烈建议 WebRTC 应用程序使用安全套接字连接。PKI(公钥基础设施)是来自 CA(证书颁发机构)的数字签名。然后,用户检查用于签署证书的私钥是否与 CA 证书的公钥匹配。出于开发目的,我们将使用自签名安全证书。

我们将使用 openssl。它是一个实现 SSL(安全套接字层)和 TLS(传输层安全性)协议的开源工具。它通常默认安装在 Unix 系统上。运行 openssl version -a 检查是否已安装。

使用 Openssl

要生成公钥和私钥安全证书密钥,请按照以下步骤操作 −

  • 生成临时服务器密码密钥

openssl genrsa -des3 -passout pass:x -out server.pass.key 2048
临时服务器密码密钥
  • 生成服务器私钥

openssl rsa -passin pass:12345 -in server.pass.key -out server.key
服务器私钥
  • 生成签名请求。系统会询问您有关公司的其他问题。只需一直按"Enter"键即可。

openssl req -new -key server.key -out server.csr
生成签名请求
  • 生成证书

openssl x509 -req -days 1095 -in server.csr -signkey server.key -out server.crt
生成证书

现在您有两个文件,证书(server.crt)和私钥(server.key)。将它们复制到信令服务器根文件夹中。

要启用安全套接字连接,请修改我们的信令服务器。

//需要文件系统模块
var fs = require('fs'); 
var httpServ = require('https');
  
//https://github.com/visionmedia/superagent/issues/205 
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
  
//安全服务器将绑定到端口 9090
var cfg = { 
   port: 9090, 
   ssl_key: 'server.key', 
   ssl_cert: 'server.crt' 
};
  
//如果是 http 请求,只需返回"OK"
var processRequest = function(req, res) { 
   res.writeHead(200); 
   res.end("OK"); 
};
  
//创建启用 SSL 的服务器
var app = httpServ.createServer({ 
   key: fs.readFileSync(cfg.ssl_key), 
   cert: fs.readFileSync(cfg.ssl_cert) 
}, processRequest).listen(cfg.port);
	
//需要我们的 websocket 库
var WebSocketServer = require('ws').Server;

//在端口 9090 处创建一个 websocket 服务器
var wss = new WebSocketServer({server: app});

//所有连接到服务器的用户
var users = {};

//需要 Node.js 中的 redis 库
var redis = require("redis");

//创建 redis 客户端对象
var redisClient = redis.createClient();

//当用户连接到我们的服务器时
wss.on('connection', function(connection){
//...其他代码

在上面的代码中,我们需要 fs 库来读取私钥和证书,使用私钥和证书的绑定端口和路径创建 cfg 对象。然后,我们使用密钥创建一个 HTTPS 服务器,并在端口 9090 上创建一个 WebSocket 服务器。

现在在 Opera 中打开 https://localhost:9090。您应该看到以下 −

Invalid Certification

单击"仍然继续"按钮。您应该看到"OK"消息。

为了测试我们的安全信令服务器,我们将修改我们在"WebRTC Text Demo"教程中创建的聊天应用程序。我们只需要添加一个密码字段。以下是整个 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 = ""> 
               <input type = "text" id = "passwordInput" 
                  class = "form-control form-group" placeholder = "Password"
                  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>

我们还需要通过此行 var conn = new WebSocket('wss://localhost:9090');client.js 文件中启用安全套接字连接。请注意 wss 协议。然后,必须修改登录按钮处理程序以将密码与用户名一起发送 −

loginBtn.addEventListener("click", function (event) { 
   name = usernameInput.value; 
   var pwd = passwordInput.value;
	
   if (name.length > 0) { 
      send({ 
         type: "login", 
         name: name, 
         password: pwd 
      }); 
   } 
	
});

以下是整个 client.js 文件 −

//我们的用户名
var name;
var ConnectedUser;

//连接到我们的信令服务器
var conn = new WebSocket('wss://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 passwordInput = document.querySelector('#passwordInput'); 
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";
  
// 当用户点击按钮时登录 
loginBtn.addEventListener("click", function (event) { 
   name = usernameInput.value; 
   var pwd = passwordInput.value;  
	
   if (name.length > 0) { 
      send({ 
         type: "login", 
         name: name, 
         password: pwd 
      }); 
   } 
	
}); 
 
function handleLogin(success) { 
   if (success === false) {
      alert("Ooops...incorrect username or password"); 
   } else { 
      loginPage.style.display = "none"; 
      callPage.style.display = "block";
		
      //********************** 
      //启动对等连接
      //********************** 
		
      //使用Google公共stun服务器
      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); 
      };
		
      //当我们收到对方的消息时,将其显示在屏幕上 
      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"); 
      });  
   } 
});
 
//当有人向我们发送 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; 
   yourConn.close(); 
   yourConn.onicecandidate = null; 
}; 
 
//当用户点击"发送消息"按钮时 
sendMsgBtn.addEventListener("click", function (event) { 
   var val = msgInput.value; 
   chatArea.innerHTML += name + ": " + val + "<br />"; 
	
   //向已连接的对等体发送消息
   dataChannel.send(val); 
   msgInput.value = ""; 
});

现在通过 node server 运行我们的安全信令服务器。在修改后的聊天演示文件夹中运行 node static。在两个浏览器选项卡中打开 localhost:8080。尝试登录。请记住,只有"user1"和"password1"以及"user2"和"password2"才允许登录。然后建立 RTCPeerConnection(呼叫另一个用户)并尝试发送消息。

建立 RTCPeerConnection

以下是我们安全信令服务器的完整代码 −

//require file system module
var fs = require('fs'); 
var httpServ = require('https');

//https://github.com/visionmedia/superagent/issues/205 
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
  
//安全服务器将绑定到端口 9090
var cfg = { 
   port: 9090, 
   ssl_key: 'server.key', 
   ssl_cert: 'server.crt' 
};
  
//如果是 http 请求,只需返回"OK"
var processRequest = function(req, res){ 
   res.writeHead(200); 
   res.end("OK"); 
};
  
//创建启用 SSL 的服务器
var app = httpServ.createServer({ 
   key: fs.readFileSync(cfg.ssl_key), 
   cert: fs.readFileSync(cfg.ssl_cert) 
}, processRequest).listen(cfg.port);
  
//需要我们的 websocket 库
var WebSocketServer = require('ws').Server;

//在端口 9090 处创建一个 websocket 服务器
var wss = new WebSocketServer({server: app});

//所有连接到服务器的用户
var users = {};

//需要 Node.js 中的 redis 库
var redis = require("redis");

//创建 redis 客户端对象
var redisClient = redis.createClient();

//当用户连接到我们的服务器时
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 = {}; 
      } 
		
      //检查用户是否已经通过身份验证
      if(data.type != "login") { 
         //如果用户没有经过身份验证
         if(!connection.isAuth) { 
            sendTo(connection, { 
               type: "error", 
               message: "You are not authenticated" 
            }); 
				
            return; 
         } 
      }
		
      //切换用户消息类型
      switch (data.type) { 
         //当用户尝试登录时
         case "login":
            console.log("User logged:", data.name); 
            //从 redis 数据库获取此用户名的密码
            redisClient.get(data.name, function(err, reply) {
			  
               //检查密码是否与redis中存储的密码匹配
               var loginSuccess = reply === data.password;
				  
               //如果有人用这个用户名或错误的密码登录
                  then refuse 
               if(users[data.name] || !loginSuccess) { 
                  sendTo(connection, { 
                     type: "login", 
                     success: false 
                  }); 
               } else { 
                  //在服务器上保存用户连接 
                  users[data.name] = connection; 
                  connection.name = data.name; 
                  connection.isAuth = true; 
						
                  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;
				
         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" 
                    }); 
                  } 
						
               } 
            } 
         });
			
         default: 
            sendTo(connection, { 
               type: "error", 
               message: "Command no found: " + data.type 
            }); 
				
            break; 
      }  
   });
	
   //当用户退出时,例如关闭浏览器窗口
   //如果我们仍处于"offer","answer" 或 "candidate" 状态,这可能会有所帮助
   connection.on("close", function() { 
      if(connection.name) { 
         delete users[connection.name]; 
      } 
   });
	
   connection.send("Hello from server"); 
});
  
function sendTo(connection, message) { 
   connection.send(JSON.stringify(message)); 
}

总结

在本章中,我们为信令服务器添加了用户身份验证。我们还学习了如何创建自签名 SSL 证书并在 WebRTC 应用程序范围内使用它们。