使用 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,或者使用您自己选择的 Web 服务器。
  • 示例代码
  • 文本编辑器
  • 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 中每个步骤的已完成版本。这些内容可供参考。

安装并验证网络服务器

虽然您可以随意使用自己的 Web 服务器,但此 Codelab 的设计宗旨是与 Chrome Web 服务器很好地搭配使用。如果您尚未安装此应用,可以从 Chrome 应用商店安装。

6ddeb4aee53c0f0e

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

1d2b4aa977ab7e24

点击 Web Server 图标:

27fce4494f641883

接下来,您会看到此对话框,您可以在其中配置本地网络服务器:

屏幕截图:2016 年 2 月 18 日上午 11.48.14 png

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

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

屏幕截图:2016 年 2 月 18 日上午 11.56.30 png

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

屏幕截图:2016 年 2 月 18 日中午 12.22.18

现在,点击突出显示的 Web Server 网址,在 Web 浏览器中访问您的工作网站。您应该会看到类似如下内容的页面:work/index.html

18a705cb6ccc5181

显然,此应用程序还没有做任何有趣的事情 - 目前为止,它只是我们用来确保您的网络服务器正常运行的最小框架。您将在后续步骤中添加功能和布局功能。

4. 使用摄像头流式传输视频

学习内容

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

  • 通过你的摄像头获取视频串流。
  • 操控直播的播放。
  • 使用 CSS 和 SVG 处理视频。

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

一段 HTML 代码...

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

工作原理

在调用 getUserMedia() 之后,浏览器会向用户请求访问其相机的权限(如果这是第一次请求对当前源的相机访问权限)。如果成功,则返回一个 MediaStream,可供媒体元素通过 srcObject 属性使用:

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,然后按回车键。(要在 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 是一个用于进行 WebRTC 调用以流式传输视频和音频以及交换数据的 API。

此示例在同一页面上的两个 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,点击开始按钮以从网络摄像头获取视频,然后点击呼叫建立对等连接。您应该会在两个视频元素中看到同一个视频(来自您的网络摄像头)。访问浏览器控制台以查看 WebRTC 日志记录。

工作原理

此步骤会执行很多操作...

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

在此示例中,两个 RTCPeerConnection 对象在同一页面上:pc1pc2。虽然没有太多的实际用途,但非常适合演示 API 的工作原理。

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

  • 为通话的每一端创建一个 RTCPeerConnection,并在每端添加来自 getUserMedia() 的本地流。
  • 获取和共享网络信息:潜在连接端点称为 ICE 候选网络。
  • 获取并共享本地和远程说明:SDP 格式的本地媒体相关元数据。

假设小红和小刚想使用 RTCPeerConnection 设置视频聊天。

首先,小丽和小刚交换了网络信息。“查找候选人”这一表达式是指使用 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 的信号通道将此会话说明发送给 Bob。
  2. Bob 使用 setRemoteDescription() 将 Alice 发送给他的说明设置为远程说明。
  3. Bob 运行 RTCPeerConnection createAnswer() 方法,向其传递从 Alice 获取的远程说明,以便生成与 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 调用的规范应用:appcode。通话建立时间低于 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,按开始设置对等连接,在左侧的 textarea 中输入一些文本,然后点击发送,使用 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 的语法被特意设计为与 WebSocket 类似,具有 send() 方法和 message 事件。

请注意 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 对象位于同一页面上,因此只需在对象之间传递元数据即可。

在实际应用中,发送器和接收器 RTCPeerConnections 在不同设备上的网页中运行,因此您需要一种方法来传递元数据。

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

必备条件:安装 Node.js

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

你可以通过此链接或通过你偏好的软件包管理器下载并安装 Node.js。

安装后,您将能够导入后续步骤(运行 npm install)所需的依赖项,以及运行小型 localhost 服务器以执行此 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

如您所见,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. 可以使用哪些替代消息机制?使用“pure”时可能会遇到哪些问题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 服务器

如果您不从 work 目录执行此 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 文件夹中。

提示

  • 如需获取 WebRTC 统计信息和调试数据,请访问 chrome://webrtc-internals
  • 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>

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

npm install

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

node index.js

确保您使用的是实现 Socket.IO 的 index.js 版本,并且在进行更改后记得重启 Node.js 服务器。如需详细了解 Node 和 Socket IO,请参阅“设置信号服务以交换消息”部分。

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

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

点击贴靠和发送按钮,然后查看页面底部其他标签页中的“接收”区域。该应用会在标签页之间传输照片。

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

911b40f36ba6ba8

奖励分

  1. 如何更改代码才能共享任何类型的文件?

了解详情

要点回顾

  • 如何使用画布元素拍摄照片并获取照片中的数据。
  • 如何与远程用户交换这些数据。

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

10. 恭喜

您构建了一个可以进行实时视频串流和数据交换的应用!

要点回顾

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

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

后续步骤

了解详情

  • webrtc.org 上提供了一系列 WebRTC 入门资源。