16、屏幕共享暂停/恢复:暂停轨道、恢复轨道、状态指示器设计
屏幕共享做到一半,突然想切出去处理个私密消息,或者需要临时遮挡一下画面——这时候如果直接关掉共享,再重新开启,流程就太笨重了。
更好的做法是什么?暂停轨道。
说白了,就是让视频轨道暂时停止发送数据,但保留连接和轨道对象。等需要的时候,再恢复发送。整个过程对远端用户来说,看到的要么是黑屏,要么是冻结帧,要么是一个「已暂停」的提示。
我早期做远程协作工具时,就踩过这个坑。用户反馈说「我只是想暂停一下,为什么每次都要重新选窗口?」嗯,从那以后,暂停/恢复就成了我屏幕共享模块的标配功能。
16.1 暂停轨道:让 MediaStreamTrack 休眠
暂停的核心 API 其实很简单——MediaStreamTrack.enabled = false。
你可能会问:enabled 和 muted 有什么区别?
我简单解释一下:
- enabled:由应用层控制。设为 false 时,轨道会停止发送实际数据,远端会收到黑帧或静音帧。
- muted:由浏览器控制。比如用户拔掉摄像头,浏览器会自动将轨道设为 muted。
所以,暂停屏幕共享,本质上就是把 enabled 置为 false。
// 暂停屏幕共享轨道
function pauseScreenShare(sender) {
const track = sender.track;
if (track && track.kind === 'video') {
track.enabled = false;
console.log('屏幕共享已暂停');
updateIndicator('paused');
}
}
这里有个细节:sender.track 是 RTCRtpSender 上关联的轨道对象。暂停后,RTCPeerConnection 不会断开,只是视频流变成了黑帧。
replaceTrack() 替换成一个静态视频轨道。我后面会讲这个进阶技巧。
16.2 恢复轨道:重新激活数据流
恢复就更直接了——把 enabled 设回 true。
// 恢复屏幕共享轨道
function resumeScreenShare(sender) {
const track = sender.track;
if (track && track.kind === 'video') {
track.enabled = true;
console.log('屏幕共享已恢复');
updateIndicator('active');
}
}
恢复后,浏览器会重新开始采集屏幕内容并发送。远端会从黑帧切回正常画面。
不过,我遇到过一个问题:恢复后画面卡在最后一帧。怎么回事?
排查后发现,是某些浏览器在 enabled 从 false 切回 true 时,需要重新触发一次 getDisplayMedia 的约束。嗯,解决方案是在恢复后,手动触发一次 track.applyConstraints(),强制浏览器重新采集。
async function resumeWithRefresh(sender) {
const track = sender.track;
if (track && track.kind === 'video') {
track.enabled = true;
// 强制刷新采集
try {
await track.applyConstraints({
width: { ideal: 1920 },
height: { ideal: 1080 },
frameRate: { ideal: 30 }
});
} catch (e) {
console.warn('刷新约束失败,但不影响恢复', e);
}
updateIndicator('active');
}
}
sender.replaceTrack(originalTrack) 换回原来的屏幕采集轨道。
16.3 状态指示器设计:让用户知道当前状态
暂停/恢复不能偷偷摸摸做。用户需要明确知道:
- 当前是「共享中」还是「已暂停」
- 暂停了多久
- 远端看到的是什么
我习惯设计三种状态:
| 状态 | 图标 | 文字提示 | 行为 |
|---|---|---|---|
| active | 🟢 绿色圆点 | 屏幕共享中 | 正常采集并发送 |
| paused | 🟡 黄色暂停符号 | 屏幕共享已暂停 | 轨道 enabled = false |
| inactive | 🔴 红色圆点 | 屏幕共享已停止 | 轨道已移除或关闭 |
状态指示器的更新逻辑,我建议放在一个统一的方法里:
function updateIndicator(state) {
const indicator = document.getElementById('screen-share-indicator');
const statusText = document.getElementById('screen-share-status');
switch (state) {
case 'active':
indicator.className = 'indicator active';
statusText.textContent = '屏幕共享中';
break;
case 'paused':
indicator.className = 'indicator paused';
statusText.textContent = '屏幕共享已暂停';
break;
case 'inactive':
indicator.className = 'indicator inactive';
statusText.textContent = '屏幕共享已停止';
break;
default:
break;
}
}
UI 上,我推荐用 CSS 动画让指示器更直观:
.indicator {
width: 12px;
height: 12px;
border-radius: 50%;
display: inline-block;
margin-right: 8px;
transition: background-color 0.3s ease;
}
.indicator.active {
background-color: #4caf50;
box-shadow: 0 0 6px #4caf50;
animation: pulse 1.5s infinite;
}
.indicator.paused {
background-color: #ff9800;
box-shadow: 0 0 6px #ff9800;
}
.indicator.inactive {
background-color: #f44336;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
你看,active 状态加了呼吸动画,paused 是静态橙色,inactive 是红色。用户一眼就能分辨。
16.4 暂停/恢复的完整流程
我把整个流程画了一张图,方便你理解:
从图里你能看到,暂停和恢复是在活跃和暂停状态之间来回切换,而停止则是彻底结束共享。远端用户看到的反馈也完全不同。
16.5 进阶:暂停时显示自定义提示画面
黑帧太单调了。我建议在暂停时,用 Canvas 生成一张「屏幕共享已暂停」的提示图,然后通过 replaceTrack() 替换掉原来的屏幕轨道。
let canvasStream = null;
async function pauseWithOverlay(sender) {
const originalTrack = sender.track;
// 创建 Canvas 并绘制提示
const canvas = document.createElement('canvas');
canvas.width = 640;
canvas.height = 360;
const ctx = canvas.getContext('2d');
ctx.fillStyle = '#2c3e50';
ctx.fillRect(0, 0, 640, 360);
ctx.fillStyle = '#ffffff';
ctx.font = 'bold 32px Arial';
ctx.textAlign = 'center';
ctx.fillText('⏸ 屏幕共享已暂停', 320, 180);
ctx.font = '18px Arial';
ctx.fillText('点击"恢复"继续共享', 320, 230);
// 生成 MediaStream
canvasStream = canvas.captureStream(1); // 1 fps 就够了
const fakeTrack = canvasStream.getVideoTracks()[0];
// 替换轨道
await sender.replaceTrack(fakeTrack);
originalTrack.enabled = false;
updateIndicator('paused');
}
async function resumeWithOriginal(sender, originalTrack) {
await sender.replaceTrack(originalTrack);
originalTrack.enabled = true;
// 停止 Canvas 流
if (canvasStream) {
canvasStream.getTracks().forEach(t => t.stop());
canvasStream = null;
}
updateIndicator('active');
}
16.6 避坑指南
最后,分享几个我踩过的坑:
- enabled 切换后,部分浏览器会触发 track.onended:我遇到过 Chrome 在 enabled 设为 false 后,如果用户切走了屏幕窗口,轨道会直接 ended。解决方案是监听 onended 事件,如果是暂停状态,不要自动停止共享。
- 恢复后画面撕裂:某些显卡驱动下,恢复后前几帧会出现撕裂。我建议恢复后延迟 200ms 再更新状态指示器,给浏览器一点缓冲时间。
- 多显示器场景:如果用户暂停时切换了显示器,恢复后可能采集到错误的屏幕。嗯,我一般会在恢复时重新检查
getDisplayMedia的约束,确保采集源正确。
暂停/恢复功能虽然代码不多,但用户体验提升巨大。你想想看,用户不用重新选窗口、不用重新授权,一键暂停一键恢复,多顺畅。
好了,这一节就到这里。下一节我们聊聊屏幕共享中的帧率控制和分辨率适配——这两个参数调好了,画面质量和性能才能兼得。
公众号:蓝海资料掘金营,微信deep3321