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 整体架构图

下面这张图展示了屏幕共享 + 数据通道 + 远程控制的完整数据流。我习惯把流程画清楚再写代码,避免后期返工。

被控端(你的电脑) getDisplayMedia() 捕获屏幕流 addTrack() 添加到 PeerConnection createDataChannel() 创建数据通道 事件监听(鼠标/键盘) 序列化后通过数据通道发送 WebRTC 媒体通道 (屏幕流) 数据通道 (控制指令) 控制端(远程电脑) ontrack 接收屏幕流 渲染到 video 标签 onmessage 接收指令 解析 JSON 消息 坐标映射 百分比 → 实际像素 模拟执行 robot.moveMouse / robot.keyTap 媒体流 数据通道

19.6 避坑指南

做屏幕共享 + 数据通道结合时,有几个坑我替你们踩过了:

  • 屏幕流停止后数据通道还在:用户点了「停止共享」,屏幕流会触发 oninactive 事件。但数据通道不会自动关闭。我建议在 oninactive 里手动关闭数据通道,避免资源泄漏。
  • 多显示器场景getDisplayMedia 默认捕获整个屏幕,但用户可能选某个显示器。这时候 displaySurface 的值会变。我习惯在捕获后检查 track.getSettings().displaySurface,确认用户选的是什么。
  • 性能问题:屏幕共享的帧率一般设 15fps 就够了,别贪高。帧率越高,编码压力越大,反而容易卡顿。我一般用 frameRate: { ideal: 15, max: 20 }
📌 总结一下:屏幕共享与数据通道结合,核心就三步——捕获屏幕流、通过媒体通道传输画面、通过数据通道传输控制指令。远程控制的基础就是「事件序列化 + 坐标映射 + 模拟执行」。别想得太复杂,先把画面传起来,再慢慢加控制逻辑。

公众号:蓝海资料掘金营,微信deep3321