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);
}
}
这里有个坑——frameRate 的 ideal 和 max 要设成一样。为什么?
我曾经遇到过,设了 ideal: 30, max: 60,结果浏览器自作聪明跑到60fps,编码器直接崩了。所以,明确告诉浏览器你要多少帧,别给它自由发挥的空间。
20.2 区域选择:只共享你想共享的部分
全屏共享?那是新手干的事。
真正做产品,你得让用户只共享某个窗口,甚至只共享屏幕的一小块区域。
浏览器原生支持窗口级采集。你调用 getDisplayMedia() 时,系统会弹出一个选择器,让用户选「整个屏幕」还是「某个窗口」。
但如果你想实现「自定义区域选择」,那就得自己动手了。
我做过一个方案,思路是这样的:
- 先全屏采集,拿到完整的屏幕流
- 在本地用 Canvas 把画面画出来
- 让用户用鼠标拖拽选择区域
- 只把选中区域裁剪后传给编码器
代码实现大概长这样:
// 区域裁剪实现
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架构图
下面这张图,把屏幕共享的完整链路画出来了。你一看就明白数据是怎么流动的。
20.5 避坑指南
做屏幕共享,有几个坑我踩过,你千万别再踩:
- 不要在高分屏上直接采集原始分辨率——Retina屏的4K画面,采集下来编码器直接炸。我一般降采样到1080p再编码。
- 注意权限问题——有些浏览器在共享时不允许隐藏系统弹窗。你想想看,用户正在演示,突然弹出个权限请求,多尴尬。
- 音频别乱采——
getDisplayMedia可以采系统音频,但不同浏览器实现不一样。Chrome支持,Firefox就不一定。我建议统一不采音频,单独用麦克风。 - 内存泄漏要小心——每次重新采集都会创建新的MediaStream,旧的track要手动stop。我曾经忘了释放,结果内存涨到2GB,用户直接骂娘。
一句话总结:屏幕共享的核心不是「能不能采」,而是「采了之后能不能流畅地编、流畅地传」。性能优化才是真正的技术活。
好了,这一章的内容就到这里。代码你拿去跑跑看,有问题随时调试。屏幕共享这东西,纸上谈兵没用,得动手。