使用屏幕捕获 API
在本文中,我们将探讨如何使用屏幕捕获 API 及其 getDisplayMedia() 方法来捕获屏幕的部分或全部内容,以便在 WebRTC 会议期间进行流式传输、录制或共享。
注意:值得注意的是,最新版本的 WebRTC adapter.js shim 包含了 getDisplayMedia() 的实现,以便在支持屏幕共享但未实现当前标准 API 的浏览器上启用屏幕共享。这至少适用于 Chrome、Edge 和 Firefox。
捕获屏幕内容
通过调用 navigator.mediaDevices.getDisplayMedia() 来启动将屏幕内容捕获为实时 MediaStream 的过程,该方法返回一个 Promise,该 Promise 解析为包含实时屏幕内容的流。下面示例中引用的 displayMediaOptions 对象可能如下所示:
const displayMediaOptions = {
video: {
displaySurface: "browser",
},
audio: {
suppressLocalAudioPlayback: false,
},
preferCurrentTab: false,
selfBrowserSurface: "exclude",
systemAudio: "include",
surfaceSwitching: "include",
monitorTypeSurfaces: "include",
};
启动屏幕捕获:async/await 方式
async function startCapture(displayMediaOptions) {
let captureStream = null;
try {
captureStream =
await navigator.mediaDevices.getDisplayMedia(displayMediaOptions);
} catch (err) {
console.error(`Error: ${err}`);
}
return captureStream;
}
启动屏幕捕获:Promise 方式
function startCapture(displayMediaOptions) {
return navigator.mediaDevices
.getDisplayMedia(displayMediaOptions)
.catch((err) => {
console.error(err);
return null;
});
}
无论哪种方式,用户代理都会通过显示用户界面来响应,提示用户选择要共享的屏幕区域。这两个 startCapture() 的实现都返回包含捕获的显示图像的 MediaStream。
有关如何指定所需表面类型以及调整结果流的其他方法,请参阅下面的选项和约束。
允许用户选择要捕获的显示界面的窗口示例

然后,您可以将捕获的流 captureStream 用于任何接受流作为输入的场景。下面的示例展示了几种使用流的方法。
可见显示表面与逻辑显示表面
就屏幕捕获 API 而言,显示表面是可以通过 API 选择用于共享目的的任何内容对象。共享表面包括浏览器标签页的内容、完整的窗口以及显示器(或组合成一个表面的显示器组)的内容。
显示表面有两种类型。可见显示表面是屏幕上完全可见的表面,例如最前面的窗口或标签页,或整个屏幕。
逻辑显示表面是部分或完全被遮挡的表面,可能是被其他对象部分覆盖,也可能是完全隐藏或超出屏幕范围。屏幕捕获 API 对它们的处理方式有所不同。通常,浏览器会提供一个图像,以某种方式遮挡逻辑显示表面的隐藏部分,例如通过模糊处理或替换为某种颜色或图案。这样做是出于安全原因,因为用户无法看到的内容可能包含他们不希望共享的数据。
用户代理可能会在获得用户许可后捕获被遮挡窗口的全部内容。在这种情况下,用户代理可能会包含被遮挡的内容,方法是获取窗口隐藏部分的当前内容,或者在当前内容不可用时显示最近可见的内容。
选项和约束
传递给 getDisplayMedia() 的选项对象用于设置结果流的选项。
传递给选项对象的 video 和 audio 对象还可以包含这些媒体轨道特有的额外约束。有关配置屏幕捕获流的额外约束(添加到 MediaTrackConstraints、MediaTrackSupportedConstraints 和 MediaTrackSettings)的详细信息,请参阅共享屏幕轨道的属性。
在选择要捕获的内容之前,不会以任何方式应用任何约束。这些约束会改变您在结果流中看到的内容。例如,如果您为视频指定 width 约束,它将在用户选择要共享的区域后通过缩放视频来应用。它不会对源本身的大小施加限制。
注意:约束绝不会导致屏幕共享 API 可用于捕获的源列表发生更改。这确保了 Web 应用程序不能通过限制源列表直到只剩一个项目来强制用户共享特定内容。
当显示捕获生效时,共享屏幕内容的机器将显示某种形式的指示器,以便用户知道正在进行共享。
注意:出于隐私和安全原因,屏幕共享源无法使用 enumerateDevices() 进行枚举。与此相关,当 getDisplayMedia() 可用的源发生变化时,永远不会发送 devicechange 事件。
捕获共享音频
getDisplayMedia() 最常用于捕获用户屏幕(或其部分)的视频。但是,用户代理可能允许与视频内容一起捕获音频。此音频的来源可能是选定的窗口、整个计算机的音频系统或用户的麦克风(或以上所有项的组合)。
在开始需要共享音频的项目之前,请务必检查 getDisplayMedia() 的浏览器兼容性,以查看您希望兼容的浏览器是否支持捕获屏幕流中的音频。
要请求共享屏幕并包含音频,传递给 getDisplayMedia() 的选项可能如下所示:
const displayMediaOptions = {
video: true,
audio: true,
};
这允许用户在用户代理支持的范围内完全自由地选择他们想要的任何内容。通过在 audio 和 video 对象中指定额外的选项和约束,可以进一步完善这一点:
const displayMediaOptions = {
video: {
displaySurface: "window",
},
audio: {
echoCancellation: true,
noiseSuppression: true,
sampleRate: 44100,
suppressLocalAudioPlayback: true,
},
surfaceSwitching: "include",
selfBrowserSurface: "exclude",
systemAudio: "exclude",
};
在此示例中,捕获的显示表面将是整个窗口。音频轨道应理想地启用降噪和回声消除功能,以及 44.1kHz 的理想音频采样率,并抑制本地音频播放。
此外,应用程序向用户代理提示它应该:
- 在屏幕共享期间提供一个控件,允许用户动态切换共享标签页。
- 在请求捕获时,从呈现给用户的选项列表中隐藏当前标签页。
- 不将系统音频包含在提供给用户的可能音频源中。
捕获音频始终是可选的,即使 Web 内容请求包含音频和视频的流,返回的 MediaStream 仍然可能只有一个视频轨道,没有音频。
使用捕获的流
由 getDisplayMedia() 返回的 promise 解析为 MediaStream,其中包含至少一个视频流,该视频流包含屏幕或屏幕区域,并根据调用 getDisplayMedia() 时指定的约束进行调整或过滤。
潜在风险
围绕屏幕共享的隐私和安全问题通常并不特别严重,但确实存在。最大的潜在问题是用户无意中共享了他们不希望共享的内容。
例如,如果用户正在共享屏幕,而可见的背景窗口恰好包含个人信息,或者他们的密码管理器在共享流中可见,那么很容易发生隐私和/或安全漏洞。当捕获逻辑显示表面时,这种影响可能会被放大,因为逻辑显示表面可能包含用户根本不知道的内容,更不用说看到了。
认真对待隐私的用户代理应该混淆屏幕上实际不可见的内容,除非已获得专门共享该内容的授权。
授权捕获显示内容
在捕获的屏幕内容流式传输开始之前,用户代理将要求用户确认共享请求,并选择要共享的内容。
示例
流式屏幕捕获
在此示例中,捕获的屏幕区域的内容流式传输到同一页面上的 <video> 元素中。
JavaScript
要实现此功能所需的代码并不多,如果您熟悉使用 getUserMedia() 从摄像头捕获视频,您会发现 getDisplayMedia() 非常熟悉。
设置
首先,设置一些常量来引用页面上我们需要访问的元素:用于流式传输捕获屏幕内容的 <video> 元素、用于绘制日志输出的框,以及用于打开和关闭屏幕图像捕获的开始和停止按钮。
对象 displayMediaOptions 包含要传递给 getDisplayMedia() 的选项;这里,displaySurface 属性设置为 window,表示应捕获整个窗口。
最后,建立事件监听器以检测用户对开始和停止按钮的点击。
const videoElem = document.getElementById("video");
const logElem = document.getElementById("log");
const startElem = document.getElementById("start");
const stopElem = document.getElementById("stop");
// Options for getDisplayMedia()
const displayMediaOptions = {
video: {
displaySurface: "window",
},
audio: false,
};
// Set event listeners for the start and stop buttons
startElem.addEventListener("click", (evt) => {
startCapture();
});
stopElem.addEventListener("click", (evt) => {
stopCapture();
});
记录内容
此示例覆盖了某些 console 方法,以将其消息输出到 ID 为 log 的 <pre> 块中。
console.log = (msg) => (logElem.textContent = `${logElem.textContent}\n${msg}`);
console.error = (msg) =>
(logElem.textContent = `${logElem.textContent}\nError: ${msg}`);
这允许我们使用 console.log() 和 console.error() 将信息记录到文档中的日志框中。
开始显示捕获
下面的 startCapture() 方法开始捕获 MediaStream,其内容取自用户选择的屏幕区域。当点击“开始捕获”按钮时,会调用 startCapture()。
async function startCapture() {
logElem.textContent = "";
try {
videoElem.srcObject =
await navigator.mediaDevices.getDisplayMedia(displayMediaOptions);
dumpOptionsInfo();
} catch (err) {
console.error(err);
}
}
清除日志内容以清除上次连接尝试留下的任何文本后,startCapture() 调用 getDisplayMedia(),并将由 displayMediaOptions 定义的约束对象传递给它。使用 await,以下代码行在 getDisplayMedia() 返回的 promise 解析后才会执行。解析后,Promise 返回一个 MediaStream,该流将流式传输用户选择的屏幕、窗口或其他区域的内容。
通过将返回的 MediaStream 存储到元素的 srcObject 中,将流连接到 <video> 元素。
dumpOptionsInfo() 函数(我们稍后会看到)将有关流的信息转储到日志框中,以用于教育目的。
如果其中任何一个失败,catch() 子句会向日志框输出错误消息。
停止显示捕获
当点击“停止捕获”按钮时,会调用 stopCapture() 方法。它通过使用 MediaStream.getTracks() 获取其轨道列表,然后调用每个轨道的 stop() 方法来停止流。完成此操作后,srcObject 将设置为 null,以确保任何感兴趣的人都明白没有连接流。
function stopCapture(evt) {
let tracks = videoElem.srcObject.getTracks();
tracks.forEach((track) => track.stop());
videoElem.srcObject = null;
}
转储配置信息
出于信息目的,上面显示的 startCapture() 方法调用了一个名为 dumpOptions() 的方法,该方法输出当前的轨道设置以及在创建流时施加在流上的约束。
function dumpOptionsInfo() {
const videoTrack = videoElem.srcObject.getVideoTracks()[0];
console.log("Track settings:");
console.log(JSON.stringify(videoTrack.getSettings(), null, 2));
console.log("Track constraints:");
console.log(JSON.stringify(videoTrack.getConstraints(), null, 2));
}
轨道列表通过对捕获屏幕的 MediaStream 调用 getVideoTracks() 获得。当前生效的设置使用 getSettings() 获得,已建立的约束使用 getConstraints() 获得。
HTML
HTML 以一个介绍性段落开始,然后进入核心内容。
<p>
This example shows you the contents of the selected part of your display.
Click the Start Capture button to begin.
</p>
<p>
<button id="start">Start Capture</button> <button id="stop">
Stop Capture
</button>
</p>
<video id="video" autoplay></video>
<br />
<strong>Log:</strong>
<br />
<pre id="log"></pre>
HTML 的关键部分是:
- 一个标记为“开始捕获”的
<button>,当点击时,它会调用startCapture()函数来请求访问并开始捕获屏幕内容。 - 第二个按钮,“停止捕获”,点击后会调用
stopCapture()来终止屏幕内容的捕获。 - 一个
<video>,捕获的屏幕内容会流式传输到其中。 - 一个
<pre>块,拦截的console方法会将日志文本放入其中。
CSS
此示例中的 CSS 完全是用于美化的。视频被赋予边框,其宽度设置为占据几乎所有可用的水平空间(width: 98%)。max-width 设置为 860px 以设置视频大小的绝对上限。
#video {
border: 1px solid #999999;
width: 98%;
max-width: 860px;
}
#log {
width: 25rem;
height: 15rem;
border: 1px solid black;
padding: 0.5rem;
overflow: scroll;
}
结果
最终产品看起来像这样。如果您的浏览器支持屏幕捕获 API,点击“开始捕获”将呈现用户代理的界面,用于选择要共享的屏幕、窗口或标签页。
安全
为了在启用 Permissions Policy 时正常工作,您需要 display-capture 权限。这可以通过 Permissions-Policy HTTP 头实现,或者——如果您在 <iframe> 中使用屏幕捕获 API,则通过 <iframe> 元素的 allow 属性实现。
例如,HTTP 头中的这一行将为文档以及从同一来源加载的任何嵌入式 <iframe> 元素启用屏幕捕获 API:
Permissions-Policy: display-capture=(self)
如果您在 <iframe> 中执行屏幕捕获,您可以仅为该框架请求权限,这显然比更一般地请求权限更安全。
<iframe src="https://mycode.example.net/etc" allow="display-capture"> </iframe>
浏览器兼容性
加载中…