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,或者使用您自己选择的 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 应用商店安装。
安装 Web Server for Chrome 应用后,请从书签栏、新标签页或应用启动器中点击 Chrome 应用快捷方式:
点击 Web Server 图标:
接下来,您会看到此对话框,您可以在其中配置本地网络服务器:
点击选择文件夹按钮,然后选择您刚刚创建的 work 文件夹。这样,您就可以通过“网络服务器”对话框中网络服务器网址部分中突出显示的网址查看您在 Chrome 中正在进行的工作。
在选项下,选中自动显示 index.html 旁边的复选框,如下所示:
将标有 Web Server: STARTED(网络服务器:已启动)的切换开关向左滑动,然后向右滑动,以停止和重启服务器。
现在,点击突出显示的 Web Server 网址,在 Web 浏览器中访问您的工作网站。您应该会看到类似如下内容的页面:work/index.html:
显然,此应用程序还没有做任何有趣的事情 - 目前为止,它只是我们用来确保您的网络服务器正常运行的最小框架。您将在后续步骤中添加功能和布局功能。
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,您应该会看到类似下图的内容(当然,其中还包括通过网络摄像头拍摄的视图!):
工作原理
在调用 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 文件夹中。
提示
- 不要忘记
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 是一个用于进行 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 对象在同一页面上:pc1
和 pc2
。虽然没有太多的实际用途,但非常适合演示 API 的工作原理。
在 WebRTC 对等体之间设置通话涉及三项任务:
- 为通话的每一端创建一个 RTCPeerConnection,并在每端添加来自
getUserMedia()
的本地流。 - 获取和共享网络信息:潜在连接端点称为 ICE 候选网络。
- 获取并共享本地和远程说明:SDP 格式的本地媒体相关元数据。
假设小红和小刚想使用 RTCPeerConnection 设置视频聊天。
首先,小丽和小刚交换了网络信息。“查找候选人”这一表达式是指使用 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。 - Bob 使用
setRemoteDescription()
将 Alice 发送给他的说明设置为远程说明。 - Bob 运行 RTCPeerConnection
createAnswer()
方法,向其传递从 Alice 获取的远程说明,以便生成与 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 调用的规范应用:app、code。通话建立时间低于 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 开发者网络,详细了解各种选项。
奖励分
- 使用 SCTP 时,WebRTC 数据通道使用的协议以及可靠有序的数据传输默认处于开启状态。RTCDataChannel 何时可能需要可靠地传输数据?何时性能更重要(即使这意味着丢失一些数据)?
- 使用 CSS 改进页面布局,并向“dataChannelReceive”文本区域。
- 在移动设备上测试网页。
要点回顾
在此步骤中,您学习了如何:
- 在两个 WebRTC 对等方之间建立连接。
- 在对等方之间交换文本数据。
此步骤的完整版本位于 step-03 文件夹中。
了解详情
- WebRTC 数据渠道(已经有几年了,但仍值得一读)
- 为什么为 WebRTC 的数据通道选择 SCTP?
后续步骤
您已了解如何在同一页面的对等方之间交换数据,但如何在不同机器之间实现这一目标呢?首先,您需要设置一个信号信道以交换元数据消息。在下一步中了解具体方法!
7. 设置信令服务以交换消息
学习内容
在此步骤中,您将了解如何:
- 使用
npm
安装 package.json 中指定的项目依赖项 - 运行 Node.js 服务器并使用 node-static 传送静态文件。
- 使用 Socket.IO 在 Node.js 上设置消息传递服务。
- 使用它来创建“聊天室”和交换消息。
此步骤的完整版本位于 step-04 文件夹中。
概念
要设置和维护 WebRTC 通话,WebRTC 客户端(对等方)需要交换元数据:
- 候选(网络)信息。
- Offer 和 answer 消息,用于提供分辨率和编解码器等媒体相关信息。
换言之,需要先交换元数据,然后才能进行音频、视频或数据的点对点流式传输。此过程称为“信令”。
在前面的步骤中,发送者和接收者的 RTCPeerConnection 对象位于同一页面上,因此只需在对象之间传递元数据即可。
在实际应用中,发送器和接收器 RTCPeerConnections 在不同设备上的网页中运行,因此您需要一种方法来传递元数据。
为此,您需要使用信令服务器,即可在 WebRTC 客户端(对等方)之间传递消息的服务器。实际的消息是纯文本:即字符串化的 JavaScript 对象。
必备条件:安装 Node.js
为了执行此 Codelab(文件夹 step-04 到 step-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
您应该会看到结束如下的安装日志:
如您所见,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 中的日志记录。
奖励分
- 可以使用哪些替代消息机制?使用“pure”时可能会遇到哪些问题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 服务器
如果您不从 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 在线播放的视频。
在浏览器控制台中查看日志记录。
奖励积分
- 此应用仅支持一对一视频聊天。如何更改设计,让多人共用同一个视频聊天室?
- 此示例对会议室名称 foo 进行硬编码。如要启用其他聊天室名称,最佳方式是什么?
- 用户会如何分享会议室名称?尝试构建替代聊天室名称的替代方案。
- 如何更改此应用
要点回顾
在此步骤中,您学习了如何:
- 使用在 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()
拍摄的照片。
此步骤的核心部分如下:
- 建立数据渠道。请注意,在此步骤中,您不会向对等连接添加任何媒体流。
- 使用
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>
如果您不从 work 目录执行此 Codelab,则可能需要安装 step-06 文件夹或当前工作文件夹的依赖项。只需从工作目录运行以下命令:
npm install
安装后,如果 Node.js 服务器未运行,请从 work 目录调用以下命令来启动它:
node index.js
确保您使用的是实现 Socket.IO 的 index.js 版本,并且在进行更改后记得重启 Node.js 服务器。如需详细了解 Node 和 Socket IO,请参阅“设置信号服务以交换消息”部分。
如有必要,请点击允许按钮,以允许该应用使用您的摄像头。
该应用会随机创建一个房间 ID,并将该 ID 添加到网址中。在新的浏览器标签页或窗口中,通过地址栏打开该网址。
点击贴靠和发送按钮,然后查看页面底部其他标签页中的“接收”区域。该应用会在标签页之间传输照片。
您应该会看到与以下类似的内容:
奖励分
- 如何更改代码才能共享任何类型的文件?
了解详情
- MediaStream Image Capture API:一个用于拍照和控制摄像头的 API - 即将在您附近的浏览器中推出!
- 用于录制音频和视频的 MediaRecorder API:演示、文档。
要点回顾
- 如何使用画布元素拍摄照片并获取照片中的数据。
- 如何与远程用户交换这些数据。
此步骤的完整版本位于 step-06 文件夹中。
10. 恭喜
您构建了一个可以进行实时视频串流和数据交换的应用!
要点回顾
在此 Codelab 中,您学习了如何执行以下操作:
- 通过你的摄像头获取视频。
- 通过 RTCPeerConnection 流式传输视频。
- 使用 RTCDataChannel 流式传输数据。
- 设置信令服务以交换消息。
- 合并对等连接和信号传输。
- 拍摄照片,然后通过数据渠道分享。
后续步骤
- 查看规范 WebRTC 聊天应用 AppRTC 的代码和架构:应用、代码。
- 试用 github.com/webrtc/samples 中的实时演示。
了解详情
- webrtc.org 上提供了一系列 WebRTC 入门资源。