1. 简介
WebRTC 是一个开源项目,旨在实现 Web 应用和原生应用中的音频、视频和数据实时通信。
WebRTC 有多个 JavaScript API - 点击链接可查看演示。
getUserMedia():拍摄音频和视频。MediaRecorder:录制音频和视频。RTCPeerConnection:在用户之间传输音频和视频。RTCDataChannel:在用户之间传输数据。
我可以在哪些地方使用 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 应用商店安装。

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

点击“Web Server”图标:

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

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

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

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

很显然,此应用目前不会执行任何有趣的操作,到目前为止,它只是一个极简的框架,用于确保您的网络服务器能够正常运行。您可以在后续步骤中添加功能和布局功能。
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,此时您应该会看到如下内容(当然包含网络摄像头的画面!):

运作方式
在 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 文件夹中。
提示
- 不要忘记
video元素上的autoplay属性。否则,您只会看到一个帧! getUserMedia()限制条件还有许多其他选项。请访问 webrtc.github.io/samples/src/content/peerconnection/constraints 查看演示。您会发现,该网站上有很多有趣的 WebRTC 示例。
最佳做法
- 确保视频元素不会溢出容器。我们添加了
width和max-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 对象位于同一页面上:pc1 和 pc2。虽然实用性不高,但很适合演示 API 的运作方式。
在 WebRTC 对等设备之间设置通话涉及三项任务:
- 为各调用端分别创建一个 RTCPeerConnection,并在每端添加来自
getUserMedia()的本地数据流。 - 获取和分享网络信息:可能的连接端点称为 ICE 候选对象。
- 获取和分享本地和远程说明:采用 SDP 格式的本地媒体元数据。
假设 Alice 和 Bob 想使用 RTCPeerConnection 设置视频聊天。
首先,Alice 和 Bob 交换网络信息。“寻找候选对象”这一表达式是指使用 ICE 框架查找网络接口和端口的过程。
- Alice 创建了一个包含
onicecandidate (addEventListener('icecandidate'))处理程序的 RTCPeerConnection 对象。这对应于 main.js 中的以下代码:
let localPeerConnection;
localPeerConnection = new RTCPeerConnection(servers);
localPeerConnection.addEventListener('icecandidate', handleConnection);
localPeerConnection.addEventListener(
'iceconnectionstatechange', handleConnectionChange);
- 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处理程序。 - Alice 向 Bob 发送经过序列化的候选数据。在真实应用中,此过程(称为信令)通过消息传递服务进行,您将在后续步骤中了解如何执行此操作。当然,在此步骤中,这两个 RTCPeerConnection 对象位于同一页面上,可以直接进行通信,无需进行外部消息传递。
- 当 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(称为提议和应答):
- Alice 运行 RTCPeerConnection
createOffer()方法。返回的 promise 提供一个 RTCSessionDescription:Alice 的本地会话描述:
trace('localPeerConnection createOffer start.');
localPeerConnection.createOffer(offerOptions)
.then(createdOffer).catch(setSessionDescriptionError);
- 如果成功,Alice 会使用
setLocalDescription()设置本地说明,然后通过其信令通道将会话描述发送给 Bob。 - Bob 使用
setRemoteDescription()将 Alice 发给他的描述设置为远程描述。 - Bob 运行 RTCPeerConnection
createAnswer()方法,并向其传递从 Alice 收到的远程描述,以便生成与她的描述相符的本地会话。createAnswer()promise 会传递一个 RTCSessionDescription:Bob 将其设置为本地描述并将其发送给 Alice。 - 在 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);
}
- Ping!
奖励分
- 查看 chrome://webrtc-internals。本页提供 WebRTC 统计信息和调试数据。(如需查看 Chrome 网址的完整列表,请前往 chrome://about。)
- 使用 CSS 设置页面的样式:
- 并排显示视频。
- 让按钮保持相同宽度,但使用较大字体。
- 确保布局适用于移动设备。
- 在 Chrome 开发者工具控制台中,查看
localStream、localPeerConnection和remotePeerConnection。 - 在控制台中,查看
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 开发者网络。
奖励分
- 借助 SCTP(WebRTC 数据通道使用的协议),可靠且有序的数据传送默认处于启用状态。在什么情况下,RTCDataChannel 需要提供可靠的数据传送,在什么情况下,性能可能更为重要(即使这意味着会丢失部分数据)?
- 使用 CSS 改善页面布局,并为“dataChannelReceive”文本区域添加占位符属性。
- 在移动设备上测试网页。
要点回顾
在此步骤中,您学习了如何:
- 在两个 WebRTC 对等互连方之间建立连接。
- 在对等互连设备之间交换文本数据。
此步骤的完整版本位于 step-03 文件夹中。
了解详情
- WebRTC 数据通道(虽然是几年前的文章,但仍值得一读)
- 为什么系统选择 SCTP 来实现 WebRTC 的数据通道?
后续步骤
您已了解如何在同一页面上的对等设备之间交换数据,但如何在不同的计算机之间交换数据呢?首先,您需要设置信令通道来交换元数据消息。请在下一步中了解具体方法!
7. 设置信令服务以交换消息
学习内容
在此步骤中,您将了解如何:
- 使用
npm安装 package.json 中指定的项目依赖项 - 运行 Node.js 服务器并使用 node-static 传送静态文件。
- 使用 Socket.IO 在 Node.js 上设置消息传递服务。
- 使用该功能创建“聊天室”并交换消息。
此步骤的完整版本位于 step-04 文件夹中。
概念
为了设置和维护 WebRTC 通话,WebRTC 客户端(对等互连方)需要交换元数据:
- 候选(网络)信息。
- 提供有关媒体(例如分辨率和编解码器)信息的 offer 和 answer 消息。
换句话说,在进行音频、视频或数据的对等流式传输之前,需要先交换元数据。此过程称为“发出信号”。
在前面的步骤中,发送方和接收方 RTCPeerConnection 对象位于同一网页上,因此“信令”只是在对象之间传递元数据。
在实际应用中,发送方和接收方 RTCPeerConnection 在不同设备上的网页中运行,您需要一种方法来让它们通信元数据。
为此,您需要使用信令服务器:一种可以在 WebRTC 客户端(对等方)之间传递消息的服务器。实际消息是纯文本:字符串化的 JavaScript 对象。
前提条件:安装 Node.js
为了运行此 Codelab 的后续步骤(文件夹 step-04 到 step-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
您应该会看到结尾如下所示的安装日志:

如您所见,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 的日志记录。
奖励分
- 有哪些可行的替代消息传送机制?您在使用“纯”WebSocket 时可能会遇到哪些问题?
- 扩缩此应用可能涉及哪些问题?您能开发一种方法来同时测试数千个或数百万个房间请求吗?
- 此应用使用 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 流式传输的“远程”视频。
在浏览器控制台中查看日志记录。
奖励积分
- 此应用仅支持一对一视频聊天。如何更改设计以便让多人共用同一个视频聊天室?
- 示例中的房间名称 foo 经过硬编码。启用其他会议室名称的最佳方式是什么?
- 用户如何共享房间名称?尝试构建一种共享房间名称的替代方法。
- 如何更改应用
要点回顾
在此步骤中,您学习了如何:
- 使用在 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() 拍摄的照片。
此步骤的核心部分如下所示:
- 建立数据通道。请注意,在此步骤中,您无需向对等连接添加任何媒体流。
- 使用
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);
});
}
- 当用户点击 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);
}
- 当用户点击 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));
}
}
- 接收端会将数据通道消息字节转换回图片,并向用户显示该图片:
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 & 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”区域。此应用可在各个标签页之间转移照片。
您应该会看到与以下类似的内容:

奖励分
- 如何更改代码以便能够共享任意类型的文件?
了解详情
- MediaStream Image Capture API:用于拍摄照片和控制摄像头的 API - 即将登陆您附近的浏览器!
- 用于录制音频和视频的 MediaRecorder API:演示、文档。
要点回顾
- 如何使用 canvas 元素拍照并从中获取数据。
- 如何与远程用户交换这些数据。
此步骤的完整版本位于 step-06 文件夹中。
10. 恭喜
您已构建了一个用于实时视频流式传输和数据交换的应用!
要点回顾
在此 Codelab 中,您学习了如何执行以下操作:
- 使用网络摄像头获取视频。
- 通过 RTCPeerConnection 流式传输视频。
- 通过 RTCDataChannel 流式传输数据。
- 设置信令服务以交换消息。
- 结合使用对等连接和信令。
- 拍照并通过数据通道分享。
后续步骤
- 查看规范 WebRTC 聊天应用 AppRTC 的代码和架构:应用、代码。
- 试用 github.com/webrtc/samples 中的实时演示。
了解详情
- 您可访问 webrtc.org,获取一系列 WebRTC 入门资源。