web页面录制、回放以及生成视频
假如有这么一个需求:
- 记录一个用户在页面上的所有操作,包括点击、滑动、表单填写等;
- 可以回放用户的操作;
- 对现有代码侵入影响小;
我们该如何去做呢?
方案一
- 客户端截图并上传至服务端
- 服务端图片合成视频
- 回放端播放视频
试想一下,如果这么做会有什么问题?
首先服务端图片合成视频可以使用ffmpeg,生成mp4文件,回放端播放mp4文件就较简单了。
但是在客户端的问题就比较多了:
1. 怎么截图?
我们可以采用html2canvas或rasterizeHTML。
html2canvas的原理:
通过遍历DOM克隆一份副本,将此副本在Canvas上重新绘制,并根据DOM的样式应用在对应的绘制元素上,再通过Canvas生成图片。转换过程可理解成:DOM→Canvas→Image。
rasterizeHTML原理:
通过遍历DOM克隆一份副本,利用SVG的foreignObject把DOM作为外部资源嵌套在SVG中,将此SVG在Canvas上重新绘制,并根据DOM的样式应用在对应的绘制元素上,再通过Canvas生成图片。转换过程可理解成:DOM→SVG的ForeignObject→Canvas→Image。
两种截图方式最后都是通过把DOM绘制到Canvas,再通过Canvas输出图片。所以并不是直接屏幕截图,而是基于从 DOM 读取的属性构建页面的表示。因此,它只能正确地描述它所理解的属性,所以许多 CSS 属性可能不起作用。
另外两种方式都存在比较多的问题和限制:
Canvas截图的限制性
- 不支持跨域图片
- 不支持iframe
- 部分浏览器上不支持SVG图片
- 不支持Flash
SVG截图的限制性
- 不支持跨域图片
- 无法渲染如lazyload等通过JS加载的资源
- 无法渲染内联background-image或JS操作background-image
2. 频繁上传图片的问题?
每秒30帧,每张图100KB为例,那每秒的图片数据大约为3MB,一分钟就是180MB,这个数据传到服务器的话,对于流量和存储都是个大问题。
3.消耗性能、电量、流量
每秒需要渲染、生成30张图片,需要消耗大量的性能、电量(移动端尤为突出),有可能造成设备的卡顿,上传图片需要消耗大量的流量和电量。
方案二
客户端:
- 全量记录DOM快照
- 页面变动,根据diff算法,计算出变动的内容
- 将变动的节点内容上传到服务器
服务端:
- 服务端按照提交内容的时间顺序保存
回放端:
- 将变动的差异按时间顺序重新构建到DOM中
那么,我们再仔细想一下这么做可行吗?会有什么问题?
- 什么时候进行DOM的diff比较?
- 没有反映再DOM中的变化如何处理?比如input
- 相对路径要转化为绝对路径,不然回放加载不到资源
- css文件内联,为了防止同名文件后续内容修改导致的回放不准确,需要将css样式文件内联到文件里面
要进行DOM的比较,首先要将DOM进行序列化,转化成一个JSON对象,我们可以选择开源的库parse5进行转换,比如这个demo。
再来说说何时进行DOM比较,理论上应该是只要页面有变动就该记录下来,但是如果我们的页面比较复杂,DOM对象的diff比较会花费很多的计算量,如果用户操作频繁,会造成用户操作界面的卡顿。
另外parse5这种库也没办法去记录input的值和相对路径的转换以及css的内联。所以这个方案也不太好直接用。
方案三
方案二的思路是可以的,但是因为diff等问题还是没办法解决,那么有啥办法获取DOM的变更记录吗?
很高兴现代浏览器给我们提供了强大的 API MutationObserver ,通过这一API我们可以实现对DOM树所做更改的监视。
通过一个demo我们可以简单看下mutationobserver使用方法。
总结一下引起页面变更的操作有下面几类:
- DOM 变动
- 节点创建、销毁
- 节点属性变化
- ⽂本变化
2. ⿏标交互
- mouse up、mouse down
- click、double click、context menu
- focus、blur
- touch start、touch move、touch end
3. 页⾯或元素滚动
4. 视窗⼤⼩改变
5. 输⼊
- input
- textarea
- select
6. ⿏标移动
通过mutationobserver可以记录DOM变动,其他的需要用addEventListener来监听事件。
mutationobserver的变动需要将其转化为序列化的json结构,便于存储和重新构建DOM结构。
mutation变动类型与json对照列表
那么我们就可以先全量获取一次页面的DOM快照,然后再依次增量记录页面的DOM变更和各种事件。
服务端:
服务端只需要记录JSON结构的字符串
回放端:
- 需要将JSON结构的变更重新构建为DOM并且添加到已有DOM上。
- 为了避免 js 执行导致的页面意外的变动,应该禁止 js 执行,所以可以将回放页面放在沙盒中并禁止 js 运行。
- 为了有更好的回放效果,需要支持倍速播放,任意时间点开始播放,在用户没有操作的时间段内加速播放等。
生成视频
回放端需要特制的播放器才能回放,有时候还需要联网,比如页面中的一些图片为外部链接之类的,外部图片是有可能被替换掉或者存在有效期问题,所以需要将操作及时录制成视频。
录制为视频可以用开源的库timecut在服务端进行录制。
原理是利用 puppeteer 来回放页面的操作,在回放的时候使用puppeteer不断的截图,比如每秒截图60张(此操作会占用磁盘较高的IO),最后,使用ffmpeg将图片转化为视频文件。
需要注意的是,这个生成过程会比较占用时间和对机器性能有要求,如果机器比较卡顿,生成的视频文件也会画面模糊或者卡顿等。
demo
在线体验 demo
源码:puzzle-game
生成视频demo(因涉及公司信息放在公司内网)
最终方案
在经历了一系列的探索之后,发现万能的github上已经有了整套的解决方案rrweb。
rrweb 主要由 3 部分组成:
- rrweb-snapshot,包含 snapshot 和 rebuild 两个功能。snapshot 用于将 DOM 及其状态转化为可序列化的数据结构并添加唯一标识;rebuild 则是将 snapshot 记录的数据结构重建为对应的 DOM。
- rrweb,包含 record 和 replay 两个功能。record 用于记录 DOM 中的所有变更(mutation);replay 则是将记录的变更按照对应的时间一一重放。
- rrweb-player,为 rrweb 提供一套 UI 控件,提供基于 GUI 的暂停、快进、拖拽至任意时间点播放等功能。
但是开源的库会存在一些bug 或者不能满足一些特殊的需求,比如:不支持iframe,
所以我基于这些库(0.9.1)进行了二次开发
rrweb-snapshot
修改:支持iframe
rrweb
修改:解决速度修改无效的问题
官方最新版已修复
rrweb-player
进行回放组件的UI定制
使用文档可以参考 https://github.com/rrweb-io/rrweb/blob/master/guide.zh_CN.md
TODO:
增加可选参数,使图片链接变为base64。关联issue
另外需要注意的是, 在提交操作事件到后台的时候,需要特殊处理下事件还未提交就关闭页面的情况,可以使用navigator.sendBeacon,注意sendBeacon对提交数据量是有限制的,一般为64KB。