WebRTC getDisplayMedia 屏幕采集约束配置与自浏览器排除工程化实践指南
在实时音视频(RTC)应用开发中,屏幕共享是协作会议、远程教学、直播推流等场景的核心功能。WebRTC 标准提供的 navigator.mediaDevices.getDisplayMedia() 接口是浏览器端实现该能力的统一入口。本文将从约束参数配置、跨浏览器兼容性处理、以及工程化难点“自浏览器排除(防止采集到当前页面导致无限镜像)”三个维度,系统梳理工程落地实践方案。
一、核心 API 与基础约束配置
getDisplayMedia(constraints) 返回一个 Promise,解析为 MediaStream。其 constraints 参数决定了采集源的类型、分辨率、帧率等关键指标。合理的约束配置能在保障清晰度的前提下,控制带宽与编解码压力。
1.1 视频约束核心字段解析
| 字段 | 类型 | 说明 | 工程建议 | ||
|---|---|---|---|---|---|
video |
`boolean | MediaTrackConstraints` | 启用视频轨道,建议显式传对象 | 必传对象,细化控制 | |
audio |
`boolean | MediaTrackConstraints` | 是否采集系统/标签页音频 | 会议场景建议 true,直播推流视业务而定 |
|
preferCurrentTab |
boolean |
提示浏览器优先展示当前标签页 | 单页应用(SPA)建议 true,减少用户选择成本 |
||
selfBrowserSurface |
`'include' | 'exclude'` | 核心字段:控制当前浏览器实例是否出现在源列表 | 主流方案设为 'exclude',配合兜底策略实现自排除 |
|
systemAudio |
`'include' | 'exclude' | 'require'` | 控制系统级音频采集(Chrome 105+) | 会议场景推荐 'include' 或 'require' |
surfaceSwitching |
`'include' | 'exclude'` | 允许用户共享过程中切换源(Chrome 112+) | 长会议场景建议 'include',提升体验 |
1.2 分辨率与帧率的动态策略
固定高分辨率(如 1920×1080@30fps)在弱网或低端设备上易导致卡顿、发热。建议采用分层约束策略:
const getVideoConstraints = (isHighDefinition = false) => ({
video: {
// 宽高比约束,避免极端长宽比导致编码异常
aspectRatio: { ideal: 16 / 9 },
// 分辨率阶梯:理想值 -> 可接受范围
width: { ideal: isHighDefinition ? 1920 : 1280, max: 1920, min: 640 },
height: { ideal: isHighDefinition ? 1080 : 720, max: 1080, min: 360 },
// 帧率:屏幕内容变化慢,15fps 通常足够;远程桌面操作建议 30fps
frameRate: { ideal: 15, max: 30 },
// 关键:显式声明不需要光标(部分场景),减少带宽
// cursor: 'never' | 'always' | 'motion'
},
audio: {
// 回声消除、噪音抑制对系统音频通常无效,但保留以兼容麦克风混音场景
echoCancellation: true,
noiseSuppression: true,
autoGainControl: true
},
preferCurrentTab: true,
selfBrowserSurface: 'exclude', // 核心自排除开关
systemAudio: 'include',
surfaceSwitching: 'include'
});
工程提示:
ideal代表“最希望值”,浏览器会尽力满足;max/min为硬性边界。实际采集分辨率取决于用户选择的源(显示器物理分辨率、窗口大小),上层应用需通过track.getSettings()实时感知并上报监控。
二、自浏览器排除:从标准属性到工程化兜底方案
“自浏览器排除”指防止用户在共享列表中看到并选择当前正在运行 Web 应用的标签页/窗口。若用户误选,画面将出现“镜中镜”无限递归,严重消耗 CPU/GPU 并干扰业务。
2.1 标准属性 selfBrowserSurface: 'exclude' 的现状
该属性源自 Screen Capture API 扩展草案,旨在由浏览器原生过滤同源/同进程的标签页。
- Chrome 107+ / Edge 107+:支持良好,能有效隐藏当前标签页及同源其他标签页。
- Firefox:较早支持,行为一致。
- Safari (WebKit):支持度滞后,macOS Ventura / iOS 16+ 部分版本开始支持,但行为差异较大(可能仅隐藏当前标签页,不隐藏同源其他标签页)。
- 国产浏览器(内核版本滞后):大量基于旧版 Chromium 定制,可能完全不识别该字段,导致无效。
结论:不能仅依赖该属性,必须构建“标准属性为主,JS 兜底为辅”的双轨制工程体系。
2.2 JS 侧兜底识别与过滤方案(核心工程化实践)
由于 getDisplayMedia 的源选择器由浏览器原生 UI 渲染,Web 侧无法直接干预 UI 列表。兜底方案的核心逻辑是:采集后校验 -> 若检测到自采集 -> 引导用户重选或自动切换源。
方案 A:Canvas 像素特征比对法(通用性强)
原理:当前页面绘制一帧特征图(如动态噪点、特定水印、时间戳),采集流通过 drawImage 绘制到 Canvas,读取像素比对特征是否存在。
class SelfCaptureDetector {
constructor(videoElement, options = {}) {
this.video = videoElement;
this.canvas = document.createElement('canvas');
this.ctx = this.canvas.getContext('2d', { willReadFrequently: true });
this.markerId = `self-capture-marker-${Date.now()}-${Math.random()}`;
this.checkInterval = options.checkInterval || 2000; // 2s 轮询
this.similarityThreshold = options.similarityThreshold || 0.98;
}
// 1. 在当前页面注入不可见特征标记
injectMarker() {
const marker = document.createElement('div');
marker.id = this.markerId;
marker.style.cssText = `
position: fixed; top: 0; left: 0; width: 16px; height: 16px;
pointer-events: none; opacity: 0.01; z-index: 2147483647;
background: linear-gradient(45deg, #ff0000 25%, #00ff00 25%, #00ff00 50%, #ff0000 50%, #ff0000 75%, #00ff00 75%, #00ff00 100%);
background-size: 4px 4px;
animation: markerMove 1s linear infinite;
`;
// 动态注入 keyframes 避免 CSS 污染
const style = document.createElement('style');
style.textContent = `@keyframes markerMove { 0% { background-position: 0 0; } 100% { background-position: 4px 4px; } }`;
document.head.appendChild(style);
document.body.appendChild(marker);
}
// 2. 启动检测循环
startDetection(onDetected) {
this.injectMarker();
this.timer = setInterval(() => this._checkFrame(onDetected), this.checkInterval);
}
_checkFrame(callback) {
if (this.video.readyState < this.video.HAVE_CURRENT_DATA) return;
this.canvas.width = this.video.videoWidth;
this.canvas.height = this.video.videoHeight;
this.ctx.drawImage(this.video, 0, 0);
// 仅采样左上角 16x16 区域,极低性能开销
const imageData = this.ctx.getImageData(0, 0, 16, 16).data;
const markerData = this._getExpectedMarkerData(); // 预计算标记像素特征
if (this._comparePixels(imageData, markerData) > this.similarityThreshold) {
this.stopDetection();
callback && callback(); // 触发自采集回调
}
}
_comparePixels(data1, data2) {
let match = 0;
for (let i = 0; i < data1.length; i += 4) {
// 简单 RGB 差值比对,容忍压缩损耗
const dr = Math.abs(data1[i] - data2[i]);
const dg = Math.abs(data1[i+1] - data2[i+1]);
const db = Math.abs(data1[i+2] - data2[i+2]);
if (dr < 30 && dg < 30 && db < 30) match++;
}
return match / (data1.length / 4);
}
stopDetection() {
clearInterval(this.timer);
const el = document.getElementById(this.markerId);
el?.remove();
}
}
优点:不依赖浏览器版本,准确率高。
缺点:需维护标记 DOM;全屏共享时标记可能被遮挡或缩放导致失效(需结合 resize 事件动态调整标记位置/大小)。
方案 B:MediaStreamTrack contentHint 与 getSettings 启发式判断
利用 track.getSettings().displaySurface 字段(值为 'browser' | 'window' | 'monitor' | 'application')。
async function startScreenShareWithFallback() {
const constraints = getVideoConstraints();
let stream;
try {
stream = await navigator.mediaDevices.getDisplayMedia(constraints);
const videoTrack = stream.getVideoTracks()[0];
// 1. 首选:读取 displaySurface (Chrome 100+, Firefox, Safari 15+)
const settings = videoTrack.getSettings();
if (settings.displaySurface === 'browser') {
console.warn('[ScreenShare] 检测到 displaySurface=browser,疑似自采集');
return handleSelfCapture(stream); // 引导重选
}
// 2. 兜底:启动 Canvas 像素检测
const detector = new SelfCaptureDetector(videoElement);
detector.startDetection(() => handleSelfCapture(stream));
return stream;
} catch (err) {
// 处理 NotAllowedError, NotFoundError 等
throw err;
}
}
function handleSelfCapture(stream) {
stream.getTracks().forEach(t => t.stop()); // 立即停止当前流
// 业务层弹窗提示:"检测到您选择了当前会议页面,请重新选择其他窗口或屏幕"
// 可提供 "重新选择" 按钮再次调用 getDisplayMedia
}
工程建议:将上述逻辑封装为 useScreenShare Hook 或 ScreenShareManager 单例类,内部维护状态机(idle -> selecting -> capturing -> self-detected -> re-selecting),对上层业务透出简洁的 start(), stop(), onSelfCapture 事件。
三、跨浏览器兼容性与异常处理清单
工程化落地中,兼容性处理往往比核心逻辑更繁杂。建议建立能力探测表,运行时动态调整策略。
3.1 关键能力探测代码片段
const ScreenCaptureCaps = {
// 探测 selfBrowserSurface 支持
async supportSelfBrowserExclude() {
try {
// 尝试调用,若浏览器不识别字段通常会忽略或抛出 TypeError
// 注意:无法同步探测,需结合 UA 白名单或尝试调用后观察行为
const stream = await navigator.mediaDevices.getDisplayMedia({
video: true,
selfBrowserSurface: 'exclude',
preferCurrentTab: true
});
stream.getTracks().forEach(t => t.stop());
return true;
} catch (e) {
return false; // 或根据 e.name 判断
}
},
// 探测 systemAudio 支持
supportSystemAudio() {
return 'systemAudio' in navigator.mediaDevices.getDisplayMedia?.toString() ||
/Chrome/(d+)/.test(navigator.userAgent) && parseInt(RegExp.$1) >= 105;
},
// 探测 surfaceSwitching 支持
supportSurfaceSwitching() {
return /Chrome/(d+)/.test(navigator.userAgent) && parseInt(RegExp.$1) >= 112;
}
};
3.2 常见异常码与处理策略
DOMException.name |
典型场景 | 处理策略 |
|---|---|---|
NotAllowedError |
用户点击“取消”或权限被拒 | 埋点上报,UI 给予引导文案(如“请在弹窗中点击允许”) |
NotFoundError |
无可用源(如无显示器、虚拟机环境) | 提示“未检测到可共享屏幕”,降级为窗口共享或取消功能 |
NotReadableError |
硬件占用、驱动异常、或 macOS 系统权限未开启 | 提示“请在系统设置-安全性与隐私-屏幕录制中授权浏览器” |
OverconstrainedError |
约束过严(如要求 4K 但显示器仅 1080P) | 捕获后自动降级约束重试,或移除 min/exact 硬性约束 |
AbortError |
页面卸载、调用 track.stop() 或新调用打断旧调用 |
正常业务流程,清理资源即可 |
四、性能优化与工程化最佳实践
4.1 编码端参数联动建议
屏幕共享内容特征(高静态、低动态、文字锐利)与摄像头视频截然不同,编码器配置需差异化:
- 编码器选择:优先
VP9/H.265 (HEVC)/AV1(需硬编支持),次选VP8/H.264。屏幕内容对 VP9 的屏幕内容编码工具(SCC)收益显著。 - 关键帧间隔 (GOP):建议设置 2s - 5s(摄像头通常 1s-2s)。屏幕变化少,大 GOP 节省带宽。
- 码率控制:推荐 CBR (恒定码率) 或 VBR 上限模式。参考分辨率设定上限:1080p@15fps 建议 1.5-3 Mbps;720p@15fps 建议 800-1500 Kbps。
- 可伸缩视频编码 (SVC / Simulcast):若服务端支持,开启 2-3 层 Simulcast(如 1080p / 720p / 360p),配合 SFU 路由,保障弱网下拉流端体验。
4.2 资源生命周期管理
class ScreenShareController {
constructor() {
this.stream = null;
this.detector = null;
this.videoEl = document.createElement('video');
this.videoEl.muted = true; // 本地预览必须静音
this.videoEl.playsInline = true;
}
async start() {
if (this.stream) return this.stream;
this.stream = await startScreenShareWithFallback(); // 含自排除逻辑
this.videoEl.srcObject = this.stream;
await this.videoEl.play().catch(() => {}); // 忽略自动播放策略拦截
// 监听轨道结束(用户点击浏览器原生停止共享按钮)
this.stream.getVideoTracks()[0].addEventListener('ended', () => this._onTrackEnded());
return this.stream;
}
_onTrackEnded() {
this.detector?.stopDetection();
this.stream = null;
this.videoEl.srcObject = null;
this.emit('stopped'); // 通知业务层更新 UI
}
stop() {
this.stream?.getTracks().forEach(t => t.stop());
this._onTrackEnded();
}
}
关键点:
- 必须监听
track.onended,这是感知用户通过浏览器原生浮窗“停止共享”的唯一可靠途径。 video.muted = true避免本地预览产生回声(若采集了系统音频)。- 组件销毁时(Vue
onUnmounted/ ReactuseEffectcleanup)强制调用stop(),防止幽灵流持续占用摄像头/麦克风图标。
五、总结与落地检查清单
WebRTC getDisplayMedia 看似调用简单,工程化落地却涉及标准演进跟踪、跨内核兼容、用户体验兜底、媒体流生命周期管理等多维度挑战。
落地交付清单:
- [ ] 约束配置:分辨率/帧率分级策略;
preferCurrentTab: true;systemAudio按业务开启。 -
[ ] 自排除双轨制:
- [ ] 传入
selfBrowserSurface: 'exclude'(现代浏览器首选)。 - [ ] 集成 Canvas 像素特征检测兜底(覆盖旧内核/国产浏览器/Safari)。
- [ ] 检测到自采集时:自动停流 + 友好引导重选 + 埋点上报。
- [ ] 传入
- [ ] 兼容性:建立浏览器最低版本要求白名单;针对不支持
getDisplayMedia的极老旧环境(如 IE、旧版 WebView)给出降级提示。 - [ ] 异常处理:全覆盖
DOMException分支,配合用户可读的错误码映射表。 - [ ] 性能监控:上报实际采集分辨率(
getSettings)、帧率、码率、延迟、自排除触发率、用户重选率。 - [ ] 资源释放:全路径(正常停止、用户原生停止、页面卸载、报错中断)均验证
track.stop()执行。
通过上述规范化实践,可构建一个鲁棒性高、兼容性广、用户体验可控的屏幕共享模块,为公司 RTC 产品提供可靠的基础设施支撑。
WebRTC getDisplayMedia 进阶工程化:多屏高DPI适配、音频管线治理、安全合规与自动化测试体系
接上篇基础配置与自排除核心实践,本文聚焦多显示器高DPI坐标映射、系统音频管线深度治理、数据安全合规落地、以及自动化测试体系建设四大进阶工程课题,解决从“能用”到“稳、合规、可迭代”的交付难题。
一、 多显示器与高 DPI 环境下的坐标映射与裁剪策略
在多显示器异构缩放(如主屏 150%、副屏 100%)及 Retina/高刷屏场景下,getDisplayMedia 返回的 MediaStreamTrack 分辨率往往与 CSS 逻辑像素、物理像素、设备独立像素(DIP)混淆,导致远端观看模糊、本地标记检测偏移、区域共享坐标错位等问题。
1.1 像素体系对齐:从 getSettings 到物理分辨率
// 获取真实物理分辨率与设备像素比 (DPR)
function getPhysicalVideoMeta(track) {
const settings = track.getSettings();
const { width, height } = settings;
// 关键:videoWidth/videoHeight 为编码器输出的物理像素宽高
// 但 track.getSettings() 在某些浏览器(如 macOS Chrome)返回的是逻辑像素
// 需结合 videoElement.videoWidth 校准
const videoEl = document.createElement('video');
videoEl.srcObject = new MediaStream([track]);
await videoEl.play();
const physicalWidth = videoEl.videoWidth;
const physicalHeight = videoEl.videoHeight;
const logicalWidth = settings.width;
const logicalHeight = settings.height;
// 计算真实 DPR (设备像素比)
const dprX = physicalWidth / logicalWidth;
const dprY = physicalHeight / logicalHeight;
return {
physical: { width: physicalWidth, height: physicalHeight },
logical: { width: logicalWidth, height: logicalHeight },
dpr: { x: dprX, y: dprY }, // 非均匀缩放场景 x/y 可能不同
displaySurface: settings.displaySurface
};
}
工程启示:
- 编码分辨率上报:监控上报
physicalWidth/Height而非settings,避免误判带宽需求。 - Canvas 检测坐标换算:上文
SelfCaptureDetector中drawImage采样区域需乘以dpr,否则在 200% 缩放下仅采样到左上角 1/4 区域,导致漏检。
1.2 区域共享坐标换算最佳实践
当业务支持“共享应用窗口部分区域”(如仅共享编辑器代码区)时,需将业务层 CSS 坐标转换为视频轨道物理坐标:
// 业务坐标 -> 视频轨道物理坐标
function mapBusinessRectToTrackRect(businessRect, trackMeta, targetWindow) {
// 1. 业务 CSS 坐标 -> 目标窗口客户区逻辑坐标
// 需扣除浏览器边框、标题栏、开发工具栏高度 (无标准 API,需启发式或 Native 通信获取)
const chromeOffsets = estimateBrowserChromeOffsets(targetWindow);
const logicalInWindow = {
x: businessRect.x + chromeOffsets.left,
y: businessRect.y + chromeOffsets.top,
width: businessRect.width,
height: businessRect.height
};
// 2. 逻辑坐标 -> 物理坐标 (乘以 DPR)
return {
x: Math.round(logicalInWindow.x * trackMeta.dpr.x),
y: Math.round(logicalInWindow.y * trackMeta.dpr.y),
width: Math.round(logicalInWindow.width * trackMeta.dpr.x),
height: Math.round(logicalInWindow.height * trackMeta.dpr.y)
};
}
规避坑位:macOS 下共享“窗口”而非“屏幕”时,返回流分辨率固定为窗口逻辑大小,但
videoWidth可能为物理像素(Retina 下 2x)。Windows 下高 DPI 缩放若未在chrome://flags开启 “High DPI support”,浏览器进程可能被 DPI 虚拟化,导致采集模糊。建议 Electron/Tauri 客户端在启动参数强制--high-dpi-support=1 --force-device-scale-factor=1。
二、 系统音频管线深度治理:分离、增益、静音检测与合规
屏幕共享音频(systemAudio)是会议体验的隐形杀手:回声、啸叫、他人隐私泄露、版权音乐触发内容审核均源于此。
2.1 音频采集架构分层设计
建议在 AudioContext 层构建可插拔处理链,而非直接将 stream.getAudioTracks()[0] 推送给 RTCPeerConnection。
graph LR
A[getDisplayMedia AudioTrack] --> B(MediaStreamAudioSourceNode)
B --> C[GainNode: 统一增益归一化 -18dBFS]
C --> D{业务开关}
D -- 会议模式 --> E[WebRTC AEC Node: 回声消除]
D -- 直播/录制模式 --> F[Bypass AEC: 保真度优先]
E --> G[DynamicsCompressorNode: 防炸音/限幅]
F --> G
G --> H[AnalyserNode: 实时音量/静音检测]
H --> I[MediaStreamAudioDestinationNode]
I --> J[RTCPeerConnection.addTrack]
2.2 关键工程控制点
| 控制点 | 实现方案 | 合规/体验价值 |
|---|---|---|
| 增益归一化 | GainNode.gain.value = 0.125 (-18dB),配合 AudioContext.getOutputTimestamp() 监控实际电平 |
解决不同应用/系统音量差异巨大问题,防止入会炸耳 |
| 回声消除 (AEC) 策略 | 会议模式:createMediaStreamSource(micStream) + createMediaStreamDestination() 构建 AEC 参考链路;直播模式:绕过 AEC |
避免“共享视频声音被麦克风拾回”形成啸叫;直播保真 |
| 静音/低音检测 | AnalyserNode.getByteFrequencyData() 计算 RMS,连续 5s < -50dBFS 触发 onAudioSilent 事件 |
UI 提示“未检测到系统声音,请检查音量合成器”;满足广告法“虚假宣传”规避 |
| 敏感音频过滤 | 集成轻量级关键词检测 (如 Porcupine/Picovoice WASM 版) 或频谱指纹匹配 | 识别版权音乐、报警声、隐私关键词,触发本地静音/替换/上报,规避合规风险 |
2.3 系统音量分离难题与妥协方案
痛点:用户希望“共享 Chrome 标签页声音,但不共享微信通知声”。
- Chrome 标签页共享:原生支持标签页级音频隔离(最佳体验)。
- 窗口/屏幕共享:仅能获取系统混音总线,无法 Web 侧分离进程音频。
-
工程妥协:
- UI 引导:“请优先选择‘标签页共享’以实现声音隔离”。
- Electron/Tauri 客户端:集成 Windows WASAPI Loopback (Process Loopback) 或 macOS ScreenCaptureKit (SCContentFilter),在原生层按进程 PID 过滤音频,再经
MediaStreamTrackProcessor/Insertable Streams注入 WebRTC 管线。这是企业级产品的核心护城河。
三、 数据安全与隐私合规:从“功能实现”到“合规交付”
依据《网络安全法》《数据安全法》《个人信息保护法》及广告法“绝对化用语”禁令,屏幕共享属于高风险个人信息处理活动,必须内置合规基因。
3.1 最小化采集与敏感信息遮罩
原则:默认不采集,用户显式授权采集何种内容。
// 合规配置生成器
function createCompliantConstraints(userConsent) {
const base = { video: true, audio: false }; // 默认关闭音频
if (userConsent.systemAudio) {
base.audio = {
systemAudio: 'require', // 强制要求,失败则中断,避免静默失败
// 广告法合规:不得承诺“完美录制系统声音”,文案用“尝试采集”
};
}
// 敏感场景:文档编辑、代码 IDE、银行页面 -> 建议仅共享窗口而非全屏
if (userConsent.scene === 'document') {
base.video = { displaySurface: 'window' }; // 强制窗口模式
base.preferCurrentTab = false;
}
return base;
}
3.2 录制与存储合规红线
- 本地录制合规:若提供“本地录制下载”,必须在录制开始前二次弹窗确认,明确告知“录制文件仅存本地,不上传服务器”,且文件名不含会议 ID 等关联标识。
-
云端录制合规:
- 服务端录制前必须获取所有参会者显式同意(含共享者)。
- 录制文件加密存储(AES-256),访问需双因子认证。
- 水印溯源:在服务端合流时嵌入不可见水印(用户 UID + 时间戳),防泄露溯源。
-
广告法文案规避:
- ❌ “完美屏幕共享”、“零延迟”、“绝对安全”、“全网最清晰”
- ✅ “高清流畅的屏幕共享体验”、“毫秒级低延迟传输”、“银行级加密传输”、“支持 4K 分辨率采集”
3.3 隐私计算视角的“遮罩层”设计
对于企业级客户(金融/医疗/政务),提供应用级遮罩 SDK:
- 原理:在共享窗口 Z-Order 顶层叠加透明
div,监听鼠标/键盘事件穿透 (pointer-events: none),但截图/录屏 API 采集时捕获到遮罩层。 - 实现:配合
Element.captureStream()或 浏览器扩展chrome.tabCapture实现“逻辑遮罩,物理不遮挡”。 - 场景:遮挡聊天工具弹窗、密码管理器下拉框、身份证水印区域。
四、 自动化测试与 CI/CD 集成:从人工验收到质量红线守护
屏幕共享依赖 OS 窗口管理器、GPU 驱动、浏览器原生 UI,单元测试覆盖率极低,必须建立端到端 (E2E) 视觉回归 + 性能基线体系。
4.1 测试环境标准化:虚拟显示器与音频虚拟设备
Linux CI (GitHub Actions / GitLab CI / Jenkins) 配置:
# .github/workflows/screen-share-e2e.yml
jobs:
e2e-screen-share:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# 1. 启动虚拟显示器 (Xvfb) + 窗口管理器 (Fluxbox) + VNC (可选调试)
- name: Setup Virtual Display
run: |
Xvfb :99 -screen 0 1920x1080x24 -ac +extension GLX +render -noreset &
fluxbox -display :99 &
export DISPLAY=:99
# 2. 创建虚拟音频设备 (PipeWire/PulseAudio) 模拟系统声音
- name: Setup Virtual Audio
run: |
pactl load-module module-null-sink sink_name=VirtualSpeaker
pactl load-module module-remap-source master=VirtualSpeaker.monitor source_name=VirtualMic
# 3. 启动被测应用 & 测试目标窗口 (如一个固定的 HTML 页面播放测试视频)
- name: Start Test Target
run: |
google-chrome-stable --disable-gpu --no-sandbox --headless=new
--use-fake-ui-for-media-stream
--use-fake-device-for-media-stream
--use-file-for-fake-video-capture=test.y4m
--use-file-for-fake-audio-capture=test.wav
http://localhost:3000/test-target.html &
# 4. 运行 Playwright/Cypress 测试
- name: Run E2E Tests
run: npm run test:e2e:screen-share
4.2 核心测试用例矩阵 (Playwright 示例)
// tests/screen-share.spec.ts
import { test, expect } from '@playwright/test';
test.describe('Screen Share Engineering Quality Gates', () => {
test('Self-Exclusion: Current tab MUST NOT appear in picker @chrome @firefox @webkit', async ({ page, browserName }) => {
await page.goto('/meeting-room');
// 1. 触发分享
const sharePromise = page.evaluate(() => navigator.mediaDevices.getDisplayMedia({
video: { selfBrowserSurface: 'exclude' },
preferCurrentTab: true
}));
// 2. Playwright 无法直接操作原生 Picker,需依赖 --use-fake-ui-for-media-stream
// 或使用 CDP/DevTools Protocol 模拟选择 (高级用法)
// 此处假设使用 fake UI 自动选择第一个非当前标签源
const stream = await sharePromise;
const track = stream.getVideoTracks()[0];
// 3. 断言:采集源不是当前页面 (通过像素特征或 displaySurface 判断)
const settings = track.getSettings();
expect(settings.displaySurface).not.toBe('browser'); // 标准属性校验
// 4. 像素级兜底校验 (调用上文 Detector 逻辑)
const isSelf = await page.evaluate(() => window.__SELF_DETECTOR__.checkOnce());
expect(isSelf).toBeFalsy();
track.stop();
});
test('High DPI: Captured physical resolution matches monitor @chrome', async ({ page }) => {
// 需在 CI 配置 200% scaling (GDK_SCALE=2, QT_AUTO_SCREEN_SCALE_FACTOR=1)
await page.goto('/meeting-room');
const stream = await page.evaluate(() => navigator.mediaDevices.getDisplayMedia({ video: true }));
const track = stream.getVideoTracks()[0];
// 断言物理分辨率 = 逻辑分辨率 * DPR
const videoEl = await page.$('video[autoplay]');
const physicalW = await videoEl.evaluate(el => el.videoWidth);
expect(physicalW).toBe(3840); // 1920 * 2
track.stop();
});
test('Audio Pipeline: System audio captured & RMS > threshold', async ({ page }) => {
// 前置:虚拟音频设备播放 1kHz 正弦波 -18dBFS
await page.evaluate(() => window.__TEST_AUDIO__.playTone(1000, -18));
const stream = await page.evaluate(() => navigator.mediaDevices.getDisplayMedia({
video: true,
audio: { systemAudio: 'require' }
}));
const audioTrack = stream.getAudioTracks()[0];
// 使用 AudioContext 分析 RMS
const rms = await page.evaluate((track) => {
const ctx = new AudioContext();
const src = ctx.createMediaStreamSource(new MediaStream([track]));
const analyser = ctx.createAnalyser();
analyser.fftSize = 2048;
src.connect(analyser);
const data = new Uint8Array(analyser.frequencyBinCount);
analyser.getByteFrequencyData(data);
// 简单 RMS 估算
const sum = data.reduce((a, b) => a + b * b, 0);
return Math.sqrt(sum / data.length);
}, audioTrack);
// 断言信号存在 (阈值需根据虚拟设备校准)
expect(rms).toBeGreaterThan(10);
audioTrack.stop();
stream.getVideoTracks()[0].stop();
});
test('Performance Baseline: CPU/Memory growth < 10% in 30min @stress', async ({ page }) => {
// 长时压力测试,监控 performance.measureUserTiming / Chrome DevTools Protocol Metrics
// 仅在 Nightly/Weekly CI 运行
});
});
4.3 视觉回归测试
利用 pixelmatch 或 looks-same 对比远端拉流画面与标准参考帧:
- 基准图生成:在受控环境(固定分辨率、固定测试视频源、固定编码参数)生成首帧、第 100 帧、动态切换帧的 PNG 基准图。
- CI 对比:每次 PR 合并前跑 E2E,计算 SSIM (结构相似性) 或像素差值。
- 阈值:SSIM < 0.98 或 像素差 > 0.1% 判定失败,防止编码器回归、色彩空间错误 (BT.709 vs BT.601)、关键帧间隔异常。
五、 总结:构建企业级屏幕共享交付标准
将上下两篇文章的工程要点沉淀为《屏幕共享工程化交付规范 v1.0》清单,作为团队代码评审、验收测试、技术债治理的统一标尺:
| 领域 | 核心指标 | 验收方式 | 责任人 |
|---|---|---|---|
| 基础约束 | 分辨率阶梯策略、帧率自适应、约束降级重试 | 单测 + 集成测试 | 客户端 Leader |
| 自排除 | selfBrowserSurface 覆盖率 100%,JS 兜底漏检率 < 0.1% |
E2E 自动化 (多浏览器矩阵) | 核心开发 |
| 多屏高DPI | 物理分辨率准确率 100%,坐标映射偏移 < 2px | 真机矩阵测试 (Win/Mac/Linux, 100%/125%/150%/200%) | QA + 客户端 |
| 音频管线 | 回声消除 MOS 提升 > 0.5,静音检测准确率 > 99% | 主观测评 (ITU-T P.800) + 自动化 RMS 校验 | 音视频专家 |
| 安全合规 | 敏感信息遮罩生效率 100%,录制授权二次确认 100% | 法务审核 + 渗透测试 + 代码扫描 | 安全/法务 |
| 工程质量 | E2E 通过率 100%,视觉回归 0 基线偏差,性能基线无回退 | CI/CD 红线阻断合并 | DevOps + QA |
结语:
WebRTC getDisplayMedia 看似是一个简单的浏览器 API 调用,实则是操作系统窗口管理、图形渲染管线、音频驱动架构、浏览器安全沙箱、隐私法规边界的交汇点。只有建立起“标准属性为骨、JS 兜底为肉、原生增强为魂、自动化测为盾”的四位一体工程体系,才能支撑起企业级实时音视频产品在复杂真实环境下的稳定交付与持续进化。
