3、获取屏幕流:调用getDisplayMedia、处理用户授权、错误处理与降级方案

屏幕共享,说白了就是把你的显示器画面“直播”给远端用户。这个功能在远程会议、在线教育、游戏直播里太常见了。但真正动手做的时候,你会发现坑比想象中多。

我个人习惯把屏幕共享分成三步走:获取流 → 处理授权 → 兜底降级。今天我们就来聊聊第一步——怎么拿到屏幕流,以及拿不到的时候怎么办。

3.1 核心API:getDisplayMedia

WebRTC 里获取屏幕流,用的是 navigator.mediaDevices.getDisplayMedia()。这玩意儿跟 getUserMedia 有点像,但区别很大——它抓的是屏幕,不是摄像头。

核心区别:

  • getUserMedia 需要用户授权摄像头/麦克风
  • getDisplayMedia 需要用户选择共享哪个窗口/屏幕
  • getDisplayMedia 返回的流自带 cursor 控制选项

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

async function startScreenShare() {
  try {
    const stream = await navigator.mediaDevices.getDisplayMedia({
      video: {
        cursor: "always"  // 显示鼠标指针
      },
      audio: false        // 不采集系统声音
    });
    return stream;
  } catch (err) {
    console.error("屏幕共享失败:", err);
    throw err;
  }
}

嗯,这里要注意:getDisplayMedia 必须在用户手势(比如点击按钮)中触发。你想想看,如果页面一加载就弹窗要共享屏幕,那用户体验得多糟糕?浏览器也不允许这么干。

3.2 处理用户授权:用户点了“取消”怎么办?

用户点击“取消”或者关闭选择窗口时,getDisplayMedia 会抛出一个 NotAllowedError。我在项目中遇到过好几次,用户手滑点错了,或者突然反悔不想共享了。这时候不能直接崩溃,得优雅处理。

我的处理策略:

  • 捕获 NotAllowedError,提示用户“共享已取消”
  • 保留之前的流(如果有的话)
  • 给用户一个重新尝试的按钮
async function handleScreenShare() {
  try {
    const stream = await navigator.mediaDevices.getDisplayMedia({
      video: { cursor: "always" }
    });
    // 成功:绑定到 video 元素
    videoElement.srcObject = stream;
  } catch (err) {
    if (err.name === "NotAllowedError") {
      // 用户取消了共享
      showToast("屏幕共享已取消,您可以稍后重试");
    } else {
      // 其他错误
      handleOtherErrors(err);
    }
  }
}

3.3 错误处理:常见的坑与解决方案

除了用户取消,还有几种常见错误。我整理了一张表,方便你对照排查:

错误名称 触发场景 解决方案
NotAllowedError 用户取消选择 / 权限被拒绝 提示用户重试,或提供降级方案
NotFoundError 没有可共享的屏幕(比如无显示器) 检查环境,提示“未检测到屏幕”
NotReadableError 硬件或系统级错误 建议用户重启浏览器或系统
AbortError 共享过程中被中断(比如用户关闭标签页) 监听 ended 事件,做清理
OverconstrainedError 约束条件无法满足(比如要求 4K 但屏幕不支持) 放宽约束,使用默认值

我曾经踩过的坑: 有一次用户反馈共享画面是黑的。排查了半天,发现是 cursor 设置成了 "never",但用户以为鼠标指针会显示。后来我改成 "always",问题解决。所以,cursor 选项最好根据场景动态设置。

3.4 降级方案:当 getDisplayMedia 不可用时

不是所有浏览器都支持 getDisplayMedia。比如老版本的 Safari、某些移动端浏览器。这时候就得准备降级方案。

我个人习惯用 特征检测 来判断:

function isScreenShareSupported() {
  return !!(
    navigator.mediaDevices &&
    navigator.mediaDevices.getDisplayMedia
  );
}

如果不支持,我有三个降级思路:

  1. 提示用户升级浏览器 —— 最简单,但体验最差
  2. 使用 getUserMedia 共享摄像头画面 —— 勉强能看,但只能拍到屏幕的一部分
  3. 使用第三方插件或扩展 —— 比如 Chrome 扩展,但维护成本高

我的建议: 降级方案不要藏着掖着。直接告诉用户“当前浏览器不支持屏幕共享,请使用 Chrome/Edge 最新版”。与其给一个半残的功能,不如让用户知道问题在哪。

3.5 知识体系总览

下面这张图,把整个获取屏幕流的流程串起来了。从调用 API 到错误处理,再到降级兜底,一目了然:

获取屏幕流核心流程 调用 getDisplayMedia 用户选择共享窗口/屏幕 (浏览器弹窗) ✅ 成功获取流 绑定到 video 元素 ❌ 错误/取消 捕获异常 降级方案 / 提示用户 监听 ended 事件 图例 成功路径 失败路径 API 调用

3.6 实战中的几个小细节

最后分享几个我实际开发中总结的点:

  • 监听 ended 事件: 用户随时可能停止共享。记得监听 stream.getVideoTracks()[0].onended,做清理工作。
  • 不要忘记释放资源: 共享结束后,调用 track.stop() 释放摄像头/屏幕资源。否则下次调用可能失败。
  • 音频采集: getDisplayMedia 的 audio 选项目前支持度有限。Chrome 可以采集系统音频,但 Safari 不行。别指望它。
  • 分辨率控制: 如果用户屏幕是 4K,采集的流会很大。建议设置 video: { width: { ideal: 1920 }, height: { ideal: 1080 } } 来限制。

一个小技巧: 如果你想让用户共享某个特定窗口(比如应用窗口),可以在 getDisplayMedia 的 options 里加 video: { displaySurface: "window" }。这样浏览器会优先推荐窗口共享。不过这只是建议,最终选择权在用户手里。

好了,获取屏幕流这部分就聊到这儿。核心就是:调用 API → 处理授权 → 兜底降级。代码不多,但细节不少。你想想看,用户点取消的时候你弹个报错框,那体验多糟糕?所以,把错误处理做扎实,比炫技更重要。


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