web页面录制、回放以及生成视频

假如有这么一个需求:

  • 记录一个用户在页面上的所有操作,包括点击、滑动、表单填写等;
  • 可以回放用户的操作;
  • 对现有代码侵入影响小;

我们该如何去做呢?

方案一

  • 客户端截图并上传至服务端
  • 服务端图片合成视频
  • 回放端播放视频

试想一下,如果这么做会有什么问题?
首先服务端图片合成视频可以使用ffmpeg,生成mp4文件,回放端播放mp4文件就较简单了。

但是在客户端的问题就比较多了:

1. 怎么截图?

我们可以采用html2canvasrasterizeHTML

html2canvas的原理:

通过遍历DOM克隆一份副本,将此副本在Canvas上重新绘制,并根据DOM的样式应用在对应的绘制元素上,再通过Canvas生成图片。转换过程可理解成:DOMCanvasImage

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中

那么,我们再仔细想一下这么做可行吗?会有什么问题?

  1. 什么时候进行DOM的diff比较?
  2. 没有反映再DOM中的变化如何处理?比如input
  3. 相对路径要转化为绝对路径,不然回放加载不到资源
  4. css文件内联,为了防止同名文件后续内容修改导致的回放不准确,需要将css样式文件内联到文件里面

要进行DOM的比较,首先要将DOM进行序列化,转化成一个JSON对象,我们可以选择开源的库parse5进行转换,比如这个demo

再来说说何时进行DOM比较,理论上应该是只要页面有变动就该记录下来,但是如果我们的页面比较复杂,DOM对象的diff比较会花费很多的计算量,如果用户操作频繁,会造成用户操作界面的卡顿。
另外parse5这种库也没办法去记录input的值和相对路径的转换以及css的内联。所以这个方案也不太好直接用。

方案三

方案二的思路是可以的,但是因为diff等问题还是没办法解决,那么有啥办法获取DOM的变更记录吗?

很高兴现代浏览器给我们提供了强大的 API MutationObserver ,通过这一API我们可以实现对DOM树所做更改的监视。

通过一个demo我们可以简单看下mutationobserver使用方法。

总结一下引起页面变更的操作有下面几类:

  1. 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结构的字符串

回放端:

  1. 需要将JSON结构的变更重新构建为DOM并且添加到已有DOM上。
  2. 为了避免 js 执行导致的页面意外的变动,应该禁止 js 执行,所以可以将回放页面放在沙盒中并禁止 js 运行。
  3. 为了有更好的回放效果,需要支持倍速播放,任意时间点开始播放,在用户没有操作的时间段内加速播放等。

生成视频

回放端需要特制的播放器才能回放,有时候还需要联网,比如页面中的一些图片为外部链接之类的,外部图片是有可能被替换掉或者存在有效期问题,所以需要将操作及时录制成视频。

录制为视频可以用开源的库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。