17、屏幕共享进阶:多源共享、标注功能、远程控制、性能优化
屏幕共享,说白了就是把你的桌面、某个窗口或者浏览器标签页,实时地传给远端的人看。基础功能我们上一章已经聊过了,但实际项目中,光能共享一个屏幕远远不够。
我做过好几个视频会议项目,客户的需求五花八门。有人想同时共享两个屏幕,有人想在共享的画面上画圈圈,还有人想直接远程操控对方的电脑。嗯,这些才是屏幕共享的进阶玩法。今天我们就来拆解这些功能。
核心要点:多源共享、标注功能、远程控制、性能优化,这四个模块构成了企业级屏幕共享的完整能力。
17.1 多源共享:同时共享多个画面
先说说多源共享。你想想看,一个老师上课,可能想同时展示PPT和代码编辑器。一个设计师评审,可能想同时展示设计稿和浏览器预览。单屏幕共享显然不够用。
WebRTC 的 getDisplayMedia() 一次只能捕获一个屏幕。那怎么实现多源共享?
我的做法是:建立多个独立的 MediaStream,每个流对应一个共享源。然后通过多个 RTCPeerConnection 或者同一个连接里的多个 RTCRtpSender 来传输。
具体来说,有两种方案:
| 方案 | 描述 | 适用场景 |
|---|---|---|
| 多 PeerConnection | 每个共享源创建一个独立的 PeerConnection | 源之间完全独立,互不干扰 |
| 多 Track 单连接 | 在同一个 PeerConnection 中添加多个 VideoTrack | 源数量少,需要同步播放 |
我个人习惯用多 Track 单连接方案。因为连接数少,信令开销小,而且多个画面在接收端可以同步渲染。
// 多源共享核心代码
async function addShareSource(streamType) {
// 捕获新的共享源
const stream = await navigator.mediaDevices.getDisplayMedia({
video: true,
audio: false
});
const track = stream.getVideoTracks()[0];
// 添加到已有的 PeerConnection
const sender = peerConnection.addTrack(track, stream);
// 记录发送者信息,方便后续移除
shareSenders.push({
sender: sender,
track: track,
type: streamType
});
// 触发重新协商
await doNegotiation();
}
// 移除某个共享源
function removeShareSource(index) {
const item = shareSenders[index];
peerConnection.removeTrack(item.sender);
item.track.stop();
shareSenders.splice(index, 1);
doNegotiation();
}
小提示:多源共享时,接收端需要为每个 Track 创建独立的 <video> 元素。建议用 ontrack 事件动态创建,不要写死。
17.2 标注功能:在共享画面上做标记
标注功能,就是让观看者能在共享的画面上画线、画框、打字。这个功能在远程协作中非常实用。
实现思路其实不复杂:在接收端的 <video> 元素上覆盖一个透明的 Canvas,然后监听鼠标事件,在 Canvas 上绘制。
但有个坑——画布和视频的坐标要对齐。视频可能有黑边,或者被缩放,直接拿鼠标坐标画上去会偏移。
我曾经在这个问题上栽过跟头。用户明明在视频中间画了个圈,结果圈跑到左上角去了。后来我加了个坐标转换函数:
// 坐标转换:从鼠标位置映射到视频实际内容位置
function mapToVideoCoords(mouseX, mouseY, videoElement, canvasElement) {
const videoRect = videoElement.getBoundingClientRect();
const canvasRect = canvasElement.getBoundingClientRect();
// 计算视频内容在 canvas 中的实际显示区域
const videoWidth = videoElement.videoWidth;
const videoHeight = videoElement.videoHeight;
// 处理 object-fit: contain 的缩放和偏移
const scaleX = videoWidth / videoRect.width;
const scaleY = videoHeight / videoRect.height;
// 计算黑边偏移
const offsetX = (canvasRect.width - videoRect.width) / 2;
const offsetY = (canvasRect.height - videoRect.height) / 2;
return {
x: (mouseX - offsetX) * scaleX,
y: (mouseY - offsetY) * scaleY
};
}
标注的数据结构也很重要。我一般用 JSON 数组来存储所有标注元素:
// 标注数据结构
const annotations = [
{
type: 'rect', // 矩形
x: 100, y: 200,
width: 300, height: 150,
color: '#ff0000',
lineWidth: 3
},
{
type: 'arrow', // 箭头
startX: 50, startY: 100,
endX: 400, endY: 300,
color: '#00ff00',
lineWidth: 2
},
{
type: 'text', // 文字
x: 200, y: 150,
content: '注意这里',
fontSize: 24,
color: '#ffffff'
}
];
注意:标注数据需要实时同步给所有观看者。建议通过 DataChannel 传输,延迟低。不要用信令服务器转发,太慢了。
17.3 远程控制:让观看者操作共享者的电脑
远程控制,这个功能比较敏感。说白了就是让观看者能模拟鼠标和键盘操作,直接控制共享者的电脑。
实现原理:观看者端发送鼠标/键盘事件 → 通过 DataChannel 传给共享者 → 共享者端模拟执行。
在共享者端,我们需要用 JavaScript 模拟鼠标移动、点击、键盘输入。浏览器有 dispatchEvent 可以触发事件,但有个限制——浏览器安全策略不允许跨域模拟用户输入。
所以远程控制通常需要配合 Native 客户端 或者 浏览器扩展 来实现。纯 Web 方案只能做到「半控制」——比如移动鼠标、滚动页面,但无法模拟键盘输入。
我做过一个折中方案:共享者安装一个轻量级的桌面应用,通过 WebSocket 接收控制指令,然后用系统 API 模拟操作。浏览器端只负责展示和传输指令。
// 观看者端:捕获鼠标事件并发送
canvas.addEventListener('mousemove', (e) => {
const coords = mapToVideoCoords(e.clientX, e.clientY, video, canvas);
dataChannel.send(JSON.stringify({
type: 'mouse_move',
x: coords.x,
y: coords.y
}));
});
canvas.addEventListener('click', (e) => {
const coords = mapToVideoCoords(e.clientX, e.clientY, video, canvas);
dataChannel.send(JSON.stringify({
type: 'mouse_click',
x: coords.x,
y: coords.y,
button: 'left'
}));
});
// 共享者端(Native 客户端):接收并执行
// 伪代码,实际需要调用系统 API
function onControlMessage(msg) {
switch(msg.type) {
case 'mouse_move':
system.mouseMove(msg.x, msg.y);
break;
case 'mouse_click':
system.mouseClick(msg.x, msg.y, msg.button);
break;
case 'key_press':
system.keyPress(msg.key);
break;
}
}
安全警告:远程控制功能必须加权限控制。建议默认关闭,由共享者手动授权。而且每次控制会话都要有明确的开始和结束提示。
17.4 性能优化:让共享流畅不卡顿
屏幕共享最怕什么?卡顿、模糊、延迟高。尤其是多源共享时,带宽压力翻倍。
性能优化,我总结为三个方向:编码优化、传输优化、渲染优化。
17.4.1 编码优化
屏幕内容跟摄像头画面不一样。屏幕大多是静态的、文字多的、颜色单一的。用默认的视频编码参数,效果很差。
我建议做两件事:
- 降低帧率:屏幕共享不需要 30fps,15fps 就够。静态内容甚至可以降到 5fps。
- 提高分辨率:屏幕共享需要清晰度,分辨率尽量保持原始比例,不要压缩。
// 设置编码参数
const params = {
video: {
width: { ideal: 1920 },
height: { ideal: 1080 },
frameRate: { ideal: 15 }, // 降低帧率
bitrate: 2000000 // 2Mbps,根据网络调整
}
};
const sender = peerConnection.getSenders().find(s => s.track.kind === 'video');
if (sender) {
const parameters = sender.getParameters();
if (!parameters.encodings) {
parameters.encodings = [{}];
}
parameters.encodings[0].maxBitrate = 2000000;
parameters.encodings[0].maxFramerate = 15;
await sender.setParameters(parameters);
}
17.4.2 传输优化
传输层面,核心是 Simulcast(联播) 和 SVC(可伸缩视频编码)。
Simulcast 就是同时发送多个分辨率的流,接收端根据网络情况选择。SVC 则是只发一个流,但编码成多层,网络差时丢弃增强层。
我个人更推荐 Simulcast,因为浏览器支持更好,控制更灵活。
// 开启 Simulcast
const transceiver = peerConnection.addTransceiver(track, {
direction: 'sendonly',
streams: [stream],
sendEncodings: [
{ rid: 'high', maxBitrate: 2000000, scaleResolutionDownBy: 1.0 },
{ rid: 'mid', maxBitrate: 800000, scaleResolutionDownBy: 2.0 },
{ rid: 'low', maxBitrate: 200000, scaleResolutionDownBy: 4.0 }
]
});
17.4.3 渲染优化
接收端渲染时,不要直接操作 DOM 频繁更新。用 requestAnimationFrame 控制渲染节奏。
另外,如果标注 Canvas 和视频重叠,记得把 Canvas 的 pointer-events 设置为 none,避免干扰视频的点击事件。
性能小技巧:屏幕共享时,如果共享者电脑性能差,可以开启硬件加速。在 getDisplayMedia 的约束中加 video: { frameRate: 10 },能明显降低 CPU 占用。
17.5 知识体系总览
下面这张图,把屏幕共享进阶的四个模块串起来了。你可以看到每个模块的核心技术和它们之间的关系。
17.6 避坑指南
最后,分享几个我踩过的坑:
- 多源共享时音频冲突:多个共享源如果都包含音频,接收端会混音。建议只保留一个源的音频,或者用
audio: false关闭其他源的音频。 - 标注 Canvas 性能:如果标注元素太多(比如上百个),Canvas 重绘会卡。建议用离屏 Canvas 缓存静态元素,只重绘动态部分。
- 远程控制的安全风险:我曾经遇到过用户反馈,远程控制时鼠标乱跳。后来发现是坐标映射没处理好,观看者屏幕分辨率跟共享者不一样。一定要做归一化处理。
- Simulcast 的兼容性:不是所有浏览器都支持 Simulcast。Chrome 没问题,但 Safari 和 Firefox 支持有限。建议加个 fallback 方案,降级到单流。
总结一下:屏幕共享进阶,核心就是解决「怎么共享多个源」「怎么在共享上做标记」「怎么远程控制」「怎么保证流畅」。这四个功能做好了,你的视频会议产品就具备了企业级能力。
嗯,今天就聊到这里。代码示例都是可运行的,建议你动手试试。遇到问题,多看看浏览器控制台的日志,WebRTC 的调试信息其实挺丰富的。