使用 WebRTC 进行实时通信

1. 简介

WebRTC 是一个开源项目,旨在实现 Web 应用和原生应用中的音频、视频和数据实时通信。

WebRTC 有多个 JavaScript API - 点击链接可查看演示。

我可以在哪些地方使用 WebRTC?

在 Firefox、Opera 以及桌面版和 Android 版 Chrome 中。WebRTC 也适用于 iOS 和 Android 上的原生应用。

什么是信令?

WebRTC 使用 RTCPeerConnection 在浏览器之间传输流式数据,但也需要一种机制来协调通信并发送控制消息,此过程称为信令。WebRTC 未规定信令方法和协议。在此 Codelab 中,您将使用 Socket.IO 进行消息传递,但还有许多替代方案

什么是 STUN 和 TURN?

WebRTC 旨在实现点对点工作,因此用户可以通过最直接的路由进行连接。不过,WebRTC 是为了应对实际网络而构建的:客户端应用需要遍历 NAT 网关和防火墙,点对点网络需要回退机制,以防直接连接失败。在此过程中,WebRTC API 使用 STUN 服务器获取您的计算机的 IP 地址,使用 TURN 服务器作为中继服务器,以防点对点通信失败。(现实场景中的 WebRTC 一文对此进行了更详细的说明。)

WebRTC 安全吗?

对于所有 WebRTC 组件而言,加密是必需的,并且其 JavaScript API 只能从安全来源(HTTPS 或 localhost)使用。WebRTC 标准未定义信令机制,因此您需要自行确保使用安全的协议。

2. 概览

构建一款应用,在其中使用网络摄像头截取视频和拍摄快照,并利用 WebRTC 点对点功能进行分享。在此过程中,您将学习如何使用核心 WebRTC API 并通过 Node.js 设置消息传递服务器。

学习内容

  • 使用网络摄像头获取视频
  • 通过 RTCPeerConnection 流式传输视频
  • 使用 RTCDataChannel 流式传输数据
  • 设置信令服务以交换消息
  • 结合使用对等连接和信令
  • 拍照并通过数据通道分享

所需条件

  • Chrome 47 或更高版本
  • Web Server for Chrome,也可以使用您自己选择的网络服务器。
  • 示例代码
  • 文本编辑器
  • 已掌握 HTML、CSS 和 JavaScript 方面的基础知识

3. 获取示例代码

下载代码

如果您熟悉 git,可以从 GitHub 克隆此 Codelab 的代码,从而下载该代码:

git clone https://github.com/googlecodelabs/webrtc-web

或者,点击以下按钮下载代码的 .zip 文件:

打开下载的 ZIP 文件。此操作会解压缩一个项目文件夹 (adaptive-web-media),其中包含本 Codelab 的每个步骤的一个文件夹,以及您需要的所有资源。

您将在名为 work 的目录中进行所有编码工作。

step-nn 文件夹包含此 Codelab 的每个步骤的已完成版本。这些内容可供参考。

安装并验证网络服务器

尽管您可以随意使用自己的网络服务器,但此 Codelab 旨在与 Chrome Web 服务器配合使用。如果您尚未安装该应用,则可以通过 Chrome 应用商店安装。

6ddeb4aee53c0f0e.png

安装 Web Server for Chrome 应用后,点击书签栏、新标签页或应用启动器中的“Chrome 应用”快捷方式:

1d2b4aa977ab7e24.png

点击“Web Server”图标:

27fce4494f641883.png

接下来,您将看到以下对话框,该对话框可让您配置本地网络服务器:

Screen Shot 2016-02-18 at 11.48.14 AM.png

点击 CHOOSE FOLDER(选择文件夹)按钮,然后选择您刚刚创建的 work 文件夹。这样,您就可以通过网络服务器对话框中(Web Server 网址(s) [网络服务器网址] 部分)突出显示的网址在 Chrome 中查看正在进行的工作。

选项下,选中自动显示 index.html 旁边的复选框,如下所示:

Screen Shot 2016-02-18 at 11.56.30 AM.png

然后,将标有 Web Server: STARTED(网络服务器:已启动)的切换开关向左滑动,然后再向右滑动,以停止并重新启动服务器。

Screen Shot 2016-02-18 at 12.22.18 PM.png

现在,点击突出显示的“Web Server 网址”,在网络浏览器中访问您的工作网站。您应该会看到如下所示的页面,它对应于 work/index.html

18a705cb6ccc5181.png

很显然,此应用目前不会执行任何有趣的操作,到目前为止,它只是一个极简的框架,用于确保您的网络服务器能够正常运行。您可以在后续步骤中添加功能和布局功能。

4. 通过网络摄像头流式传输视频

学习内容

在此步骤中,您将了解如何执行以下操作:

  • 从网络摄像头获取视频流。
  • 操纵视频流播放。
  • 使用 CSS 和 SVG 来处理视频。

此步骤的完整版本位于 step-01 文件夹中。

添加 HTML 格式的 dash…

work 目录的 index.html 中添加 video 元素和 script 元素:

<!DOCTYPE html>
<html>

<head>

  <title>Realtime communication with WebRTC</title>

  <link rel="stylesheet" href="css/main.css" />

</head>

<body>

  <h1>Realtime communication with WebRTC</h1>

  <video autoplay playsinline></video>

  <script src="js/main.js"></script>

</body>

</html>

...以及少量 JavaScript

将以下内容添加到 js 文件夹中的 main.js 文件中:

'use strict';

// On this codelab, you will be streaming only video (video: true).
const mediaStreamConstraints = {
  video: true,
};

// Video element where stream will be placed.
const localVideo = document.querySelector('video');

// Local stream that will be reproduced on the video.
let localStream;

// Handles success by adding the MediaStream to the video element.
function gotLocalMediaStream(mediaStream) {
  localStream = mediaStream;
  localVideo.srcObject = mediaStream;
}

// Handles error by logging a message to the console with the error message.
function handleLocalMediaStreamError(error) {
  console.log('navigator.getUserMedia error: ', error);
}

// Initializes media stream.
navigator.mediaDevices.getUserMedia(mediaStreamConstraints)
  .then(gotLocalMediaStream).catch(handleLocalMediaStreamError);

试试看

在浏览器中打开 index.html,此时您应该会看到如下内容(当然包含网络摄像头的画面!):

9297048e43ed0f3d.png

运作方式

getUserMedia() 调用之后,浏览器会请求用户授予摄像头使用权限(如果这是当前来源首次请求摄像头使用权限)。如果成功,则返回 MediaStream,媒体元素可通过 srcObject 属性使用该 MediaStream:

navigator.mediaDevices.getUserMedia(mediaStreamConstraints)
  .then(gotLocalMediaStream).catch(handleLocalMediaStreamError);


}
function gotLocalMediaStream(mediaStream) {
  localVideo.srcObject = mediaStream;
}

constraints 参数用于指定要获取的媒体。在本例中,媒体只是视频,因为音频默认处于停用状态:

const mediaStreamConstraints = {
  video: true,
};

您可以针对其他要求(例如视频分辨率)使用限制条件:

const hdConstraints = {
  video: {
    width: {
      min: 1280
    },
    height: {
      min: 720
    }
  }
}

MediaTrackConstraints 规范列出了所有可能的限制条件类型,但并非所有浏览器都支持全部选项。如果当前选择的摄像头不支持所请求的分辨率,getUserMedia() 会被拒绝并显示 OverconstrainedError,并且系统不会提示用户授予摄像头使用权限。

如果 getUserMedia() 成功,系统就会将网络摄像头的视频串流设为视频元素的来源:

function gotLocalMediaStream(mediaStream) {
  localVideo.srcObject = mediaStream;
}

奖励分

  • 传递给 getUserMedia()localStream 对象处于全局范围内,因此您可以从浏览器控制台进行检查:打开控制台,输入 stream,然后按 Return 键。(如需在 Chrome 中查看控制台,请按 Ctrl-Shift-J;如果您使用的是 Mac,请按 Command-Option-J。)
  • localStream.getVideoTracks() 会返回什么?
  • 尝试拨打 localStream.getVideoTracks()[0].stop()
  • 查看 Constraints 对象:将其更改为 {audio: true, video: true} 后会发生什么?
  • 视频元素的大小是多少?如何从 JavaScript 中获取视频的自然大小,而不是显示大小?您可以使用 Chrome 开发者工具进行检查。
  • 尝试向视频元素添加 CSS 过滤器。例如:
video {
  filter: blur(4px) invert(1) opacity(0.5);
}
  • 尝试添加 SVG 滤镜。例如:
video {
   filter: hue-rotate(180deg) saturate(200%);
 }

要点回顾

在此步骤中,您学习了如何:

  • 使用网络摄像头获取视频。
  • 设置媒体限制。
  • 视频元素出现问题。

此步骤的完整版本位于 step-01 文件夹中。

提示

最佳做法

  • 确保视频元素不会溢出容器。我们添加了 widthmax-width,用于设置视频的首选大小和最大大小。浏览器会自动计算高度:
video {
  max-width: 100%;
  width: 320px;
}

后续步骤

您有视频,但如何进行直播呢?请在下一步中了解详情!

5. 通过 RTCPeerConnection 流式传输视频

学习内容

在此步骤中,您将了解如何执行以下操作:

  • 使用 WebRTC shim adapter.js 抽象化浏览器差异。
  • 使用 RTCPeerConnection API 流式传输视频。
  • 控制媒体捕获和流式传输。

此步骤的完整版本位于 step-2 文件夹中。

什么是 RTCPeerConnection?

RTCPeerConnection 是一种 API,用于进行 WebRTC 调用以流式传输视频和音频,以及交换数据。

此示例在同一页面上设置两个 RTCPeerConnection 对象(称为对等互连)之间的连接。

虽然实用性不高,但有助于了解 RTCPeerConnection 的运作方式。

添加视频元素和控制按钮

index.html 中,将单个视频元素替换为两个视频元素和三个按钮:

<video id="localVideo" autoplay playsinline></video>
<video id="remoteVideo" autoplay playsinline></video>


<div>
  <button id="startButton">Start</button>
  <button id="callButton">Call</button>
  <button id="hangupButton">Hang Up</button>
</div>

一个视频元素将显示来自 getUserMedia() 的数据流,另一个将显示通过 RTCPeerconnection 流式传输的同一视频。(在实际应用中,一个视频元素会显示本地数据流,另一个显示远程数据流。)

添加 adapter.js shim

在指向 main.js 的链接上方添加指向当前版本 adapter.js 的链接:

<script src="https://webrtc.github.io/adapter/adapter-latest.js"></script>

Index.html 现在应如下所示:

<!DOCTYPE html>
<html>

<head>
  <title>Realtime communication with WebRTC</title>
  <link rel="stylesheet" href="css/main.css" />
</head>

<body>
  <h1>Realtime communication with WebRTC</h1>

  <video id="localVideo" autoplay playsinline></video>
  <video id="remoteVideo" autoplay playsinline></video>

  <div>
    <button id="startButton">Start</button>
    <button id="callButton">Call</button>
    <button id="hangupButton">Hang Up</button>
  </div>

  <script src="https://webrtc.github.io/adapter/adapter-latest.js"></script>
  <script src="js/main.js"></script>
</body>
</html>

安装 RTCPeerConnection 代码

main.js 替换为 step-02 文件夹中的版本。

拨打电话

打开 index.html,点击 Start 按钮以从网络摄像头获取视频,然后点击 Call 以建立对等连接。您应该会在两个视频元素中看到使用网络摄像头录制的相同视频。请查看浏览器控制台,了解 WebRTC 日志记录。

运作方式

此步骤会完成很多操作...

WebRTC 使用 RTCPeerConnection API 设置连接,以便在 WebRTC 客户端(称为对等设备)之间流式传输视频。

在此示例中,这两个 RTCPeerConnection 对象位于同一页面上:pc1pc2。虽然实用性不高,但很适合演示 API 的运作方式。

在 WebRTC 对等设备之间设置通话涉及三项任务:

  • 为各调用端分别创建一个 RTCPeerConnection,并在每端添加来自 getUserMedia() 的本地数据流。
  • 获取和分享网络信息:可能的连接端点称为 ICE 候选对象。
  • 获取和分享本地和远程说明:采用 SDP 格式的本地媒体元数据。

假设 Alice 和 Bob 想使用 RTCPeerConnection 设置视频聊天。

首先,Alice 和 Bob 交换网络信息。“寻找候选对象”这一表达式是指使用 ICE 框架查找网络接口和端口的过程。

  1. Alice 创建了一个包含 onicecandidate (addEventListener('icecandidate')) 处理程序的 RTCPeerConnection 对象。这对应于 main.js 中的以下代码:
let localPeerConnection;
localPeerConnection = new RTCPeerConnection(servers);
localPeerConnection.addEventListener('icecandidate', handleConnection);
localPeerConnection.addEventListener(
    'iceconnectionstatechange', handleConnectionChange);
  1. Alice 调用 getUserMedia() 并添加传递给该目标的数据流:
navigator.mediaDevices.getUserMedia(mediaStreamConstraints).
  then(gotLocalMediaStream).
  catch(handleLocalMediaStreamError);
function gotLocalMediaStream(mediaStream) {
  localVideo.srcObject = mediaStream;
  localStream = mediaStream;
  trace('Received local stream.');
  callButton.disabled = false;  // Enable call button.
}
localPeerConnection.addStream(localStream);
trace('Added local stream to localPeerConnection.');
  1. 当候选网络变为可用时,系统会调用第 1 步中的 onicecandidate 处理程序。
  2. Alice 向 Bob 发送经过序列化的候选数据。在真实应用中,此过程(称为信令)通过消息传递服务进行,您将在后续步骤中了解如何执行此操作。当然,在此步骤中,这两个 RTCPeerConnection 对象位于同一页面上,可以直接进行通信,无需进行外部消息传递。
  3. 当 Bob 从 Alice 收到候选消息时,他会调用 addIceCandidate(),将候选对象添加到远程对等设备描述中:
function handleConnection(event) {
  const peerConnection = event.target;
  const iceCandidate = event.candidate;

  if (iceCandidate) {
    const newIceCandidate = new RTCIceCandidate(iceCandidate);
    const otherPeer = getOtherPeer(peerConnection);

    otherPeer.addIceCandidate(newIceCandidate)
      .then(() => {
        handleConnectionSuccess(peerConnection);
      }).catch((error) => {
        handleConnectionFailure(peerConnection, error);
      });

    trace(`${getPeerName(peerConnection)} ICE candidate:\n` +
          `${event.candidate.candidate}.`);
  }
}

WebRTC 对等设备还需要发现和交换本地和远程音频和视频媒体信息,如分辨率和编解码器功能。发出交换媒体配置信息的信号使用会话描述协议格式(称为 SDP)继续交换元数据 BLOB(称为提议应答):

  1. Alice 运行 RTCPeerConnection createOffer() 方法。返回的 promise 提供一个 RTCSessionDescription:Alice 的本地会话描述:
trace('localPeerConnection createOffer start.');
localPeerConnection.createOffer(offerOptions)
  .then(createdOffer).catch(setSessionDescriptionError);
  1. 如果成功,Alice 会使用 setLocalDescription() 设置本地说明,然后通过其信令通道将会话描述发送给 Bob。
  2. Bob 使用 setRemoteDescription() 将 Alice 发给他的描述设置为远程描述。
  3. Bob 运行 RTCPeerConnection createAnswer() 方法,并向其传递从 Alice 收到的远程描述,以便生成与她的描述相符的本地会话。createAnswer() promise 会传递一个 RTCSessionDescription:Bob 将其设置为本地描述并将其发送给 Alice。
  4. 在 Alice 获取 Bob 的会话描述后,她通过 setRemoteDescription() 将其设置为远程描述。
// Logs offer creation and sets peer connection session descriptions.
function createdOffer(description) {
  trace(`Offer from localPeerConnection:\n${description.sdp}`);

  trace('localPeerConnection setLocalDescription start.');
  localPeerConnection.setLocalDescription(description)
    .then(() => {
      setLocalDescriptionSuccess(localPeerConnection);
    }).catch(setSessionDescriptionError);

  trace('remotePeerConnection setRemoteDescription start.');
  remotePeerConnection.setRemoteDescription(description)
    .then(() => {
      setRemoteDescriptionSuccess(remotePeerConnection);
    }).catch(setSessionDescriptionError);

  trace('remotePeerConnection createAnswer start.');
  remotePeerConnection.createAnswer()
    .then(createdAnswer)
    .catch(setSessionDescriptionError);
}

// Logs answer to offer creation and sets peer connection session descriptions.
function createdAnswer(description) {
  trace(`Answer from remotePeerConnection:\n${description.sdp}.`);

  trace('remotePeerConnection setLocalDescription start.');
  remotePeerConnection.setLocalDescription(description)
    .then(() => {
      setLocalDescriptionSuccess(remotePeerConnection);
    }).catch(setSessionDescriptionError);

  trace('localPeerConnection setRemoteDescription start.');
  localPeerConnection.setRemoteDescription(description)
    .then(() => {
      setRemoteDescriptionSuccess(localPeerConnection);
    }).catch(setSessionDescriptionError);
}
  1. Ping!

奖励分

  1. 查看 chrome://webrtc-internals。本页提供 WebRTC 统计信息和调试数据。(如需查看 Chrome 网址的完整列表,请前往 chrome://about。)
  2. 使用 CSS 设置页面的样式:
  • 并排显示视频。
  • 让按钮保持相同宽度,但使用较大字体。
  • 确保布局适用于移动设备。
  1. 在 Chrome 开发者工具控制台中,查看 localStreamlocalPeerConnectionremotePeerConnection
  2. 在控制台中,查看 localPeerConnectionpc1.localDescription。SDP 格式是什么样的?

要点回顾

在此步骤中,您学习了如何:

  • 使用 WebRTC shim adapter.js 抽象化浏览器差异。
  • 使用 RTCPeerConnection API 流式传输视频。
  • 控制媒体捕获和流式传输。
  • 在对等方之间共享媒体和网络信息,以启用 WebRTC 通话。

此步骤的完整版本位于 step-2 文件夹中。

提示

  • 在此步骤中,您将学习很多内容!如需查找更详细地说明 RTCPeerConnection 的其他资源,请访问 webrtc.org。如果您想使用 WebRTC,但不想处理 API,本页会提供一些 JavaScript 框架建议。
  • 如需详细了解 adapter.js shim,请参阅 adapter.js GitHub 代码库
  • 想看看全球最出色的视频聊天应用是什么样的吗?请查看 AppRTC,这是 WebRTC 项目用于 WebRTC 调用的规范应用:应用代码。通话设置时间不到 500 毫秒。

最佳做法

  • 为确保您的代码在未来可继续使用,请使用基于 Promise 的新 API,并通过 adapter.js 实现与不支持这些 API 的浏览器的兼容性。

后续步骤

此步骤展示了如何使用 WebRTC 在对等设备之间流式传输视频,但此 Codelab 也与数据有关!

在下一步中,了解如何使用 RTCDataChannel 流式传输任意数据。

6. 使用 RTCDataChannel 交换数据

学习内容

  • 如何在 WebRTC 端点(对等互连)之间交换数据。

此步骤的完整版本位于 step-03 文件夹中。

更新 HTML

在这一步中,您将使用 WebRTC 数据通道在同一页面上的两个 textarea 元素之间发送文本。这不是很实用,但确实演示了如何使用 WebRTC 共享数据和流式传输视频。

index.html 中移除视频和按钮元素,并将其替换为以下 HTML:

<textarea id="dataChannelSend" disabled
    placeholder="Press Start, enter some text, then press Send."></textarea>
<textarea id="dataChannelReceive" disabled></textarea>

<div id="buttons">
  <button id="startButton">Start</button>
  <button id="sendButton">Send</button>
  <button id="closeButton">Stop</button>
</div>

一个文本区用于输入文本,另一个用于显示在对等设备间流式传输的文本。

index.html 现在应如下所示:

<!DOCTYPE html>
<html>

<head>

  <title>Realtime communication with WebRTC</title>

  <link rel="stylesheet" href="css/main.css" />

</head>

<body>

  <h1>Realtime communication with WebRTC</h1>

  <textarea id="dataChannelSend" disabled
    placeholder="Press Start, enter some text, then press Send."></textarea>
  <textarea id="dataChannelReceive" disabled></textarea>

  <div id="buttons">
    <button id="startButton">Start</button>
    <button id="sendButton">Send</button>
    <button id="closeButton">Stop</button>
  </div>

  <script src="https://webrtc.github.io/adapter/adapter-latest.js"></script>
  <script src="js/main.js"></script>

</body>

</html>

更新 JavaScript

main.js 替换为 step-03/js/main.js 的内容。

尝试在对等设备之间流式传输数据:打开 index.html,按 Start 设置对等连接,在左侧的 textarea 中输入一些文本,然后点击 Send,使用 WebRTC 数据通道传输文本。

运作方式

此代码使用 RTCPeerConnection 和 RTCDataChannel 实现短信交换。

此步骤中的大部分代码与 RTCPeerConnection 示例中的代码相同。

sendData()createConnection() 函数的代码大多是新的:

function createConnection() {
  dataChannelSend.placeholder = '';
  var servers = null;
  pcConstraint = null;
  dataConstraint = null;
  trace('Using SCTP based data channels');
  // For SCTP, reliable and ordered delivery is true by default.
  // Add localConnection to global scope to make it visible
  // from the browser console.
  window.localConnection = localConnection =
      new RTCPeerConnection(servers, pcConstraint);
  trace('Created local peer connection object localConnection');

  sendChannel = localConnection.createDataChannel('sendDataChannel',
      dataConstraint);
  trace('Created send data channel');

  localConnection.onicecandidate = iceCallback1;
  sendChannel.onopen = onSendChannelStateChange;
  sendChannel.onclose = onSendChannelStateChange;

  // Add remoteConnection to global scope to make it visible
  // from the browser console.
  window.remoteConnection = remoteConnection =
      new RTCPeerConnection(servers, pcConstraint);
  trace('Created remote peer connection object remoteConnection');

  remoteConnection.onicecandidate = iceCallback2;
  remoteConnection.ondatachannel = receiveChannelCallback;

  localConnection.createOffer().then(
    gotDescription1,
    onCreateSessionDescriptionError
  );
  startButton.disabled = true;
  closeButton.disabled = false;
}

function sendData() {
  var data = dataChannelSend.value;
  sendChannel.send(data);
  trace('Sent Data: ' + data);
}

我们故意让 RTCDataChannel 的语法类似于使用 send() 方法和 message 事件的 WebSocket。

请注意这里使用 dataConstraint。数据通道可以配置为启用不同类型的数据共享,例如优先考虑消息传递的可靠性而非性能。如需详细了解相关选项,请访问 Mozilla 开发者网络

奖励分

  1. 借助 SCTP(WebRTC 数据通道使用的协议),可靠且有序的数据传送默认处于启用状态。在什么情况下,RTCDataChannel 需要提供可靠的数据传送,在什么情况下,性能可能更为重要(即使这意味着会丢失部分数据)?
  2. 使用 CSS 改善页面布局,并为“dataChannelReceive”文本区域添加占位符属性。
  3. 在移动设备上测试网页。

要点回顾

在此步骤中,您学习了如何:

  • 在两个 WebRTC 对等互连方之间建立连接。
  • 在对等互连设备之间交换文本数据。

此步骤的完整版本位于 step-03 文件夹中。

了解详情

后续步骤

您已了解如何在同一页面上的对等设备之间交换数据,但如何在不同的计算机之间交换数据呢?首先,您需要设置信令通道来交换元数据消息。请在下一步中了解具体方法!

7. 设置信令服务以交换消息

学习内容

在此步骤中,您将了解如何:

  • 使用 npm 安装 package.json 中指定的项目依赖项
  • 运行 Node.js 服务器并使用 node-static 传送静态文件。
  • 使用 Socket.IO 在 Node.js 上设置消息传递服务。
  • 使用该功能创建“聊天室”并交换消息。

此步骤的完整版本位于 step-04 文件夹中。

概念

为了设置和维护 WebRTC 通话,WebRTC 客户端(对等互连方)需要交换元数据:

  • 候选(网络)信息。
  • 提供有关媒体(例如分辨率和编解码器)信息的 offeranswer 消息。

换句话说,在进行音频、视频或数据的对等流式传输之前,需要先交换元数据。此过程称为“发出信号”

在前面的步骤中,发送方和接收方 RTCPeerConnection 对象位于同一网页上,因此“信令”只是在对象之间传递元数据。

在实际应用中,发送方和接收方 RTCPeerConnection 在不同设备上的网页中运行,您需要一种方法来让它们通信元数据。

为此,您需要使用信令服务器:一种可以在 WebRTC 客户端(对等方)之间传递消息的服务器。实际消息是纯文本:字符串化的 JavaScript 对象。

前提条件:安装 Node.js

为了运行此 Codelab 的后续步骤(文件夹 step-04step-06),您需要使用 Node.js 在本地主机上运行服务器。

您可以从此链接下载并安装 Node.js,也可以通过您偏好的软件包管理器进行安装。

安装完成后,您将能够导入后续步骤(运行 npm install)所需的依赖项,以及运行小型本地主机服务器来执行 Codelab(运行 node index.js)。这些命令将在需要时在后面进行说明。

关于此应用

WebRTC 使用客户端 JavaScript API,但在实际使用中还需要信令(消息传递)服务器以及 STUN 和 TURN 服务器。如需了解详情,可点击此处

在此步骤中,您将使用 Socket.IO Node.js 模块和 JavaScript 库构建一个简单的 Node.js 信令服务器,用于消息传递。具备 Node.js 和 Socket.IO 方面的经验会有所帮助,但不是必需的;消息传递组件非常简单。

在此示例中,服务器(Node.js 应用)是在 index.js 中实现的,在其上运行的客户端(Web 应用)是在 index.html 中实现的。

此步骤中的 Node.js 应用有两个任务。

首先,它充当消息中继:

socket.on('message', function (message) {
  log('Got message: ', message);
  socket.broadcast.emit('message', message);
});

其次,它负责管理 WebRTC 视频聊天“室”:

if (numClients === 0) {
  socket.join(room);
  socket.emit('created', room, socket.id);
} else if (numClients === 1) {
  socket.join(room);
  socket.emit('joined', room, socket.id);
  io.sockets.in(room).emit('ready');
} else { // max two clients
  socket.emit('full', room);
}

简单的 WebRTC 应用允许最多两个对等设备共用一个房间。

HTML 和 JavaScript

更新 index.html,使其如下所示:

<!DOCTYPE html>
<html>

<head>

  <title>Realtime communication with WebRTC</title>

  <link rel="stylesheet" href="css/main.css" />

</head>

<body>

  <h1>Realtime communication with WebRTC</h1>

  <script src="/socket.io/socket.io.js"></script>
  <script src="js/main.js"></script>
  
</body>

</html>

在这一步,您在页面上将不会看到任何内容:所有日志记录均在浏览器控制台中完成。(如需在 Chrome 中查看控制台,请按 Ctrl-Shift-J;如果您使用的是 Mac,请按 Command-Option-J。)

js/main.js 替换为以下内容:

'use strict';

var isInitiator;

window.room = prompt("Enter room name:");

var socket = io.connect();

if (room !== "") {
  console.log('Message from client: Asking to join room ' + room);
  socket.emit('create or join', room);
}

socket.on('created', function(room, clientId) {
  isInitiator = true;
});

socket.on('full', function(room) {
  console.log('Message from client: Room ' + room + ' is full :^(');
});

socket.on('ipaddr', function(ipaddr) {
  console.log('Message from client: Server IP address is ' + ipaddr);
});

socket.on('joined', function(room, clientId) {
  isInitiator = false;
});

socket.on('log', function(array) {
  console.log.apply(console, array);
});

设置 Socket.IO 以在 Node.js 上运行

在 HTML 文件中,您可能已看到您使用的是 Socket.IO 文件:

<script src="/socket.io/socket.io.js"></script>

work 目录的顶层,创建一个名为 package.json 的文件,其中包含以下内容:

{
  "name": "webrtc-codelab",
  "version": "0.0.1",
  "description": "WebRTC codelab",
  "dependencies": {
    "node-static": "^0.7.10",
    "socket.io": "^1.2.0"
  }
}

这是一个应用清单,用于告知 Node Package Manager (npm) 要安装哪些项目依赖项。

如需安装依赖项(例如 /socket.io/socket.io.js),请从命令行终端在 work 目录中运行以下命令:

npm install

您应该会看到结尾如下所示的安装日志:

3ab06b7bcc7664b9.png

如您所见,npm 已安装 package.json 中定义的依赖项。

work 目录(而非 js 目录)的顶层创建一个新文件 index.js,并添加以下代码:

'use strict';

var os = require('os');
var nodeStatic = require('node-static');
var http = require('http');
var socketIO = require('socket.io');

var fileServer = new(nodeStatic.Server)();
var app = http.createServer(function(req, res) {
  fileServer.serve(req, res);
}).listen(8080);

var io = socketIO.listen(app);
io.sockets.on('connection', function(socket) {

  // convenience function to log server messages on the client
  function log() {
    var array = ['Message from server:'];
    array.push.apply(array, arguments);
    socket.emit('log', array);
  }

  socket.on('message', function(message) {
    log('Client said: ', message);
    // for a real app, would be room-only (not broadcast)
    socket.broadcast.emit('message', message);
  });

  socket.on('create or join', function(room) {
    log('Received request to create or join room ' + room);

    var clientsInRoom = io.sockets.adapter.rooms[room];
    var numClients = clientsInRoom ? Object.keys(clientsInRoom.sockets).length : 0;

    log('Room ' + room + ' now has ' + numClients + ' client(s)');

    if (numClients === 0) {
      socket.join(room);
      log('Client ID ' + socket.id + ' created room ' + room);
      socket.emit('created', room, socket.id);

    } else if (numClients === 1) {
      log('Client ID ' + socket.id + ' joined room ' + room);
      io.sockets.in(room).emit('join', room);
      socket.join(room);
      socket.emit('joined', room, socket.id);
      io.sockets.in(room).emit('ready');
    } else { // max two clients
      socket.emit('full', room);
    }
  });

  socket.on('ipaddr', function() {
    var ifaces = os.networkInterfaces();
    for (var dev in ifaces) {
      ifaces[dev].forEach(function(details) {
        if (details.family === 'IPv4' && details.address !== '127.0.0.1') {
          socket.emit('ipaddr', details.address);
        }
      });
    }
  });

});

从命令行终端,在 work 目录中运行以下命令:

node index.js

在浏览器中,打开 localhost:8080

每次打开此网址时,系统都会提示您输入房间名称。若要加入同一房间,每次都选择相同的房间名称,例如“foo”。

打开一个新标签页,然后再次打开 localhost:8080。选择相同的聊天室名称。

在第三个标签页或窗口中打开 localhost:8080。再次选择相同的聊天室名称。

查看每个标签页中的控制台:您应该会看到来自上述 JavaScript 的日志记录。

奖励分

  1. 有哪些可行的替代消息传送机制?您在使用“纯”WebSocket 时可能会遇到哪些问题?
  2. 扩缩此应用可能涉及哪些问题?您能开发一种方法来同时测试数千个或数百万个房间请求吗?
  3. 此应用使用 JavaScript 提示来获取房间名称。想办法通过网址获取房间名称。例如,根据 localhost:8080/foo 可得知房间名称为 foo

要点回顾

在此步骤中,您学习了如何:

  • 使用 npm 安装 package.json 中指定的项目依赖项
  • 运行 Node.js 服务器以提供静态文件。
  • 使用 socket.io 在 Node.js 上设置消息传递服务。
  • 使用该功能创建“聊天室”并交换消息。

此步骤的完整版本位于 step-04 文件夹中。

了解详情

后续步骤

了解如何使用信令来让两位用户建立对等连接。

8. 结合使用对等连接和信令

学习内容

在此步骤中,您将了解如何执行以下操作:

  • 使用在 Node.js 上运行的 Socket.IO 运行 WebRTC 信令服务
  • 使用该服务在对等方之间交换 WebRTC 元数据。

此步骤的完整版本位于 step-05 文件夹中。

替换 HTML 和 JavaScript

index.html 的内容替换为以下内容:

<!DOCTYPE html>
<html>

<head>

  <title>Realtime communication with WebRTC</title>

  <link rel="stylesheet" href="/css/main.css" />

</head>

<body>

  <h1>Realtime communication with WebRTC</h1>

  <div id="videos">
    <video id="localVideo" autoplay muted></video>
    <video id="remoteVideo" autoplay></video>
  </div>

  <script src="/socket.io/socket.io.js"></script>
  <script src="https://webrtc.github.io/adapter/adapter-latest.js"></script>
  <script src="js/main.js"></script>
  
</body>

</html>

js/main.js 替换为 step-05/js/main.js 的内容。

运行 Node.js 服务器

如果您没有从工作目录完成此 Codelab,则可能需要安装 step-05 文件夹或当前工作文件夹的依赖项。从工作目录运行以下命令:

npm install

安装完毕后,如果 Node.js 服务器未运行,请在 work 目录中调用以下命令来启动该服务器:

node index.js

请确保您使用的是上一步中实现 Socket.IO 的 index.js 版本。如需详细了解 Node 和 Socket IO,请查看“设置信令服务以交换消息”部分。

在浏览器中,打开 localhost:8080

在新标签页或窗口中再次打开 localhost:8080。一个视频元素将显示来自 getUserMedia() 的本地数据流,另一个将显示通过 RTCPeerconnection 流式传输的“远程”视频。

在浏览器控制台中查看日志记录。

奖励积分

  1. 此应用仅支持一对一视频聊天。如何更改设计以便让多人共用同一个视频聊天室?
  2. 示例中的房间名称 foo 经过硬编码。启用其他会议室名称的最佳方式是什么?
  3. 用户如何共享房间名称?尝试构建一种共享房间名称的替代方法。
  4. 如何更改应用

要点回顾

在此步骤中,您学习了如何:

  • 使用在 Node.js 上运行的 Socket.IO 运行 WebRTC 信令服务。
  • 使用该服务在对等方之间交换 WebRTC 元数据。

此步骤的完整版本位于 step-05 文件夹中。

提示

  • 您可以通过 chrome://webrtc-internals 查看 WebRTC 统计信息和调试数据。
  • 您可以使用 test.webrtc.org 检查本地环境,并测试摄像头和麦克风。
  • 如果您遇到缓存方面的奇怪问题,请尝试执行以下操作:
  • 按住 Ctrl 键并点击重新加载按钮,执行硬刷新
  • 重启浏览器
  • 从命令行运行 npm cache clean

后续步骤

了解如何拍照、获取图片数据,以及在远程对等方之间共享这些数据。

9. 拍照并通过数据通道分享

学习内容

在此步骤中,您将学习如何:

  • 拍照并使用 canvas 元素从中获取数据。
  • 与远程用户交换图片数据。

此步骤的完整版本位于 step-06 文件夹中。

运作方式

之前,您学习了如何使用 RTCDataChannel 收发短信。

此步骤可让您共享整个文件:在此示例中,即通过 getUserMedia() 拍摄的照片。

此步骤的核心部分如下所示:

  1. 建立数据通道。请注意,在此步骤中,您无需向对等连接添加任何媒体流。
  2. 使用 getUserMedia() 捕获用户的网络摄像头视频串流:
var video = document.getElementById('video');

function grabWebCamVideo() {
  console.log('Getting user media (video) ...');
  navigator.mediaDevices.getUserMedia({
    video: true
  })
  .then(gotStream)
  .catch(function(e) {
    alert('getUserMedia() error: ' + e.name);
  });
}
  1. 当用户点击 Snap 按钮时,获取视频串流的快照(视频帧),并将其显示在 canvas 元素中:
var photo = document.getElementById('photo');
var photoContext = photo.getContext('2d');

function snapPhoto() {
  photoContext.drawImage(video, 0, 0, photo.width, photo.height);
  show(photo, sendBtn);
}
  1. 当用户点击 Send 按钮时,将图片转换为字节并通过数据通道发送这些字节:
function sendPhoto() {
  // Split data channel message in chunks of this byte length.
  var CHUNK_LEN = 64000;
  var img = photoContext.getImageData(0, 0, photoContextW, photoContextH),
    len = img.data.byteLength,
    n = len / CHUNK_LEN | 0;

  console.log('Sending a total of ' + len + ' byte(s)');
  dataChannel.send(len);

  // split the photo and send in chunks of about 64KB
  for (var i = 0; i < n; i++) {
    var start = i * CHUNK_LEN,
      end = (i + 1) * CHUNK_LEN;
    console.log(start + ' - ' + (end - 1));
    dataChannel.send(img.data.subarray(start, end));
  }

  // send the reminder, if any
  if (len % CHUNK_LEN) {
    console.log('last ' + len % CHUNK_LEN + ' byte(s)');
    dataChannel.send(img.data.subarray(n * CHUNK_LEN));
  }
}
  1. 接收端会将数据通道消息字节转换回图片,并向用户显示该图片:
function receiveDataChromeFactory() {
  var buf, count;

  return function onmessage(event) {
    if (typeof event.data === 'string') {
      buf = window.buf = new Uint8ClampedArray(parseInt(event.data));
      count = 0;
      console.log('Expecting a total of ' + buf.byteLength + ' bytes');
      return;
    }

    var data = new Uint8ClampedArray(event.data);
    buf.set(data, count);

    count += data.byteLength;
    console.log('count: ' + count);

    if (count === buf.byteLength) {
      // we're done: all data chunks have been received
      console.log('Done. Rendering photo.');
      renderPhoto(buf);
    }
  };
}

function renderPhoto(data) {
  var canvas = document.createElement('canvas');
  canvas.width = photoContextW;
  canvas.height = photoContextH;
  canvas.classList.add('incomingPhoto');
  // trail is the element holding the incoming images
  trail.insertBefore(canvas, trail.firstChild);

  var context = canvas.getContext('2d');
  var img = context.createImageData(photoContextW, photoContextH);
  img.data.set(data);
  context.putImageData(img, 0, 0);
}

获取代码

work 文件夹的内容替换为 step-06 的内容。现在,work 中的 index.html 文件看起来应如下所示**:**

<!DOCTYPE html>
<html>

<head>

  <title>Realtime communication with WebRTC</title>

  <link rel="stylesheet" href="/css/main.css" />

</head>

<body>

  <h1>Realtime communication with WebRTC</h1>

  <h2>
    <span>Room URL: </span><span id="url">...</span>
  </h2>

  <div id="videoCanvas">
    <video id="camera" autoplay></video>
    <canvas id="photo"></canvas>
  </div>

  <div id="buttons">
    <button id="snap">Snap</button><span> then </span><button id="send">Send</button>
    <span> or </span>
    <button id="snapAndSend">Snap &amp; Send</button>
  </div>

  <div id="incoming">
    <h2>Incoming photos</h2>
    <div id="trail"></div>
  </div>

  <script src="/socket.io/socket.io.js"></script>
  <script src="https://webrtc.github.io/adapter/adapter-latest.js"></script>
  <script src="js/main.js"></script>

</body>

</html>

如果您没有从工作目录完成此 Codelab,则可能需要安装 step-06 文件夹或当前工作文件夹的依赖项。您只需从工作目录运行以下命令:

npm install

安装完毕后,如果 Node.js 服务器未运行,请从 work 目录调用以下命令来启动它:

node index.js

请确保您使用的是实现了 Socket.IO 的 index.js 版本,并且如果您做出更改,请记得重启 Node.js 服务器。如需详细了解 Node 和 Socket IO,请查看“设置信令服务以交换消息”部分。

如有必要,请点击允许按钮,以允许应用使用您的网络摄像头。

此应用将创建一个随机房间 ID,并将该 ID 添加到相应网址中。从新的浏览器标签页或窗口的地址栏中打开该网址。

点击 Snap & Send 按钮,然后在另一个标签页的页面底部查看“Incoming”区域。此应用可在各个标签页之间转移照片。

您应该会看到与以下类似的内容:

911b40f36ba6ba8.png

奖励分

  1. 如何更改代码以便能够共享任意类型的文件?

了解详情

要点回顾

  • 如何使用 canvas 元素拍照并从中获取数据。
  • 如何与远程用户交换这些数据。

此步骤的完整版本位于 step-06 文件夹中。

10. 恭喜

您已构建了一个用于实时视频流式传输和数据交换的应用!

要点回顾

在此 Codelab 中,您学习了如何执行以下操作:

  • 使用网络摄像头获取视频。
  • 通过 RTCPeerConnection 流式传输视频。
  • 通过 RTCDataChannel 流式传输数据。
  • 设置信令服务以交换消息。
  • 结合使用对等连接和信令。
  • 拍照并通过数据通道分享。

后续步骤

了解详情

  • 您可访问 webrtc.org,获取一系列 WebRTC 入门资源。