视频控制:暂停、切换、分辨率与滤镜
视频控制这块,说实话是WebRTC应用里最常被用户吐槽的地方。用户不会管你底层用了什么编解码器,他们只关心:能不能暂停?能不能换摄像头?画面清不清晰?能不能加个美颜?
我做过好几个视频会议项目,每次上线前,产品经理都会追着问这些功能。今天我就把这几块核心能力拆开来讲,每个点都带实战代码。
核心知识点一览:
- 视频流的暂停与恢复(Track.enabled)
- 摄像头切换(enumerateDevices + applyConstraints)
- 分辨率动态调整(getSettings + setConstraints)
- 视频特效与滤镜(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('视频已恢复');
}
小技巧: 用 enabled 比 stop() 好在哪里?我遇到过用户频繁开关摄像头导致浏览器崩溃的情况。用 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