跳转到内容

能力 · API

Screen Capture API(getDisplayMedia)

发布于 更新于

navigator.mediaDevices.getDisplayMedia() 打开浏览器的屏幕、窗口或标签页选择器,兑现为一个恰好含一条视频轨、至多一条音频轨的 MediaStream,可直接交给 MediaRecorder、<video> 元素或 RTCPeerConnection。与 getUserMedia() 不同,授权从不持久化:每次调用都会再弹选择器,页面无法枚举显示源,也无法预选。

桌面端的 Chrome 72、Edge 79、Firefox 66 与 macOS 上的 Safari 13 实现了它(Edge 17 到 79 在 navigator 上暴露过早期版本)。Android 上的 Chrome 72 到 88 与 Firefox 66 到 79 暴露了该方法,但每次调用都以 NotAllowedError 拒绝(Chromium bug 40418135);如今 Android 上的 Chrome、Firefox 与 iOS 上的 Safari 都不再暴露它(BCD api.MediaDevices.getDisplayMedia)。video 与 audio 之外的捕获选项只有 Chromium 实现。

navigator.mediaDevices.getDisplayMedia()
navigator.mediaDevices.getDisplayMedia(options)

返回 Promise<MediaStream>。调用需要瞬时激活、处于 fully active 且拥有焦点的文档、安全上下文,以及 display-capture Permissions Policy(默认允许列表为 'self',跨源 iframe 需要 allow="display-capture")。浏览器会消耗这次激活,第二次调用需要再点一次。

唯一的可选参数是 DisplayMediaStreamOptions 字典。video 与 audio 来自 W3C 规范;其余成员是浏览器可以忽略的提示,只有 Chromium 实现(版本号来自 BCD)。

成员 类型 必填 说明
video boolean 或 MediaTrackConstraints 否,默认 true 传 false 以 TypeError 拒绝。约束对象可带作为提示的 displaySurface("browser"、"window"、"monitor")以及 width、height、frameRate、aspectRatio、cursor;约束在用户选定之后才应用,不会过滤选择器。min、exact 与 advanced 会被拒绝。
audio boolean 或 MediaTrackConstraints 否,默认 false 请求音频轨;浏览器可以只返回视频。Chrome 74 在 Windows 与 ChromeOS 上捕获系统音频,其他平台只捕获标签页音频。
controller CaptureController 否 用户选定标签页或窗口后,让页面通过 setFocusBehavior() 保留或转移焦点;每次调用一个控制器。Chrome 109。
preferCurrentTab boolean 否,默认 false 把调用方标签页排在选择器最前。Chrome 94。
selfBrowserSurface "include" 或 "exclude" 否 调用方标签页是否出现在选择器中;Chrome 112 默认 "exclude"(107 到 111 默认 "include")。
surfaceSwitching "include" 或 "exclude" 否 捕获期间 Chrome 是否显示「改为共享此标签页」控件。Chrome 107。
systemAudio "include" 或 "exclude" 否 选择整个屏幕时选择器是否提供系统音频。Chrome 105。
windowAudio "exclude"、"window" 或 "system" 否 选择窗口时提供的音频;Chrome 141 只接受 "exclude" 与 "system"。
monitorTypeSurfaces "include" 或 "exclude" 否 是否提供整个屏幕作为选项。Chrome 119。

Promise 以下列之一拒绝。Chrome 的消息引自 media_devices.cc。

异常 条件
InvalidStateError 没有瞬时激活(Chrome:getDisplayMedia() requires transient activation (user gesture).);文档不是 fully active 或没有焦点;传入的 controller 已被用过(Chrome:A CaptureController object may only be used with a single getDisplayMedia() invocation.)。
TypeError video: false,或约束集中含 advanced、min 或 exact。
OverconstrainedError 某个 max 约束低于该属性的下限,或对所选表面应用约束失败。
NotAllowedError 用户关闭了选择器、权限状态为 "denied"、操作系统或企业策略禁止捕获,或 display-capture Permissions Policy 拒绝了该文档(Chrome:Access to the feature "display-capture" is disallowed by permissions policy.)。
NotFoundError 不存在所请求类型的源。
NotReadableError 用户已授权,但操作系统层面的锁阻止读取该表面。
AbortError 设备访问因上述之外的原因失败。

每个示例都从按钮点击开始以保证激活存在,调用前先检测方法,并给出捕获不可用或被拒绝时页面的做法。

预览所选表面,不可用时给出提示

Section titled “预览所选表面,不可用时给出提示”

检测 navigator.mediaDevices 上的 getDisplayMedia;在非安全源上 mediaDevices 本身就不存在。方法缺失覆盖了 iOS 上的 Safari 与所有 Android 浏览器;选择器出现之后的 NotAllowedError 是用户改了主意,不应当作失败上报。

const button = document.querySelector("#share-screen");
const preview = document.querySelector("video#preview");
const notice = document.querySelector("#capture-notice");
if (!navigator.mediaDevices?.getDisplayMedia) {
button.disabled = true;
notice.textContent = "此浏览器不支持屏幕共享。";
} else {
button.addEventListener("click", async () => {
try {
const stream = await navigator.mediaDevices.getDisplayMedia({
video: { displaySurface: "window" },
audio: false,
});
preview.srcObject = stream;
stream.getVideoTracks()[0].addEventListener("ended", () => {
preview.srcObject = null; // 用户点了浏览器自己的「停止共享」控件
});
} catch (err) {
notice.textContent = err.name === "NotAllowedError" ? "已取消共享。" : `${err.name}: ${err.message}`;
}
});
}

ended 事件是用户从浏览器自身 UI 停止共享的唯一信号;不监听它,<video> 会一直停在最后一帧。

用 preferCurrentTab(Chrome 94 的提示,其他浏览器忽略)请求当前标签页并带音频,再把流交给 MediaRecorder。轨道结束时停止录制器,这样即使用户从浏览器工具栏而不是你的按钮结束共享,文件也能收尾。

async function recordTab() {
if (!navigator.mediaDevices?.getDisplayMedia || typeof MediaRecorder === "undefined") {
return null;
}
const stream = await navigator.mediaDevices.getDisplayMedia({
video: true,
audio: true,
preferCurrentTab: true,
});
const recorder = new MediaRecorder(stream, { mimeType: "video/webm" });
const chunks = [];
recorder.addEventListener("dataavailable", (event) => chunks.push(event.data));
const done = new Promise((resolve) => {
recorder.addEventListener("stop", () => resolve(new Blob(chunks, { type: "video/webm" })));
});
stream.getVideoTracks()[0].addEventListener("ended", () => recorder.stop());
recorder.start(1000);
return { stop: () => recorder.stop(), done };
}

向用户承诺有音频轨之前先看 stream.getAudioTracks().length:Firefox 与 Safari 只返回视频,macOS 与 Linux 上的 Chrome 只在共享标签页时带音频,窗口与屏幕都没有。

用 CaptureController 让焦点留在你的页面

Section titled “用 CaptureController 让焦点留在你的页面”

默认情况下,选择器关闭后 Chrome 会把焦点交给被捕获的标签页或窗口,用户刚点过的控件随之被挡住。调用前创建的 CaptureController 可以要求 Chrome 停在原处;必须在 Promise 兑现后的那一刻调用,其他浏览器根本没有这个构造函数。

async function startCaptureKeepingFocus() {
const options = { video: true };
let controller = null;
if ("CaptureController" in window) {
controller = new CaptureController();
options.controller = controller;
}
const stream = await navigator.mediaDevices.getDisplayMedia(options);
if (controller && stream.getVideoTracks()[0].getSettings().displaySurface !== "monitor") {
controller.setFocusBehavior("no-focus-change");
}
return stream;
}

对整个屏幕 setFocusBehavior() 会被忽略,因为没有可聚焦的对象;规范的「finalize focus decision」步骤在 Promise 兑现后紧接着排队的任务里执行,此后再调用会抛 InvalidStateError,所以 await 之后要同步调用。

规范

规范状态
Screen Capture: getDisplayMedia() methodW3C
Screen Capture: DisplayMediaStreamOptions dictionaryW3C