From 33058f2b292b3a581333bdfb21b8f671898c5060 Mon Sep 17 00:00:00 2001 From: Peter Bengtsson Date: Tue, 8 Dec 2020 14:40:17 -0500 Subject: initial commit --- .../zh-cn/web/api/webrtc_api/adapter.js/index.html | 40 ++ .../web/api/webrtc_api/architecture/index.html | 17 + .../web/api/webrtc_api/connectivity/index.html | 85 +++ files/zh-cn/web/api/webrtc_api/index.html | 131 ++++ files/zh-cn/web/api/webrtc_api/overview/index.html | 22 + .../zh-cn/web/api/webrtc_api/protocols/index.html | 75 +++ .../signaling_and_video_calling/index.html | 674 +++++++++++++++++++++ .../simple_rtcdatachannel_sample/index.html | 276 +++++++++ .../api/webrtc_api/taking_still_photos/index.html | 231 +++++++ .../web/api/webrtc_api/webrtc_basics/index.html | 262 ++++++++ 10 files changed, 1813 insertions(+) create mode 100644 files/zh-cn/web/api/webrtc_api/adapter.js/index.html create mode 100644 files/zh-cn/web/api/webrtc_api/architecture/index.html create mode 100644 files/zh-cn/web/api/webrtc_api/connectivity/index.html create mode 100644 files/zh-cn/web/api/webrtc_api/index.html create mode 100644 files/zh-cn/web/api/webrtc_api/overview/index.html create mode 100644 files/zh-cn/web/api/webrtc_api/protocols/index.html create mode 100644 files/zh-cn/web/api/webrtc_api/signaling_and_video_calling/index.html create mode 100644 files/zh-cn/web/api/webrtc_api/simple_rtcdatachannel_sample/index.html create mode 100644 files/zh-cn/web/api/webrtc_api/taking_still_photos/index.html create mode 100644 files/zh-cn/web/api/webrtc_api/webrtc_basics/index.html (limited to 'files/zh-cn/web/api/webrtc_api') diff --git a/files/zh-cn/web/api/webrtc_api/adapter.js/index.html b/files/zh-cn/web/api/webrtc_api/adapter.js/index.html new file mode 100644 index 0000000000..f09f219b51 --- /dev/null +++ b/files/zh-cn/web/api/webrtc_api/adapter.js/index.html @@ -0,0 +1,40 @@ +--- +title: Improving compatibility using WebRTC adapter.js +slug: Web/API/WebRTC_API/adapter.js +translation_of: Web/API/WebRTC_API/adapter.js +--- +

{{WebRTCSidebar}}

+ +

虽然 WebRTC 规范已经相对健全稳固了,但是并不是所有的浏览器都实现了它所有的功能。除此之外。有些浏览器需要在一些或者所有的 WebRTC API上添加前缀才能正常使用。尽管你可以自己写代码解决这种问题,但是还有一个比较简单的方法。WebRTC 组织在github上提供了一个 WebRTC适配器(WebRTC adapter)来解决在不同浏览器上实现 WebRTC 的兼容性问题。这个适配器是一个JavaScript垫片,它可以让你根据 WebRTC 规范描述的那样去写代码,在所有支持 WebRTC的浏览器中不用去写前缀或者其他兼容性解决方法。

+ +
+

注意: 由于WebRTC和支持的浏览器中的API的功能和命名在不断变动,推荐使用这个适配器。

+
+ +

这个 adapter(适配器)是基于BSD开源协议的。

+ +

adapter.js 是干什么的

+ +

对于每个支持 WebRTC 的浏览器的各个版本,adapter.js添加必要的polyfills(填充),使用没有前缀的API,以及使用一些修改让浏览器可以运行根据 WebRTC 规范写的代码。

+ +

举个例子,在火狐浏览器版本号38之前,adapter 增加了{{domxref("RTCPeerConnection.urls")}}属性;火狐浏览器并不原生的支持这个属性直到38版本,然而在谷歌浏览器中如果API不支持{{jsxref("Promise")}} 就添加支持。这只是一些例子;当然还有其他措施来实现这种统一API。

+ +

WebRTC adapter现在支持火狐、谷歌、和Edge浏览器

+ +

使用adapter.js

+ +

要使用 adapter.js,你需要在使用 WebRTC APIs 的每个页面都引入 adapter.js :

+ +
    +
  1. 从GitHub上下载一个最新adapter.js的副本。
  2. +
  3. 在你的网站文件目录里添加这个文件(比如在放在scripts目录下)。
  4. +
  5. 在你的项目里包含这个文件:<script src="adapter.js"></script>
  6. +
  7. 写代码,按照 WebRTC APIs 规范去写,知道你的代码应该在所有浏览器上工作。
  8. +
  9. 注意,即使有一个像这样优秀的 adapter 并不意味着你不需要在不同的浏览器上测试代码(以及在同一个浏览器中的不同版本)。
  10. +
+ +

相关链接

+ + diff --git a/files/zh-cn/web/api/webrtc_api/architecture/index.html b/files/zh-cn/web/api/webrtc_api/architecture/index.html new file mode 100644 index 0000000000..d1db1d4b69 --- /dev/null +++ b/files/zh-cn/web/api/webrtc_api/architecture/index.html @@ -0,0 +1,17 @@ +--- +title: WebRTC 架构概览 +slug: Web/API/WebRTC_API/Architecture +tags: + - WebRTC 架构概览 +translation_of: Web/API/WebRTC_API/Protocols +--- +

{{WebRTCSidebar}}

+ +

用来创建WebRTC连接的API底层使用了一系列的网络协议和连接标准。这篇文章涵盖了这些标准。

+ +

为了让WebRTC正常工作,需要的协议、标准和API比较繁多。因此对于初学者来说可能会比较难以理解。当你一旦上手,你会惊喜地发现原来这一切都是如此的优雅和简单易懂。至于你信不信,反正我是信了。

+ + + + +

重定向 WebRTC 协议介绍

diff --git a/files/zh-cn/web/api/webrtc_api/connectivity/index.html b/files/zh-cn/web/api/webrtc_api/connectivity/index.html new file mode 100644 index 0000000000..647cce7449 --- /dev/null +++ b/files/zh-cn/web/api/webrtc_api/connectivity/index.html @@ -0,0 +1,85 @@ +--- +title: WebRTC connectivity +slug: Web/API/WebRTC_API/Connectivity +tags: + - API + - Advanced + - WebRTC + - 媒体 + - 指南 + - 草案 + - 视频 + - 音频 +translation_of: Web/API/WebRTC_API/Connectivity +--- +

{{WebRTCSidebar}}{{draft}}

+ +

现在我们已经单独介绍了协议,我们可以将它们放在一起。 本文介绍了 WebRTC各种相关协议如何相互交互,以便在对等体之间创建连接和传输数据和/或媒体。

+ +
+

这个页面需要对结构完整性和内容完整性进行大量重写。这里有很多信息,但是组织混乱,现在这里跟个垃圾场一样。

+
+ +

什么是提议/应答和信号通道?

+ +

不幸的是,WebRTC中间无法创建没有某种服务器的连接。 我们称之为信号通道。 无论是通过电子邮件,明信片还是一只信鸽...,都可以通过任何通信方式交换信息,这取决于你。

+ +

我们需要交换的信息是提议和应答,其中仅包含下面提到的SDP。

+ +

将作为连接发起者的同伴A将创建一个提议。 然后他们将使用所选择的信号通道将此提议发送给对等体B. 对等体B将从信号通道接收提议并创建应答。 然后,它们将沿着信号通道发送回对等体A。

+ +

会话描述

+ +

WebRTC连接上的端点配置称为会话描述。 该描述包括关于要发送的媒体类型,其格式,正在使用的传输协议,端点的IP地址和端口以及描述媒体传输端点所需的其他信息的信息。 使用会话描述协议({{Glossary("SDP")}})来交换和存储该信息; 如果您想要有关SDP数据格式的详细信息,可以在{{RFC(2327)}}中找到。

+ +

当用户对另一个用户启动WebRTC调用时,将创建一个称为提议(offer)的特定描述。 该描述包括有关呼叫者建议的呼叫配置的所有信息。 接收者然后用应答(answer)进行响应,这是他们对呼叫结束的描述。 以这种方式,两个设备彼此共享以便交换媒体数据所需的信息。 该交换是使用交互式连接建立(ICE)({{Glossary("ICE")}}处理的,这是一种协议,即使两个设备通过网络地址转换({{Glossary( "NAT")}})。

+ +

然后,每个对等端保持两个描述:描述本身的本地描述和描述呼叫的远端的远程描述

+ +

在首次建立呼叫时,还可以在呼叫格式或其他配置需要更改的任何时候执行提议/应答过程。 无论是新呼叫还是重新配置现有的呼叫,这些都是交换提议和回答所必需的基本步骤,暂时忽略了ICE层:

+ +
    +
  1. 呼叫者通过 {{domxref("navigator.mediaDevices.getUserMedia()")}} 捕捉本地媒体。
  2. +
  3. 呼叫者创建一个RTCPeerConnection 并调用 {{domxref("RTCPeerConnection.addTrack()")}} (注: addStream 已经过时。)
  4. +
  5. 呼叫者调用 ("RTCPeerConnection.createOffer()")来创建一个提议(offer).
  6. +
  7. 呼叫者调用 ("RTCPeerConnection.setLocalDescription()") 将提议(Offer)   设置为本地描述 (即,连接的本地描述).
  8. +
  9. setLocalDescription()之后, 呼叫者请求 STUN 服务创建ice候选(ice candidates)
  10. +
  11. 呼叫者通过信令服务器将提议(offer)传递至 本次呼叫的预期的接受者.
  12. +
  13. 接受者收到了提议(offer) 并调用 ("RTCPeerConnection.setRemoteDescription()") 将其记录为远程描述 (也就是连接的另一端的描述).
  14. +
  15. 接受者做一些可能需要的步骤结束本次呼叫:捕获本地媒体,然后通过{{domxref("RTCPeerConnection.addTrack()")}}添加到连接中。
  16. +
  17. 接受者通过("RTCPeerConnection.createAnswer()")创建一个应答。
  18. +
  19. 接受者调用 ("RTCPeerConnection.setLocalDescription()") 将应答(answer)   设置为本地描述. 此时,接受者已经获知连接双方的配置了.
  20. +
  21. 接受者通过信令服务器将应答传递到呼叫者.
  22. +
  23. 呼叫者接受到应答.
  24. +
  25. 呼叫者调用 ("RTCPeerConnection.setRemoteDescription()") 将应答设定为远程描述. 如此,呼叫者已经获知连接双方的配置了.
  26. +
+ +

待定的和当前描述

+ +

进一步了解该过程,我们发现localDescription和remoteDescription(返回这两个描述的属性 )并不像外观那样简单。 因为在重新协商期间,提议可能会被拒绝,因为它提出了不兼容的格式,每个端点都有能力提出一种新的格式,但是实际上不会切换到另一个对等体,直到它被其他对等体接受为止。 因此,WebRTC使用待定和当前的描述。

+ +

当前描述(由 ("RTCPeerConnection.currentLocalDescription") 和 ("RTCPeerConnection.currentRemoteDescription") 属性返回 )表示连接实际使用的描述。 这是双方已经完全同意使用的最新连接。

+ +

待定的描述(由 ("RTCPeerConnection.pendingLocalDescription" ) 和 ("RTCPeerConnection.pendingRemoteDescription") 返回 )表示当 分别调用setLocalDescription( )或setRemoteDescription( )。

+ +

当读取描述( ("RTCPeerConnection.localDescription" ) 和 ("RTCPeerConnection.remoteDescription" )  )返回时,返回的值是pendingLocalDescription / pendingRemoteDescription的值,如果有待处理的描述( 也就是说,待处理描述不为null ); 否则,返回当前描述(currentLocalDescription / currentRemoteDescription )。

+ +

通过调用setLocalDescription( )或setRemoteDescription( )更改描述时,将指定的描述设置为待定描述,WebRTC层开始评估是否可以接受。 一旦建议的描述已经达成一致,currentLocalDescription或currentRemoteDescription的值将更改为待处理描述,并且待处理的描述再次设置为null,表示没有待处理的描述。

+ +
+

pendingLocalDescription不仅包含正在考虑的提议或答案,而且自从提议或应答以来已经收集到的任何本地ICE候选人都被创建。 类似地,pendingRemoteDescription包括通过调用 ("RTCPeerConnection.addIceCandidate( )" ) 提供的任何远程ICE候选。

+
+ +

有关这些属性和方法的更多细节,请参阅各个文章。

+ +

什么是ICE候选地址?

+ +

除了交换关于媒体的信息(上面提到的Offer / Answer和SDP )中,对等体必须交换关于网络连接的信息。 这被称为ICE候选者,并详细说明了对等体能够直接或通过TURN服务器进行通信的可用方法。 通常,每个对点将优先提出最佳的ICE候选,逐次尝试到不佳的候选中。 理想情况下,候选地址是UDP(因为速度更快,媒体流能够相对容易地从中断恢复 ),但ICE标准也允许TCP候选。

+ +
+

一般来说,使用TCP的ICE候选者只有当UDP不可用或被限制使其不适用于媒体流时才会被使用。 不是所有的浏览器都支持ICE over TCP。

+
+ +

The entire exchange in a complicated diagram

+ +

A complete architectural diagram showing the whole WebRTC process.

diff --git a/files/zh-cn/web/api/webrtc_api/index.html b/files/zh-cn/web/api/webrtc_api/index.html new file mode 100644 index 0000000000..de5a87f5c5 --- /dev/null +++ b/files/zh-cn/web/api/webrtc_api/index.html @@ -0,0 +1,131 @@ +--- +title: WebRTC API +slug: Web/API/WebRTC_API +tags: + - API + - WebRTC + - 中文 +translation_of: Web/API/WebRTC_API +--- +

{{WebRTCSidebar}}

+ +

WebRTC (Web Real-Time Communications) 是一项实时通讯技术,它允许网络应用或者站点,在不借助中间媒介的情况下,建立浏览器之间点对点(Peer-to-Peer)的连接,实现视频流和(或)音频流或者其他任意数据的传输。WebRTC包含的这些标准使用户在无需安装任何插件或者第三方的软件的情况下,创建点对点(Peer-to-Peer)的数据分享和电话会议成为可能。

+ +

WebRTC包含了若干相互关联的API和协议以达到这个目标。你在这里看到的文档将会帮助你理解WebRTC的基本概念,还会教你如何去建立和使用可以传输媒体数据和其他任意数据的连接。当然你还会学到更多其他的东西。

+ +

参考

+ +
+ +
+ +

指南

+ +
+
WebRTC 架构概述
+
用来创建WebRTC连接的API底层使用了一系列的网络协议和连接标准。这篇文章涵盖了这些标准。
+
WebRTC 基础
+
这篇文建将带你贯穿一个跨浏览器RTC应用的整个创建过程。结束的时候,你将拥有一个可以运行的点对点(peer-to-peer)数据通道(data channel)和媒体通道(media channel)。
+
WebRTC 协议
+
这篇文章介绍了一系列的协议,WebRTC API就是建立于他们之上。
+
WebRTC 连接
+
这篇文章将向你介绍为创建点对点的连接并且实现数据或/和媒体的传输,各个WebRTC相关的协议是如何相互协作的。
+
WebRTC API 概览
+
WebRTC包含了一些关联的API和协议,它们相互协作以支持数据或媒体在两个或多个点之间传输。这篇文章将向你展示这些API的简要介绍以及它们的用途。
+
WebRTC 会话的生命周期
+
WebRTC让你可以在基于浏览器应用中实现任意点对点(peer-to-peer)的数据、音频、视频 或它们的任意组合的通信。在这篇文章中我们一起来看一下WebRTC的生命周期。从建立连接开始,一直到不再需要的时候将它关闭掉。
+
+ +

教程

+ +
+
使用WebRTC adapter.js提高应用的兼容性
+
WebRTC组织在GitHub上提供了WebRTC适配器,来解决因不同浏览器对WebRTC实现不同导致的问题。adapter.js是一个JavaScript库,可以让你写的WebRTC应用“一处编写,处处运行”。
+
使用WebRTC拍摄静止的照片
+
这篇文章介绍了如何在WebRTC的支持下可以访问到电脑或者手机的摄像头并且使用它来拍摄照片。
+
一个简易RTCDataChannel的例子
+
{{domxref("RTCDataChannel")}} 接口是一个特性,使用它你可以在两个点之间发送和接收任意数据。它的API和WebSocket API非常相似,所以同样的代码对他们来说都可以使用。
+
+ + + +

协议

+ +

WebRTC-proper protocols

+ + + +

相关的支持协议

+ + + +

规范

+ + + + + + + + + + + + + + + + + + + + + + + + + + +
标准状态说明
{{SpecName('WebRTC 1.0')}}{{Spec2('WebRTC 1.0')}}WebRTC API 的初始定义
{{SpecName('Media Capture')}}{{Spec2('Media Capture')}} +

传输媒体内容流的对象的初始定义

+
{{SpecName('Media Capture DOM Elements')}}{{Spec2('Media Capture DOM Elements')}} +

如何从DOM标签的内容中获取流的初始定义

+
+ + + + + + diff --git a/files/zh-cn/web/api/webrtc_api/overview/index.html b/files/zh-cn/web/api/webrtc_api/overview/index.html new file mode 100644 index 0000000000..b037d259cd --- /dev/null +++ b/files/zh-cn/web/api/webrtc_api/overview/index.html @@ -0,0 +1,22 @@ +--- +title: WebRTC API overview +slug: Web/API/WebRTC_API/Overview +translation_of: Web/API/WebRTC_API#WebRTC_concepts_and_usage +--- +

{{WebRTCSidebar}}

+ +

WebRTC是由一些关联的API和协议一起协作,支持两个或多个终端之间交换数据和媒体信息的技术。这篇文章提供了这些APIs的介绍和提供的功能。

+ +

RTCPeerConnection

+ +

在媒体能够交换,或者数据通道建立之前,你需要把两个终端连接起来。这个连接过程的完成就是使用{{domxref("RTCPeerConnection")}} 接口。

+ +

MediaStream

+ +

{{domxref("MediaStream")}}接口描述了终端之间传输的媒体流。这个流由一个或多个媒体通道信息;通常这是一个音频通道或者视频通道信息。一个媒体流能够传输实时的媒体(例如音频通话或者视频会议等)或者已存的媒体(例如网上电影)。

+ +

RTCDataChannel

+ +

WebRTC支持在建立连接的两个终端之间相互的传输二进制数据。这个过程通过{{domxref("RTCDataChannel")}}接口。

+ +

这个接口可以作为数据的反向通道,甚至作为主要的数据通道去交换各种数据。例如在游戏应用中,通过这个接口可以实现多玩家支持,相互传送玩家的动作更新之类的数据。

diff --git a/files/zh-cn/web/api/webrtc_api/protocols/index.html b/files/zh-cn/web/api/webrtc_api/protocols/index.html new file mode 100644 index 0000000000..a4304e290b --- /dev/null +++ b/files/zh-cn/web/api/webrtc_api/protocols/index.html @@ -0,0 +1,75 @@ +--- +title: WebRTC 协议介绍 +slug: Web/API/WebRTC_API/Protocols +tags: + - API + - ICE + - NAT + - SDP + - STUN + - WebRTC + - 初学者 + - 向导 + - 媒体 + - 视频 + - 音频 +translation_of: Web/API/WebRTC_API/Protocols +--- +

{{WebRTCSidebar}}{{draft}}

+ +

本文介绍了基于WebRTC API构建的协议。

+ +

ICE

+ +

交互式连接设施Interactive Connectivity Establishment (ICE) 是一个允许你的浏览器和对端浏览器建立连接的协议框架。在实际的网络当中,有很多原因能导致简单的从A端到B端直连不能如愿完成。这需要绕过阻止建立连接的防火墙,给你的设备分配一个唯一可见的地址(通常情况下我们的大部分设备没有一个固定的公网地址),如果路由器不允许主机直连,还得通过一台服务器转发数据。ICE通过使用以下几种技术完成上述工作。

+ +

STUN

+ +

NAT的会话穿越功能Session Traversal Utilities for NAT (STUN) (缩略语的最后一个字母是NAT的首字母)是一个允许位于NAT后的客户端找出自己的公网地址,判断出路由器阻止直连的限制方法的协议。

+ +

客户端通过给公网的STUN服务器发送请求获得自己的公网地址信息,以及是否能够被(穿过路由器)访问。

+ +

An interaction between two users of a WebRTC application involving a STUN server.

+ +

NAT

+ +

网络地址转换协议Network Address Translation (NAT) 用来给你的(私网)设备映射一个公网的IP地址的协议。一般情况下,路由器的WAN口有一个公网IP,所有连接这个路由器LAN口的设备会分配一个私有网段的IP地址(例如192.168.1.3)。私网设备的IP被映射成路由器的公网IP和唯一的端口,通过这种方式不需要为每一个私网设备分配不同的公网IP,但是依然能被外网设备发现。

+ +

一些路由器严格地限定了部分私网设备的对外连接。这种情况下,即使STUN服务器识别了该私网设备的公网IP和端口的映射,依然无法和这个私网设备建立连接。这种情况下就需要转向TURN协议。

+ +

TURN

+ +

一些路由器使用一种“对称型NAT”的NAT模型。这意味着路由器只接受和对端先前建立的连接(就是下一次请求建立新的连接映射)。

+ +

NAT的中继穿越方式Traversal Using Relays around NAT (TURN) 通过TURN服务器中继所有数据的方式来绕过“对称型NAT”。你需要在TURN服务器上创建一个连接,然后告诉所有对端设备发包到服务器上,TURN服务器再把包转发给你。很显然这种方式是开销很大的,所以只有在没得选择的情况下采用。

+ +

An interaction between two users of a WebRTC application involving STUN and TURN servers.

+ +

SDP

+ +

会话描述协议Session Description Protocol (SDP) 是一个描述多媒体连接内容的协议,例如分辨率,格式,编码,加密算法等。所以在数据传输时两端都能够理解彼此的数据。本质上,这些描述内容的元数据并不是媒体流本身。

+ +

从技术上讲,SDP并不是一个真正的协议,而是一种数据格式,用于描述在设备之间共享媒体的连接。

+ +

记录SDP远远超出了本文档的范围。但是,这里有几件事值得注意。

+ + + +

结构体

+ +

SDP由一行或多行UTF-8文本组成,每行以一个字符的类型开头,后跟等号(“ =”),然后是包含值或描述的结构化文本,其格式取决于类型。以给定字母开头的文本行通常称为“字母行”。例如,提供媒体描述的行的类型为“ m”,因此这些行称为“ m行”。

+ +

获取更多信息

+ +

要了解有关SDP的更多信息,请参见以下有用的资源:

+ + + + + +
+
+
diff --git a/files/zh-cn/web/api/webrtc_api/signaling_and_video_calling/index.html b/files/zh-cn/web/api/webrtc_api/signaling_and_video_calling/index.html new file mode 100644 index 0000000000..f8f8ab33aa --- /dev/null +++ b/files/zh-cn/web/api/webrtc_api/signaling_and_video_calling/index.html @@ -0,0 +1,674 @@ +--- +title: 信令与视频通话 +slug: Web/API/WebRTC_API/Signaling_and_video_calling +translation_of: Web/API/WebRTC_API/Signaling_and_video_calling +--- +

{{WebRTCSidebar}}

+ +

WebRTC允许在两个设备之间进行实时的对等媒体交换。通过称为信令的发现和协商过程建立连接。本教程将指导你构建双向视频通话。

+ +

WebRTC是一个完全对等技术,用于实时交换音频、视频和数据,同时提供一个中心警告。如其他地方所讨论的,必须进行一种发现和媒体格式协商,以使不同网络上的两个设备相互定位。这个过程被称为信令,并涉及两个设备连接到第三个共同商定的服务器。通过这个第三方服务器,这两台设备可以相互定位,并交换协商消息。

+ +

在本文中,我们将进一步扩充 WebSocket chat 作为我们的WebSocket文档的一部分(本文链接即将发布;它实际上还没有在线),以支持在用户之间的双向视频通话。你可以在Glitch上查看这个例子,你也尝试修改这个例子。您还可以在GitHub上查看完整的项目代码。

+ +
+

注意:如果你尝试在Glitch的例子,请注意任何代码的改动将立即重置所有连接。并且这个例子有短暂的延迟;Glitch的例子仅仅作为简单的实验和测试用途。

+
+ +

信令服务器

+ +

两个设备之间建立WebRTC连接需要一个信令服务器来实现双方通过网络进行连接。信令服务器的作用是作为一个中间人帮助双方在尽可能少的暴露隐私的情况下建立连接。那我们如何实现这个服务器并且它是如何工作的呢?

+ +

WebRTC并没有提供信令传递机制,你可以使用任何你喜欢的方式如WebSocket 或者{{domxref("XMLHttpRequest")}} 等等,来交换彼此的令牌信息。

+ +

重要的是信令服务器并不需要理解和解释信令数据内容。虽然它基于 {{Glossary("SDP")}}但这并不重要:通过信令服务器的消息的内容实际上是一个黑盒。重要的是,当{{Glossary("ICE")}}子系统指示你将信令数据发送给另一个对等方时,你就这样做,而另一个对等方知道如何接收此信息并将其传递给自己的ICE子系统。你所要做的就是来回传递信息。内容对信令服务器一点都不重要。

+ +

开始准备聊天服务器来处理信令

+ +

我们的聊天服务器和客户端使用 WebSocket API  {{Glossary("JSON")}} 格式的字符串来传递数据。服务器支持多种消息格式来处理不同的任务,比如注册新用户、设置用户名、发送公共信息等等。

+ +

为了让服务器支持信令和ICE协商,我们需要升级代码,我们需要直接发送聊天系统到指定的用户而不是发送给所有人,并且保证服务器在不需要理解数据内容的情况下传递未被认可的任何消息类型。这让我们可以使用一台服务器来传递信令和消息而不是多台。

+ +

让我们看一下我们还需要做些什么让它支持WebRTC信令. 代码在 chatserver.js.中实现。

+ +

首先来看 sendToOneUser()  函数,如名所示它发送JSON字符串到指定的用户。

+ +
function sendToOneUser(target, msgString) {
+  var isUnique = true;
+  var i;
+
+  for (i=0; i<connectionArray.length; i++) {
+    if (connectionArray[i].username === target) {
+      connectionArray[i].sendUTF(msgString);
+      break;
+    }
+  }
+}
+ +

这个函数遍历所有在线用户直到找到给定的用户名然后发送数据 msgString 一个JSON字符串对象,我们可以让它接收我们的原始消息对象,但是在当前这种情况下它的效率更高因为我们的消息已经字符串化了,我们达到了不需要进一步处理就可以发送消息的目的。

+ +

我们原来的DEMO不能发送消息到指定的用户,我们可以通过修改WebSocket消息处理句柄来实现这个功能,这需要在 connection.on() 尾部修改。

+ +
if (sendToClients) {
+  var msgString = JSON.stringify(msg);
+  var i;
+
+  // If the message specifies a target username, only send the
+  // message to them. Otherwise, send it to every user.
+  if (msg.target && msg.target !== undefined && msg.target.length !== 0) {
+    sendToOneUser(msg.target, msgString);
+  } else {
+    for (i=0; i<connectionArray.length; i++) {
+      connectionArray[i].sendUTF(msgString);
+    }
+  }
+}
+ +

代码会检查我们的数据是否提供了 target 属性. 这个属性包含了我们想要发送给的人的用户名。如果提供了 target 属性, 通过调用 sendToOneUser() 消息将只发送给指定的人. 否则的话将遍历在线列表发送给每一个人。

+ +

由于现行的代码可以发送任意类型的消息,所以我们不需要做任何的修改。现在我们的客户端可以发送任意消息给指定的用户。

+ +

我们需要做的在服务器这边,现在我们来考虑信令协议的设计与实现。

+ +

设计信令协议

+ +

 现在我们要构建一套信息交换规则,我们需要一套协议来定义消息格式。实现这个有好多种办法,demo里只是其中一种,并不是唯一。

+ +

例子中的服务器使用字符串化的JSON对象来和客户端通信,意味着我们的信令消息也将使用JSON格式,其内容指定消息类型和如何处理这些消息。

+ +

交换会话描述信息

+ +

开始处理信号的时候,用户的初始化操作会创建一个请求(offer) ,根据 {{Glossary("SDP")}} 协议其中会包含一个session描述符,并且需要把这个发送到我们称之为接收者(callee)那里, 接受者需要返回一个包含描述符的应答(answer)信息。我们的服务器使用 WebSocket 来传递 "video-offer" "video-answer"  两种类型的消息数据。这些消息包含以下属性:

+ +
+
 type
+
消息类型; "video-offer" 或 "video-answer"
+
name
+
发送者用户名
+
target
+
接受者的用户名(如果呼叫者正在发送消息,则指定被呼叫者,反之亦然)
+
sdp
+
描述连接本地端SDP(Session Description Protocol)协议字符串(从接收者的角度来看,它描述远程端)
+
+ +

到此为止双方都知道使用什么样的代码和参数进行通信了。尽管如此他们仍然不知道自己该如何传递媒体数据。 {{Glossary('ICE', 'Interactive Connectivity Establishment (ICE)')}}协议该上场了。

+ +

交换 ICE 候选

+ +

两个节点需要交换ICE候选来协商他们自己具体如何连接。每一个ICE候选描述一个发送者使用的通信方法,每个节点按照他们被发现的顺序发送候选并且保持发送直到退出,即使媒体数据流已经开始传递也要如此。

+ +

使用 pc.setLocalDescription(offer) 添加本地描述符后一个 icecandidate 事件将被发送到 {{domxref("RTCPeerConnection")}} 

+ +

一旦两端同意了一个互相兼容的候选,该候选的SDP就被用来创建并打开一个连接,通过该连接媒体流就开始运转。如果之后他们同意了一个更好(通常更高效)的候选,流亦会按需变更格式。

+ +

虽然当前并未被支持,一个候选在媒体流已经开始运转之后理论上如果需要的话也可以降级至一个低带宽的连接。

+ +

每个 ICE候选通过信令服务器发送一个 "new-ice-candidate" 类型的JSON信息来送给远程的另一端。每个候选信息包括以下字段:

+ +
+
类型
+
消息类型: "new-ice-candidate".
+
目标
+
待建立联系人的用户名;服务器将仅会管理与该用户的信息。
+
候选
+
SDP候选字符串,描述了计划的连接方法。通常不需要查看此字符串的内容。你需要做的所有代码都是使用信令服务器将其路由到远程对等机。
+
+ +

每个ICE消息都建议提供一个通信协议(TCP或UDP)、IP地址、端口号、连接类型(例如,指定的IP是对等机本身还是中继服务器),以及将两台计算机连接在一起所需的其他信息。这包括NAT或其他网络问题。

+ +
+

注意: 最需要注意的是: 你的代码在ICE协商期间唯一需要负责的是从ICE层接受外向候选并通过与另一端的信号连接发送他们,当你的  {{domxref("RTCPeerConnection.onicecandidate", "onicecandidate")}} 控制器已经执行后, 同时从信令服务器接收 ICE候选消息 (当接收到 "new-ice-candidate" 消息时) 然后通过调用{{domxref("RTCPeerConnection.addIceCandidate()")}}发送他们到你的ICE层。 嗯,就是这样。

+ +

SDP的内容基本上在所有情况下都是与你不相关的。在你真正知道自己在做什么之前,不要试图让事情变得更复杂。否则情况会非常混乱。

+
+ +

你的信令服务器现在需要做的就是发送它请求的消息。你的工作流还可能需要登录/身份验证功能,但这些细节都是大同小异的。

+ +

信令事务流程

+ +

信令过程涉及到使用中间层信令服务器在两个对等机之间交换消息。当然,具体的处理过程会有所不同,但一般来说,处理信令消息的关键点有以下几个:

+ +

信令过程涉及多个点之间的消息交换:

+ + + +

假设Naomi和Priya正在使用聊天软件进行讨论,Naomi决定在两人之间打开一个视频通话。以下是预期的事件顺序:

+ +

Diagram of the signaling process

+ +

在本文的整个过程中,我们将看到更详细的信息。

+ +

ICE 候选交换过程

+ +

当每端的ICE层开始发送候选时,它会在链中的各个点之间进行交换,如下所示:

+ +

Diagram of ICE candidate exchange process

+ +

每一端从本地的ICE层接收候选时,都会将其发送给另一方;不存在轮流或成批的候选。一旦两端就一个候选达成一致,双方就都可以用此候选来交换媒体数据,媒体数据就开始流动。即使在媒体数据已经开始流动之后,每一端都会继续向候选发送消息,直到他们没有选择的余地。这样做是为了找到比最初选择的更好的选择。

+ +

如果条件发生变化,例如网络连接恶化,一个或两个对等方可能建议切换到较低带宽的媒体分辨率,或其他编解码器。这将触发新的候选交换,之后可能会发生另一种媒体格式和/或编解码器更改。

+ +

作为可选项, 查看 {{RFC(5245, "Interactive Connectivity Establishment")}}, section 2.6 ("Concluding ICE")如果你想更深入地了解这一过程,就要在ICE层内部完成。你应该注意到,候选交换后,一旦ICE层满足要求,媒体数据就开始流动。所有这些都是在幕后处理端。我们的任务就是简单地通过信令服务器来回发送候选。

+ +

客户端应用

+ +

任何信号处理的核心是其消息处理。使用WebSockets来发送信号并不是必须的,但这是一种常见的解决方案。当然,您应该选择一种机制来交换适合你的应用程序的信号信息。

+ +

让我们更新聊天客户端以支持视频呼叫。

+ +

更新 HTML

+ +

我们客户端的HTML需要一个视频显示位置。也就是视频框和挂断电话的按钮:

+ +
      <div class="flexChild" id="camera-container">
+        <div class="camera-box">
+          <video id="received_video" autoplay></video>
+          <video id="local_video" autoplay muted></video>
+          <button id="hangup-button" onclick="hangUpCall();" disabled>
+            Hang Up
+          </button>
+        </div>
+      </div>
+ +

此处定义的页面结构使用了 {{HTMLElement("div")}} 元素,通过启用CSS,我们可以完全控制页面布局。我们将跳过本指南中的布局细节,但你可以看看GitHub上的CSS,了解如何处理它。 注意这两个 {{HTMLElement("video")}} 元素,一个用于观看自己,一个用于连接,还有 {{HTMLElement("button")}} 元素.

+ +

 id  为 "received_video" 的 <video> 元素将显示从连接的用户接收的视频。我们指定了autoplay 属性,确保一旦视频到达,它立即播放。这消除了在代码中显式处理回放的任何需要。"local_video<video> 元素显示用户相机的预览;指定 muted 属性,因为我们不需要在此预览面板中听到本地音频。

+ +

最后,定义"hangup-button" {{HTMLElement("button")}} 来挂断一个呼叫,并将其配置为禁用启动(将此设置为未连接任何调用时的默认设置),并在单击时调用函数  hangUpCall() 。这个函数的作用是关闭调用,并向另一个对等端发送一个信号服务器通知,请求它也关闭。

+ +

JavaScript 代码

+ +

我们将把这段代码划分为多个功能区,以便更容易地描述它是如何工作的。该代码的主体位于 connect() 函数中:它在6503端口上打开一个{{domxref("WebSocket")}} 服务器,并建立一个处理程序来接收JSON对象格式的消息。此代码通常像以前那样处理文本聊天消息。

+ +

向信令服务器发送信息

+ +

在整个代码中,我们调用 sendToServer() 以便向信令服务器发送消息。此函数使用WebSocket连接执行其工作:

+ +
function sendToServer(msg) {
+  var msgJSON = JSON.stringify(msg);
+
+  connection.send(msgJSON);
+}
+ +

通过调用{{jsxref("JSON.stringify()")}},将传递到此方法的消息对象转换为json字符串,然后调用WebSocket连接的 {{domxref("WebSocket.send", "send()")}} 方法将消息传输到服务器。

+ +

开始通话的交互

+ +

处理 "userlist" 消息的代码会调用 handleUserlistMsg()。在这里,我们在聊天面板左侧显示的用户列表中为每个连接的用户设置处理程序。此方法接收一个消息对象,其 users 属性是一个字符串数组,指定每个连接用户的用户名。

+ +
function handleUserlistMsg(msg) {
+  var i;
+  var listElem = document.querySelector(".userlistbox");
+
+  while (listElem.firstChild) {
+    listElem.removeChild(listElem.firstChild);
+  }
+
+  msg.users.forEach(function(username) {
+    var item = document.createElement("li");
+    item.appendChild(document.createTextNode(username));
+    item.addEventListener("click", invite, false);
+
+    listElem.appendChild(item);
+  });
+}
+ +

在获得对 {{HTMLElement("ul")}} 的引用(其中包含变量 listElem中的用户名列表)后,我们通过删除其每个子元素清空列表。

+ +
+

注意:显然,通过添加和删除单个用户而不是每次更改时都重新构建整个列表来更新列表会更有效,但对于本例而言,这已经足够好了。

+
+ +

然后我们使用 {{jsxref("Array.forEach", "forEach()")}} 迭代用户名数组。对于每个名称,我们创建一个新的 {{HTMLElement("li")}} 元素,然后使用{{domxref("Document.createTextNode", "createTextNode()")}} 创建一个包含用户名的新文本节点。该文本节点被添加为 <li> 元素的子节点。接下来,我们为列表项上的 {{event("click")}} 事件设置一个处理程序,单击用户名将调用 invite() 方法,我们将在下一节中查看该方法。

+ +

开始一个通话

+ +

当用户单击要调用的用户名时,将调用 invite() 函数作为该事件的事件处理程序 {{event("click")}} 事件:

+ +
var mediaConstraints = {
+  audio: true, // We want an audio track
+  video: true // ...and we want a video track
+};
+
+function invite(evt) {
+  if (myPeerConnection) {
+    alert("You can't start a call because you already have one open!");
+  } else {
+    var clickedUsername = evt.target.textContent;
+
+    if (clickedUsername === myUsername) {
+      alert("I'm afraid I can't let you talk to yourself. That would be weird.");
+      return;
+    }
+
+    targetUsername = clickedUsername;
+
+    createPeerConnection();
+
+    navigator.mediaDevices.getUserMedia(mediaConstraints)
+    .then(function(localStream) {
+      document.getElementById("local_video").srcObject = localStream;
+      myPeerConnection.addStream(localStream);
+    })
+    .catch(handleGetUserMediaError);
+  }
+}
+ +

这从一个基本的健全性检查开始:用户是否连在一起?如果没有{{domxref("RTCPeerConnection")}} ,他们显然无法进行呼叫。然后,从事件目标的 {{domxref("Node.textContent", "textContent")}} 属性中获取单击的用户的名称,并检查以确保尝试启动调用的不是同一个用户。

+ +

然后我们将要调用的用户的名称复制到变量 targetUsername 中,并调用 createPeerConnection(),该函数将创建并执行{{domxref("RTCPeerConnection")}} 的基本配置。

+ +

创建 RTCPeerConnection 后,我们通过调用 {{domxref("MediaDevices.getUserMedia()")}},请求访问用户的相机和麦克风,该命令通过 {{domxref("Navigator.mediaDevices.getUserMedia")}} 属性向我们公开。当成功完成返回的promise时,将执行我们的 then 处理程序。它接收一个 {{domxref("MediaStream")}} 对象作为输入,该对象表示来自用户麦克风的音频和来自网络摄像机的视频流。

+ +
+

注意:我们可以通过调用 {{domxref("MediaDevices.enumerateDevices", "navigator.mediaDevices.enumerateDevices()")}} 获取设备列表,根据所需条件筛选结果列表,然后使用所选设备{{domxref("MediaTrackConstraints.deviceId", "deviceId")}} 传入getUserMedia()  mediaConstraints 对象的deviceId 字段中的值。事实上,除非必须要不然很少这样用,因为大部分工作都是由 getUserMedia()为你完成的。

+
+ +

我们通过设置元素的 {{domxref("HTMLMediaElement.srcObject", "srcObject")}} 属性,将传入流附加到本地预览  {{HTMLElement("video")}} 元素。由于元素被配置为自动播放传入的视频,因此流开始在本地预览框中播放。

+ +

然后遍历流中的磁道,调用 {{domxref("RTCPeerConnection.addTrack", "addTrack()")}} 将每个磁道添加到 RTCPeerConnection。尽管连接尚未完全建立,但必须尽快开始向其发送媒体数据,因为媒体数据将帮助ICE层决定采取的最佳连接方式,这有助于协商过程。

+ +

一旦媒体数据连接到 RTCPeerConnection,就会在连接处触发事件{{event("negotiationneeded")}} 事件,以便启动ICE协商。

+ +

如果在尝试获取本地媒体流时发生错误,catch子句将调用handleGetUserMediaError(),根据需要向用户显示适当的错误。

+ +

处理 getUserMedia() 错误

+ +

如果 getUserMedia() 返回的promise失败,将执行handleGetUserMediaError() 函数。

+ +
function handleGetUserMediaError(e) {
+  switch(e.name) {
+    case "NotFoundError":
+      alert("Unable to open your call because no camera and/or microphone" +
+            "were found.");
+      break;
+    case "SecurityError":
+    case "PermissionDeniedError":
+      // Do nothing; this is the same as the user canceling the call.
+      break;
+    default:
+      alert("Error opening your camera and/or microphone: " + e.message);
+      break;
+  }
+
+  closeVideoCall();
+}
+ +

除了一条错误信息外,所有情况下都会显示一条错误信息。在本例中,我们忽略"SecurityError" 和 "PermissionDeniedError" 结果,处理拒绝授予使用媒体硬件的权限与用户取消呼叫的方法是相同的。

+ +

不管尝试获取流失败的原因是什么,我们调用 closeVideoCall()函数关闭 {{domxref("RTCPeerConnection")}},并释放尝试调用过程中已分配的任何资源。此代码旨在安全地处理部分启动的调用。

+ +

创建端到端连接

+ +

调用方和被调用方都使用 createPeerConnection() 函数来构造它们的 {{domxref("RTCPeerConnection")}} 对象及其各自的WebRTC连接端。当调用者试图启动调用时,由 invite() 调用;当被调用者从调用者接收到要约消息时,由handleVideoOfferMsg() 调用。

+ +
function createPeerConnection() {
+  myPeerConnection = new RTCPeerConnection({
+      iceServers: [     // Information about ICE servers - Use your own!
+        {
+          urls: "stun:stun.stunprotocol.org"
+        }
+      ]
+  });
+
+  myPeerConnection.onicecandidate = handleICECandidateEvent;
+  myPeerConnection.ontrack = handleTrackEvent;
+  myPeerConnection.onnegotiationneeded = handleNegotiationNeededEvent;
+  myPeerConnection.onremovetrack = handleRemoveTrackEvent;
+  myPeerConnection.oniceconnectionstatechange = handleICEConnectionStateChangeEvent;
+  myPeerConnection.onicegatheringstatechange = handleICEGatheringStateChangeEvent;
+  myPeerConnection.onsignalingstatechange = handleSignalingStateChangeEvent;
+}
+ +

当使用 {{domxref("RTCPeerConnection.RTCPeerConnection", "RTCPeerConnection()")}} 构造函数时,我们将指定一个{{domxref("RTCConfiguration")}}-兼容对象,为连接提供配置参数。在这个例子中,我们只使用其中的一个: iceServers。这是描述 {{Glossary("ICE")}} 层的STUN和/或TURN服务器的对象数组,在尝试在呼叫者和被呼叫者之间建立路由时使用。这些服务器用于确定在对等端之间通信时要使用的最佳路由和协议,即使它们位于防火墙后面或使用 {{Glossary("NAT")}}。

+ +
+

注意:你应该始终使用你拥有的或你有特定授权使用的STUN/TURN服务器。这个例子是使用一个已知的公共服务器,但是滥用这些是不好的。

+
+ +

iceServers 中的每个对象至少包含一个 urls 字段,该字段提供可以访问指定服务器的URLs。它还可以提供 username 和 credential值,以便在需要时进行身份验证。

+ +

在创建了 {{domxref("RTCPeerConnection")}} 之后,我们为对我们重要的事件设置了处理程序。

+ +

前三个事件处理程序是必需的;你必须处理它们才能使用WebRTC执行任何涉及流媒体的操作。其余的并不是严格要求的,但可能有用,我们将对此进行探讨。在这个例子中,还有一些其他的事件我们没有使用。下面是我们将要实现的每个事件处理程序的摘要:

+ +

{{domxref("RTCPeerConnection.onicecandidate")}}

+ +
+
+

当需要你通过信令服务器将一个ICE候选发送给另一个对等端时,本地ICE层将会调用你的 {{event("icecandidate")}} 事件处理程序。有关更多信息,请参阅{{anch("Sending ICE candidates")}} 以查看此示例的代码。

+
+
{{domxref("RTCPeerConnection.ontrack")}}
+
当向连接中添加磁道时,{{event("track")}} 事件的此处理程序由本地WebRTC层调用。例如,可以将传入媒体连接到元素以显示它。详见 {{anch("Receiving new streams")}} 。
+
{{domxref("RTCPeerConnection.onnegotiationneeded")}}
+
每当WebRTC基础结构需要你重新启动会话协商过程时,都会调用此函数。它的工作是创建和发送一个请求,给被叫方,要求它与我们联系。参见{{anch("Starting negotiation")}} 了解我们如何处理这一问题。
+
{{domxref("RTCPeerConnection.onremovetrack")}}
+
调用与 ontrack相对应的对象来处理 {{event("removetrack")}} 事件;当远程对等端从正在发送的媒体中删除磁道时,它将发送到RTCPeerConnection 。参见 {{anch("Handling the removal of tracks")}} 。
+
{{domxref("RTCPeerConnection.oniceconnectionstatechange")}}
+
ICE层发送{{event("iceconnectionstatechange")}} 事件,让你了解ICE连接状态的更改。这可以帮助你了解连接何时失败或丢失。我们将在下面的{{anch("ICE connection state")}} 中查看此示例的代码。
+
{{domxref("RTCPeerConnection.onicegatheringstatechange")}}
+
当ICE代理收集候选对象的过程从一个状态切换到另一个状态(例如开始收集候选对象或完成协商)时,ICE层将向你发送事件(“ICegulatingStateChange”)事件。见下文 {{anch("ICE gathering state")}}。
+
{{domxref("RTCPeerConnection.onsignalingstatechange")}}
+
+

当信令进程的状态更改时(或如果到信令服务器的连接更改时),WebRTC架构将向你发送 {{event("signalingstatechange")}} 消息。参见{{anch("Signaling state")}} 查看我们的代码。

+
+
+ +

开始协商

+ +

一旦调用者创建了其 {{domxref("RTCPeerConnection")}} ,创建了媒体流,并将其磁道添加到连接中,如 {{anch("Starting a call")}} 所示,浏览器将向{{domxref("RTCPeerConnection")}} 传递一个 {{event("negotiationneeded")}} 事件,以指示它已准备好开始与其他对等方协商。以下是我们处理 {{event("negotiationneeded")}} 事件的代码:

+ +
function handleNegotiationNeededEvent() {
+  myPeerConnection.createOffer().then(function(offer) {
+    return myPeerConnection.setLocalDescription(offer);
+  })
+  .then(function() {
+    sendToServer({
+      name: myUsername,
+      target: targetUsername,
+      type: "video-offer",
+      sdp: myPeerConnection.localDescription
+    });
+  })
+  .catch(reportError);
+}
+ +

要开始协商过程,我们需要创建一个SDP请求并将其发送给我们想要连接的对等端。此请求包括支持的连接配置列表,包括有关我们在本地添加到连接的媒体流(即,我们希望发送到呼叫另一端的视频)的信息,以及ICE层已经收集到的任何ICE候选。我们通过调用 {{domxref("RTCPeerConnection.createOffer", "myPeerConnection.createOffer()")}} 创建此请求。

+ +

当 createOffer() 成功(执行promise)时,我们将创建的请求信息传递到{{domxref("RTCPeerConnection.setLocalDescription", "myPeerConnection.setLocalDescription()")}} ,它为调用方的连接端配置连接和媒体配置状态。

+ +
+

注意:从技术上讲, createOffer() 返回的字符串是{{RFC(3264)}} 请求。

+
+ +

我们知道描述是有效的,并且在满足setLocalDescription() 返回的promise时已经设置好了。也就是说我们创建了一个包含本地描述(现在与请求相同)的新 "video-offer" 消息,然后通过我们的信令服务器将请求发送给被叫方。请求有以下要素:

+ +
+
type
+
消息类型: "video-offer".
+
name
+
调用方的用户名。
+
target
+
被调用方的用户名
+
sdp
+
SDP 字符串描述了请求
+
+ +

如果在初始 createOffer() 或后面的任何实现处理程序中发生错误,则通过调用 reportError() 函数报告错误。

+ +

在 setLocalDescription()的实现处理程序运行后,ICE代理开始向其发现的每个潜在 {{domxref("RTCPeerConnection")}} 配置发送 {{event("icecandidate")}} 事件。我们的 icecandidate 事件处理程序负责将候选对象传输到另一个对等方。

+ +

会话协商

+ +

既然我们已经开始与另一个对等方进行协商并传输了一个请求,那么让我们来看一下在连接的被叫方方面会发生什么。被调用方接收该请求并调用 handleVideoOfferMsg()函数来处理它。让我们看看被叫方如何处理 "video-offer" 消息。

+ +
处理请求
+ +

当请求到达时,调用被调用方的 handleVideoOfferMsg() 函数时会收到"video-offer" 消息。这个函数需要做两件事。首先,它需要创建自己的{{domxref("RTCPeerConnection")}} 并添加包含麦克风和网络摄像头的音频和视频的磁道。其次,它需要对收到的请求进行处理,构建并返回应答。

+ +
function handleVideoOfferMsg(msg) {
+  var localStream = null;
+
+  targetUsername = msg.name;
+  createPeerConnection();
+
+  var desc = new RTCSessionDescription(msg.sdp);
+
+  myPeerConnection.setRemoteDescription(desc).then(function () {
+    return navigator.mediaDevices.getUserMedia(mediaConstraints);
+  })
+  .then(function(stream) {
+    localStream = stream;
+    document.getElementById("local_video").srcObject = localStream;
+
+    localStream.getTracks().forEach(track => myPeerConnection.addTrack(track, localStream));
+  })
+  .then(function() {
+    return myPeerConnection.createAnswer();
+  })
+  .then(function(answer) {
+    return myPeerConnection.setLocalDescription(answer);
+  })
+  .then(function() {
+    var msg = {
+      name: myUsername,
+      target: targetUsername,
+      type: "video-answer",
+      sdp: myPeerConnection.localDescription
+    };
+
+    sendToServer(msg);
+  })
+  .catch(handleGetUserMediaError);
+}
+ +

此代码与我们在 {{anch("Starting a call")}} 中在 invite() 函数中所做的非常相似。它首先使用 createPeerConnection() 函数创建和配置{{domxref("RTCPeerConnection")}} 。然后,它从收到的 "video-offer" 消息中获取SDP请求,并使用它创建一个表示调用方会话描述的新 {{domxref("RTCSessionDescription")}} 对象。

+ +

然后将该会话描述传递到 {{domxref("RTCPeerConnection.setRemoteDescription", "myPeerConnection.setRemoteDescription()")}}。这将把接收到的请求建立为连接的远程(调用方)端的描述。如果成功,promise成功处理程序(在then()子句中)将使用 {{domxref("MediaDevices.getUserMedia", "getUserMedia()")}},将磁道添加到连接,以此类推,如前面在 invite()中看到的那样。

+ +

一旦使用 {{domxref("RTCPeerConnection.createAnswer", "myPeerConnection.createAnswer()")}} 创建了应答,通过调用{{domxref("RTCPeerConnection.setLocalDescription", "myPeerConnection.setLocalDescription()")}} 连接本地端的描述被设置为应答的SDP,则通过信令服务器将应答发送给调用者,让他们知道应答是什么。

+ +

捕捉到的任何错误都会被传递给 handleGetUserMediaError(),详见 {{anch("Handling getUserMedia() errors")}} 。

+ +
+

注意:与调用者的情况一样,一旦 setLocalDescription()实现处理程序运行完毕,浏览器将开始触发被调用者必须处理的{{event("icecandidate")}} 事件,每个需要传输到远程对等方的候选事件对应一个事件。

+
+ +
发送 ICE 候选
+ +

ICE协商过程涉及到每一个对等端不断地向另一个对等端发送候选,直到它用尽了支持 RTCPeerConnection的媒体传输需求的潜在方法。因为ICE不知道你的信令服务器,所以你的处理程序代码需要处理 {{event("icecandidate")}} 事件中每个候选的传输。

+ +

你的 {{domxref("RTCPeerConnection.onicecandidate", "onicecandidate")}} 处理程序接收一个事件,该事件的候选属性是描述该候选的SDP(或为 null ,表示ICE层已耗尽建议的潜在配置)。候选的内容是你需要使用信令服务器传输的内容。下面是我们的示例实现:

+ +
function handleICECandidateEvent(event) {
+  if (event.candidate) {
+    sendToServer({
+      type: "new-ice-candidate",
+      target: targetUsername,
+      candidate: event.candidate
+    });
+  }
+}
+ +

这将构建一个包含候选对象的对象,然后使用前面在 {{anch("Sending messages to the signaling server")}} 中描述的sendToServer() 函数将其发送给另一个对等方。消息属性为:

+ +

type

+ +
+
消息类型: "new-ice-candidate".
+
target
+
ICE候选需要传递到的用户名。这允许信令服务器路由消息。
+
candidate
+
代表ICE层想要传输给另一个对等体的候选体的SDP。
+
+ +

此消息的格式(与处理信号时所做的所有操作一样)完全取决于你的需要;你可以根据需要提供其他信息。

+ +
+

注意:重要的是要记住, {{event("icecandidate")}} 事件不会在ICE候选从呼叫的另一端到达时发送。相反,它们是由你自己的呼叫端发送的,这样你就可以承担通过你选择的任何通道传输数据的任务。当你刚接触WebRTC时,这会让人困惑。

+
+ +
接收 ICE 候选
+ +

信令服务器使用它选择的任何方法将每个ICE候选传递给目标对等机;在我们的示例中,我们用的是JSON对象, type 属性包含字符串 "new-ice-candidate"。我们的r handleNewICECandidateMsg() 函数由主WebSocket传入消息代码调用,以处理这些消息:

+ +
function handleNewICECandidateMsg(msg) {
+  var candidate = new RTCIceCandidate(msg.candidate);
+
+  myPeerConnection.addIceCandidate(candidate)
+    .catch(reportError);
+}
+ +

此函数通过将接收到的SDP传递给它的构造函数来构造一个 {{domxref("RTCIceCandidate")}}对象,然后通过{{domxref("RTCPeerConnection.addIceCandidate", "myPeerConnection.addIceCandidate()")}}将候选传递给ICE层。这把新建的ICE候选交给了当地的ICE层,最终,我们在处理整个候选的过程中的角色就完整的了。

+ +

每一个对等端向另一个对等端发送一个候选的可能传输配置,它认为这对于正在交换的媒体可能是可行的。在某种程度上,这两端认为,一个给定的候选是一个很好的选择,于是他们打开连接,开始分享媒体数据。然而,重点要注意的是,一旦媒体数据开始流动,ICE上协商就不会停止。相反,在对话开始后,候选对象可能仍然在不断地进行交换,可能是在试图找到更好的连接方法的同时,也可能只是因为在对等方成功建立连接时,他们已经在传输中了。

+ +

此外,如果发生什么事情导致流场景发生变化,协商将再次开始,将事件{{event("negotiationneeded")}}事件发送到{{domxref("RTCPeerConnection")}},整个过程将如前所述重新开始。这可能发生在各种情况下,包括:

+ + + +
接收新的流数据
+ +

当新的磁道添加到 RTCPeerConnection时——通过调用其{{domxref("RTCPeerConnection.addTrack", "addTrack()")}} 方法,或者由于重新协商流的格式——对于添加到连接的每个磁道, 一个{{event("track")}}事件设置为 RTCPeerConnection 。使用新添加的媒体需要实现 track 事件的处理程序。常见的需要是将传入的媒体附加到适当的HTML元素。在我们的示例中,我们将磁道的流添加到显示传入视频的 {{HTMLElement("video")}} 元素:

+ +
function handleAddStreamEvent(event) {
+  document.getElementById("received_video").srcObject = event.stream;
+  document.getElementById("hangup-button").disabled = false;
+}
+ +

传入流附加到 "received_video"{{HTMLElement("video")}}  元素,并且启用"Hang Up" {{HTMLElement("button")}}元素,以便用户挂断呼叫。

+ +

完成此代码后,其他对等方发送的视频将显示在本地浏览器窗口中!

+ +
处理流的移除
+ +

当远程对等方通过调用{{domxref("RTCPeerConnection.removeTrack()")}}.从连接中删除磁道时,你的代码将接收事件{{event("removetrack")}}事件。 "removetrack" 的处理程序是:

+ +
function handleRemoveTrackEvent(event) {
+  var stream = document.getElementById("received_video").srcObject;
+  var trackList = stream.getTracks();
+
+  if (trackList.length == 0) {
+    closeVideoCall();
+  }
+}
+ +

此代码从"received_video" {{HTMLElement("video")}}元素的{{htmlattrxref("srcObject", "video")}} 属性获取传入视频 {{domxref("MediaStream.getTracks", "getTracks()")}} 方法获取流的磁道数组。

+ +

如果数组的长度为零,意味着流中没有剩余的磁道,则通过调用 closeVideoCall()结束调用。这样就可以将我们的应用程序恢复到可以启动或接收另一个呼叫的状态。请参阅 {{anch("Ending the call")}} 了解 closeVideoCall() 的工作原理。

+ +

结束通话

+ +

通话可能结束的原因有很多。一个通话可能已经结束,当一方或双方都挂断了电话。可能发生了网络故障,或者某个用户退出了浏览器,或者发生了系统崩溃。无论如何,一切美好的事物都必须结束。

+ +
挂机
+ +

当用户单击"Hang Up"按钮结束调用时,将调用 hangUpCall() 函数:

+ +
function hangUpCall() {
+  closeVideoCall();
+  sendToServer({
+    name: myUsername,
+    target: targetUsername,
+    type: "hang-up"
+  });
+}
+ +

hangUpCall() 执行 closeVideoCall() 来关闭并重置连接并释放资源。然后它会生成一个 "hang-up" 消息,并将其发送到呼叫的另一端,告诉另一个对等端整齐地关闭自己。

+ +
结束通话
+ +

 closeVideoCall() 函数,如下所示,负责停止流、清理和处理 {{domxref("RTCPeerConnection")}} 对象:

+ +
function closeVideoCall() {
+  var remoteVideo = document.getElementById("received_video");
+  var localVideo = document.getElementById("local_video");
+
+  if (myPeerConnection) {
+    myPeerConnection.ontrack = null;
+    myPeerConnection.onremovetrack = null;
+    myPeerConnection.onremovestream = null;
+    myPeerConnection.onicecandidate = null;
+    myPeerConnection.oniceconnectionstatechange = null;
+    myPeerConnection.onsignalingstatechange = null;
+    myPeerConnection.onicegatheringstatechange = null;
+    myPeerConnection.onnegotiationneeded = null;
+
+    if (remoteVideo.srcObject) {
+      remoteVideo.srcObject.getTracks().forEach(track => track.stop());
+    }
+
+    if (localVideo.srcObject) {
+      localVideo.srcObject.getTracks().forEach(track => track.stop());
+    }
+
+    myPeerConnection.close();
+    myPeerConnection = null;
+  }
+
+  remoteVideo.removeAttribute("src");
+  remoteVideo.removeAttribute("srcObject");
+  localVideo.removeAttribute("src");
+  remoteVideo.removeAttribute("srcObject");
+
+  document.getElementById("hangup-button").disabled = true;
+  targetUsername = null;
+}
+ +

在引用了两个 {{HTMLElement("video")}} 元素之后,我们检查WebRTC 连接是否仍然存在;如果存在,则继续断开并关闭调用:

+ +
    +
  1. 所有事件处理程序都将被删除。这可以防止在连接关闭过程中触发杂散事件处理程序,从而可能导致错误。
  2. +
  3. 对于远程视频流和本地视频流,我们对每个磁道进行迭代,调用{{domxref("MediaStreamTrack.stop()")}} 方法关闭每个磁道。
  4. +
  5. 通过调用{{domxref("RTCPeerConnection.close", "myPeerConnection.close()")}}.关闭 {{domxref("RTCPeerConnection")}} 。
  6. +
  7. 设置 myPeerConnection 为 null,确保我们的代码知道没有正在进行的调用;当用户单击用户列表中的名称时,这很有用。
  8. +
+ +

然后,对于传入和传出的{{HTMLElement("video")}}元素,我们使用它们的{{domxref("Element.removeAttribute", "removeAttribute()")}} 方法删除它们的 {{htmlattrxref("srcObject", "video")}}和{{htmlattrxref("src", "video")}} 属性。这就完成了流与视频元素的分离。

+ +

最后,我们在"Hang Up"按钮上将{{domxref("HTMLElement.disabled", "disabled")}}属性设置为 true,使其在没有调用的情况下不可点击;然后我们将targetUsername 设置为 null ,因为我们不再与任何人交谈。这允许用户呼叫另一个用户,或接收来电。

+ +

处理状态变更

+ +

还有许多其他事件可以设置监听器,用于通知代码各种状态更改。我们使用三种方法: {{event("iceconnectionstatechange")}},{{event("icegatheringstatechange")}},和 {{event("signalingstatechange")}}。

+ +
ICE 连接状态
+ +

事件{{event("iceconnectionstatechange")}}当连接状态更改时(例如,当从另一端终止调用时),由ICE层将事件发送到{{domxref("RTCPeerConnection")}} 。

+ +
function handleICEConnectionStateChangeEvent(event) {
+  switch(myPeerConnection.iceConnectionState) {
+    case "closed":
+    case "failed":
+    case "disconnected":
+      closeVideoCall();
+      break;
+  }
+}
+ +

这里,当ICE连接状态更改为"closed""failed",或者 "disconnected"时,我们将应用 closeVideoCall()函数。这将处理关闭我们的连接端,以便我们准备好重新开始或接受呼叫。

+ +
ICE 信令状态
+ +

同样,我们监听{{event("signalingstatechange")}}事件。如果信号状态变为 closed,我们同样关闭呼叫。

+ +
  myPeerConnection.onsignalingstatechange = function(event) {
+    switch(myPeerConnection.signalingState) {
+      case "closed":
+        closeVideoCall();
+        break;
+    }
+  };
+ +
+

注意:  closed的信令状态已被弃用,取而代之的是 closed{{domxref("RTCPeerConnection.iceConnectionState", "iceConnectionState")}}。我们在这里监听它以增加一点向后兼容性。

+
+ +
ICE 收集状态
+ +

{{event("icegatheringstatechange")}} 事件用于让你知道何时ICE候选收集进程状态发生更改。我们的示例并没有将其用于任何用途,但是为了调试的目的观察这些事件以及检测候选集合何时完成都是有用的。

+ +
function handleICEGatheringStateChangeEvent(event) {
+  // Our sample just logs information to console here,
+  // but you can do whatever you need.
+}
+
+ +

下一步

+ +

现在您可以在Glitch上尝试这个例子,以看到它的实际效果。打开两个设备上的Web控制台并查看记录的输出,尽管你在上面所示的代码中看不到它,但是服务器上(以及GitHub上)的代码有很多控制台输出,因此你可以看到信令和连接进程在工作。

+ +

另一个明显的改进是添加了一个“铃声”功能,这样一来,一个"用户X正在呼叫。你是否要应答?" 提示会首先出现,而不仅仅是请求用户允许使用相机和麦克风。

diff --git a/files/zh-cn/web/api/webrtc_api/simple_rtcdatachannel_sample/index.html b/files/zh-cn/web/api/webrtc_api/simple_rtcdatachannel_sample/index.html new file mode 100644 index 0000000000..d42b8eaea5 --- /dev/null +++ b/files/zh-cn/web/api/webrtc_api/simple_rtcdatachannel_sample/index.html @@ -0,0 +1,276 @@ +--- +title: RTCDataChannel 简单示例 +slug: Web/API/WebRTC_API/Simple_RTCDataChannel_sample +tags: + - WebRTC + - 建立 + - 数据通道 +translation_of: Web/API/WebRTC_API/Simple_RTCDataChannel_sample +--- +

{{WebRTCSidebar}}

+ +

{{domxref("RTCDataChannel")}} 接口是WebRTC API的一个功能,可以让您在两个对等体之间打开一个通道,您可以通过该通道发送和接收任意数据。 API有意地类似于WebSocket API,因此可以为每个API使用相同的编程模型。

+ +

在本示例中, 我们会在一个页面内建立 一条{{domxref("RTCDataChannel")}}链接 . 这个场景是为了演示如何链接两个Peer,实际场景并不常见。在本示例中解释了协商和建立链接的过程,定位和链接另外一台主机的场景在另外的一个示例中。 

+ +

The HTML

+ +

首先让我们看看我们需要的HTML代码HTML that's needed. 其实很简单,我们先有两个按钮用来链接和断开链接。

+ +
<button id="connectButton" name="connectButton" class="buttonleft">
+  Connect
+</button>
+<button id="disconnectButton" name="disconnectButton" class="buttonright" disabled>
+  Disconnect
+</button>
+ +

然后我们还有一个输入框,用来输入消息。一个按钮,来触发发送事件。这个 {{HTMLElement("div")}} 是给channel中第一个节点使用的。

+ +
  <div class="messagebox">
+    <label for="message">Enter a message:
+      <input type="text" name="message" id="message" placeholder="Message text"
+              inputmode="latin" size=60 maxlength=120 disabled>
+    </label>
+    <button id="sendButton" name="sendButton" class="buttonright" disabled>
+      Send
+    </button>
+  </div>
+ +

最后, 还有一个小DIV用来显示收到的内容. 这个 {{HTMLElement("div")}} 是给channel中第二个peer使用的。

+ +
<div class="messagebox" id="receivebox">
+  <p>Messages received:</p>
+</div>
+ +

The JavaScript code

+ +

你可以直接到look at the code itself on GitHub来看代码, 下面我们也会一步一步的解释。

+ +

WebRTC API 大量使用了{{jsxref("Promise")}}. 这样会让建立链接的过程变得简单;如果你还没有到ECMAScript 2015了解过Promise, 你应该先去看看. 另外本示例还使用了箭头语法arrow functions

+ +

启动

+ +

当脚本开始运行时, 我们对load事件挂接 {{event("load")}} 事件侦听, 因此一旦页面完全加载, startup() 函数将被调用。

+ +
function startup() {
+  connectButton = document.getElementById('connectButton');
+  disconnectButton = document.getElementById('disconnectButton');
+  sendButton = document.getElementById('sendButton');
+  messageInputBox = document.getElementById('message');
+  receiveBox = document.getElementById('receivebox');
+
+  // Set event listeners for user interface widgets
+
+  connectButton.addEventListener('click', connectPeers, false);
+  disconnectButton.addEventListener('click', disconnectPeers, false);
+  sendButton.addEventListener('click', sendMessage, false);
+}
+ +

上述逻辑一目了然. 我们拿到所有需要操作的页面元素引用, 之后对三个按钮设置事件侦听 {{domxref("EventListener", "event listeners")}} 。

+ +

建立连接

+ +

当用户点击 "Connect" 按钮,  connectPeers() 方法被调用。下面将逐一分析该方法中的细节。

+ +
+

注意: 尽管参与连接的两端都在同一页面,我们将启动连接的一端称为 "local" 端,另一端称为 "remote" 端。

+
+ +

建立本地节点

+ +
localConnection = new RTCPeerConnection();
+
+sendChannel = localConnection.createDataChannel("sendChannel");
+sendChannel.onopen = handleSendChannelStatusChange;
+sendChannel.onclose = handleSendChannelStatusChange;
+
+ +

第一步是建立该连接的 "local" 端,它是发起连接请求的一方。 下一步是通过调用{{domxref("RTCPeerConnection.createDataChannel()")}} 来创建 {{domxref("RTCDataChannel")}} 并设置事件侦听以监视该数据通道, 从而获知该通道的打开或关闭 (即获得该对等连接的通道打开或者关闭的时机)。

+ +

请务必记住该通道的每一端都拥有自己的 {{domxref("RTCDataChannel")}} 对象。

+ +

建立远程节点

+ +
remoteConnection = new RTCPeerConnection();
+remoteConnection.ondatachannel = receiveChannelCallback;
+ +

远程端的建立过程类似“local”端, 但它无需自己创建 {{domxref("RTCDataChannel")}} , 因为我们将通过上面建立的渠道进行连接。 我们创建对 {{event("datachannel")}} 的事件处理回调;数据通道打开时该逻辑将被执行, 该回调处理将接收到一个 RTCDataChannel 对象,此过程将在文章后面部分描述。

+ +

设立ICE 候选人

+ +

下一步为每个连接建立 ICE 候选侦听处理, 当连接的一方出现新的 ICE 候选时该侦听逻辑将被调用以告知连接的另一方此消息。

+ +
+

注意: 在现实场景,当参与连接的两节点运行于不同的上下文,建立连接的过程或稍微复杂些,每一次双方通过调用{{domxref("RTCPeerConnection.addIceCandidate()")}},提出连接方式的建议  (例如: UDP,、中继UDP 、 TCP之类的) , 双方来回往复直到达成一致。本文既然不涉及现实网络环境,因此我们假定双方接受首次连接建议。 

+
+ +
    localConnection.onicecandidate = e => !e.candidate
+        || remoteConnection.addIceCandidate(e.candidate)
+        .catch(handleAddCandidateError);
+
+    remoteConnection.onicecandidate = e => !e.candidate
+        || localConnection.addIceCandidate(e.candidate)
+        .catch(handleAddCandidateError);
+ +

我们配置每个 {{domxref("RTCPeerConnection")}} 对于事件 {{event("icecandidate")}} 建立事件处理。

+ +

启动连接尝试

+ +

建立节点连接的最后一项是创建一个连接offer.

+ +
    localConnection.createOffer()
+    .then(offer => localConnection.setLocalDescription(offer))
+    .then(() => remoteConnection.setRemoteDescription(localConnection.localDescription))
+    .then(() => remoteConnection.createAnswer())
+    .then(answer => remoteConnection.setLocalDescription(answer))
+    .then(() => localConnection.setRemoteDescription(remoteConnection.localDescription))
+    .catch(handleCreateDescriptionError);
+ +

逐行解读上面的代码:

+ +
    +
  1. 首先调用{{domxref("RTCPeerConnection.createOffer()")}} 方法创建 {{Glossary("SDP")}} (Session Description Protocol) 字节块用于描述我们期待建立的连接。该方法可选地接受一个描述连接限制的对象,例如连接是否必须支持音频、视频或者两者都支持。在我们的简单示例中,没有引入该限制。
  2. +
  3. 如果该offer成功建立, 我们将上述字节块传递给local连接的 {{domxref("RTCPeerConnection.setLocalDescription()")}} 方法。 用于配置local端的连接。
  4. +
  5. 下一步通过调用remoteConnection.{{domxref("RTCPeerConnection.setRemoteDescription()")}},告知remote节点上述描述,将local 节点连接到到远程 。  现在 remoteConnection 了解正在建立的连接。
  6. +
  7. 该是remote 节点回应的时刻了。remote 节点调用 {{domxref("RTCPeerConnection.createAnswer", "createAnswer()")}} 方法予以回应。 该方法生成一个SDP二进制块,用于描述 remote 节点愿意并且有能力建立的连接。 这样的连接配置是两端均可以支持可选项的结合。
  8. +
  9. 应答建立之后,通过调用{{domxref("RTCPeerConnection.setLocalDescription()")}}传入remoteConnection 。该调用完成了remote端连接的建立 (对于对端的remote 节点而言, 是它的local 端。 这种叙述容易使人困惑,但是看多了您就习惯了。
  10. +
  11. 最终,通过调用localConnection的{{domxref("RTCPeerConnection.setRemoteDescription()")}}方法,本地连接的远端描述被设置为指向remote节点。
  12. +
  13. catch() 调用一个用于处理任何异常的逻辑。
  14. +
+ +
+

注意: 再次申明,上述处理过程并非针对现实世界的实现,在正常环境下,建立连接的两端的机器,运行两块不同的代码,用于交互和协商连接过程。

+
+ +

对成功的对等连接的处理

+ +

当peer-to-peer连接的任何一方成功连接, 相应的 {{domxref("RTCPeerConnection")}}的{{event("icecandidate")}} 事件将被触发。 在事件的处理中可以执行任何需要的操作, 但在本例中,我们所需要做的只是更新用户界面。

+ +
  function handleLocalAddCandidateSuccess() {
+    connectButton.disabled = true;
+  }
+
+  function handleRemoteAddCandidateSuccess() {
+    disconnectButton.disabled = false;
+  }
+ +

当local节点连接成功时,禁用 "Connect" 按钮, 当remote节点连接时许用 "Disconnect" 按钮。

+ +

数据通道(data channel)的连接

+ +

{{domxref("RTCPeerConnection")}} 一旦open, 事件{{event("datachannel")}} 被发送到远端以完成打开数据通道的处理, 该事件触发 receiveChannelCallback() 方法,如下所示:

+ +
  function receiveChannelCallback(event) {
+    receiveChannel = event.channel;
+    receiveChannel.onmessage = handleReceiveMessage;
+    receiveChannel.onopen = handleReceiveChannelStatusChange;
+    receiveChannel.onclose = handleReceiveChannelStatusChange;
+  }
+ +

事件{{event("datachannel")}} 在它的channel属性中包括了:  对代表remote节点的 channel的{{domxref("RTCDataChannel")}} 的指向, 它保存了我们用以在该channel上对我们希望处理的事件建立的事件监听。 一旦侦听建立, 每当remote节点接收到数据 handleReceiveMessage() 方法将被调用, 每当通道的连接状态发生改变 handleReceiveChannelStatusChange() 方法将被调用, 因此通道完全打开或者关闭时我们都可以作出相应的相应。

+ +

对通道状态变化的处理

+ +

local节点和remote节点采用同样的方法处理表示通道连接状态变更的事件。

+ +

当local节点遭遇open 或者 close 事件, handleSendChannelStatusChange() 方法被调用:

+ +
  function handleSendChannelStatusChange(event) {
+    if (sendChannel) {
+      var state = sendChannel.readyState;
+
+      if (state === "open") {
+        messageInputBox.disabled = false;
+        messageInputBox.focus();
+        sendButton.disabled = false;
+        disconnectButton.disabled = false;
+        connectButton.disabled = true;
+      } else {
+        messageInputBox.disabled = true;
+        sendButton.disabled = true;
+        connectButton.disabled = false;
+        disconnectButton.disabled = true;
+      }
+    }
+  }
+ +

如果通道状态已经变更为 "open", 意味着我们已经完成了在两对等节点之间建立连接。 相应地用户界面根据状态更新,许用并将输入光标聚焦在text 输入框,以便用户可以立即输入要发送给对方的文本消息, 同时界面许用 "Send" 和 "Disconnect" 按钮(既然它们已经准备好了),禁用"Connect"按钮,既然在已经建立连接的情况下用不着它。

+ +

当连接状态变更为 "closed"时,界面执行相反的操作: 禁用文本输入框和 "Send" 按钮 , 许用"Connect" 按钮, 以便用户在需要时可以打开新的连接,禁用"Disconnect" 按钮,既然没有连接时用不着它。

+ +

另一方面,作为我们例子的remote 节点, 则无视这些状态改变事件, 仅仅是在控制台输出它们:

+ +
  function handleReceiveChannelStatusChange(event) {
+    if (receiveChannel) {
+      console.log("Receive channel's status has changed to " +
+                  receiveChannel.readyState);
+    }
+  }
+ +

handleReceiveChannelStatusChange() 方法接收到发生的事件,事件类型为 {{domxref("RTCDataChannelEvent")}}.

+ +

发送消息

+ +

当用户按下 "Send" 按钮,触发我们已建立的该按钮的 {{event("click")}} 事件处理逻辑,在处理逻辑中调用sendMessage() 方法。 该方法也足够简单:

+ +
  function sendMessage() {
+    var message = messageInputBox.value;
+    sendChannel.send(message);
+
+    messageInputBox.value = "";
+    messageInputBox.focus();
+  }
+ +

首先,待发送的消息文本从文本输入框的 {{htmlattrxref("value", "input")}}属性获得,之后该文本通过调用 {{domxref("RTCDataChannel.send", "sendChannel.send()")}}发送到remote节点。 都搞定了! 余下的只是些用户体验糖 ——清空并聚焦文本输入框,以便用户可以立即开始下一条消息的输入。

+ +

接收消息

+ +

当远程通道发生“message”事件时,我们的handleReceiveMessage()方法被调用来处理事件

+ +
  function handleReceiveMessage(event) {
+    var el = document.createElement("p");
+    var txtNode = document.createTextNode(event.data);
+
+    el.appendChild(txtNode);
+    receiveBox.appendChild(el);
+  }
+ +

该方法只是简单地注入了一些 {{Glossary("DOM")}}, 它创建了 {{HTMLElement("p")}} (paragraph) 元素, 然后创建了 {{domxref("Text")}} 用于显示从事件的data 属性拿到的消息文本。该text node作为子节点附加到receiveBox block,显示在浏览器窗口内容区。

+ +

断开节点

+ +

当用户点击"Disconnect" 按钮。在之前我们设置的按钮事件处理逻辑中disconnectPeers() 方法被调用。

+ +
  function disconnectPeers() {
+
+    // Close the RTCDataChannels if they're open.
+
+    sendChannel.close();
+    receiveChannel.close();
+
+    // Close the RTCPeerConnections
+
+    localConnection.close();
+    remoteConnection.close();
+
+    sendChannel = null;
+    receiveChannel = null;
+    localConnection = null;
+    remoteConnection = null;
+
+    // Update user interface elements
+
+    connectButton.disabled = false;
+    disconnectButton.disabled = true;
+    sendButton.disabled = true;
+
+    messageInputBox.value = "";
+    messageInputBox.disabled = true;
+  }
+
+ +

该方法首先关闭每个节点的{{domxref("RTCDataChannel")}},之后类似地关闭每个节点的 {{domxref("RTCPeerConnection")}}。将所有对它们的指向置为null 以避免意外的复用。 之后更新界面状态以符合目前已经不存在连接的事实。

+ +

下一步

+ +

You should try out this example and take a look at the webrtc-simple-datachannel source code, available on GitHub.

diff --git a/files/zh-cn/web/api/webrtc_api/taking_still_photos/index.html b/files/zh-cn/web/api/webrtc_api/taking_still_photos/index.html new file mode 100644 index 0000000000..c8fc957a8b --- /dev/null +++ b/files/zh-cn/web/api/webrtc_api/taking_still_photos/index.html @@ -0,0 +1,231 @@ +--- +title: Taking still photos with WebRTC +slug: Web/API/WebRTC_API/Taking_still_photos +tags: + - API + - Advanced + - Example + - Sample code + - Video + - WebRTC + - webcam +translation_of: Web/API/WebRTC_API/Taking_still_photos +--- +

{{WebRTCSidebar}}

+ +

本文介绍如何使用 WebRTC在 支持 WebRTC 的计算机或手机上访问摄像机,并用其拍照。 尝试一下这个示例然后继续阅读,了解它的工作原理。

+ +

WebRTC-based image capture app — on the left we have a video stream taken from a webcam and a take photo button, on the right we have the still image output from taking the photo

+ +

如果你喜欢,你也可以直接跳转到 GitHub 上的代码

+ +

HTML 标记

+ +

我们的 HTML 界面有两个主要的操作区域:流和捕获面板以及演示面板。 它们俩都在它们自己的 {{HTMLElement("div")}} 中并排呈现,以便于造型和控制。

+ +

左边的面板包含两个组件:一个 {{HTMLElement("video")}} 元素,它将接收来自 WebRTC 的流,以及用户点击捕获视频帧的 {{HTMLElement("button")}}。

+ +
  <div class="camera">
+    <video id="video">Video stream not available.</video>
+    <button id="startbutton">Take photo</button>
+  </div>
+ +

这很简单,当我们进入 JavaScript 代码时,我们将看到他们是如何紧密联系在一起的。

+ +

接下来,我们有一个 {{HTMLElement("canvas")}} 元素,捕获的帧被存储到其中,可能以某种方式进行操作,然后转换为输出图像文件。 通过使用样式 {{cssxref("display")}}:none 将画布保持隐藏,以避免画面的混乱 —— 用户不需要看到这个中间过程。

+ +

我们还有一个 {{HTMLElement("img")}} 元素,我们将涌起绘制图像——这是让用户看到的最终显示。

+ +
  <canvas id="canvas">
+  </canvas>
+  <div class="output">
+    <img id="photo" alt="The screen capture will appear in this box.">
+  </div>
+ +

这是所有相关的HTML。 其余的只是一些页面布局和提供一个返回页面链接的些许文本。

+ +

JavaScript 代码

+ +

现在来看看 JavaScript 代码。 我们将把它分解成几个小的部分,使其更容易解释。

+ +

初始化

+ +

我们首先将整个脚本包装在匿名函数中,以避免使用全局变量,然后设置我们将要使用的各种变量。

+ +
(function() {
+  var width = 320;    // We will scale the photo width to this
+  var height = 0;     // This will be computed based on the input stream
+
+  var streaming = false;
+
+  var video = null;
+  var canvas = null;
+  var photo = null;
+  var startbutton = null;
+ +

那些变量是:

+ +
+
width
+
无论输入视频的尺寸如何,我们将把所得到的图像缩放到 320 像素宽。
+
height
+
给定流的 width 和宽高比,计算出图像的输出高度。
+
streaming
+
指示当前是否有活动的视频流正在运行。
+
video
+
这将是页面加载完成后对 {{HTMLElement("video")}} 元素的引用。
+
canvas
+
这将是页面加载完成后对 {{HTMLElement("canvas")}} 元素的引用。
+
photo
+
这将在页面加载完成后引用 {{HTMLElement("img")}} 元素。
+
startbutton
+
这将引用用于触发捕获的 {{HTMLElement("button")}} 元素。 我们会在页面加载完成之后得到。
+
+ +

startup( )函数

+ +

当页面加载完成时,startup( ) 函数运行,由window.addEventListener( )提供。 此功能的作用是请求访问用户的网络摄像头,将输出<img>初始化为默认状态,并建立从相机接收每帧视频所需的事件侦听器,并在点击按钮捕获图像时作出反应 。

+ +

获取元素引用

+ +

首先,我们参考我们需要访问的主要内容。

+ +
  function startup() {
+    video = document.getElementById('video');
+    canvas = document.getElementById('canvas');
+    photo = document.getElementById('photo');
+    startbutton = document.getElementById('startbutton');
+ +

获取流媒体

+ +

接下来的任务是获取媒体流:

+ +
    navigator.mediaDevices.getUserMedia({ video: true, audio: false })
+    .then(function(stream) {
+        video.srcObject = stream;
+        video.play();
+    })
+    .catch(function(err) {
+        console.log("An error occured! " + err);
+    });
+
+ +

在这里,我们正在调用 {{domxref("MediaDevices.getUserMedia()")}}  并请求视频流(无音频)。 它返回一个 promise,我们给它附加成功和失败情况下的回调方法。

+ +

成功回调接收一个 stream 对象作为输入。它是新视频的 {{HTMLElement("video")}} 元素的源。

+ +

一旦流被链接到 {{HTMLElement("video")}} 元素,我们通过调用 HTMLMediaElement.play() 开始播放。

+ +

如果打开流失败,则调用失败回调函数。 在没有连接兼容的相机,或者用户拒绝访问时,则会发生这种情况。

+ +

监听视频开始播放

+ +

在 {{HTMLElement("video")}} 上调用 HTMLMediaElement.play() 之后,在视频流开始流动之前,有一段(希望简短)的时间段过去了。 为了避免在此之前一直阻塞,我们为 {{HTMLElement("video")}} 加上一个 {{event("canplay")}} 事件的监听器,当视频播放实际开始时会触发该事件。 那时,视频对象中的所有属性都已基于流的格式进行配置。

+ +
    video.addEventListener('canplay', function(ev){
+      if (!streaming) {
+        height = video.videoHeight / (video.videoWidth/width);
+
+        video.setAttribute('width', width);
+        video.setAttribute('height', height);
+        canvas.setAttribute('width', width);
+        canvas.setAttribute('height', height);
+        streaming = true;
+      }
+    }, false);
+ +

这个回调什么都不做,除非它是第一次被调用; 这是通过查看我们的流变量的值进行测试,这是第一次运行此方法时为false。

+ +

如果这是第一次运行,我们会根据视频的实际大小,video.videoWidth和要渲染视频宽度的宽度之间的大小差异来设置视频的高度。

+ +

最后,通过在每个元素的两个属性的每一个上调用Element.setAttribute()来设置视频和画布的宽度和高度,并根据需要设置宽度和高度。 最后,我们将流变量设置为true,以防止我们再次无意中运行此设置代码。

+ +

处理按钮上的点击

+ +

为了在每次用户单击启动按钮时捕获静态照片,我们需要向按钮添加一个事件侦听器,以便在发出点击事件时被调用:

+ +

+ +
    startbutton.addEventListener('click', function(ev){
+      takepicture();
+      ev.preventDefault();
+    }, false);
+ +

这个方法很简单:它只是调用我们的takepicture()函数,在从流中捕获一个帧的部分中定义,然后在接收的事件上调用Event.preventDefault(),以防止点击被多次处理。

+ +

包装startup()方法

+ +

startup()方法中只有两行代码:

+ +
    clearphoto();
+  }
+ +

这就是我们在Clearflow()方法中,我们将在下面的清理照片框中进行描述。

+ +

清除照片框

+ +

清除照片框包括创建一个图像,然后将其转换为可以显示最近捕获的帧的<img>元素使用的格式。 该代码如下所示:

+ +
  function clearphoto() {
+    var context = canvas.getContext('2d');
+    context.fillStyle = "#AAA";
+    context.fillRect(0, 0, canvas.width, canvas.height);
+
+    var data = canvas.toDataURL('image/png');
+    photo.setAttribute('src', data);
+  }
+ +

我们首先得到对我们用于屏幕外渲染的隐藏的<canvas>元素的引用。 接下来,我们将fillStyle设置为#AAA(相当浅灰色),并通过调用fillRect()来填充整个画布。

+ +

最后在此功能中,我们将画布转换为PNG图像,并调用photo.setAttribute()以使我们捕获的静止框显示图像。

+ +

从流中捕获帧

+ +

最后一个定义的功能是整个练习的重点:takepicture()函数,其捕获当前显示的视频帧的作业将其转换为PNG文件,并将其显示在捕获的帧框中。 代码如下所示:

+ +
  function takepicture() {
+    var context = canvas.getContext('2d');
+    if (width && height) {
+      canvas.width = width;
+      canvas.height = height;
+      context.drawImage(video, 0, 0, width, height);
+
+      var data = canvas.toDataURL('image/png');
+      photo.setAttribute('src', data);
+    } else {
+      clearphoto();
+    }
+  }
+ +

正如我们需要处理画布内容的情况一样,我们首先得到隐藏画布的2D绘图上下文。

+ +

然后,如果宽度和高度都是非零(意味着至少有潜在有效的图像数据),我们将画布的宽度和高度设置为与捕获帧的宽度和高度相匹配,然后调用drawImage()来绘制当前的 将视频帧放入上下文中,用帧图像填充整个画布。

+ +
+

注意:这可以利用HTMLVideoElement接口看起来像任何接受HTMLImageElement作为参数的API的HTMLImageElement,将视频的当前帧呈现为图像的内容。

+
+ +

一旦画布包含捕获的图像,我们通过调用它的HTMLCanvasElement.toDataURL()将它转换为PNG 格式; 最后,我们调用photo.setAttribute()来使我们捕获的静态框显示图像。

+ +

如果没有可用的有效图像(即宽度和高度均为0),则通过调用clearphoto()清除捕获帧框的内容。

+ +
// 加载完毕后开始运行
+    window.addEventListener("load", startup, false);
+})();
+ +

过滤器的乐趣

+ +


+ 由于我们通过从<video>元素中抓取帧来捕获用户网络摄像头的图像,因此我们可以非常轻松地将过滤器和有趣的效果应用于视频。 事实证明,使用过滤器属性应用于元素的任何CSS过滤器都会影响捕获的照片。 这些过滤器可以从简单(使图像黑白)到极限(高斯模糊和色调旋转)。

+ +

您可以使用例如Firefox开发人员工具的风格编辑器来播放此效果; 有关如何执行此操作的详细信息,请参阅编辑CSS过滤器。

+ +

另请参阅

+ + diff --git a/files/zh-cn/web/api/webrtc_api/webrtc_basics/index.html b/files/zh-cn/web/api/webrtc_api/webrtc_basics/index.html new file mode 100644 index 0000000000..5558186d7a --- /dev/null +++ b/files/zh-cn/web/api/webrtc_api/webrtc_basics/index.html @@ -0,0 +1,262 @@ +--- +title: WebRTC basics +slug: Web/API/WebRTC_API/WebRTC_basics +translation_of: Web/API/WebRTC_API/Signaling_and_video_calling +--- +

{{WebRTCSidebar}}

+ +
+

当你理解 WebRTC 架构之后, 你就可以阅读本篇文章了。本篇文章将带领你贯穿整个跨浏览器 RTC 应用的创建过程。到本章结束的时候,你就拥有了一个可以运行的点对点的数据通道和媒体通道。

+
+ +
+

本页的案例过期了! 不要再尝试他们了。

+
+ +

注意

+ +

由于近期 API 做了一些修改,一些比较老的案例需要修复,暂时不可用。

+ + + +

当前可以正常工作的的案例是:

+ + + +

正确的实现方式或许可以从标准中推断出来。

+ +

本页包含的其余的过期的信息,在 bugzilla

+ +

Shims

+ +

就像你想的那样,这样前卫的 API 肯定需要浏览器前缀才可以,然后再将它转换成通用的变量。

+ +
var RTCPeerConnection = window.mozRTCPeerConnection || window.webkitRTCPeerConnection;
+var IceCandidate = window.mozRTCIceCandidate || window.RTCIceCandidate;
+var SessionDescription = window.mozRTCSessionDescription || window.RTCSessionDescription;
+navigator.getUserMedia = navigator.getUserMedia || navigator.mozGetUserMedia || navigator.webkitGetUserMedia;
+ +

RTCPeerConnection

+ +

这是使用 peer 创建连接的起始点。它接收一个配置选项,其中配置了用来创建连接的 ICE 服务器信息。

+ +
var pc = new RTCPeerConnection(configuration);
+ +

RTCConfiguration

+ +

 {{domxref("RTCConfiguration")}} 对象包含了一些信息,这些信息是关于用来做 ICE 的 TURN 和 / 或 STUN 服务器的。这是用来确保大多数用户可以避免因 NAT 和防火墙导致的无法建立连接。

+ +
var configuration = {
+    iceServers: [
+        {urls: "stun:23.21.150.121"},
+        {urls: "stun:stun.l.google.com:19302"},
+        {urls: "turn:numb.viagenie.ca", credential: "webrtcdemo", username: "louis%40mozilla.com"}
+    ]
+}
+ +

Google 运行了一个我们可以使用的公共 STUN 服务器。我也在 http://numb.viagenie.ca/ 创建了一个免费的可以访问 TURN 服务器的账户。可能你也想这么做,你只要替换到你自己的许可证书就可以了。

+ + + +

ICECandidate

+ + + +

创建好 PeerConnection 并传入可用的 STUNTURN 之后,当 ICE 框架找到可以使用 Peer 创建连接的 “候选者”,会立即触发一个事件(onicecandidate)。这些 “候选者” 就是 ICECandidate, 而且他们会在 PeerConnection#onicecandidate 事件中执行一个回调函数。

+ +
pc.onicecandidate = function (e) {
+    // candidate exists in e.candidate
+    if (!e.candidate) return;
+    send("icecandidate", JSON.stringify(e.candidate));
+};
+ +

回调函数执行之后,我们必须使用信令通道 (signal channel) 将候选发送到其他的点。在 Chrome 中,ICE 框架一般会找到重复的候选者,所以,我一般只发送第一个,然后将处理函数删除。Firfox 中将候选者包含在了 Offer SDP 中。

+ +

Signal Channel

+ +

现在我们有了 ICE 候选,然后需要将它发送到我们的另一端,以让它知道如何跟我们建立连接。然而,现在有一个先有鸡还是先有蛋的问题。我们想要 PeerConnection 发送数据到另一端,但是在那之前我们需要先把元数据发送过去……

+ +

现在就是用到信令通道(Signal Channel)的时候了。它的任何一个数据传输方法都允许两个端点之间交换信息。在本文中,我们将使用 FireBase。因为它设置起来灰常简单,并且不需要任何主机或者服务器端的代码编写。

+ +

现在我们想象只有两个两个方法:send(), 它将使用一个键,然后将数据赋值给它;recv() ,当一个键有值的时候会调用一个处理函数。

+ +

数据库的结构就像下面这样:

+ +
{
+    "": {
+        "candidate:": …
+        "offer": …
+        "answer": …
+    }
+}
+ +

连接会被 roomId 分割,然后会保存四条信息:发起者 (oferer) 的 ICE 候选、应答者 (answerer) 的 ICE 候选、offer SDP 和 answer SDP.

+ +

Offer

+ +

Offer SDP 是用来向另一端描述期望格式(视频, 格式, 解编码, 加密, 解析度, 尺寸 等等)的元数据。

+ +

一次信息的交换需要从一端拿到 offer,然后另外一端接受这个 offer 然后返回一个 answer。

+ +
pc.createOffer(function (offer) {
+    pc.setLocalDescription(offer, function() {
+        send("offer", JSON.stringify(pc.localDescription));
+    }, errorHandler);
+}, errorHandler, options);
+ +

errorHandler

+ +

创建 offer 的过程如果出现问题,就会执行这个函数,并且将错误的详细信息作为第一个参数。

+ +
var errorHandler = function (err) {
+    console.error(err);
+};
+ +

options

+ +

Offer SDP 的选项.

+ +
var options = {
+    offerToReceiveAudio: true,
+    offerToReceiveVideo: true
+};
+ +

offerToReceiveAudio/Video 告诉另一端,你想接收视频还是音频。对于 DataChannel 来说,这些是不需要的。

+ +

offer 创建好之后,我们必须将本地 SDP 设置进去,然后通过信令通道将它发送到另一端,之久就静静地等待另一端的 Answer SDP 吧.

+ +

Answer

+ +

Answer SDP 跟 offer 是差不多的,只不过它是作为相应的。有点像接电话一样。我们只能在接收到 offer 的时候创建一次 Answer.

+ +
recv("offer", function (offer) {
+    offer = new SessionDescription(JSON.parse(offer))
+    pc.setRemoteDescription(offer);
+
+    pc.createAnswer(function (answer) {
+        pc.setLocalDescription(answer, function() {
+            send("answer", JSON.stringify(pc.localDescription));
+        }, errorHandler);
+    }, errorHandler);
+});
+ +

DataChannel

+ +

我将首先阐述如何将 PeerConnection 用在 DataChannels API 上,并且如何在 peers 之间传输任意数据。

+ +

注意:撰写这篇文章的时候,DataChannels 在 Chrome 和 Firefox 之间是无法互用的。Chrome 支持了一个类似的私有协议,这个协议将在不久被标准协议支持。

+ +
var channel = pc.createDataChannel(channelName, channelOptions);
+ +

offer 的创建者和 channel 的创建者应该是同一个 peer。 answer 的创建者将在 PeerConnection 的 ondatachannel 回调中接收到 这个 channel。你必须在创建了这个 offer 之后立即调用 createDataChannel()。

+ +

channelName

+ +

这个字符串会作为 channel 名字的标签。注意:确保你 Channel 的名字中没有空格,否则在 Chrome 中调用 createAnswer() 将会失败。

+ +

channelOptions

+ +
var channelOptions = {};
+ +

当前 Chrome 还不支持这些选项,所以你可以将这些选项留空。检查 RFC 以获取更多关于这些选项的信息。

+ +

Channel 事件和方法

+ +

onopen

+ +

当连接建立的时候会被执行。

+ +

onerror

+ +

连接创建出错时执行。第一个参数是一个 error 对象。

+ +
channel.onerror = function (err) {
+    console.error("Channel Error:", err);
+};
+ +

onmessage

+ +
channel.onmessage = function (e) {
+    console.log("Got message:", e.data);
+}
+ +

这是连接的核心。当你接收到消息的时候,这个方法会执行。第一个参数是一个包含了数据、接收时间和其它信息的 event 对象。

+ +

onclose

+ +

当连接关闭的时候执行。

+ +

绑定事件

+ +

如果你是这个 channel 的创建者(offerer), 你可以直接在通过 createChannel 创建的 DataChannel 上绑定事件。如果你是 answerer 你必须使用 PeerConnection 的 ondatachannel 回调去访问这个 channel。

+ +
pc.ondatachannel = function (e) {
+    e.channel.onmessage = function () { … };
+};
+ +

这个 channel 可以在传递到回调函数中的 event 对象中以 e.channel 的方式访问。

+ +

send()

+ +
channel.send("Hi Peer!");
+ +

这个方法允许你直接传递数据到 peer!令人惊奇。你必须传递确保传递的数据是 String, Blob, ArrayBuffer 或者 ArrayBufferView 中的一种类型,所以,确保字符串化对象。

+ +

close()

+ +

在连接应该中止的时候关闭 channel。推荐在页面卸载的时候调用。

+ +

Media

+ +

现在我们将会涉及到如何传递诸如音视频的媒体的话题。要显示音视频,你必须在文档中加入包含 autoplay 属性的  <video> 标签。

+ +

Get User Media

+ +
<video id="preview" autoplay></video>
+
+var video = document.getElementById("preview");
+navigator.getUserMedia(constraints, function (stream) {
+    video.src = URL.createObjectURL(stream);
+}, errorHandler);
+ +

constraints

+ +

约定你想从用户获取到的媒体类型。

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

如果你只想进行音频聊天,移除 video 成员。

+ +

errorHandler

+ +

当返回请求的媒体错误时被调用

+ +

Media Events and Methods

+ +

addStream

+ +

将从 getUserMedia 返回的流加入 PeerConnection。

+ +
pc.addStream(stream);
+ +

onaddstream

+ +
<video id="otherPeer" autoplay></video>
+
+var otherPeer = document.getElementById("otherPeer");
+pc.onaddstream = function (e) {
+    otherPeer.src = URL.createObjectURL(e.stream);
+};
+ +

当连接建立并且其它的 peer 通过 addStream 将流加入到了连接中时会被调用。你需要另外的 <video> 标签去显示其它 peer 的媒体。

+ +

第一个参数是包含了其它 peer 流媒体的 event 对象。

-- cgit v1.2.3-54-g00ecf