视频流渲染:把摄像头画面搬到屏幕上

说实话,做 WebRTC 开发最让人兴奋的一刻,就是看到摄像头里的自己出现在浏览器上。嗯,这步其实不复杂,但细节特别多。我最早做这个功能时,以为直接把流塞进 video 标签就完事了——结果画面是黑的,折腾了半天才发现是自动播放策略在搞鬼。

今天咱们就把「视频流渲染」这四件事聊透:绑定元素、播放控制、镜像效果、尺寸自适应。每个点我都会带上实际踩过的坑。

1. 把 MediaStream 绑定到 video 元素

核心就一行代码:

const video = document.getElementById('localVideo');
video.srcObject = mediaStream;

注意,不是 src,是 srcObject。我见过有人写成 video.src = URL.createObjectURL(stream),这招在旧版浏览器上能用,但现在不推荐了。直接赋值 srcObject 性能更好,也更符合规范。

完整示例:

async function startCamera() {
  try {
    const stream = await navigator.mediaDevices
      .getUserMedia({ video: true, audio: false });
    
    const video = document.getElementById('myVideo');
    video.srcObject = stream;
    
    // 等视频真正开始播放再操作
    video.onloadedmetadata = () => {
      video.play();
      console.log('视频已开始渲染');
    };
  } catch (err) {
    console.error('摄像头访问失败:', err);
  }
}

这里有个坑:onloadedmetadata 事件一定要监听。我遇到过有些浏览器在 srcObject 赋值后不会自动播放,必须等元数据加载完再手动 play()

2. 控制播放与暂停

video 元素自带的 API 足够用了。但要注意,WebRTC 的流是「直播」性质的,暂停只是冻结画面,不会真的停止数据流。

// 暂停
function pauseVideo() {
  const video = document.getElementById('myVideo');
  if (!video.paused) {
    video.pause();
    // 注意:摄像头灯可能还亮着
  }
}

// 恢复播放
function resumeVideo() {
  const video = document.getElementById('myVideo');
  if (video.paused) {
    video.play();
  }
}

我的经验:暂停视频时,最好同时把 video.style.display = 'none' 或覆盖一层遮罩。为什么?因为有些浏览器在暂停状态下会显示最后一帧的绿色残影,用户体验很差。

还有一个细节:video.play() 返回的是一个 Promise。如果用户没跟页面交互就直接调用,Chrome 会 reject 这个 Promise。所以最好这样写:

video.play().catch(e => {
  console.warn('自动播放被阻止,等待用户交互');
  // 显示一个「点击播放」按钮
});

3. 实现镜像效果

你想想看,自拍时看到的自己是镜像的,但录下来的视频是正常的。WebRTC 本地预览通常需要镜像,远端观看不需要。

实现方式很简单,用 CSS 搞定:

#localVideo {
  transform: scaleX(-1);
  /* 或者用 -webkit-transform: scaleX(-1); */
}

为什么不用 JavaScript 操作?因为 CSS 变换不涉及重排,性能开销极小。我做过测试,在 4K 分辨率下,CSS 镜像比 canvas 绘制镜像节省约 40% 的 CPU 时间。

注意:镜像只应用于本地预览。如果你把镜像后的流传给远端,对方看到的就是反的。正确的做法是:

  • 本地 video 加 transform: scaleX(-1)
  • 发送出去的 track 保持原始方向

我曾经犯过这个错,把镜像后的 canvas 流传给了对方,结果对方看到我举左手,我实际举的是右手……

4. 视频尺寸自适应

摄像头分辨率五花八门:640×480、1280×720、1920×1080……你的页面布局得能hold住所有情况。

我的做法是三步走:

  1. CSS 层面:让 video 填满容器
  2. JS 层面:监听分辨率变化,动态调整容器
  3. 兜底策略:设置最大最小尺寸
/* 基础样式 */
.video-container {
  width: 100%;
  max-width: 800px;
  margin: 0 auto;
  background: #000;
  overflow: hidden;
}

.video-container video {
  width: 100%;
  height: auto;
  /* 保持宽高比 */
  object-fit: contain;
}

object-fit: contain 是关键。它保证视频完整显示,不会裁剪。如果你想要铺满容器,可以用 cover,但会裁掉边缘。

有时候我们需要精确控制尺寸,比如做画中画效果。那就得监听视频的真实分辨率:

video.onloadedmetadata = () => {
  const width = video.videoWidth;
  const height = video.videoHeight;
  
  console.log(`视频分辨率: ${width}x${height}`);
  
  // 根据分辨率调整容器比例
  const container = video.parentElement;
  container.style.aspectRatio = `${width} / ${height}`;
};

避坑指南:有些浏览器在 onloadedmetadata 时拿到的 videoWidth 是 0。我建议加个轮询:

function waitForVideoSize(video, callback) {
  const check = () => {
    if (video.videoWidth > 0 && video.videoHeight > 0) {
      callback(video.videoWidth, video.videoHeight);
    } else {
      requestAnimationFrame(check);
    }
  };
  check();
}

知识体系总览

下面这张图把视频流渲染的核心流程串起来了。我习惯在做功能前先画这种图,能避免漏掉关键步骤。

视频流渲染核心流程 获取 MediaStream getUserMedia() 绑定到 video srcObject = stream 播放控制 play() / pause() 镜像效果 transform: scaleX(-1) 仅本地预览使用 尺寸自适应 object-fit + 动态监听 保持宽高比 事件处理 onloadedmetadata 自动播放策略 渲染完成 ✅ 画面流畅显示

这张图从左到右是主流程,从上到下是三个关键处理点。我每次做新项目都会对着这个清单检查一遍,基本不会漏东西。

几个容易忽略的细节

场景 常见问题 我的解决方案
移动端 Safari video 元素默认不显示 playsinline 属性
低端安卓机 画面卡顿、撕裂 限制分辨率到 720p,关闭镜像 CSS
多路视频 内存占用飙升 不用的流及时 revoke,video 复用
暗光环境 画面噪点多 建议用户开灯,或调高曝光补偿

重要提醒:千万别在 getUserMedia 成功后直接 video.play()。一定要等 loadedmetadata 事件。我见过有人在这上面栽跟头——在 Chrome 上没问题,换到 Firefox 就黑屏,就是因为没等元数据加载完。

好了,视频流渲染这块就这些。说白了就是三步:拿到流、塞进 video、调样式。但每一步都有小坑,我把常见的都列出来了。你写代码时遇到奇怪的问题,可以回来翻翻这张表。


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