使用 channel messaging - Web API 接口参考

使用 channel messaging
====================

Channel Messaging API 可以让运行在不同浏览器上下文中的独立脚本，连接到同一份文档（比如：两个 IFrame, 或者主文档和一个 IFrame, 或者使用同一个 `SharedWorker` 的两份文档），并直接通信，通过每端一个 port 的双向频道（或管道）在两者之间传递消息。

在文本中，我们将探索这项技术的基本用法。

**备注：** 此特性在 Web Worker 中可用

用例
--

Channel messaging 在这样的场景中特别有用：假如你有一个社交站点，它在主界面中通过 IFrame 内嵌了来自其他站点的内容，比如游戏，通讯录或者一个音乐播放器，有着个性化的音乐选择。当这些内容作为独立的单元时，一切都是 OK 的，但是当你想在主站点和 IFrame, 或者在不同的 IFrame 中交互时，困难就出现了。举例来说，假如你想从主站点向通讯录里添加一个联系人；或者想从游戏里，把最高分加入到个人资料；又或者，希望从音频播放器里，添加新的背景音乐到游戏中？因为浏览器使用的安全模型，使用传统的 web 技术来做这些事并不容易。你必须去考虑不同的源之间彼此是否信任，以及如何传递消息。

换个角度说，Message Channels 可以提供一个安全的通道让你在不同的浏览器上下文间传递数据。

**备注：** 要了解更多信息和思考，规范的 Ports 作为 Web 上一个对象兼容模型的基础 章节值得一读。

简单的例子
-----

为了帮助你开始，我们在 Github 上传了一些 demo. 一开始可以先看我们的 channel messaging 基本示例（也可以在线运行），它展示了一个非常简单的消息传递，发生在页面和内嵌 `<iframe>` 之间。

然后，看看我们的 multimessaging 演示（在线运行），它展示了一个稍微复杂一点的例子，可以在主页面和 IFrame 之间发送多条消息。

本文中，我们重点说后面的这个例子。它看起来像是这样：

![](/zh-CN/docs/Web/API/Channel_Messaging_API/Using_channel_messaging/channel-messaging-demo.png)

创建 channel
----------

在例子的主页面，我们有一个简单的表单，内含一个文本输入框，用来输入要发送到 `<iframe>` 的消息。我们还有一个段落，我们在稍后将会用它来显示 `<iframe>` 回传回来的确认消息。

```js
var input = document.getElementById("message-input");
var output = document.getElementById("message-output");
var button = document.querySelector("button");
var iframe = document.querySelector("iframe");

var channel = new MessageChannel();
var port1 = channel.port1;

// 等待 iframe 加载
iframe.addEventListener("load", onLoad);

function onLoad() {
  // 监听按钮点击
  button.addEventListener("click", onClick);

  // 在 port1 监听消息
  port1.onmessage = onMessage;

  // 把 port2 传给 iframe
  iframe.contentWindow.postMessage("init", "\*", [channel.port2]);
}

// 当按钮点击时，在 port1 上发送一个消息
function onClick(e) {
  e.preventDefault();
  port1.postMessage(input.value);
}

// 处理 port1 收到的消息
function onMessage(e) {
  output.innerHTML = e.data;
  input.value = "";
}
```

我们从使用 `MessageChannel()` 构造函数创建了一个 message channel 开始。

当 IFrame 加载完成，我们在按钮上注册了`onclick` 事件处理函数，在 `MessageChannel.port1` 注册了 `onmessage` 事件处理函数。最后，我们使用 `window.postMessage` 方法把 `MessageChannel.port2` 传给 IFrame.

让我们来了解一下 `iframe.contentWindow.postMessage` 更多的工作细节。它接受三个参数：

1. 被发送的消息。对于一开始的 port 传递，这个消息可以是个空字符串，但是在例子里，我们传了 `'init'`.
2. 消息将被发送到的源（origin）。 `*` 意思是 "任何源".
3. 一个对象，它的所有权会被传给接受的浏览器上下文。在本例中，我们把 `MessageChannel.port2` 传给了 IFrame, 然后它就可以用于与主页面通信了。

当我们的按钮被点击时，我们阻止了默认的表单提交，然后把输入到输入框里的内容通过 `MessageChannel` 发送给 IFrame.

在 IFrame 里接收 port 和消息
---------------------

在 IFrame 里，我们有下面的 JavaScript:

```js
var list = document.querySelector("ul");
var port2;

// 监听初始的 port 传递消息
window.addEventListener("message", initPort);

// 设置传过来的 port
function initPort(e) {
  port2 = e.ports[0];
  port2.onmessage = onMessage;
}

// 处理 port2 收到的消息
function onMessage(e) {
  var listItem = document.createElement("li");
  listItem.textContent = e.data;
  list.appendChild(listItem);
  port2.postMessage('Message received by IFrame: "' + e.data + '"');
}
```

当收到从主页面通过 `window.postMessage` 方法传来的初始化消息时，我们运行 `initPort` 函数。它会保存传来的 port, 并且注册了一个 onmessage 事件处理器，每当有消息通过我们的 `MessageChannel` 传来时，它都会被调用。

当收到从主页面发来的消息时，我们创建一个列表项，并把它插入到这个无序列表中，然后把这个列表项的 `textContent` 设置为事件的 `data` 属性（里面包含真正的消息）。

然后，我们通过在初始化时传到 IFrame 的 `MessageChannel.port2` 上调用 `MessagePort.postMessage` (en-US) 来使用 message channel 发送一个确认消息给主页面。

在主页面中接收确认消息
-----------

回到主页面，我们来一起看看 onmessage 事件处理函数。

```js
// 处理 port1 上收到的消息
function onMessage(e) {
  output.innerHTML = e.data;
  input.value = "";
}
```

当收到从 IFrame 发来的确认消息，说明原始消息被成功接收时，它把确认消息输出到段落中，并清空输入框，为输入下一个要被发送的消息做准备。

规范
--

| Specification |
| --- |
| HTML Standard # message-channels |
| HTML Standard # message-ports |

浏览器兼容性
------

### api.MessageChannel

BCD tables only load in the browser with JavaScript enabled. Enable JavaScript to view data.

### api.MessagePort

BCD tables only load in the browser with JavaScript enabled. Enable JavaScript to view data.

参见
--

* Channel Messaging API
* Web Workers API
* Broadcast Channel API


URLSearchParams.has() - Web API 接口参考

URLSearchParams.has()
=====================

`URLSearchParams` 的 **`has()`** 方法返回一个布尔值，表示一个指定的键名对应的值是否存在。

语法
--
```

var hasName = URLSearchParams.has(name)
```

### 参数

键名

要查找的参数的键名。

### 返回值

一个 `Boolean`.

示例
--

```js
let url = new URL("https://example.com?foo=1&bar=2");
let params = new URLSearchParams(url.search.slice(1));

params.has("bar") === true; //true
```

规范
--

| Specification |
| --- |
| URL Standard # dom-urlsearchparams-has |

浏览器兼容性
------

BCD tables only load in the browser with JavaScript enabled. Enable JavaScript to view data.


URLSearchParams.get() - Web API 接口参考

URLSearchParams.get()
=====================

`URLSearchParams` 接口的**get()**方法返回第一个与搜索参数对应的值**。**

语法
--
```

URLSearchParams.get(name)
```

### 参数

键名

将要返回的参数的键名。

### 返回值

返回一个 `USVString` ；如果没找到，返回 **`null`**.

示例
--

如果一个页面的 URL 是 `https://example.com/?name=Jonathan&age=18` ，你可以这样解析参数“name”和“age”:
```

let params = new URLSearchParams(document.location.search.substring(1));
let name = params.get("name"); // is the string "Jonathan"
let age = parseInt(params.get("age"), 10); // is the number 18
```

查找一个不存在的键名则返回 **`null`**:
```

let address = params.get("address"); // null
```

规范
--

| Specification |
| --- |
| URL Standard # dom-urlsearchparams-get |

浏览器兼容性
------

BCD tables only load in the browser with JavaScript enabled. Enable JavaScript to view data.


URLSearchParams.sort() - Web API 接口参考

URLSearchParams.sort()
======================

**`URLSearchParams.sort()`** 方法对包含在此对象中的所有键/值对进行排序，并返回 undefined。排序顺序是根据键的 Unicode 代码点。该方法使用稳定的排序算法 (即，将保留具有相等键的键/值对之间的相对顺序)。

Syntax
------
```

searchParams.sort();
```

### Return value

Returns `undefined`.

Example
-------

```js
// Create a test URLSearchParams object
var searchParams = new URLSearchParams("c=4&a=2&b=3&a=1");

// Sort the key/value pairs
searchParams.sort();

// Display the sorted query string
console.log(searchParams.toString());
```

The result is:
```

a=2&a=1&b=3&c=4
```

Specifications
--------------

| Specification |
| --- |
| URL Standard # dom-urlsearchparams-sort |

Browser compatibility
---------------------

BCD tables only load in the browser with JavaScript enabled. Enable JavaScript to view data.


URLSearchParams.delete() - Web API 接口参考

URLSearchParams.delete()
========================

**delete()** 是 `URLSearchParams` 接口的一个方法。可以删除指定名称的所有搜索参数。

语法
--
```

URLSearchParams.delete(name)
```

### 参数

name

需要删除的键值名称

### 返回

无

规范
--

| Specification |
| --- |
| URL Standard # dom-urlsearchparams-delete |

浏览器兼容性
------

BCD tables only load in the browser with JavaScript enabled. Enable JavaScript to view data.


URLSearchParams() - Web API 接口参考

URLSearchParams()
=================

**`URLSearchParams()`** 构造函数创建并返回一个新的 `URLSearchParams` 对象。

**备注：** 此特性在 Web Worker 中可用

语法
--

```js
new URLSearchParams()
new URLSearchParams(options)
```

### 参数

`options` 可选

可以是以下之一：

* 一个字符串，这个字符串从 `application/x-www-form-urlencoded` 的格式解析而来。开头的 `'?'` 字符会被忽略。
* 一系列基于字面量的字符串键值对，或者任何对象（例如 `FormData` 对象），能提供一系列字符串对的迭代器对象。需要注意，`File` 将被序列化为 `[object File]`，而不是它们的文件名（就像 `application/x-www-form-urlencoded` 格式中的那样）。
* 一个由字符串键和字符串值组成的键值对对象。请注意，不支持嵌套。

### 返回值

一个 `URLSearchParams` 实例。

示例
--

下面的例子展示了用各种传入的参数来创建一个 `URLSearchParams` 对象。

```js
// Pass in a string literal
var url = new URL("https://example.com?foo=1&bar=2");
// Retrieve from window.location
var url2 = new URL(window.location);

// Retrieve params via url.search, passed into ctor
var params = new URLSearchParams(url.search);
var params2 = new URLSearchParams(url2.search);

// Pass in a sequence
var params3 = new URLSearchParams([
  ["foo", 1],
  ["bar", 2],
]);

// Pass in a record
var params4 = new URLSearchParams({ foo: 1, bar: 2 });
```

这个示例展示了如何使用来自一个带有查询参数（search parameter）的 URL 构建的查询参数对象创建一个新的 URL。

```js
const url = new URL("https://example.com/?a=hello&b=world");

console.log(url.href);
// https://example.com/?a=hello&b=world

console.log(url.origin);
// https://example.com

const add_params = {
  c: "a",
  d: new String(2),
  e: false.toString(),
};

const new_params = new URLSearchParams([
  ...Array.from(url.searchParams.entries()), // [["a","hello"],["b","world"]]
  ...Object.entries(add_params), // [["c","a"],["d","2"],["e","false"]]
]).toString();
console.log(new_params);
// a=hello&b=world&c=a&d=2&e=false

const new_url = new URL(`${url.origin}${url.pathname}?${new\_params}`);

console.log(new_url.href);
// https://example.com/?a=hello&b=world&c=a&d=2&e=false

// Here it is as a function that accepts (URL, Record<string, string>)
const addSearchParams = (url, params = {}) =>
  new URL(
    `${url.origin}${url.pathname}?${new URLSearchParams([
 ...Array.from(url.searchParams.entries()),
 ...Object.entries(params),
 ])}`,
  );
```

规范
--

| Specification |
| --- |
| URL Standard # dom-urlsearchparams-urlsearchparams |

浏览器兼容性
------

BCD tables only load in the browser with JavaScript enabled. Enable JavaScript to view data.


URLSearchParams.forEach() - Web API 接口参考

URLSearchParams.forEach()
=========================

URLSearchParams 的实例对象上的方法 forEach 允许通过回调函数来遍历 URLSearchParams 实例对象上的键值对

**备注：** 此特性在 Web Worker 中可用

语法
--
```

searchParams.forEach(callback(value,key,searchParams));
```

### 参数

回调函数

该回调函数可以接收到 3 个参数 value,key,searchParams，我们可以在回调函数中对接收到的参数进行处理。而三个参数的含义如下：

`value`

当前遍历到的键值

`key`

当前遍历到的键名

`searchParams`

当前调用 forEach 方法的实例对象

### 返回值

空

例子
--

```js
// 创建 URLSearchParams 对象的实例对象，用于测试
var searchParams = new URLSearchParams("key1=value1&key2=value2");

let returnValue = searchParams.forEach(function (value, key, searchParams) {
  // 打印值
  console.log(value, key, searchParams);
});

// 输出返回值
console.log(returnValue);
```

结果是：
```

value1 key1 当前调用 forEach 方法的实例对象 (也就是 searchParams)
value2 key2 当前调用 forEach 方法的实例对象 (也就是 searchParams)
undefined  // 即没有返回值
```

Specifications
--------------

| Specification |
| --- |
| URL Standard # dom-urlsearchparams-urlsearchparams |

Browser compatibility
---------------------

BCD tables only load in the browser with JavaScript enabled. Enable JavaScript to view data.

See also
--------

* The `URL` interface.


URLSearchParams.entries() - Web API 接口参考

URLSearchParams.entries()
=========================

**`URLSearchParams.entries()`** 方法返回一个`迭代器`，允许遍历该对象中包含的所有键/值对。迭代器返回键/值对，其顺序与它们在查询字符串中出现的顺序相同。每一组键和值都是字符串对象。

**备注：** 该方法在 Web Workers也是可用的。

语法
--
```

searchParams.entries();
```

### 返回值

返回一个 `iterator`.

例子
--

```js
// 创建一个测试用 URLSearchParams 对象
var searchParams = new URLSearchParams("key1=value1&key2=value2");

// 显示键/值对
for (var pair of searchParams.entries()) {
  console.log(pair[0] + ", " + pair[1]);
}
```

结果如下：
```

key1, value1
key2, value2
```

浏览器兼容性
------

BCD tables only load in the browser with JavaScript enabled. Enable JavaScript to view data.

参见
--

* 其他 URL 相关接口：`URL`, `URLUtils`.


URLSearchParams.toString() - Web API 接口参考

URLSearchParams.toString()
==========================

`URLSearchParams` 接口的 **`toString()`** 方法返回适用在 URL 中的查询字符串。

语法
--
```

URLSearchParams.toString()
```

### 返回值

字符串

实例
--

```js
let url = new URL("https://example.com?foo=1&bar=2");
let params = new URLSearchParams(url.search.slice(1));

//Add a second foo parameter.
params.append("foo", 4);
console.log(params.toString());
//Prints 'foo=1&bar=2&foo=4'.
```

规范
--

| Specification |
| --- |
| URL Standard # urlsearchparams-stringification-behavior |

浏览器兼容性
------

BCD tables only load in the browser with JavaScript enabled. Enable JavaScript to view data.

参考
--

* Other URL-related interfaces: `URL`, `URLUtils`.
* Google Developers: Easy URL manipulation with URLSearchParams


URLSearchParams.append() - Web API 接口参考

URLSearchParams.append()
========================

**append()** 是 `URLSearchParams` 接口的一个方法。可以插入一个新搜索参数。

语法
--
```

URLSearchParams.append(name, value)
```

### 参数

name

需要插入搜索参数的键名。

value

需要插入搜索参数的值。

### 返回

无

例子
--

```js
let url = new URL("https://example.com?foo=1&bar=2");
let params = new URLSearchParams(url.search.slice(1));

//添加第二个 foo 搜索参数。
params.append("foo", 4);
//查询字符串变成：'foo=1&bar=2&foo=4'
```

规范
--

| Specification |
| --- |
| URL Standard # dom-urlsearchparams-append |

浏览器兼容性
------

BCD tables only load in the browser with JavaScript enabled. Enable JavaScript to view data.

参见
--

* 其他 URL 相关的接口：`URL`, `URLUtils`.
* 谷歌开发者：简单 URL 与 URLSearchParams 操纵


URLSearchParams.values() - Web API 接口参考

URLSearchParams.values()
========================

URLSearchParams.values() 方法返回一个`iterator`，该遍历器允许遍历对象中包含的所有值。这些值都是`USVString`对象。

**备注：** 该方法在Web Workers中也可使用

语法
--
```

searchParams.values();
```

### 返回值

返回一个`iterator`.

例子
--

```js
// 创建一个测试用 URLSearchParams 对象
var searchParams = new URLSearchParams("key1=value1&key2=value2");

// 输出值
for (var value of searchParams.values()) {
  console.log(value);
}
```

结果如下：
```

value1
value2
```

浏览器兼容性
------

BCD tables only load in the browser with JavaScript enabled. Enable JavaScript to view data.

另请参考
----

* 其他 URL 相关接口：`URL`, `URLUtils`.


URLSearchParams.getAll() - Web API 接口参考

URLSearchParams.getAll()
========================

`URLSearchParams`接口的 **`getAll()`** 方法以数组的形式返回与指定搜索参数对应的所有值。

语法
--
```

URLSearchParams.getAll(name)
```

### 参数

name

要返回的参数的名称。

### 返回值

一个`USVString`数组。

例子
--
```

let url = new URL('https://example.com?foo=1&bar=2');
let params = new URLSearchParams(url.search.slice(1));

//为 foo 参数添加第二个值
params.append('foo', 4);

console.log(params.getAll('foo')) //输出 ["1","4"].
```

规范
--

| Specification |
| --- |
| URL Standard # dom-urlsearchparams-getall |

浏览器兼容性
------

BCD tables only load in the browser with JavaScript enabled. Enable JavaScript to view data.


URLSearchParams.keys() - Web API 接口参考

URLSearchParams.keys()
======================

URLSearchParams.keys() 返回一个`iterator`，遍历器允许遍历对象中包含的所有键。这些键都是字符串对象。

**备注：** 该方法在 Web Workers中也可使用

语法
--
```

searchParams.keys();
```

### 返回值

返回一个`iterator`.

例子
--

```js
// 建立一个测试用 URLSearchParams 对象
var searchParams = new URLSearchParams("key1=value1&key2=value2");

// 输出键值对
for (var key of searchParams.keys()) {
  console.log(key);
}
```

结果如下：
```

key1
key2
```

浏览器兼容性
------

BCD tables only load in the browser with JavaScript enabled. Enable JavaScript to view data.

另请参考
----

* 其他 URL 相关接口：`URL`, `URLUtils`.


URLSearchParams.set() - Web API 接口参考

URLSearchParams.set()
=====================

`URLSearchParams`接口的 **`set()`** 方法用于设置和搜索参数相关联的值。如果设置前已经存在匹配的值，该方法会删除多余的，如果将要设置的值不存在，则创建它。

语法
--
```

URLSearchParams.set(name, value)
```

### 参数

键名

将要设置的参数的健值名。

参数值

所要设置的参数值。

### 返回值

Void

例子
--

```js
let url = new URL("https://example.com?foo=1&bar=2");
let params = new URLSearchParams(url.search.slice(1));

//Add a third parameter.
params.set("baz", 3);
```

Specifications
--------------

| Specification |
| --- |
| URL Standard # dom-urlsearchparams-set |

Browser compatibility
---------------------

BCD tables only load in the browser with JavaScript enabled. Enable JavaScript to view data.


HTMLSelectElement.add() - Web API 接口参考

HTMLSelectElement.add()
=======================

**`HTMLSelectElement.add()`** 方法用于向 `select` 元素的 `option` 元素集合中添加一个元素。

语法
--
```

collection.add(item[, before]);
```

### 参数

* *item* 是一个 `HTMLOptionElement` 或 `HTMLOptGroupElement`
* *before* 是可选的，是集合中的一个元素或者类型为 *long* 的一个索引，表示上面的 *item* 在此之前插入。如果这个参数是 `null`（或索引不存在），新元素会添加在集合的末尾。
### 异常

* 如果传入的 *item* 是 `HTMLSelectElement` (en-US) 的祖先元素，`HierarchyRequestError` 类型的 `DOMError` 会被抛出。
示例
--

### 从零开始创建元素

```js
var sel = document.createElement("select");
var opt1 = document.createElement("option");
var opt2 = document.createElement("option");

opt1.value = "1";
opt1.text = "Option: Value 1";

opt2.value = "2";
opt2.text = "Option: Value 2";

sel.add(opt1, null);
sel.add(opt2, null);

/\*
 概念上与下述代码相同：

 <select>
 <option value="1">Option: Value 1</option>
 <option value="2">Option: Value 2</option>
 </select>
\*/
```

before 参数是可选的，因此也可以这样写：

```js
...
sel.add(opt1);
sel.add(opt2);
...
```

### 添加到已存在集合的末尾

```js
var sel = document.getElementById("existingList");

var opt = document.createElement("option");
opt.value = "3";
opt.text = "Option: Value 3";

sel.add(opt, null);

/\*
 获取这个已存在的 select 对象：

 <select id="existingList">
 <option value="1">Option: Value 1</option>
 <option value="2">Option: Value 2</option>
 </select>

 将其变成这样：

 <select id="existingList">
 <option value="1">Option: Value 1</option>
 <option value="2">Option: Value 2</option>
 <option value="3">Option: Value 3</option>
 </select>
\*/
```

同样，before 参数是可选的，因此也可以这样写：

```js
...
sel.add(opt);
...
```

### 插入到已存在的集合中间

```js
var sel = document.getElementById("existingList");

var opt = document.createElement("option");
opt.value = "3";
opt.text = "Option: Value 3";

sel.add(opt, sel.options[1]);

/\*
 获取这个已存在的 select 对象：

 <select id="existingList">
 <option value="1">Option: Value 1</option>
 <option value="2">Option: Value 2</option>
 </select>

 将其变成这样：

 <select id="existingList">
 <option value="1">Option: Value 1</option>
 <option value="3">Option: Value 3</option>
 <option value="2">Option: Value 2</option>
 </select>
\*/
```

规范
--

| Specification |
| --- |
| HTML Standard # dom-select-add-dev |

浏览器兼容性
------

BCD tables only load in the browser with JavaScript enabled. Enable JavaScript to view data.


HTMLSelectElement.setCustomValidity() - Web API 接口参考

HTMLSelectElement.setCustomValidity()
=====================================

Baseline 2023
-------------

Newly availableSince March 2023, this feature works across the latest devices and browser versions. This feature might not work in older devices or browsers.

* Learn more
* See full compatibility
* Report feedback
**`HTMLSelectElement.setCustomValidity()`** 方法设置指定的验证消息为选择元素的自定义验证消息。

使用空字符串来表示该元素没有自定义的验证错误。

语法
--

```js
selectElt.setCustomValidity(string);
```

### Parameters

* *string* 将作为错误信息包含到 `DOMString` 之中。
Specifications
--------------

| Specification |
| --- |
| HTML Standard # dom-cva-setcustomvalidity-dev |

浏览器兼容性
------

BCD tables only load in the browser with JavaScript enabled. Enable JavaScript to view data.

查看更多
----

* Form validation. (en-US)


HTMLSelectElement.remove() - Web API 接口参考

HTMLSelectElement.remove()
==========================

**`HTMLSelectElement.remove()`** 方法从一个 select 元素中删除指定序数的 option 元素。没有传参时为删除当前元素本身：ChildNode.remove() (en-US)。

语法
--
```

collection.remove(index);
```

### 参数

* `index` 是从 option `HTMLOptionElement` 集合中要移除元素的序数。如果序数对应的元素不存在，调用这个方法就没有任何效果。
例子
--

```js
var sel = document.getElementById("existingList");
sel.remove(1);

/\*
 上面的代码会将下面的 select 元素结构：

 <select id="existingList" name="existingList">
 <option value="1">Option: Value 1</option>
 <option value="2">Option: Value 2</option>
 <option value="3">Option: Value 3</option>
 </select>

 变成这样：

 <select id="existingList" name="existingList">
 <option value="1">Option: Value 1</option>
 <option value="3">Option: Value 3</option>
 </select>
\*/
```

Specifications
--------------

| Specification |
| --- |
| HTML Standard # dom-select-remove |

浏览器兼容性
------

BCD tables only load in the browser with JavaScript enabled. Enable JavaScript to view data.

参考
--

* `ChildNode.remove` (en-US)，当没有参数时 `HTMLSelectElement` (en-US) 调用的是这个方法
* `HTMLSelectElement` (en-US) 实现了这个方法。


HTMLSelectElement.selectedIndex - Web API 接口参考

HTMLSelectElement.selectedIndex
===============================

**`HTMLSelectElement.selectedIndex`** 是一个长整型数，它反映了被选中的第一个 `<option>` 元素的索引值。值为 -1 时表明没有元素被选中。

语法
--
```

var index = selectElem.selectedIndex;
selectElem.selectedIndex = index;
```

示例
--

### HTML

```html
<p id="p">selectedIndex: 0</p>

<select id="select">
  <option selected>Option A</option>
  <option>Option B</option>
  <option>Option C</option>
  <option>Option D</option>
  <option>Option E</option>
</select>
```

### JavaScript

```js
var selectElem = document.getElementById("select");
var pElem = document.getElementById("p");

// 当有新的<option>元素被选中时
selectElem.addEventListener("change", function () {
  var index = selectElem.selectedIndex;
  // 把 index 数据添加到 p 元素中
  pElem.innerHTML = "selectedIndex: " + index;
});
```

规范
--

| Specification |
| --- |
| HTML Standard # dom-select-selectedindex-dev |

浏览器兼容性
------

BCD tables only load in the browser with JavaScript enabled. Enable JavaScript to view data.


HTMLSelectElement.checkValidity() - Web API 接口参考

HTMLSelectElement.checkValidity()
=================================

Baseline 2023
-------------

Newly availableSince March 2023, this feature works across the latest devices and browser versions. This feature might not work in older devices or browsers.

* Learn more
* See full compatibility
* Report feedback
**`HTMLSelectElement.checkValidity()`** 会检查元素是否有任何输入约束条件，并且检查值是否符合约束条件。如果值是不符合约束条件的，浏览器就会在该元素上触发一个可以撤销的 `invalid` 事件，然后返回 `false`.

Syntax
------

```js
var result = selectElt.checkValidity();
```

Specifications
--------------

| Specification |
| --- |
| HTML Standard # dom-cva-checkvalidity-dev |

Browser compatibility
---------------------

BCD tables only load in the browser with JavaScript enabled. Enable JavaScript to view data.

See also
--------

* Form validation. (en-US)


TextMetrics.width - Web API 接口参考

TextMetrics.width
=================

只读的 **`TextMetrics.width`** 属性，包含文本先前的宽度（行内盒子的宽度），使用 CSS 像素计算。

语法
--
```

readonly metrics.width;
```

示例
--

事先给定 `<canvas>` 元素：

```html
<canvas id="canvas"></canvas>
```

你可以使用下面的代码得到一个 `TextMetrics` 对象：

```js
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");

var text = ctx.measureText("foo"); // TextMetrics object
text.width; // 16;
```

规范
--

| Specification |
| --- |
| HTML Standard # dom-textmetrics-width-dev |

浏览器兼容性
------

BCD tables only load in the browser with JavaScript enabled. Enable JavaScript to view data.

参见
--

* `TextMetrics`


MediaQueryList.matches - Web API 接口参考

MediaQueryList.matches
======================

`MediaQueryList` 接口的 **`matches`** 只读属性是一个布尔值，如果 `document` 当前匹配媒体查询列表，该属性返回 `true`，否则返回 `false`。

通过侦听发生在 `MediaQueryList` 的 `change` 事件，当 `matches` 的值发生变化时，你可以收到通知。

值
-

一个布尔值，如果 `document` 目前匹配媒体查询列表，该值为 `true`，反之为 `false`。

示例
--

该示例通过使用 `orientation` 媒体查询特性来创建一个媒体查询，以检测视口横纵方向的变化：

```js
function addMQListener(mq, callback) {
  if (mq.addEventListener) {
    mq.addEventListener("change", callback);
  } else {
    mq.addListener(callback);
  }
}

addMQListener(window.matchMedia("(orientation:landscape)"), (event) => {
  if (event.matches) {
    /\* 现在是横向 \*/
  } else {
    /\* 现在是纵向 \*/
  }
});
```

规范
--

| Specification |
| --- |
| CSSOM View Module # dom-mediaquerylist-matches |

浏览器兼容性
------

BCD tables only load in the browser with JavaScript enabled. Enable JavaScript to view data.

参见
--

* 媒体查询
* 在代码中使用媒体查询
* `window.matchMedia()`
* `MediaQueryList`
* `MediaQueryListEvent`


MediaQueryList: change event - Web API 接口参考

MediaQueryList: change event
============================

当媒体查询的支持状况改变时，`MediaQueryList` 接口的 **`change`** 事件触发。

语法
--

在像 `addEventListener()` 这样的方法中使用该事件的名字，或者设置一个事件处理函数属性。

```js
addEventListener("change", (event) => {});

onchange = (event) => {};
```

事件类型
----

`MediaQueryListEvent` 类型。继承自 `Event`。

Event

MediaQueryListEvent事件属性
----

*`MediaQueryListEvent` 接口的属性继承自它的父接口，`Event`.*

`MediaQueryListEvent.matches` (en-US) 只读

 一个布尔值，如果 `document`
 当前匹配媒体查询列表，该值为 `true`，反之为 `false`。

`MediaQueryListEvent.media` (en-US) 只读

一个字符串，代表着一个序列化后的媒体查询。

示例
--

```js
var mql = window.matchMedia("(max-width: 600px)");

mql.onchange = (e) => {
  if (e.matches) {
    /\* 视口等于或小于 600 像素 \*/
    console.log("This is a narrow screen — less than 600px wide.");
  } else {
    /\* 视口大于 600 像素 \*/
    console.log("This is a wide screen — more than 600px wide.");
  }
};
```

规范
--

| Specification |
| --- |
| CSSOM View Module # dom-mediaquerylist-onchange |

浏览器兼容性
------

BCD tables only load in the browser with JavaScript enabled. Enable JavaScript to view data.

参见
--

* 媒体查询
* 在代码中使用媒体查询
* `window.matchMedia()`
* `MediaQueryList`
* `MediaQueryListEvent`


MediaQueryList.addListener() - Web API 接口参考

MediaQueryList.addListener()
============================

`MediaQueryList`接口的 **`addListener()`** 方法向`MediaQueryListener`添加一个侦听器，该侦听器将运行自定义回调函数以响应媒体查询状态的更改。

从本质上讲，这是`EventTarget.addEventListener()`的别名，用于向后兼容。较旧的浏览器应使用`addListener`而不是`addEventListener`，因为 MediaQueryList 仅从较新的浏览器中的 EventTarget 继承。

语法
--
```

MediaQueryList.addListener(func)
```

### 参数

func

表示你要在媒体查询状态更改时运行的回调函数的函数或函数引用。在原始实现中，回调是一个非标准的`MediaQueryListListener` (en-US)对象。在新的实现中，使用标准事件机制，回调是标准函数，事件对象是`MediaQueryListEvent`，它继承自`Event`。

### 返回值

Void.

例子
--

```js
var mql = window.matchMedia("(max-width: 600px)");

function screenTest(e) {
  if (e.matches) {
    /\* the viewport is 600 pixels wide or less \*/
    para.textContent = "This is a narrow screen — less than 600px wide.";
    document.body.style.backgroundColor = "red";
  } else {
    /\* the viewport is more than than 600 pixels wide \*/
    para.textContent = "This is a wide screen — more than 600px wide.";
    document.body.style.backgroundColor = "blue";
  }
}

mql.addListener(screenTest);
```

规范
--

| Specification |
| --- |
| CSSOM View Module # dom-mediaquerylist-addlistener |

浏览器兼容性
------

BCD tables only load in the browser with JavaScript enabled. Enable JavaScript to view data.

参见
--

* Media queries (en-US)
* Using media queries from code (en-US)
* `window.matchMedia()`
* `MediaQueryList`
* `MediaQueryListEvent`


ResizeObserverEntry.devicePixelContentBoxSize - Web API 接口参考

ResizeObserverEntry.devicePixelContentBoxSize
=============================================

`ResizeObserverEntry` 接口的只读属性 **`devicePixelContentBoxSize`** 在回调运行时，返回一个包含被监听元素设备像素大小的数组。

值
-

一个数组，包含被监听元素的设备像素大小的对象。该数组在支持多列的场景中出现多个部分的元素时是必要的。数组中的每个对象都包含以下两个属性：

`blockSize`

被监听的元素在块方向上的内容盒的设备像素长度。对于具有水平 `writing-mode` 的盒子，这是垂直尺寸或者高度；如果 writing-mode 是垂直的，这是水平的尺寸或者宽度。

`inlineSize`

被监听的元素在内联方向上内容盒的设备像素长度。对于具有水平 `writing-mode` 的盒子，这是水平尺寸或者宽度；如果 writing-mode 是垂直的，这是垂直的尺寸或者高度。

**备注：** 更多关于书写模式和块以及内联尺寸的解释，请阅读处理不同方向的文本。

示例
--

以下片段取自文章使用 devicePixelContentBox 完美地渲染。由于 `ResizeObserver` 的回调函数在绘制前和布局后调用。这提供了一个精确记录物理像素大小的机会，以确保将画布像素与物理像素一对一映射。

```js
const observer = new ResizeObserver((entries) => {
  const entry = entries.find((entry) => entry.target === canvas);
  canvas.width = entry.devicePixelContentBoxSize[0].inlineSize;
  canvas.height = entry.devicePixelContentBoxSize[0].blockSize;

  /\* … render to canvas … \*/
});
observer.observe(canvas, { box: "device-pixel-content-box" });
```

规范
--

| Specification |
| --- |
| Resize Observer # dom-resizeobserverentry-devicepixelcontentboxsize |

浏览器兼容性
------

BCD tables only load in the browser with JavaScript enabled. Enable JavaScript to view data.


ResizeObserverEntry.borderBoxSize - Web API 接口参考

ResizeObserverEntry.borderBoxSize
=================================

`ResizeObserverEntry` 接口的只读属性 **`borderBoxSize`** 在回调运行时，返回一个包含被监听元素的新的边框盒大小的数组。

值
-

一个数组，包含被监听元素的新边框盒大小的对象。该数组在支持多列的场景中出现多个部分的元素时是必要的。数组中的每个对象都包含以下两个属性：

`blockSize`

被监听的元素在块方向上的长度。对于具有水平 `writing-mode` 的盒子，这是垂直尺寸或者高度；如果 writing-mode 是垂直的，这是水平的尺寸或者宽度。

`inlineSize`

被监听的元素在内联方向上的长度。对于具有水平 `writing-mode` 的盒子，这是水平尺寸或者宽度；如果 writing-mode 是垂直的，这是垂直的尺寸或者高度。

**备注：** 更多关于书写模式和块以及内联尺寸的解释，请阅读处理不同方向的文本。

示例
--

```js
const resizeObserver = new ResizeObserver((entries) => {
  const calcBorderRadius = (size1, size2) =>
    `${Math.min(100, size1 / 10 + size2 / 10)}px`;

  for (const entry of entries) {
    if (entry.borderBoxSize?.length > 0) {
      entry.target.style.borderRadius = calcBorderRadius(
        entry.borderBoxSize[0].inlineSize,
        entry.borderBoxSize[0].blockSize,
      );
    } else {
      entry.target.style.borderRadius = calcBorderRadius(
        entry.contentRect.width,
        entry.contentRect.height,
      );
    }
  }
});

resizeObserver.observe(document.querySelector("div"));
```

规范
--

| Specification |
| --- |
| Resize Observer # dom-resizeobserverentry-borderboxsize |

浏览器兼容性
------

BCD tables only load in the browser with JavaScript enabled. Enable JavaScript to view data.


ResizeObserverEntry.contentBoxSize - Web API 接口参考

ResizeObserverEntry.contentBoxSize
==================================

`ResizeObserverEntry` 接口的只读属性 **`contentBoxSize`** 在回调运行时，返回一个包含被监听元素的内容盒大小的数组。

值
-

一个数组，包含被监听元素的新内容盒大小的对象。该数组在支持多列的场景中出现多个部分的元素时是必要的。数组中的每个对象都包含以下两个属性：

`blockSize`

被监听的元素在块方向上的内容盒的长度。对于具有水平 `writing-mode` 的盒子，这是垂直尺寸或者高度；如果 writing-mode 是垂直的，这是水平的尺寸或者宽度。

`inlineSize`

被监听的元素在内联方向上内容盒的长度。对于具有水平 `writing-mode` 的盒子，这是水平尺寸或者宽度；如果 writing-mode 是垂直的，这是垂直的尺寸或者高度。

**备注：** 更多关于书写模式和块以及内联尺寸的解释，请阅读处理不同方向的文本。

示例
--

 以下示例取自 resize-observer-border-radius.html
 （见源码）。这个示例包含绿色的盒子，大小按照视口大小的百分比变化。当视口的大小改变，盒子的圆角会和盒子的大小成比例的变化。我们可以使用百分比的形式来设置 `border-radius`，但是很快就得到了丑陋的椭圆形角；以下这个解决方案给你提供了漂亮的方形角，可以随盒子的大小缩放。

```js
const resizeObserver = new ResizeObserver((entries) => {
  const calcBorderRadius = (size1, size2) =>
    `${Math.min(100, size1 / 10 + size2 / 10)}px`;

  for (const entry of entries) {
    if (entry.borderBoxSize?.length > 0) {
      entry.target.style.borderRadius = calcBorderRadius(
        entry.borderBoxSize[0].inlineSize,
        entry.borderBoxSize[0].blockSize,
      );
    } else {
      entry.target.style.borderRadius = calcBorderRadius(
        entry.contentRect.width,
        entry.contentRect.height,
      );
    }
  }
});

resizeObserver.observe(document.querySelector("div"));
```

规范
--

| Specification |
| --- |
| Resize Observer # dom-resizeobserverentry-contentboxsize |

浏览器兼容性
------

BCD tables only load in the browser with JavaScript enabled. Enable JavaScript to view data.


ResizeObserverEntry.contentRect - Web API 接口参考

ResizeObserverEntry.contentRect
===============================

`ResizeObserverEntry` 接口的只读属性 `contentRect` 在回调运行时，返回一个包含被监听元素大小的 `DOMRectReadOnly` 对象。注意，该属性比 `ResizeObserverEntry.borderBoxSize` 或 `ResizeObserverEntry.contentBoxSize` 有着更好的支持，但是它是在 Resize Observer API 早期实现遗留下来的，出于对浏览器的兼容性原因，仍然保留在规范中，并且在未来的版本中可能被弃用。

值
-

一个 `DOMRectReadOnly` 对象，包含着 `target` 属性表示的元素的新的大小。

如果 `target` 是一个 HTML `元素`，返回的 `contentRect` 是元素的内容盒。如果 `target` 是 `SVGElement`，返回的 `contentRect` 是 SVG 的边界框。

示例
--

以下示例取自 resize-observer-text.html（见源码）。它使用了一个简单的功能检测测试来查看浏览器是否支持较新的 `ResizeObserverEntry.contentBoxSize` 属性——如果支持，则使用它来获取需要的尺寸数据。如果不支持，则使用 `contentRect`。

```js
const resizeObserver = new ResizeObserver((entries) => {
  for (const entry of entries) {
    if (entry.contentBoxSize) {
      h1Elem.style.fontSize = `${Math.max(
 1.5,
 entry.contentBoxSize.inlineSize / 200,
 )}rem`;
      pElem.style.fontSize = `${Math.max(
 1,
 entry.contentBoxSize.inlineSize / 600,
 )}rem`;
    } else {
      h1Elem.style.fontSize = `${Math.max(
 1.5,
 entry.contentRect.width / 200,
 )}rem`;
      pElem.style.fontSize = `${Math.max(1, entry.contentRect.width / 600)}rem`;
    }
  }
});

resizeObserver.observe(divElem);
```

规范
--

| Specification |
| --- |
| Resize Observer # dom-resizeobserverentry-contentrect |

浏览器兼容性
------

BCD tables only load in the browser with JavaScript enabled. Enable JavaScript to view data.


ResizeObserverEntry.target - Web API 接口参考

ResizeObserverEntry.target
==========================

`ResizeObserverEntry` 接口的只读属性 **`target`** 返回一个正在被监听的 `Element` 或 `SVGElement` 的引用。

值
-

一个正在被监听的 `Element` 或 `SVGElement` 元素。

示例
--

 以下示例取自 resize-observer-border-radius.html
 （见源码）。这个示例包含绿色的盒子，大小按照视口大小的百分比变化。当视口的大小改变，盒子的圆角会和盒子的大小成比例的变化。我们可以使用百分比的形式来设置 `border-radius`，但是很快就得到了丑陋的椭圆形角；以下这个解决方案给你提供了漂亮的方形角，可以随盒子的大小缩放。

为了获取到被监听元素的引用，以便我们在每次更改后可以更新它的 `border-radius` 值，我们可以使用每个条目的 `target` 属性——`entry.target.style.borderRadius`。

```js
const resizeObserver = new ResizeObserver((entries) => {
  for (const entry of entries) {
    if (entry.contentBoxSize) {
      entry.target.style.borderRadius = `${Math.min(
 100,
 entry.contentBoxSize.inlineSize / 10 +
 entry.contentBoxSize.blockSize / 10,
 )}px`;
    } else {
      entry.target.style.borderRadius = `${Math.min(
 100,
 entry.contentRect.width / 10 + entry.contentRect.height / 10,
 )}px`;
    }
  }
});

resizeObserver.observe(document.querySelector("div"));
```

规范
--

| Specification |
| --- |
| Resize Observer # dom-resizeobserverentry-target |

浏览器兼容性
------

BCD tables only load in the browser with JavaScript enabled. Enable JavaScript to view data.


使用 Web Speech API - Web API 接口参考

使用 Web Speech API
=================

Web Speech API 提供了两类不同方向的函数——语音识别和语音合成 (也被称为文本转为语音，英语简写是 tts)——开启了有趣的新可用性和控制机制。这篇文章提供了这两个方向的简单介绍，并且都带有例子。

Speech recognition
------------------

Speech recognition(语音识别) 涉及三个过程：首先，需要设备的麦克风接收这段语音；其次，speech recognition service(语音识别服务器) 会根据一系列语法 (基本上，语法是你希望在具体的应用中能够识别出来的词汇) 来检查这段语音；最后，当一个单词或者短语被成功识别后，结果会以文本字符串的形式返回 (结果可以有多个)，以及更多的行为可以设置被触发。

Web Speech API 有一个主要的控制接口——`SpeechRecognition` (en-US)，外加一些如表示语法、表示结果等等亲密相关的接口。通常，设备都有可使用的默认语音识别系统，大部分现代操作系统使用这个语音识别系统来处理语音命令，比如 Mac OS X 上的 Dictation，iOS 上的 Siri，Win10 上的 Cortana，Android Speech 等等。

Demo

为了简单展示 Web speech recognition 的作用，我们写了一个 demo——Speech color changer。点击屏幕之后，说出 HTML 颜色关键字 (网页里罗列的单词就是)，接下来应用的背景颜色就会变成你说的颜色。

![The UI of an app titled Speech Color changer. It invites the user to tap the screen and say a color, and then it turns the background of the app that colour. In this case it has turned the background red.](/zh-CN/docs/Web/API/Web_Speech_API/Using_the_Web_Speech_API/speech-color-changer.png)

为了跑这个 demo，可以 clone Github 仓库 (上面甩出的就是，或者directly download)，可以在支持的移动端浏览器 (比如 Chrome) 导航到 live demo URL 直接观看 (亲测 desktop browser 也是可以的，不过只能是 Chrome)，也可以通过 WebIDE 作为一个 app 加载到 Firefox OS(Firefox OS 使用 API 的权限问题见下文)。

### Browser support

对于 Web Speech API speech recognition(语音识别) 的支持，在各浏览器中还不成熟，还在发展，现在主要的限制如下：

* Firefox 桌面端和移动端在 Gecko 44+ 中都支持，并且是没有前缀的，它可以在`about:config` 中把 `media.webspeech.recognition.enable` 设置为 `true` 打开。权限设置/UI 还没有整理出来，所以权限还不能被用户使用，也就是不能用。不过很快会修复吧~
* Firefox OS 2.5+ 也支持，但作为一个特权 API(privileged API) 需要权限，因此你需要在manifest.webapp (也可以通过 WebIDE 下载，或者使应用得到验证后在 Firefox Marketplace 可使用) 如下设置：

```json
"permissions": {
  "audio-capture" : {
    "description" : "Audio capture"
  },
  "speech-recognition" : {
    "description" : "Speech recognition"
  }
}
```

```json
"type": "privileged"
```

* Chrome 桌面端和 Android 端自 version 33 以来均支持，但是带有前缀，所以你需要使用带有前缀的版本，比如：`webkitSpeechRecognition`
### HTML 和 CSS

对于这个应用来说，HTML 和 CSS 部分是无足轻重的。仅仅只有一个标题，一个介绍段落和一个 div 用来输出 check 的结果。

```html
<h1>Speech color changer</h1>
<p>Tap/click then say a color to change the background color of the app.</p>
<div>
  <p class="output"><em>...diagnostic messages</em></p>
</div>
```

CSS 也只是提供了简单的响应式样式，跨设备看上去也是 ok 的。

### JavaScript

JavaScript 部分会介绍更多细节。

#### 浏览器支持

之前有说到过，Chrome 现在支持的是带有前缀的 speech recognition，因此在 code 开始部分得加些内容保证在需要前缀的 Chrome 和不需要前缀的像 Firefox 中，使用的 object 都是正确的。

```js
var SpeechRecognition = SpeechRecognition || webkitSpeechRecognition;
var SpeechGrammarList = SpeechGrammarList || webkitSpeechGrammarList;
var SpeechRecognitionEvent =
  SpeechRecognitionEvent || webkitSpeechRecognitionEvent;
```

#### The grammar

这部分是我们的代码定义希望应用能够识别的语法。语法放在下面定义的变量`grammar`中：

```js
var colors = [ 'aqua' , 'azure' , 'beige', 'bisque', 'black', 'blue', 'brown', 'chocolate', 'coral' ... ];
var grammar = '#JSGF V1.0; grammar colors; public <color> = ' + colors.join(' | ') + ' ;'
```

语法格式使用的是 JSpeech Grammar Format（**JSGF**）——你可以在前面的链接中了解更多关于语法格式的规范。不过现在，让我们快速地浏览它：

* 每一行用分号分隔，和 js 中一样
* 第一行——`#JSGF V1.0`——说的是语法使用的格式和版本。这总是需要首先包括在内
* 第二行表示我们想要识别的`term` 的类型 (这里就是`colors`)。`public` 声明这是一条公共规则，尖括号中的字符串定义需要识别`term` 的名字 (这里就是`color`)，等号后面的是这个`term` 可以被识别和接受的具体值。得注意每一个值如何被一个管道符号分割开的
* 你可以按照上面的结构，在多行中，想定义多少就定义多少`terms` ，也可以包括相当复杂的语法定义。对于我们这个简单的 demo，就把语法定义的简单些

#### 将 grammer 插入 speech recognition

接下来是使用 `SpeechRecognition()` (en-US) 构造函数，定义一个 speech recognition 实例，控制对于这个应用的识别。还需要使用 `SpeechGrammarList()` (en-US) 构造函数，创立一个 speech grammer list 对象，包含我们的语法。

```js
var recognition = new SpeechRecognition();
var speechRecognitionList = new SpeechGrammarList();
```

使用 `SpeechGrammarList.addFromString()` (en-US) 把语法添加到列表 (list)，这个方法接收两个参数，第一个是我们想要添加的包含语法内容的字符串，第二个是对添加的这条语法的权重 (权重值范围是 0~1)，权重其实是相对于其他语法，这一条语法的重要程度。添加到列表的语法就是可用的，并且是一个`SpeechGrammar` 实例。

```js
speechRecognitionList.addFromString(grammar, 1);
```

我们然后通过设置 `SpeechRecognition.grammars` (en-US) 属性值，把我们的`SpeechGrammarList` (en-US) 添加到 speech recognition 实例。在继续往前走之前，我们还需要设置 recognition 实例其他的一些属性：

* `SpeechRecognition.lang` (en-US) ：设置识别的是什么语言。这个设定是良好的做好，因此墙裂推荐~
* `SpeechRecognition.interimResults` (en-US) ：定义 speech recognition 系统要不要返回临时结果 (interim results)，还是只返回最终结果。对于这个简单 demo，只返回最终结果就够了。
* `SpeechRecognition.maxAlternatives` (en-US) ：定义每次结果返回的可能匹配值的数量。这有时有用，比如要的结果不明确，你想要用一个列表展示所有可能值，让用户自己从中选择一个正确的。但这里这个简单 demo 就不用了，因此我们设置为 1(1 也就是默认值)。

```js
recognition.grammars = speechRecognitionList;
//recognition.continuous = false;
recognition.lang = "en-US";
recognition.interimResults = false;
recognition.maxAlternatives = 1;
```

**备注：** `SpeechRecognition.continuous` (en-US) 控制的是每一次允许多个结果被捕捉 (比如在这个 demo 中连着说两个颜色关键字，都可以被捕捉)，或者一次只能识别一个结果。代码中它被注释掉的原因是，在 Gecko 中它还不被支持，所以如果把它加进去会破坏这个应用。你可以在收到第一个结果后简单地停止识别，从而得到类似的结果，稍后将会看到。

#### 开始语音识别

在获取输出的`<div>` 和 html 元素引用之后 (这些我们可以用来待会输出语音识别诊断的结果，更新应用的背景颜色)，我们添加了一个`onclick` 事件处理，作用是当屏幕被点击后，语音识别服务将开启——这通过调用 `SpeechRecognition.start()` (en-US) 实现。`forEach()` 方法内部的工作是，为每个颜色关键字添加一个这个颜色的背景色，这样就直观知道了这个颜色关键字指向什么颜色。

```js
var diagnostic = document.querySelector(".output");
var bg = document.querySelector("html");
var hints = document.querySelector(".hints");

var colorHTML = "";
colors.forEach(function (v, i, a) {
  console.log(v, i);
  colorHTML += '<span style="background-color:' + v + ';"> ' + v + " </span>";
});
hints.innerHTML =
  "Tap/click then say a color to change the background color of the app. Try " +
  colorHTML +
  ".";

document.body.onclick = function () {
  recognition.start();
  console.log("Ready to receive a color command.");
};
```

#### 接收、处理结果

一旦语音识别开始，有许多 event handlers 可以用于做结果返回的后续操作，除了识别的结果外还有些零碎的相关信息可供操作 (可查看 `SpeechRecognition` event handlers list (en-US) )。最常见会使用的一个是 `SpeechRecognition.onresult` (en-US) ，这在收到一个成功的结果时候触发。

```js
recognition.onresult = function (event) {
  var last = event.results.length - 1;
  var color = event.results[last][0].transcript;
  diagnostic.textContent = "Result received: " + color + ".";
  bg.style.backgroundColor = color;
  console.log("Confidence: " + event.results[0][0].confidence);
};
```

代码中第三行看上去有一点复杂，让我们一步一步解释以下。`SpeechRecognitionEvent.results` (en-US) 属性返回的是一个`SpeechRecognitionResultList` (en-US) 对象 (这个对象会包含`SpeechRecognitionResult` (en-US) 对象们)，它有一个 getter，所以它包含的这些对象可以像一个数组被访问到——所以`[last]` 返回的是排在最后位置 (最新) 的`SpeechRecognitionResult`对象。每个`SpeechRecognitionResult` 对象包含的 `SpeechRecognitionAlternative` (en-US) 对象含有一个被识别的单词。这些`SpeechRecognitionResult` 对象也有一个 getter，所以`[0]` 返回的是其中包含的第一个`SpeechRecognitionAlternative` (en-US) 对象。最后返回的`transcript`属性就是被识别单词的字符串，把背景颜色设置为这个颜色，并在 UI 中报告出这个结果信息。

也使用了 `SpeechRecognition.onspeechend` (en-US) 这个 handle 停止语音识别服务 (具体工作在`SpeechRecognition.stop()` (en-US)) ，一旦一个单词被识别就不能再说咯 (必须再点击屏幕再次开启语音识别)

```js
recognition.onspeechend = function () {
  recognition.stop();
};
```

#### 处理 error 和未能识别的语音

最后两个 handlers 处理的两种情况，一种是你说的内容不在定义的语法中所以识别不了，另一种是出现了 error。

`SpeechRecognition.onnomatch` (en-US) 支持的就是第一种，但是得注意它似乎在 Firefox 或者 Chrome 中触发会有问题；它只是返回任何被识别的内容：

```js
recognition.onnomatch = function (event) {
  diagnostic.textContent = "I didnt recognise that color.";
};
```

`SpeechRecognition.onerror` (en-US) 处理的是第二种情况，识别成功了但是有 error 出现——`SpeechRecognitionError.error` (en-US) 属性包含的信息就是返回的确切的 error 是什么。

```js
recognition.onerror = function (event) {
  diagnostic.textContent = "Error occurred in recognition: " + event.error;
};
```

Speech synthesis
----------------

语音合成 (也被称作是文本转为语音，英语简写是 tts) 包括接收 app 中需要语音合成的文本，再在设备麦克风播放出来这两个过程。

Web Speech API 对此有一个主要控制接口——`SpeechSynthesis` ，外加一些处理如何表示要被合成的文本 (也被称为 utterances)，用什么声音来播出 utterances 等工作的相关接口。同样的，许多操作系统都有自己的某种语音合成系统，在这个任务中我们调用可用的 API 来使用语音合成系统。

### Demo

为了展示 Web 语音合成的简单使用，我们提供了一个例子——Speak easy synthesis 。例子是一套表单控件，包括输入需要被合成的文本，设置音调、语速和发出文本时需要的语音。在输入文本之后，按下`Enter`/`Return`键使它播放。

![UI of an app called speak easy synthesis. It has an input field in which to input text to be synthesised, slider controls to change the rate and pitch of the speech, and a drop down menu to choose between different voices.](/zh-CN/docs/Web/API/Web_Speech_API/Using_the_Web_Speech_API/speak-easy-synthesis.png)

想跑这个例子，你可以 git clone Github 仓库中的部分 (或者直接下载)，在桌面版支持的浏览器打开 index.html 文件，或者在移动端浏览器直接导向 live demo URL ，像 Chrome 和 Firefox OS。

### 浏览器支持

Web Speech API 语音合成部分在各浏览器中还是在发展，还不成熟，现在有以下几个限制点：

* Firefox 桌面版和移动版在 Gecko 42+(Windows)/44+ 支持，但是没有前缀，可以通过将`media.webspeech.synth.enabled`标志在`about:config`中转为`true`来启用。
* Firefox OS 2.5+ 支持，但是默认的，不需要任何权限。
* Chrome 桌面版和安卓版自 33 版以来都支持，但是没有前缀
### HTML 和 CSS

HTML 和 CSS 还是无足轻重，只是简单包含一个标题，一段介绍文字，以及一个表格带有一些简单控制功能。`<select>` 元素初始时空的，之后会用 js 使用`<option>` 填充。

```html
<h1>Speech synthesiser</h1>

<p>
  Enter some text in the input below and press return to hear it. change voices
  using the dropdown menu.
</p>

<form>
  <input type="text" class="txt" />
  <div>
    <label for="rate">Rate</label
 ><input type="range" min="0.5" max="2" value="1" step="0.1" id="rate" />
    <div class="rate-value">1</div>
    <div class="clearfix"></div>
  </div>
  <div>
    <label for="pitch">Pitch</label
 ><input type="range" min="0" max="2" value="1" step="0.1" id="pitch" />
    <div class="pitch-value">1</div>
    <div class="clearfix"></div>
  </div>
  <select></select>
</form>
```

### JavaScript

让我们看看 js 在这个 app 中的强大表现。

#### 设置变量

首先我们获得 UI 中涉及的 DOM 元素的引用，但更有趣的是，我们获得了`Window.speechSynthesis` (en-US) 的引用。这是 API 的入口点——它返回了`SpeechSynthesis` 的一个实例，对于 web 语音合成的控制接口。

```js
var synth = window.speechSynthesis;

var inputForm = document.querySelector("form");
var inputTxt = document.querySelector(".txt");
var voiceSelect = document.querySelector("select");

var pitch = document.querySelector("#pitch");
var pitchValue = document.querySelector(".pitch-value");
var rate = document.querySelector("#rate");
var rateValue = document.querySelector(".rate-value");

var voices = [];
```

#### 填充 select 元素

为使用设备可使用的不同的语音选项填充`<select>`元素，我们写了`populateVoiceList()` 方法。首先调用`SpeechSynthesis.getVoices()` ，这个函数返回包含所有可用语音 (`SpeechSynthesisVoice` (en-US)对象) 的列表。接下来循环这个列表，每次创建一个`<option>` 元素，设置它的文本内容以显示声音的名称（从`SpeechSynthesisVoice.name`获取），语音的语言（从`SpeechSynthesisVoice.lang`获取），如果某个语音是合成引擎默认的 (检查`SpeechSynthesisVoice.default` (en-US)属性返回`true`) 在文本内容后面添加`-- DEFAULT`。

对于每个`option`元素，我们也创建了`data-` 属性，属性值是语音的名字和语言，这样在之后我们可以轻松获取这个信息。之后把所有的`option`元素作为孩子添加到`select` 元素。

```js
function populateVoiceList() {
  voices = synth.getVoices();

  for (i = 0; i < voices.length; i++) {
    var option = document.createElement("option");
    option.textContent = voices[i].name + " (" + voices[i].lang + ")";

    if (voices[i].default) {
      option.textContent += " -- DEFAULT";
    }

    option.setAttribute("data-lang", voices[i].lang);
    option.setAttribute("data-name", voices[i].name);
    voiceSelect.appendChild(option);
  }
}
```

接下来是运行这个函数，我们做如下代码工作。因为 Firefox 不支持`SpeechSynthesis.onvoiceschanged` (en-US) ，所以很常规地只是返回语音对象列表当`SpeechSynthesis.getVoices()` 被触发。但是 Chrome 就有点不同了，在`SpeechSynthesis.getVoices()` 被触发时，先要等待事件触发 (有点绕~按照下面代码，`populateVoiceList` 函数在 Firefox 运行一次，在 Chrome 中运行两次)：

```js
populateVoiceList();
if (speechSynthesis.onvoiceschanged !== undefined) {
  speechSynthesis.onvoiceschanged = populateVoiceList;
}
```

#### 说出输入的文本

接下来我们创建一个事件处理器 (handler)，开始说出在文本框中输入的文本。我们把`onsubmit` 处理器挂在表单上，当`Enter/Return` 被按下，对应行为就会发生。我们首先通过构造函数创建一个新的`SpeechSynthesisUtterance()` (en-US) 实例——把文本输入框中的值作为参数传递。

接下来，我们需要弄清楚使用哪种语音。使用`HTMLSelectElement` (en-US) `selectedOptions` 属性返回当前选中的 `<option>` 元素。然后使用元素的`data-name`属性，找到 `SpeechSynthesisVoice` (en-US) 对象的`name`匹配`data-name` 的值。把匹配的语音对象设置为`SpeechSynthesisUtterance.voice` 的属性值。

最后，我们设置 `SpeechSynthesisUtterance.pitch` (en-US) 和`SpeechSynthesisUtterance.rate` (en-US) 属性值为对应范围表单元素中的值。哈哈所有准备工作就绪，调用 `SpeechSynthesis.speak()` (en-US) 开始说话。把 `SpeechSynthesisUtterance` (en-US) 实例作为参数传递。

```js
inputForm.onsubmit = function(event) {
  event.preventDefault();

  var utterThis = new SpeechSynthesisUtterance(inputTxt.value);
  var selectedOption = voiceSelect.selectedOptions[0].getAttribute('data-name');
  for(i = 0; i < voices.length ; i++) {
    if(voices[i].name === selectedOption) {
      utterThis.voice = voices[i];
    }
  }
  utterThis.pitch = pitch.value;
  utterThis.rate = rate.value;
  synth.speak(utterThis);
```

在事件处理器的最后部分，我们加入了一个 `SpeechSynthesisUtterance.onpause` (en-US) 处理器，来展示`SpeechSynthesisEvent` (en-US) 如何可以很好地使用。当 `SpeechSynthesis.pause()` (en-US) 被调用，这将返回一条消息，报告该语音暂停的字符编号和名称。

```js
utterThis.onpause = function (event) {
  var char = event.utterance.text.charAt(event.charIndex);
  console.log(
    "Speech paused at character " +
      event.charIndex +
      ' of "' +
      event.utterance.text +
      '", which is "' +
      char +
      '".',
  );
};
```

最后，我们在文本输入框添加了 blur() (en-US) 方法。这主要是在 Firefox 操作系统上隐藏键盘

```js
  inputTxt.blur();
}
```

#### Updating the displayed pitch and rate values

代码的最后部分，在每次滑动条移动时，简单地更新`pitch/rate`在 UI 中展示的值。

```js
pitch.onchange = function () {
  pitchValue.textContent = pitch.value;
};

rate.onchange = function () {
  rateValue.textContent = rate.value;
};
```


SourceBufferList.length - Web API 接口参考

SourceBufferList.length
=======================

**实验性:** **这是一项实验性技术**
在将其用于生产之前，请仔细检查浏览器兼容性表格。

`SourceBufferList` 接口的只读属性 **`length`** 返回列表中 `SourceBuffer` 对象的数量。

值
-

一个无符号长整型数值。

示例
--

待定

规范
--

| Specification |
| --- |
| Media Source Extensions™ # dom-sourcebufferlist-length |

浏览器兼容性
------

BCD tables only load in the browser with JavaScript enabled. Enable JavaScript to view data.

参见
--

* `MediaSource`
* `SourceBuffer`


IDBFactory.open - Web API 接口参考

IDBFactory.open
===============

**`IDBFactory.open`** 方法用于打开一个数据库连接。本方法立即返回一个 `IDBOpenDBRequest` (en-US) 对象，但打开数据库的操作是异步执行的。

连接数据库在一个单独的线程中进行，包括以下几个步骤：

1. 指定数据库已经存在时：
    * 等待 `versionchange` 操作完成。
    * 如果数据库已计划删除，那等着删除完成。
2. 如果已有数据库版本高于给定的 `version`，中止操作并返回类型为 `VersionError` 的 `DOMError`。
3. 如果已有数据库版本低于给定的 `version`，触发一个 `versionchange` 操作。
4. 如果数据库不存在，创建指定名称的数据库，将版本号设置为给定版本，如果给定版本号，则设置为 1，and no object stores.
5. 创建数据库连接。

如果操作成功执行，将触发 `success` 事件 on the request object that is returned from this method, with its `result` attribute set to the new `IDBDatabase` object for the connection.

If an error occurs while the database connection is being opened, then an error event is fired on the request object returned from this method.

Syntax
------

For the current standard:
```

 IDBOpenDBRequest open (DOMString name, [EnforceRange] optional unsigned long long version);
```

For the experimental version with `options` (see below):
```

IDBOpenDBRequest open (DOMString name, optional IDBOpenDBOptions options);
```

示例
--

For the current standard:

```js
var request = window.indexedDB.open("toDoList", 4);
```

For the experimental version with `options` (see below):

```js
var request = window.indexedDB.open("toDoList", {
  version: 4,
  storage: "temporary",
});
```

参数
--

name

数据库名称

version

指定数据库版本，当你想要更改数据库格式（比如增加对象存储，非增加记录），必须指定更高版本，通过 versionchange 来更改

options (version and storage)
非标准

In Gecko, since version 26, you can include an `options` object as a parameter of `IDBFactory.open` that contains the `version` number of the database, plus a storage value that specifies whether you want to use `permanent` (the default value) storage for the IndexedDB, or `temporary` storage (aka shared pool.) See Firefox bug 785884 for more details. This is a non-standard feature that we are looking to standardise sometime in the future.

**备注：** Data in temporary storage persists until the global limit for the pool is reached. The global limit calculation is relatively complex, but we are considering changing it (see Firefox bug 968272). When the global limit is reached, then data for the least recently used origin is deleted. There's also a group limit (eTLD+1 group/domain) which is currently 20% of the global limit. All requets that would exceed the group limit are just rejected.

返回
--

`IDBOpenDBRequest` (en-US)

The request object on which subsequent events related to this request are fired.

Exceptions
----------

This method may raise a `DOMException` with a DOMError (en-US) of the following types:

| Exception | 描述 |
| --- | --- |
| `TypeError` | The value of version is zero or a negative number or not a number. |

规范
--

| Specification |
| --- |
| Indexed Database API 3.0 # ref-for-dom-idbfactory-open② |

浏览器兼容性
------

BCD tables only load in the browser with JavaScript enabled. Enable JavaScript to view data.


HTMLDialogElement：close 事件 - Web API 接口参考

HTMLDialogElement：close 事件
==========================

Baseline 2022
-------------

Newly availableSince March 2022, this feature works across the latest devices and browser versions. This feature might not work in older devices or browsers.

* Learn more
* See full compatibility
* Report feedback
**`close`** 事件在对话框被关闭时，会在对应的 `HTMLDialogElement` 对象上被触发。

该事件不可取消，也不会冒泡。

语法
--

```js
target.onclose = functionRef;
```

### 参数

`functionRef` 是一个函数名称或函数表达式。该函数接受一个 `Event` 对象作为它唯一的参数。

每次只能给一个对象添加 `onclose` 回调。你可能更喜欢使用 `EventTarget.addEventListener()` 方法，因为它更加灵活。

规范
--

| Specification |
| --- |
| HTML Standard # event-close |

浏览器兼容性
------

BCD tables only load in the browser with JavaScript enabled. Enable JavaScript to view data.

参见
--

* HTML `<dialog>` 元素


信令与视频通话 - Web API 接口参考

信令与视频通话
=======

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

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

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

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

信令服务器
-----

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

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

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

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

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

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

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

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

```js
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()` 尾部修改。

```js
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）** ，根据 SDP 协议其中会包含一个 session 描述符，并且需要把这个发送到我们称之为\*\*接收者（callee）**那里，接受者需要返回一个包含描述符的**应答（answer）\*\*信息。我们的服务器使用 WebSocket 来传递 `"video-offer"` `"video-answer"` 两种类型的消息数据。这些消息包含以下属性：

`type`

消息类型; `"video-offer"` 或 `"video-answer"`

`name`

发送者用户名

`target`

接受者的用户名（如果呼叫者正在发送消息，则指定被呼叫者，反之亦然）

`sdp`

描述连接本地端 SDP（Session Description Protocol）协议字符串（从接收者的角度来看，它描述远程端）

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

### 交换 ICE 候选

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

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

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

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

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

`类型`

消息类型： `"new-ice-candidate"`.

`目标`

待建立联系人的用户名；服务器将仅会管理与该用户的信息。

`候选`

SDP 候选字符串，描述了计划的连接方法。通常不需要查看此字符串的内容。你需要做的所有代码都是使用信令服务器将其路由到远程对等机。

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

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

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

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

### 信令事务流程

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

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

* 在 Web 浏览器中运行的每个用户的客户端
* 每个用户的 Web 浏览器
* 信令服务器
* 承载聊天服务的 Web 服务器

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

![Diagram of the signaling process](/zh-CN/docs/Web/API/WebRTC_API/Signaling_and_video_calling/webrtc_-_signaling_diagram.svg)

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

### ICE 候选交换过程

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

![Diagram of ICE candidate exchange process](/zh-CN/docs/Web/API/WebRTC_API/Signaling_and_video_calling/webrtc_-_ice_candidate_exchange.svg)

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

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

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

客户端应用
-----

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

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

### 更新 HTML

我们客户端的 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>
```

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

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

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

### JavaScript 代码

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

#### 向信令服务器发送信息

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

```js
function sendToServer(msg) {
  var msgJSON = JSON.stringify(msg);

  connection.send(msgJSON);
}
```

通过调用`JSON.stringify()`，将传递到此方法的消息对象转换为 json 字符串，然后调用 WebSocket 连接的 `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);
  });
}
```

在获得对 `<ul>` 的引用（其中包含变量 `listElem`中的用户名列表）后，我们通过删除其每个子元素清空列表。

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

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

#### 开始一个通话

当用户单击要调用的用户名时，将调用 `invite()` 函数作为该事件的事件处理程序 `click` 事件：

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

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

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

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

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

我们通过设置元素的 `srcObject` 属性，将传入流附加到本地预览 `<video>` 元素。由于元素被配置为自动播放传入的视频，因此流开始在本地预览框中播放。

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

一旦媒体数据连接到 `RTCPeerConnection`，就会在连接处触发 `negotiationneeded` (en-US) 事件，以便启动 ICE 协商。

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

#### 处理 getUserMedia() 错误

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

```js
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()`函数关闭 `RTCPeerConnection`，并释放尝试调用过程中已分配的任何资源。此代码旨在安全地处理部分启动的调用。

#### 创建端到端连接

调用方和被调用方都使用 `createPeerConnection()` 函数来构造它们的 `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;
}
```

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

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

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

在创建了 `RTCPeerConnection` 之后，我们为对我们重要的事件设置了处理程序。

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

`RTCPeerConnection.onicecandidate`

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

`RTCPeerConnection.ontrack`

当向连接中添加磁道时，`track` 事件的此处理程序由本地 WebRTC 层调用。例如，可以将传入媒体连接到元素以显示它。详见接收新的流数据 。

`RTCPeerConnection.onnegotiationneeded` (en-US)

每当 WebRTC 基础结构需要你重新启动会话协商过程时，都会调用此函数。它的工作是创建和发送一个请求，给被叫方，要求它与我们联系。参见开始协商了解我们如何处理这一问题。

`RTCPeerConnection.onremovetrack`

调用与 `ontrack`相对应的对象来处理 `removetrack` 事件；当远程对等端从正在发送的媒体中删除磁道时，它将发送到`RTCPeerConnection`。参见处理流的移除 。

`RTCPeerConnection.oniceconnectionstatechange` (en-US)

ICE 层发送 `iceconnectionstatechange` 事件，让你了解 ICE 连接状态的更改。这可以帮助你了解连接何时失败或丢失。我们将在下面的ICE 连接状态中查看此示例的代码。

`RTCPeerConnection.onicegatheringstatechange` (en-US)

当 ICE 代理收集候选对象的过程从一个状态切换到另一个状态（例如开始收集候选对象或完成协商）时，ICE 层将向你发送事件（“ICegulatingStateChange”）事件。见下文 ICE 收集状态。

`RTCPeerConnection.onsignalingstatechange` (en-US)

当信令进程的状态更改时（或如果到信令服务器的连接更改时），WebRTC 架构将向你发送 `signalingstatechange` 消息。参见ICE 信令状态查看我们的代码。

#### 开始协商

一旦调用者创建了其 `RTCPeerConnection` ，创建了媒体流，并将其磁道添加到连接中，如 开始通话的交互 所示，浏览器将向 `RTCPeerConnection` 传递一个 `negotiationneeded` (en-US) 事件，以指示它已准备好开始与其他对等方协商。以下是我们处理 `negotiationneeded` (en-US) 事件的代码：

```js
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 候选。我们通过调用 `myPeerConnection.createOffer()` 创建此请求。

当 `createOffer()` 成功（执行 promise）时，我们将创建的请求信息传递到`myPeerConnection.setLocalDescription()` (en-US) ，它为调用方的连接端配置连接和媒体配置状态。

**备注：** 从技术上讲， `createOffer()` 返回的字符串是RFC 3264 请求。

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

`type`

消息类型：`"video-offer"`.

`name`

调用方的用户名。

`target`

被调用方的用户名

`sdp`

SDP 字符串描述了请求

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

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

#### 会话协商

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

##### 处理请求

当请求到达时，调用被调用方的 `handleVideoOfferMsg()` 函数时会收到`"video-offer"` 消息。这个函数需要做两件事。首先，它需要创建自己的 `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);
}
```

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

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

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

捕捉到的任何错误都会被传递给 `handleGetUserMediaError()`，详见 处理 getUserMedia() 错误 。

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

##### 发送 ICE 候选

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

你的 `onicecandidate` 处理程序接收一个事件，该事件的候选属性是描述该候选的 SDP（或为 `null` ，表示 ICE 层已耗尽建议的潜在配置）。候选的内容是你需要使用信令服务器传输的内容。下面是我们的示例实现：

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

这将构建一个包含候选对象的对象，然后使用前面在 向信令服务器发送信息 中描述的`sendToServer()` 函数将其发送给另一个对等方。消息属性为：

`type`

消息类型：`"new-ice-candidate"`.

`target`

ICE 候选需要传递到的用户名。这允许信令服务器路由消息。

`candidate`

代表 ICE 层想要传输给另一个对等体的候选体的 SDP。

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

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

##### 接收 ICE 候选

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

```js
function handleNewICECandidateMsg(msg) {
  var candidate = new RTCIceCandidate(msg.candidate);

  myPeerConnection.addIceCandidate(candidate).catch(reportError);
}
```

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

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

此外，如果发生什么事情导致流场景发生变化，协商将再次开始，将事件 `negotiationneeded` (en-US) 事件发送到`RTCPeerConnection`，整个过程将如前所述重新开始。这可能发生在各种情况下，包括：

* 网络状态的变化，如带宽变化、从 WiFi 过渡到蜂窝连接等。
* 在手机的前后摄像头之间切换。
* 流的配置更改，如分辨率或帧速率。

##### 接收新的流数据

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

```js
function handleAddStreamEvent(event) {
  document.getElementById("received\_video").srcObject = event.stream;
  document.getElementById("hangup-button").disabled = false;
}
```

传入流附加到 `"received_video"``<video>` 元素，并且启用"Hang Up" `<button>`元素，以便用户挂断呼叫。

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

##### 处理流的移除

当远程对等方通过调用`RTCPeerConnection.removeTrack()` (en-US).从连接中删除磁道时，你的代码将接收事件 `removetrack` (en-US) 事件。`"removetrack"` 的处理程序是：
```

function handleRemoveTrackEvent(event) {
  var stream = document.getElementById("received_video").srcObject;
  var trackList = stream.getTracks();

  if (trackList.length == 0) {
    closeVideoCall();
  }
}
```

此代码从`"received_video"` `<video>`元素的 `srcobject` 属性获取传入视频 `getTracks()` 方法获取流的磁道数组。

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

#### 结束通话

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

##### 挂机

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

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

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

##### 结束通话

`closeVideoCall()` 函数，如下所示，负责停止流、清理和处理 `RTCPeerConnection` 对象：

```js
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;
}
```

在引用了两个 `<video>` 元素之后，我们检查 WebRTC 连接是否仍然存在；如果存在，则继续断开并关闭调用：

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

然后，对于传入和传出的`<video>`元素，我们使用它们的`removeAttribute()` 方法删除它们的 `srcobject`和`src` 属性。这就完成了流与视频元素的分离。

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

#### 处理状态变更

还有许多其他事件可以设置监听器，用于通知代码各种状态更改。我们使用三种方法：`iceconnectionstatechange` (en-US)、`icegatheringstatechange` (en-US) 和 `signalingstatechange` (en-US)。

##### ICE 连接状态

事件 `iceconnectionstatechange` (en-US) 当连接状态更改时（例如，当从另一端终止调用时），由 ICE 层将事件发送到`RTCPeerConnection` 。

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

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

##### ICE 信令状态

同样，我们监听 `signalingstatechange` (en-US) 事件。如果信号状态变为 `closed`，我们同样关闭呼叫。

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

**备注：** `closed`的信令状态已被弃用，取而代之的是 `closed``iceConnectionState`。我们在这里监听它以增加一点向后兼容性。

##### ICE 收集状态

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

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

下一步
---

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

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


WebRTC 介绍 - Web API 接口参考

WebRTC 介绍
=========

**备注：** 此页面正在建设中，部分内容会移至其他页面，因为 WebRTC 指导资料已经建成。

WebRTC 允许你将任意数据，音频或视频（或其任何组合）的点对点通信构建到浏览器应用程序中。在本文中，我们将介绍一个 WebRTC 会话的生命周期，从建立连接到不再需要时关闭连接。

创建连接
----

互联网很大 很大。那么多年以前，聪明的人看到它有多大，增长速度有多快，32 位 IP 寻址系统的局限性，并意识到有些事情要做，所以他们开始设计一个新的 64 位寻址系统。但是他们意识到，完成转换需要更长时间才能持续 32 位地址，所以其他一些机智的人们想出了让多台计算机共享同一个 32 位 IP 地址的方法。网络地址转换（NAT）是通过操纵 LAN 上的所有设备的数据出入的路由来支持这种地址共享的标准，所有这些设备都共享同一个 WAN（全局）IP 地址。

用户的问题是互联网上的每台个人计算机不再一定具有唯一的 IP 地址，实际上，每个设备的 IP 地址也会变，不仅可能由于设备从一个网络移动到另一个网络，也可能被 NAT 和/或 DHCP 改变。对于尝试进行点对点网络的开发人员，这引入了一个难题：没有每个用户设备的唯一标识符，不可能立即自动地知道如何连接到 Internet 上的特定设备。即使你知道你想谈论的人，你不一定知道如何到达他们，甚至也不知道他们的地址。

这就像你尝试向你的朋友 Michelle 发送一个包裹，但你只在这个包裹上贴了一个写着“Michelle”的标签，而你并不知道她的地址。你得查到她的地址并将其包装在包中，要不然她会在想为什么你又忘记她的生日了。

这就是信令（Signaling）的由来。

### 信令

信令是在两个设备之间发送控制信息以确定通信协议、信道、媒体编解码器和格式以及数据传输方法以及任何所需的路由信息的过程。关于 WebRTC 的信令流程最重要的一点是：\*\*信令在规范中并没有定义。\*\*所以开发者需要自己决定如何实现这个过程。开发者可以为应用程序引擎选择任意的信息协议（如 SIP 或 XMPP），任意双向通信信道（如 WebSocket 或 XMLHttpRequest) 与持久连接服务器的 API（如Google Channel API）一起工作。

你可能会想，为什么这么一个对于建立 WebRTC 连接至关重要的基本过程居然没有定义在规范里？答案很简单：由于两个设备无法直接相互联系，规范无法预测 WebRTC 的所有可能用例，因此更明智的做法就是让开发人员们自己选择合适的网络技术和消息传递协议。

尤其是如果一个开发人员已经有了一个连接两个设备的方法，那也没有必要强迫他们就为了 WebRTC 使用另一个规范定义的方法。由于 WebRTC 没有生活在真空中，所以可能还有其他的连接，因此，如果可以使用已有的连接通道，就可以避免添加额外的连接通道。

为了交换信令信息，你可以选择通过 WebSocket 连接来回发送 JSON 对象，或者你可以在适当的通道（Channel）上使用 XMPP 或 SIP，或者你可以通过 HTTPS 使用 XMLHttpRequest 进行轮询或者其他任何你可以想出来的技术组合。你甚至可以使用电子邮件作为信号通道。

还值得注意的是，用于执行信令的信道甚至不需要通过网络。一个 Peer 可以输出一个数据对象，这个数据对象可以被打印出来，然后物理携带（步行或由信鸽）直到进入另一个设备，然后由该设备输出响应，并以同种方式返回，直到 WebRTC 对等连接打开。这将带来非常高的延迟，但也是可以做到的。

#### 信令期间交换的信息

信令期间需要交换的信息有三种基本类型：

* 控制消息：用于设置、打开、关闭通信通道并处理错误。
* 为了建立连接所需的信息：设备间能够彼此交谈所需的 IP 寻址和端口信息。
* 媒体能力协商：交互双方可以理解哪些编解码器和媒体数据格式？这些都需要在 WebRTC 会议开始之前达成一致。

只有在信令成功完成之后，打开 WebRTC 对等连接的过程才真正开始。

值得注意的是，在信令期间，信令服务器实际上不需要理解两个设备之间交换的数据或者对这些数据做任何处理。信令服务器本质上就是一个中继器：两端连接的公共点，两端都知道它们的信令数据可以通过这个点来传输。服务器不需要以任何方式对此信息做出反应。

#### 信令过程

为了开启一个 WebRTC 会话，以下事件需要依次发生：

1. 每个 Peer 创建一个 RTCPeerConnection 对象，用来表示其 WebRTC 会话端。
2. 每个 Peer 建立一个 icecandidate 事件的响应程序，用来在监测到该事件时将这些 candidates 通过信令通道发送给另一个 Peer。
3. 每个 Peer 建立一个 track 事件的响应程序，这个事件会在远程 Peer 添加一个 track 到其 stream 上时被触发。这个响应程序应将 tracks 和其消受者联系起来，例如`<video>`元素。
4. 呼叫者创建并与接收者共享一个唯一的标识符或某种令牌，使得它们之间的呼叫可以由信令服务器上的代码来识别。此标识符的确切内容和形式取决于你。
5. 每个 Peer 连接到一个约定的信令服务器，如 WebSocket 服务器，他们都知道如何与之交换消息。
6. 每个 Peer 告知信令服务器他们想加入同一 WebRTC 会话（由步骤 4 中建立的令牌标识）。
7. *描述、候选地址等——之后会有更多*
ICE 重连
------

有时，在 WebRTC 会话的整个生命周期内，网络条件会发生变化。其中一个用户可能会从蜂窝网络转移到 WiFi 网络，或者网络可能会变得拥塞。当这种情况发生时，ICE 代理可以选择执行 ICE 重连。在这个过程中网络连接会进行重新协商，与执行初始 ICE 协商的方式完全相同，除了：媒体继续流过原始网络连接直到新的开始运行。然后媒体转移到新的网络连接，旧的关闭。

不同的浏览器支持在不同情况下的进行 ICE 重连。例如，并不是所有的浏览器都会因为网络拥塞执行 ICE 重连。

ICE 重连有两个级别：**全 ICE 重连**会导致会话中的所有媒体流重新协商。 **部分 ICE 重连**允许 ICE 重新协商特定媒体流，而不是所有媒体流进行重新协商。然而，有些浏览器还不支持部分 ICE 重启。 <<<你如何触发每一个？>>>

如果你需要以某种方式更改连接的配置（例如更改为不同的 ICE 服务器集），你可以调用 RTCPeerConnection.setConfiguration() 设置新的 RTCConfiguration 字典，然后重新启动 ICE。

要明确触发 ICE 重新启动，只需通过调用 RTCPeerConnection.createOffer() 启动一个协商过程，同时指定 iceRestart 选项为 true。然后就像平时那样处理连接过程即可。

发送
--

### getUserMedia（获取用户媒体）

LocalMediaStream object

接收
--

WebRTC 在 Firefox 浏览器的偏好选择选项是隐藏的。可以到 about:config 这个页面设置 'media.navigator.enabled' 为 'true'。

**备注：** 在 Source tree 中有一些测试文件可以提供给你关于 WebRTC 如何工作的一个想法。具体例子请查看：dom/media/tests/local\_video\_test.html。你也可以尝试 服务器 demo ，源代码： server source。


RTCDataChannel 简单示例 - Web API 接口参考

RTCDataChannel 简单示例
===================

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

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

The HTML
--------

首先让我们看看我们需要的 HTML 代码。其实很简单，我们先有两个按钮用来链接和断开连接。

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

然后我们还有一个输入框，用来输入消息。一个按钮，来触发发送事件。这个 `<div>` 是给 channel 中第一个节点使用的。

```html
<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 用来显示收到的内容。这个 `<div>` 是给 channel 中第二个 peer 使用的。

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

The JavaScript code
-------------------

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

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

### 启动

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

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

上述逻辑一目了然。我们拿到所有需要操作的页面元素引用，之后对三个按钮设置事件侦听 `event listeners` 。

### 建立连接

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

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

#### 建立本地节点

```js
localConnection = new RTCPeerConnection();

sendChannel = localConnection.createDataChannel("sendChannel");
sendChannel.onopen = handleSendChannelStatusChange;
sendChannel.onclose = handleSendChannelStatusChange;
```

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

请务必记住该通道的每一端都拥有自己的 `RTCDataChannel` 对象。

#### 建立远程节点

```js
remoteConnection = new RTCPeerConnection();
remoteConnection.ondatachannel = receiveChannelCallback;
```

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

#### 设立 ICE 候选人

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

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

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

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

我们配置每个 `RTCPeerConnection` 对于事件 `icecandidate` 建立事件处理。

#### 启动连接尝试

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

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

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

#### 对成功的对等连接的处理

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

```js
function handleLocalAddCandidateSuccess() {
  connectButton.disabled = true;
}

function handleRemoteAddCandidateSuccess() {
  disconnectButton.disabled = false;
}
```

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

#### 数据通道（data channel）的连接

`RTCPeerConnection` 一旦 open，事件`datachannel` 被发送到远端以完成打开数据通道的处理，该事件触发 `receiveChannelCallback()` 方法，如下所示：

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

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

### 对通道状态变化的处理

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

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

```js
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 节点，则无视这些状态改变事件，仅仅是在控制台输出它们：

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

`handleReceiveChannelStatusChange()` 方法接收到发生的事件，事件类型为 `RTCDataChannelEvent` (en-US).

### 发送消息

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

```js
function sendMessage() {
  var message = messageInputBox.value;
  sendChannel.send(message);

  messageInputBox.value = "";
  messageInputBox.focus();
}
```

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

### 接收消息

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

```js
function handleReceiveMessage(event) {
  var el = document.createElement("p");
  var txtNode = document.createTextNode(event.data);

  el.appendChild(txtNode);
  receiveBox.appendChild(el);
}
```

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

### 断开节点

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

```js
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;
}
```

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

下一步
---

查看 GitHub 上提供的 webrtc-simple-datachannel 源代码。


WebRTC 协议介绍 - Web API 接口参考

WebRTC 协议介绍
===========

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

ICE
---

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

STUN
----

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

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

![An interaction between two users of a WebRTC application involving a STUN server.](/zh-CN/docs/Web/API/WebRTC_API/Protocols/webrtc-stun.png)

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.](/zh-CN/docs/Web/API/WebRTC_API/Protocols/webrtc-turn.png)

SDP
---

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

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

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

### 结构体

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

### 获取更多信息

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

* 规范：RFC 4566: SDP: Session Description Protocol
* IANA registry of SDP parameters


WebRTC connectivity - Web API 接口参考

WebRTC connectivity
===================

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

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

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

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

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

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

### 会话描述

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

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

然后，每个对等端保持两个描述：描述本身的**本地描述**和描述呼叫的远端的**远程描述**。

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

1. 呼叫者通过 `navigator.mediaDevices.getUserMedia()` (en-US) 捕捉本地媒体。
2. 呼叫者创建一个`RTCPeerConnection` 并调用 `RTCPeerConnection.addTrack()` (注： `addStream` 已经过时。)
3. 呼叫者调用 `RTCPeerConnection.createOffer()` 来创建一个提议 (offer).
4. 呼叫者调用 `RTCPeerConnection.setLocalDescription()` (en-US) 将提议 (Offer) 设置为本地描述 (即，连接的本地描述).
5. setLocalDescription() 之后，呼叫者请求 STUN 服务创建 ice 候选 (ice candidates)
6. 呼叫者通过信令服务器将提议 (offer) 传递至 本次呼叫的预期的接受者。
7. 接受者收到了提议 (offer) 并调用 `RTCPeerConnection.setRemoteDescription()` 将其记录为远程描述 (也就是连接的另一端的描述).
8. 接受者做一些可能需要的步骤结束本次呼叫：捕获本地媒体，然后通过`RTCPeerConnection.addTrack()`添加到连接中。
9. 接受者通过 `RTCPeerConnection.createAnswer()` (en-US) 创建一个应答。
10. 接受者调用 `RTCPeerConnection.setLocalDescription()` (en-US) 将应答 (answer) 设置为本地描述。此时，接受者已经获知连接双方的配置了。
11. 接受者通过信令服务器将应答传递到呼叫者。
12. 呼叫者接受到应答。
13. 呼叫者调用 `RTCPeerConnection.setRemoteDescription()` 将应答设定为远程描述。如此，呼叫者已经获知连接双方的配置了。
### 待定的和当前描述

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

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

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

当读取描述 ( `RTCPeerConnection.localDescription` (en-US) 和 `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.](/zh-CN/docs/Web/API/WebRTC_API/Connectivity/webrtc-complete-diagram.png)


HTMLCollection.length - Web API 接口参考

HTMLCollection.length
=====================

**`HTMLCollection.length`** 属性返回 `HTMLCollection` 中的元素数量。

值
-

一个正整数，表示 `HTMLCollection` 中元素的数量。

示例
--

`length` 属性在 DOM 编程中通常很有用。它通常用于测试集合的长度，去看它是否真的存在。它也通常用于 `for` 循环中的迭代器，如下示例。

```js
// All the elements with the class ".test" in the document
var items = document.getElementsByClassName("test");

// For each test item in the list,
// append the entire element as a string of HTML
var gross = "";
for (var i = 0; i < items.length; i++) {
  gross += items[i].innerHTML;
}

// gross is now all the HTML for the test elements
```

规范
--

| Specification |
| --- |
| DOM Standard # ref-for-dom-htmlcollection-length① |

浏览器兼容性
------

BCD tables only load in the browser with JavaScript enabled. Enable JavaScript to view data.


HTMLCollection.item - Web API 接口参考

HTMLCollection.item
===================

`HTMLCollection.item()` 由位置获取元素。

### 参数

index

想要被返回的 Node 的位置。元素在 HTML Collection 中的顺序和他们在源文档的顺序保持一致。

### 返回值

指定的 index 的`Node` , 如果 index 小于 0 或者不小于它的长度属性则返回 null。

Description
-----------

HTMLCollection 中 `item()` 方法返回一个编号的元素，在 JavaScript 中把 HTMLCollection 当成是一个是数组并用数组符号去索引十分简单。

Example
-------

```js
var c = document.images; // This is an HTMLCollection
var img0 = c.item(0); // You can use the item( ) method this way
var img1 = c[1]; // But this notation is easier and more common
```

See also
--------

* `NodeList.item()`


HTMLCollection.namedItem() - Web API 接口参考

HTMLCollection.namedItem()
==========================

`HTMLCollection` 接口的 **`namedItem()`** 方法返回第一个与集合中的 `id` 或者 `name` 属性匹配成功的 `Element`，如果没有匹配成功，则返回 `null`。

在 JavaScript 中，对一个 `String` 使用数组括号语法，例如 `collection["value"]` 等价于 `collection.namedItem("value")`。

语法
--

```js
const item = collection.namedItem(key);
```

### 参数

* `key` 是一个字符串，表示我们正在查找元素的 `id` 或者 `name` 属性。
### 返回值

* `item` 是 `HTMLCollection` 第一个与 *key* 匹配的 `Element`，如果没有，则返回 `null`。
示例
--

### HTML

```html
<div id="personal">
  <span name="title">Dr.</span>
  <span name="firstname">John</span>
  <span name="lastname">Doe</span>
  <span id="degree">(MD)</span>
</div>
```

### JavaScript

```js
const container = document.getElementById("personal");

// Returns the HTMLSpanElement with the name "title" if no such element exists null is returned
const titleSpan = container.children.namedItem("title");

// The following variants return undefined instead of null if there's no element with a matching name or id
const firstnameSpan = container.children["firstname"];
const lastnameSpan = container.children.lastname;

// Returns the span element with the id "degree"
const degreeSpan = container.children.namedItem("degree");

const output = document.createElement("div");
output.textContent = `Result: ${titleSpan.textContent} ${firstnameSpan.textContent} ${lastnameSpan.textContent} ${degreeSpan.textContent}`;

container.insertAdjacentElement("afterend", output);
```

规范
--

| Specification |
| --- |
| DOM Standard # dom-htmlcollection-nameditem-key |

浏览器兼容性
------

BCD tables only load in the browser with JavaScript enabled. Enable JavaScript to view data.


HTMLFormElement.elements - Web API 接口参考

HTMLFormElement.elements
========================

小结

**elements** 返回一个 `HTMLFormControlsCollection` (en-US) (`HTMLCollection`) 其中包含 FORM 的所有控件。需要注意的是，其中不包括 `type` 等于 `image` 的 `input` 元素。

你可以通过 name (en-US) 或 id来访问对应的控件。

语法
--
```

nodeList = HTMLFormElement.elements
```

实例
--

```js
var inputs = document.getElementById("form1").elements;
var inputByIndex = inputs[2];
var inputByName = inputs["login"];
```

规范
--

* HTML5, Section 4.10.3, The form Element
* DOM Level 2 HTML: elements


HTMLFormElement.action - Web API 接口参考

HTMLFormElement.action
======================

**`HTMLFormElement.action`** 这个 js 语句能够定义一个`<form>`元素中的 action 属性

表单的 action 属性是在服务器上提交表单，这个属性可以被检索或者设置。

语法
--
```

string = form.action
form.action = string
```

例子
--

form.action = "/cgi-bin/publish";

详细说明
----

HTML 5, Section 4.10.19.6, Form submission

DOM Level 2 HTML: action


HTMLFormElement.reportValidity() - Web API 接口参考

HTMLFormElement.reportValidity()
================================

Baseline 2023
-------------

Newly availableSince March 2023, this feature works across the latest devices and browser versions. This feature might not work in older devices or browsers.

* Learn more
* See full compatibility
* Report feedback
方法 **`HTMLFormElement.reportValidity()`** 返回布尔值，如果 form 表单的子表单控件满足验证限制条件，则该方法返回 `true` ，否则返回`false`。当返回 `false` 时，每个不合法的子控件的 `invalid` (en-US) 事件将会被触发，并且验证中存在的问题会报告该用户。

Syntax
------
```

HTMLFormElement.reportValidity()
```

### Return value

`布尔`Example
-------

```js
document.forms["myform"].addEventListener(
  "invalid",
  function () {
    // Optional response here
  },
  false,
);

document.forms["myform"].addEventListener(
  "submit",
  function () {
    document.forms["myform"].reportValidity();
  },
  false,
);
```

Specifications
--------------

| Specification |
| --- |
| HTML Standard # dom-cva-reportvalidity-dev |

Browser compatibility
---------------------

BCD tables only load in the browser with JavaScript enabled. Enable JavaScript to view data.


GlobalEventHandlers.onformdata - Web API 接口参考

GlobalEventHandlers.onformdata
==============================

`GlobalEventHandlers` 混入对象的属性 **`onformdata`** 是用于处理 `formdata` 事件的，它在整个列表展示所构建的表单数据之后被触发。触发会发生在表单发送时，但也可能由对某个`FormData()` 结构体的调用所触发。`onformdata` 在 `HTMLFormElement`上有效。

语法
--
```

target.onformdata = functionRef;
```

### Value

`functionRef` 是一个函数名或者称为 function expression. 此函数接受一个`FormDataEvent` (en-US)对象作为其唯一表达式参数。

示例
--

```js
// grab reference to form

const formElem = document.querySelector("form");

// submit handler

formElem.addEventListener("submit", (e) => {
  // on form submission, prevent default
  e.preventDefault();

  // construct a FormData object, which fires the formdata event
  new FormData(formElem);
});

// formdata handler to retrieve data

formElem.onformdata = (e) => {
  console.log("formdata fired");

  // Get the form data from the event object
  let data = e.formData;
  for (var value of data.values()) {
    console.log(value);
  }

  // submit the data via XHR
  var request = new XMLHttpRequest();
  request.open("POST", "/formHandler");
  request.send(data);
};
```

规范
--

| Specification |
| --- |
| HTML Standard # event-formdata |

浏览器兼容性
------

BCD tables only load in the browser with JavaScript enabled. Enable JavaScript to view data.

参见
--

* `formdata` 事件
* `FormDataEvent` (en-US)
* 使用 FormData 对象


HTMLFormElement: submit event - Web API 接口参考

HTMLFormElement: submit event
=============================

当表单 `<form>` 提交的时候触发 `submit` 事件。

|  |  |
| --- | --- |
| 是否冒泡 | 是（虽然其通常被看作不冒泡的简单事件） |
| 是否可取消 | 是 |
| 接口 | `SubmitEvent` (en-US) |
| 事件处理器属性 | `GlobalEventHandlers.onsubmit` |

注意 `submit` 事件只能作用于 `<form>` 元素本身，不能作用于 `<button>` 或者 <input type="submit">。但 `SubmitEvent` (en-US) 被用于表单提交时，会携带 `submitter` 属性，这是触发提交请求时会调用的按钮。

当用户点击提交按钮（`<button>` 或 <input type="submit">），亦或是在表单里输入时（e.g. <input type="text">）按下 `Enter` 键，`submit` 事件将会被触发。直接调用 `form.submit()` 方法时，事件不会发送到表单。

**备注：** 当试图提交一个未通过验证的表单时，会触发 `invalid` 事件。在这种情况下，表单提交将会被验证机制阻止。因此，`sumbit` 事件也就不存在了。

示例
--

这个示例使用 `EventTarget.addEventListener()` 去监听表单的提交。并记录当前的 `Event.timeStamp`，然后阻止提交表单的默认操作。

### HTML

```html
<form id="form">
  <label>Test field: <input type="text" /></label>
  <br /><br />
  <button type="submit">Submit form</button>
</form>
<p id="log"></p>
```

### JavaScript

```js
function logSubmit(event) {
  log.textContent = `Form Submitted! Time stamp: ${event.timeStamp}`;
  event.preventDefault();
}

const form = document.getElementById("form");
const log = document.getElementById("log");
form.addEventListener("submit", logSubmit);
```

### 结果

规范
--

| Specification |
| --- |
| HTML Standard # event-submit |
| HTML Standard # handler-onsubmit |

浏览器兼容性
------

BCD tables only load in the browser with JavaScript enabled. Enable JavaScript to view data.

参见
--

* HTML `<form>` 元素
* 相关事件：`invalid`


HTMLFormElement.enctype - Web API 接口参考

HTMLFormElement.enctype
=======================

**`HTMLFormElement.enctype`** 属性常用来指明提交表单的内容类型，可选的值如下：

* `application/x-www-form-urlencoded`: 初始的默认值
* `multipart/form-data`: 适用于使用`<input>` 标签上传文件
* `text/plain`: HTML5 引入的类型

这些值可以通过元素 `<button>` or `<input>` 的属性 form.enctype 来改写

语法
--
```

string = form.enctype
form.enctype = string
```

例子
--
```

form.enctype = "application/x-www-form-urlencoded";
```

参考
--

HTML 5, Section 4.10.19.6, Form submission

DOM Level 2 HTML: enctype


form.submit - Web API 接口参考

form.submit
===========

**`HTMLFormElement.submit()`** 用来提交表单 `<form>`。

这个方法和触发提交表单按钮很类似，但有所不同：

* 没有引发 `submit` 事件。即，表单的 `onsubmit` 事件处理程序不会运行。
* 不会触发约束验证 。

`HTMLFormElement.requestSubmit()` 方法与触发表单提交的 `<button>` 的效果是相同的。

如果一个表单控件（比如一个提交按钮）的 `name` 或 `id` 的值为 `submit`，则它将覆盖表单的 submit 方法。

使用 `HTMLFormElement.submit()` 方法时，属性 `type="submit"` 的 `<input>` 不会与表单一起被提交，但在使用原始的 HTML 表单提交时，它将被提交。

语法
--

```js
submit()
```

### 参数

无。

### 返回值

无（`undefined`）。

示例
--

```js
document.forms["myform"].submit();
```

规范
--

| Specification |
| --- |
| HTML Standard # dom-form-submit-dev |

浏览器兼容性
------

BCD tables only load in the browser with JavaScript enabled. Enable JavaScript to view data.


HTMLFormElement.requestSubmit() - Web API 接口参考

HTMLFormElement.requestSubmit()
===============================

`HTMLFormElement` 的 **`requestSubmit()`** 方法要求使用一个指定的提交按钮提交表单。

语法
--

```js
requestSubmit()
requestSubmit(submitter)
```

### 参数

`submitter` 可选

提交按钮，其属性描述了提交表单的方式。可以是 `type` 属性为 `submit` 的 `<input>` 或 `<button>` 元素。

如果你省略了 `submitter` 参数，表单元素自身会作为 submitter 使用。

### 返回值

无（`undefined`）。

### 异常

`TypeError`

如果指定的 `submitter` 不是一个提交按钮则抛出该错误。

`NotFoundError` `DOMException`

如果调用 `requestSubmit()` 时指定的 `submitter` 不是表单的成员则抛出该错误。submitter 必须是一个表单元素的后代或者必须具有引用表单的 `form` 属性。

使用说明
----

显而易见的问题是：我们一开始就有 `submit()` 方法，为什么还要有这个方法？

答案是显而易见的。`submit()` 仅仅是提交表单。而 `requestSubmit()` 的行为就像是点击了提交按钮一样。表单的内容会被验证并且表单仅在验证通过时提交。表单在被提交后，表单对象上会触发 `submit` 事件。

示例
--

在下面的示例中，如果可用，会尝试使用 `requestSubmit()` 发送请求提交表单。如果一个找到有 ID 为 `main-submit` 的提交按钮，则将其用于提交表单。否则，表单提交时不携带 `submitter` 参数，所以直接由表单自己提交。

另一方面，如果 `requestSubmit()` 不是可用的，这个代码回退到调用表单的 `submit()` 方法。

```js
let myForm = document.querySelector("form");
let submitButton = myForm.querySelector("#main-submit");

if (myForm.requestSubmit) {
  if (submitButton) {
    myForm.requestSubmit(submitButton);
  } else {
    myForm.requestSubmit();
  }
} else {
  myForm.submit();
}
```

规范
--

| Specification |
| --- |
| HTML Standard # dom-form-requestsubmit-dev |

浏览器兼容性
------

BCD tables only load in the browser with JavaScript enabled. Enable JavaScript to view data.