屏幕共享原理:getDisplayMedia API、屏幕流与摄像头流的区别、MediaStream对象

屏幕共享,说白了就是把你的电脑屏幕「直播」出去。很多同学一开始觉得这玩意儿很神秘,其实底层原理并不复杂。我最早接触屏幕共享是在做一个远程协作工具的时候,当时被各种流的概念搞得晕头转向。今天咱们就把这块彻底掰开揉碎。

一、getDisplayMedia API:屏幕共享的入口

屏幕共享的核心API就是 getDisplayMedia。它和 getUserMedia 是兄弟俩,一个管屏幕,一个管摄像头。

核心区别:getUserMedia 需要用户授权摄像头/麦克风,getDisplayMedia 需要用户选择共享哪个窗口或整个屏幕。

来看一个最基础的调用示例:

async function startScreenShare() {
  try {
    const stream = await navigator.mediaDevices.getDisplayMedia({
      video: {
        cursor: "always" // 显示鼠标指针
      },
      audio: false // 默认不采集系统音频
    });
    // 拿到 stream 后,可以赋值给 video 元素
    const video = document.getElementById("preview");
    video.srcObject = stream;
  } catch (err) {
    console.error("用户取消了共享", err);
  }
}

嗯,这里要注意一点:getDisplayMedia 返回的是一个 Promise,用户如果点了「取消」,就会 reject。我在项目中遇到过用户手滑点取消的情况,所以一定要加 try-catch。

二、屏幕流 vs 摄像头流:到底差在哪?

你想想看,屏幕流和摄像头流虽然都是 MediaStream,但本质上天差地别。我整理了一个对比表,一目了然:

对比维度 屏幕流 (getDisplayMedia) 摄像头流 (getUserMedia)
视频源 显示器/窗口/浏览器标签 物理摄像头
帧率 通常 30fps,可配置 通常 30fps,受硬件限制
分辨率 取决于屏幕分辨率(可能 4K) 取决于摄像头(通常 720p/1080p)
音频 默认不包含系统音频 默认包含麦克风音频
用户交互 弹出选择窗口的对话框 弹出摄像头权限请求
隐私风险 高(可能泄露敏感信息) 中(只暴露画面)

我个人习惯在屏幕共享时,把摄像头流和屏幕流分开处理。为什么呢?因为屏幕流的分辨率通常很高,直接推到远端带宽扛不住。我曾经在一个项目中,用户用 4K 屏幕共享,结果带宽直接打满,画面卡成 PPT。后来我加了一层降分辨率处理,才解决问题。

避坑指南:屏幕流默认不带系统音频!如果你想采集系统声音(比如播放视频的声音),需要额外处理。我曾经踩过这个坑,用户共享了视频播放窗口,结果对方听不到声音,排查了半天才发现是音频没开。

三、MediaStream 对象:流的本质

不管是屏幕流还是摄像头流,最终都封装成 MediaStream 对象。这个对象到底长什么样?咱们来解剖一下。

一个 MediaStream 包含多个 MediaStreamTrack。每个 track 代表一种媒体类型:

  • 视频 track:包含画面数据
  • 音频 track:包含声音数据

你可以这样查看流里的内容:

const stream = await navigator.mediaDevices.getDisplayMedia({ video: true });

// 查看所有 track
console.log(stream.getTracks());
// 输出:[MediaStreamTrack]

// 单独获取视频 track
const videoTrack = stream.getVideoTracks()[0];
console.log(videoTrack.label); // 比如 "屏幕 1" 或 "Chrome 标签页"

// 单独获取音频 track(如果有的话)
const audioTrack = stream.getAudioTracks()[0];
console.log(audioTrack?.label); // 可能 undefined

这里有个小技巧:你可以用 clone() 方法复制一个 track,但要注意——克隆出来的 track 和原 track 共享同一个媒体源。说白了就是,你关掉其中一个,另一个也会受影响。

个人经验:我在做多端同步时,经常需要把屏幕流拆成多个 track 分别处理。比如视频 track 用来推流,音频 track 用来做本地录制。用 getVideoTracks()getAudioTracks() 分开拿,比直接操作整个 stream 灵活得多。

四、知识体系结构图

下面这张图帮你理清本章的核心逻辑:

屏幕共享核心知识体系 getDisplayMedia API MediaStream 对象 屏幕流 (getDisplayMedia) 摄像头流 (getUserMedia) 高分辨率 默认无音频 低分辨率 默认含音频

五、实际开发中的注意事项

最后分享几个我在实战中总结的经验:

  1. 屏幕流一定要降分辨率:4K 屏幕直接推流,带宽至少需要 20Mbps。我一般降到 1080p 或 720p,用户体验好很多。
  2. 别忘了处理用户取消getDisplayMedia 被取消会抛异常,一定要 catch 住,别让页面崩溃。
  3. 停止共享时要释放资源:调用 track.stop() 来关闭屏幕流,否则摄像头灯可能一直亮着(如果是摄像头流)。
  4. 屏幕流和摄像头流可以混用:比如把摄像头画面画中画叠加到屏幕流上,这个后面章节会细讲。

一句话总结:getDisplayMedia 是屏幕共享的入口,返回的 MediaStream 包含视频 track(可能还有音频 track),和摄像头流最大的区别在于分辨率高、默认无音频、需要用户选择共享区域。

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