19、屏幕共享与数据结合:使用getDisplayMedia捕获屏幕、将屏幕流与数据通道结合、远程控制基础
屏幕共享,说白了就是把你的电脑屏幕「直播」给对面看。这在远程会议、在线教育、远程协助里太常见了。但咱们这章要聊的,不只是「看」,而是「看的同时还能传数据」——甚至实现远程控制。
我个人习惯把屏幕共享分成三层:捕获层、传输层、控制层。捕获层用 getDisplayMedia 拿屏幕流,传输层靠 WebRTC 的媒体通道,控制层则依赖数据通道。嗯,今天咱们重点讲前两层,控制层给个基础思路。
19.1 用 getDisplayMedia 捕获屏幕
这个 API 其实很简单,一行代码就能唤起系统的屏幕选择弹窗。但坑也不少,我踩过。
async function startScreenShare() {
try {
const stream = await navigator.mediaDevices.getDisplayMedia({
video: {
cursor: "always", // 显示鼠标
displaySurface: "monitor", // 默认整个屏幕
},
audio: false, // 屏幕共享一般不传系统声音
});
return stream;
} catch (err) {
console.error("用户取消了屏幕共享", err);
}
}
这里有几个细节要注意:
- cursor 参数:我建议设成 "always",否则远程那边看不到鼠标,体验很差。
- displaySurface:可以指定 "monitor"(整个屏幕)、"window"(某个窗口)、"browser"(当前标签页)。但注意,这个参数不是强制的,用户仍然可以自己选。
- audio:别开。系统音频的采集在不同浏览器上表现差异巨大,而且容易产生回声。
⚠️ 重要提醒:getDisplayMedia 必须在用户手势(比如点击按钮)中调用,否则会被浏览器拦截。我在项目中遇到过用户点了按钮但没反应,就是因为异步调用链断了。
19.2 把屏幕流塞进 WebRTC
拿到屏幕流之后,下一步就是把它加到 PeerConnection 里。跟摄像头流一样处理就行。
const pc = new RTCPeerConnection({
iceServers: [{ urls: "stun:stun.l.google.com:19302" }],
});
// 添加屏幕流
screenStream.getVideoTracks().forEach((track) => {
pc.addTrack(track, screenStream);
});
// 别忘了,数据通道也要在这时候创建
const dataChannel = pc.createDataChannel("remote-control");
你想想看,这时候远程端收到的就是你的屏幕画面了。但光有画面还不够,咱们还得能传控制指令。
19.3 屏幕流与数据通道结合
数据通道在这里扮演的角色,就是「控制信令的快递员」。屏幕流负责画面,数据通道负责指令,两者并行不悖。
我一般这样设计消息格式:
// 发送端(被控端)
canvas.addEventListener("mousemove", (e) => {
const msg = {
type: "mouse-move",
x: e.clientX,
y: e.clientY,
};
dataChannel.send(JSON.stringify(msg));
});
// 接收端(控制端)
dataChannel.onmessage = (event) => {
const msg = JSON.parse(event.data);
if (msg.type === "mouse-move") {
// 模拟鼠标移动
robot.moveMouse(msg.x, msg.y);
}
};
💡 小技巧:数据通道的消息要尽量精简。我习惯用短字段名,比如 "x" 而不是 "positionX",能省几个字节是几个。屏幕共享场景下,控制指令的实时性要求很高。
19.4 远程控制的基础思路
远程控制,说白了就是「把本地的鼠标键盘事件,通过数据通道发给远端,远端再模拟执行」。听起来简单,但实现起来有几个坎:
- 坐标映射:你的屏幕是 1920x1080,对方可能是 2560x1440。直接传原始坐标肯定不对。我一般传百分比:
x / screenWidth,远端再乘以自己的屏幕宽度。 - 权限问题:浏览器里没法直接模拟鼠标键盘。需要借助 Native 插件或者 Electron 的
robotjs之类的库。纯 Web 方案只能做到「展示 + 指令传输」,真正的控制还得靠客户端。 - 延迟控制:数据通道虽然快,但屏幕流有编码延迟。我建议把控制指令走单独的数据通道,别跟媒体流抢带宽。
📌 核心要点:远程控制的本质是「事件序列化 + 可靠传输 + 精确回放」。数据通道保证了可靠传输,剩下的就是事件格式设计和坐标映射算法。
19.5 整体架构图
下面这张图展示了屏幕共享 + 数据通道 + 远程控制的完整数据流。我习惯把流程画清楚再写代码,避免后期返工。
19.6 避坑指南
做屏幕共享 + 数据通道结合时,有几个坑我替你们踩过了:
- 屏幕流停止后数据通道还在:用户点了「停止共享」,屏幕流会触发
oninactive事件。但数据通道不会自动关闭。我建议在oninactive里手动关闭数据通道,避免资源泄漏。 - 多显示器场景:
getDisplayMedia默认捕获整个屏幕,但用户可能选某个显示器。这时候displaySurface的值会变。我习惯在捕获后检查track.getSettings().displaySurface,确认用户选的是什么。 - 性能问题:屏幕共享的帧率一般设 15fps 就够了,别贪高。帧率越高,编码压力越大,反而容易卡顿。我一般用
frameRate: { ideal: 15, max: 20 }。
📌 总结一下:屏幕共享与数据通道结合,核心就三步——捕获屏幕流、通过媒体通道传输画面、通过数据通道传输控制指令。远程控制的基础就是「事件序列化 + 坐标映射 + 模拟执行」。别想得太复杂,先把画面传起来,再慢慢加控制逻辑。
公众号:蓝海资料掘金营,微信deep3321