使用屏幕捕获 API

在本文中,我们将探讨如何使用屏幕捕获 API 及其 getDisplayMedia() 方法来捕获屏幕的一部分或全部,以在 WebRTC 会议期间进行流式传输、录制或共享。

注意: 值得注意的是,最新版本的 WebRTC adapter.js shim 包含 getDisplayMedia() 的实现,以支持屏幕共享,即使在支持此功能但未实现当前标准 API 的浏览器中也是如此。这至少适用于 Chrome、Edge 和 Firefox。

捕获屏幕内容

将屏幕内容作为实时 MediaStream 捕获是通过调用 navigator.mediaDevices.getDisplayMedia() 启动的,该方法返回一个 Promise,该 Promise 解析为包含实时屏幕内容的流。下面示例中引用的 displayMediaOptions 对象可能如下所示

js
const displayMediaOptions = {
  video: {
    displaySurface: "browser",
  },
  audio: {
    suppressLocalAudioPlayback: false,
  },
  preferCurrentTab: false,
  selfBrowserSurface: "exclude",
  systemAudio: "include",
  surfaceSwitching: "include",
  monitorTypeSurfaces: "include",
};

开始屏幕捕获:async/await 样式

js
async function startCapture(displayMediaOptions) {
  let captureStream = null;

  try {
    captureStream =
      await navigator.mediaDevices.getDisplayMedia(displayMediaOptions);
  } catch (err) {
    console.error(`Error: ${err}`);
  }
  return captureStream;
}

您可以使用异步函数和 await 运算符编写此代码,如上面所示,也可以使用 Promise 直接编写,如下所示。

开始屏幕捕获:Promise 样式

js
function startCapture(displayMediaOptions) {
  return navigator.mediaDevices
    .getDisplayMedia(displayMediaOptions)
    .catch((err) => {
      console.error(err);
      return null;
    });
}

无论哪种方式,用户代理 都将通过呈现一个用户界面来响应,提示用户选择要共享的屏幕区域。这两个 startCapture() 实现都返回包含捕获的显示图像的 MediaStream

请参阅下面有关 选项和约束 的内容,了解更多关于如何指定所需表面类型以及调整结果流的其他方法。

允许用户选择要捕获的显示表面的窗口示例

Screenshot of Chrome's window for picking a source surface

然后,您可以将捕获的流 captureStream 用于任何接受流作为输入的内容。下面 示例 显示了一些使用流的方法。

可见与逻辑显示表面

就屏幕捕获 API 而言,显示表面 是任何可以通过 API 选择以进行共享的コンテンツ对象。共享表面包括浏览器标签的内容、整个窗口以及显示器(或组合在一起形成一个表面的显示器组)。

显示表面有两种类型。可见显示表面 是完全可见于屏幕上的表面,例如最前面的窗口或标签,或者整个屏幕。

逻辑显示表面 是部分或全部被遮挡的表面,要么是被另一个对象部分重叠,要么是被完全隐藏或屏幕外。屏幕捕获 API 对这些表面的处理方式各不相同。通常,浏览器将提供一个图像,以某种方式遮挡逻辑显示表面的隐藏部分,例如模糊或用颜色或图案替换。这样做是为了安全考虑,因为用户无法看到的内容可能包含他们不想共享的数据。

用户代理可能会允许捕获整个隐藏窗口的内容,前提是用户已授权执行此操作。在这种情况下,用户代理可能会包含隐藏的内容,要么通过获取隐藏的窗口部分的当前内容,要么通过呈现最后一次可见的内容(如果当前内容不可用)。

选项和约束

传递给 getDisplayMedia() 的选项对象用于为结果流设置选项。

传递给选项对象的 videoaudio 对象还可以包含特定于这些媒体轨道的附加约束。请参阅 共享屏幕轨道的属性,了解有关配置屏幕捕获流的附加约束的详细信息,这些约束添加到 MediaTrackConstraintsMediaTrackSupportedConstraintsMediaTrackSettings 中。

所有约束都将在选择要捕获的内容后才应用。约束会改变您在结果流中看到的内容。例如,如果您为视频指定了 width 约束,它将在用户选择要共享的区域后通过缩放视频来应用。它不会对源的大小设置限制。

注意: 约束永远不会更改屏幕共享 API 可捕获的源列表。这确保了 Web 应用程序无法通过限制源列表来强制用户共享特定内容,直到只剩下一个项目为止。

在显示捕获生效时,共享屏幕内容的机器将显示某种形式的指示器,以便用户知道正在进行共享。

注意: 出于隐私和安全考虑,屏幕共享源不可通过 enumerateDevices() 列举。与之相关的是,devicechange 事件永远不会在 getDisplayMedia() 可用源发生更改时发送。

捕获共享音频

getDisplayMedia() 最常用于捕获用户屏幕(或其部分)的视频。但是,用户代理 可能会允许与视频内容一起捕获音频。此音频的来源可能是选定的窗口、整个计算机的音频系统、用户的麦克风(或以上所有来源的组合)。

在开始需要共享音频的项目之前,请务必查看 浏览器兼容性,以了解您希望与之兼容的浏览器是否支持捕获的屏幕流中的音频。

若要请求共享包含音频的屏幕,传递给 getDisplayMedia() 的选项可能如下所示

js
const displayMediaOptions = {
  video: true,
  audio: true,
};

这允许用户在用户代理支持的范围内完全自由地选择他们想要的任何内容。可以通过在 audiovideo 对象中指定其他选项和约束来进一步细化此内容

js
const displayMediaOptions = {
  video: {
    displaySurface: "window",
  },
  audio: {
    echoCancellation: true,
    noiseSuppression: true,
    sampleRate: 44100,
    suppressLocalAudioPlayback: true,
  },
  surfaceSwitching: "include",
  selfBrowserSurface: "exclude",
  systemAudio: "exclude",
};

在此示例中,捕获的显示表面应该是整个窗口。音频轨道理想情况下应该启用降噪和回声消除功能,以及理想的音频采样率 44.1kHz,以及抑制本地音频回放。

此外,该应用程序暗示用户代理应该

  • 在屏幕共享期间提供一个控件,允许用户动态切换共享的标签。
  • 从向用户呈现的捕获请求选项列表中隐藏当前标签。
  • 不将系统音频包含在提供给用户的可能音频源中。

捕获音频始终是可选的,即使 Web 内容请求包含音频和视频的流,返回的 MediaStream 仍然可能只包含一个视频轨道,而没有音频。

使用捕获的流

promisegetDisplayMedia() 返回,解析为一个 MediaStream,该流至少包含一个包含屏幕或屏幕区域的视频流,并且根据调用 getDisplayMedia() 时指定的约束进行调整或过滤。

潜在风险

围绕屏幕共享的隐私和安全问题通常并不十分严重,但确实存在。最大的潜在问题是用户无意间共享了他们不想共享的内容。

例如,如果用户正在共享屏幕,而可见的背景窗口恰好包含个人信息,或者他们的密码管理器在共享流中可见,那么很容易发生隐私或安全违规。当捕获逻辑显示表面时,这种影响会被放大,因为逻辑显示表面可能包含用户根本不知道的内容,更不用说看到的内容了。

认真对待隐私的用户代理应该模糊化屏幕上不可见的内容,除非已获得专门共享该内容的授权。

授权捕获显示内容

在开始捕获屏幕内容的流式传输之前,用户代理 将要求用户确认共享请求并选择要共享的内容。

示例

流式传输屏幕捕获

在此示例中,捕获的屏幕区域的内容将流式传输到同一页面上的 <video> 元素中。

JavaScript

使之起作用所需的代码并不多,如果您熟悉使用 getUserMedia() 从摄像头捕获视频,您会发现 getDisplayMedia() 非常熟悉。

设置

首先,设置一些常量来引用我们需要访问的页面上的元素:将捕获的屏幕内容流式传输到的 <video>、将记录的输出绘制到的框以及启动和停止捕获屏幕图像的启动和停止按钮。

对象 displayMediaOptions 包含传递给 getDisplayMedia() 的选项;这里,displaySurface 属性被设置为 window,表示应捕获整个窗口。

最后,建立事件侦听器来检测用户在开始和停止按钮上的点击。

js
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();
  },
  false,
);

stopElem.addEventListener(
  "click",
  (evt) => {
    stopCapture();
  },
  false,
);
记录内容

此示例重写了某些 console 方法,以便将它们的 messages 输出到 ID 为 log<pre> 块。

js
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()

js
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,以确保任何感兴趣的人都能明白没有连接的流。

js
function stopCapture(evt) {
  let tracks = videoElem.srcObject.getTracks();

  tracks.forEach((track) => track.stop());
  videoElem.srcObject = null;
}
转储配置信息

出于信息目的,上面显示的 startCapture() 方法调用了一个名为 dumpOptions() 的方法,该方法输出当前轨道设置以及在创建流时对流施加的约束。

js
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 以一个介绍性段落开头,然后进入主题。

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>&nbsp;<button id="stop">
    Stop Capture
  </button>
</p>

<video id="video" autoplay></video>
<br />

<strong>Log:</strong>
<br />
<pre id="log"></pre>

HTML 的关键部分是

  1. 一个标记为“开始捕获”的 <button>,当单击它时,会调用 startCapture() 函数来请求访问屏幕内容并开始捕获屏幕内容。
  2. 第二个按钮“停止捕获”,当单击它时会调用 stopCapture() 来终止捕获屏幕内容。
  3. 一个 <video>,捕获的屏幕内容被流式传输到其中。
  4. 一个 <pre> 块,被拦截的 console 方法将日志文本放置到其中。

CSS

在此示例中,CSS 完全是装饰性的。视频被赋予边框,其宽度设置为占据几乎所有可用的水平空间 (width: 98%)。max-width 设置为 860px,以设置视频大小的绝对上限,

css
#video {
  border: 1px solid #999;
  width: 98%;
  max-width: 860px;
}

#log {
  width: 25rem;
  height: 15rem;
  border: 1px solid black;
  padding: 0.5rem;
  overflow: scroll;
}

结果

最终产品看起来像这样。如果您的浏览器支持屏幕捕获 API,单击“开始捕获”将显示 用户代理 的界面,用于选择要共享的屏幕、窗口或选项卡。

安全

为了在启用 权限策略 时正常运行,您需要 display-capture 权限。这可以通过使用 Permissions-Policy HTTP 头部来实现,或者 - 如果您在 <iframe> 中使用屏幕捕获 API,则可以使用 <iframe> 元素的 allow 属性。

例如,HTTP 头部中的这行将为文档和从相同来源加载的任何嵌入式 <iframe> 元素启用屏幕捕获 API

http
Permissions-Policy: display-capture=(self)

如果您在 <iframe> 中执行屏幕捕获,则可以仅为此框架请求权限,这显然比更普遍地请求权限更安全。

html
<iframe src="https://mycode.example.net/etc" allow="display-capture"> </iframe>

浏览器兼容性

BCD 表只在浏览器中加载

另请参见