问题背景
近期实验室项目要求需要前端对我们构建的地理场景MapBox进行录制,因为使用我们编辑器构建的地理场景可以在系统内部进行审阅、浏览、查看等,但依旧需要解决一个脱离系统本身的产物(录制视频),为避免需要用户去使用自身的录制软件(ev录屏、Bandicam等)进行效果的展示,需要思考一种前端的解决方案去实现录制。
技术探索
1. 浏览器WebRTC录制
由我们使用的录屏软件进一步思考,我们最先想到的还是浏览器能否支持一种方案直接帮我们把前端操作进行一个录制呢。目前浏览器本身提供了一套前端录制的解决方案,WebRTC(Web Real-Time Communications),在我们的录屏使用场景主要关注以下几个 API:
- getDisplayMedia() - 提示用户给予使用媒体输入的许可从而获取屏幕的流;
- MediaRecorder() - 生成对指定的媒体流进行录制的 MediaRecorder 对象;
- ondataavailable - 当 MediaRecorder 将媒体数据传递到应用程序以供使用时将触发该事件;
<template>
<button @click="startRecording">开启录制</button>
<button @click="stopRecording">结束录制</button>
<button @click="handleDownload">下载视频</button>
</template>
<script lang="ts" setup>
import { reactive } from 'vue';
const state = reactive({
mediaRecorder: null as MediaRecorder | null,
recordedChunks: [] as Blob[],
videoBlob: null as Blob | null,
isRecording: false
});
// 开始录制
const startRecording = async () => {
try {
const stream = await navigator.mediaDevices.getDisplayMedia({
video: { displaySurface: 'monitor' },
audio: true
});
state.mediaRecorder = new MediaRecorder(stream, { mimeType: 'video/webm' });
state.recordedChunks = [];
state.mediaRecorder.ondataavailable = (event) => {
if (event.data.size > 0) {
state.recordedChunks.push(event.data);
}
};
state.mediaRecorder.onstop = () => {
// 完成录制,生成完整的 Blob
state.videoBlob = new Blob(state.recordedChunks, { type: 'video/webm' });
state.isRecording = false;
};
state.mediaRecorder.start();
state.isRecording = true;
// 自动停止录制(例如,10分钟后)
setTimeout(() => {
if (state.isRecording) {
stopRecording();
}
}, 10 * 60 * 1000); // 10分钟
} catch (error) {
console.error('Error starting recording:', error);
}
};
// 结束录制
const stopRecording = () => {
if (state.mediaRecorder && state.isRecording) {
state.mediaRecorder.stop();
state.mediaRecorder.stream.getTracks().forEach((track) => track.stop());
}
};
// 下载录制的视频
const handleDownload = () => {
if (!state.videoBlob) return;
const url = URL.createObjectURL(state.videoBlob);
const a = document.createElement('a');
a.href = url;
a.download = 'video.webm';
a.click();
};
</script>
常用的录屏软件SCRE.IO的录制方案就是基于这个的,但是在使用过程中也发现了很多小问题:
- 用户感知并录制: 开始录制行为时浏览器会弹出对话框询问选择录制的屏幕和页面,是否对声音也进行录制,这种提示用户去授权完成的录制的方式其实是不符合我们大部分人对系统内部录制的一个预期的,我们希望针对与这个场景的录制方案是一个无感的不需要用户去选择的;
- 录制页面局限性: 因为我们无法对录制内容、录制的屏幕声音进行控制,所以可能对地理场景外部的内容也进行录制,这是我们所不期望的;
- WebRTC兼容性: WebRTC API针对不同浏览器,他的兼容性也各不相同API兼容性查询
2. html2canvas 截图合成
接下来我们换一种思路,依据我们对地理场景项目的封面制作方案(ps:利用前端截图插件对场景内部进行截图),视频可以看作为一帧一帧的图片,将截的所有图片按顺序以截取时候的速度进行合成就实现了地理场景的录制了。
我们采用html2canvas对场景进行“录制”。
<template>
<button @click="handleStart">开启录制</button>
<button @click="handleStop">停止录制</button>
<button @click="handleReplay">播放录制</button>
<img :src="state.imgs[state.num ?? 0]" />
</template>
<script lang="ts" setup>
import { reactive } from 'vue';
import html2canvas from 'html2canvas';
const state = reactive({
visible: false,
imgs: [] as string[],
num: 0,
recordInterval: null as any,
replayInterval: null as any,
});
const FPS = 60;
const interval = 1000 / FPS;
const handleStart = async () => {
handleReset();
state.recordInterval = setInterval(() => {
if (state.imgs.length > 100) {
handleStop();
return;
}
html2canvas(document.body).then((canvas: any) => {
const img = canvas.toDataURL();
state.imgs.push(img);
});
}, interval);
};
const handleStop = () => {
state.recordInterval && clearInterval(state.recordInterval);
};
const handleReplay = async () => {
state.recordInterval && clearInterval(state.recordInterval);
state.num = 0;
state.visible = true;
state.replayInterval = setInterval(() => {
if (state.num >= state.imgs.length - 1) {
clearInterval(state.replayInterval);
return;
}
state.num++;
}, interval);
};
const handleReset = () => {
state.imgs = [];
state.recordInterval = null;
state.replayInterval = null;
state.num = 0;
};
</script>
注意:
Q:![]()
A:Mapbox
无法html2canvas
导出解决:# 用 html2canvas 导出 mapbox 失败的排查过程
- 局限性: html2canvas有很多不支持的
CSS
样式,导致很多样式错误等; - 性能消耗: 若想使得此方案录制的视频效果好,我们不得不采取大量的连续的截图方案来保证,但是如此截图过程中的掉帧严重,而且录制的体积暴涨,最后“录制”的结果并不友好;
- 忽略元素: 我们页面中除去的场景画布之外的其他元素
data-html2canvas-ignore
。
3. rrweb 还原现场
rrweb
全称 'record and replay the web'
,是当下很流行的一个录制屏幕的开源库。与我们传统认知的录屏方式(如 WebRTC
)不同的是,rrweb
录制的不是真正的视频流,而是一个记录页面 DOM
变化的 JSON
数组,因此不能录制整个显示器的屏幕,只能录制浏览器的一个页签。rrweb 主要包括以下三个组成部分:
- rrweb-snapshot,包含 snapshot 和 rebuild 两部分,snapshot 用于将 DOM 及其状态转化为可序列化的数据结构并添加唯一标识,rebuild 是将 snapshot 记录的数据结构重建为对应 DOM。
- rrweb,包含 record 和 replay 两个功能,record 用于记录 DOM 中的所有变更,replay 则是将记录的变更按照对应的时间一一重放。
- rrweb-player,为 rrweb 提供一套 UI 控件,提供基于 GUI 的暂停、快进、拖拽至任意时间点播放等功能。
<template>
<button @click="handleReplay">播放录制</button>
<div ref="replayRef"></div>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import * as rrweb from 'rrweb';
import rrwebPlayer from 'rrweb-player';
import 'rrweb-player/dist/style.css';
const replayRef = ref();
const events = ref<any[]>([]);
// 开始记录
const stopRecording = rrweb.record({
emit(event) {
events.value.push(event);
}
});
// 停止记录(可以在需要的时候调用)
setTimeout(() => {
(stopRecording as any)();
}, 5000); // 停止记录 5 秒后
const handleReplay = () => {
new rrwebPlayer({
target: replayRef.value,
props: {
events: events.value
}
});
};
</script>
结合上述代码运行的效果可以看出,rrweb
录制内容存储了完整的页面结构可以较好的还原整个页面的dom
操作,并且录制过程具有较好的清晰度,也不会和html2canvas
一样过分掉帧,具有较好的性能;并且rrweb
的可操作性和可扩展性要更优秀,可将录制得到的json
产物转换成视频、并支持多哥可扩展式插件等;可以看到上图左下角只显示了Mapbox
的logo
并没有记录地图瓦片,是因为Mapbox
底层依靠WebGL
进行地图渲染,而目前rrweb
对WebGL
的部分功能无法进行还原..
4. 方案对比
所选方案 | WebRTC | html2canvas | rrweb |
---|---|---|---|
是否有感 | 有感 | 无感 | 有感 |
产物大小 | 大 | 大 | 相对较小 |
兼容性 | 浏览器兼容性 | 部分内容无法记录 | 相对友好 |
地图Mapbox | ✅ | ✅ | ❌ |
注意: 尽管在各方性能对比上
rrweb
在各方面均比较优秀,但我们针对Mapbox
开发的地理场景无法通过rrweb
进行记录,最后不得不选择利用WebRTC API
进录制,再利用FFmpeg
进行数据格式的转化并导出。
Whether rrweb can record WebGL content in canvas or not
😭😭😭
总结
浅浅总结一下,虽然针对于这三个录制的方式,总体而言,还是rrweb
为代表的dom
快照方式是最优秀的解决方案,但我们针对地理场景的录制可能依旧要使用视频录制的方式;一点点探索,也让我了解了很对技术。
文章如有不对或者优化的地方,欢迎大家在评论区留言!
参考资料
还原现场 🔍 前端录制用户行为技术方案
rrweb 实现原理介绍
WebRTC API - MDN Web Docs - Mozilla
rrweb 带你还原问题现场
记一次PC页面录屏+转码+导出MP4
文章荐读: