WebRTC - 快速指南
WebRTC - 概述
随着WebRTC(Web 实时通信)的出现,Web 对实时通信不再陌生。尽管它于 2011 年 5 月发布,但它仍在发展,其标准也在变化。一组协议由 IETF(互联网工程任务组)的 Web 浏览器实时通信工作组 (http://tools.ietf.org/wg/rtcweb/) 标准化,而新的API集由W3C(万维网联盟)的Web 实时通信工作组,网址为http://www.w3.org/2011/04/webrtc/。随着 WebRTC 的出现,现代 Web 应用程序可以轻松地将音频和视频内容传输给数百万人。
基本方案
WebRTC 允许您快速轻松地建立与其他网络浏览器的点对点连接。要从头开始构建这样的应用程序,您需要大量的框架和库来处理数据丢失、连接断开和 NAT 遍历等典型问题。借助 WebRTC,所有这些都开箱即用地内置到浏览器中。该技术不需要任何插件或第三方软件。它是开源的,其源代码可在http://www.webrtc.org/ 上免费获取。
WebRTC API包括媒体捕获、音频和视频编码和解码、传输层和会话管理。
媒体捕捉
第一步是访问用户设备的摄像头和麦克风。我们检测可用设备的类型,获取用户访问这些设备的权限并管理流。
音频和视频编码和解码
通过互联网发送音频和视频数据流并不是一件容易的事。这就是使用编码和解码的地方。这是将视频帧和音频波分割成更小的块并压缩它们的过程。这种算法称为编解码器。有大量不同的编解码器,由具有不同业务目标的不同公司维护。WebRTC 内部还有许多编解码器,如 H.264、iSAC、Opus 和 VP8。当两个浏览器连接在一起时,它们会选择两个用户之间支持的最佳编解码器。幸运的是,WebRTC 在幕后完成了大部分编码工作。
传输层
传输层管理数据包的顺序、处理数据包丢失以及连接到其他用户。WebRTC API 再次使我们能够轻松访问事件,这些事件会告诉我们连接何时出现问题。
会话管理
会话管理涉及管理、打开和组织连接。这通常称为信令。如果您将音频和视频流传输给用户,那么传输附属数据也是有意义的。这是由RTCDataChannel API完成的。
来自 Google、Mozilla、Opera 等公司的工程师在将这种实时体验带入网络方面做了出色的工作。
浏览器兼容性
WebRTC 标准是网络上发展最快的标准之一,因此这并不意味着每个浏览器同时支持所有相同的功能。要检查您的浏览器是否支持WebRTC,您可以访问http://caniuse.com/#feat=rtcpeerconnection。在所有教程中,我建议您使用 Chrome 来查看所有示例。
尝试 WebRTC
现在让我们开始使用 WebRTC。将浏览器导航至演示站点:https://apprtc.appspot.com/
单击“加入”按钮。您应该会看到一个下拉通知。
单击“允许”按钮开始将视频和音频流式传输到网页。您应该会看到自己的视频流。
现在在新的浏览器选项卡中打开您当前所在的 URL,然后单击“加入”。您应该看到两个视频流 - 一个来自您的第一个客户,另一个来自第二个客户。
现在您应该明白为什么 WebRTC 是一个强大的工具了。
用例
实时网络打开了通往全新应用范围的大门,包括基于文本的聊天、屏幕和文件共享、游戏、视频聊天等等。除了通信之外,您还可以将 WebRTC 用于其他目的,例如 -
- 实时营销
- 实时广告
- 后台通信(CRM、ERP、SCM、FFM)
- 人力资源管理
- 社交网络
- 约会服务
- 在线医疗咨询
- 金融服务
- 监视
- 多人游戏
- 现场直播
- 电子学习
概括
现在您应该对WebRTC这个术语有了清晰的了解。您还应该了解可以使用 WebRTC 构建哪些类型的应用程序,因为您已经在浏览器中尝试过了。综上所述,WebRTC是一项相当有用的技术。
WebRTC - 架构
整个 WebRTC 架构非常复杂。
在这里你可以找到三个不同的层 -
Web 开发人员的 API - 该层包含 Web 开发人员所需的所有 API,包括 RTCPeerConnection、RTCDataChannel 和 MediaStrean 对象。
浏览器制造商的API
可重写的 API,浏览器制造商可以挂钩。
传输组件允许跨各种类型的网络建立连接,而语音和视频引擎是负责将音频和视频流从声卡和摄像头传输到网络的框架。对于Web开发者来说,最重要的部分是WebRTC API。
如果我们从客户端-服务器端看 WebRTC 架构,我们可以看到最常用的模型之一是受到 SIP(会话启动协议)梯形的启发。
在此模型中,两个设备都运行来自不同服务器的 Web 应用程序。RTCPeerConnection 对象配置流,以便它们可以点对点地相互连接。此信号通过 HTTP 或 WebSocket 完成。
但最常用的模型是三角形 -
在此模型中,两个设备使用相同的 Web 应用程序。它为 Web 开发人员在管理用户连接时提供了更大的灵活性。
WebRTC API
它由一些主要的 javascript 对象组成 -
- RTCP对等连接
- 媒体流
- RTC数据通道
RTCPeerConnection 对象
该对象是 WebRTC API 的主要入口点。它帮助我们连接到对等点、初始化连接并附加媒体流。它还管理与另一个用户的 UDP 连接。
RTCPeerConnection 对象的主要任务是设置和创建对等连接。我们可以轻松地挂钩连接的关键点,因为该对象在出现一组事件时会触发它们。这些事件使您可以访问我们的连接配置 -
RTCPeerConnection 是一个简单的 javascript 对象,您可以这样简单地创建它 -
[code] var conn = new RTCPeerConnection(conf); conn.onaddstream = function(stream) { // use stream here }; [/code]
RTCPeerConnection 对象接受一个conf参数,我们将在这些教程的后面部分介绍该参数。当远程用户将视频或音频流添加到其对等连接时,会触发onaddstream事件。
媒体流API
现代浏览器允许开发人员访问getUserMedia API(也称为MediaStream API)。功能有三个关键点 -
它使开发人员可以访问表示视频和音频流的流对象
如果用户的设备上有多个摄像头或麦克风,它可以管理输入用户设备的选择
它提供了一个安全级别,随时询问用户想要获取流的情况
为了测试这个 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] //checks if the browser supports 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; //get both video and audio streams from user's camera navigator.getUserMedia({ video: true, audio: true }, function (stream) { var video = document.querySelector('video'); //insert stream into the video tag 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(); //establishing peer connection //... //end of establishing peer connection var dataChannel = peerConnection.createDataChannel("myChannel", dataChannelOptions); // here we can start sending direct messages to another peer [/code]
这就是您所需要的,只需两行代码。其他一切都在浏览器的内部层完成。您可以在任何对等连接上创建通道,直到RTCPeerConnection 对象关闭。
概括
您现在应该已经牢牢掌握了 WebRTC 架构。我们还介绍了 MediaStream、RTCPeerConnection 和 RTCDataChannel API。WebRTC API 是一个不断变化的目标,因此请始终跟上最新规范。
WebRTC - 环境
在开始构建 WebRTC 应用程序之前,我们应该设置编码环境。首先,您应该有一个文本编辑器或 IDE,可以在其中编辑 HTML 和 Javascript。当您阅读本教程时,您很可能已经选择了首选。至于我,我正在使用 WebStorm IDE。您可以在https://www.jetbrains.com/webstorm/下载其试用版。我还使用 Linux Mint 作为我选择的操作系统。
常见 WebRTC 应用程序的另一个要求是拥有一个服务器来托管 HTML 和 Javascript 文件。仅通过双击文件该代码将无法工作,因为除非文件由实际服务器提供服务,否则不允许浏览器连接到摄像头和麦克风。这样做显然是出于安全问题。
有大量不同的 Web 服务器,但在本教程中,我们将使用 Node.js 和 node-static -
访问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
现在应该可以从命令行使用节点命令。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.js 的 WebSockets 库。要在终端中运行npm install ws进行安装。
为了测试我们的信令服务器,我们将使用 wscat 实用程序。要安装它,请在终端窗口中运行npm install -g wscat 。
序列号 | 协议和描述 |
---|---|
1 | WebRTC 协议
WebRTC 应用程序使用 UDP(用户数据报协议)作为传输协议。当今大多数 Web 应用程序都是使用 TCP(传输控制协议)构建的 |
2 | 会话描述协议
SDP是WebRTC的重要组成部分。它是一种旨在描述媒体通信会话的协议。 |
3 | 寻找路线
为了连接到其他用户,您应该在自己的网络和其他用户的网络之间找到一条清晰的路径。但您使用的网络可能有多个级别的访问控制,以避免安全问题。 |
4 | 流控制传输协议
通过对等连接,我们能够快速发送视频和音频数据。今天,当使用 RTCDataChannel 对象时,SCTP 协议用于在我们当前设置的对等连接之上发送 blob 数据。 |
概括
在本章中,我们介绍了几种支持对等连接的技术,例如 UDP、TCP、STUN、TURN、ICE 和 SCTP。您现在应该对 SDP 的工作原理及其用例有一个初步的了解。
WebRTC - 媒体流 API
MediaStream API 旨在轻松访问本地摄像头和麦克风的媒体流。getUserMedia ()方法是访问本地输入设备的主要方法。
该 API 有几个关键点 -
实时媒体流由视频或音频形式的流对象表示
它通过在 Web 应用程序开始获取流之前询问用户的用户权限来提供安全级别
输入设备的选择由 MediaStream API 处理(例如,当有两个摄像头或麦克风连接到设备时)
每个 MediaStream 对象都包含多个 MediaStreamTrack 对象。它们代表来自不同输入设备的视频和音频。
每个 MediaStreamTrack 对象可以包括多个通道(右音频通道和左音频通道)。这些是 MediaStream API 定义的最小部分。
输出 MediaStream 对象有两种方法。首先,我们可以将输出渲染为视频或音频元素。其次,我们可以将输出发送到 RTCPeerConnection 对象,然后该对象将其发送到远程对等点。
使用媒体流 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() { //check if the browser supports the WebRTC return !!(navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia); } if (hasUserMedia()) { navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia; //enabling video and audio channels navigator.getUserMedia({ video: true, audio: true }, function (stream) { var video = document.querySelector('video'); //inserting our stream to the video tag video.src = window.URL.createObjectURL(stream); }, function (err) {}); } else { alert("WebRTC is not supported"); }
这里我们创建了hasUserMedia()函数来检查是否支持 WebRTC。然后我们访问getUserMedia函数,其中第二个参数是接受来自用户设备的流的回调。然后我们使用window.URL.createObjectURL将流加载到视频元素中,这会创建一个代表参数中给定对象的 URL。
现在刷新您的页面,单击“允许”,您应该在屏幕上看到您的脸。
请记住使用 Web 服务器运行所有脚本。我们已经在WebRTC环境教程中安装了一个。
媒体流API
特性
MediaStream.active(只读) - 如果 MediaStream 处于活动状态,则返回 true,否则返回 false。
MediaStream.ished(只读,已弃用) - 如果已在对象上触发结束事件,则返回 true,这意味着流已被完全读取,如果尚未到达流的末尾,则返回 false。
MediaStream.id(只读) - 对象的唯一标识符。
MediaStream.label(只读,已弃用) - 由用户代理分配的唯一标识符。
您可以在我的浏览器中看到上述属性的外观 -
事件处理程序
MediaStream.onactive -当 MediaStream 对象变为活动状态时触发的活动事件的处理程序。
MediaStream.onaddtrack -添加新MediaStreamTrack对象时触发的addtrack事件的处理程序。
MediaStream.onished(已弃用) -流终止时触发的已结束事件的处理程序。
MediaStream.oninactive - 当MediaStream对象变为非活动状态时触发的非活动事件的处理程序。
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() { //check if the browser supports the WebRTC return !!(navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia); } if (hasUserMedia()) { navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia; //enabling video and audio channels navigator.getUserMedia({ video: true, audio: true }, function (s) { stream = s; var video = document.querySelector('video'); //inserting our stream to the video tag 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() - 音频按钮。现在应该删除音轨。然后对视频轨道执行相同的操作。
如果单击getTracks()按钮,您应该会看到所有MediaStreamTrack(所有连接的视频和音频输入)。然后点击getTrackById()获取音频 MediaStreamTrack。
概括
在本章中,我们使用 MediaStream API 创建了一个简单的 WebRTC 应用程序。现在您应该对使 WebRTC 工作的各种 MediaStream API 有一个清晰的概述。
WebRTC - RTCPeerConnection API
RTCPeerConnection API 是每个浏览器之间点对点连接的核心。要创建 RTCPeerConnection 对象,只需编写
var pc = RTCPeerConnection(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 事件。可能的值 -
新- ICE 代理正在等待远程候选人或收集地址
检查- ICE 代理有远程候选者,但尚未找到连接
已连接- ICE 代理已找到可用的连接,但仍在检查更远程的候选连接以获得更好的连接。
已完成- ICE 代理已找到可用的连接并停止测试远程候选者。
失败- ICE 代理已检查所有远程候选者,但未找到至少一个组件的匹配项。
断开连接- 至少一个组件不再存在。
关闭- ICE 代理关闭。
RTCPeerConnection.iceGatheringState(只读) - 返回一个 RTCIceGatheringState 枚举,描述连接的 ICE 收集状态 -
new - 对象刚刚创建。
收集- ICE 代理人正在收集候选人
完成ICE 代理已完成收集。
RTCPeerConnection.localDescription(只读) - 返回描述本地会话的 RTCSessionDescription。如果尚未设置,则可以为 null。
RTCPeerConnection.peerIdentity(只读) - 返回 RTCIdentityAssertion。它由 idp(域名)和代表远程对等点身份的名称组成。
RTCPeerConnection.remoteDescription(只读) - 返回描述远程会话的 RTCSessionDescription。如果尚未设置,则可以为 null。
RTCPeerConnection.signalingState(只读) - 返回描述本地连接的信令状态的 RTCSignalingState 枚举。该状态描述了 SDP 提议。当该值更改时,将触发signalingstatechange 事件。可能的值 -
稳定- 初始状态。没有正在进行的 SDP 提议/应答交换。
have-local-offer - 连接的本地端已在本地应用 SDP Offer。
have-remote-offer - 连接的远程端已在本地应用 SDP Offer。
have-local-pranswer - 已应用远程 SDP Offer,并在本地应用 SDP pranswer。
have-remote-pranswer - 已应用本地 SDP,并远程应用 SDP pranswer。
已关闭- 连接已关闭。
事件处理程序
编号 | 事件处理程序和描述 |
---|---|
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 从此连接中删除时发送此事件。 |
方法
编号 | 方法与说明 |
---|---|
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() 创建一个与特定 MediaStreamTrack 关联的新 RTCDTMFSender。允许通过连接发送 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; //when a user clicks the login button loginBtn.addEventListener("click", function(event){ name = loginInput.value; if(name.length > 0){ send({ type: "login", name: name }); } }); //handle messages from the server 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; } }; //when a user logs in function onLogin(success) { if (success === false) { alert("oops...try a different username"); } else { //creating our RTCPeerConnection object 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); }; // Alias for sending messages in JSON format function send(message) { if (connectedUser) { message.name = connectedUser; } connection.send(JSON.stringify(message)); };
您可以看到我们与信令服务器建立了套接字连接。当用户单击登录按钮时,应用程序将其用户名发送到服务器。如果登录成功,应用程序将创建 RTCPeerConnection 对象并设置 onicecandidate 处理程序,该处理程序将所有找到的icecandidate 发送到其他对等方。现在打开页面并尝试登录。您应该看到以下控制台输出 -
下一步是向其他同行创建报价。将以下代码添加到您的client.js文件中 -
//setup a peer connection with another user 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."); }); } }); //when somebody wants to call us 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"); }); } //when another user answers to our offer 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 提议。我们还设置了onAnswer和onCandidate处理程序。重新加载您的页面,在两个选项卡中打开它,使用两个用户登录并尝试在他们之间建立连接。您应该看到以下控制台输出 -
现在点对点连接已建立。在接下来的教程中,我们将添加视频和音频流以及文本聊天支持。
WebRTC - RTCDataChannel API
WebRTC 不仅擅长传输音频和视频流,还擅长传输我们可能拥有的任何任意数据。这就是 RTCDataChannel 对象发挥作用的地方。
RTC数据通道API
特性
RTCDataChannel.label(只读) - 返回包含数据通道名称的字符串。
RTCDataChannel.ordered(只读) - 如果消息的传递顺序得到保证,则返回 true;如果不能保证,则返回 false。
RTCDataChannel.protocol(只读) - 返回包含用于该通道的子协议名称的字符串。
RTCDataChannel.id(只读) - 返回在创建 RTCDataChannel 对象时设置的通道的唯一 ID。
RTCDataChannel.readyState(只读) - 返回表示连接状态的 RTCDataChannelState 枚举。可能的值 -
连接- 表示连接尚未激活。这是初始状态。
open - 表示连接正在运行。
关闭- 表示连接正在关闭过程中。缓存的消息正在发送或接收,但没有新创建的任务正在接受。
close - 表示连接无法建立或已关闭。
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 - 当消息事件被触发时调用此事件处理程序。当数据通道上有消息可用时发送该事件。
RTCDataChannel.onbufferedamountlow - 当 bufferedamoutlow 事件被触发时,调用此事件处理程序。当 RTCDataChannel.bufferedAmount 减小到 RTCDataChannel.bufferedAmountLowThreshold 属性以下时,发送此事件。
RTCDataChannel.onclose - 触发关闭事件时调用此事件处理程序。当数据连接关闭时发送此事件。
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; //when a user clicks the login button loginBtn.addEventListener("click", function(event) { name = loginInput.value; if(name.length > 0) { send({ type: "login", name: name }); } }); //handle messages from the server 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; } }; //when a user logs in function onLogin(success) { if (success === false) { alert("oops...try a different username"); } else { //creating our RTCPeerConnection object 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); }; // Alias for sending messages in JSON format function send(message) { if (connectedUser) { message.name = connectedUser; } connection.send(JSON.stringify(message)); };
您可以看到我们与信令服务器建立了套接字连接。当用户单击登录按钮时,应用程序将其用户名发送到服务器。如果登录成功,应用程序将创建RTCPeerConnection对象并设置onicecandidate处理程序,该处理程序将所有找到的icecandidate 发送到其他对等点。它还运行 openDataChannel() 函数来创建数据通道。请注意,在创建 RTCPeerConnection 对象时,如果您使用的是 Chrome 或 Opera,则构造函数中的第二个参数可选:[{RtpDataChannels: true}] 是必需的。下一步是向其他同行创建报价。将以下代码添加到您的client.js文件中 -
//setup a peer connection with another user 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."); }); } }); //when somebody wants to call us 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"); }); } //when another user answers to our offer 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 提议。我们还设置了onAnswer和onCandidate处理程序。最后,让我们实现openDataChannel()函数来创建我们的 dataChannel。将以下代码添加到您的client.js文件中 -
//creating data channel 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); }; } //when a user clicks the send message button sendMsgBtn.addEventListener("click", function (event) { console.log("send message"); var val = msgInput.value; dataChannel.send(val); });
在这里,我们为连接创建数据通道,并为“发送消息”按钮添加事件处理程序。现在在两个选项卡中打开此页面,使用两个用户登录,建立连接,然后尝试发送消息。您应该在控制台输出中看到它们。请注意,上面的示例是在 Opera 中测试的。
现在您可能会发现 RTCDataChannel 是 WebRTC API 中极其强大的部分。该对象还有很多其他用例,例如点对点游戏或基于 torrent 的文件共享。
WebRTC - 信令
大多数 WebRTC 应用程序不仅仅能够通过视频和音频进行通信。他们还需要许多其他功能。在本章中,我们将构建一个基本的信令服务器。
信令和协商
要连接到另一个用户,您应该知道他在网络上的位置。您设备的 IP 地址允许支持 Internet 的设备直接在彼此之间发送数据。RTCPeerConnection对象负责此操作。一旦设备知道如何通过互联网找到彼此,它们就会开始交换有关每个设备支持的协议和编解码器的数据。
要与其他用户通信,您只需交换联系信息,其余的将由 WebRTC 完成。连接到其他用户的过程也称为信令和协商。它由几个步骤组成 -
创建对等连接的潜在候选者列表。
用户或应用程序选择要与之建立连接的用户。
信令层通知另一个用户有人想要连接到他。他可以接受也可以拒绝。
第一个用户会收到接受要约的通知。
第一个用户发起与另一个用户的RTCPeerConnection。
双方用户通过信令服务器交换软硬件信息。
两个用户交换位置信息。
连接成功或失败。
WebRTC 规范不包含任何有关交换信息的标准。因此请记住,以上只是信号发送如何发生的一个示例。您可以使用您喜欢的任何协议或技术。
构建服务器
我们要构建的服务器将能够将不在同一台计算机上的两个用户连接在一起。我们将创建我们自己的信号机制。我们的信令服务器将允许一个用户呼叫另一个用户。一旦用户呼叫另一个用户,服务器就会在他们之间传递提议、应答、ICE 候选项并建立 WebRTC 连接。
上图是使用信令服务器时用户之间的消息传递流程。首先,每个用户向服务器注册。在我们的例子中,这将是一个简单的字符串用户名。用户注册后,就可以互相呼叫。用户 1 使用他希望呼叫的用户标识符提出报价。其他用户应该回答。最后,ICE 候选者会在用户之间发送,直到他们可以建立连接。
要创建 WebRTC 连接,客户端必须能够在不使用 WebRTC 对等连接的情况下传输消息。这是我们将使用 HTML5 WebSockets 的地方——两个端点(Web 服务器和 Web 浏览器)之间的双向套接字连接。现在让我们开始使用 WebSocket 库。创建server.js文件并插入以下代码 -
//require our websocket library var WebSocketServer = require('ws').Server; //creating a websocket server at port 9090 var wss = new WebSocketServer({port: 9090}); //when a user connects to our sever wss.on('connection', function(connection) { console.log("user connected"); //when server gets a message from a connected user connection.on('message', function(message){ console.log("Got message from a user:", message); }); connection.send("Hello from server"); });
第一行需要我们已经安装的 WebSocket 库。然后我们在端口 9090 上创建一个套接字服务器。接下来,我们监听连接事件。当用户与服务器建立 WebSocket 连接时,将执行此代码。然后我们监听用户发送的任何消息。最后,我们向连接的用户发送一条响应“Hello from server”。
现在运行节点服务器,服务器应该开始侦听套接字连接。
为了测试我们的服务器,我们将使用我们也已经安装的wscat实用程序。该工具有助于直接连接到 WebSocket 服务器并测试命令。在一个终端窗口中运行我们的服务器,然后打开另一个终端窗口并运行wscat -c ws://localhost:9090命令。您应该在客户端看到以下内容 -
服务器还应该记录连接的用户 -
用户注册
在我们的信令服务器中,我们将为每个连接使用基于字符串的用户名,以便我们知道向何处发送消息。让我们稍微改变一下我们的连接处理程序 -
connection.on('message', function(message) { var data; //accepting only JSON messages try { data = JSON.parse(message); } catch (e) { console.log("Invalid JSON"); data = {}; } });
这样我们只接受 JSON 消息。接下来,我们需要将所有连接的用户存储在某处。我们将使用一个简单的 Javascript 对象来实现它。更改文件的顶部 -
//require our websocket library var WebSocketServer = require('ws').Server; //creating a websocket server at port 9090 var wss = new WebSocketServer({port: 9090}); //all connected to the server users var users = {};
我们将为来自客户端的每条消息添加一个类型字段。例如,如果用户想要登录,他会发送登录类型消息。让我们定义它 -
connection.on('message', function(message){ var data; //accepting only JSON messages try { data = JSON.parse(message); } catch (e) { console.log("Invalid JSON"); data = {}; } //switching type of the user message switch (data.type) { //when a user tries to login case "login": console.log("User logged:", data.name); //if anyone is logged in with this username then refuse if(users[data.name]) { sendTo(connection, { type: "login", success: false }); } else { //save user connection on the server 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; } });
如果用户发送带有登录类型的消息,我们 -
检查是否有人已经使用该用户名登录
如果是,则告诉用户尚未成功登录
如果没有人使用此用户名,我们将用户名添加为连接对象的键。
如果有逗号