在利用WebRTC进行拍照之前,先来学习两个概念:非编码帧、编码帧。
1. 非编码帧
播放音频文件的时候,播放的其实是一幅幅图像数据,在播放器播放某个音频文件的时候,会按照一定的时间间隔从视频文件中读取解码后的视频帧,这样视频就动了起来。播放从摄像头中获取的视频帧也是如此,只不过从摄像头中获取到的本来就是非编码帧,无需解码。
从摄像头中采集到的非编码帧,非编码帧的格式一般是YUV或者RGB的格式。
2. 编码帧
相比于非编码帧,经过编码器(H264/H265、VP8/VP9)压缩之后的帧称为是编码帧,以H264为例,经过H264编码的帧包括下面三种类型:
关于I帧、P帧、B帧
以H264视频压缩标准为例
我们在传输视频数据的每一帧数据的时候,发现单纯的传输视频图像,视频帧的数据量是非常大的,在以太网中单个数据包的大小是1.5k,那么为了完整的传输一个图片帧可能需要几十个数据包,对于现有的网络是无法接受的。在视频传输和存储的过程中,人们发现视频帧之间存在大量的重复数据,如果将这些重复数据剔除,在接收端再进行恢复,这样就可以大大减少网络带宽的压力,这就是H264视频压缩标准。
编码器将多张图片帧编码成一组GOP(Group Of Picture),这组GOP数据是一组连续的画面,在这组GOP数据中,第一帧是I帧和其他多个P/B帧组成。I帧称为是关键帧,I帧的压缩率低,可以解码出一张完整的图像帧,P帧是前向预测帧,B帧是双向内插帧,I帧是一副完整的图像帧,而P/B帧记录的是在I帧的基础上视频流数据发生的变化,如果没有I帧,P/B帧无法解码的,而无P/B帧,I帧就是一个静态的画面。编码器在进行编码的时候,会比较前后两个视频帧的变化率,要是变化率达到了一定程度(比如前后两幅24位真彩图中有70%的数据发生了改变),那么就会从后一帧开始重新划分一个GOP。
播放器播放的视频帧是非编码帧,我们拍照的过程其实就是从连续播放的一幅幅非编码帧中抽取一张正在播放的帧。
使用WebRTC进行拍照,并添加滤镜:
<template>
<div class="panel">
<video class="small_panel" autoplay playsinline></video>
</div>
<div class="operate">
<div><button @click="handleTakePhoto">Take</button></div>
<canvas id="picture" :class="filterSelectClazz"></canvas>
<div><button @click="handleSavePhoto">Save</button></div>
</div>
<div class="panel">
<select @change="handleSelectFilter">
<option value="none">None</option>
<option value="blur">blur</option>
<option value="grayscale">Grayscale</option>
<option value="invert">Invert</option>
<option value="sepia">sepia</option>
</select>
</div>
</template>
<script>
import { defineComponent, onMounted, ref } from "vue";
export default defineComponent({
name: "TestWebRTC",
setup () {
// 采集视频数据
const getUserMedia = () => {
const mediaStreamConstrains = {
video: true,
};
const localVideo = document.querySelector('video');
navigator.mediaDevices.getUserMedia(mediaStreamConstrains).then((mediaStream) => {
localVideo.srcObject = mediaStream;
// 将mediaStream挂载到window上
window.mediaStream = mediaStream;
}).catch((error) => {
console.log('[Error]getUSerMedia:', error);
});
};
// 从video的视频流中提取图片
const handleTakePhoto = () => {
const picture = document.querySelector('canvas#picture');
picture.width = 200;
picture.height = 200;
const videoPlayer = document.querySelector('video');
picture.getContext('2d').drawImage(videoPlayer, 0, 0, picture.width, picture.height);
};
// 保存图片
const handleSavePhoto = () => {
const aTag = document.createElement('a');
aTag.download = 'phote_' + new Date().getTime();
const canvas = document.querySelector('canvas#picture');
aTag.href = canvas.toDataURL("image/png");
document.body.appendChild(aTag);
aTag.click();
aTag.remove();
};
const filterSelectClazz = ref("");
// 设置图片滤镜
const handleSelectFilter = (e) => {
if (e.target.value) {
filterSelectClazz.value = e.target.value;
} else {
filterSelectClazz.value = '';
}
};
onMounted(() => {
getUserMedia();
});
return {
handleTakePhoto,
handleSavePhoto,
filterSelectClazz,
handleSelectFilter,
};
}
})
</script>
<style scoped>
.panel { text-align: center; margin-top: 20px; }
.small_panel { width: 160px; height: 160px; }
.none {
-webkit-filter: none;
}
.blur {
-webkit-filter: blur(3px);
}
.grayscale {
-webkit-filter: grayscale(1);
}
.invert {
-webkit-filter: invert(1);
}
.sepia {
-webkit-filter: sepia(1);
}
</style>