利用 WebRTC 实现实时通信

1. 准备工作

此 Codelab 会教您如何构建应用,以使用网络摄像头获取视频和拍摄快照,并利用 WebRTC 点对点分享这些视频和快照。您还将学习如何使用核心 WebRTC API 并通过 Node.js 设置消息传递服务器。

前提条件

  • HTML、CSS 和 JavaScript 的基本知识

构建内容

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

所需条件

  • Chrome 47 或更高版本
  • Web Server for Chrome 或您选择的网络服务器
  • 您选择的文本编辑器
  • Node.js

2. 获取示例代码

下载代码

  1. 如果您熟悉 Git,请运行以下命令,以从 GitHub 克隆此 Codelab 的代码:
git clone https://github.com/googlecodelabs/webrtc-web

或者,点击此链接下载 ZIP 文件格式的代码:

下载源代码

  1. 打开下载的 ZIP 文件,将名为 webrtc-web-master 的项目文件夹解压缩,其中包含一个文件夹(对应此 Codelab 的每个步骤)以及您需要的所有资源。

您可以在名为 work 的目录中处理所有代码。

step-nn 文件夹包含此 Codelab 的每个步骤的已完成版本,以供参考。

安装并验证网络服务器

尽管您可以随意使用自己的网络服务器,但此 Codelab 旨在与 Web Server for Chrome 配合使用。

  1. 如果您没有 Web Server for Chrome,请点击此链接从 Chrome 网上应用店安装它:

安装 Web Server for Chrome

d0a4649b4920cf3.png

  1. 点击添加至 Chrome,这会安装 Web Server for Chrome 并自动在新标签页中打开您的 Google 应用。
  2. 点击 Web Server

27fce4494f641883.png

系统会显示一个对话框,您可以在其中配置本地网络服务器:

a300381a486b9e22.png

  1. 点击 Choose Folder
  2. 选择您刚刚创建的 work 文件夹。

Web Server URL(s) 下方,您会看到可供您在 Chrome 中查看工作进展情况的

网址。

  1. Options (may require restart) 下方,选中 Automatically show index.html 复选框。
  2. 切换 Web Server: Started 两次以停止然后重启服务器。

f23cafb3993dfac1.png

  1. 点击 Web Server URL(s) 下方的网址,在网络浏览器中查看您的工作。

您应该会看到如下所示的页面,它对应于 work/index.html

18a705cb6ccc5181.png

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

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

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

添加 HTML 格式的 dash

复制此代码并将其粘贴到 work 目录下的 index.html 文件中,以添加 videoscript 元素:

<!DOCTYPE html>
<html>

<head>

  <title>Real-time communication with WebRTC</title>

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

</head>

<body>

  <h1>Real-time communication with WebRTC</h1>

  <video autoplay playsinline></video>

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

</body>

</html>

添加 JavaScript 格式的 pinch

复制此代码并将其粘贴到 js 文件夹中的 main.js 文件内:

'use strict';

// In this codelab, you  only stream video, not audio (video: true).
const mediaStreamConstraints = {
  video: true,
};

// The video element where the stream is displayed
const localVideo = document.querySelector('video');

// The local stream that's displayed on the video
let localStream;

// Handle success and add the MediaStream to the video element
function gotLocalMediaStream(mediaStream) {
  localStream = mediaStream;
  localVideo.srcObject = mediaStream;
}

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

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

试用

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

9297048e43ed0f3d.png

运作方式

getUserMedia() 调用之后,浏览器会请求摄像头使用权限(如果这是当前来源的首个摄像头使用权限请求)。

如果成功,则返回 MediaStreammedia 元素可通过 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,,然后按 Enter(在 Mac 上,按 Return)。如需在 Chrome 中查看控制台,请按 Control+Shift+J(或者在 Mac 上,按 Command+Option+J)。
  • localStream.getVideoTracks() 会返回什么?
  • 调用 localStream.getVideoTracks()[0].stop()
  • 查看 Constraints 对象。将其更改为 {audio: true, video: true} 后会发生什么?
  • 视频元素的大小是多少?如何从 JavaScript 中获取视频的自然大小,而不是显示大小?请使用 Google Chrome 开发者工具进行检查。
  • 向视频元素添加 CSS 过滤器,如下所示:
video {
  filter: blur(4px) invert(1) opacity(0.5);
}
  • 添加 SVG 过滤器,如下所示:
video {
   filter: hue-rotate(180deg) saturate(200%);
 }

提示

最佳做法

确保视频元素不会溢出容器。此 Codelab 添加了 widthmax-width,用于设置视频的首选大小和最大大小。浏览器会自动计算高度。

video {
  max-width: 100%;
  width: 320px;
}

4. 通过 RTCPeerConnection API 流式传输视频

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

添加视频元素和控制按钮

index.html 文件中,将单个 video 元素替换为两个 video 元素和三个 button 元素:

<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 流式传输的同一视频。(在实际应用中,一个 video 元素会显示本地数据流,另一个显示远程数据流。)

添加 adapter.js shim

复制此脚本元素并将其粘贴到 main.js 的脚本元素上方:

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

现在,您的 index.html 文件看起来应如下所示:

<!DOCTYPE html>
<html>

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

<body>
  <h1>Real-time 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 文件夹中的版本。

执行调用

  1. 打开 index.html 文件。
  2. 点击 Start,使用网络摄像头获取视频。
  3. 点击 Call 以建立对等连接

您应该会在两个 video 元素中看到使用网络摄像头录制的相同视频。

  1. 请查看浏览器控制台,了解 WebRTC 日志记录。

运作方式

此步骤会完成很多操作。

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

在此示例中,这两个 RTCPeerConnection 对象位于同一页面上:pc1pc2

WebRTC 对等设备之间的调用设置涉及三项任务:

  1. 为各调用端分别创建一个 RTCPeerConnection,并在每端添加来自 getUserMedia() 的本地数据流。
  2. 获取和分享网络信息。

可能的连接端点称为 ICE 候选对象。

  1. 获取和分享本地和远程说明。

与本地媒体相关的元数据采用会话描述协议 (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. 当候选网络变为可用时,系统会调用第一步中的 onicecandidate 处理程序。
  2. Alice 向 Bob 发送经过序列化的候选数据。

在真实应用中,此过程称为“信令”,通过消息传递服务进行。您将在后续步骤中了解如何执行此操作。当然,在此步骤中,这两个 RTCPeerConnection 对象位于同一页面上,可以直接进行通信,无需进行外部消息传递。

  1. 当 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 收到的远程描述,以便生成与她的描述相符的本地会话。
  4. createAnswer() promise 会传递 RTCSessionDescription,Bob 将后者设置为本地描述并将其发送给 Alice。
  5. 在 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. 转到 chrome://webrtc-internals。

本页提供 WebRTC 统计信息和调试数据。(您可以前往 chrome://about 查找 Chrome 网址的完整列表。)

  1. 使用 CSS 设置页面的样式:
  2. 并排显示视频。
  3. 让按钮保持相同宽度,但使用较大字体。
  4. 确保布局适用于移动设备。
  5. 在 Chrome 开发者工具控制台中,查看 localStreamlocalPeerConnectionremotePeerConnection
  6. 在控制台中,查看 localPeerConnectionpc1.localDescription

SDP 格式是什么样的?

提示

  • 如需详细了解 adapter.js shim,请参阅 adapter.js GitHub 代码库
  • 请查看 AppRTC 及其代码,这是 WebRTC 项目用于 WebRTC 调用的规范应用。调用设置时间不到 500 毫秒。

最佳做法

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

5. 使用数据通道交换数据

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

更新 HTML

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

index.html, 中移除 videobutton 元素,并将其替换为以下 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>

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

现在,您的 index.html 文件看起来应如下所示:

<!DOCTYPE html>
<html>

<head>

  <title>Real-time communication with WebRTC</title>

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

</head>

<body>

  <h1>Real-time 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

  1. main.js 替换为 step-03/js/main.js 的内容。
  1. 尝试在对等设备之间流式传输数据:
  2. 打开 index.html
  3. 点击 Start 以设置对等连接。
  4. 在左侧的 textarea 中输入一些文本。
  5. 点击 Send,使用 WebRTC 数据通道传输文本。

运作方式

此代码使用 RTCPeerConnectionRTCDataChannel 实现短信交换。

此步骤中的大部分代码与 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。数据通道可以配置为启用不同类型的数据共享,例如优先考虑消息传递的可靠性而非性能。

获得奖励积分

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

了解详情

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

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

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

关于此应用

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

在此步骤中,您将使用 Socket.IO Node.js 模块和 JavaScript 库构建一个简单的 Node.js 信令服务器,用于消息传递。

在此示例中,服务器(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

  1. 更新 index.html,使其如下所示:
<!DOCTYPE html>
<html>

<head>

  <title>Real-time communication with WebRTC</title>

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

</head>

<body>

  <h1>Real-time communication with WebRTC</h1>

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

</body>

</html>

在这一步,您在页面上将不会看到任何内容。所有日志记录均在浏览器控制台中完成。如需在 Chrome 中查看控制台,请按 Control+Shift+J(或者在 Mac 上,按 Command+Option+J)。

  1. 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);
});

设置要在 Node.js 上运行的 Socket.IO 文件

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

<script src="/socket.io/socket.io.js"></script>
  1. 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) 要安装哪些项目

依赖项。

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

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

3ab06b7bcc7664b9.png

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

  1. 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);
        }
      });
    }
  });

});
  1. 从命令行终端,在 work 目录中运行以下命令:
node index.js
  1. 在浏览器中,转到 http://localhost:8080

每次转到此网址时,系统都会提示您输入房间名称。

若要加入同一房间,每次都输入相同的房间名称,例如 foo

  1. 打开一个新标签页,再次转到 http://localhost:8080,然后重新输入相同的房间名称。
  2. 打开另一个新标签页,再次转到 http://localhost:8080,然后重新输入相同的房间名称。
  3. 查看每个标签页中的控制台。

您应该会看到来自 JavaScript 的日志记录。

获得奖励积分

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

了解详情

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

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

替换 HTML 和 JavaScript

  1. index.html 的内容替换为以下代码:
<!DOCTYPE html>
<html>

<head>

  <title>Real-time communication with WebRTC</title>

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

</head>

<body>

  <h1>Real-time 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>
  1. js/main.js 替换为 step-05/js/main.js 的内容。

运行 Node.js 服务器

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

  1. 从工作目录运行以下命令:
npm install
  1. 安装完毕后,如果 Node.js 服务器未运行,请在 work 目录中运行以下命令来启动该服务器:
node index.js

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

  1. 在浏览器中,转到 http://localhost:8080。
  2. 打开一个新标签页,然后再次转到 http://localhost:8080。

一个 video 元素显示来自 getUserMedia() 的本地数据流,另一个元素显示通过 RTCPeerconnection 流式传输的远程视频。

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

获得奖****励积分

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

提示

  • 如需查找 WebRTC 统计信息和调试数据,请访问 chrome://webrtc-internals。
  • 使用 WebRTC 问题排查工具可以检查您的本地环境,并测试您的摄像头和麦克风。
  • 如果您遇到缓存方面的奇怪问题,请尝试执行以下操作:
  1. Control,然后点击重新加载此页
  2. 重启浏览器。
  3. 从命令行运行 npm cache clean

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

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

运作方式

之前,您学习了如何使用 RTCDataChannel 收发短信。此步骤可让您共享整个文件。在此示例中,照片是使用 getUserMedia() 拍摄的。

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

  1. 建立数据通道。

在此步骤中,您无需向对等连接添加任何媒体流。

  1. 使用 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 the 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 approximately 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 applicable.
  if (len % CHUNK_LEN) {
    console.log('last ' + len % CHUNK_LEN + ' byte(s)');
    dataChannel.send(img.data.subarray(n * CHUNK_LEN));
  }
}

接收端会将数据通道消息字节转换为图片,并向用户显示该图片:

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');
  // The trail is the element that holds 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);
}

获取代码

  1. work 文件夹的内容替换为 step-06 的内容。

现在,work 中的 index.html 文件看起来应如下所示**:**

<!DOCTYPE html>
<html>

<head>

  <title>Real-time communication with WebRTC</title>

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

</head>

<body>

  <h1>Real-time 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>
  1. 如果您没有从 work 目录完成此 Codelab,则可能需要安装 step-06 文件夹或当前工作文件夹的依赖项。您只需从工作目录运行以下命令:
npm install
  1. 安装完毕后,如果 Node.js 服务器未运行,请从 work 目录运行以下命令来启动它:
node index.js
    Make sure that you're using the version of `index.js` that implements Socket.IO and

如果您做出更改,请记得重启 Node.js 服务器。

如需详细了解 Node 和 Socket.IO,请查看“设置信令

服务以交换消息”部分。

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

此应用将创建一个随机房间 ID,并将该 ID 添加到相应网址中。

  1. 从新的浏览器标签页或窗口的地址栏中打开该网址。
  2. 点击 Snap & Send,然后在另一个标签页的页面底部查看 Incoming photos

此应用可在各个标签页之间转移照片。

您应会看到类似下图的内容:

911b40f36ba6ba8.png

获得奖励积分

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

了解详情

9. 恭喜

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

了解详情