17、屏幕共享实现:屏幕共享流程、动态切换共享源、共享与摄像头切换、共享性能优化

屏幕共享,说白了就是把你的电脑屏幕内容实时传给对方。这个功能在远程会议、在线教育、游戏直播里太常见了。我刚开始做WebRTC时,以为屏幕共享就是换个视频源那么简单,结果踩了不少坑。今天咱们就把这块彻底讲透。

屏幕共享的核心流程

屏幕共享的底层逻辑其实不复杂。你想想看,摄像头是采集物理世界的光信号,屏幕共享是采集操作系统里的画面帧。WebRTC把这两者都当作MediaStream来处理。

关键API就一个:getDisplayMedia()。调用它,浏览器会弹出选择窗口,让用户选共享哪个窗口、标签页还是整个屏幕。

async function startScreenShare() {
  try {
    const stream = await navigator.mediaDevices.getDisplayMedia({
      video: {
        cursor: "always", // 显示鼠标指针
        displaySurface: "monitor" // 默认共享整个屏幕
      },
      audio: false // 屏幕共享一般不传系统声音
    });
    
    // 把流绑定到本地预览video
    const localVideo = document.getElementById('localVideo');
    localVideo.srcObject = stream;
    
    // 替换掉原来的视频轨道
    const pc = getPeerConnection();
    const [videoTrack] = stream.getVideoTracks();
    const sender = pc.getSenders().find(s => s.track.kind === 'video');
    await sender.replaceTrack(videoTrack);
    
    // 监听停止事件
    videoTrack.onended = () => {
      console.log('用户停止了屏幕共享');
      // 切回摄像头
    };
    
  } catch (err) {
    console.error('屏幕共享失败:', err);
  }
}

关键点getDisplayMedia()返回的流,视频轨道是MediaStreamTrack。用replaceTrack()替换掉原来的摄像头轨道,就能无缝切换。

我在项目中遇到过一个问题:用户点了共享后,浏览器弹窗被广告拦截器屏蔽了。嗯,这个没法完全避免,但可以加个提示,让用户手动允许弹窗。

动态切换共享源

用户共享了屏幕A,突然想换成屏幕B,怎么办?最直接的做法是重新调用getDisplayMedia()。但这样会再次弹出选择窗口,体验不太好。

我个人习惯的做法是:保留一个MediaStream引用,当用户想切换时,停止旧轨道,创建新轨道,再replaceTrack

let screenStream = null;

async function switchScreenSource() {
  // 停止旧轨道
  if (screenStream) {
    screenStream.getTracks().forEach(track => track.stop());
  }
  
  // 重新获取新源
  screenStream = await navigator.mediaDevices.getDisplayMedia({
    video: { cursor: "always" }
  });
  
  const newVideoTrack = screenStream.getVideoTracks()[0];
  const pc = getPeerConnection();
  const sender = pc.getSenders().find(s => s.track.kind === 'video');
  await sender.replaceTrack(newVideoTrack);
  
  // 更新本地预览
  document.getElementById('localVideo').srcObject = screenStream;
}

注意:切换源时,旧轨道必须手动stop(),否则摄像头灯会一直亮着。我曾经因为这个被同事吐槽过——共享结束了,摄像头灯还亮着,很尴尬。

共享与摄像头切换

实际场景中,用户经常需要在「共享屏幕」和「显示自己」之间来回切换。比如开会时先共享PPT,然后切回摄像头回答问题。

实现思路很简单:维护两个MediaStream,一个来自摄像头,一个来自屏幕。切换时用replaceTrack()替换发送轨道。

let cameraStream = null;
let screenStream = null;
let isSharingScreen = false;

async function toggleShare() {
  const pc = getPeerConnection();
  const sender = pc.getSenders().find(s => s.track.kind === 'video');
  
  if (isSharingScreen) {
    // 切回摄像头
    if (!cameraStream) {
      cameraStream = await navigator.mediaDevices.getUserMedia({ video: true });
    }
    await sender.replaceTrack(cameraStream.getVideoTracks()[0]);
    document.getElementById('localVideo').srcObject = cameraStream;
    isSharingScreen = false;
  } else {
    // 切到屏幕共享
    screenStream = await navigator.mediaDevices.getDisplayMedia({ video: true });
    await sender.replaceTrack(screenStream.getVideoTracks()[0]);
    document.getElementById('localVideo').srcObject = screenStream;
    isSharingScreen = true;
  }
}

小技巧:切换时最好保留摄像头流的引用,避免每次都要重新获取权限。用户已经授权过一次,没必要再弹窗。

这里有个坑:如果用户先共享屏幕,然后切回摄像头,再切回屏幕,第二次getDisplayMedia()可能不会弹窗——浏览器会复用之前的授权。但不同浏览器行为不一样,Chrome会弹,Firefox可能直接复用。嗯,这个得做好兼容处理。

共享性能优化

屏幕共享比摄像头传输更耗性能。为什么呢?因为屏幕分辨率通常很高(1920x1080甚至4K),而且画面变化频繁(鼠标移动、窗口切换)。

我总结了几条优化经验:

  • 降低分辨率:不是所有场景都需要全高清。共享PPT时,720p足够了。
  • 控制帧率:屏幕共享不需要30fps。15fps已经非常流畅,静态内容5fps都行。
  • 动态码率调整:根据网络状况自动降低码率,避免卡顿。
// 设置屏幕共享的编码参数
const constraints = {
  video: {
    width: { ideal: 1280 },
    height: { ideal: 720 },
    frameRate: { ideal: 15, max: 30 }
  }
};

const stream = await navigator.mediaDevices.getDisplayMedia(constraints);

// 在RTCPeerConnection中设置编码参数
const transceiver = pc.getTransceivers().find(t => t.sender.track.kind === 'video');
if (transceiver) {
  const params = transceiver.sender.getParameters();
  if (!params.encodings) {
    params.encodings = [{}];
  }
  params.encodings[0].maxBitrate = 500000; // 500kbps
  params.encodings[0].maxFramerate = 15;
  await transceiver.sender.setParameters(params);
}

核心思路:屏幕共享的优化,本质是在「清晰度」和「流畅度」之间找平衡。静态内容(PPT、文档)可以降低帧率提高分辨率;动态内容(视频、游戏)需要提高帧率但可以降低分辨率。

我曾经遇到一个极端案例:用户共享4K屏幕,网络带宽只有2Mbps。结果画面卡成PPT,对方根本看不清。后来我加了自适应码率逻辑:检测到带宽不足时,自动降到720p@10fps,问题就解决了。

屏幕共享的完整流程图

下面这张图展示了屏幕共享从开始到结束的完整流程,以及切换源、切换摄像头的决策路径。

屏幕共享完整流程 开始屏幕共享 调用 getDisplayMedia() 用户选择共享源(窗口/标签/屏幕) 获取视频轨道 MediaStreamTrack replaceTrack() 替换发送轨道 共享进行中 用户点击「切换源」 停止旧轨道 重新 getDisplayMedia() replaceTrack() 新轨道 用户点击「切回摄像头」 获取摄像头流(缓存) replaceTrack() 摄像头轨道 停止屏幕共享轨道

性能优化的几个实战技巧

除了编码参数,还有几个地方值得注意:

  • 硬件加速:WebRTC默认使用硬件编码器。如果CPU占用高,可以尝试关闭硬件加速,用软件编码。虽然更耗CPU,但兼容性更好。
  • 降噪处理:屏幕共享时,鼠标闪烁、窗口抖动都会增加码率。可以加一个简单的帧差检测,只有画面变化超过阈值才发送新帧。
  • 分辨率自适应:根据接收端的窗口大小动态调整分辨率。对方用手机看,你传4K画面纯属浪费带宽。
// 帧差检测示例(简化版)
let lastFrameData = null;

function shouldSendFrame(currentFrame) {
  if (!lastFrameData) {
    lastFrameData = currentFrame;
    return true;
  }
  
  // 计算像素差异
  let diff = 0;
  for (let i = 0; i < currentFrame.length; i += 4) {
    diff += Math.abs(currentFrame[i] - lastFrameData[i]);
  }
  
  const threshold = 10000; // 根据实际情况调整
  lastFrameData = currentFrame;
  return diff > threshold;
}

经验之谈:帧差检测不要做得太复杂。简单的像素差值就够用。我见过有人用SSIM(结构相似性)算法,效果是好,但计算开销太大,反而拖慢了整体性能。

最后说一句:屏幕共享的调试比摄像头麻烦。因为getDisplayMedia()在非安全上下文(HTTP)下不能用,而且不同浏览器的实现细节有差异。我建议你在开发时,先用Chrome调试,再测Firefox和Edge。Safari对屏幕共享的支持比较弱,需要额外处理。

好了,屏幕共享这块就聊到这儿。核心就是记住:getDisplayMedia获取源,replaceTrack切换轨道,编码参数控制性能。把这三点吃透,屏幕共享功能就稳了。


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