十二、将 WebRTC 用于视频聊天

最近几年,浏览器制造商突破了以前认为的原生 JavaScript 代码的界限,增加了 API 来支持许多新功能,包括我们在前一章中看到的基于像素的绘制,现在,终于有了一种方法,使用称为 Web 实时通信(WebRTC) API 的 API 通过互联网将多媒体数据(视频和音频)从一个浏览器传输到另一个浏览器,所有这些都不需要使用插件。虽然在撰写本文时,这种支持目前只出现在 Chrome、Firefox 和 Opera 的桌面版本中,占全球 web 使用量的 50%多一点(source:http://bit . ly/cani use _ webrtc),但我觉得这种技术对互联网通信的未来非常重要,作为开发人员,我们需要在这种 API 还处于采用阶段时就了解它。

在本章中,我们将介绍 WebRTC 规范的基础知识,包括如何使用对等网络从连接到设备的网络摄像头和麦克风发送和接收数据,以及如何使用 JavaScript 在浏览器中构建简单的视频聊天客户端。

WebRTC 规范

WebRTC 规范最初是由 Google 发起的,目的是包含在他们的 Chrome 浏览器中,并承诺提供一个 API,允许开发人员:

  • 检测设备功能,包括基于连接到设备的摄像头和/或麦克风的视频和/或音频支持
  • 从设备连接的硬件捕获媒体数据
  • 编码并通过网络传输媒体
  • 在浏览器之间建立直接的对等连接,自动处理防火墙或网络地址转换(NAT)带来的任何复杂问题
  • 解码媒体流,将其呈现给最终用户,使音频和视频同步,并消除任何音频回声

WebRTC 项目页面可以通过 http:// www。webrtc。org ,包括规范的当前状态,并包含跨浏览器互操作性的注释,以及演示和到其他网站的链接。

接入网络摄像头和麦克风

如果我们想使用 WebRTC 规范创建一个视频聊天应用,我们需要确定如何从连接到运行该应用的设备的网络摄像头和麦克风访问数据。JavaScript API 方法navigator.getUserMedia()是其中的关键。我们通过传递三个参数来调用它:一个详细说明我们希望访问哪种类型的媒体的对象(videoaudio是目前唯一可用的属性选项),一个在成功建立到网络摄像头和/或麦克风的连接时执行的回调函数,以及一个在没有成功建立到网络摄像头和/或麦克风的连接时执行的回调函数。当执行该方法时,浏览器会提示用户当前网页正试图访问他们的网络摄像头和/或麦克风,并提示他们是否允许或拒绝访问,如图 12-1 所示。如果他们拒绝访问,或者用户没有网络摄像头或麦克风连接,则执行第二个回调函数,指示多媒体数据不能被访问;否则,执行第一个回调函数。

A978-1-4302-6269-5_12_Fig1_HTML.jpg

图 12-1。

The user must allow or deny access to their webcam and microphone before we can access them

在撰写本文时,在某些浏览器中通过带前缀的方法名来访问getUserMedia()方法,但是在所有浏览器中具有相同的输入参数序列。因此,我们可以编写一个小的 polyfill 来支持在所有支持的浏览器中访问这个 API,这样我们就可以在整个代码中通过getUserMedia()方法名来访问它。清单 12-1 中的代码显示了一个简单的 polyfill,允许在所有支持的 web 浏览器中通过相同的方法调用来访问网络摄像头和麦克风。

清单 12-1。getUserMedia() API 的简单聚合填充

// Expose the browser-specific versions of the getUserMedia() method through the standard

// method name. If the standard name is already supported in the browser (as it is in Opera),

// use that, otherwise fall back to Mozilla's, Google's or Microsoft's implementations as

// appropriate for the current browser

navigator.getUserMedia = navigator.getUserMedia || navigator.mozGetUserMedia ||

navigator.webkitGetUserMedia || navigator.msGetUserMedia;

出于安全原因,WebRTC 仅在使用 web 服务器访问试图使用它的文件时工作,而不是直接从浏览器中加载的本地文件运行代码。有许多方法可以在本地启动 web 服务器来运行本章中的代码清单,尽管最简单的方法可能是从 http://httpd.apache.org 下载 Apache web 服务器软件并在您的机器上运行。如果你更喜欢冒险,请跳到第 14 章,在那里我将解释如何使用 Node.js 应用平台创建和运行本地 web 服务器。

我们可以使用清单 12-1 中的 polyfill 来调用getUserMedia()方法,尝试访问用户的网络摄像头和麦克风,如清单 12-2 所示。

清单 12-2。接入网络摄像头和麦克风

// Define a function to execute if we are successfully able to access the user's webcam and

// microphone

function onSuccess() {

alert("Successful connection made to access webcam and microphone");

}

// Define a function to execute if we are unable to access the user's webcam and microphone -

// either because the user denied access or because of a technical error

function onError() {

throw new Error("There has been a problem accessing the webcam and microphone");

}

// Using the polyfill from Listing 12-1, we know the getUserMedia() method is supported in the

// browser if the method exists

if (navigator.getUserMedia) {

// We can now execute the getUserMedia() method, passing in an object telling the browser

// which form of media we wish to access ("video" for the webcam, "audio" for the

// microphone). We pass in a reference to the onSuccess() and onError() functions which

// will be executed based on whether the user grants us access to the requested media types

navigator.getUserMedia({

video: true,

audio: true

}, onSuccess, onError);

} else {

// Throw an error if the getUserMedia() method is unsupported by the user's browser

throw new Error("Sorry, getUserMedia() is not supported in your browser");

}

当运行清单 12-2 中的代码时,如果您发现没有提示您允许访问网络摄像头和麦克风,请检查以确保您正在 web 服务器上的 HTML 页面的上下文中运行您的代码,无论是在本地计算机上还是通过网络连接。如果你的浏览器显示你正在使用file:///协议浏览一个 URL,那么你将需要通过http://使用一个网络服务器来运行它。在 Chrome 和 Opera 中,浏览器会在浏览器标签中显示的页面名称旁边使用红色圆圈图标,直观地表明它当前正在录制您的音频和/或视频,而 Firefox 会在地址栏中显示绿色摄像头图标来表明这一事实。

既然我们能够访问用户的网络摄像头和麦克风,我们需要能够对他们返回的数据做一些事情。由getUserMedia()方法触发的onSuccess()回调方法被传递一个参数,该参数表示由设备提供的原始数据流,以便在应用中使用。我们可以将这个流直接传递到页面上的 HTML5 <video>元素的输入中,允许用户从他们自己的网络摄像头和麦克风接收数据。清单 12-3 中的代码展示了如何使用浏览器的window.URL.createObjectURL()方法来实现这一点,它创建了一个特定的本地 URL,可以用来访问多媒体流以这种方式提供的数据。

清单 12-3。将网络摄像头和麦克风传回给用户

// Use the getUserMedia() polyfill from Listing 12-1 for best cross-browser support

// Define a function to execute if we are successfully able to access the user's webcam and

// microphone, taking the stream of data provided and passing it as the "src" attribute of a

// new <video> element, which is then placed onto the current HTML page, relaying back to the

// user the output from theirwebcam and microphone

function onSuccess(stream) {

// Create a new <video> element

var video = document.createElement("video"),

// Get the browser to create a unique URL to reference the binary data directly from

// the provided stream, as it is not a file with a fixed URL

videoSource = window.URL.createObjectURL(stream);

// Ensure the <video> element start playing the video immediately

video.autoplay = true;

// Point the "src" attribute of the <video> element to the generated stream URL, to relay

// the data from the webcam and microphone back to the user

video.src = videoSource;

// Add the <video> element to the end of the current page

document.body.appendChild(video);

}

function onError() {

throw new Error("There has been a problem accessing the webcam and microphone");

}

if (navigator.getUserMedia) {

navigator.getUserMedia({

video: true,

audio: true

}, onSuccess, onError);

} else {

throw new Error("Sorry, getUserMedia() is not supported in your browser");

}

既然我们有能力访问用户的网络摄像头和麦克风,并将他们的输入反馈给用户,我们就有了完全在浏览器中运行的简单双向视频聊天应用的开端。

创建一个简单的视频聊天 Web 应用

让我们通过创建一个浏览器内视频聊天 web 应用来了解 WebRTC 规范的重要部分。

视频聊天应用的要点是,我们从正在浏览同一 web 服务器的两个用户的设备中捕获视频和音频,并将捕获的视频和音频流从一个设备传输到另一个设备,反之亦然。我们已经介绍了如何使用getUserMedia() API 方法从用户设备捕获数据流,所以让我们研究一下如何建立一个呼叫,以及如何发送和接收数据流,以便构建我们的视频聊天应用。

连接和信号

我们需要一种方法将一个设备直接连接到另一个设备,并在两者之间保持开放的数据连接。我们希望在两台设备之间建立直接或对等的连接,而不需要中间服务器来中继数据,从而将连接速度以及视频和音频质量保持在最佳水平。无论设备是使用公共 IP 地址直接连接到互联网、位于防火墙之后,还是位于采用网络地址转换(NAT)与本地网络中大量设备共享有限公共 IP 地址的路由器设备之后,这种连接都必须是可能的。

WebRTC 依赖于交互式连接建立(ICE)框架,该框架是一种规范,允许两个设备直接相互建立对等连接,而不管一个或两个设备是否直接连接到公共 IP 地址、防火墙后面或采用 NAT 的网络上。它简化了整个连接过程,所以我们不必担心这个问题,并且它可以在支持的浏览器中使用RTCPeerConnection“class”接口。

建立对等连接的过程包括创建这个RTCPeerConnection“类”的一个实例,传入一个包含一个或多个能够帮助建立设备间连接的服务器的详细信息的对象。这些服务器可以采用两种服务器协议,以不同的方式帮助建立这种连接:NAT 的会话穿越实用程序(STUN)和使用 NAT 周围中继的穿越(TURN)。

STUN 服务器将设备的内部 IP 地址和未使用的本地端口号映射到其外部可见的 IP 地址和未使用的面向外部的端口号(在本地网络之外),以便可以使用该端口将流量直接路由到本地设备。这是一个快速而有效的系统,因为服务器仅在初始连接时需要,并且一旦建立就可以移动到其他任务,然而仅在相当简单的 NAT 配置上工作,其中只有一个设备,即客户端,实际上位于 NAT 之后。任何支持多个 NAT 设备或其他大型企业系统的设置都不能使用这种类型的服务器建立对等连接。这就是另一个选择,转而介入的地方。

TURN 服务器使用公共互联网上的中继 IP 地址,通常是公共 TURN 服务器本身的 IP 地址,将一个设备连接到另一个设备,其中两个设备都在 NAT 之后。因为它起着中继的作用,所以它必须中继通信双方之间的所有数据,因为直接连接是不可能的。这意味着数据带宽被有效地减少,并且服务器必须在整个视频呼叫期间都是活动的以中继数据,这使得它不是一个有吸引力的解决方案,尽管即使在这样复杂的 NAT 设置中仍然允许视频呼叫发生。

ICE framework 使用适合所连接设备的 STUN 或 TURN 服务器,在视频聊天双方之间建立连接。ICE 的配置是通过传递一个被称为候选服务器的列表,然后对其进行优先级排序。它使用会话描述协议(SDP)来通知远程用户本地用户的连接细节,这被称为要约,并且远程用户在它识别要约时做同样的事情,被称为应答。一旦双方都有了对方的详细资料,他们之间的对等连接就建立了,通话就可以开始了。虽然看起来有很多步骤,但这一切都发生得非常快。

您的应用中有许多公共 STUN 服务器可供使用,包括一些由 Google 运行和操作的服务器,您可以在 http:// bit. ly/ stun_ list 上找到这些服务器的在线列表。公共 TURN 服务器不太常见,因为它们中继数据,因此需要大量的可用带宽,这可能是昂贵的;然而,一项服务可以通过http://number。维亚吉尼。ca ,这将允许你建立一个免费账户,通过他们的服务器运行你自己的眩晕/变身服务器。

包含 ICE 服务器详细信息的示例配置对象可能如下所示,用于建立与RTCPeerConnection“类”的对等连接:

{

iceServers: [{

// Mozilla's public STUN server

url: "stun:23.21.150.121"

}, {

// Google's public STUN server

url: "stun:stun.l.google.com:19302"

}, {

// Create your own TURN server athttp://numb.viagenie.caT2】

// escape any extended characters in the username, e.g. the @ character becomes %40

url: "turn:numb.viagenie.ca",

username: "denodell%40gmail.com",

credential: "password"

}]

}

现在,如果你仔细阅读,你可能会注意到一个令人困惑的情况,关于我们的点对点连接的设置,即:如果我们还不知道我们连接的是谁,我们如何通过中介在两个对等点之间建立连接?当然,答案是我们不能,这让我们进入了理解如何建立视频通话的下一部分——信令通道。

我们需要做的是提供一种机制,在呼叫建立之前和建立期间在连接方之间发送消息。每一方都需要监听另一方发送的消息,并在需要时发送消息。这种消息传递可以使用 Ajax 来处理,尽管这样效率会很低,因为双方都必须频繁地询问中间服务器另一方是否发送了任何消息——大多数情况下答案是“没有”。更好的解决方案是更新的EventSource API(在http://bit . ly/event source _ basics了解更多信息)或 WebSockets 技术(在http://bit . ly/web sockets _ basics了解更多信息),这两者都允许服务器将消息“推送”到连接的客户端后一种方法的好处是,它在支持 WebRTC 的所有浏览器中都受支持,因此使用这种方法不需要多种填充或变通方法。

将 Firebase 服务用于简单的信令

我们不用构建自己的服务器来支持 WebSocket 连接,而是可以利用现有的基于云的解决方案来代表我们在连接方之间存储和传输数据。一个这样的解决方案是 Firebase(可从 http:// firebase 获得)。com ,如图 12-2 所示,它提供了一个简单的在线数据库和一个小的 JavaScript API,用于使用 WebSockets 通过 web 访问数据(如果运行 WebSockets 的浏览器不支持 web sockets,它实际上会退回到其他解决方案)。其免费的基本解决方案足以满足我们作为信令服务连接和配置视频聊天客户端的需求。

A978-1-4302-6269-5_12_Fig2_HTML.jpg

图 12-2。

Firebase provides a data-access API perfect for realtime communication between devices

访问 Firebase 网站并注册一个免费帐户。然后,您将收到一封电子邮件,告知您新创建的在线数据库的详细访问信息。每一个都有自己独特的 URL 和在页面上使用的<script>标签的详细信息,以加载在代码中使用的 Firebase API。不管创建的 URL 是什么,<script>标签总是相同的:

<script src="https://cdn.firebase.com/v0/firebase.js"></scriptT2】

对于使用给定名称 https://glaring-fire-9593.firebaseio.com/ ,创建的 URL,可以使用以下 JavaScript 代码在您的代码中建立连接,并在数据库中保存数据:

var dataRef = new Firebase("https://glaring-fire-9593.firebaseio.comT2】

dataRef.set("I am now writing data into Firebase!");

Firebase 中的数据以类似于 JavaScript 对象的方式存储为嵌套对象。事实上,当您访问您的唯一 URL 时,您能够以您应该非常熟悉的伪 JSON 格式查看您的数据。可以使用 Firebase API 的set()方法将数据添加到数据库中,并且我们能够检测任何连接的客户端何时使用 API 的on()方法添加了数据。这意味着在我们的视频聊天中,一旦使用 ICE 框架建立了连接,双方就可以通知对方,当双方从对方收到所需的信息时,视频通话就可以开始了。

现在,如果我们在任何人都可以访问的公共 web 服务器上托管我们的视频聊天客户端,我们将需要一种方法来限制哪些人可以相互聊天,否则我们会冒着完全陌生的人相互建立呼叫的风险,因为他们是最先连接到服务器的两个用户——我们在这里不是在构建聊天轮盘!我们需要一种方式让特定的用户能够相互连接,一种简单的技术是允许连接的用户创建聊天室,并允许连接的客户能够创建或加入这些聊天室之一。当两个用户加入一个房间时,我们可以限制我们的信令只发生在该房间的用户之间,以便只有指定的各方可以相互通信。我们将允许视频聊天的第一方(发起者)命名他们的聊天室。然后,他们可以将这个房间名称通知给他们的呼叫伙伴,他们将在访问聊天客户端网页时指定这个房间名称。然后,我们将把各方之间发送和接收的要约和应答消息与聊天室名称相关联,并与 ICE 候选人详细信息一起直接连接双方,这样我们就可以通过这种方式将多方相互连接起来。这导致 Firebase 中的数据库结构可以在 JSON 中以类似于下面的格式表示:

{

"chatRooms": {

"room-001": {

"offer": "...",

"answer": "...",

"candidate": "..."

},

"room-002": {

...

}

}

}

Firebase 在需要客户端和服务器互相推送消息的应用中使用简单,这是它与视频聊天客户端一起使用的优势,提供了在同一个聊天中将双方连接在一起所需的信令通道。

构建视频聊天客户端

我们已经到了这样一个地步,我们可以将我们所学的一切整合在一起,构建一个视频聊天客户端,我们将构建一个如图 12-3 所示的例子。我们可以访问本地用户的网络摄像头和麦克风,我们可以建立一个信号通道,锁定到一个已知的聊天室名称,以共享有关如何将聊天双方相互连接的信息,然后我们可以使用 ICE 框架在双方之间建立一个对等连接,通过该连接流式传输视频和音频数据,以显示在远程方的浏览器窗口中。

A978-1-4302-6269-5_12_Fig3_HTML.jpg

图 12-3。

Example video call using our simple chat client

我们将代码分成三个文件,第一个是包含两个 HTML5 <video>标签的 HTML 页面,一个向用户显示本地视频,另一个显示远程方的视频——音频也通过这个标签输出。该页面还包含 Start Call 和 Join Call 按钮,前者将随机生成一个聊天室名称,然后可以传递给远程方,后者允许用户输入一个聊天室名称来加入,通过使用相同的聊天室名称来连接双方。第二个和第三个文件是 JavaScript 文件,前者用于配置一个可重用的“类”,用于创建支持视频聊天所必需的代码,后者是一个特定的使用示例,根据 HTML 网页中显示的当前应用本身的需求进行配置。

我们首先需要建立一个 web 服务器,因为getUserMedia() API 只能在运行在httphttp协议上的 HTML 页面的上下文中工作。有许多产品可供使用;然而,最简单的方法可能是从 http:// httpd 下载、安装和配置 Apache。阿帕奇。org

一旦您在本地或使用托管的在线解决方案启动并运行了 web 服务器,您将使用 HTML5 <video>标签创建一个 HTML 页面,在该页面中将呈现来自远程用户的视频和音频。清单 12-4 显示了这样一个 HTML 页面的例子。

清单 12-4。一个简单的 HTML 页面,显示从远程方捕获的视频和音频

<!DOCTYPE html>

<html>

<head>

<title>In-Browser Video Chat</title>

<meta charset="utf-8">

<style>

body {

font-family: Helvetica, Arial, sans-serif;

}

.videos {

position: relative;

}

.video {

display: block;

position: absolute;

top: 0;

left: 0;

}

.local-video {

z-index: 1;

border: 1px solid black;

}

</style>

</head>

<body>

<h1>In-Browser Video Chat</h1>

<!-- Display the video chat room name at the top of the page -->

<p id="room-name"></p>

<!-- Create buttons which start a new video call or join an existing video call -->

<button id="start-call">Start Call</button>

<button id="join-call">Join Call</button>

<!-- Display the local and remote video streams, with the former displayed smaller

and layered above to the top left corner of the other -->

<div class="videos">

<video class="video local-video" id="local-video" width="200" autoplay muted></video>

<video class="video" id="remote-video" width="600" autoplay></video>

</div>

<!-- Load the script to enable Firebase support within this application -->

<script src="https://cdn.firebase.com/v0/firebase.js"></scriptT2】

<!-- Load the VideoChat "class" definition -->

<script src="Listing12-5.js"></script>

<!-- Load the code to instantiate the VideoChat "class" and connect it to this page -->

<script src="Listing12-6.js"></script>

</body>

</html>

清单 12-5 中的代码显示了如何创建 VideoChat“类”,它创建了必要的代码来处理本地浏览器和远程浏览器之间的通信,在两者之间传输视频和音频。

清单 12-5。支持浏览器内视频聊天的视频聊天“类”

// Define a "class" that can be used to create a peer-to-peer video chat in the browser. We

// have a dependency on Firebase, whose client API script should be loaded in the browser

// before this script executes

var VideoChat = (function(Firebase) {

// Polyfill the required browser features to support video chat as some are still using

// prefixed method and "class" names

// The PeerConnection "class" allows the configuration of a peer to peer connection

// between the current web page running on this device and the same running on another,

// allowing the addition of data streams to be passed from one to another, allowing for

// video chat-style appliations to be built

var PeerConnection = window.mozRTCPeerConnection || window.webkitRTCPeerConnection,

// The RTCSessionDescription "class" works together with the RTCPeerConnection to

// initialize the peer to peer data stream using the Session Description Protocol (SDP)

SessionDescription = window.mozRTCSessionDescription || window.RTCSessionDescription,

// The IceCandidate "class" allows instances of peer to peer "candidates" to be created

//  - a candidate provides the details of the connection directly to our calling

// partner, allowing the two browsers to chat

IceCandidate = window.mozRTCIceCandidate || window.RTCIceCandidate,

// Define the two types of participant in a call, the person who initiated it and the

// person who responded

_participantType = {

INITIATOR: "initiator",

RESPONDER: "responder"

},

// Define an object containing the settings we will use to create our PeerConnection

// object, allowing the two participants in the chat to locate each other's IP

// addresses over the internet

_peerConnectionSettings = {

// Define the set of ICE servers to use to attempt to locate the IP addresses

// of each of the devices participating in the chat. For best chances of

// success, we include at least one server supporting the two different

// protocols, STUN and TURN, which provide this IP lookup mechanism

server: {

iceServers: [{

// Mozilla's public STUN server

url: "stun:23.21.150.121"

}, {

// Google's public STUN server

url: "stun:stun.l.google.com:19302"

}, {

// Create your own TURN server athttp://numb.viagenie.caT3】

url: "turn:numb.viagenie.ca",

username: "denodell%40gmail.com",

credential: "password"

}]

},

// For interoperability between different browser manufacturers' code, we set

// this DTLS/SRTP property to "true" as it is "true" by default in Firefox

options: {

optional: [{

DtlsSrtpKeyAgreement: true

}]

}

};

// Polyfill the getUserMedia() method, which allows us to access a stream of data provided

// by the user's webcam and/or microphone

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

// If the current browser does not support the "classes" and methods required for video

// chat, throw an error - at the time of writing, Google Chrome, Mozilla Firefox and

// Opera are the only browsers supporting the features required to support video chat

if (!navigator.getUserMedia && !window.RTCPeerConnection) {

throw new Error("Your browser does not support video chat");

}

// Define a generic error handler function which will throw an error in the browser

function onError(error) {

throw new Error(error);

}

// Define the VideoChat "class" to use to create a new video chat on a web page

function VideoChat(options) {

options = options || {};

// Allow two callback functions, onLocalStream() and onRemoteStream() to be passed in.

// The former is executed once a connection has been made to the local webcam and

// microphone, and the latter is executed once a connection has been made to the remote

// user's webcam and microphone. Both pass along a stream URL which can be used to

// display the contents of the stream inside a <video> tag within the HTML page

if (typeof options.onLocalStream === "function") {

this.onLocalStream = options.onLocalStream;

}

if (typeof options.onRemoteStream === "function") {

this.onRemoteStream = options.onRemoteStream;

}

// Initialize Firebase data storage using the provided URL

this.initializeDatabase(options.firebaseUrl || "");

// Set up the peer-to-peer connection for streaming video and audio between two devices

this.setupPeerConnection();

}

VideoChat.prototype = {

// Define the participant type for the current user in this chat - the "initiator", the

// one starting the call

participantType: _participantType.INITIATOR,

// Define the participant type for the remote user in this chat - the "responder", the

// one responding to a request for a call

remoteParticipantType: _participantType.RESPONDER,

// Create a property to store the name for the chat room in which the video call will

// take place - defined later

chatRoomName: "",

// Define a property to allow loading and saving of data to the Firebase database

database: null,

// Define a method to be called when a local data stream has been initiated

onLocalStream: function() {},

// Define a method to be called when a remote data stream has been connected

onRemoteStream: function() {},

// Define a method to initialize the Firebase database

initializeDatabase: function(firebaseUrl) {

// Connect to our Firebase database using the provided URL

var firebase = new Firebase(firebaseUrl);

// Define and store the data object to hold all the details of our chat room

// connections

this.database = firebase.child("chatRooms");

},

// Define a method to save a given name-value pair to Firebase, stored against the

// chat room name given for this call

saveData: function(chatRoomName, name, value) {

if (this.database) {

this.database.child(chatRoomName).child(name).set(value);

}

},

// Define a method to load stored data from Firebase by its name and chat room name,

// executing a callback function when that data is found - the connection will wait

// until that data is found, even if it is generated at a later time

loadData: function(chatRoomName, name, callback) {

if (this.database) {

// Make a request for the data asynchronously and execute a callback function once

// the data has been located

this.database.child(chatRoomName).child(name).on("value", function(data) {

// Extract the value we're after from the response

var value = data.val();

// If a callback function was provided to this method, execute it, passing

// along the located value

if (value && typeof callback === "function") {

callback(value);

}

});

}

},

// Define a method to set up a peer-to-peer connection between two devices and stream

// data between the two

setupPeerConnection: function() {

var that = this;

// Create a PeerConnection instance using the STUN and TURN servers defined

// earlier to establish a peer-to-peer connection even across firewalls and NAT

this.peerConnection = new PeerConnection(_peerConnectionSettings.server,         _peerConnectionSettings.options);

// When a remote stream has been added to the peer-to-peer connection, get the

// URL of the stream and pass this to the onRemoteStream() method to allow the

// remote video and audio to be presented within the page inside a <video> tag

this.peerConnection.onaddstream = function(event) {

// Get a URL that represents the stream object

var streamURL = window.URL.createObjectURL(event.stream);

// Pass this URL to the onRemoteStream() method, passed in on instantiation

// of this VideoChat instance

that.onRemoteStream(streamURL);

};

// Define a function to execute when the ICE framework finds a suitable candidate

// for allowing a peer-to-peer data connection

this.peerConnection.onicecandidate = function(event) {

if (event.candidate) {

// Google Chrome often finds multiple candidates, so let's ensure we only

// ever get the first it supplies by removing the event handler once a

// candidate has been found

that.peerConnection.onicecandidate = null;

// Read out the remote party's ICE candidate connection details

that.loadData(that.chatRoomName, "candidate:" + that.remoteParticipantType, function(candidate) {

// Connect the remote party's ICE candidate to this connection forming

// the peer-to-peer connection

that.peerConnection.addIceCandidate(new IceCandidate(JSON.parse(candidate)));

});

// Save our ICE candidate connection details for connection by the remote

// party

that.saveData(that.chatRoomName, "candidate:" + that.participantType, JSON.stringify(event.candidate));

}

};

},

// Define a method to get the local device's webcam and microphone stream and handle

// the handshake between the local device and the remote party's device to set up the

// video chat call

call: function() {

var that = this,

// Set the constraints on our peer-to-peer chat connection. We want to be

// able to support both audio and video so we set the appropriate properties

_constraints = {

mandatory: {

OfferToReceiveAudio: true,

OfferToReceiveVideo: true

}

};

// Get the local device's webcam and microphone stream - prompts the user to

// authorize the use of these

navigator.getUserMedia({

video: true,

audio: true

}, function(stream) {

// Add the local video and audio data stream to the peer connection, making

// it available to the remote party connected to that same peer-to-peer

// connection

that.peerConnection.addStream(stream);

// Execute the onLocalStream() method, passing the URL of the local stream,

// allowing the webcam and microphone data to be presented to the local

// user within a <video> tag on the current HTML page

that.onLocalStream(window.URL.createObjectURL(stream));

// If we are the initiator of the call, we create an offer to any connected

// peer to join our video chat

if (that.participantType === _participantType.INITIATOR) {

// Create an offer of a video call in this chat room and wait for an

// answer from any connected peers

that.peerConnection.createOffer(function(offer) {

// Store the generated local offer in the peer connection object

that.peerConnection.setLocalDescription(offer);

// Save the offer details for connected peers to access

that.saveData(that.chatRoomName, "offer", JSON.stringify(offer));

// If a connected peer responds with an "answer" to our offer, store

// their details in the peer connection object, opening the channels

// of communication between the two

that.loadData(that.chatRoomName, "answer", function(answer) {

that.peerConnection.setRemoteDescription(

new SessionDescription(JSON.parse(answer))

);

});

}, onError, _constraints);

// If we are the one joining an existing call, we answer an offer to set up

// a peer-to-peer connection

} else {

// Load an offer provided by the other party - waits until an offer is

// provided if one is not immediately present

that.loadData(that.chatRoomName, "offer", function(offer) {

// Store the offer details of the remote party, using the supplied

// data

that.peerConnection.setRemoteDescription(

new SessionDescription(JSON.parse(offer))

);

// Generate an "answer" in response to the offer, enabling the

// two-way peer-to-peer connection we need for the video chat call

that.peerConnection.createAnswer(function(answer) {

// Store the generated answer as the local connection details

that.peerConnection.setLocalDescription(answer);

// Save the answer details, making them available to the initiating

// party, opening the channels of communication between the two

that.saveData(that.chatRoomName, "answer", JSON.stringify(answer));

}, onError, _constraints);

});

}

}, onError);

},

// Define a method which initiates a video chat call, returning the generated chat

// room name which can then be given to the remote user to use to connect to

startCall: function() {

// Generate a random 3-digit number with padded zeros

var randomNumber = Math.round(Math.random() * 999);

if (randomNumber < 10) {

randomNumber = "00" + randomNumber;

} else if (randomNumber < 100) {

randomNumber = "0" + randomNumber;

}

// Create a simple chat room name based on the generated random number

this.chatRoomName = "room-" + randomNumber;

// Execute the call() method to start transmitting and receiving video and audio

// using this chat room name

this.call();

// Return the generated chat room name so it can be provided to the remote party

// for connection

return this.chatRoomName;

},

// Define a method to join an existing video chat call using a specific room name

joinCall: function(chatRoomName) {

// Store the provided chat room name

this.chatRoomName = chatRoomName;

// If we are joining an existing call, we must be the responder, rather than

// initiator, so update the properties accordingly to reflect this

this.participantType = _participantType.RESPONDER;

this.remoteParticipantType = _participantType.INITIATOR;

// Execute the call() method to start transmitting and receiving video and audio

// using the provided chat room name

this.call();

}

};

// Return the VideoChat "class" for use throughout the rest of the code

return VideoChat;

}(Firebase));

清单 12-5 中的代码可以如清单 12-6 所示在浏览器中创建一个简单的视频聊天应用,与清单 12-4 中创建的 HTML 页面一起工作。

清单 12-6。使用视频聊天“类”创建浏览器内视频聊天

// Get a reference to the <video id="local-video"> element on the page

var localVideoElement = document.getElementById("local-video"),

// Get a reference to the <video id="remote-video"> element on the page

remoteVideoElement = document.getElementById("remote-video"),

// Get a reference to the <button id="start-call"> element on the page

startCallButton = document.getElementById("start-call"),

// Get a reference to the <button id="join-call"> element on the page

joinCallButton = document.getElementById("join-call"),

// Get a reference to the <p id="room-name"> element on the page

roomNameElement = document.getElementById("room-name"),

// Create an instance of the Video Chat "class"

videoChat = new VideoChat({

// The Firebase database URL for use when loading and saving data to the cloud - create

// your own personal URL athttp://firebase.comT3】

firebaseUrl: "https://glaring-fire-9593.firebaseio.com/T2】

// When the local webcam and microphone stream is running, set the "src" attribute

// of the <div id="local-video"> element to display the stream on the page

onLocalStream: function(streamSrc) {

localVideoElement.src = streamSrc;

},

// When the remote webcam and microphone stream is running, set the "src" attribute

// of the <div id="remote-video"> element to display the stream on the page

onRemoteStream: function(streamSrc) {

remoteVideoElement.src = streamSrc;

}

});

// When the <button id="start-call"> button is clicked, start a new video call and

// display the generated room name on the page for providing to the remote user

startCallButton.addEventListener("click", function() {

// Start the call and get the chat room name

var roomName = videoChat.startCall();

// Display the chat room name on the page

roomNameElement.innerHTML = "Created call with room name: " + roomName;

}, false);

// When the <button id="join-call"> button is clicked, join an existing call by

// entering the room name to join at the prompt

joinCallButton.addEventListener("click", function() {

// Ask the user for the chat room name to join

var roomName = prompt("What is the name of the chat room you would like to join?");

// Join the chat by the provided room name - as long as this room name matches the

// other, the two will be connected over a peer-to-peer connection and video streaming

// will take place between the two

videoChat.joinCall(roomName);

// Display the room name on the page

roomNameElement.innerHTML = "Joined call with room name: " + roomName;

}, false);

因此,我们创建了一个简单而实用的浏览器内视频聊天客户端。你可以进一步扩展这一想法,用特定的登录用户 id 取代聊天室名称的概念,通过一个列出你的“朋友”的界面(如 Skype 等提供的界面)将用户相互连接,但增加的好处是在浏览器内运行,而不需要下载任何特殊的应用或插件来支持这一点。

随着浏览器对本章所涉及的 API 的支持的提高,可以构建的应用的可能性也会提高。通过http://bit . ly/can use _ WebRTC了解当前浏览器对 WebRTC 的支持水平,并在浏览器中尝试自己的点对点聊天想法。

摘要

在本教程中,我解释了什么是 WebRTC,并演示了如何通过这个 API 从用户的机器上访问网络摄像头和麦克风。然后,我解释了流、对等连接和信令信道的基础知识,当它们一起使用时,有助于支持在浏览器中构建简单的视频聊天客户端。

在下一章中,我将介绍 JavaScript 中的 HTML 模板,以及它在基于 web 的应用中简化服务器返回的数据量的潜力。