三、浏览器之间的实时通信

为了在需要实时对等(浏览器到浏览器)数据传输的网站中实现音频/视频聊天或其他一些功能,或者需要从麦克风、网络摄像头或任何其他设备检索音频/视频流,我们必须使用浏览器插件,如 Java 和 Flash。让网站依赖浏览器插件有各种各样的问题,比如移动浏览器不支持插件,插件需要保持最新。因此,WebRTC 的引入解决了这些问题,即支持 WebRTC 的浏览器提供了 API,可以直接在浏览器之间实时交换数据,也可以从物理媒体源中检索流,而无需使用插件。在这一章中,我们将讨论 WebRTC,以及包装 WebRTC APIs 的 PeerJS 库,以提供一个易于使用的 API 来使用 WebRTC。

在本章中,我们将涵盖以下主题:

  • 讨论 WebRTC 提供的各种 API
  • 从物理媒体输入设备检索流
  • 显示媒体流
  • 讨论 WebRTC 使用的协议
  • 使用 PeerJS 在对等点之间交换媒体流和任意数据
  • 讨论与 WebRTC 和 PeerJS 基础相关的主题

术语

在我们进入 WebRTC 和 PeerJS 之前,你需要知道我们将要使用的一些术语的含义。这些术语将在以下章节中讨论。

溪流

一个是任何种类的数据的序列,随着时间的推移而变得可用。流对象表示一个流。通常,事件处理程序或回调被附加到流对象,每当有新数据可用时就调用它。

一个媒体流是一个其数据或者是音频或者是视频的流。类似地,媒体源 是提供音频或视频数据的物理设备、文件或其他东西。一个媒体消费者 也是一个物理设备、API 或者使用媒体流的东西。

WebRTC 允许我们检索物理媒体源的媒体流,如麦克风、网络摄像头、屏幕等。我们将在本章的后面讨论它。

对等网络模型

对等模式是客户端-服务器模式的对立面。在客户机-服务器模型中,服务器向客户机提供资源,而在对等模型中,网络中的每个 Node 都充当服务器和客户机,即每个 Node 都提供和消耗资源。对等模型中的对等体直接相互通信。

为了建立对等连接,我们需要一个信令服务器,用于信令。信令指的是对等方之间建立对等连接所需的数据交换。建立对等连接需要会话控制消息、网络配置等数据。信令服务器实现信令协议,例如 SIP、叮当声或一些其他协议。

根据应用的需求和资源可用性来选择模型。让我们考虑一些例子:

  • 要构建一个视频聊天应用,我们应该使用对等模型,而不是客户端-服务器模型。在这种情况下,由于每个 Node 都将产生大量数据(或帧),并将数据实时发送到其他 Node,服务器需要大量网络和其他资源,从而增加了服务器的运行成本。因此,点对点模式是视频聊天应用的最佳选择。例如,Skype 视频聊天基于对等模式。
  • 要构建一个将消息存储在集中数据库中的文本聊天应用,我们应该使用客户机-服务器模型,因为客户机产生的数据量不是很大,您也希望将消息存储在集中数据库中。例如,脸书信使基于客户机-服务器模式。

要使用 WebRTC 建立对等连接,您将需要一个信令服务器、STUN 服务器和可选的 TURN 服务器。我们将在本章的后面讨论它。

实时数据

实时数据是需要处理和传输的数据,没有太多延迟。比如视频聊天、直播分析、直播股价、直播流媒体、文字聊天、直播评分、在线多人游戏数据等等都是实时数据。

实时数据传输是一项难以实现的任务。用于实时数据传输的技术取决于数据量以及数据传输过程中的数据丢失是否可容忍。如果实时数据量很大,数据丢失是不可容忍的,那么实现实时数据传输就需要大量的资源,实际上不可能实现实时数据传输。比如视频聊天的时候,每个用户都会生成很多帧。如果丢失了一些帧,那么是可以容忍的,因此在这种情况下,我们可以使用 UDP 协议作为传输层协议,它不可靠,而且开销也比 TCP 小,使得 UDP 非常适合视频聊天应用。

WebRTC 允许我们使用 SRTP 协议传输它产生的实时媒体流。为了传输任意数据,它使用 SCTP 协议。我们将在本章后面讨论这些协议。

WebRTC 简介

网络实时通信(WebRTC) 是一种浏览器技术,能够检索物理媒体源的媒体流,并实时交换媒体流或任何其他数据。它包括三个应用编程接口:MediaStream构造器、RTCPeerConnection构造器和RTCDataChannel接口。

简而言之,MediaStream用于检索物理媒体源的流,RTCPeerConnection用于在对等体之间实时交换MediaStream,最后,RTCDataChannel用于在对等体之间交换任意数据。

让我们看看这些 API 是如何工作的。

媒体流接口

MediaStream API 的两个主要组件是MediaStream构造器和MediaStreamTrack接口。

轨道代表媒体源的流。一个轨迹实现MediaStreamTrack界面。轨道可以是音频轨道,也可以是视频轨道。也就是说,附加到音频源的轨道是音频轨道,而附加到视频源的轨道是视频轨道。特定媒体源可以附加多个轨道。我们还可以将约束附加到轨道上。例如,连接到网络摄像头的轨道可能会有一些限制,例如最低视频分辨率和 FPS。每条赛道都有自己的限制。

使用MediaStreamTrack界面的applyConstraints()方法创建轨迹后,您可以更改轨迹的约束。您可以使用MediaStreamTrack界面的getSettings()方法随时检索应用于轨迹的约束。要从媒体源分离轨道,即永久停止轨道,我们可以使用MediaStreamTrack界面的stop()方法。要暂停一个曲目,即暂时停止该曲目,我们可以将false分配给MediaStreamTrack界面的enabled属性。

上找到更多关于MediaStreamTrack界面的信息。

轨道可以是本地轨道,也可以是远程轨道。本地轨道代表本地媒体源的流;而远程轨道代表远程媒体源的流。您不能将约束应用到远程轨道。要找到一个轨道是本地的还是远程的,我们可以使用MediaStreamTrack界面的remote属性。

我们会在对等点之间交换轨道时遇到远程轨道。当我们向一个对等方发送本地轨道时,另一个对等方会收到该轨道的远程版本。

A MediaStream将多首曲目组合在一起。从技术上来说,它没有任何作用。它只是代表一组应该同步播放、存储或传输的曲目。

了解更多关于 https://developer.mozilla.org/en/docs/Web/API/MediaStream 的MediaStream建造师的信息。

MediaStreamTrack对象的 getSources()方法允许我们检索所有媒体设备的 ID,比如扬声器、麦克风、网络摄像头等等。如果标识代表媒体输入设备,我们可以使用标识来创建轨道。下面是一个演示这一点的示例:

MediaStreamTrack.getSources(function(sources){
  for(var count = 0; count < sources.length; count++)
  {
    console.log("Source " + (count + 1) + " info:");
    console.log("ID is: " + sources[count].id);

    if(sources[count].label == "")
    {
      console.log("Name of the source is: unknown");
    }
    else
    {
      console.log("Name of the source is: " + sources[count].label);
    }

    console.log("Kind of source: " + sources[count].kind);

    if(sources[count].facing == "")
    {
      console.log("Source facing: unknown");
    }
    else
    {
      console.log("Source facing: " + sources[count].facing);
    }
  }
})

每个人的产出会有所不同。这是我得到的输出:

Source 1 info:
ID is: 0c1cb4e9e97088d405bd65ea5a44a20dab2e9da0d298438f82bab57ff9787675
Name of the source is: unknown
Kind of source: audio
Source facing: unknown
Source 2 info:
ID is: 68fb69033c86a4baa4a03f60cac9ad1c29a70f208e392d3d445f3c2d6731f478
Name of the source is: unknown
Kind of source: audio
Source facing: unknown
Source 3 info:
ID is: c83fc025afe6c7841a1cbe9526a6a4cb61cdc7d211dd4c3f10405857af0776c5
Name of the source is: unknown
Kind of source: video
Source facing: unknown

导航器. 获取使用媒体

有各种带着轨迹返回MediaStream的 API。其中一种方法就是navigator.getUserMedia()。使用navigator.getUserMedia(),我们可以从媒体输入源中检索流,例如麦克风、网络摄像头等。下面是一个示例来演示:

navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia;

var constraints = {
  audio: true, 
  video: {
    mandatory: {
      minWidth: 640,
      minHeight: 360
    },
    optional: [{
      minWidth: 1280
    }, {
      minHeight: 720
    }]
  }
}

var av_stream = null;

navigator.getUserMedia(constraints, function(mediastream){
  av_stream = mediastream; //this is the MediaStream
}, function(err){
  console.log("Failed to get MediaStream", err);
});

当您运行上述代码时,浏览器将显示一个弹出窗口,寻求用户的许可。用户必须给予代码访问媒体输入设备的许可。

默认情况下,使用getUserMedia()时轨道所连接的媒体输入设备取决于浏览器。有些浏览器让用户选择想要使用的音频和视频设备,而其他浏览器则使用操作系统配置中列出的默认音频和视频设备。

我们还可以在约束对象的audiovideo属性的mandatory属性中提供分配给媒体输入设备的标识的sourceId属性,以使getUserMedia()将轨道附加到这些设备。所以,如果有多个摄像头和麦克风,那么可以使用MediaStreamTrack.getSources()让用户选择一个媒体输入设备,并将这个媒体输入设备 ID 提供给getUserMedia(),而不是依赖浏览器,这并不能保证是否会让用户选择一个媒体输入设备。

它采用的第一个参数是带有音频和视频轨道约束的约束对象。强制约束是那些必须应用的约束。可选表示它们不是很重要,因此如果无法应用它们,可以省略它们。

音轨的一些重要约束是volumesampleRatesampleSizeechoCancellation。视频轨道的一些重要约束是aspectRatiofacingModeframeRateheightwidth。如果未提供约束,则使用其默认值。

如果不想分别创建音频或视频轨道,可以简单地将audiovideo属性设置为false

我们可以使用MediaStreamgetTracks()方法检索MediaStream的轨迹。类似地,我们可以分别使用addTrack()removeTrack()方法添加或移除轨迹。每当添加曲目时,触发onaddtrack事件。类似地,每当轨道被移除时,onendtrack被触发。

如果我们已经有了一些轨迹,那么我们可以直接使用MediaStream构造器用轨迹创建MediaStreamMediaStream构造器获取一组轨迹,并返回MediaStream,其中添加了轨迹的参考。

MediaStream的轨迹读取数据的应用编程接口被称为MediaStream消费者。一些MediaStream消费者是<audio>标签、<video>标签、RTCPeerConnectionMedia Recorder原料药、Image Capture原料药、Web Audio原料药等等。

下面是一个演示如何在视频标签中显示MediaStream轨迹数据的例子:

<!doctype html>
<html>
  <body>

    <video id="myVideo"></video>
    <br>
    <input value="Pause" onclick="pause()" type="button" />

    <script type="text/javascript">

      navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia;

      var constraints = {
        audio: true, 
        video: true
      }

      var av_stream = null;

      navigator.getUserMedia(constraints, function(mediastream){

        av_stream = mediastream;

        document.getElementById("myVideo").setAttribute("src", URL.createObjectURL(mediastream));
        document.getElementById("myVideo").play();
      }, function(err){
        console.log("Failed to get MediaStream", err);
      });

      function pause()
      {
        av_stream.getTracks()[0].enabled = !av_stream.getTracks()[0].enabled;
        av_stream.getTracks()[1].enabled = !av_stream.getTracks()[1].enabled;
      }

    </script>
  </body>
</html>

这里我们有一个<video>标签和一个暂停按钮。视频标签获取一个网址并显示资源。

在 HTML5 之前,HTML 标签和 CSS 属性只能读取http://file://URL 的数据。然而,在 HTML5 中,他们可以阅读blob://data://mediastream://等类似的网址。

要在<video>标签中显示MediaStream的输出,我们需要使用URL.createObjectURL()方法,该方法获取一个 blob、文件对象或MediaStream,并提供一个读取其数据的网址。URL.createObjectURL()需要额外的内存和 CPU 时间来访问通过网址传递给它的值,因此,当我们不再需要网址时,使用URL.revokeObjectURL()释放网址是明智的。

如果MediaStream中有多个音视频轨道,则<video>读取第一个音视频轨道。

RTCPeerConnection API

RTCPeerConnection允许两个浏览器实时交换 MediaStreamRTCPeerConnectionRTCPeerConnection构造器的一个实例。

建立对等连接

为了建立对等连接,需要信令服务器。通过信令服务器,对等方交换建立对等连接所需的数据。实际的数据传输直接发生在点对点之间。信令服务器只是用来交换建立对等连接的预先要求。一旦建立了对等连接,两个对等体都可以断开与信令服务器的连接。信令服务器不需要是高度配置的服务器,因为实际数据不通过它传输。单个对等连接的数据传输将在几千字节,所以一个合适的服务器可以用于信令。

信令服务器通常使用信令协议,但是如果它是一个 HTTP 服务器,只要它可以在两个对等体之间传递消息,也是可以的。WebRTC 不会强迫我们使用任何特定的信令协议。

例如,假设有两个用户,爱丽丝和鲍勃,在两个不同的浏览器上。如果爱丽丝想与鲍勃建立点对点的聊天连接,那么这就是他们之间建立点对点连接的方式:

  1. 它们都将连接到信号服务器。
  2. 然后,爱丽丝将通过信令服务器向鲍勃发送请求,请求聊天。
  3. 信令服务器可以可选地检查是否允许爱丽丝与鲍勃聊天,以及爱丽丝和鲍勃是否登录。如果是,则信令服务器将消息传递给鲍勃。
  4. Bob 收到请求后,通过信令服务器向 Alice 发送消息,确认建立对等连接。
  5. 现在,他们都需要交换与会话控制、网络配置和媒体功能相关的消息。所有这些信息都通过RTCPeerConnection在它们之间交换。因此,它们都需要创建一个RTCPeerConnection,启动它,并为RTCPeerConnection附加一个事件处理程序,当RTCPeerConnection想要通过信令服务器发送消息时,该事件处理程序将被触发。RTCPeerConnection将消息以会话描述协议 ( SDP )格式传递给事件处理程序,从信令服务器接收到的RTCPeerConnection消息必须以 SDP 格式反馈给RTCPeerConnection,即RTCPeerConnection只理解 SDP 格式。您需要使用自己的编程逻辑来拆分自定义消息和RTCPeerConnection的消息。

前面的步骤似乎没有问题;但是,也存在一些主要问题。对等体可能在 NAT 设备或防火墙后面,因此找到它们的公共 IP 地址是一项具有挑战性的任务,有时实际上不可能找到它们的 IP 地址。那么,RTCPeerConnection当对等体可能在 NAT 设备或防火墙后面时,如何找到它们的 IP 地址呢?

RTCPeerConnection使用一种称为交互式连接建立 ( ICE )的技术来解决所有这些问题。

ICE 涉及 NAT ( STUN )的会话遍历实用程序和使用 NAT ( TURN )服务器周围的中继进行的遍历来解决问题。STUN 服务器用于查找对等方的公共 IP 地址。如果找不到对等体的 IP 地址,或者由于某种其他原因无法建立对等体,则使用 TURN 服务器来重定向 Flex,也就是说,两个对等体都通过 TURN 服务器进行通信。

我们只需要提供 STUN 和 TURN 服务器的地址,RTCPeerConnection处理剩下的。谷歌提供了一个公共的 STUN 服务器,每个人都在使用。构建 TURN 服务器需要大量资源,因为实际的数据流会抛出它。因此,WebRTC 使使用 TURN 服务器成为可选的。如果RTCPeerConnection无法在两个对等体之间建立直接通信,并且没有提供 TURN 服务器,则对等体没有其他通信方式,对等连接建立失败。

WebRTC 没有提供任何使信号安全的方法。你的工作是确保信号安全。

传输媒体流

我们看到如何RTCPeerConnection建立对等连接。现在,要转移MediaStream,我们只需要将MediaStream的引用传递给RTCPeerConnection,它就会将MediaStream转移给连接的对等体。

当我们说MediaStream被转移时,我们指的是单个轨道的流被转移。

以下是关于MediaStream的转移你需要知道的一些事情:

  • RTCPeerConnection使用 SRTP 作为应用层协议,UDP 作为传输层协议来传输MediaStream。SRTP 专为实时媒体流传输而设计。
  • UDP 不保证数据包的顺序,但 SRTP 会注意帧的顺序。
  • 数据报传输层安全 ( DTLS )协议用于保护MediaStream传输。所以,转移MediaStream的时候不用担心安全问题。
  • 远程对等点接收的轨道的约束可能不同于本地轨道的约束,因为RTCPeerConnection根据带宽和其他网络因素自动修改流以加快传输,从而实现实时数据传输。例如,RTCPeerConnection在传输时可能会降低视频流的分辨率和帧率。
  • 如果您从已经发送的MediaStream中添加或删除了一个曲目,则RTCPeerConnection通过信令服务器与另一个对等方通信来更新另一个对等方的MediaStream
  • 如果暂停正在发送的曲目,则RTCPeerConnection暂停该曲目的传输。
  • 如果您停止正在发送的曲目,RTCPeerConnection将停止该曲目的传输。

您可以通过单个RTCPeerConnection发送和接收多个MediaStream实例,也就是说,您没有来创建多个RTCPeerConnection实例,以便向对等方发送和从对等方接收多个MediaStream实例。每当您向RTCPeerConnection添加新的MediaStream或从RTCPeerConnection移除新的【】时,对等方通过信令服务器交换与此相关的信息。

RTCDataChannel API

RTCDataChannel是用来在对等体之间传输MediaStream以外的数据,以传输任意数据。建立对等连接以传输任意数据的机制类似于前面部分中解释的机制。

RTCDataChannel是实现RTCDataChannel接口的对象。

以下是关于RTCDataChannel你需要知道的一些事情:

  • RTCDataChannel使用 UDP 上的 SCTP 作为传输层协议来传输数据。它不使用未分层的 SCTP 协议,因为许多操作系统不支持 SCPT 协议。
  • SCTP 可以配置可靠性和交付顺序,不像 UDP 那样不可靠和无序。
  • RTCDataChannel还使用 DTLS 来保护数据传输。因此,通过RTCDataChannel传输数据时,您完全不用担心安全性。

我们可以在浏览器之间打开多个对等连接。例如,我们可以有三个对等连接,即第一个用于网络摄像头流传输,第二个用于文本消息传输,第三个用于文件传输。

使用 PeerJS 的 WebRTC 应用

PeerJS 是一个客户端 JavaScript 库,它提供了一个易于使用的 API 来与 WebRTC 一起工作。它只提供了一个在对等体之间交换MediaStream和任意数据的 API。它没有提供与MediaStream一起工作的 API。

对等服务器

PeerServer 是 peer js 用来建立对等连接的开源信令服务器。PeerServer 是用 Node.js 编写的,如果不想运行自己的 PeerServer 实例,那么可以使用 PeerServer 云,它托管供公众使用的 PeerServer。PeerServer 云允许您免费建立最多 50 个并发连接。

唯一的标识标识连接到对等服务器的每个对等方。对等服务器本身可以生成标识,或者对等服务器可以提供自己的标识。一个对等点要与另一个对等点建立对等连接,只需要知道另一个对等点的 ID。

当您想要向对等服务器添加更多功能或想要支持 50 多个并发连接时,您可能想要运行自己的对等服务器实例。例如,如果您想检查用户是否登录到对等服务器,那么您需要添加此功能并托管您自己定制的对等服务器。

在本章中,我们将使用对等服务器云,但在下一章中,我们将创建自己的对等服务器实例。因此,要继续本章,请在对等服务器云上创建一个帐户并检索应用编程接口密钥。每个应用都获得一个访问对等服务器云的应用编程接口密钥。如果您托管自己的对等服务器,则不需要应用编程接口密钥。PeerServer 云使用 API 键来跟踪应用建立的总连接。要创建帐户并检索应用编程接口密钥,请访问http://peerjs.com/peerserver

peerjt API

让我们通过创建一个简单的应用来讨论 PeerJS 应用接口,该应用允许用户与任何有身份的用户交换视频和短信。

在你的网络服务器中创建一个peerjs-demo目录,并在其中放置一个名为index.html的文件。

index.html文件中,我们需要先将PeerJS库入队。从http://peerjs.com/下载PeerJS。在撰写本文时,PeerJS 的最新版本是 0.3.14。对于下面的例子,我建议您坚持这个版本。将此起始代码放入index.html文件中:

<!doctype html>
<html>
  <head>
    <title>PeerJS Demo</title>
  </head>
  <body>

    <!-- Place HTML code here -->

    <script src="peer.min.js"></script>
    <script>
      //place JavaScript code here
    </script>
  </body>
</html>

在这里,我将迷你版本的 PeerJS 加入了队列。

PeerJS API 由三个主要构造函数组成,如下所示:

  • Peer:一个Peer的实例代表网络中的一个对等体。对等体连接到信令服务器和 STUN,并可选地连接到 TURN。
  • DataConnection : DataConnection(即DataConnection的实例)表示为对等连接,用于交换任意数据。技术上,它包裹着RTCDataChannel
  • MediaConnection : MediaConnection(即MediaConnection的实例)代表一个用来交换MediaStream的对等连接。技术上,它包裹RTCPeerConnection

如果一个对等体想要与另一个对等体建立DataConnectionMediaConnection,那么它只需要知道另一个对等体的 ID。贵族不会给另一个贵族选择接受或拒绝DataConnection。同样,在MediaConnection的情况下,PeerJS 不给另一个对等体接受或拒绝MediaConnection的选项,但是MediaConnection将是不活动的,直到它被另一个对等体以编程方式激活,以便MediaStream可以被转移,否则MediaStream将不会被转移。所以,我们可以自己写逻辑,让对方用户接受或者拒绝DataConnection或者MediaConnecton,也就是DataConnection或者MediaConnection一成立,就可以通过询问用户意见来取消。

目前一个MediaConnection只能转一个MediaStream。在未来版本的 PeerJS 中,单个MediaConnection将支持多个媒体流的传输。

现在,我们需要创建一个<video>标签来显示视频,一个按钮来连接到对等点,还有一个文本框来发送消息。下面是显示所有这些的 HTML 代码:

<video id="remoteVideo"></video>
<br>
<button onclick="connect()">Connect</button>
<br>
<input type="text" id="message">
<button onclick="send_message()">Send Message</button>

现在随着页面的加载,我们需要连接到PeerServer和 ICE 服务器,这样其他对等方就可以和我们对话,并且当用户点击连接按钮时,我们可以建立DataConnectionMediaConnection。以下是这方面的代码:

var peer = null;

window.addEventListener("load", function(){
  var id = prompt("Please enter an unique name");

  peer = new Peer(id, {key: "io3esxy6y43zyqfr"}); 

  peer.on("open", function(id){
    alert("Connected to PeerServer successfully with ID: " + id);
  });

  peer.on("error", function(err){
    alert("An error occured. Error type: " + err.type);
  })

  peer.on("disconnected", function(){
    alert("Disconnected from signaling server. You ID is taken away. Peer-to-peer connections is still intact");
  })

  peer.on("close", function(){
    alert("Connection to signaling server and peer-to-peer connections have been killed. You ID is taken away. You have been destroyed");
  })

  peer.on("connection", function(dataConnection){
    setTimeout(function(){
      if(confirm(dataConnection.peer + " wants to send data to you. Do you want to accept?"))
      {
        acceptDataConnection(dataConnection);
      }
      else
      {
        dataConnection.close();
      }
    }, 100)
  })

  peer.on("call", function(mediaConnection){
    setTimeout(function(){
      if(confirm("Got a call from " + mediaConnection.peer + ". Do you want to pick the call?"))
      {
        acceptMediaConnection(mediaConnection);
      }
      else
      {
        mediaConnection.close();
      }
    }, 100);
  })
});

下面是代码的工作原理:

  • 首先,我们显示了一个提示框,将该标识作为输入,以便每个对等体都可以决定自己的标识。
  • 然后我们用 ID 和 PeerServer 云密钥创建了一个Peer的实例。这里我们没有提供信令和 ICE 服务器的 URL,因此,PeerJS 将使用 PeerServer 云作为信令服务器和谷歌的公共 STUN 服务器。它不会使用任何 TURN 服务器。一旦创建了Peer实例,该实例就会连接到信令服务器并注册给定的标识。
  • 然后我们将五个事件处理程序附加到peer对象上。
  • 当与PeerServer的连接成功时,触发 open事件。
  • 对于peer对象上的错误,触发 error事件。
  • 当与信令服务器的连接断开时,触发 disconnected事件。由于网络问题或手动调用peer.disconnect()方法,与信令服务器的连接可能会断开。一旦你断开连接,你的身份证就可以被别人拿走。您可以尝试使用peer.reconnect()方法重新连接同一个标识。您可以使用peer.disconnect布尔属性检查peer是否连接到信令服务器。
  • close事件在peer被破坏时触发,即不能再使用,所有MediaConnectionsDataConnections被杀死,与信令服务器的连接被杀死,ID 被拿走,等等。当你不再需要的时候,你可能想要手动摧毁peer。您可以使用peer.destroy()方法摧毁一个对等体。
  • 当其他对等方与您建立DataConnection时,触发 connection事件。正如我之前所说的,DataConnection是在没有进一步许可的情况下建立的,但是如果你愿意,你可以在它建立后立即关闭它。这里我们让用户决定是继续还是关闭另一个对等体建立的DataConnection。附加到事件的事件处理程序通过代表当前建立的DataConnection的参数接收DataConnection的实例。
  • 当其他对等方与您建立MediaConnection时,触发 call事件。在这里,我们还让用户决定是继续还是关闭另一个对等点建立的MediaConnection。附加到事件的事件处理程序通过代表当前建立的MediaConnection的参数接收MediaConnection的实例。
  • 这里,在callconnection事件处理程序中,我们异步显示了确认弹出框,以防止阻止导致某些浏览器出现问题的事件处理程序的执行,即阻止它无法建立DataConnectionMediaConnection

现在,让我们实现acceptDataConnection()acceptMediaConnection()功能,以便当其他对等方与我们建立DataConnectionMediaConnection时,我们可以显示短信和远程MediaStream。下面是代码:

navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia;

var myDataConnection = null;
var myMediaConnection = null;

function acceptDataConnection(dataConnection)
{
  myDataConnection = dataConnection;

  dataConnection.on("data", function(data){
    alert("Message from " + dataConnection.peer + ".\n" + data)
  })

  dataConnection.on("close", function(data){
    alert("DataConnecion closed");
  })

  dataConnection.on("error", function(err){
    alert("Error occured on DataConnection. Error: " + err);
  })
}

function acceptMediaConnection(mediaConnection)
{
  myMediaConnection = mediaConnection;

  mediaConnection.on("stream", function(remoteStream){

    document.getElementById("remoteVideo").setAttribute("src", URL.createObjectURL(remoteStream));
    document.getElementById("remoteVideo").play();
  })

  mediaConnection.on("close", function(data){
    alert("MediaConnecion closed");
  })

  mediaConnection.on("error", function(err){
    alert("Error occured on MediaConnection. Error: " + err);
  })

  navigator.getUserMedia({video: true, audio: true}, function(mediaStream) {
    mediaConnection.answer(mediaStream);
  }, function(e){ alert("Error with MediaStream: " + e); });
}

以上代码是这样工作的:

  • acceptDataConnection()函数中,我们将三个事件处理程序附加到DataConnection上。当另一方向我们发送数据时,触发data事件。当DataConnection关闭时,触发close事件。最后,error事件在DataConnection发生错误时触发。我们可以使用dataConnection.close()方法手动关闭DataConnection
  • acceptMediaConnection()函数中,我们附加了三个事件处理程序,并将我们的MediaStream转移到另一个对等体。当其他对等方向我们发送MediaStream时,会触发stream事件。当MediaConnection关闭时,close事件被触发。最后,我们通过传递MediaStream使用mediaConnection.answer()方法激活MediaConnectionMediaConnection激活后,将触发stream事件。

我们完成了代码的编写,以处理另一个与我们同行的人建立的MediaConnectionDataConnection。现在我们需要编写一个代码来创建用户点击连接按钮的MediaConnectionDataConnection。下面是代码:

function connect()
{
  var id = prompt("Please enter other peer ID");
  establishDataConnection(id);
  establishMediaConnection(id);
}

function establishDataConnection(id)

{
  var dataConnection = peer.connect(id, {reliable: true, ordered: true});

  myDataConnection = dataConnection;

  dataConnection.on("open", function(){
    alert("DataConnecion Established");
  });

  dataConnection.on("data", function(data){
    alert("Message from " + dataConnection.peer + ".\n" + data)
  })

  dataConnection.on("close", function(data){
    alert("DataConnecion closed");
  })

  dataConnection.on("error", function(err){
    alert("Error occured on DataConnection. Error: " + err);
  })
}

function establishMediaConnection(id)
{
  var mediaConnection = null;

  navigator.getUserMedia({video: true, audio: true}, function(mediaStream) {
    mediaConnection = peer.call(id, mediaStream);

    myMediaConnection = mediaConnection;

    mediaConnection.on("stream", function(remoteStream){
      document.getElementById("remoteVideo").setAttribute("src", URL.createObjectURL(remoteStream));
      document.getElementById("remoteVideo").play();
    })

    mediaConnection.on("error", function(err){
      alert("Error occured on MediaConnection. Error: " + err);
    })

    mediaConnection.on("close", function(data){
      alert("MediaConnecion closed");
    })
  }, function(e){ alert("Error with MediaStream: " + e); });
}

下面是代码的工作原理:

  • 首先我们要求用户输入另一个用户的 ID。
  • 然后我们建立了DataConnection。要与另一个用户建立DataConnection,我们需要用其他对等方的标识调用Peer实例的connect()方法。我们也使DataConnection变得可靠和有序。然后,我们附加了事件处理程序。我们也看到了datacloseerror事件是如何工作的。open事件在DataConnection成立时触发。
  • 建立DataConnection后,我们建立了MediaConnection。要建立MediaConnection,我们需要调用Peer实例的call()方法。我们需要通过MediaStreamcall()的方法。最后,我们附加了事件处理程序。当其他用户调用MediaConnection实例的answer()方法时,即激活 MediaConnection 时,将触发stream事件。

现在我们需要做的最后一件事是编写代码,当用户点击发送消息按钮时发送消息。这是代码:

function send_message()
{
  var text = document.getElementById("message").value;

  myDataConnection.send(text);
}

要通过MediaConnection发送数据,我们需要调用MediaConnection实例的send()方法。这里,我们发送一个字符串,但是您可以传递任何类型的数据,包括 blobs 和对象。

现在,要测试应用,请在两个不同的浏览器、设备或选项卡中打开index.html页面网址。我假设您已经在两个不同的设备中打开了网址。在每个设备中,提供不同的标识来标识用户。然后单击任何一台设备中的连接按钮,并输入另一台设备的 ID。现在在另一台设备上接受请求。完成后,这两个设备将能够显示彼此的网络摄像头视频和麦克风音频。你也可以在他们之间发送消息。

您可以在http://peerjs.com/docs/#api找到 PeerJS API 的官方文档。

杂项

在撰写本文时,WebRTC 规范仍未最终确定。关于 WebRTC 做什么和如何工作的总体想法已经最终确定。只是 API 还在开发中。

例如,WebRTC 引入了navigator.getUserMedia()方法的替代方法,即navigator.mediaDevices.getUserMedia()方法。在撰写本文时,任何浏览器都不支持navigator.mediaDevices.getUserMedia()。两者的区别在于navigator.mediaDevices.getUserMedia()方法基于承诺模式,而navigator.getUserMedia()基于回调模式。由于向后兼容的原因,目前还没有摆脱navigator.getUserMedia()的计划,但未来navigator.getUserMedia()可能会被移除,因为 WebRTC 想要使用 promise 模式实现所有的 API,因此,很难维护多个做同样事情的 API。同样的,navigator.mediaDevices.enumerateDevice()MediaStreamTrack.getSources()的替代,也就是navigator.mediaDevices.enumerateDevice()是基于承诺模式。

你可以在http://www.w3.org/TR/#tr_Web_Real_Time_Communication找到 WebRTC 的官方规范。

由于同一个特性有多个 API,每个 API 都有不同的浏览器支持,所以 WebRTC 提供了一个名为adapter.js的脚本,它是一个将网站与规范更改和前缀差异隔离开来的垫片。你可以在https://github.com/webrtc/adapter找到垫片。

WebRTC 有一个 GitHub 存储库,里面放了很多示例项目,展示了一些可以使用 WebRTC 构建的东西。你可以在https://github.com/webrtc/samples找到这个仓库。只需查看示例及其源代码,您就可以了解更多关于 WebRTC 的信息。

总结

在本章中,我们通过创建一个简单的应用来讨论 WebRTC 和 PeerJS 的基础。我们讨论了 WebRTC 用来实现实时对等通信和读取物理媒体源流的各种协议、技术和其他技术。我们还看到了 PeerServer 的概述。现在,您必须能够轻松地使用 PeerServer 云构建任何类型的 WebRTC 应用。

在下一章中,我们将使用定制的 PeerServer 构建一个高级的 WebRTC 应用。