aboutsummaryrefslogtreecommitdiff
path: root/files/zh-cn/web/api/webrtc_api/signaling_and_video_calling/index.html
blob: 50a7f2de6e7ce104286be2bd1fa62fc126abc6fa (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
---
title: 信令与视频通话
slug: Web/API/WebRTC_API/Signaling_and_video_calling
translation_of: Web/API/WebRTC_API/Signaling_and_video_calling
---
<p>{{WebRTCSidebar}}</p>

<p><span class="seoSummary"><a href="https://developer.mozilla.org/en-US/docs/Web/API/WebRTC_API">WebRTC</a>允许在两个设备之间进行实时的对等媒体交换。通过称为<strong>信令</strong>的发现和协商过程建立连接。本教程将指导你构建双向视频通话。</span></p>

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

<p>在本文中,我们将进一步扩充 <a class="external external-icon" href="https://mdn-samples.mozilla.org/s/websocket-chat" rel="noopener">WebSocket chat</a> 作为我们的WebSocket文档的一部分(本文链接即将发布;它实际上还没有在线),以支持在用户之间的双向视频通话。你可以在<a href="https://webrtc-from-chat.glitch.me/">Glitch</a>上查看这个例子,你也尝试<a href="https://glitch.com/edit/#!/remix/webrtc-from-chat">修改</a>这个例子。您还可以在<a href="https://github.com/mdn/samples-server/tree/master/s/webrtc-from-chat">GitHub</a>上查看完整的项目代码。</p>

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

<h2 id="信令服务器">信令服务器</h2>

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

<p>WebRTC并没有提供信令传递机制,你可以使用任何你喜欢的方式如<a href="https://developer.mozilla.org/en-US/docs/Web/API/WebSocket_API">WebSocket</a> 或者{{domxref("XMLHttpRequest")}} 等等,来交换彼此的令牌信息。</p>

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

<h3 id="开始准备聊天服务器来处理信令">开始准备聊天服务器来处理信令</h3>

<p>我们的<a href="https://github.com/mdn/samples-server/tree/master/s/websocket-chat">聊天服务器和客户端</a>使用 <a href="https://developer.mozilla.org/en-US/docs/Web/API/WebSocket_API">WebSocket API</a>  {{Glossary("JSON")}} 格式的字符串来传递数据。服务器支持多种消息格式来处理不同的任务,比如注册新用户、设置用户名、发送公共信息等等。</p>

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

<p>让我们看一下我们还需要做些什么让它支持WebRTC信令. 代码在 <a href="https://github.com/mdn/samples-server/tree/master/s/webrtc-from-chat/chatserver.js">chatserver.js</a>.中实现。</p>

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

<pre class="brush: js">function sendToOneUser(target, msgString) {
  var isUnique = true;
  var i;

  for (i=0; i&lt;connectionArray.length; i++) {
    if (connectionArray[i].username === target) {
      connectionArray[i].sendUTF(msgString);
      break;
    }
  }
}</pre>

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

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

<pre class="brush: 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 &amp;&amp; msg.target !== undefined &amp;&amp; msg.target.length !== 0) {
    sendToOneUser(msg.target, msgString);
  } else {
    for (i=0; i&lt;connectionArray.length; i++) {
      connectionArray[i].sendUTF(msgString);
    }
  }
}</pre>

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

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

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

<h3 id="设计信令协议">设计信令协议</h3>

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

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

<h4 id="交换会话描述信息"><strong>交换会话描述信息</strong></h4>

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

<dl>
 <dt><code><font face="Arial, x-locale-body, sans-serif"><span style="background-color: #ffffff; font-weight: 400;"> </span></font>type</code></dt>
 <dd>消息类型; <code>"video-offer"</code> 或 <code>"video-answer"</code></dd>
 <dt><code>name</code></dt>
 <dd>发送者用户名</dd>
 <dt><code>target</code></dt>
 <dd>接受者的用户名(如果呼叫者正在发送消息,则指定被呼叫者,反之亦然)</dd>
 <dt><code>sdp</code></dt>
 <dd>描述连接本地端SDP(Session Description Protocol)协议字符串(从接收者的角度来看,它描述远程端)</dd>
</dl>

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

<h3 id="交换_ICE_候选">交换 ICE 候选</h3>

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

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

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

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

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

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

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

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

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

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

<h3 id="信令事务流程">信令事务流程</h3>

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

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

<ul>
 <li>在Web浏览器中运行的每个用户的客户端</li>
 <li>每个用户的Web浏览器</li>
 <li>信令服务器</li>
 <li>承载聊天服务的Web服务器</li>
</ul>

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

<p><a href="https://mdn.mozillademos.org/files/12363/WebRTC%20-%20Signaling%20Diagram.svg"><img alt="Diagram of the signaling process" src="https://mdn.mozillademos.org/files/12363/WebRTC%20-%20Signaling%20Diagram.svg" style="height: 865px; width: 700px;"></a></p>

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

<h3 id="ICE_候选交换过程">ICE 候选交换过程</h3>

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

<p><a href="https://mdn.mozillademos.org/files/12365/WebRTC%20-%20ICE%20Candidate%20Exchange.svg"><img alt="Diagram of ICE candidate exchange process" src="https://mdn.mozillademos.org/files/12365/WebRTC%20-%20ICE%20Candidate%20Exchange.svg" style="height: 590px; width: 700px;"></a></p>

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

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

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

<h2 id="客户端应用">客户端应用</h2>

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

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

<h3 id="更新_HTML">更新 HTML</h3>

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

<pre class="brush: html">      &lt;div class="flexChild" id="camera-container"&gt;
        &lt;div class="camera-box"&gt;
          &lt;video id="received_video" autoplay&gt;&lt;/video&gt;
          &lt;video id="local_video" autoplay muted&gt;&lt;/video&gt;
          &lt;button id="hangup-button" onclick="hangUpCall();" disabled&gt;
            Hang Up
          &lt;/button&gt;
        &lt;/div&gt;
      &lt;/div&gt;</pre>

<p>此处定义的页面结构使用了 {{HTMLElement("div")}} 元素,通过启用CSS,我们可以完全控制页面布局。我们将跳过本指南中的布局细节,但你可以<a href="https://github.com/mdn/samples-server/tree/master/s/webrtc-from-chat/chat.css">看看GitHub上的CSS</a>,了解如何处理它。 注意这两个 {{HTMLElement("video")}} 元素,一个用于观看自己,一个用于连接,还有 {{HTMLElement("button")}} 元素.</p>

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

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

<h3 id="JavaScript_代码">JavaScript 代码</h3>

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

<h4 id="向信令服务器发送信息"><strong>向信令服务器发送信息</strong></h4>

<p>在整个代码中,我们调用 <code>sendToServer()</code> 以便向信令服务器发送消息。此函数使用<a href="https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API">WebSocket</a>连接执行其工作:</p>

<pre class="brush: js">function sendToServer(msg) {
  var msgJSON = JSON.stringify(msg);

  connection.send(msgJSON);
}</pre>

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

<h4 id="开始通话的交互">开始通话的交互</h4>

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

<pre><code>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);
  });
}</code></pre>

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

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

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

<h4 id="开始一个通话">开始一个通话</h4>

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

<pre class="brush: 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);
  }
}</pre>

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

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

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

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

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

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

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

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

<h4 id="处理_getUserMedia()_错误">处理 getUserMedia() 错误</h4>

<p>如果 <code>getUserMedia()</code> 返回的promise失败,将执行<code>handleGetUserMediaError()</code> 函数。</p>

<pre class="brush: 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();
}</pre>

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

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

<h4 id="创建端到端连接">创建端到端连接</h4>

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

<pre><code>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;
}</code></pre>

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

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

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

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

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

<p><strong style="font-size: 1rem; font-weight: 700; letter-spacing: -0.00278rem;">{{domxref("RTCPeerConnection.onicecandidate")}}</strong></p>

<dl>
 <dd>
 <p>当需要你通过信令服务器将一个ICE候选发送给另一个对等端时,本地ICE层将会调用你的 {{event("icecandidate")}} 事件处理程序。有关更多信息,请参阅<a href="#交换_ice_候选">交换 ICE 候选</a> 以查看此示例的代码。</p>
 </dd>
 <dt>{{domxref("RTCPeerConnection.ontrack")}}</dt>
 <dd>当向连接中添加磁道时,{{event("track")}} 事件的此处理程序由本地WebRTC层调用。例如,可以将传入媒体连接到元素以显示它。详见<a href="#接收新的流数据">接收新的流数据</a></dd>
 <dt>{{domxref("RTCPeerConnection.onnegotiationneeded")}}</dt>
 <dd>每当WebRTC基础结构需要你重新启动会话协商过程时,都会调用此函数。它的工作是创建和发送一个请求,给被叫方,要求它与我们联系。参见<a href="#开始协商">开始协商</a>了解我们如何处理这一问题。</dd>
 <dt>{{domxref("RTCPeerConnection.onremovetrack")}}</dt>
 <dd>调用与 <code>ontrack</code>相对应的对象来处理 {{event("removetrack")}} 事件;当远程对等端从正在发送的媒体中删除磁道时,它将发送到<code>RTCPeerConnection</code>。参见 <a href="#处理流的移除">处理流的移除</a></dd>
 <dt>{{domxref("RTCPeerConnection.oniceconnectionstatechange")}}</dt>
 <dd>ICE层发送{{event("iceconnectionstatechange")}} 事件,让你了解ICE连接状态的更改。这可以帮助你了解连接何时失败或丢失。我们将在下面的<a href="#ice_连接状态">ICE 连接状态</a>中查看此示例的代码。</dd>
 <dt>{{domxref("RTCPeerConnection.onicegatheringstatechange")}}</dt>
 <dd>当ICE代理收集候选对象的过程从一个状态切换到另一个状态(例如开始收集候选对象或完成协商)时,ICE层将向你发送事件(“ICegulatingStateChange”)事件。见下文 <a href="#ice_收集状态">ICE 收集状态</a></dd>
 <dt>{{domxref("RTCPeerConnection.onsignalingstatechange")}}</dt>
 <dd>
 <p>当信令进程的状态更改时(或如果到信令服务器的连接更改时),WebRTC架构将向你发送 {{event("signalingstatechange")}} 消息。参见<a href="#ice_信令状态">ICE 信令状态</a>查看我们的代码。</p>
 </dd>
</dl>

<h4 id="开始协商">开始协商</h4>

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

<pre class="brush: 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);
}</pre>

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

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

<div class="note">
<p>注意:从技术上讲, <code>createOffer()</code> 返回的字符串是{{RFC(3264)}} 请求。</p>
</div>

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

<dl>
 <dt><code>type</code></dt>
 <dd>消息类型: <code>"video-offer"</code>.</dd>
 <dt><code>name</code></dt>
 <dd>调用方的用户名。</dd>
 <dt><code>target</code></dt>
 <dd>被调用方的用户名</dd>
 <dt><code>sdp</code></dt>
 <dd>SDP 字符串描述了请求</dd>
</dl>

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

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

<h4 id="会话协商">会话协商</h4>

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

<h5 id="处理请求">处理请求</h5>

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

<pre><code>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 =&gt; 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);
}</code></pre>

<p class="brush: js">此代码与我们在<a href="#开始通话的交互">开始通话的交互</a>中在 <code>invite()</code> 函数中所做的非常相似。它首先使用 <code>createPeerConnection()</code> 函数创建和配置{{domxref("RTCPeerConnection")}} 。然后,它从收到的 <code>"video-offer"</code> 消息中获取SDP请求,并使用它创建一个表示调用方会话描述的新 {{domxref("RTCSessionDescription")}} 对象。</p>

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

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

<p>捕捉到的任何错误都会被传递给 <code>handleGetUserMediaError()</code>,详见 <a href="#处理_getusermedia()_错误">处理 getUserMedia() 错误</a></p>

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

<h5 id="发送_ICE_候选">发送 ICE 候选</h5>

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

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

<pre class="brush: js">function handleICECandidateEvent(event) {
  if (event.candidate) {
    sendToServer({
      type: "new-ice-candidate",
      target: targetUsername,
      candidate: event.candidate
    });
  }
}</pre>

<p>这将构建一个包含候选对象的对象,然后使用前面在 <a href="#向信令服务器发送信息">向信令服务器发送信息</a> 中描述的<code>sendToServer()</code> 函数将其发送给另一个对等方。消息属性为:</p>

<p><strong><span style='background-color: rgba(220, 220, 220, 0.5); font-family: consolas,"Liberation Mono",courier,monospace; font-size: 1rem; font-style: inherit; font-weight: inherit; letter-spacing: -0.00278rem;'>type</span></strong></p>

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

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

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

<h5 id="接收_ICE_候选">接收 ICE 候选</h5>

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

<pre class="brush: js">function handleNewICECandidateMsg(msg) {
  var candidate = new RTCIceCandidate(msg.candidate);

  myPeerConnection.addIceCandidate(candidate)
    .catch(reportError);
}</pre>

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

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

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

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

<h5 id="接收新的流数据">接收新的流数据</h5>

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

<pre class="brush: js">function handleAddStreamEvent(event) {
  document.getElementById("received_video").srcObject = event.stream;
  document.getElementById("hangup-button").disabled = false;
}</pre>

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

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

<h5 id="处理流的移除">处理流的移除</h5>

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

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

  if (trackList.length == 0) {
    closeVideoCall();
  }
}</code></pre>

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

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

<h4 id="结束通话">结束通话</h4>

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

<h5 id="挂机">挂机</h5>

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

<pre class="brush: js">function hangUpCall() {
  closeVideoCall();
  sendToServer({
    name: myUsername,
    target: targetUsername,
    type: "hang-up"
  });
}</pre>

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

<h5 id="结束通话_2">结束通话</h5>

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

<pre class="brush: js">function closeVideoCall() {
  var remoteVideo = document.getElementById("received_video");
  var localVideo = document.getElementById("local_video");

  if (myPeerConnection) {
<code>    myPeerConnection.ontrack = null;
    myPeerConnection.onremovetrack = null;
    myPeerConnection.onremovestream = null;
    myPeerConnection.onicecandidate = null;
    myPeerConnection.oniceconnectionstatechange = null;
    myPeerConnection.onsignalingstatechange = null;
    myPeerConnection.onicegatheringstatechange = null;
    myPeerConnection.onnegotiationneeded = null;</code>

    if (remoteVideo.srcObject) {
      remoteVideo.srcObject.getTracks().forEach(track =&gt; track.stop());
    }

    if (localVideo.srcObject) {
      localVideo.srcObject.getTracks().forEach(track =&gt; track.stop());
    }

    myPeerConnection.close();
    myPeerConnection = null;
  }
<code>
  remoteVideo.removeAttribute("src");
  remoteVideo.removeAttribute("srcObject");
  localVideo.removeAttribute("src");
  remoteVideo.removeAttribute("srcObject");</code>

  document.getElementById("hangup-button").disabled = true;
  targetUsername = null;
}</pre>

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

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

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

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

<h4 id="处理状态变更">处理状态变更</h4>

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

<h5 id="ICE_连接状态">ICE 连接状态</h5>

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

<pre class="brush: js">function handleICEConnectionStateChangeEvent(event) {
  switch(myPeerConnection.iceConnectionState) {
    case "closed":
    case "failed":
    case "disconnected":
      closeVideoCall();
      break;
  }
}</pre>

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

<h5 id="ICE_信令状态">ICE 信令状态</h5>

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

<pre class="brush: js">  myPeerConnection.onsignalingstatechange = function(event) {
    switch(myPeerConnection.signalingState) {
      case "closed":
        closeVideoCall();
        break;
    }
  };</pre>

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

<h5 id="ICE_收集状态">ICE 收集状态</h5>

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

<pre class="brush: js">function handleICEGatheringStateChangeEvent(event) {
  // Our sample just logs information to console here,
  // but you can do whatever you need.
}
</pre>

<h2 id="下一步">下一步</h2>

<p>现在您可以<a href="https://webrtc-from-chat.glitch.me/">在Glitch上尝试这个例子</a>,以看到它的实际效果。打开两个设备上的Web控制台并查看记录的输出,尽管你在上面所示的代码中看不到它,但是服务器上(以及<a href="https://github.com/mdn/samples-server/tree/master/s/webrtc-from-chat">GitHub</a>上)的代码有很多控制台输出,因此你可以看到信令和连接进程在工作。</p>

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