20、屏幕共享进阶:屏幕流采集、区域选择、性能优化

屏幕共享,说白了就是把你的显示器画面实时传给对方。

很多人觉得这功能简单——不就是抓个屏嘛?

嗯,我刚开始也这么想。直到有一次给客户做远程协作,一共享屏幕,CPU直接飙到90%,风扇呼呼转,对方看到的画面还卡成PPT。那场面,尴尬得我脚趾抠地。

后来我花了整整两周,把屏幕采集的底层逻辑翻了个底朝天。今天就把这些经验掰开揉碎了讲给你。

20.1 屏幕流采集的核心机制

屏幕采集,本质上是一个持续抓取帧数据的过程。

浏览器提供了 getDisplayMedia() 这个API。但你别以为调一下就完事了。它背后涉及三个关键环节:

  • 帧源:操作系统提供的桌面图像缓冲区
  • 采集方式:是走GPU还是CPU?这决定了性能上限
  • 传输管道:采集到的帧怎么送到编码器

我习惯把屏幕采集比作「给显示器拍照」。但问题是,显示器每秒刷新60次,你不可能每帧都拍。所以我们要做帧率控制

核心原则:屏幕采集的帧率,不要超过编码器能处理的上限。否则内存堆积,延迟爆炸。

来看一段基础采集代码:

// 基础屏幕采集
async function startScreenCapture() {
  try {
    const stream = await navigator.mediaDevices.getDisplayMedia({
      video: {
        frameRate: { ideal: 30, max: 30 },
        width: { ideal: 1920, max: 1920 },
        height: { ideal: 1080, max: 1080 }
      },
      audio: false
    });
    
    // 把流交给RTCPeerConnection
    stream.getTracks().forEach(track => {
      pc.addTrack(track, stream);
    });
  } catch (err) {
    console.error('采集失败:', err);
  }
}

这里有个坑——frameRateidealmax 要设成一样。为什么?

我曾经遇到过,设了 ideal: 30, max: 60,结果浏览器自作聪明跑到60fps,编码器直接崩了。所以,明确告诉浏览器你要多少帧,别给它自由发挥的空间。

20.2 区域选择:只共享你想共享的部分

全屏共享?那是新手干的事。

真正做产品,你得让用户只共享某个窗口,甚至只共享屏幕的一小块区域

浏览器原生支持窗口级采集。你调用 getDisplayMedia() 时,系统会弹出一个选择器,让用户选「整个屏幕」还是「某个窗口」。

但如果你想实现「自定义区域选择」,那就得自己动手了。

我做过一个方案,思路是这样的:

  1. 先全屏采集,拿到完整的屏幕流
  2. 在本地用 Canvas 把画面画出来
  3. 让用户用鼠标拖拽选择区域
  4. 只把选中区域裁剪后传给编码器

代码实现大概长这样:

// 区域裁剪实现
function cropVideoTrack(stream, region) {
  const canvas = document.createElement('canvas');
  const ctx = canvas.getContext('2d');
  
  canvas.width = region.width;
  canvas.height = region.height;
  
  const video = document.createElement('video');
  video.srcObject = stream;
  video.play();
  
  // 每帧只绘制选中区域
  function drawFrame() {
    ctx.drawImage(
      video,
      region.x, region.y,      // 源区域起点
      region.width, region.height, // 源区域大小
      0, 0,                     // 目标区域起点
      region.width, region.height  // 目标区域大小
    );
    
    requestAnimationFrame(drawFrame);
  }
  
  drawFrame();
  
  // 从canvas生成新的MediaStream
  return canvas.captureStream(30);
}

注意:这种方案会增加一次额外的图像拷贝。如果用户机器性能差,建议降低采集帧率到15fps,或者提示用户选择更小的区域。

20.3 性能优化:别让屏幕共享吃掉整个CPU

屏幕共享是性能杀手。我见过太多应用,一开共享就卡死。

优化方向主要有三个:

优化方向 具体手段 效果
降低分辨率 采集1080p,编码时降为720p或540p CPU占用降低40%
动态帧率 画面静止时降到5fps,有变化时升到30fps 平均码率降低60%
跳过重复帧 检测画面是否变化,没变化就不送编码器 编码负载降低70%

我个人最推荐的是动态帧率方案。它聪明,不浪费资源。

实现思路是这样的:

// 动态帧率控制
class AdaptiveFrameController {
  constructor() {
    this.lastFrame = null;
    this.changeThreshold = 0.05; // 5%的像素变化才触发
    this.currentFps = 5;
    this.maxFps = 30;
    this.minFps = 5;
  }
  
  shouldSendFrame(currentFrame) {
    if (!this.lastFrame) {
      this.lastFrame = currentFrame;
      return true;
    }
    
    // 计算画面变化比例
    const diff = this.calculateDifference(currentFrame, this.lastFrame);
    
    if (diff > this.changeThreshold) {
      // 画面变化大,提高帧率
      this.currentFps = Math.min(this.maxFps, this.currentFps + 5);
      this.lastFrame = currentFrame;
      return true;
    } else {
      // 画面静止,降低帧率
      this.currentFps = Math.max(this.minFps, this.currentFps - 2);
      return false;
    }
  }
  
  calculateDifference(frame1, frame2) {
    // 这里用简单的像素对比算法
    // 实际项目中可以用更高效的SSIM或MSE
    let diffPixels = 0;
    const totalPixels = frame1.width * frame1.height;
    
    // 采样对比,不用逐像素
    const step = 10; // 每10个像素采样一次
    for (let y = 0; y < frame1.height; y += step) {
      for (let x = 0; x < frame1.width; x += step) {
        const p1 = frame1.getPixel(x, y);
        const p2 = frame2.getPixel(x, y);
        if (Math.abs(p1.r - p2.r) > 30 ||
            Math.abs(p1.g - p2.g) > 30 ||
            Math.abs(p1.b - p2.b) > 30) {
          diffPixels++;
        }
      }
    }
    
    return diffPixels / (totalPixels / (step * step));
  }
}

经验之谈:画面变化检测不要逐像素对比,太慢了。采样对比就够用。我一般每10个像素取一个点,准确率能到95%以上,速度提升100倍。

20.4 屏幕共享的SVG架构图

下面这张图,把屏幕共享的完整链路画出来了。你一看就明白数据是怎么流动的。

屏幕共享数据流架构图 屏幕采集 getDisplayMedia() 区域裁剪 Canvas裁剪/窗口选择 动态帧率 5~30fps自适应 视频编码 VP8/H264编码 RTP传输 WebRTC发送 性能监控 CPU/内存/码率 getStats() 实时反馈 虚线表示性能监控反馈链路,用于动态调整采集参数

20.5 避坑指南

做屏幕共享,有几个坑我踩过,你千万别再踩:

  • 不要在高分屏上直接采集原始分辨率——Retina屏的4K画面,采集下来编码器直接炸。我一般降采样到1080p再编码。
  • 注意权限问题——有些浏览器在共享时不允许隐藏系统弹窗。你想想看,用户正在演示,突然弹出个权限请求,多尴尬。
  • 音频别乱采——getDisplayMedia 可以采系统音频,但不同浏览器实现不一样。Chrome支持,Firefox就不一定。我建议统一不采音频,单独用麦克风。
  • 内存泄漏要小心——每次重新采集都会创建新的MediaStream,旧的track要手动stop。我曾经忘了释放,结果内存涨到2GB,用户直接骂娘。

一句话总结:屏幕共享的核心不是「能不能采」,而是「采了之后能不能流畅地编、流畅地传」。性能优化才是真正的技术活。

好了,这一章的内容就到这里。代码你拿去跑跑看,有问题随时调试。屏幕共享这东西,纸上谈兵没用,得动手。


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