在使用 Screen Capture API 也就是 getDisplayMedia 做屏幕共享或录制功能时,前端通常需要传入一个包含视频约束的对象。很多同学在 TypeScript 项目里直接写了 { video: true },一旦想配置分辨率、帧率、显示器类型这些细节,就开始犯难:这些字段到底该怎么声明类型?直接用 any 固然省事,但编译期检查就形同虚设。这篇文章就来把屏幕捕获场景下视频约束的数据类型定义彻底讲清楚。

一、先弄清楚原生类型体系
TypeScript 的 DOM 库(lib.dom.d.ts)其实已经内置了与媒体捕获相关的类型。getDisplayMedia 挂在 MediaDevices 原型上,签名大致是 getDisplayMedia(options?: DisplayMediaStreamOptions): Promise<MediaStream>。而 DisplayMediaStreamOptions 里的 video 字段并不是普通的 boolean,它可以是布尔值,也可以是 MediaTrackConstraints。
MediaTrackConstraints 继承自 MediaTrackConstraintSet,后者包含了宽度、高度、帧率、宽高比等所有可约束属性。理解这个继承关系很重要:你自定义的约束类型如果 extends 了这些原生接口,就能同时获得摄像头和屏幕捕获共有的字段,再补充屏幕捕获特有的字段即可。下面是一个基础的自定义类型:
// 屏幕捕获专有的约束字段
interface ScreenCaptureConstraintSet {
displaySurface?: 'monitor' | 'window' | 'browser';
logicalSurface?: boolean;
cursor?: 'always' | 'motion' | 'never';
restrictOwnAudio?: boolean;
}
// 组合通用约束与屏幕捕获专有约束
type ScreenVideoConstraints = MediaTrackConstraintSet & ScreenCaptureConstraintSet;
interface ScreenCaptureOptions {
video: boolean | ScreenVideoConstraints;
audio?: boolean | MediaTrackConstraints;
// 是否请求系统级权限( Elevated permissions )
systemAudio?: 'include' | 'exclude';
surfaceSwitching?: 'include' | 'exclude';
selfBrowserSurface?: 'include' | 'exclude';
}这样定义的好处是显而易见的:宽度、帧率这类字段直接复用官方类型,ConstrainULong 既允许传数字也允许传 { ideal: 1920 } 这种形式,不需要你自己重复描述。而 displaySurface 这类屏幕捕获独有字段则用字面量联合类型约束,写错取值编译器立刻报错。
二、约束值的进阶写法与类型收窄
原生类型里每个约束字段的值都是宽松的联合类型,比如 width?: ConstrainULong,展开后等于 number | ConstrainULongRange。实际业务中我们往往只需要 { ideal?: number; max?: number; min?: number } 这几种形态。可以通过工具类型做一层收窄,让调用方传参更受控,同时保留灵活性。
// 只保留 ideal / min / max 的范围约束
type NarrowULong = number | { ideal?: number; min?: number; max?: number };
type NarrowConstraintSet = {
width?: NarrowULong;
height?: NarrowULong;
frameRate?: NarrowULong;
aspectRatio?: NarrowULong;
displaySurface?: 'monitor' | 'window' | 'browser';
cursor?: 'always' | 'motion' | 'never';
};
async function captureScreen(video: NarrowConstraintSet) {
const stream = await navigator.mediaDevices.getDisplayMedia({
video: video as MediaTrackConstraints,
audio: false,
});
return stream;
}
captureScreen({ width: { ideal: 1920 }, frameRate: 15, displaySurface: 'monitor' });注意这里的 as MediaTrackConstraints 断言。因为我们收窄后的类型是原生类型的子集,理论上可以直接赋值,但如果把字段写成了可选的 exact、advanced 等扩展项,就可能需要断言兜底。断言要谨慎使用,仅在你确认字段取值范围合法时才加上,否则等于亲手关掉了类型检查。
另一个常见需求是区分“预置配置”。比如产品里可能提供“高清录制”“流畅共享”两种模式,用联合类型配合判别字段可以让函数签名更清晰:
type CaptureMode =
| { mode: 'hd'; constraints: { width: { ideal: 1920 }; height: { ideal: 1080 }; frameRate: 30 } }
| { mode: 'smooth'; constraints: { frameRate: { ideal: 60 }; displaySurface: 'monitor' } };
function startCapture(config: CaptureMode) {
return navigator.mediaDevices.getDisplayMedia({ video: config.constraints, audio: false });
}三、运行时校验与类型定义的配合
再完善的静态类型也拦不住运行时数据,比如约束配置来自后端接口或用户在界面上勾选。这时候类型定义只保证编译期正确,真正传给浏览器前还应该做一次字段白名单过滤。白名单的意义在于,浏览器对未知字段不报错但也不会生效,而过滤掉非法字段能避免误导排查方向。
const ALLOWED_KEYS = [
'width', 'height', 'frameRate', 'aspectRatio',
'displaySurface', 'logicalSurface', 'cursor',
] as const;
type AllowedKey = typeof ALLOWED_KEYS[number];
function sanitizeConstraints(input: Record<string, unknown>): ScreenVideoConstraints {
const result: Record<AllowedKey, unknown> = {} as Record<AllowedKey, unknown>;
for (const key of ALLOWED_KEYS) {
if (key in input) {
result[key] = input[key];
}
}
return result as ScreenVideoConstraints;
}
// 后端下发的配置先过滤再使用
const remoteConfig = { width: { ideal: 2560 }, frameRate: 30, evilField: 'x' };
const safe = sanitizeConstraints(remoteConfig);
navigator.mediaDevices.getDisplayMedia({ video: safe, audio: false });这套方案里,静态类型负责开发阶段的提示与约束,白名单过滤负责运行阶段的兜底,两层防护结合起来才算完整。还可以进一步用 zod 这类校验库替代手写白名单,把 schema 推导出的类型直接赋给约束对象,实现一处定义、编译期和运行时共用。
最后提醒一点,屏幕捕获的约束语义和摄像头不同:displaySurface 只是给浏览器的提示,用户在选共享源时仍可改选;width、height 也受共享源实际分辨率限制,通常配合 applyConstraints 在拿到轨道后再二次调整。理解了这些行为差异,类型定义才能真正贴合实际运行效果,而不是停留在纸面上。
TypeScriptScreen Capture API视频约束修改时间:2026-09-04 17:42:41