本地使用 PeerConnection 对象:一个示例

现在让我们从 例3-1 中显示的简单HTML代码开始。

例3-1 本地 RTCPeerConnection 用法示例

  1. <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
  2. "http://www.w3.org/TR/html4/loose.dtd">
  3. <html>
  4. <head>
  5. <title>Local PeerConnection() example</title>
  6. </head>
  7. <body>
  8. <table border="1" width="100%">
  9. <tr>
  10. <th>Local video</th>
  11. <th>'Remote' video</th>
  12. </tr>
  13. <tr>
  14. <td><video id="localVideo" autoplay></video></td>
  15. <td><video id="remoteVideo" autoplay></video></td>
  16. </tr>
  17. <tr>
  18. <td align="center">
  19. <div>
  20. <button id="startButton">Start</button>
  21. <button id="callButton">Call</button>
  22. <button id="hangupButton">Hang Up</button>
  23. </div>
  24. </td>
  25. <td><!-- void --></td>
  26. </tr>
  27. </table>
  28. <script src="js/localPeerConnection.js"></script>
  29. </body>
  30. </html>

例3-1 充当两个视频流的容器,以表格式并排表示。 左侧的流表示本地捕获,而右侧的流则模拟远程方(实际上是对本地音频和视频设备的进一步捕获)。 媒体捕获和渲染是由与三个按钮关联的事件触发的,这三个按钮分别用于启动应用程序,在本地和(假)远程用户之间进行呼叫以及挂断该呼叫。 像往常一样,此应用程序的核心是文件 localPeerConnection.js 中包含的 JavaScript 代码,其报告如下:

<<< @/js/localPeerConnection.js

为了轻松理解此代码的内容,让我们逐步跟踪我们应用程序的发展。 我们将显示使用 Chrome 和 Firefox 拍摄的屏幕截图,因此您可以欣赏与应用程序外观和两种浏览器提供的开发人员工具相关的差异。

Starting the Application

当用户单击Chrome(图3-2)和Firefox(图3-3)中的“开始”按钮时,会发生以下情况

图3-2

图3-2 在 Chrome 中加载的示例页面

图3-3

图3-3 在 Firefox 中加载的示例页面

从两个图中都可以看到,浏览器正在征求用户同意访问本地音频和视频设备。 从上一章我们知道,这是由于执行了 getUserMedia() 调用,如下面的 JavaScript 片段所示:

  1. // Function associated with clicking on the Start button
  2. // This is the event triggering all other actions
  3. function start() {
  4. log("Requesting local stream");
  5. // First of all, disable the Start button on the page
  6. startButton.disabled = true;
  7. // Get ready to deal with different browser vendors...
  8. navigator.getUserMedia = navigator.getUserMedia ||
  9. navigator.webkitGetUserMedia || navigator.mozGetUserMedia;
  10. // Now, call getUserMedia()
  11. navigator.getUserMedia({audio:true, video:true}, successCallback, function(error) {
  12. log("navigator.getUserMedia error: ", error);
  13. });
  14. }

一旦用户同意,就会触发 successCallback() 函数。该函数只是将本地流(包含音频和视频轨道)附加到 HTML5 页面中的 localVideo 元素:

  1. // Associate the local video element with the retrieved stream
  2. if (window.URL) {
  3. localVideo.src = URL.createObjectURL(stream);
  4. } else {
  5. localVideo.src = stream;
  6. }
  7. localStream = stream;

图3-4(Chrome)和图3-5(Firefox)中显示了执行回调的效果。

Figure 3-4. The example page after user grants consent, in Chrome

Figure 3-5. The example page after user grants consent, in Firefox

拨打电话 Placing a Call

一旦获得同意,“开始”按钮将被禁用,而“呼叫”按钮将依次变为启用状态。 如果用户单击它,则会触发 call() 函数。 该功能首先执行一些基本的内务处理,例如禁用“呼叫”按钮和启用“挂断”按钮。 然后,对于 Chrome 和 Opera ( Firefox 当前未实现此功能),它将有关可用媒体轨道的一些信息记录到控制台:

  1. // Function associated with clicking on the Call button
  2. // This is enabled upon successful completion of the Start button handler
  3. function call() {
  4. // First of all, disable the Call button on the page...
  5. callButton.disabled = true;
  6. // ...and enable the Hangup button
  7. hangupButton.disabled = false;
  8. log("Starting call");
  9. // Note that getVideoTracks() and getAudioTracks() are not currently
  10. // supported in Firefox...
  11. // ...just use them with Chrome
  12. if (navigator.webkitGetUserMedia) {
  13. // Log info about video and audio device in use
  14. if (localStream.getVideoTracks().length > 0) {
  15. log('Using video device: ' + localStream.getVideoTracks()[0].label);
  16. }
  17. if (localStream.getAudioTracks().length > 0) {
  18. log('Using audio device: ' + localStream.getAudioTracks()[0].label);
  19. }
  20. }
  21. ...

::: warning 注意

由 Media Capture and Streams API 中的 MediaStream 构造函数定义的 getVideoTracks()getAudioTracks() 方法,返回一系列 MediaStreamTrack 对象,分别表示流中的视频轨道和音频轨道。

:::

完成前面的操作后,我们终于进入了代码的核心,即我们第一次遇到 RTCPeerConnection 对象的部分:

  1. // Chrome
  2. if (navigator.webkitGetUserMedia) {
  3. RTCPeerConnection = webkitRTCPeerConnection;
  4. // Firefox
  5. } else if(navigator.mozGetUserMedia) {
  6. RTCPeerConnection = mozRTCPeerConnection;
  7. RTCSessionDescription = mozRTCSessionDescription;
  8. RTCIceCandidate = mozRTCIceCandidate;
  9. }
  10. log("RTCPeerConnection object: " + RTCPeerConnection);

上面的代码段包含一些 JavaScript 代码,这些代码的唯一目的是检测使用的浏览器的类型,以便为正确的对象提供正确的名称。 您会从代码中注意到,标准 RTCPeerConnection 对象当前在 Chrome(webkitRTCPeerConnection) 和 Firefox(mozRTCPeerConnection) 中都是前缀。 顺便说一下,后一种浏览器也有一种非标准的方式来命名相关的 RTCSessionDescriptionRTCIceCandidate 对象,它们分别与要协商的会话的描述和 ICE 协议候选地址的表示相关联(请参阅第4章)。

一旦确定了(正确的)RTCPeerConnection 对象,我们最终可以实例化它:

  1. // This is an optional configuration string, associated with
  2. // NAT traversal setup
  3. var servers = null;
  4. // Create the local PeerConnection object
  5. localPeerConnection = new RTCPeerConnection(servers);
  6. log("Created local peer connection object localPeerConnection");
  7. // Add a handler associated with ICE protocol events
  8. localPeerConnection.onicecandidate = gotLocalIceCandidate;

上面的代码片段显示了 RTCPeerConnection 对象是通过使用可选的 server 参数作为输入的构造函数实例化的。 可以使用此参数来正确处理 NAT 遍历问题,如第4章中所述。


RTCPeerConnection

调用 new RTCPeerConnection(configuration) 将创建一个 RTCPeerConnection 对象。 该配置具有查找和访问 STUN 和 TURN 服务器的信息(每种类型可以有多个服务器,任何 TURN 服务器也可以用作 STUN 服务器)。 (可选)还可以使用第19页的 MediaConstraints 对象“Media Constraints”。

调用 RTCPeerConnection 构造函数时,它还会创建一个 ICE 代理,该 ICE 代理由浏览器直接控制负责 ICE 状态机。 当 IceTransports 约束未设置为 “none” 时,ICE 代理将继续收集候选地址。

RTCPeerConnection 对象具有两个关联的流集。 表示当前正在发送的流的本地流集(local streams set)和表示通过此 RTCPeerConnection 对象当前接收的流的远程流集(remote streams set)。 创建 RTCPeerConnection 对象时,流集将初始化为空集。

这里要注意的有趣事情是,通过定义适当的回调方法,新创建的 PeerConnection 的配置是异步完成的。


::: warning 注意

每当浏览器内部的 ICE 协议机器将新候选者提供给本地对等方时,就会触发 onicecandidate 处理程序。

:::

  1. // Handler to be called whenever a new local ICE candidate becomes available
  2. function gotLocalIceCandidate(event) {
  3. if (event.candidate) {
  4. // Add candidate to the remote PeerConnection
  5. remotePeerConnection.addIceCandidate(new RTCIceCandidate(event.candidate));
  6. log("Local ICE candidate: \n" + event.candidate.candidate);
  7. }
  8. }

::: tip 译者 注

if (event.candidate === null) { console.log("ICE Candidate was null, done") }

这个 event.candidate 是可以为空的,当为空时说明 iceServer 已经遍历完成,不会再有新的 candidate 产生

:::

::: warning 注意

addIceCandidate() 方法向 ICE 代理提供远程候选对象。 除了将其添加到远程描述中之外,只要 IceTransports 约束未设置为 “none”,连通性检查将被发送到新的候选对象。

:::

该代码片段理所当然地认为远程对等点实际上是在本地运行的,从而避免了通过正确配置的信令通道将有关收集的本地地址的信息发送给另一方的需求。 这就是为什么如果您尝试在两台远程计算机上运行该应用程序将根本无法运行的原因。 在随后的章节中,我们将讨论如何创建这样的信令通道,并使用它来将 ICE 相关(以及会话相关)信息传输到远程方。 目前,我们仅将收集的本地网络可达性信息添加到(本地可用)远程对等连接(remote peer connection)。 显然,在主叫方和被叫方之间切换角色时,同样的道理也适用,即,只要有远程候选者,它们就会被简单地添加到本地对等连接(local peer connection)中:

  1. // Create the remote PeerConnection object
  2. remotePeerConnection = new RTCPeerConnection(servers);
  3. log("Created remote peer connection object remotePeerConnection");
  4. // Add a handler associated with ICE protocol events...
  5. remotePeerConnection.onicecandidate = gotRemoteIceCandidate;
  6. // ...and a second handler to be activated as soon as the remote
  7. // stream becomes available
  8. remotePeerConnection.onaddstream = gotRemoteStream;

::: warning 注意

每当远程对等方分别添加或删除 MediaStream 时,都会调用 onaddstreamonremovestream 处理函数。这两者仅在执行 setRemoteDescription() 方法时才会被触发。

:::

上面的代码片段与 onaddstream 处理函数有关,该处理函数的实现在将远程流(一旦可用时)附加到 HTML5 页面的 remoteVideo 元素后进行查找,如下所示:

  1. // Handler to be called as soon as the remote stream becomes available
  2. function gotRemoteStream(event) {
  3. // Associate the remote video element with the retrieved stream
  4. if (window.URL) {
  5. // Chrome
  6. remoteVideo.src = window.URL.createObjectURL(event.stream);
  7. } else {
  8. // Firefox
  9. remoteVideo.src = event.stream;
  10. }
  11. log("Received remote stream");
  12. }

回到 Call() 函数,剩下的唯一动作是将本地流添加到本地 PeerConnection 并最终在其上调用 createOffer() 方法:

  1. ...
  2. // Add the local stream (as returned by getUserMedia()
  3. // to the local PeerConnection
  4. localPeerConnection.addStream(localStream);
  5. log("Added localStream to localPeerConnection");
  6. // We're all set! Create an Offer to be 'sent' to the callee as soon as
  7. // the local SDP is ready
  8. localPeerConnection.createOffer(gotLocalDescription,onSignalingError);
  9. }
  10. function onSignalingError(error) {
  11. console.log('Failed to create signaling message : ' + error.name);
  12. }

::: warning 注意

addStream()removeStream() 方法分别向 RTCPeerConnection 对象添加流和移除流。

:::

createOffer() 方法起着基本作用,因为它要求浏览器正确检查 PeerConnection 的内部状态并生成适当的 RTCSessionDescription 对象,从而启动 “提供/应答(Offer/Answer)” 状态机。

::: warning 注意

createOffer() 方法生成一个 SDP Blob,其中包含:

  1. 具有会话支持的配置 RFC3264offer
  2. 附加(attached)的 localMediaStreams 的描述
  3. 浏览器支持的 codec/RTP/RTCP 选项
  4. ICE 收集的所有候选对象
  5. 可以提供约束参数以对生成的要约提供附加控制

:::

当会话描述对应用程序可用时,createOffer() 方法就将调用回调(gotLocalDescription)作为输入。 同样在这种情况下,当会话描述可用时,则本地对等方(local peer)应使用信令信道将其发送给被叫方。 目前,我们将跳过此阶段,并再次假设远程方实际上是本地可到达的一方,这将转换为以下操作:

  1. // Handler to be called when the 'local' SDP becomes available
  2. function gotLocalDescription(description){
  3. // Add the local description to the local PeerConnection
  4. localPeerConnection.setLocalDescription(description);
  5. log("Offer from localPeerConnection: \n" + description.sdp);
  6. // ...do the same with the 'pseudoremote' PeerConnection
  7. // Note: this is the part that will have to be changed if
  8. // you want the communicating peers to become remote
  9. // (which calls for the setup of a proper signaling channel)
  10. remotePeerConnection.setRemoteDescription(description);
  11. // Create the Answer to the received Offer based on the 'local' description
  12. remotePeerConnection.createAnswer(gotRemoteDescription,onSignalingError);
  13. }

如上面的注释片段所述,我们在此将检索到的会话描述直接设置为本地对等方的本地描述和远程对等方的远程描述。

::: warning 注意

setLocalDescription()setRemoteDescription() 方法指示 RTCPeerConnection 将提供的 RTCSessionDescription 分别应用为本地描述(local description)和远程的 offeranswer

:::

然后,我们通过调用远程对等体连接上的 createAnswer() 方法来要求远程对等体应答所提供的会话。 一旦远程浏览器将其自己的会话描述提供给远程对等方,此方法就将要调用的回调(gotRemoteDescription)作为输入参数。 这样的处理程序实际上反映了呼叫方的伴随回调的行为:

  1. // Handler to be called when the remote SDP becomes available
  2. function gotRemoteDescription(description){
  3. // Set the remote description as the local description of the
  4. // remote PeerConnection
  5. remotePeerConnection.setLocalDescription(description);
  6. log("Answer from remotePeerConnection: \n" + description.sdp);
  7. // Conversely, set the remote description as the remote description
  8. // of the local PeerConnection
  9. localPeerConnection.setRemoteDescription(description);
  10. }

createAnswer() 方法使用与远程配置中的参数兼容的会话支持的配置生成 SDP answer

实际上,可以在浏览器的控制台上跟踪上述整个呼叫流程,如图3-6(Chrome)和图3-7(Firefox)所示。

图3-6

图3-6 Chrome 控制台跟踪两个本地对等方之间的呼叫

图3-7

图3-7 Firefox 控制台跟踪两个本地对等方之间的呼叫

这两个快照显示了应用程序已记录的事件序列以及符合SDP格式的会话描述信息。 当我们在第4章中简要介绍会话描述协议时,日志的最后一部分将变得更加清晰。

完成上述所有步骤后,我们终于可以在浏览器窗口中看到两个流,如图3-8(Chrome)和图3-9(Firefox)所示。

Figure 3-8. Chrome showing local and remote media after a successful call

Figure 3-9. Firefox showing local and remote media after a successful call

挂断 Hanging Up

通话结束后,用户可以通过单击“挂断”按钮将其删除。 这触发了关联处理程序的执行:

  1. // Handler to be called when hanging up the call
  2. function hangup() {
  3. log("Ending call");
  4. // Close PeerConnection(s)
  5. localPeerConnection.close();
  6. remotePeerConnection.close();
  7. // Reset local variables
  8. localPeerConnection = null;
  9. remotePeerConnection = null;
  10. // Disable Hangup button
  11. hangupButton.disabled = true;
  12. // Enable Call button to allow for new calls to be established
  13. callButton.disabled = false;
  14. }

正如我们从快速浏览代码中看到的那样,hangup() 处理程序仅关闭实例化的对等连接并释放资源。 然后,它禁用 “挂断” 按钮并启用 “呼叫” 按钮,从而将设置回滚到我们首次启动应用程序后(即,在 getUserMedia() 调用之后)立即到达的位置。 从中可以发出新的呼叫,并且可以重新开始游戏。 图3-10(Chrome)和图3-11(Firefox)中描述了这种情况。

::: warning 注意

close() 方法销毁 RTCPeerConnection ICE 代理,突然结束任何活动的 ICE 处理和任何活动的流,并释放任何相关资源。

:::

Figure 3-10. Chrome after tearing down a call

Figure 3-11. Firefox after tearing down a call

请注意,两个窗口中的两个帧是不同的,这说明了一个事实,即使不再有对等连接可用,我们现在仍具有实时本地流和冻结的远程流。 这也在控制台日志中报告。