视频控制:暂停、切换、分辨率与滤镜

视频控制这块,说实话是WebRTC应用里最常被用户吐槽的地方。用户不会管你底层用了什么编解码器,他们只关心:能不能暂停?能不能换摄像头?画面清不清晰?能不能加个美颜?

我做过好几个视频会议项目,每次上线前,产品经理都会追着问这些功能。今天我就把这几块核心能力拆开来讲,每个点都带实战代码。

核心知识点一览:

  • 视频流的暂停与恢复(Track.enabled)
  • 摄像头切换(enumerateDevices + applyConstraints)
  • 分辨率动态调整(getSettings + setConstraints)
  • 视频特效与滤镜(Canvas + CSS filter)
视频控制核心模块 MediaStream 暂停 / 恢复 track.enabled 摄像头切换 enumerateDevices 分辨率调整 applyConstraints 滤镜 / 特效 Canvas / CSS filter

1. 视频暂停与恢复

很多人以为暂停视频就是关掉摄像头,其实不是。你想想看,频繁开关摄像头对设备不友好,而且重新获取流会有延迟。

我个人的习惯是用 MediaStreamTrack.enabled 这个属性。把它设为 false,视频轨道会继续存在,但会发送黑帧。接收端看到的就是黑屏,但连接没断。

// 获取本地视频轨道
const videoTrack = localStream.getVideoTracks()[0];

// 暂停视频
function pauseVideo() {
  videoTrack.enabled = false;
  console.log('视频已暂停,发送黑帧');
}

// 恢复视频
function resumeVideo() {
  videoTrack.enabled = true;
  console.log('视频已恢复');
}

小技巧:enabledstop() 好在哪里?我遇到过用户频繁开关摄像头导致浏览器崩溃的情况。用 enabled 切换,延迟几乎为零,而且不会触发重新协商。

2. 摄像头切换

做多摄像头切换时,我踩过一个坑:直接调用 getUserMedia 重新获取流,结果导致 PeerConnection 需要重新 addTrack,非常麻烦。

正确的做法是用 applyConstraints 来切换。先枚举所有设备,然后指定新的设备ID。

// 枚举所有视频输入设备
async function getCameras() {
  const devices = await navigator.mediaDevices.enumerateDevices();
  return devices.filter(d => d.kind === 'videoinput');
}

// 切换到指定摄像头
async function switchCamera(deviceId) {
  const videoTrack = localStream.getVideoTracks()[0];
  const constraints = {
    deviceId: { exact: deviceId }
  };
  
  try {
    await videoTrack.applyConstraints(constraints);
    console.log('摄像头切换成功');
  } catch (err) {
    console.error('切换失败:', err);
  }
}

注意: 有些浏览器在切换摄像头时,如果新设备不支持当前分辨率,会抛出 OverconstrainedError。我建议在切换前先获取新设备的能力集(capabilities),再设置合适的约束。

3. 分辨率动态调整

分辨率调整这个功能,说白了就是根据网络状况动态升降级。我在做远程医疗项目时,医生端要求至少720p,但患者网络差的时候只能降到360p。

核心思路是:先获取当前设置,再修改约束。

// 获取当前视频轨道设置
function getCurrentResolution() {
  const videoTrack = localStream.getVideoTracks()[0];
  const settings = videoTrack.getSettings();
  console.log(`当前分辨率: ${settings.width}x${settings.height}`);
  return settings;
}

// 动态调整分辨率
async function setResolution(width, height) {
  const videoTrack = localStream.getVideoTracks()[0];
  const constraints = {
    width: { exact: width },
    height: { exact: height }
  };
  
  try {
    await videoTrack.applyConstraints(constraints);
    console.log(`分辨率已调整为 ${width}x${height}`);
  } catch (err) {
    // 如果不支持精确值,退而求其次
    const fallbackConstraints = {
      width: { ideal: width },
      height: { ideal: height }
    };
    await videoTrack.applyConstraints(fallbackConstraints);
  }
}
分辨率 宽高比 推荐场景
1280x720 16:9 高清会议、直播
640x480 4:3 普通视频通话
320x240 4:3 弱网环境

避坑指南: 我曾经在移动端遇到过设置 exact 约束导致黑屏的问题。后来发现是摄像头不支持那个精确分辨率。建议先用 getCapabilities() 查一下支持的范围,再用 ideal 而不是 exact

4. 视频特效与滤镜

滤镜这块,我见过两种主流做法:一种是直接用 CSS filter,简单粗暴;另一种是用 Canvas 做像素级处理,灵活但性能开销大。

我个人推荐先用 CSS filter 做基础滤镜,如果需求复杂再上 Canvas。

4.1 CSS filter 方式

// 给 video 元素加滤镜
function applyFilter(filterType) {
  const video = document.getElementById('localVideo');
  switch(filterType) {
    case 'grayscale':
      video.style.filter = 'grayscale(100%)';
      break;
    case 'sepia':
      video.style.filter = 'sepia(80%)';
      break;
    case 'blur':
      video.style.filter = 'blur(3px)';
      break;
    case 'brightness':
      video.style.filter = 'brightness(1.5)';
      break;
    default:
      video.style.filter = 'none';
  }
}

4.2 Canvas 像素处理方式

如果你需要更复杂的特效,比如人脸美颜、背景替换,那就得用 Canvas 了。基本流程是:把视频帧画到 Canvas 上,处理像素数据,再通过 canvas.captureStream() 生成新的 MediaStream。

// 创建 Canvas 处理视频帧
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
const video = document.getElementById('localVideo');

// 每帧处理
function processFrame() {
  canvas.width = video.videoWidth;
  canvas.height = video.videoHeight;
  
  // 绘制当前帧
  ctx.drawImage(video, 0, 0);
  
  // 获取像素数据
  const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
  
  // 这里可以做像素级处理,比如反色
  for (let i = 0; i < imageData.data.length; i += 4) {
    imageData.data[i] = 255 - imageData.data[i];     // R
    imageData.data[i+1] = 255 - imageData.data[i+1]; // G
    imageData.data[i+2] = 255 - imageData.data[i+2]; // B
  }
  
  // 放回处理后的数据
  ctx.putImageData(imageData, 0, 0);
  
  requestAnimationFrame(processFrame);
}

// 生成新的流用于传输
const filteredStream = canvas.captureStream(30); // 30fps

性能警告: Canvas 像素处理非常消耗 CPU。我在一个项目中做过实时美颜,结果低端手机直接卡死。建议只在桌面端或高端移动设备上使用,并且帧率控制在 15-20fps 即可。

5. 综合实战:一个完整的视频控制面板

最后,我把这些功能整合成一个控制面板。嗯,这里要注意,所有操作都要基于同一个 MediaStream,不要重复获取。

class VideoController {
  constructor(localStream) {
    this.stream = localStream;
    this.videoTrack = localStream.getVideoTracks()[0];
    this.currentFilter = 'none';
  }
  
  // 暂停/恢复
  togglePause() {
    this.videoTrack.enabled = !this.videoTrack.enabled;
    return this.videoTrack.enabled;
  }
  
  // 切换摄像头
  async switchCamera(deviceId) {
    await this.videoTrack.applyConstraints({
      deviceId: { exact: deviceId }
    });
  }
  
  // 调整分辨率
  async setResolution(width, height) {
    await this.videoTrack.applyConstraints({
      width: { ideal: width },
      height: { ideal: height }
    });
  }
  
  // 应用滤镜
  applyFilter(filterType, videoElement) {
    this.currentFilter = filterType;
    videoElement.style.filter = filterType === 'none' ? 'none' : filterType;
  }
}

// 使用示例
const controller = new VideoController(localStream);
controller.togglePause(); // 暂停
controller.setResolution(640, 480); // 降分辨率
controller.applyFilter('sepia(80%)', document.getElementById('localVideo'));

核心总结: 视频控制的关键在于操作 MediaStreamTrack 而不是整个 MediaStream。暂停用 enabled,切换用 applyConstraints,滤镜用 CSS 或 Canvas。记住这三点,大部分场景都能搞定。


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